diff --git a/packages/frontend/editor-ui/src/plugins/codemirror/completions/datatype.completions.ts b/packages/frontend/editor-ui/src/plugins/codemirror/completions/datatype.completions.ts index 7543562163b..d9e289c0056 100644 --- a/packages/frontend/editor-ui/src/plugins/codemirror/completions/datatype.completions.ts +++ b/packages/frontend/editor-ui/src/plugins/codemirror/completions/datatype.completions.ts @@ -1151,8 +1151,9 @@ export const secretOptions = (base: string) => { if (typeof resolved !== 'object') { return []; } - return Object.entries(resolved).map(([secret, value]) => - createCompletionOption({ + return Object.entries(resolved).map(([secret, value]) => { + const needsBracketAccess = /\//.test(secret); + const option = createCompletionOption({ name: secret, doc: { name: secret, @@ -1160,8 +1161,15 @@ export const secretOptions = (base: string) => { description: i18n.baseText('codeNodeEditor.completer.$secrets.provider.varName'), docURL: i18n.baseText('settings.externalSecrets.docs'), }, - }), - ); + }); + + // Override the apply handler for keys that need bracket access + if (needsBracketAccess) { + option.apply = applyBracketAccessCompletion; + } + + return option; + }); } catch { return []; } diff --git a/packages/frontend/editor-ui/src/plugins/codemirror/completions/utils.test.ts b/packages/frontend/editor-ui/src/plugins/codemirror/completions/utils.test.ts index 0434130e71a..983ed6f0d7b 100644 --- a/packages/frontend/editor-ui/src/plugins/codemirror/completions/utils.test.ts +++ b/packages/frontend/editor-ui/src/plugins/codemirror/completions/utils.test.ts @@ -6,7 +6,12 @@ import { EditorState } from '@codemirror/state'; import { EditorView } from '@codemirror/view'; import { NodeConnectionTypes, type IConnections } from 'n8n-workflow'; import type { MockInstance } from 'vitest'; -import { autocompletableNodeNames, expressionWithFirstItem, stripExcessParens } from './utils'; +import { + autocompletableNodeNames, + expressionWithFirstItem, + stripExcessParens, + isAllowedInDotNotation, +} from './utils'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { mockedStore } from '@/__tests__/utils'; import { createTestingPinia } from '@pinia/testing'; @@ -183,4 +188,35 @@ describe('completion utils', () => { expect(view.state.doc.toString()).toEqual(expected); }); }); + + describe('isAllowedInDotNotation', () => { + it('should return false for keys with forward slashes', () => { + expect( + isAllowedInDotNotation( + 'applications/n8n/available-to-users/google-cloud-geocoding-api-key', + ), + ).toBe(false); + expect(isAllowedInDotNotation('path/to/secret')).toBe(false); + expect(isAllowedInDotNotation('secret/with/slashes')).toBe(false); + }); + + it('should return false for keys with other special characters', () => { + expect(isAllowedInDotNotation('key with spaces')).toBe(false); + expect(isAllowedInDotNotation('key-with-hyphens')).toBe(false); + expect(isAllowedInDotNotation('key.with.dots')).toBe(false); + expect(isAllowedInDotNotation('key[with]brackets')).toBe(false); + }); + + it('should return true for valid JavaScript identifiers', () => { + expect(isAllowedInDotNotation('validKey')).toBe(true); + expect(isAllowedInDotNotation('valid_key')).toBe(true); + expect(isAllowedInDotNotation('validKey123')).toBe(true); + expect(isAllowedInDotNotation('_validKey')).toBe(true); + }); + + it('should return false for keys starting with numbers', () => { + expect(isAllowedInDotNotation('123key')).toBe(false); + expect(isAllowedInDotNotation('0invalid')).toBe(false); + }); + }); }); diff --git a/packages/frontend/editor-ui/src/plugins/codemirror/completions/utils.ts b/packages/frontend/editor-ui/src/plugins/codemirror/completions/utils.ts index 35c1c86a64e..43e7194714d 100644 --- a/packages/frontend/editor-ui/src/plugins/codemirror/completions/utils.ts +++ b/packages/frontend/editor-ui/src/plugins/codemirror/completions/utils.ts @@ -136,7 +136,7 @@ export const isPseudoParam = (candidate: string) => { * Whether a string may be used as a key in object dot access notation. */ export const isAllowedInDotNotation = (str: string) => { - const DOT_NOTATION_BANNED_CHARS = /^(\d)|[\\ `!@#$%^&*()+\-=[\]{};':"\\|,.<>?~]/g; + const DOT_NOTATION_BANNED_CHARS = /^(\d)|[\\ `!@#$%^&*()+\-=[\]{};':"\\|,.<>?~\/]/g; return !DOT_NOTATION_BANNED_CHARS.test(str); };