From 1d547740edbe874556b9b08cd067ca6482c3f9e6 Mon Sep 17 00:00:00 2001 From: Kent Wang Date: Fri, 14 Aug 2026 16:03:18 +0800 Subject: [PATCH 1/2] fix copy paste issue --- packages/insomnia/src/common/constants.ts | 1 + .../common/templating/__tests__/utils.test.ts | 67 +++++++++++++++++++ .../insomnia/src/common/templating/utils.ts | 15 +++++ .../.client/codemirror/code-editor.tsx | 21 ++++-- .../.client/codemirror/one-line-editor.tsx | 14 +++- .../ui/components/templating/tag-editor.tsx | 3 +- 6 files changed, 111 insertions(+), 10 deletions(-) diff --git a/packages/insomnia/src/common/constants.ts b/packages/insomnia/src/common/constants.ts index 9564f74b5bf..7ba7c916629 100644 --- a/packages/insomnia/src/common/constants.ts +++ b/packages/insomnia/src/common/constants.ts @@ -347,6 +347,7 @@ export const sortOrderName: Record = { }; export const EXTERNAL_VAULT_PLUGIN_NAME = '@kong/insomnia-plugin-external-vault'; +export const EXTERNAL_VAULT_TAG_ID_PREFIX = 'externalVaultTag'; export const AI_PLUGIN_NAME = '@kong/insomnia-plugin-ai'; export type DashboardSortOrder = 'name-asc' | 'name-desc' | 'created-asc' | 'created-desc' | 'modified-desc'; diff --git a/packages/insomnia/src/common/templating/__tests__/utils.test.ts b/packages/insomnia/src/common/templating/__tests__/utils.test.ts index 16c3ae2f553..fd2737ff9ee 100644 --- a/packages/insomnia/src/common/templating/__tests__/utils.test.ts +++ b/packages/insomnia/src/common/templating/__tests__/utils.test.ts @@ -332,6 +332,73 @@ describe('decodeEncoding()', () => { }); }); +describe('replaceVaultTagIdIfNeeded()', () => { + const vaultTagIdRegex = new RegExp(`^${utils.externalVaultTagPrefix}_\\w+$`); + const normalTemplate = + "{% vault 'aws', 'externalVaultTag_0b1ea47e317f4f03ae147dcb6c5f66c6', 'eyJTZWNyZXRJZCI6IjEifQ==' %}"; + + it('leaves plain text without template tag symbols untouched', () => { + const template = 'normal string'; + expect(utils.replaceVaultTagIdIfNeeded(template)).toBe(template); + }); + + it('leaves multiple line of input unchanged', () => { + const input = 'line 1\nline 2\nline 3'; + expect(utils.replaceVaultTagIdIfNeeded(input)).toBe(input); + }); + + it('leaves a template tag with no vault tag id untouched', () => { + const template = "{% response 'body', 'req_123', 'b64', '' %}"; + expect(utils.replaceVaultTagIdIfNeeded(template)).toBe(template); + }); + + it('replaces the vault tag id inside a template tag with a freshly generated one', () => { + const result = utils.replaceVaultTagIdIfNeeded(normalTemplate); + + expect(result).not.toBe(normalTemplate); + expect(result).not.toContain('externalVaultTag_0b1ea47e317f4f03ae147dcb6c5f66c6'); + + const [, newId] = result.match(/'(externalVaultTag_\w+)'/) || []; + expect(newId).toMatch(vaultTagIdRegex); + }); + + it('generates a different id on each call', () => { + const first = utils.replaceVaultTagIdIfNeeded(normalTemplate); + const second = utils.replaceVaultTagIdIfNeeded(normalTemplate); + expect(first).not.toBe(second); + }); + + it('does not replace a vault tag id-like string outside of a {% %} block', () => { + const idLikeString = 'externalVaultTag_0b1ea47e317f4f03ae147dcb6c5f66c6'; + const template = `see ${idLikeString} for reference ${normalTemplate}`; + + const result = utils.replaceVaultTagIdIfNeeded(template); + + // the id-like text outside the tag is left alone + expect(result).toContain(idLikeString); + // the id inside the {% %} tag is still replaced + expect(result).not.toContain(normalTemplate); + }); + + it('replaces each vault tag id with a distinct new id when the template contains multiple tags', () => { + const template = + 'templateStart' + + "{% vault 'aws', 'externalVaultTag_0b1ef47e317f4f03ae147dcb6c5f66c6', 'eyJTZWNyZXRJZCI6IjEifQ==' %}" + + "{% vault 'aws', 'externalVaultTag_1b1ef47e317f4f03ae147dcb6c5f66c6', 'eyJTZWNyZXRJZCI6IjEifQ==' %}" + + "{% vault 'aws', 'externalVaultTag_2b1ef47e317f4f03ae147dcb6c5f66c6', 'eyJTZWNyZXRJZCI6IjEifQ==' %}" + + 'templateEnd'; + + const result = utils.replaceVaultTagIdIfNeeded(template); + + const newIds = result.match(new RegExp(`${utils.externalVaultTagPrefix}_\\w+`, 'g')) || []; + expect(newIds).toHaveLength(3); + expect(new Set(newIds).size).toBe(3); + newIds.forEach(id => expect(id).toMatch(vaultTagIdRegex)); + expect(result.startsWith('templateStart')).toBe(true); + expect(result.endsWith('templateEnd')).toBe(true); + }); +}); + describe('extractUndefinedVariableKey()', () => { it('extract nunjucks variable key', () => { expect(extractUndefinedVariableKey('{{name}}', {})).toEqual(['name']); diff --git a/packages/insomnia/src/common/templating/utils.ts b/packages/insomnia/src/common/templating/utils.ts index da0a65df999..50f06c5ef07 100644 --- a/packages/insomnia/src/common/templating/utils.ts +++ b/packages/insomnia/src/common/templating/utils.ts @@ -1,10 +1,12 @@ import type { EditorFromTextArea, MarkerRange } from 'codemirror'; +import { generateId } from '~/common/misc'; import type { NunjucksParsedTag, NunjucksParsedTagArg } from '~/common/templating/types'; import { base64ToUtf8, utf8ToBase64 } from '~/common/utils/utf8-bytes'; import { tokenizeArgs } from './tokenize-args'; export { tokenizeArgs }; + import objectPath from './third_party/object-path'; /** @@ -163,3 +165,16 @@ export function extractNunjucksTagFromCoords( } export const responseTagRegex = new RegExp('{% *response *.* %}'); + +// Unique id prefix for external vault tags +export const externalVaultTagPrefix = 'externalVaultTag'; +export const generateExternalVaultTagId = () => generateId(externalVaultTagPrefix); +const tagRegex = /{%[\s\S]+?%}/g; +const vaultTagIdRegex = new RegExp(`${externalVaultTagPrefix}_\\w+`, 'g'); +export const containExternalVaultTag = (input: string) => { + const hasTemplateTagSymbols = input.includes('{%') && input.includes('%}'); + return hasTemplateTagSymbols && input.includes(externalVaultTagPrefix); +}; +// Replace the unique id in vault tag with a new unique id to avoid duplicates when pasting +export const replaceVaultTagIdIfNeeded = (input: string) => + input.replace(tagRegex, tag => tag.replace(vaultTagIdRegex, () => generateExternalVaultTagId())); diff --git a/packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx b/packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx index 95697e48fa1..e665031b618 100644 --- a/packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx +++ b/packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx @@ -22,7 +22,11 @@ import vkBeautify from 'vkbeautify'; import { DEBOUNCE_MILLIS } from '~/common/constants'; import * as misc from '~/common/misc'; import { type NunjucksParsedTag, type nunjucksTagContextMenuOptions } from '~/common/templating/types'; -import { extractNunjucksTagFromCoords } from '~/common/templating/utils'; +import { + containExternalVaultTag, + extractNunjucksTagFromCoords, + replaceVaultTagIdIfNeeded, +} from '~/common/templating/utils'; import { useRootLoaderData } from '~/root'; import { AnalyticsEvent, trackOnceDaily } from '~/ui/analytics'; import { Icon } from '~/ui/components/icon'; @@ -489,10 +493,17 @@ export const CodeEditor = memo( doc.scrollTo(0, scrollPosition); } - if (onPaste && change.origin === 'paste' && change.update) { - const translatedText = onPaste(change.text.join('\n')).split('\n'); - - change.update(change.from, change.to, translatedText); + if (change.origin === 'paste' && change.update) { + let translatedText = change.text.join('\n'); + if (onPaste) { + translatedText = onPaste(translatedText); + } + if (containExternalVaultTag(translatedText)) { + translatedText = replaceVaultTagIdIfNeeded(translatedText); + } + if (translatedText !== change.text.join('\n')) { + change.update(change.from, change.to, translatedText.split('\n')); + } } }); diff --git a/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx b/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx index f7aad16ccfa..699c9755339 100644 --- a/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx +++ b/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx @@ -19,7 +19,11 @@ import * as reactUse from 'react-use'; import { DEBOUNCE_MILLIS } from '~/common/constants'; import * as misc from '~/common/misc'; import { type NunjucksParsedTag, type nunjucksTagContextMenuOptions } from '~/common/templating/types'; -import { extractNunjucksTagFromCoords } from '~/common/templating/utils'; +import { + containExternalVaultTag, + extractNunjucksTagFromCoords, + replaceVaultTagIdIfNeeded, +} from '~/common/templating/utils'; import { isCurlCommand } from '~/common/utils/curl'; import { useRootLoaderData } from '~/root'; import { showModal } from '~/ui/components/modals'; @@ -200,7 +204,7 @@ export const OneLineEditor = forwardRef }; codeMirror.current = CodeMirror.fromTextArea(textAreaRef.current, initialOptions); codeMirror.current.on('beforeChange', (_: CodeMirror.Editor, change: CodeMirror.EditorChangeCancellable) => { - const isPaste = change.text && change.text.length > 1; + const isPaste = change.origin === 'paste' && change.update; if (isPaste) { const pastedText = change.text.join('\n'); const hasContent = pastedText.trim(); @@ -208,8 +212,12 @@ export const OneLineEditor = forwardRef change.cancel(); return; } + let editorPasteText = change.text.join('').replace(/\n/g, ' '); + if (containExternalVaultTag(editorPasteText)) { + editorPasteText = replaceVaultTagIdIfNeeded(editorPasteText); + } // If we're in single-line mode, merge all changed lines into one - change.update?.(change.from, change.to, [change.text.join('').replace(/\n/g, ' ')]); + change.update?.(change.from, change.to, [editorPasteText]); } }); codeMirror.current.on('paste', (_, e: ClipboardEvent) => { diff --git a/packages/insomnia/src/ui/components/templating/tag-editor.tsx b/packages/insomnia/src/ui/components/templating/tag-editor.tsx index 6d9b4afb9ff..f16c1a101f2 100644 --- a/packages/insomnia/src/ui/components/templating/tag-editor.tsx +++ b/packages/insomnia/src/ui/components/templating/tag-editor.tsx @@ -7,7 +7,6 @@ import { Button, Link } from 'react-aria-components'; import * as reactUse from 'react-use'; import { getAppBundlePlugins } from '~/common/constants'; -import { generateId } from '~/common/misc'; import { localTemplateTags } from '~/common/templating/local-template-tags'; import type { NunjucksParsedTag, NunjucksParsedTagArg } from '~/common/templating/types'; import * as templateUtils from '~/common/templating/utils'; @@ -193,7 +192,7 @@ export const TagEditor: FC = props => { // Generate a unique tag id for the vault and link it to the credential in pluginData. // Update the tag arg to store the unique id instead of the raw (per-user) credential id. function convertLegacyCredentialTag(legacyCredentialId: string, argIndex: number) { - const tagUniqueId = generateId('externalVaultTag'); + const tagUniqueId = templateUtils.generateExternalVaultTagId(); updateArg(tagUniqueId, argIndex); return services.pluginData .upsertByKey(vaultPluginName, tagUniqueId, legacyCredentialId) From dbb5569ff769a118ab3c36cbf2d617493b20c4f1 Mon Sep 17 00:00:00 2001 From: Kent Wang Date: Fri, 14 Aug 2026 17:33:35 +0800 Subject: [PATCH 2/2] fix issues from comment --- packages/insomnia/src/common/constants.ts | 1 - .../insomnia/src/common/templating/__tests__/utils.test.ts | 5 ++--- packages/insomnia/src/common/templating/utils.ts | 6 +++--- .../src/ui/components/.client/codemirror/code-editor.tsx | 4 ++-- .../ui/components/.client/codemirror/one-line-editor.tsx | 4 ++-- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/insomnia/src/common/constants.ts b/packages/insomnia/src/common/constants.ts index 7ba7c916629..9564f74b5bf 100644 --- a/packages/insomnia/src/common/constants.ts +++ b/packages/insomnia/src/common/constants.ts @@ -347,7 +347,6 @@ export const sortOrderName: Record = { }; export const EXTERNAL_VAULT_PLUGIN_NAME = '@kong/insomnia-plugin-external-vault'; -export const EXTERNAL_VAULT_TAG_ID_PREFIX = 'externalVaultTag'; export const AI_PLUGIN_NAME = '@kong/insomnia-plugin-ai'; export type DashboardSortOrder = 'name-asc' | 'name-desc' | 'created-asc' | 'created-desc' | 'modified-desc'; diff --git a/packages/insomnia/src/common/templating/__tests__/utils.test.ts b/packages/insomnia/src/common/templating/__tests__/utils.test.ts index fd2737ff9ee..525d7a576af 100644 --- a/packages/insomnia/src/common/templating/__tests__/utils.test.ts +++ b/packages/insomnia/src/common/templating/__tests__/utils.test.ts @@ -333,9 +333,8 @@ describe('decodeEncoding()', () => { }); describe('replaceVaultTagIdIfNeeded()', () => { - const vaultTagIdRegex = new RegExp(`^${utils.externalVaultTagPrefix}_\\w+$`); - const normalTemplate = - "{% vault 'aws', 'externalVaultTag_0b1ea47e317f4f03ae147dcb6c5f66c6', 'eyJTZWNyZXRJZCI6IjEifQ==' %}"; + const vaultTagIdRegex = utils.vaultTagIdRegex; + const normalTemplate = `{% vault 'aws', '${utils.generateExternalVaultTagId()}', 'eyJTZWNyZXRJZCI6IjEifQ==' %}`; it('leaves plain text without template tag symbols untouched', () => { const template = 'normal string'; diff --git a/packages/insomnia/src/common/templating/utils.ts b/packages/insomnia/src/common/templating/utils.ts index 50f06c5ef07..f6cca8d99f3 100644 --- a/packages/insomnia/src/common/templating/utils.ts +++ b/packages/insomnia/src/common/templating/utils.ts @@ -170,9 +170,9 @@ export const responseTagRegex = new RegExp('{% *response *.* %}'); export const externalVaultTagPrefix = 'externalVaultTag'; export const generateExternalVaultTagId = () => generateId(externalVaultTagPrefix); const tagRegex = /{%[\s\S]+?%}/g; -const vaultTagIdRegex = new RegExp(`${externalVaultTagPrefix}_\\w+`, 'g'); -export const containExternalVaultTag = (input: string) => { - const hasTemplateTagSymbols = input.includes('{%') && input.includes('%}'); +export const vaultTagIdRegex = new RegExp(`${externalVaultTagPrefix}_[a-z0-9]{32}`, 'g'); +export const containsExternalVaultTag = (input: string) => { + const hasTemplateTagSymbols = input.match(tagRegex); return hasTemplateTagSymbols && input.includes(externalVaultTagPrefix); }; // Replace the unique id in vault tag with a new unique id to avoid duplicates when pasting diff --git a/packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx b/packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx index e665031b618..3aafe8edd1c 100644 --- a/packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx +++ b/packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx @@ -23,7 +23,7 @@ import { DEBOUNCE_MILLIS } from '~/common/constants'; import * as misc from '~/common/misc'; import { type NunjucksParsedTag, type nunjucksTagContextMenuOptions } from '~/common/templating/types'; import { - containExternalVaultTag, + containsExternalVaultTag, extractNunjucksTagFromCoords, replaceVaultTagIdIfNeeded, } from '~/common/templating/utils'; @@ -498,7 +498,7 @@ export const CodeEditor = memo( if (onPaste) { translatedText = onPaste(translatedText); } - if (containExternalVaultTag(translatedText)) { + if (containsExternalVaultTag(translatedText)) { translatedText = replaceVaultTagIdIfNeeded(translatedText); } if (translatedText !== change.text.join('\n')) { diff --git a/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx b/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx index 699c9755339..05e5822bd62 100644 --- a/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx +++ b/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx @@ -20,7 +20,7 @@ import { DEBOUNCE_MILLIS } from '~/common/constants'; import * as misc from '~/common/misc'; import { type NunjucksParsedTag, type nunjucksTagContextMenuOptions } from '~/common/templating/types'; import { - containExternalVaultTag, + containsExternalVaultTag, extractNunjucksTagFromCoords, replaceVaultTagIdIfNeeded, } from '~/common/templating/utils'; @@ -213,7 +213,7 @@ export const OneLineEditor = forwardRef return; } let editorPasteText = change.text.join('').replace(/\n/g, ' '); - if (containExternalVaultTag(editorPasteText)) { + if (containsExternalVaultTag(editorPasteText)) { editorPasteText = replaceVaultTagIdIfNeeded(editorPasteText); } // If we're in single-line mode, merge all changed lines into one