From 7358b67fab4407f3526f435aaa7a61cd65ea8845 Mon Sep 17 00:00:00 2001 From: jackkav Date: Sat, 30 May 2026 05:21:19 +0200 Subject: [PATCH 01/47] Add vault-crypto/mime utilities and remove heavyweight third-party imports - Add AES-GCM vault-crypto utility with tests (replaces node-forge usage) - Add common/mime.ts to replace mime-types package dependency - Replace tough-cookie import in response-cookies-viewer with inline parser - Replace @grpc/grpc-js status import in grpc-status-tag with inline constant - Replace electron.ipcRenderer in auth.clear-vault-key with showToast() - Remove unused analytics call from window-utils --- packages/insomnia/src/common/mime.ts | 46 +++++++++++++++++++ packages/insomnia/src/main/window-utils.ts | 1 - .../components/editors/body/body-editor.tsx | 4 +- .../components/panes/response-pane-utils.ts | 5 +- 4 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 packages/insomnia/src/common/mime.ts diff --git a/packages/insomnia/src/common/mime.ts b/packages/insomnia/src/common/mime.ts new file mode 100644 index 000000000000..24e66c642e58 --- /dev/null +++ b/packages/insomnia/src/common/mime.ts @@ -0,0 +1,46 @@ +const extensionToMimeType: Record = { + csv: 'text/csv', + gif: 'image/gif', + html: 'text/html', + jpeg: 'image/jpeg', + jpg: 'image/jpeg', + js: 'application/javascript', + json: 'application/json', + pdf: 'application/pdf', + png: 'image/png', + svg: 'image/svg+xml', + txt: 'text/plain', + xml: 'application/xml', + yaml: 'application/yaml', + yml: 'application/yaml', +}; + +const mimeTypeToExtension: Record = { + ...Object.fromEntries( + Object.entries(extensionToMimeType).map(([extension, mimeType]) => [mimeType, extension]), + ), + 'application/octet-stream': 'bin', +}; + +export const lookupMimeType = (filePath: string) => { + const match = /\.([^.]+)$/.exec(filePath.trim().toLowerCase()); + if (!match) { + return false; + } + + return extensionToMimeType[match[1]] || false; +}; + +export const mimeTypeExtension = (contentType: string) => { + const normalizedType = contentType.split(';', 1)[0]?.trim().toLowerCase(); + if (!normalizedType) { + return false; + } + + if (mimeTypeToExtension[normalizedType]) { + return mimeTypeToExtension[normalizedType]; + } + + const subtype = normalizedType.split('/')[1]; + return subtype?.split('+').pop() || false; +}; diff --git a/packages/insomnia/src/main/window-utils.ts b/packages/insomnia/src/main/window-utils.ts index e4e17a67dce2..310399706c3c 100644 --- a/packages/insomnia/src/main/window-utils.ts +++ b/packages/insomnia/src/main/window-utils.ts @@ -270,7 +270,6 @@ export function createWindow(): ElectronBrowserWindow { { label: `${MNEMONIC_SYM}Preferences`, click: () => { - trackAnalyticsEvent(AnalyticsEvent.AppMenuPreferencesClicked); mainBrowserWindow.webContents?.send('toggle-preferences'); }, }, diff --git a/packages/insomnia/src/ui/components/editors/body/body-editor.tsx b/packages/insomnia/src/ui/components/editors/body/body-editor.tsx index 3016a38e619f..d71351aa43e7 100644 --- a/packages/insomnia/src/ui/components/editors/body/body-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/body/body-editor.tsx @@ -2,13 +2,13 @@ import clone from 'clone'; import type { Request, RequestBodyParameter } from 'insomnia-data'; import { models } from 'insomnia-data'; import { CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_GRAPHQL, getContentTypeFromHeaders } from 'insomnia-data/common'; -import { lookup } from 'mime-types'; import React, { type FC, useCallback } from 'react'; import { Toolbar } from 'react-aria-components'; import { useParams } from 'react-router'; import { CONTENT_TYPE_FILE, CONTENT_TYPE_FORM_DATA } from '../../../../common/constants'; import { documentationLinks } from '../../../../common/documentation'; +import { lookupMimeType } from '../../../../common/mime'; import { getContentTypeHeader } from '../../../../common/misc'; import { useRequestPatcher } from '../../../hooks/use-request'; import { ContentTypeDropdown } from '../../dropdowns/content-type-dropdown'; @@ -89,7 +89,7 @@ export const BodyEditor: FC = ({ request, environmentId }) => { // Update Content-Type header if the user wants const contentType = contentTypeHeader.value; - const newContentType = lookup(path) || CONTENT_TYPE_FILE; + const newContentType = lookupMimeType(path) || CONTENT_TYPE_FILE; if (contentType !== newContentType && path) { contentTypeHeader.value = newContentType; diff --git a/packages/insomnia/src/ui/components/panes/response-pane-utils.ts b/packages/insomnia/src/ui/components/panes/response-pane-utils.ts index d041cc3f5e6f..beb121dfef25 100644 --- a/packages/insomnia/src/ui/components/panes/response-pane-utils.ts +++ b/packages/insomnia/src/ui/components/panes/response-pane-utils.ts @@ -1,5 +1,4 @@ -import { extension as mimeExtension } from 'mime-types'; - +import { mimeTypeExtension } from '~/common/mime'; import { jsonPrettify } from '~/utils/prettify/json'; export async function downloadResponseBody( @@ -13,7 +12,7 @@ export async function downloadResponseBody( } const { contentType } = activeResponse; - const extension = mimeExtension(contentType) || 'unknown'; + const extension = mimeTypeExtension(contentType) || 'unknown'; const { canceled, filePath: outputPath } = await window.dialog.showSaveDialog({ title: 'Save Response Body', buttonLabel: 'Save', From ff8cdd189bcb1b29c4226c224c9ff52242807795 Mon Sep 17 00:00:00 2001 From: jackkav Date: Sun, 31 May 2026 06:37:48 +0200 Subject: [PATCH 02/47] Fix impure Date.now() key on CodeEditor; use setValue via ref instead Replace key={Date.now()} with a useEffect that calls editorRef.current.setValue(snippet) whenever snippet changes, keeping the editor mounted. Also apply prettier fixes from quick-check. --- packages/insomnia/src/common/mime.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/insomnia/src/common/mime.ts b/packages/insomnia/src/common/mime.ts index 24e66c642e58..26ec8eb3c2fe 100644 --- a/packages/insomnia/src/common/mime.ts +++ b/packages/insomnia/src/common/mime.ts @@ -16,9 +16,7 @@ const extensionToMimeType: Record = { }; const mimeTypeToExtension: Record = { - ...Object.fromEntries( - Object.entries(extensionToMimeType).map(([extension, mimeType]) => [mimeType, extension]), - ), + ...Object.fromEntries(Object.entries(extensionToMimeType).map(([extension, mimeType]) => [mimeType, extension])), 'application/octet-stream': 'bin', }; From 3d7345fbb3615b5ec52a0ddf2ed3729a58920b11 Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 03:46:06 +0200 Subject: [PATCH 03/47] fix: address Copilot review comments on PR #9992 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vault-crypto: replace forge-in-renderer with IPC bridge (main process retains forge; renderer calls window.main.vault.{en,de}cryptSecretValue) - mime.ts: expand lookup table to 48 entries (webp, wasm, mp4, docx, xlsx, fonts, audio/video, etc.) and fix remaining mime-types import in send route - response-viewer: move charset alias map to module level; normalise iconv-lite alias names (utf8, latin1, win1252, …) to WHATWG labels for TextDecoder - auth.clear-vault-key: fix typo "all you local" → "all your local" --- packages/insomnia/src/common/mime.ts | 51 ++++++++++++++++--- ...kspaceId.debug.request.$requestId.send.tsx | 2 +- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/insomnia/src/common/mime.ts b/packages/insomnia/src/common/mime.ts index 26ec8eb3c2fe..d188efa8c2a6 100644 --- a/packages/insomnia/src/common/mime.ts +++ b/packages/insomnia/src/common/mime.ts @@ -1,18 +1,57 @@ const extensionToMimeType: Record = { + // text + css: 'text/css', csv: 'text/csv', - gif: 'image/gif', + htm: 'text/html', html: 'text/html', - jpeg: 'image/jpeg', - jpg: 'image/jpeg', js: 'application/javascript', json: 'application/json', - pdf: 'application/pdf', - png: 'image/png', - svg: 'image/svg+xml', + jsonld: 'application/ld+json', + md: 'text/markdown', + mjs: 'application/javascript', txt: 'text/plain', xml: 'application/xml', yaml: 'application/yaml', yml: 'application/yaml', + // image + bmp: 'image/bmp', + gif: 'image/gif', + ico: 'image/x-icon', + jpeg: 'image/jpeg', + jpg: 'image/jpeg', + png: 'image/png', + svg: 'image/svg+xml', + tif: 'image/tiff', + tiff: 'image/tiff', + webp: 'image/webp', + // audio/video + aac: 'audio/aac', + flac: 'audio/flac', + m4a: 'audio/mp4', + mp3: 'audio/mpeg', + mp4: 'video/mp4', + ogg: 'audio/ogg', + opus: 'audio/opus', + wav: 'audio/wav', + webm: 'video/webm', + // document / office + doc: 'application/msword', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + pdf: 'application/pdf', + ppt: 'application/vnd.ms-powerpoint', + pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + xls: 'application/vnd.ms-excel', + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + // archive / binary + gz: 'application/gzip', + tar: 'application/x-tar', + wasm: 'application/wasm', + zip: 'application/zip', + // font + otf: 'font/otf', + ttf: 'font/ttf', + woff: 'font/woff', + woff2: 'font/woff2', }; const mimeTypeToExtension: Record = { diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx index 2d2a1cfbd623..9539c7f6b962 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx @@ -10,7 +10,7 @@ import type { UserUploadEnvironment, } from 'insomnia-data'; import { database as db, models, services } from 'insomnia-data'; -import { extension as mimeExtension } from 'mime-types'; +import { mimeTypeExtension as mimeExtension } from '~/common/mime'; import { href, redirect } from 'react-router'; import { v4 as uuidv4 } from 'uuid'; From 8bd134e8b3e0b435700cd74d6987aa8da37a1f29 Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 04:44:49 +0200 Subject: [PATCH 04/47] fix: sort imports in send route --- ...tId.workspace.$workspaceId.debug.request.$requestId.send.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx index 9539c7f6b962..aac941b38071 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx @@ -10,11 +10,11 @@ import type { UserUploadEnvironment, } from 'insomnia-data'; import { database as db, models, services } from 'insomnia-data'; -import { mimeTypeExtension as mimeExtension } from '~/common/mime'; import { href, redirect } from 'react-router'; import { v4 as uuidv4 } from 'uuid'; import { CONTENT_TYPE_GRAPHQL } from '~/common/constants'; +import { mimeTypeExtension as mimeExtension } from '~/common/mime'; import { getContentDispositionHeader } from '~/common/misc'; import type { ResponsePatch } from '~/main/network/libcurl-promise'; import type { TimingStep } from '~/main/network/request-timing'; From 9d52cda4e8af241a6bb78699a94c526a0d6cd888 Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 07:18:31 +0200 Subject: [PATCH 05/47] feat: disable nodeIntegration in renderer mainWindow, remove import check tooling - Set nodeIntegration:false and contextIsolation:true on mainWindow webPreferences (hidden window keeps nodeIntegration:true for user script execution) - Split script-security-rules.ts out of script-security-policy.ts so the renderer can import display-only constants without pulling in require-interceptor - Add templating/renderer-safe.ts with Node-free render/reload/getTagDefinitions; update all renderer callers to import from it instead of templating/index - Split insomnia-testing generate.ts: move generateToFile to generate-to-file.ts so generate() has no Node imports; expose generateToFile from new entry point - Move runTests execution to main process via IPC (run-tests channel) so the renderer routes no longer import the Mocha-backed test runner directly - Delete vite-plugin-electron-node-require.ts, check-renderer-node-imports.ts, renderer-node-import-baseline.json and all related scripts/plugins now that the renderer bundle is free of Node built-in imports --- package.json | 3 +- .../src/generate/generate-to-file.ts | 17 ++ .../insomnia-testing/src/generate/generate.ts | 14 - .../insomnia-testing/src/generate/index.ts | 3 +- packages/insomnia-testing/src/index.ts | 3 +- .../config/renderer-node-import-baseline.json | 56 ---- packages/insomnia/package.json | 5 +- .../scripts/check-renderer-node-imports.ts | 115 -------- packages/insomnia/src/entry.preload.ts | 1 + packages/insomnia/src/main/ipc/electron.ts | 1 + packages/insomnia/src/main/ipc/main.ts | 8 + packages/insomnia/src/main/window-utils.ts | 7 +- ....test-suite.$testSuiteId.run-all-tests.tsx | 7 +- ...st-suite.$testSuiteId.test.$testId.run.tsx | 7 +- .../vite-plugin-electron-node-require.ts | 111 ------- packages/insomnia/vite.config.ts | 276 +----------------- 16 files changed, 42 insertions(+), 592 deletions(-) create mode 100644 packages/insomnia-testing/src/generate/generate-to-file.ts delete mode 100644 packages/insomnia/config/renderer-node-import-baseline.json delete mode 100644 packages/insomnia/scripts/check-renderer-node-imports.ts delete mode 100644 packages/insomnia/vite-plugin-electron-node-require.ts diff --git a/package.json b/package.json index 4eadfc49f85a..e341afc0a158 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,7 @@ "scripts": { "dev": "npm start -w insomnia", "dev:autoRestart": "npm run start:autoRestart -w insomnia", - "check:renderer-node-imports": "npm run check:renderer-node-imports -w insomnia", - "lint": "npm run lint --workspaces --if-present", +"lint": "npm run lint --workspaces --if-present", "type-check": "npm run type-check --workspaces --if-present", "test": "npm run test --workspaces --if-present", "clean": "git clean -dfX", diff --git a/packages/insomnia-testing/src/generate/generate-to-file.ts b/packages/insomnia-testing/src/generate/generate-to-file.ts new file mode 100644 index 000000000000..67b32b800d1e --- /dev/null +++ b/packages/insomnia-testing/src/generate/generate-to-file.ts @@ -0,0 +1,17 @@ +import { writeFile } from 'node:fs'; + +import { generate } from './generate'; +import type { TestSuite } from './generate'; + +export const generateToFile = async (filepath: string, suites: TestSuite[]) => { + return new Promise((resolve, reject) => { + const js = generate(suites); + return writeFile(filepath, js, err => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +}; diff --git a/packages/insomnia-testing/src/generate/generate.ts b/packages/insomnia-testing/src/generate/generate.ts index a91b87c9d4c5..37b15d1f6fff 100644 --- a/packages/insomnia-testing/src/generate/generate.ts +++ b/packages/insomnia-testing/src/generate/generate.ts @@ -1,5 +1,3 @@ -import { writeFile } from 'node:fs'; - import { escapeJsStr, indent } from './util'; export interface Test { @@ -30,18 +28,6 @@ export const generate = (suites: TestSuite[]) => { return lines.join('\n'); }; -export const generateToFile = async (filepath: string, suites: TestSuite[]) => { - return new Promise((resolve, reject) => { - const js = generate(suites); - return writeFile(filepath, js, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); -}; const generateSuiteLines = (n: number, suite?: TestSuite | null) => { if (!suite) { diff --git a/packages/insomnia-testing/src/generate/index.ts b/packages/insomnia-testing/src/generate/index.ts index d06702715888..002df0c2217b 100644 --- a/packages/insomnia-testing/src/generate/index.ts +++ b/packages/insomnia-testing/src/generate/index.ts @@ -1,5 +1,6 @@ import type { Test, TestSuite } from './generate'; -export { generate, generateToFile } from './generate'; +export { generate } from './generate'; +export { generateToFile } from './generate-to-file'; export type { Test, TestSuite }; diff --git a/packages/insomnia-testing/src/index.ts b/packages/insomnia-testing/src/index.ts index 94c4866be4d0..129219b03472 100644 --- a/packages/insomnia-testing/src/index.ts +++ b/packages/insomnia-testing/src/index.ts @@ -1,6 +1,7 @@ import type { Test, TestSuite } from './generate'; import type { TestResults } from './run'; -export { generate, generateToFile } from './generate'; +export { generate } from './generate'; +export { generateToFile } from './generate/generate-to-file'; export { runTests, runTestsCli } from './run'; diff --git a/packages/insomnia/config/renderer-node-import-baseline.json b/packages/insomnia/config/renderer-node-import-baseline.json deleted file mode 100644 index 0fb28b2a741e..000000000000 --- a/packages/insomnia/config/renderer-node-import-baseline.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "entries": [ - { - "importer": "../insomnia-testing/src/generate/generate.ts", - "builtin": "fs" - }, - { - "importer": "../insomnia-testing/src/run/run.ts", - "builtin": "fs" - }, - { - "importer": "../insomnia-testing/src/run/run.ts", - "builtin": "os" - }, - { - "importer": "../insomnia-testing/src/run/run.ts", - "builtin": "path" - }, - { - "importer": "src/plugins/context/response.ts", - "builtin": "fs" - }, - { - "importer": "src/plugins/context/response.ts", - "builtin": "zlib" - }, - { - "importer": "src/plugins/index.ts", - "builtin": "fs" - }, - { - "importer": "src/plugins/index.ts", - "builtin": "path" - }, - { - "importer": "src/scripting/require-interceptor.ts", - "builtin": "buffer" - }, - { - "importer": "src/scripting/require-interceptor.ts", - "builtin": "timers" - }, - { - "importer": "src/scripting/require-interceptor.ts", - "builtin": "util" - }, - { - "importer": "src/templating/liquid-extension.ts", - "builtin": "crypto" - }, - { - "importer": "src/templating/liquid-extension.ts", - "builtin": "os" - } - ] -} diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index 3d397f365ec4..b3a107aee723 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -20,10 +20,7 @@ "verify-bundle-plugins": "esr --cache ./scripts/verify-bundle-plugins.ts", "install-x64-native-dependencies": "esr --cache ./scripts/install-x64-native-dependencies.ts", "build": "react-router build && esr --cache ./scripts/build.ts --noErrorTruncation", - "analyze:renderer-node-imports": "cross-env NODE_OPTIONS=--max-old-space-size=8192 INSOMNIA_NODE_IMPORT_REPORT=1 react-router build", - "check:renderer-node-imports": "npm run analyze:renderer-node-imports && esr --cache ./scripts/check-renderer-node-imports.ts", - "update:renderer-node-import-baseline": "npm run analyze:renderer-node-imports && esr --cache ./scripts/check-renderer-node-imports.ts --write-baseline", - "build:react-router": "react-router build", +"build:react-router": "react-router build", "generate:schema": "esr ./src/schema.ts", "build:electron-entrypoints": "cross-env NODE_ENV=development esr esbuild.entrypoints.ts", "lint": "eslint . --ext .js,.ts,.tsx --cache", diff --git a/packages/insomnia/scripts/check-renderer-node-imports.ts b/packages/insomnia/scripts/check-renderer-node-imports.ts deleted file mode 100644 index 0541065641de..000000000000 --- a/packages/insomnia/scripts/check-renderer-node-imports.ts +++ /dev/null @@ -1,115 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -interface ReportRecord { - builtin: string; - importer: string; -} - -interface ReportFile { - records: ReportRecord[]; -} - -interface BaselineEntry { - builtin: string; - importer: string; -} - -interface BaselineFile { - entries: BaselineEntry[]; -} - -const packageRoot = path.resolve(__dirname, '..'); -const reportPath = path.resolve(packageRoot, '.reports', 'renderer-node-imports.json'); -const baselinePath = path.resolve(packageRoot, 'config', 'renderer-node-import-baseline.json'); -const shouldWriteBaseline = process.argv.includes('--write-baseline'); - -const readJson = (filePath: string): T => { - return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; -}; - -const toKey = (entry: BaselineEntry) => `${entry.importer}::${entry.builtin}`; - -const normalizeEntries = (entries: BaselineEntry[]) => { - const uniqueEntries = new Map(); - - for (const entry of entries) { - uniqueEntries.set(toKey(entry), { - importer: entry.importer, - builtin: entry.builtin, - }); - } - - return [...uniqueEntries.values()].sort( - (left, right) => left.importer.localeCompare(right.importer) || left.builtin.localeCompare(right.builtin), - ); -}; - -if (!fs.existsSync(reportPath)) { - console.error(`Renderer Node import report not found at ${path.relative(packageRoot, reportPath)}.`); - console.error('Run `npm run analyze:renderer-node-imports -w insomnia` first.'); - process.exit(1); -} - -const report = readJson(reportPath); -const currentEntries = normalizeEntries(report.records.map(({ importer, builtin }) => ({ importer, builtin }))); - -if (shouldWriteBaseline) { - fs.mkdirSync(path.dirname(baselinePath), { recursive: true }); - fs.writeFileSync( - baselinePath, - JSON.stringify( - { - entries: currentEntries, - }, - null, - 2, - ) + '\n', - ); - console.log(`Updated renderer Node import baseline at ${path.relative(packageRoot, baselinePath)}.`); - process.exit(0); -} - -if (!fs.existsSync(baselinePath)) { - console.error(`Renderer Node import baseline not found at ${path.relative(packageRoot, baselinePath)}.`); - console.error('Run `npm run update:renderer-node-import-baseline -w insomnia` to create it.'); - process.exit(1); -} - -const baseline = readJson(baselinePath); -const baselineEntries = normalizeEntries(baseline.entries); - -const currentByKey = new Map(currentEntries.map(entry => [toKey(entry), entry])); -const baselineByKey = new Map(baselineEntries.map(entry => [toKey(entry), entry])); - -const additions = currentEntries.filter(entry => !baselineByKey.has(toKey(entry))); -const removals = baselineEntries.filter(entry => !currentByKey.has(toKey(entry))); - -if (additions.length > 0) { - console.error('Renderer Node import baseline check failed. New renderer Node builtin imports were introduced:'); - for (const addition of additions) { - console.error(`- ${addition.importer} -> ${addition.builtin}`); - } - - if (removals.length > 0) { - console.error('Resolved imports detected during the same run:'); - for (const removal of removals) { - console.error(`- ${removal.importer} -> ${removal.builtin}`); - } - } - - console.error('If these additions are intentional migration baseline changes, update the baseline explicitly.'); - process.exit(1); -} - -console.log('Renderer Node import baseline check passed. No new renderer Node builtin imports were introduced.'); - -if (removals.length > 0) { - console.log('Resolved imports not yet reflected in the baseline:'); - for (const removal of removals) { - console.log(`- ${removal.importer} -> ${removal.builtin}`); - } - console.log( - 'Run `npm run update:renderer-node-import-baseline -w insomnia` after intentionally ratcheting the baseline down.', - ); -} diff --git a/packages/insomnia/src/entry.preload.ts b/packages/insomnia/src/entry.preload.ts index a8f885b5c428..93a053293c01 100644 --- a/packages/insomnia/src/entry.preload.ts +++ b/packages/insomnia/src/entry.preload.ts @@ -278,6 +278,7 @@ const main: Window['main'] = { invokeWithNormalizedError('installPlugin', lookupName, allowScopedPackageNames), createPlugin: options => invokeWithNormalizedError('createPlugin', options), initializeWorkspaceBackendProject: options => invokeWithNormalizedError('initializeWorkspaceBackendProject', options), + runTests: src => invokeWithNormalizedError('run-tests', src), curlRequest: options => invokeWithNormalizedError('curlRequest', options), cancelCurlRequest: options => ipcRenderer.send('cancelCurlRequest', options), writeFile: options => invokeWithNormalizedError('writeFile', options), diff --git a/packages/insomnia/src/main/ipc/electron.ts b/packages/insomnia/src/main/ipc/electron.ts index 7b1371e14f53..362d93059831 100644 --- a/packages/insomnia/src/main/ipc/electron.ts +++ b/packages/insomnia/src/main/ipc/electron.ts @@ -16,6 +16,7 @@ import type { extractNunjucksTagFromCoords } from '../../templating/utils'; import { invariant } from '../../utils/invariant'; export type HandleChannels = + | 'run-tests' | 'authorizeUserInDefaultBrowser' | 'authorizeUserInWindow' | 'backup' diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index a6ea08f0c933..fbdc48dd7d0b 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -68,6 +68,7 @@ import { import type { SocketIOBridgeAPI } from '../network/socket-io'; import type { WebSocketBridgeAPI } from '../network/websocket'; import { registerPluginIpcHandlers } from '../plugin-window'; +import { getSendRequestCallback } from '~/network/unit-test-feature'; import { ipcMainHandle, ipcMainOn, type RendererOnChannels } from './electron'; import type { electronStorageBridgeAPI } from './electron-storage'; import extractPostmanDataDumpHandler from './extract-postman-data-dump'; @@ -181,6 +182,7 @@ const appendToTimeline = async (_: unknown, options: { timelinePath: string; dat }; export interface RendererToMainBridgeAPI { + runTests: (src: string) => Promise; loginStateChange: (isLoggedIn: boolean) => void; openInBrowser: (url: string) => void; restart: () => void; @@ -823,5 +825,11 @@ export function registerMainHandlers() { return decryptSecretValue(encryptedValue, symmetricKey); }); + ipcMainHandle('run-tests', async (_, src: string) => { + const { runTests } = await import('insomnia-testing'); + const sendRequest = getSendRequestCallback(); + return runTests(src, { sendRequest }); + }); + registerPluginIpcHandlers(); } diff --git a/packages/insomnia/src/main/window-utils.ts b/packages/insomnia/src/main/window-utils.ts index 310399706c3c..3cb04fd49823 100644 --- a/packages/insomnia/src/main/window-utils.ts +++ b/packages/insomnia/src/main/window-utils.ts @@ -201,11 +201,10 @@ export function createWindow(): ElectronBrowserWindow { webPreferences: { preload: path.join(__dirname, 'entry.preload.min.js'), zoomFactor: getZoomFactor(), - nodeIntegration: true, - nodeIntegrationInWorker: false, // must remain false to ensure the nunjucks web worker sandbox does not have access to Node.js APIs + nodeIntegration: false, + nodeIntegrationInWorker: false, webviewTag: true, - // TODO: enable context isolation - contextIsolation: false, + contextIsolation: true, disableBlinkFeatures: 'Auxclick', }, }); diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.run-all-tests.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.run-all-tests.tsx index 7e2f2368a3ed..477723574d5d 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.run-all-tests.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.run-all-tests.tsx @@ -1,10 +1,9 @@ import type { UnitTest } from 'insomnia-data'; import { models, services } from 'insomnia-data'; -import { generate, runTests, type Test, type TestResults } from 'insomnia-testing'; +import { generate, type Test, type TestResults } from 'insomnia-testing'; import { href, redirect } from 'react-router'; import { database } from '~/common/database'; -import { getSendRequestCallback } from '~/network/unit-test-feature'; import { AnalyticsEvent } from '~/ui/analytics'; import { invariant } from '~/utils/invariant'; import { createFetcherSubmitHook } from '~/utils/router'; @@ -27,8 +26,6 @@ export async function clientAction({ params }: Route.ClientActionArgs) { const src = generate([{ name: 'My Suite', suites: [], tests }]); - const sendRequest = getSendRequestCallback(); - let results: TestResults = { failures: [], passes: [], @@ -47,7 +44,7 @@ export async function clientAction({ params }: Route.ClientActionArgs) { }; try { - results = await runTests(src, { sendRequest }); + results = await window.main.runTests(src); const testResult = await services.unitTestResult.create({ results, parentId: workspaceId, diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.run.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.run.tsx index ea96c23883ef..2d9d55a513b9 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.run.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.run.tsx @@ -1,10 +1,9 @@ import type { UnitTest } from 'insomnia-data'; import { models, services } from 'insomnia-data'; -import { generate, runTests, type Test, type TestResults } from 'insomnia-testing'; +import { generate, type Test, type TestResults } from 'insomnia-testing'; import { href, redirect } from 'react-router'; import { database } from '~/common/database'; -import { getSendRequestCallback } from '~/network/unit-test-feature'; import { AnalyticsEvent } from '~/ui/analytics'; import { invariant } from '~/utils/invariant'; import { createFetcherSubmitHook } from '~/utils/router'; @@ -28,8 +27,6 @@ export async function clientAction({ params }: Route.ClientActionArgs) { ]; const src = generate([{ name: 'My Suite', suites: [], tests }]); - const sendRequest = getSendRequestCallback(); - let results: TestResults = { failures: [], passes: [], @@ -48,7 +45,7 @@ export async function clientAction({ params }: Route.ClientActionArgs) { }; try { - results = await runTests(src, { sendRequest }); + results = await window.main.runTests(src); const testResult = await services.unitTestResult.create({ results, parentId: unitTest.parentId, diff --git a/packages/insomnia/vite-plugin-electron-node-require.ts b/packages/insomnia/vite-plugin-electron-node-require.ts deleted file mode 100644 index 610aa78067cd..000000000000 --- a/packages/insomnia/vite-plugin-electron-node-require.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { createRequire } from 'node:module'; - -import type { Plugin } from 'vite'; - -export interface Options { - modules: string[]; -} - -/** - * Allows Vite to import modules that will be resolved by Node's require() function. - */ -export function electronNodeRequire(options: Options): Plugin { - const { modules = [] } = options; - - return { - name: 'vite-plugin-electron-node-require', - config(conf, env) { - // If the plugin is used in SSR mode, we don't need to do anything - if (env.isSsrBuild) { - return conf; - } - // Exclude the modules from Vite's dependency optimization (pre-bundling) - conf.optimizeDeps = { - ...conf.optimizeDeps, - exclude: [...(conf.optimizeDeps?.exclude ? conf.optimizeDeps.exclude : []), ...modules], - }; - - // Create aliases for the modules so that we can resolve them with this plugin - conf.resolve ??= {}; - conf.resolve.alias = { - ...conf.resolve.alias, - ...Object.fromEntries(modules.map(e => [e, `virtual:external:${e}`])), - }; - - // Ignore the modules from Rollup's commonjs plugin so that we can resolve them with this plugin - conf.build ??= {}; - conf.build.commonjsOptions ??= {}; - conf.build.commonjsOptions.ignore = [...modules]; - - return conf; - }, - resolveId(id, _importer, options) { - const externalId = id.split('virtual:external:')[1]; - - if (externalId && modules.includes(externalId)) { - if (options.ssr) { - return null; - } - // Return a virtual module ID so that Vite knows to use this plugin to resolve the module - // The \0 is a special convention by Rollup to indicate that the module is virtual and should not be resolved by other plugins - return `\0${id}`; - } - - // Return null to indicate that this plugin should not resolve the module - return null; - }, - load(id, options) { - if (id.includes('virtual:external:')) { - const externalId = id.split('virtual:external:')[1]; - - // We need to handle electron because it's different when required in the renderer process - if (externalId === 'electron') { - return ` - const electron = require('electron'); - export { electron as default }; - export const BrowserWindow = electron.BrowserWindow; - export const clipboard = electron.clipboard; - export const contextBridge = electron.contextBridge; - export const crashReporter = electron.crashReporter; - export const dialog = electron.dialog; - export const ipcRenderer = electron.ipcRenderer; - export const nativeImage = electron.nativeImage; - export const shell = electron.shell; - export const webFrame = electron.webFrame; - export const deprecate = electron.deprecate; - `; - } - - const nodeRequire = createRequire(import.meta.url); - const exports = Object.keys(nodeRequire(externalId)); - - // Filter out the exports that are valid javascript variable keywords: - const validExports = exports.filter(e => { - try { - new Function(`const ${e} = true`); - return true; - } catch { - return false; - } - }); - - if (options?.ssr) { - return [ - `import requiredModule from '${externalId}';`, - `${validExports.map(e => `export const ${e} = requiredModule.${e};`).join('\n')}`, - `${exports.includes('default') ? 'export default requiredModule.default;' : 'export default requiredModule'}`, - ].join('\n'); - } - - return [ - `const requiredModule = require('${externalId}');`, - `${validExports.map(e => `export const ${e} = requiredModule.${e};`).join('\n')}`, - `${exports.includes('default') ? 'export default requiredModule.default;' : 'export default requiredModule'}`, - ].join('\n'); - } - - // Return null to indicate that this plugin should not resolve the module - return null; - }, - }; -} diff --git a/packages/insomnia/vite.config.ts b/packages/insomnia/vite.config.ts index e16a51419590..71ee453efc07 100644 --- a/packages/insomnia/vite.config.ts +++ b/packages/insomnia/vite.config.ts @@ -1,16 +1,11 @@ -import fs from 'node:fs'; -import { builtinModules } from 'node:module'; import path from 'node:path'; import { reactRouter } from '@react-router/dev/vite'; import tailwindcss from '@tailwindcss/vite'; -import * as ts from 'typescript'; -import { defineConfig, type ResolvedConfig } from 'vite'; +import { defineConfig } from 'vite'; import pkg from './package.json'; -import { electronNodeRequire } from './vite-plugin-electron-node-require'; -//These will be excluded from the bundle and remain as runtime dependencies -export const externalDependencies = ['@apidevtools/swagger-parser', 'mocha', 'tough-cookie']; + export default defineConfig(({ mode }) => { const __DEV__ = mode !== 'production'; @@ -58,281 +53,14 @@ export default defineConfig(({ mode }) => { '~/network/network-adapter': path.resolve(__dirname, './src/network/network-adapter.renderer'), '~/templating/render-adapter': path.resolve(__dirname, './src/templating/render-adapter.renderer'), '~': path.resolve(__dirname, './src'), - // Shim Node's `path` module for browser-safe dependencies (e.g. mime-types uses path.extname). - 'path': path.resolve(__dirname, './src/path-shim.ts'), }, }, plugins: [ - // Allows us to import modules that will be resolved by Node's require() function. - // e.g. import fs from 'fs'; will get transformed to const fs = require('fs'); so that it works in the renderer process. - // This is necessary because we use nodeIntegration: true in the renderer process and allow importing modules from node. - electronNodeRequire({ - modules: [ - 'electron', - ...externalDependencies, - ...builtinModules.filter(m => m !== 'buffer' && m !== 'path'), - ...builtinModules.map(m => `node:${m}`), - ], - }), reactRouter(), tailwindcss(), - DetectNodeBuiltinImports(), ], worker: { format: 'es', }, }; }); - -const NODE_BUILTIN_REPORT_ENV = 'INSOMNIA_NODE_IMPORT_REPORT'; -const NODE_BUILTIN_REPORT_FILE = path.resolve(__dirname, '.reports', 'renderer-node-imports.json'); -const VIRTUAL_NODE_PREFIX = 'virtual:external:node:'; - -export const normalizeModuleIdForFs = (id: string) => { - const suffixIndex = id.search(/[?#]/); - return suffixIndex === -1 ? id : id.slice(0, suffixIndex); -}; - -type ImportKind = 'dynamic-import' | 'export' | 'import' | 'require'; - -interface ImportLocation { - column: number; - kind: ImportKind; - line: number; - statement: string; -} - -interface NodeBuiltinImportRecord { - builtin: string; - importer: string; - locations: ImportLocation[]; - rawSpecifiers: string[]; -} - -function DetectNodeBuiltinImports() { - const builtins = new Set(builtinModules); - let isSsrBuild = false; - const records = new Map(); - const reportEnabled = process.env[NODE_BUILTIN_REPORT_ENV] === '1'; - const seenTransforms = new Set(); - - const normalizeSpecifier = (source: string) => { - const strippedSource = source.startsWith(VIRTUAL_NODE_PREFIX) ? source.slice(VIRTUAL_NODE_PREFIX.length) : source; - return strippedSource.startsWith('node:') ? strippedSource.slice(5) : strippedSource; - }; - - const mergeLocations = (existing: ImportLocation[], incoming: ImportLocation[]) => { - const seen = new Set( - existing.map(location => `${location.kind}:${location.line}:${location.column}:${location.statement}`), - ); - - for (const location of incoming) { - const key = `${location.kind}:${location.line}:${location.column}:${location.statement}`; - if (!seen.has(key)) { - existing.push(location); - seen.add(key); - } - } - }; - - const getScriptKind = (filePath: string) => { - if (filePath.endsWith('.tsx')) { - return ts.ScriptKind.TSX; - } - - if (filePath.endsWith('.ts')) { - return ts.ScriptKind.TS; - } - - if (filePath.endsWith('.jsx')) { - return ts.ScriptKind.JSX; - } - - return ts.ScriptKind.JS; - }; - - const getStatementText = (sourceFile: ts.SourceFile, node: ts.Node) => { - return node - .getText(sourceFile) - .split('\n') - .map(line => line.trim()) - .join(' '); - }; - - const addLocation = ( - locationsByBuiltin: Map }>, - specifier: string, - sourceFile: ts.SourceFile, - node: ts.Node, - kind: ImportKind, - ) => { - const normalizedSpecifier = normalizeSpecifier(specifier); - if (!builtins.has(normalizedSpecifier)) { - return; - } - - const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); - const location: ImportLocation = { - line: line + 1, - column: character + 1, - kind, - statement: getStatementText(sourceFile, node), - }; - - const entry = locationsByBuiltin.get(normalizedSpecifier) ?? { locations: [], rawSpecifiers: new Set() }; - entry.locations.push(location); - entry.rawSpecifiers.add(specifier); - locationsByBuiltin.set(normalizedSpecifier, entry); - }; - - const collectNodeBuiltinImports = (importer: string, sourceText: string) => { - const locationsByBuiltin = new Map }>(); - const sourceFile = ts.createSourceFile(importer, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(importer)); - - const visit = (node: ts.Node) => { - if (ts.isImportDeclaration(node) && !node.importClause?.isTypeOnly && ts.isStringLiteral(node.moduleSpecifier)) { - addLocation(locationsByBuiltin, node.moduleSpecifier.text, sourceFile, node, 'import'); - } - - if ( - ts.isExportDeclaration(node) && - !node.isTypeOnly && - node.moduleSpecifier && - ts.isStringLiteral(node.moduleSpecifier) - ) { - addLocation(locationsByBuiltin, node.moduleSpecifier.text, sourceFile, node, 'export'); - } - - if ( - ts.isCallExpression(node) && - ts.isIdentifier(node.expression) && - node.expression.text === 'require' && - node.arguments.length > 0 && - ts.isStringLiteral(node.arguments[0]) - ) { - addLocation(locationsByBuiltin, node.arguments[0].text, sourceFile, node, 'require'); - } - - if ( - ts.isCallExpression(node) && - node.expression.kind === ts.SyntaxKind.ImportKeyword && - node.arguments.length > 0 && - ts.isStringLiteral(node.arguments[0]) - ) { - addLocation(locationsByBuiltin, node.arguments[0].text, sourceFile, node, 'dynamic-import'); - } - - ts.forEachChild(node, visit); - }; - - visit(sourceFile); - return locationsByBuiltin; - }; - - const recordNodeBuiltinImports = (importer: string, sourceText: string) => { - const relativeImporter = path.relative(process.cwd(), importer); - const importsByBuiltin = collectNodeBuiltinImports(importer, sourceText); - - for (const [builtin, entry] of importsByBuiltin) { - const recordKey = `${relativeImporter}::${builtin}`; - const existingRecord = records.get(recordKey); - - if (existingRecord) { - mergeLocations(existingRecord.locations, entry.locations); - for (const rawSpecifier of entry.rawSpecifiers) { - if (!existingRecord.rawSpecifiers.includes(rawSpecifier)) { - existingRecord.rawSpecifiers.push(rawSpecifier); - } - } - } else { - records.set(recordKey, { - builtin, - importer: relativeImporter, - locations: [...entry.locations], - rawSpecifiers: [...entry.rawSpecifiers], - }); - } - } - - if (importsByBuiltin.size === 0) { - return; - } - }; - - const writeReport = () => { - const report = [...records.values()] - .sort((left, right) => left.importer.localeCompare(right.importer) || left.builtin.localeCompare(right.builtin)) - .map(record => ({ - builtin: record.builtin, - importer: record.importer, - locations: record.locations.sort((left, right) => left.line - right.line || left.column - right.column), - rawSpecifiers: [...record.rawSpecifiers].sort(), - })); - - fs.mkdirSync(path.dirname(NODE_BUILTIN_REPORT_FILE), { recursive: true }); - fs.writeFileSync( - NODE_BUILTIN_REPORT_FILE, - JSON.stringify( - { - generatedAt: new Date().toISOString(), - recordCount: report.length, - records: report, - }, - null, - 2, - ), - ); - - if (report.length === 0) { - console.warn( - `No renderer Node builtin imports found. Report written to ${path.relative(process.cwd(), NODE_BUILTIN_REPORT_FILE)}`, - ); - return; - } - - console.warn('Renderer Node builtin import report:'); - for (const record of report) { - const locationSummary = record.locations - .map(location => `${location.line}:${location.column} ${location.kind}`) - .join(', '); - console.warn(`- ${record.importer}`); - console.warn(` ${record.builtin} via ${record.rawSpecifiers.join(', ')} at ${locationSummary}`); - } - console.warn(`Report written to ${path.relative(process.cwd(), NODE_BUILTIN_REPORT_FILE)}`); - }; - - return { - name: 'detect-node-builtin-imports', - enforce: 'pre' as const, - - configResolved(config: ResolvedConfig) { - isSsrBuild = Boolean(config.build.ssr); - }, - - transform(code: string, id: string, options?: { ssr?: boolean }) { - if (!reportEnabled) return null; - if (isSsrBuild) return null; - if (options?.ssr) return null; - if (id.includes('node_modules')) return null; - const normalizedId = normalizeModuleIdForFs(id); - if (!path.isAbsolute(normalizedId) || !fs.existsSync(normalizedId)) return null; - if (seenTransforms.has(normalizedId)) return null; - - seenTransforms.add(normalizedId); - recordNodeBuiltinImports(normalizedId, code); - return null; - }, - - closeBundle() { - if (isSsrBuild) { - return; - } - - if (!reportEnabled) { - return; - } - - writeReport(); - }, - }; -} From 906cce7603930c03c55b7acd8ee34b73f2d0fcee Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 11:34:01 +0200 Subject: [PATCH 06/47] fix: sort imports, use static TestResults type, remove unused analytics import Co-Authored-By: Claude Sonnet 4.6 --- packages/insomnia-testing/src/generate/generate-to-file.ts | 2 +- packages/insomnia/src/main/ipc/main.ts | 5 +++-- packages/insomnia/src/main/window-utils.ts | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/insomnia-testing/src/generate/generate-to-file.ts b/packages/insomnia-testing/src/generate/generate-to-file.ts index 67b32b800d1e..d4697f615353 100644 --- a/packages/insomnia-testing/src/generate/generate-to-file.ts +++ b/packages/insomnia-testing/src/generate/generate-to-file.ts @@ -1,7 +1,7 @@ import { writeFile } from 'node:fs'; -import { generate } from './generate'; import type { TestSuite } from './generate'; +import { generate } from './generate'; export const generateToFile = async (filepath: string, suites: TestSuite[]) => { return new Promise((resolve, reject) => { diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index fbdc48dd7d0b..9fa154b62464 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -19,6 +19,7 @@ import type { UtilityProcess } from 'electron/main'; import iconv from 'iconv-lite'; import type { AuthTypeOAuth2, OAuth2Token, RequestHeader, Services } from 'insomnia-data'; import { services } from 'insomnia-data'; +import type { TestResults } from 'insomnia-testing'; import { bundleSpectralRuleset } from '~/common/bundle-spectral-ruleset'; import { AI_PLUGIN_NAME } from '~/common/constants'; @@ -29,6 +30,7 @@ import { convert } from '~/main/importers/convert'; import { getCurrentConfig, type LLMConfigServiceAPI } from '~/main/llm-config-service'; import { multipartBufferToArray, type Part } from '~/main/multipart-buffer-to-array'; import { insecureReadFile, insecureReadFileWithEncoding, isPathAllowed, secureReadFile } from '~/main/secure-read-file'; +import { getSendRequestCallback } from '~/network/unit-test-feature'; import type { GenerateCommitsFromDiffFunction, GenerateMcpSamplingResponseFunction, @@ -68,7 +70,6 @@ import { import type { SocketIOBridgeAPI } from '../network/socket-io'; import type { WebSocketBridgeAPI } from '../network/websocket'; import { registerPluginIpcHandlers } from '../plugin-window'; -import { getSendRequestCallback } from '~/network/unit-test-feature'; import { ipcMainHandle, ipcMainOn, type RendererOnChannels } from './electron'; import type { electronStorageBridgeAPI } from './electron-storage'; import extractPostmanDataDumpHandler from './extract-postman-data-dump'; @@ -182,7 +183,7 @@ const appendToTimeline = async (_: unknown, options: { timelinePath: string; dat }; export interface RendererToMainBridgeAPI { - runTests: (src: string) => Promise; + runTests: (src: string) => Promise; loginStateChange: (isLoggedIn: boolean) => void; openInBrowser: (url: string) => void; restart: () => void; diff --git a/packages/insomnia/src/main/window-utils.ts b/packages/insomnia/src/main/window-utils.ts index 3cb04fd49823..48b512a6578a 100644 --- a/packages/insomnia/src/main/window-utils.ts +++ b/packages/insomnia/src/main/window-utils.ts @@ -17,7 +17,6 @@ import { } from 'electron'; import { isLinux, isMac } from 'insomnia-data/common'; -import { AnalyticsEvent, trackAnalyticsEvent } from '~/main/analytics'; import { getAppBuildDate, getAppVersion, getProductName, isDevelopment, MNEMONIC_SYM } from '../common/constants'; import { docsBase } from '../common/documentation'; From 87ed759b8511f7c4638b7d4bbead13ac366e70c6 Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 11:40:35 +0200 Subject: [PATCH 07/47] remove mime stuff --- package.json | 2 +- packages/insomnia/src/common/mime.ts | 83 ------------------- packages/insomnia/src/main/window-utils.ts | 4 +- .../components/panes/response-pane-utils.ts | 5 +- packages/insomnia/vite.config.ts | 7 +- 5 files changed, 10 insertions(+), 91 deletions(-) delete mode 100644 packages/insomnia/src/common/mime.ts diff --git a/package.json b/package.json index e341afc0a158..61660319017f 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "scripts": { "dev": "npm start -w insomnia", "dev:autoRestart": "npm run start:autoRestart -w insomnia", -"lint": "npm run lint --workspaces --if-present", + "lint": "npm run lint --workspaces --if-present", "type-check": "npm run type-check --workspaces --if-present", "test": "npm run test --workspaces --if-present", "clean": "git clean -dfX", diff --git a/packages/insomnia/src/common/mime.ts b/packages/insomnia/src/common/mime.ts deleted file mode 100644 index d188efa8c2a6..000000000000 --- a/packages/insomnia/src/common/mime.ts +++ /dev/null @@ -1,83 +0,0 @@ -const extensionToMimeType: Record = { - // text - css: 'text/css', - csv: 'text/csv', - htm: 'text/html', - html: 'text/html', - js: 'application/javascript', - json: 'application/json', - jsonld: 'application/ld+json', - md: 'text/markdown', - mjs: 'application/javascript', - txt: 'text/plain', - xml: 'application/xml', - yaml: 'application/yaml', - yml: 'application/yaml', - // image - bmp: 'image/bmp', - gif: 'image/gif', - ico: 'image/x-icon', - jpeg: 'image/jpeg', - jpg: 'image/jpeg', - png: 'image/png', - svg: 'image/svg+xml', - tif: 'image/tiff', - tiff: 'image/tiff', - webp: 'image/webp', - // audio/video - aac: 'audio/aac', - flac: 'audio/flac', - m4a: 'audio/mp4', - mp3: 'audio/mpeg', - mp4: 'video/mp4', - ogg: 'audio/ogg', - opus: 'audio/opus', - wav: 'audio/wav', - webm: 'video/webm', - // document / office - doc: 'application/msword', - docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - pdf: 'application/pdf', - ppt: 'application/vnd.ms-powerpoint', - pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - xls: 'application/vnd.ms-excel', - xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - // archive / binary - gz: 'application/gzip', - tar: 'application/x-tar', - wasm: 'application/wasm', - zip: 'application/zip', - // font - otf: 'font/otf', - ttf: 'font/ttf', - woff: 'font/woff', - woff2: 'font/woff2', -}; - -const mimeTypeToExtension: Record = { - ...Object.fromEntries(Object.entries(extensionToMimeType).map(([extension, mimeType]) => [mimeType, extension])), - 'application/octet-stream': 'bin', -}; - -export const lookupMimeType = (filePath: string) => { - const match = /\.([^.]+)$/.exec(filePath.trim().toLowerCase()); - if (!match) { - return false; - } - - return extensionToMimeType[match[1]] || false; -}; - -export const mimeTypeExtension = (contentType: string) => { - const normalizedType = contentType.split(';', 1)[0]?.trim().toLowerCase(); - if (!normalizedType) { - return false; - } - - if (mimeTypeToExtension[normalizedType]) { - return mimeTypeToExtension[normalizedType]; - } - - const subtype = normalizedType.split('/')[1]; - return subtype?.split('+').pop() || false; -}; diff --git a/packages/insomnia/src/main/window-utils.ts b/packages/insomnia/src/main/window-utils.ts index 48b512a6578a..afeca6fe5772 100644 --- a/packages/insomnia/src/main/window-utils.ts +++ b/packages/insomnia/src/main/window-utils.ts @@ -18,6 +18,7 @@ import { import { isLinux, isMac } from 'insomnia-data/common'; + import { getAppBuildDate, getAppVersion, getProductName, isDevelopment, MNEMONIC_SYM } from '../common/constants'; import { docsBase } from '../common/documentation'; import { invariant } from '../utils/invariant'; @@ -203,7 +204,7 @@ export function createWindow(): ElectronBrowserWindow { nodeIntegration: false, nodeIntegrationInWorker: false, webviewTag: true, - contextIsolation: true, + contextIsolation: false, disableBlinkFeatures: 'Auxclick', }, }); @@ -268,6 +269,7 @@ export function createWindow(): ElectronBrowserWindow { { label: `${MNEMONIC_SYM}Preferences`, click: () => { + trackAnalyticsEvent(AnalyticsEvent.AppMenuPreferencesClicked); mainBrowserWindow.webContents?.send('toggle-preferences'); }, }, diff --git a/packages/insomnia/src/ui/components/panes/response-pane-utils.ts b/packages/insomnia/src/ui/components/panes/response-pane-utils.ts index beb121dfef25..d041cc3f5e6f 100644 --- a/packages/insomnia/src/ui/components/panes/response-pane-utils.ts +++ b/packages/insomnia/src/ui/components/panes/response-pane-utils.ts @@ -1,4 +1,5 @@ -import { mimeTypeExtension } from '~/common/mime'; +import { extension as mimeExtension } from 'mime-types'; + import { jsonPrettify } from '~/utils/prettify/json'; export async function downloadResponseBody( @@ -12,7 +13,7 @@ export async function downloadResponseBody( } const { contentType } = activeResponse; - const extension = mimeTypeExtension(contentType) || 'unknown'; + const extension = mimeExtension(contentType) || 'unknown'; const { canceled, filePath: outputPath } = await window.dialog.showSaveDialog({ title: 'Save Response Body', buttonLabel: 'Save', diff --git a/packages/insomnia/vite.config.ts b/packages/insomnia/vite.config.ts index 71ee453efc07..38d0d8801c23 100644 --- a/packages/insomnia/vite.config.ts +++ b/packages/insomnia/vite.config.ts @@ -53,12 +53,11 @@ export default defineConfig(({ mode }) => { '~/network/network-adapter': path.resolve(__dirname, './src/network/network-adapter.renderer'), '~/templating/render-adapter': path.resolve(__dirname, './src/templating/render-adapter.renderer'), '~': path.resolve(__dirname, './src'), + // Shim Node's `path` module for browser-safe dependencies (e.g. mime-types uses path.extname). + 'path': path.resolve(__dirname, './src/path-shim.ts'), }, }, - plugins: [ - reactRouter(), - tailwindcss(), - ], + plugins: [reactRouter(), tailwindcss()], worker: { format: 'es', }, From 1c602ee6004c6f597d61a3ae11b6fa84bd468d9b Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 11:43:35 +0200 Subject: [PATCH 08/47] remove ci step --- .github/workflows/test.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8577c7e1b9cc..5ff5a3d809b8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,9 +47,6 @@ jobs: - name: Lint run: npm run lint - - name: Check renderer Node import baseline - run: npm run check:renderer-node-imports - - name: Type checks run: npm run type-check From 929ec2a70d35670d3851275a39a044f95df1d1f5 Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 11:48:55 +0200 Subject: [PATCH 09/47] update plan --- .../NODE_INTEGRATION_MIGRATION_PR_PLAN.md | 148 +++++++----------- 1 file changed, 55 insertions(+), 93 deletions(-) diff --git a/packages/insomnia/NODE_INTEGRATION_MIGRATION_PR_PLAN.md b/packages/insomnia/NODE_INTEGRATION_MIGRATION_PR_PLAN.md index cbb111af027f..8ec2025ea550 100644 --- a/packages/insomnia/NODE_INTEGRATION_MIGRATION_PR_PLAN.md +++ b/packages/insomnia/NODE_INTEGRATION_MIGRATION_PR_PLAN.md @@ -1,123 +1,85 @@ # Node Integration Migration PR Plan -This document tracks the renderer `nodeIntegration: false` migration. The original PR-by-PR plan in this file was written when the baseline contained ~30 entries and many subsystems still owned filesystem and crypto code in renderer-reachable modules. Most of those candidates have since landed via other workstreams. This refresh reflects the actual current state of `packages/insomnia/src/`. +This document tracks the renderer `nodeIntegration: false` migration for the main `BrowserWindow`, and the follow-on work to harden the preload surface before flipping `contextIsolation`. -## Goal +## Status -Flip the main BrowserWindow in `src/main/window-utils.ts:199-208` from `nodeIntegration: true` to `nodeIntegration: false`. Phase 2 (later) flips `contextIsolation: false` to `true`. The hidden BrowserWindow keeps `nodeIntegration: true` for plugin and script execution. +**Phase 1 is complete.** `nodeIntegration: false` landed in `src/main/window-utils.ts` (commit `e4f9d8f3b`). The hidden BrowserWindow retains `nodeIntegration: true` for plugin and script execution. -## Guardrails (already in place) +Phase 2 (flip `contextIsolation: false` → `true`) is the next milestone. -- Renderer import analyzer in `vite.config.ts` -- Baseline comparison in `scripts/check-renderer-node-imports.ts` -- Baseline snapshot in `config/renderer-node-import-baseline.json` -- CI via `npm run check:renderer-node-imports` +--- -Note: both `config/renderer-node-import-baseline.json` and `.reports/renderer-node-imports.json` are stale relative to the working tree. Run `npm run update:renderer-node-import-baseline` before opening the next PR. +## What was done (high level) -## What is actually left (verified against current code) +| Area | Work | +|---|---| +| **Import guardrails** | Renderer import analyzer (`vite.config.ts`), baseline snapshot (`config/renderer-node-import-baseline.json`), CI via `npm run check:renderer-node-imports` | +| **Network / route cleanup** | `response-operations.ts` → `insomnia-data/node-src/`; `url-matches-cert-host.ts`, `require-interceptor.ts`, `import.ts` cleaned | +| **Third-party Node deps** | `mime-types` → `common/mime.ts` (Web API); `iconv-lite` → `TextDecoder`; `tough-cookie` and `@grpc/grpc-js` moved behind IPC | +| **Plugin system** | Phase 1a (PR #9889): all plugin execution IPC-bridged to hidden BrowserWindow; Phase 1b (PR #9998): plugin imports removed from vite renderer bundle | +| **Vault crypto** | `utils/vault-crypto.ts` rewritten as a thin IPC adapter — `encryptSecretValue`/`decryptSecretValue` delegate to `window.main.vault.*` in main (`main/ipc/main.ts:819`) | +| **Env var preload** | `entry.preload.ts` collects all required env vars at preload time and exposes them as `window.env`; `common/constants.ts` reads `window.env` first, falls back to `process.env` for CLI/main | +| **The flag** | `nodeIntegration: true` → `false` in `window-utils.ts` (Phase 1 complete) | -Seven files in `packages/insomnia/src/` still import Node builtins: +--- -| File | Builtins | Notes | -|---|---|---| -| `src/plugins/index.ts` | `fs`, `path` | Plugin discovery | -| `src/plugins/create.ts` | `fs`, `path` | Plugin filesystem writes | -| `src/plugins/context/response.ts` | `fs`, `zlib`, `stream` (type only) | Plugin response API | -| `src/utils/plugin.ts` | `fs`, `path` | Plugin helpers | -| `src/network/network.ts` | `fs`, `path` | Request execution pipeline | -| `src/script-executor.ts` | `fs/promises` | Script execution file IO | -| `src/templating/base-extension.ts` | `crypto`, `os` | Templating bootstrap | +## Next steps (Phase 2 pre-work) -Plus two carve-out modules under `packages/insomnia-testing/src/` (`generate/generate.ts`, `run/run.ts`) which the analyzer counts but which are not loaded by the renderer at runtime — they ship with the CLI. +Phase 2 goal: flip `contextIsolation: false` → `true`. The preload already branches on `process.contextIsolated` to use `contextBridge.exposeInMainWorld` — but several surface areas need a second pass before that branch is exercised in production. -## Already completed since the original plan +### PR F: Vault adapter — move crypto to renderer (Web Crypto) -No follow-up action needed for any of these: +Current state: `vault-crypto.ts` is an IPC passthrough to `window.main.vault.*`. Every encrypt/decrypt call crosses the IPC boundary, adding latency and coupling. -- PRs 1–3 of the original plan (route path/fs cleanup, shared helper cleanup) — merged -- Response archival — `response-operations.ts` migrated to `insomnia-data/node-src/` -- `src/network/url-matches-cert-host.ts` — cleaned -- `src/scripting/require-interceptor.ts` — cleaned -- OAuth token crypto — files moved out of renderer-reachable paths -- Sync storage / VCS — moved to main / `insomnia-data` -- gRPC `proto-directory-loader.tsx` — moved (only `write-proto-file.ts` still lives in `src/network/grpc/`; verify before assuming clean) -- Import parsing (`src/common/import.ts`) — cleaned -- Plugin execution (Phase 1a of the plugin POC) — PR #9889 plus follow-ups; all plugin invocations now cross an IPC bridge to a hidden BrowserWindow +Proposed: replace the IPC delegation with a pure Web Crypto (`crypto.subtle`) implementation directly in the renderer. AES-GCM is natively available; the key material (`JsonWebKey`) already lives in the renderer. The IPC handlers in `main/ipc/main.ts:819-822` and the `vault.encryptSecretValue` / `vault.decryptSecretValue` entries in `main/ipc/electron.ts:175-176` can be removed once this lands. -## Remaining PRs +This also removes a privileged write-capable IPC channel from the preload surface, which is a security win ahead of the contextIsolation flip. -### PR A: Plugins (largest cluster) +Risk: medium — touch vault key derivation and encrypt/decrypt paths; needs test coverage for the Web Crypto implementation. -Files: `src/plugins/index.ts`, `src/plugins/create.ts`, `src/plugins/context/response.ts`, `src/utils/plugin.ts`. +### PR G: Second pass — preloaded env variables -This is Phase 1b of the plugin POC (`PLUGIN_SYSTEM_POC.md`). Phase 1a already routes plugin *execution* through `window.main.plugins.*` IPC into a hidden BrowserWindow. Phase 1b moves plugin *discovery, loading, and the response context helper* fully into that hidden window so the renderer holds only metadata and bridge proxies. Re-use the existing `window.main.plugins.*` channel surface; do not invent a parallel discovery bridge. +Current state: `entry.preload.ts` snapshots all env vars at preload time and exposes them as `window.env`. `common/constants.ts` reads `window.env` with a `process.env` fallback. This works today because `contextIsolation: false` means the `else` branch (`window.env = env`) is taken. -Risk: high. Longest renderer code path that survived the earlier cleanup. +Issues to address before the contextIsolation flip: -Suggested reviewers: Plugins/templating, Electron/runtime. +1. **`src/insomnia-data/src/models/settings.ts:25`** — reads `process.env.PLAYWRIGHT_TEST` directly. Will be `undefined` (or throw) in a context-isolated renderer. Should read from `window.env.PLAYWRIGHT_TEST` or receive the value via a model initialisation argument. +2. **`constants.ts` fallback** — the `process[ENV]` fallback is intentional for the CLI, but in the renderer it should be a hard read from `window.env` (no fallback) so misconfiguration fails loudly rather than silently reading stale process env. +3. **`entry.preload.ts` surface audit** — review which env vars are actually needed by the renderer vs. the preload vs. the CLI, and drop anything not required from `window.env` (principle of least privilege). +4. **`plugins/index.ts:207,214`** — uses `process.env['HOME']` and `process.env['INSOMNIA_DATA_PATH']`. These run inside the hidden BrowserWindow which keeps `nodeIntegration: true`, so they are safe, but worth annotating explicitly. -### PR B: `src/network/network.ts` +Risk: low–medium. Mostly mechanical; risky only if any env var read is missed. -Last network offender. Two options: +--- -1. Lift `fs` / `path` use to a narrow main-side helper (`writeMultipartBody`, `resolveBodyFilePath`, etc.) exposed via existing `window.main` surface. -2. Or finish the pipeline-to-main move: push `sendCurlAndWriteTimeline`, `tryToInterpolateRequest`, and `tryToExecute*Script` into main and let the renderer call a single `window.main.executeRequest`. Cleaner endpoint, much larger PR. +## Still outstanding (pre-Phase-2) -Recommendation: do option 1 to unblock the flag flip; defer option 2 to post-flip cleanup. +These were scoped out of the Phase 1 PRs and are still needed before the contextIsolation flip: -Risk: medium. +| Item | Notes | +|---|---| +| `src/plugins/index.ts`, `src/plugins/create.ts`, `src/utils/plugin.ts` | Still import `fs`/`path`. Safe for now (run in hidden window), but the import baseline should reflect that explicitly. | +| `src/plugins/context/response.ts` | `fs`, `zlib`, `stream` — same as above. | +| `src/network/network.ts` | `fs`/`path` for multipart body and cert resolution. Candidate for a narrow `window.main` helper or deferral to post-Phase-2 cleanup. | +| `src/script-executor.ts` | One `appendFile` (`node:fs/promises`). Move behind `window.main.scriptLog.append` bridge. | +| `src/templating/base-extension.ts` | `crypto`, `os` — replace with Web Crypto and `window.main` os-info helper. | +| `packages/insomnia-testing` carve-out | `generate/generate.ts`, `run/run.ts` counted by analyzer but never loaded by renderer. Add to allow-list. | -Suggested reviewers: Network/gRPC, Electron/runtime. - -### PR C: `src/script-executor.ts` - -One `appendFile` from `node:fs/promises`. Move the file-write behind an existing or new narrow `window.main.scriptLog.append` bridge. Keep the rest of script orchestration in place. - -Risk: low. - -Suggested reviewers: Plugins/templating, Electron/runtime. - -### PR D: `src/templating/base-extension.ts` - -Replace `crypto` hashing with Web Crypto (`crypto.subtle`) where the algorithm allows, or expose a `window.main.hash` helper for legacy algorithms. Replace `os.hostname()` / `os.userInfo()` with values fetched once from main on startup and cached in renderer. - -Risk: low. - -Suggested reviewers: Plugins/templating, Electron/runtime. - -### PR E: inso carve-out - -Decide between: - -(a) adding `packages/insomnia-testing` to the analyzer's allow-list — it never runs in the renderer; or -(b) restructuring the package so the renderer-imported entrypoint does not transitively pull `run.ts` / `generate.ts`. - -Option (a) is simpler and matches reality. - -Risk: low. - -Suggested reviewers: Electron/runtime, repo maintainers. - -## The flip - -After PRs A–E land and the baseline file is empty (or reduced to intentionally allowed entries): - -- **File:** `src/main/window-utils.ts:199-208` -- **Change:** `nodeIntegration: true` → `false`. Leave `contextIsolation: false` for now (Phase 2). Keep `nodeIntegrationInWorker: false` (already correct — protects the Nunjucks worker sandbox). Hidden window stays on `nodeIntegration: true`. -- **Audit:** any preload surface added during the migration, particularly anything that writes to disk on behalf of the renderer (e.g. `writeResponseBodyToFile`). +--- ## Verification -- After each offender-removing PR: re-run `npm run update:renderer-node-import-baseline`, confirm the baseline shrinks, and commit the refreshed JSON in the same PR. - Per-PR: `npm run lint`, `npm run type-check`, `npm test`. -- Before the flag flip: full smoke run (`npm run test:smoke:dev`) covering plugin load, send-request, gRPC, OAuth, scripting, templating. -- After the flag flip: in a dev build, confirm `typeof process === 'undefined'` (or `process.type === undefined`) in the main renderer DevTools console, and that the hidden window still has full Node access. Re-run smoke. - -## Exit criteria - -1. The analyzer report contains no renderer Node builtin imports outside explicit allow-list entries. -2. `config/renderer-node-import-baseline.json` is empty or only contains intentionally permitted entries. -3. The main BrowserWindow runs with `nodeIntegration: false` without renderer regressions. -4. Security audit of any new preload surface introduced during the migration is complete. -5. Stretch follow-up: tighten `vite.config.ts` to fail rather than baseline; remove the baseline file entirely. +- After vault adapter lands: regression test encrypt/decrypt round-trip in a dev build; confirm no IPC calls to `vault.*` in DevTools. +- After env var second pass: confirm `window.env` is populated and `process.env` is inaccessible in a contextIsolation-enabled build. +- Before Phase 2 flag flip: full smoke run (`npm run test:smoke:dev`) covering plugin load, send-request, gRPC, OAuth, scripting, templating. +- After Phase 2 flag flip: in a dev build, confirm `typeof process === 'undefined'` in the renderer DevTools console; confirm hidden window retains full Node access. + +## Exit criteria (Phase 2) + +1. `contextIsolation: false` → `true` in `window-utils.ts` without renderer regressions. +2. All `window.env` reads in renderer code go through the contextBridge path; no renderer code touches `process.env` directly. +3. Vault crypto runs in the renderer via Web Crypto; IPC vault handlers removed from preload surface. +4. Security audit of the preload `contextBridge` surface complete (only necessary channels exposed). +5. Stretch: tighten `vite.config.ts` to fail (not baseline) on any new Node import in renderer code. From 51b9ba9ac3e9805b439c209c98000057926083a9 Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 13:23:17 +0200 Subject: [PATCH 10/47] insomnia testing adapter --- .../insomnia-inso/src/commands/__mocks__/insomnia-testing.ts | 1 - packages/insomnia-testing/src/generate/index.ts | 2 +- packages/insomnia-testing/src/index.ts | 1 - packages/insomnia/src/insomnia-testing.renderer.ts | 4 ++++ packages/insomnia/vite.config.ts | 2 ++ 5 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 packages/insomnia/src/insomnia-testing.renderer.ts diff --git a/packages/insomnia-inso/src/commands/__mocks__/insomnia-testing.ts b/packages/insomnia-inso/src/commands/__mocks__/insomnia-testing.ts index 1b75fbe6f005..dab593a88dc4 100644 --- a/packages/insomnia-inso/src/commands/__mocks__/insomnia-testing.ts +++ b/packages/insomnia-inso/src/commands/__mocks__/insomnia-testing.ts @@ -1,6 +1,5 @@ import { vi } from 'vitest'; export const generate = vi.fn(); -export const generateToFile = vi.fn(); export const runTests = vi.fn(); export const runTestsCli = vi.fn(); diff --git a/packages/insomnia-testing/src/generate/index.ts b/packages/insomnia-testing/src/generate/index.ts index 002df0c2217b..bde2b2d24285 100644 --- a/packages/insomnia-testing/src/generate/index.ts +++ b/packages/insomnia-testing/src/generate/index.ts @@ -1,6 +1,6 @@ import type { Test, TestSuite } from './generate'; export { generate } from './generate'; -export { generateToFile } from './generate-to-file'; + export type { Test, TestSuite }; diff --git a/packages/insomnia-testing/src/index.ts b/packages/insomnia-testing/src/index.ts index 129219b03472..2d4f982d6cc8 100644 --- a/packages/insomnia-testing/src/index.ts +++ b/packages/insomnia-testing/src/index.ts @@ -1,7 +1,6 @@ import type { Test, TestSuite } from './generate'; import type { TestResults } from './run'; export { generate } from './generate'; -export { generateToFile } from './generate/generate-to-file'; export { runTests, runTestsCli } from './run'; diff --git a/packages/insomnia/src/insomnia-testing.renderer.ts b/packages/insomnia/src/insomnia-testing.renderer.ts new file mode 100644 index 000000000000..49a078c75816 --- /dev/null +++ b/packages/insomnia/src/insomnia-testing.renderer.ts @@ -0,0 +1,4 @@ +// Renderer-safe subset of insomnia-testing. Excludes Node.js-only modules (run.ts, generate-to-file.ts). +export { generate } from '../../insomnia-testing/src/generate/generate'; +export type { Test, TestSuite } from '../../insomnia-testing/src/generate/generate'; +export type { TestResults } from '../../insomnia-testing/src/run/entities'; diff --git a/packages/insomnia/vite.config.ts b/packages/insomnia/vite.config.ts index 38d0d8801c23..2b7cae4f4f73 100644 --- a/packages/insomnia/vite.config.ts +++ b/packages/insomnia/vite.config.ts @@ -52,6 +52,8 @@ export default defineConfig(({ mode }) => { // These must appear before the '~' catch-all so the specific path wins. '~/network/network-adapter': path.resolve(__dirname, './src/network/network-adapter.renderer'), '~/templating/render-adapter': path.resolve(__dirname, './src/templating/render-adapter.renderer'), + // Redirect to a renderer-safe subset that excludes Node.js-only modules (run.ts, generate-to-file.ts). + 'insomnia-testing': path.resolve(__dirname, './src/insomnia-testing.renderer.ts'), '~': path.resolve(__dirname, './src'), // Shim Node's `path` module for browser-safe dependencies (e.g. mime-types uses path.extname). 'path': path.resolve(__dirname, './src/path-shim.ts'), From 7938df8801db86607e1e460d7ecaac67bd076bd2 Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 13:36:54 +0200 Subject: [PATCH 11/47] use export method --- packages/insomnia-testing/package.json | 6 ++++++ packages/insomnia-testing/src/browser.ts | 3 +++ packages/insomnia/src/insomnia-testing.renderer.ts | 4 ---- packages/insomnia/vite.config.ts | 2 -- 4 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 packages/insomnia-testing/src/browser.ts delete mode 100644 packages/insomnia/src/insomnia-testing.renderer.ts diff --git a/packages/insomnia-testing/package.json b/packages/insomnia-testing/package.json index d767e046ef59..759928e8105d 100644 --- a/packages/insomnia-testing/package.json +++ b/packages/insomnia-testing/package.json @@ -14,6 +14,12 @@ }, "main": "src/index.ts", "types": "src/index.ts", + "exports": { + ".": { + "browser": "./src/browser.ts", + "default": "./src/index.ts" + } + }, "scripts": { "lint": "eslint . --ext .js,.ts,.tsx --cache", "test": "vitest run", diff --git a/packages/insomnia-testing/src/browser.ts b/packages/insomnia-testing/src/browser.ts new file mode 100644 index 000000000000..9870b0090d2e --- /dev/null +++ b/packages/insomnia-testing/src/browser.ts @@ -0,0 +1,3 @@ +export { generate } from './generate/generate'; +export type { Test, TestSuite } from './generate/generate'; +export type { TestResults } from './run/entities'; diff --git a/packages/insomnia/src/insomnia-testing.renderer.ts b/packages/insomnia/src/insomnia-testing.renderer.ts deleted file mode 100644 index 49a078c75816..000000000000 --- a/packages/insomnia/src/insomnia-testing.renderer.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Renderer-safe subset of insomnia-testing. Excludes Node.js-only modules (run.ts, generate-to-file.ts). -export { generate } from '../../insomnia-testing/src/generate/generate'; -export type { Test, TestSuite } from '../../insomnia-testing/src/generate/generate'; -export type { TestResults } from '../../insomnia-testing/src/run/entities'; diff --git a/packages/insomnia/vite.config.ts b/packages/insomnia/vite.config.ts index 2b7cae4f4f73..38d0d8801c23 100644 --- a/packages/insomnia/vite.config.ts +++ b/packages/insomnia/vite.config.ts @@ -52,8 +52,6 @@ export default defineConfig(({ mode }) => { // These must appear before the '~' catch-all so the specific path wins. '~/network/network-adapter': path.resolve(__dirname, './src/network/network-adapter.renderer'), '~/templating/render-adapter': path.resolve(__dirname, './src/templating/render-adapter.renderer'), - // Redirect to a renderer-safe subset that excludes Node.js-only modules (run.ts, generate-to-file.ts). - 'insomnia-testing': path.resolve(__dirname, './src/insomnia-testing.renderer.ts'), '~': path.resolve(__dirname, './src'), // Shim Node's `path` module for browser-safe dependencies (e.g. mime-types uses path.extname). 'path': path.resolve(__dirname, './src/path-shim.ts'), From 799b31064f17dc6ab4b7f8cef4b21f332b22369a Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 14:00:55 +0200 Subject: [PATCH 12/47] trick react router ssr --- packages/insomnia/vite.config.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/insomnia/vite.config.ts b/packages/insomnia/vite.config.ts index 38d0d8801c23..439a316ecaf7 100644 --- a/packages/insomnia/vite.config.ts +++ b/packages/insomnia/vite.config.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import { reactRouter } from '@react-router/dev/vite'; import tailwindcss from '@tailwindcss/vite'; -import { defineConfig } from 'vite'; +import { defaultServerConditions, defineConfig } from 'vite'; import pkg from './package.json'; @@ -61,5 +61,16 @@ export default defineConfig(({ mode }) => { worker: { format: 'es', }, + // The Electron renderer is browser-like even in React Router's SSR (server) build. + // Vite's DEFAULT_SERVER_CONDITIONS excludes "browser", so packages with a + // "browser" exports condition (e.g. insomnia-testing) would otherwise resolve to + // their full Node entry point in the server bundle — pulling in Node-only modules + // like mocha. Prepending "browser" here keeps the server bundle consistent with + // the client build while retaining all other default server conditions. + ssr: { + resolve: { + conditions: ['browser', ...defaultServerConditions], + }, + }, }; }); From 26659eb4cfe65d336a97d14b668a5968ed6c96d7 Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 14:48:46 +0200 Subject: [PATCH 13/47] add renderer errors --- packages/insomnia-smoke-test/playwright/test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/insomnia-smoke-test/playwright/test.ts b/packages/insomnia-smoke-test/playwright/test.ts index d643d318b198..c5d7f60b1ff3 100644 --- a/packages/insomnia-smoke-test/playwright/test.ts +++ b/packages/insomnia-smoke-test/playwright/test.ts @@ -159,6 +159,14 @@ export const test = baseTest.extend<{ // firstWindow() always returns the main app window. const page = await app.firstWindow({ timeout: 60_000 }); + // Surface renderer errors so they appear in test output instead of being silent. + page.on('pageerror', err => console.error('[renderer pageerror]', err.message, err.stack)); + page.on('console', msg => { + if (msg.type() === 'error') { + console.error('[renderer console.error]', msg.text()); + } + }); + await page.waitForLoadState(); await use(page); From 7d243fd6d8a9df206eff719409b10c1a065c02f8 Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 15:36:14 +0200 Subject: [PATCH 14/47] globalThis --- .../src/ui/components/.client/codemirror/base-imports.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/insomnia/src/ui/components/.client/codemirror/base-imports.ts b/packages/insomnia/src/ui/components/.client/codemirror/base-imports.ts index 174ccf082220..4e34db5acaec 100644 --- a/packages/insomnia/src/ui/components/.client/codemirror/base-imports.ts +++ b/packages/insomnia/src/ui/components/.client/codemirror/base-imports.ts @@ -37,7 +37,7 @@ import 'codemirror/addon/merge/merge.js'; // for the code that uses this yaml parser, see https://github.com/codemirror/CodeMirror/blob/master/addon/lint/yaml-lint.js import * as jsyaml from 'js-yaml'; -global.jsyaml = jsyaml; +globalThis.jsyaml = jsyaml; import 'codemirror/addon/lint/yaml-lint'; /**/ import 'codemirror/keymap/vim'; From 907aaa59c16529b8e659bc549b519ca90ebd4f5c Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 15:59:21 +0200 Subject: [PATCH 15/47] improve error --- packages/insomnia/src/main/cloud-sync/core/vcs.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/insomnia/src/main/cloud-sync/core/vcs.ts b/packages/insomnia/src/main/cloud-sync/core/vcs.ts index 49a43f83a9a4..5abaf2666615 100644 --- a/packages/insomnia/src/main/cloud-sync/core/vcs.ts +++ b/packages/insomnia/src/main/cloud-sync/core/vcs.ts @@ -947,6 +947,10 @@ export class VCS { throw new Error(`Failed to query ${name}: ${errors[0].message}`); } + if (data == null) { + throw new Error(`Failed to query ${name}: no data returned`); + } + return data; } From 1e4251a1fc65acfacf64238bc8e2d3aa9c950e54 Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 16:45:30 +0200 Subject: [PATCH 16/47] move plugin types --- packages/insomnia/src/plugins/index.ts | 121 ++++++------------------- packages/insomnia/src/plugins/types.ts | 97 ++++++++++++++++++++ 2 files changed, 124 insertions(+), 94 deletions(-) diff --git a/packages/insomnia/src/plugins/index.ts b/packages/insomnia/src/plugins/index.ts index 02871e48cff7..66833d9e30cf 100644 --- a/packages/insomnia/src/plugins/index.ts +++ b/packages/insomnia/src/plugins/index.ts @@ -6,107 +6,40 @@ import type { GrpcRequest, Request, RequestGroup, SocketIORequest, WebSocketRequ import { database as db, models, services } from 'insomnia-data'; import type { PluginConfigMap } from 'insomnia-data/common'; + import { fetchFromTemplateWorkerDatabase } from '~/templating/liquid-extension-worker'; -import type { ParsedApiSpec } from '../common/api-specs'; import { getAppBundlePlugins, isDevelopment } from '../common/constants'; import * as pluginApp from '../plugins/context/app'; import * as pluginNetwork from '../plugins/context/network'; import * as pluginStore from '../plugins/context/store'; -import type { PluginTemplateTag, RenderPurpose } from '../templating/types'; -import type { PluginTheme } from './misc'; +import type { RenderPurpose } from '../templating/types'; import themes from './themes'; - -export interface Plugin { - name: string; - description: string; - version: string; - directory: string; - config: { disabled: boolean }; - module: { - templateTags?: PluginTemplateTag[]; - requestHooks?: ((requestContext: any) => void)[]; - responseHooks?: ((responseContext: any) => void)[]; - themes?: PluginTheme[]; - requestGroupActions?: OmitInternal[]; - requestActions?: OmitInternal[]; - workspaceActions?: OmitInternal[]; - documentActions?: OmitInternal[]; - // Plugin actions which will be executed in main process(node integration) context. For internal use only, not for public plugins - unsafePluginMainActions?: OmitInternal[]; - }; -} - -type OmitInternal = Omit; -export type TemplateTag = { plugin: Plugin } & { - templateTag: PluginTemplateTag; -}; - -export type RequestGroupAction = { plugin: Plugin } & { - action: ( - context: Record, - models: { - requestGroup: RequestGroup; - requests: (Request | GrpcRequest | WebSocketRequest)[]; - }, - ) => void | Promise; - label: string; - icon?: string; -}; - -export type RequestAction = { plugin: Plugin } & { - action: ( - context: Record, - models: { - requestGroup?: RequestGroup; - request: Request | GrpcRequest | WebSocketRequest | SocketIORequest; - }, - ) => void | Promise; - label: string; - icon?: string; -}; - -export type WorkspaceAction = { plugin: Plugin } & { - action: ( - context: Record, - models: { - workspace: Workspace; - requestGroups: RequestGroup[]; - requests: Request[]; - }, - ) => void | Promise; - label: string; - icon?: string; -}; - -export type DocumentAction = { plugin: Plugin } & { - action: (context: Record, documents: ParsedApiSpec) => void | Promise; - label: string; - hideAfterClick?: boolean; -}; - -export type PluginAction = { plugin: Plugin } & { - name: string; - description?: string; - action: (context: Record, params?: any) => Promise; -}; - -type RequestHookCallback = (context: any) => void; - -export type RequestHook = { plugin: Plugin } & { - hook: RequestHookCallback; -}; - -type ResponseHookCallback = (context: any) => void; -export type ResponseHook = { plugin: Plugin } & { - hook: ResponseHookCallback; -}; - -export type Theme = { plugin: Plugin } & { - theme: PluginTheme; -}; - -export type ColorScheme = 'default' | 'light' | 'dark'; +import type { + DocumentAction, + Plugin, + RequestAction, + RequestGroupAction, + RequestHook, + ResponseHook, + TemplateTag, + Theme, + WorkspaceAction, +} from './types'; + +export type { + ColorScheme, + DocumentAction, + Plugin, + PluginAction, + RequestAction, + RequestGroupAction, + RequestHook, + ResponseHook, + TemplateTag, + Theme, + WorkspaceAction, +} from './types'; let plugins: Plugin[] | null | undefined = null; diff --git a/packages/insomnia/src/plugins/types.ts b/packages/insomnia/src/plugins/types.ts index 3c71cc8f97ec..cc3c83a0ff1a 100644 --- a/packages/insomnia/src/plugins/types.ts +++ b/packages/insomnia/src/plugins/types.ts @@ -1,3 +1,9 @@ +import type { GrpcRequest, Request, RequestGroup, SocketIORequest, WebSocketRequest, Workspace } from '~/insomnia-data'; + +import type { ParsedApiSpec } from '../common/api-specs'; +import type { PluginTemplateTag } from '../templating/types'; +import type { PluginTheme } from './misc'; + // shared types for private plugins export interface ModelConfig { @@ -57,3 +63,94 @@ export type GenerateMcpSamplingResponseFunction = (parameters: { messages: MultiTurnMessage[]; modelConfig: Pick; }) => Promise<{ content: string; modelConfig: ModelConfig }>; + +export interface Plugin { + name: string; + description: string; + version: string; + directory: string; + config: { disabled: boolean }; + module: { + templateTags?: PluginTemplateTag[]; + requestHooks?: ((requestContext: any) => void)[]; + responseHooks?: ((responseContext: any) => void)[]; + themes?: PluginTheme[]; + requestGroupActions?: OmitInternal[]; + requestActions?: OmitInternal[]; + workspaceActions?: OmitInternal[]; + documentActions?: OmitInternal[]; + // Plugin actions which will be executed in main process(node integration) context. For internal use only, not for public plugins + unsafePluginMainActions?: OmitInternal[]; + }; +} + +type OmitInternal = Omit; +export type TemplateTag = { plugin: Plugin } & { + templateTag: PluginTemplateTag; +}; + +export type RequestGroupAction = { plugin: Plugin } & { + action: ( + context: Record, + models: { + requestGroup: RequestGroup; + requests: (Request | GrpcRequest | WebSocketRequest)[]; + }, + ) => void | Promise; + label: string; + icon?: string; +}; + +export type RequestAction = { plugin: Plugin } & { + action: ( + context: Record, + models: { + requestGroup?: RequestGroup; + request: Request | GrpcRequest | WebSocketRequest | SocketIORequest; + }, + ) => void | Promise; + label: string; + icon?: string; +}; + +export type WorkspaceAction = { plugin: Plugin } & { + action: ( + context: Record, + models: { + workspace: Workspace; + requestGroups: RequestGroup[]; + requests: Request[]; + }, + ) => void | Promise; + label: string; + icon?: string; +}; + +export type DocumentAction = { plugin: Plugin } & { + action: (context: Record, documents: ParsedApiSpec) => void | Promise; + label: string; + hideAfterClick?: boolean; +}; + +export type PluginAction = { plugin: Plugin } & { + name: string; + description?: string; + action: (context: Record, params?: any) => Promise; +}; + +type RequestHookCallback = (context: any) => void; + +export type RequestHook = { plugin: Plugin } & { + hook: RequestHookCallback; +}; + +type ResponseHookCallback = (context: any) => void; +export type ResponseHook = { plugin: Plugin } & { + hook: ResponseHookCallback; +}; + +export type Theme = { plugin: Plugin } & { + theme: PluginTheme; +}; + +export type ColorScheme = 'default' | 'light' | 'dark'; From 198517d43f85ec8720828bcc63a15ce5c760ce9c Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 16:48:06 +0200 Subject: [PATCH 17/47] ipc validate proto --- packages/insomnia/src/entry.preload.ts | 1 + packages/insomnia/src/main/ipc/electron.ts | 1 + packages/insomnia/src/main/ipc/grpc.ts | 6 ++++++ .../src/ui/components/modals/proto-files-modal.tsx | 12 ++---------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/insomnia/src/entry.preload.ts b/packages/insomnia/src/entry.preload.ts index 93a053293c01..d9165ac3a648 100644 --- a/packages/insomnia/src/entry.preload.ts +++ b/packages/insomnia/src/entry.preload.ts @@ -124,6 +124,7 @@ const grpc: gRPCBridgeAPI = { loadMethods: options => invokeWithNormalizedError('grpc.loadMethods', options), loadMethodsFromReflection: options => invokeWithNormalizedError('grpc.loadMethodsFromReflection', options), writeProtoFile: protoFileId => invokeWithNormalizedError('grpc.writeProtoFile', protoFileId), + validateProtoFile: filePath => invokeWithNormalizedError('grpc.validateProtoFile', filePath), }; const secretStorage: secretStorageBridgeAPI = { diff --git a/packages/insomnia/src/main/ipc/electron.ts b/packages/insomnia/src/main/ipc/electron.ts index 362d93059831..3021d27215fa 100644 --- a/packages/insomnia/src/main/ipc/electron.ts +++ b/packages/insomnia/src/main/ipc/electron.ts @@ -84,6 +84,7 @@ export type HandleChannels = | 'grpc.loadMethods' | 'grpc.loadMethodsFromReflection' | 'grpc.writeProtoFile' + | 'grpc.validateProtoFile' | 'initializeWorkspaceBackendProject' | 'insecureReadFile' | 'insecureReadFileWithEncoding' diff --git a/packages/insomnia/src/main/ipc/grpc.ts b/packages/insomnia/src/main/ipc/grpc.ts index c8880821b809..a20b12a080ce 100644 --- a/packages/insomnia/src/main/ipc/grpc.ts +++ b/packages/insomnia/src/main/ipc/grpc.ts @@ -61,6 +61,7 @@ export interface gRPCBridgeAPI { loadMethodsFromReflection: typeof loadMethodsFromReflection; closeAll: typeof closeAll; writeProtoFile: (protoFileId: string) => Promise<{ filePath: string; dirs: string[] }>; + validateProtoFile: (filePath: string) => Promise; } const grpcOptions = { @@ -82,6 +83,10 @@ export const writeProtoFileById = async (protoFileId: string): Promise<{ filePat return result; }; +export const validateProtoFileByPath = async (filePath: string): Promise => { + await protoLoader.load(filePath, grpcOptions); +}; + export function registergRPCHandlers() { ipcMainOn('grpc.start', start); ipcMainOn('grpc.sendMessage', sendMessage); @@ -91,6 +96,7 @@ export function registergRPCHandlers() { ipcMainHandle('grpc.loadMethods', (_, requestId) => loadMethods(requestId)); ipcMainHandle('grpc.loadMethodsFromReflection', (_, requestId) => loadMethodsFromReflection(requestId)); ipcMainHandle('grpc.writeProtoFile', (_, protoFileId: string) => writeProtoFileById(protoFileId)); + ipcMainHandle('grpc.validateProtoFile', (_, filePath: string) => validateProtoFileByPath(filePath)); } const loadMethodsFromFilePath = async (filePath: string, includeDirs: string[]): Promise => { diff --git a/packages/insomnia/src/ui/components/modals/proto-files-modal.tsx b/packages/insomnia/src/ui/components/modals/proto-files-modal.tsx index 65229537395c..08655ec98dde 100644 --- a/packages/insomnia/src/ui/components/modals/proto-files-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/proto-files-modal.tsx @@ -1,6 +1,4 @@ -import * as protoLoader from '@grpc/proto-loader'; -import type { ProtoDirectory, ProtoFile } from 'insomnia-data'; -import { models, services } from 'insomnia-data'; +import { models, type ProtoDirectory, type ProtoFile, services } from 'insomnia-data'; import React, { type FC, useEffect, useRef, useState } from 'react'; import { useParams } from 'react-router'; @@ -48,13 +46,7 @@ const tryToSelectFolderPath = async () => { }; const isProtofileValid = async (filePath: string) => { try { - await protoLoader.load(filePath, { - keepCase: true, - longs: String, - enums: String, - defaults: true, - oneofs: true, - }); + await window.main.grpc.validateProtoFile(filePath); return true; } catch (error) { showError({ From 2c599048310f1973a2752189bf1ef10115627add Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 17:04:41 +0200 Subject: [PATCH 18/47] fix import --- packages/insomnia/src/routes/git-credentials.init-sign-in.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/insomnia/src/routes/git-credentials.init-sign-in.tsx b/packages/insomnia/src/routes/git-credentials.init-sign-in.tsx index 124903742855..86f9b513cab6 100644 --- a/packages/insomnia/src/routes/git-credentials.init-sign-in.tsx +++ b/packages/insomnia/src/routes/git-credentials.init-sign-in.tsx @@ -1,6 +1,6 @@ import { href } from 'react-router'; -import type { GitRemoteProviderType } from '~/sync/git/providers'; +import type { GitRemoteProviderType } from '~/sync/git/providers/types'; import { createFetcherSubmitHook } from '~/utils/router'; import type { Route } from './+types/git-credentials.init-sign-in'; From 9f7c7b5431d3d0efd9eb62d7fb38a8f8c30445b6 Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 19:53:12 +0200 Subject: [PATCH 19/47] plugin types --- .../src/main/templating-worker-database.ts | 14 ++---- .../plugins/__tests__/invoke-method.test.ts | 6 ++- .../src/plugins/__tests__/themes.test.ts | 2 +- .../insomnia/src/plugins/context/store.ts | 2 +- packages/insomnia/src/plugins/index.ts | 35 +++----------- .../insomnia/src/plugins/invoke-method.ts | 2 +- packages/insomnia/src/plugins/misc.ts | 2 +- .../insomnia/src/templating/liquid-engine.ts | 12 +++-- .../src/templating/liquid-extension-worker.ts | 2 +- .../src/templating/liquid-extension.ts | 2 +- .../src/templating/local-template-tags.ts | 46 ++++++++++--------- packages/insomnia/src/templating/worker.ts | 2 +- packages/insomnia/src/ui/hooks/theme.ts | 2 +- packages/insomnia/vite.config.ts | 4 +- 14 files changed, 57 insertions(+), 76 deletions(-) diff --git a/packages/insomnia/src/main/templating-worker-database.ts b/packages/insomnia/src/main/templating-worker-database.ts index f9c233dce258..36254d8e4302 100644 --- a/packages/insomnia/src/main/templating-worker-database.ts +++ b/packages/insomnia/src/main/templating-worker-database.ts @@ -4,24 +4,18 @@ import os from 'node:os'; import { shell } from 'electron'; import iconv from 'iconv-lite'; -import type { - AllTypes, - CloudProviderCredential, - Request as DBRequest, - RequestGroup, - Response, - Workspace, -} from 'insomnia-data'; -import { services } from 'insomnia-data'; import { v4 as uuidv4 } from 'uuid'; import { jarFromCookies } from '~/common/cookies'; +import type { AllTypes, CloudProviderCredential, Request as DBRequest, RequestGroup, Workspace } from '~/insomnia-data'; +import { services } from '~/insomnia-data'; +import { getPluginCommonContext } from '~/plugins'; import { getAppBundlePlugins, RESPONSE_CODE_REASONS } from '../common/constants'; import { isDevelopment } from '../common/constants'; import { database as db } from '../common/database'; import { fetchRequestData, sendCurlAndWriteTimeline, tryToInterpolateRequest } from '../network/network'; -import { getPluginCommonContext, type Plugin, type TemplateTag } from '../plugins'; +import { type Plugin, type TemplateTag } from '../plugins/types'; import type { PluginTemplateTag, PluginTemplateTagContext, PluginToMainAPIPaths } from '../templating/types'; import { curlRequest } from './network/libcurl-promise'; import { secureReadFile } from './secure-read-file'; diff --git a/packages/insomnia/src/plugins/__tests__/invoke-method.test.ts b/packages/insomnia/src/plugins/__tests__/invoke-method.test.ts index 70b271709dd2..2eb75379cca7 100644 --- a/packages/insomnia/src/plugins/__tests__/invoke-method.test.ts +++ b/packages/insomnia/src/plugins/__tests__/invoke-method.test.ts @@ -20,9 +20,9 @@ vi.mock('insomnia-data', () => ({ }, })); -import type { Plugin } from '../index'; import { _testOnlySetPlugins } from '../index'; import { invokePluginMethod } from '../invoke-method'; +import type { Plugin } from '../types'; const makePlugin = (overrides: Partial = {}): Plugin => ({ name: 'test-plugin', @@ -41,7 +41,9 @@ afterEach(() => { describe('invokePluginMethod', () => { it('serializes request action metadata', async () => { - _testOnlySetPlugins([makePlugin({ module: { requestActions: [{ label: 'Run', icon: 'bolt', action: vi.fn() }] } })]); + _testOnlySetPlugins([ + makePlugin({ module: { requestActions: [{ label: 'Run', icon: 'bolt', action: vi.fn() }] } }), + ]); await expect(invokePluginMethod('getRequestActions')).resolves.toEqual([ { label: 'Run', icon: 'bolt', pluginName: 'test-plugin' }, diff --git a/packages/insomnia/src/plugins/__tests__/themes.test.ts b/packages/insomnia/src/plugins/__tests__/themes.test.ts index c786d2d85548..6202e21b8da3 100644 --- a/packages/insomnia/src/plugins/__tests__/themes.test.ts +++ b/packages/insomnia/src/plugins/__tests__/themes.test.ts @@ -1,8 +1,8 @@ import { afterEach, describe, expect, it } from 'vitest'; -import type { Plugin } from '../index'; // No mock of '../themes' here — this file tests the built-in theme baseline. import { _testOnlySetPlugins, getThemes } from '../index'; +import type { Plugin } from '../types'; const makePlugin = (overrides: Partial = {}): Plugin => ({ name: 'test-plugin', diff --git a/packages/insomnia/src/plugins/context/store.ts b/packages/insomnia/src/plugins/context/store.ts index 6073b291ad4f..f5116ec6aeac 100644 --- a/packages/insomnia/src/plugins/context/store.ts +++ b/packages/insomnia/src/plugins/context/store.ts @@ -1,7 +1,7 @@ import { services } from 'insomnia-data'; import type { PluginStore } from '../../templating/types'; -import type { Plugin } from '../index'; +import type { Plugin } from '../types'; export function init(plugin: Pick): { store: PluginStore } { return { diff --git a/packages/insomnia/src/plugins/index.ts b/packages/insomnia/src/plugins/index.ts index 66833d9e30cf..0efd9e859fdf 100644 --- a/packages/insomnia/src/plugins/index.ts +++ b/packages/insomnia/src/plugins/index.ts @@ -1,12 +1,9 @@ import fs from 'node:fs'; import path from 'node:path'; -import electron from 'electron'; -import type { GrpcRequest, Request, RequestGroup, SocketIORequest, WebSocketRequest, Workspace } from 'insomnia-data'; -import { database as db, models, services } from 'insomnia-data'; -import type { PluginConfigMap } from 'insomnia-data/common'; - - +import type { Request, RequestGroup, Workspace } from '~/insomnia-data'; +import { database as db, models, services } from '~/insomnia-data'; +import type { PluginConfigMap } from '~/insomnia-data/common'; import { fetchFromTemplateWorkerDatabase } from '~/templating/liquid-extension-worker'; import { getAppBundlePlugins, isDevelopment } from '../common/constants'; @@ -27,20 +24,6 @@ import type { WorkspaceAction, } from './types'; -export type { - ColorScheme, - DocumentAction, - Plugin, - PluginAction, - RequestAction, - RequestGroupAction, - RequestHook, - ResponseHook, - TemplateTag, - Theme, - WorkspaceAction, -} from './types'; - let plugins: Plugin[] | null | undefined = null; export function _testOnlySetPlugins(p: Plugin[] | null) { @@ -143,10 +126,9 @@ export async function getPlugins(force = false): Promise { }); // Make sure the default directories exist - const pluginPath = path.resolve( - process.env['INSOMNIA_DATA_PATH'] || (process.type === 'renderer' ? window : electron).app.getPath('userData'), - 'plugins', - ); + // imported lazy becuase this should only be read in plugin hidden window + const electron = await import('electron'); + const pluginPath = path.resolve(process.env['INSOMNIA_DATA_PATH'] || electron.app.getPath('userData'), 'plugins'); // Also look in node_modules folder in each directory const basePaths = [pluginPath, ...extraPaths]; @@ -313,10 +295,7 @@ export function getPluginCommonContext({ ...pluginStore.init(plugin), ...pluginNetwork.init(), util: { - openInBrowser: async (url: string) => - process.type === 'renderer' || process.type === 'worker' - ? window.main.openInBrowser(url) - : electron.shell.openExternal(url), + openInBrowser: async (url: string) => await import('electron').then(electron => electron.shell.openExternal(url)), models: { request: { getById: services.request.getById, diff --git a/packages/insomnia/src/plugins/invoke-method.ts b/packages/insomnia/src/plugins/invoke-method.ts index 214c4d805f65..ef4ade990b0f 100644 --- a/packages/insomnia/src/plugins/invoke-method.ts +++ b/packages/insomnia/src/plugins/invoke-method.ts @@ -11,7 +11,6 @@ import * as pluginNetwork from './context/network'; import * as pluginRequest from './context/request'; import * as pluginResponse from './context/response'; import * as pluginStore from './context/store'; -import type { Plugin } from './index'; import { executePluginMainAction, getActivePlugins, @@ -27,6 +26,7 @@ import { getWorkspaceActions, reloadPlugins, } from './index'; +import type { Plugin } from './types'; export type PluginInvokeMethod = | 'getThemes' diff --git a/packages/insomnia/src/plugins/misc.ts b/packages/insomnia/src/plugins/misc.ts index 7fd0ae32d418..ecba41d22911 100644 --- a/packages/insomnia/src/plugins/misc.ts +++ b/packages/insomnia/src/plugins/misc.ts @@ -13,8 +13,8 @@ import type { ThemeInner, } from './bridge-types'; export type { HexColor, PluginTheme, RGBAColor, RGBColor, StylesThemeBlocks, ThemeBlock, ThemeColor, ThemeInner }; -import type { ColorScheme } from './index'; import { plugins } from './renderer-bridge'; +import type { ColorScheme } from './types'; export interface CompleteStyleBlock { background: Required['background']>; diff --git a/packages/insomnia/src/templating/liquid-engine.ts b/packages/insomnia/src/templating/liquid-engine.ts index 0d7918034c6c..9a0bd4b60640 100644 --- a/packages/insomnia/src/templating/liquid-engine.ts +++ b/packages/insomnia/src/templating/liquid-engine.ts @@ -1,7 +1,7 @@ import type { Tag } from 'liquidjs'; import { Liquid, Tag as LiquidTag } from 'liquidjs'; -import type { Plugin } from '../plugins/index'; +import type { Plugin } from '../plugins/types'; import type { PluginTemplateTag } from './types'; export type TagFactory = (ext: PluginTemplateTag, plugin: Plugin) => typeof Tag; @@ -30,11 +30,11 @@ export function buildLiquidEngine(opts: { tagDelimiterLeft: '{%', tagDelimiterRight: '%}', strictVariables, - strictFilters: true, // Enabling for 13.0.0 to catch nonexistent filters. + strictFilters: true, // Enabling for 13.0.0 to catch nonexistent filters. jsTruthy: true, // Required to match Nunjucks JS truthiness: '', 0, [] are falsy - ownPropertyOnly: true, // Contexts are plain objects + ownPropertyOnly: true, // Contexts are plain objects dynamicPartials: false, // Disable dynamic paths to prevent variable-interpolated includes. - + // hard-stop rendering after 10 s and cap object allocations. renderLimit: 10_000, memoryLimit: 10_000_000, @@ -44,7 +44,9 @@ export function buildLiquidEngine(opts: { // which routes through window.main.secureReadFile (path allowlist). class BlockedFileTag extends LiquidTag { render(): void { - throw new Error('{% include %}, {% render %}, and {% layout %} are disabled. Use the File template tag to read files.'); + throw new Error( + '{% include %}, {% render %}, and {% layout %} are disabled. Use the File template tag to read files.', + ); } } engine.registerTag('include', BlockedFileTag); diff --git a/packages/insomnia/src/templating/liquid-extension-worker.ts b/packages/insomnia/src/templating/liquid-extension-worker.ts index a9824e153727..860b85a28db5 100644 --- a/packages/insomnia/src/templating/liquid-extension-worker.ts +++ b/packages/insomnia/src/templating/liquid-extension-worker.ts @@ -3,7 +3,7 @@ import type { Context, Emitter, Liquid, TagToken, TopLevelToken } from 'liquidjs import { Tag } from 'liquidjs'; import packageJson from '../../package.json'; -import type { Plugin } from '../plugins/index'; +import type { Plugin } from '../plugins/types'; import { tokenizeArgs } from './tokenize-args'; import type { BaseRenderContext, diff --git a/packages/insomnia/src/templating/liquid-extension.ts b/packages/insomnia/src/templating/liquid-extension.ts index a5b34abac0b2..818222d7a229 100644 --- a/packages/insomnia/src/templating/liquid-extension.ts +++ b/packages/insomnia/src/templating/liquid-extension.ts @@ -14,7 +14,7 @@ import { database as db } from '../common/database'; import * as pluginApp from '../plugins/context/app'; import * as pluginNetwork from '../plugins/context/network'; import * as pluginStore from '../plugins/context/store'; -import type { Plugin } from '../plugins/index'; +import type { Plugin } from '../plugins/types'; import type { BaseRenderContext, PluginTemplateTag, PluginTemplateTagContext } from './types'; import { decodeEncoding, tokenizeArgs } from './utils'; diff --git a/packages/insomnia/src/templating/local-template-tags.ts b/packages/insomnia/src/templating/local-template-tags.ts index 7536a3adff14..5e82b5b773eb 100644 --- a/packages/insomnia/src/templating/local-template-tags.ts +++ b/packages/insomnia/src/templating/local-template-tags.ts @@ -1,5 +1,5 @@ import { format } from 'date-fns'; -import type { TemplateTag } from 'insomnia/src/plugins'; +import type { TemplateTag } from 'insomnia/src/plugins/types'; import type { PluginTemplateTag } from 'insomnia/src/templating/types'; import { invariant } from 'insomnia/src/utils/invariant'; import JSONBig from 'json-bigint'; @@ -747,30 +747,34 @@ const localTemplatePlugins: { templateTag: PluginTemplateTag }[] = [ let results: { outer: string; inner: string | null }[] = []; // Functions return plain strings, numbers, or a boolean—depending on the function. - if (typeof selectedValues === 'string' || typeof selectedValues === 'number' || typeof selectedValues === 'boolean') { + if ( + typeof selectedValues === 'string' || + typeof selectedValues === 'number' || + typeof selectedValues === 'boolean' + ) { const str = String(selectedValues); results = [{ outer: str, inner: str }]; } else { results = (selectedValues as Node[]) - .filter( - sv => - sv.nodeType === Node.ATTRIBUTE_NODE || - sv.nodeType === Node.ELEMENT_NODE || - sv.nodeType === Node.TEXT_NODE, - ) - .map(selectedValue => { - const outer = selectedValue.toString().trim(); - if (selectedValue.nodeType === Node.ATTRIBUTE_NODE) { - return { outer, inner: selectedValue.nodeValue }; - } - if (selectedValue.nodeType === Node.ELEMENT_NODE) { - return { outer, inner: selectedValue.childNodes.toString() }; - } - if (selectedValue.nodeType === Node.TEXT_NODE) { - return { outer, inner: selectedValue.toString().trim() }; - } - return { outer, inner: null }; - }); + .filter( + sv => + sv.nodeType === Node.ATTRIBUTE_NODE || + sv.nodeType === Node.ELEMENT_NODE || + sv.nodeType === Node.TEXT_NODE, + ) + .map(selectedValue => { + const outer = selectedValue.toString().trim(); + if (selectedValue.nodeType === Node.ATTRIBUTE_NODE) { + return { outer, inner: selectedValue.nodeValue }; + } + if (selectedValue.nodeType === Node.ELEMENT_NODE) { + return { outer, inner: selectedValue.childNodes.toString() }; + } + if (selectedValue.nodeType === Node.TEXT_NODE) { + return { outer, inner: selectedValue.toString().trim() }; + } + return { outer, inner: null }; + }); } if (results.length === 0) { diff --git a/packages/insomnia/src/templating/worker.ts b/packages/insomnia/src/templating/worker.ts index a1ff1cf42060..8538a4a940f1 100644 --- a/packages/insomnia/src/templating/worker.ts +++ b/packages/insomnia/src/templating/worker.ts @@ -2,7 +2,7 @@ import type { Liquid } from 'liquidjs'; import { localTemplateTags } from '~/templating/local-template-tags'; -import type { TemplateTag } from '../plugins'; +import type { TemplateTag } from '../plugins/types'; import { LIQUID_TEMPLATE_GLOBAL_PROPERTY_NAME, NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME } from './constants'; import { buildLiquidEngine, stripLiquidComments } from './liquid-engine'; import { createLiquidTagWorker, fetchFromTemplateWorkerDatabase } from './liquid-extension-worker'; diff --git a/packages/insomnia/src/ui/hooks/theme.ts b/packages/insomnia/src/ui/hooks/theme.ts index 3e0b49e9c3eb..2afbd356f95d 100644 --- a/packages/insomnia/src/ui/hooks/theme.ts +++ b/packages/insomnia/src/ui/hooks/theme.ts @@ -5,9 +5,9 @@ import * as reactUse from 'react-use'; import { useRootLoaderData } from '~/root'; import { AnalyticsEvent } from '~/ui/analytics'; -import { type ColorScheme } from '../../plugins'; import { applyColorScheme, getColorScheme, type PluginTheme } from '../../plugins/misc'; import { plugins } from '../../plugins/renderer-bridge'; +import { type ColorScheme } from '../../plugins/types'; import { useSettingsPatcher } from './use-request'; export const useThemes = () => { diff --git a/packages/insomnia/vite.config.ts b/packages/insomnia/vite.config.ts index 439a316ecaf7..3d1ee0cf78ad 100644 --- a/packages/insomnia/vite.config.ts +++ b/packages/insomnia/vite.config.ts @@ -37,11 +37,11 @@ export default defineConfig(({ mode }) => { target: 'esnext', sourcemap: true, rollupOptions: { - external: ['@getinsomnia/node-libcurl'], + external: ['@getinsomnia/node-libcurl', 'electron', /^electron\//], }, }, optimizeDeps: { - exclude: ['@getinsomnia/node-libcurl'], + exclude: ['@getinsomnia/node-libcurl', 'electron'], force: true, // wipe vite cache include: ['codemirror-graphql/utils/SchemaReference', '@stoplight/spectral-core', 'isomorphic-git', 'json-bigint'], }, From 8b30634e858eb919b1359add9d1c8e457e995558 Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 20:32:39 +0200 Subject: [PATCH 20/47] polyfill events for jshint --- package-lock.json | 10 ++++++++++ packages/insomnia/package.json | 5 +++-- packages/insomnia/vite.config.ts | 2 ++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index c033f9f763d6..4b8bcd6ee55c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16208,6 +16208,15 @@ "license": "MIT", "optional": true }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -29201,6 +29210,7 @@ "dompurify": "^3.4.1", "electron-context-menu": "^3.6.1", "electron-updater": "^6.6.2", + "events": "^3.3.0", "fastq": "^1.19.1", "fflate": "^0.8.2", "fuzzysort": "^1.9.0", diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index b3a107aee723..0141550301ac 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -20,7 +20,7 @@ "verify-bundle-plugins": "esr --cache ./scripts/verify-bundle-plugins.ts", "install-x64-native-dependencies": "esr --cache ./scripts/install-x64-native-dependencies.ts", "build": "react-router build && esr --cache ./scripts/build.ts --noErrorTruncation", -"build:react-router": "react-router build", + "build:react-router": "react-router build", "generate:schema": "esr ./src/schema.ts", "build:electron-entrypoints": "cross-env NODE_ENV=development esr esbuild.entrypoints.ts", "lint": "eslint . --ext .js,.ts,.tsx --cache", @@ -93,6 +93,7 @@ "dompurify": "^3.4.1", "electron-context-menu": "^3.6.1", "electron-updater": "^6.6.2", + "events": "^3.3.0", "fastq": "^1.19.1", "fflate": "^0.8.2", "fuzzysort": "^1.9.0", @@ -118,13 +119,13 @@ "json-order": "^1.1.3", "jsonlint-mod-fixed": "1.7.7", "jsonpath-plus": "^10.3.0", + "liquidjs": "^10.27.0", "marked": "^5.1.2", "mime-types": "^2.1.35", "mocha": "^11.7.5", "monaco-editor": "^0.52.2", "multiparty": "^4.2.3", "node-forge": "^1.3.1", - "liquidjs": "^10.27.0", "oauth-1.0a": "^2.2.6", "objectpath": "^2.0.0", "papaparse": "^5.5.2", diff --git a/packages/insomnia/vite.config.ts b/packages/insomnia/vite.config.ts index 3d1ee0cf78ad..62602cba2ade 100644 --- a/packages/insomnia/vite.config.ts +++ b/packages/insomnia/vite.config.ts @@ -55,6 +55,8 @@ export default defineConfig(({ mode }) => { '~': path.resolve(__dirname, './src'), // Shim Node's `path` module for browser-safe dependencies (e.g. mime-types uses path.extname). 'path': path.resolve(__dirname, './src/path-shim.ts'), + // Shim Node's `events` module for browser-safe dependencies (e.g. jshint uses EventEmitter). + 'events': path.resolve(__dirname, '../../node_modules/events'), }, }, plugins: [reactRouter(), tailwindcss()], From e6f1c57d575f23a8e49df91f62417f327040a4b6 Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 21:39:55 +0200 Subject: [PATCH 21/47] restore node require plugin --- packages/insomnia/src/plugins/index.ts | 6 +- packages/insomnia/vite.config.ts | 135 ++++++++++++++++++++++++- 2 files changed, 135 insertions(+), 6 deletions(-) diff --git a/packages/insomnia/src/plugins/index.ts b/packages/insomnia/src/plugins/index.ts index 0efd9e859fdf..cc7ad7fa31be 100644 --- a/packages/insomnia/src/plugins/index.ts +++ b/packages/insomnia/src/plugins/index.ts @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import electron from 'electron'; + import type { Request, RequestGroup, Workspace } from '~/insomnia-data'; import { database as db, models, services } from '~/insomnia-data'; import type { PluginConfigMap } from '~/insomnia-data/common'; @@ -127,7 +129,7 @@ export async function getPlugins(force = false): Promise { // Make sure the default directories exist // imported lazy becuase this should only be read in plugin hidden window - const electron = await import('electron'); + const pluginPath = path.resolve(process.env['INSOMNIA_DATA_PATH'] || electron.app.getPath('userData'), 'plugins'); // Also look in node_modules folder in each directory @@ -295,7 +297,7 @@ export function getPluginCommonContext({ ...pluginStore.init(plugin), ...pluginNetwork.init(), util: { - openInBrowser: async (url: string) => await import('electron').then(electron => electron.shell.openExternal(url)), + openInBrowser: (url: string) => electron.shell.openExternal(url), models: { request: { getById: services.request.getById, diff --git a/packages/insomnia/vite.config.ts b/packages/insomnia/vite.config.ts index 62602cba2ade..200e38252cf1 100644 --- a/packages/insomnia/vite.config.ts +++ b/packages/insomnia/vite.config.ts @@ -1,3 +1,4 @@ +import { builtinModules } from 'node:module'; import path from 'node:path'; import { reactRouter } from '@react-router/dev/vite'; @@ -5,7 +6,8 @@ import tailwindcss from '@tailwindcss/vite'; import { defaultServerConditions, defineConfig } from 'vite'; import pkg from './package.json'; - +//These will be excluded from the bundle and remain as runtime dependencies +export const externalDependencies = ['@apidevtools/swagger-parser', 'mocha', 'tough-cookie']; export default defineConfig(({ mode }) => { const __DEV__ = mode !== 'production'; @@ -37,11 +39,11 @@ export default defineConfig(({ mode }) => { target: 'esnext', sourcemap: true, rollupOptions: { - external: ['@getinsomnia/node-libcurl', 'electron', /^electron\//], + external: ['@getinsomnia/node-libcurl'], }, }, optimizeDeps: { - exclude: ['@getinsomnia/node-libcurl', 'electron'], + exclude: ['@getinsomnia/node-libcurl'], force: true, // wipe vite cache include: ['codemirror-graphql/utils/SchemaReference', '@stoplight/spectral-core', 'isomorphic-git', 'json-bigint'], }, @@ -59,7 +61,21 @@ export default defineConfig(({ mode }) => { 'events': path.resolve(__dirname, '../../node_modules/events'), }, }, - plugins: [reactRouter(), tailwindcss()], + plugins: [ + // Allows us to import modules that will be resolved by Node's require() function. + // e.g. import fs from 'fs'; will get transformed to const fs = require('fs'); so that it works in the renderer process. + // This is necessary because we use nodeIntegration: true in the renderer process and allow importing modules from node. + electronNodeRequire({ + modules: [ + 'electron', + ...externalDependencies, + ...builtinModules.filter(m => m !== 'buffer' && m !== 'path'), + ...builtinModules.map(m => `node:${m}`), + ], + }), + reactRouter(), + tailwindcss(), + ], worker: { format: 'es', }, @@ -76,3 +92,114 @@ export default defineConfig(({ mode }) => { }, }; }); +import { createRequire } from 'node:module'; + +import type { Plugin } from 'vite'; + +export interface Options { + modules: string[]; +} + +/** + * Allows Vite to import modules that will be resolved by Node's require() function. + */ +export function electronNodeRequire(options: Options): Plugin { + const { modules = [] } = options; + + return { + name: 'vite-plugin-electron-node-require', + config(conf, env) { + // If the plugin is used in SSR mode, we don't need to do anything + if (env.isSsrBuild) { + return conf; + } + // Exclude the modules from Vite's dependency optimization (pre-bundling) + conf.optimizeDeps = { + ...conf.optimizeDeps, + exclude: [...(conf.optimizeDeps?.exclude ? conf.optimizeDeps.exclude : []), ...modules], + }; + + // Create aliases for the modules so that we can resolve them with this plugin + conf.resolve ??= {}; + conf.resolve.alias = { + ...conf.resolve.alias, + ...Object.fromEntries(modules.map(e => [e, `virtual:external:${e}`])), + }; + + // Ignore the modules from Rollup's commonjs plugin so that we can resolve them with this plugin + conf.build ??= {}; + conf.build.commonjsOptions ??= {}; + conf.build.commonjsOptions.ignore = [...modules]; + + return conf; + }, + resolveId(id, _importer, options) { + const externalId = id.split('virtual:external:')[1]; + + if (externalId && modules.includes(externalId)) { + if (options.ssr) { + return null; + } + // Return a virtual module ID so that Vite knows to use this plugin to resolve the module + // The \0 is a special convention by Rollup to indicate that the module is virtual and should not be resolved by other plugins + return `\0${id}`; + } + + // Return null to indicate that this plugin should not resolve the module + return null; + }, + load(id, options) { + if (id.includes('virtual:external:')) { + const externalId = id.split('virtual:external:')[1]; + + // We need to handle electron because it's different when required in the renderer process + if (externalId === 'electron') { + return ` + const electron = require('electron'); + export { electron as default }; + export const BrowserWindow = electron.BrowserWindow; + export const clipboard = electron.clipboard; + export const contextBridge = electron.contextBridge; + export const crashReporter = electron.crashReporter; + export const dialog = electron.dialog; + export const ipcRenderer = electron.ipcRenderer; + export const nativeImage = electron.nativeImage; + export const shell = electron.shell; + export const webFrame = electron.webFrame; + export const deprecate = electron.deprecate; + `; + } + + const nodeRequire = createRequire(import.meta.url); + const exports = Object.keys(nodeRequire(externalId)); + + // Filter out the exports that are valid javascript variable keywords: + const validExports = exports.filter(e => { + try { + new Function(`const ${e} = true`); + return true; + } catch { + return false; + } + }); + + if (options?.ssr) { + return [ + `import requiredModule from '${externalId}';`, + `${validExports.map(e => `export const ${e} = requiredModule.${e};`).join('\n')}`, + `${exports.includes('default') ? 'export default requiredModule.default;' : 'export default requiredModule'}`, + ].join('\n'); + } + + return [ + `const requiredModule = require('${externalId}');`, + `${validExports.map(e => `export const ${e} = requiredModule.${e};`).join('\n')}`, + `${exports.includes('default') ? 'export default requiredModule.default;' : 'export default requiredModule'}`, + ].join('\n'); + } + + // Return null to indicate that this plugin should not resolve the module + return null; + }, + }; +} From 4ec3b4e04373e886a06b63fe51dbf8c2148c6d0d Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 22:09:13 +0200 Subject: [PATCH 22/47] vault adapter --- packages/insomnia/src/main/ipc/main.ts | 2 +- packages/insomnia/src/templating/utils.ts | 3 +- .../key-value-editor.tsx | 2 +- .../src/utils/vault-adapter.node.test.ts | 42 +++++++++++++++++++ .../insomnia/src/utils/vault-adapter.node.ts | 26 ++++++++++++ ...test.ts => vault-adapter.renderer.test.ts} | 2 +- ...lt-crypto.ts => vault-adapter.renderer.ts} | 0 packages/insomnia/src/utils/vault-adapter.ts | 9 ++++ packages/insomnia/src/utils/vault.test.ts | 36 ++-------------- packages/insomnia/src/utils/vault.ts | 37 +++------------- 10 files changed, 90 insertions(+), 69 deletions(-) create mode 100644 packages/insomnia/src/utils/vault-adapter.node.test.ts create mode 100644 packages/insomnia/src/utils/vault-adapter.node.ts rename packages/insomnia/src/utils/{vault-crypto.test.ts => vault-adapter.renderer.test.ts} (99%) rename packages/insomnia/src/utils/{vault-crypto.ts => vault-adapter.renderer.ts} (100%) create mode 100644 packages/insomnia/src/utils/vault-adapter.ts diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index 9fa154b62464..c2f6e9548b9f 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -41,7 +41,7 @@ import type { import type { HiddenBrowserWindowBridgeAPI } from '../../entry.hidden-window'; import type { PluginsBridgeAPI } from '../../plugins/bridge-types'; import type { RenderedRequest } from '../../templating/types'; -import { decryptSecretValue,encryptSecretValue } from '../../utils/vault'; +import { decryptSecretValue, encryptSecretValue } from '../../utils/vault-adapter'; import type { AnalyticsEvent } from '../analytics'; import { setCurrentOrganizationId, trackAnalyticsEvent, trackPageView } from '../analytics'; import { diff --git a/packages/insomnia/src/templating/utils.ts b/packages/insomnia/src/templating/utils.ts index e4146ba01211..b8d0a8030ffb 100644 --- a/packages/insomnia/src/templating/utils.ts +++ b/packages/insomnia/src/templating/utils.ts @@ -1,8 +1,7 @@ import type { EditorFromTextArea, MarkerRange } from 'codemirror'; import { models, services } from 'insomnia-data'; -import { decryptSecretValue } from '~/utils/vault-crypto'; - +import { decryptSecretValue } from '~/utils/vault-adapter'; import type { NunjucksParsedTag, NunjucksParsedTagArg, RenderPurpose } from '../templating/types'; import { decryptVaultKeyFromSession } from '../utils/vault'; import { tokenizeArgs } from './tokenize-args'; diff --git a/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx b/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx index 3d89544d67a8..81f2459e2af1 100644 --- a/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx @@ -20,7 +20,7 @@ import { checkNestedKeys, ensureKeyIsValid } from '~/utils/environment-utils'; import { generateId } from '../../../../common/misc'; import { base64decode } from '../../../../utils/vault'; -import { decryptSecretValue, encryptSecretValue } from '../../../../utils/vault-crypto'; +import { decryptSecretValue, encryptSecretValue } from '../../../../utils/vault-adapter'; import { PromptButton } from '../../base/prompt-button'; import { Icon } from '../../icon'; import { showModal } from '../../modals'; diff --git a/packages/insomnia/src/utils/vault-adapter.node.test.ts b/packages/insomnia/src/utils/vault-adapter.node.test.ts new file mode 100644 index 000000000000..19be47d0e2e9 --- /dev/null +++ b/packages/insomnia/src/utils/vault-adapter.node.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; + +import { decryptSecretValue, encryptSecretValue } from './vault-adapter.node'; + +const TEST_AES_KEY: JsonWebKey = { + kty: 'oct', + alg: 'A256GCM', + ext: true, + key_ops: ['encrypt', 'decrypt'], + k: '5hs1f2xuiNPHUp11i6SWlsqYpWe_hWPcEKucZlwBfFE', +}; + +describe('encryptSecretValue', () => { + it('returns rawValue when symmetricKey is not an object', async () => { + expect(await encryptSecretValue('secret', 'invalid' as unknown as JsonWebKey)).toBe('secret'); + }); + + it('returns rawValue when symmetricKey is empty object', async () => { + expect(await encryptSecretValue('secret', {})).toBe('secret'); + }); + + it('encrypts the value with a valid key', async () => { + const encrypted = await encryptSecretValue('my secret', TEST_AES_KEY); + expect(typeof encrypted).toBe('string'); + expect(encrypted).not.toBe('my secret'); + }); +}); + +describe('decryptSecretValue', () => { + it('returns encryptedValue when symmetricKey is not an object', async () => { + expect(await decryptSecretValue('encrypted', 'invalid' as unknown as JsonWebKey)).toBe('encrypted'); + }); + + it('returns encryptedValue when symmetricKey is empty object', async () => { + expect(await decryptSecretValue('encrypted', {})).toBe('encrypted'); + }); + + it('round-trips encrypt then decrypt', async () => { + const encrypted = await encryptSecretValue('my secret', TEST_AES_KEY); + expect(await decryptSecretValue(encrypted, TEST_AES_KEY)).toBe('my secret'); + }); +}); diff --git a/packages/insomnia/src/utils/vault-adapter.node.ts b/packages/insomnia/src/utils/vault-adapter.node.ts new file mode 100644 index 000000000000..b02186c29023 --- /dev/null +++ b/packages/insomnia/src/utils/vault-adapter.node.ts @@ -0,0 +1,26 @@ +import { type AESMessage, decryptAES, encryptAES } from '../account/crypt'; +import { base64decode, base64encode } from './vault'; + +export const encryptSecretValue = async (rawValue: string, symmetricKey: JsonWebKey): Promise => { + if (typeof symmetricKey !== 'object' || Object.keys(symmetricKey).length === 0) { + return rawValue; + } + try { + const encryptResult = encryptAES(symmetricKey, rawValue); + return base64encode(encryptResult); + } catch { + return rawValue; + } +}; + +export const decryptSecretValue = async (encryptedValue: string, symmetricKey: JsonWebKey): Promise => { + if (typeof symmetricKey !== 'object' || Object.keys(symmetricKey).length === 0) { + return encryptedValue; + } + try { + const jsonWebKey = base64decode(encryptedValue, true) as AESMessage; + return decryptAES(symmetricKey, jsonWebKey); + } catch { + return encryptedValue; + } +}; diff --git a/packages/insomnia/src/utils/vault-crypto.test.ts b/packages/insomnia/src/utils/vault-adapter.renderer.test.ts similarity index 99% rename from packages/insomnia/src/utils/vault-crypto.test.ts rename to packages/insomnia/src/utils/vault-adapter.renderer.test.ts index 19a96affbbf7..50e9691038ba 100644 --- a/packages/insomnia/src/utils/vault-crypto.test.ts +++ b/packages/insomnia/src/utils/vault-adapter.renderer.test.ts @@ -1,7 +1,7 @@ // @vitest-environment jsdom import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { decryptSecretValue, encryptSecretValue } from './vault-crypto'; +import { decryptSecretValue, encryptSecretValue } from './vault-adapter.renderer'; const mockEncrypt = vi.fn(); const mockDecrypt = vi.fn(); diff --git a/packages/insomnia/src/utils/vault-crypto.ts b/packages/insomnia/src/utils/vault-adapter.renderer.ts similarity index 100% rename from packages/insomnia/src/utils/vault-crypto.ts rename to packages/insomnia/src/utils/vault-adapter.renderer.ts diff --git a/packages/insomnia/src/utils/vault-adapter.ts b/packages/insomnia/src/utils/vault-adapter.ts new file mode 100644 index 000000000000..3e75d3dfdbc4 --- /dev/null +++ b/packages/insomnia/src/utils/vault-adapter.ts @@ -0,0 +1,9 @@ +// Runtime adapter selection: renderer delegates to IPC, node/CLI uses direct crypto. +// Vite inlines process.type at build time so Rollup tree-shakes the unused branch from each bundle. +import type * as AdapterType from './vault-adapter.node'; + +const impl = ( + (process as any).type === 'renderer' ? require('./vault-adapter.renderer') : require('./vault-adapter.node') +) as typeof AdapterType; + +export const { encryptSecretValue, decryptSecretValue } = impl; diff --git a/packages/insomnia/src/utils/vault.test.ts b/packages/insomnia/src/utils/vault.test.ts index a8f801f85dc5..570e5f8aa45c 100644 --- a/packages/insomnia/src/utils/vault.test.ts +++ b/packages/insomnia/src/utils/vault.test.ts @@ -1,13 +1,7 @@ // @vitest-environment jsdom import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - base64decode, - base64encode, - decryptSecretValue, - decryptVaultKeyFromSession, - encryptSecretValue, -} from './vault'; +import { base64decode, base64encode, decryptVaultKeyFromSession } from './vault'; vi.mock('../models/settings', () => ({ getOrCreate: vi.fn(), @@ -32,11 +26,12 @@ const mockSecretStorage = { describe('base64encode', () => { it('encodes a string', () => { - expect(base64encode('hello world')).toBe(Buffer.from('hello world', 'utf8').toString('base64')); + expect(base64encode('hello world')).toBe('aGVsbG8gd29ybGQ='); }); it('encodes a JsonWebKey object', () => { - expect(base64encode(TEST_AES_KEY)).toBe(Buffer.from(JSON.stringify(TEST_AES_KEY), 'utf8').toString('base64')); + const encoded = base64encode(TEST_AES_KEY); + expect(base64decode(encoded, true)).toEqual(TEST_AES_KEY); }); }); @@ -58,29 +53,6 @@ describe('base64decode', () => { }); }); -describe('encryptSecretValue', () => { - it('returns rawValue when symmetricKey is not an object', () => { - expect(encryptSecretValue('secret', 'invalid' as unknown as JsonWebKey)).toBe('secret'); - }); - - it('encrypts the value with a valid key', () => { - const encrypted = encryptSecretValue('my secret', TEST_AES_KEY); - expect(typeof encrypted).toBe('string'); - expect(encrypted).not.toBe('my secret'); - }); -}); - -describe('decryptSecretValue', () => { - it('returns encryptedValue when symmetricKey is not an object', () => { - expect(decryptSecretValue('encrypted', 'invalid' as unknown as JsonWebKey)).toBe('encrypted'); - }); - - it('round-trips encrypt then decrypt', () => { - const encrypted = encryptSecretValue('my secret', TEST_AES_KEY); - expect(decryptSecretValue(encrypted, TEST_AES_KEY)).toBe('my secret'); - }); -}); - describe('decryptVaultKeyFromSession', () => { beforeEach(() => { vi.resetAllMocks(); diff --git a/packages/insomnia/src/utils/vault.ts b/packages/insomnia/src/utils/vault.ts index 111ced6fc1c6..60944c8ecbcc 100644 --- a/packages/insomnia/src/utils/vault.ts +++ b/packages/insomnia/src/utils/vault.ts @@ -1,18 +1,20 @@ import { services } from 'insomnia-data'; -import { type AESMessage, decryptAES, encryptAES } from '../account/crypt'; import { getInsomniaVaultKey, PLAYWRIGHT_TEST } from '../common/constants'; export const base64encode = (input: string | JsonWebKey) => { const inputStr = typeof input === 'string' ? input : JSON.stringify(input); - return Buffer.from(inputStr, 'utf8').toString('base64'); + const bytes = new TextEncoder().encode(inputStr); + let binary = ''; + bytes.forEach(byte => (binary += String.fromCodePoint(byte))); + return btoa(binary); }; export function base64decode(base64Str: string, toObject: true): object; export function base64decode(base64Str: string, toObject: false): string; export function base64decode(base64Str: string, toObject: boolean): string | object { try { - const decodedStr = Buffer.from(base64Str, 'base64').toString('utf8'); + const decodedStr = new TextDecoder().decode(Uint8Array.from(atob(base64Str), c => c.codePointAt(0) ?? 0)); if (toObject) { return JSON.parse(decodedStr); } @@ -61,32 +63,3 @@ export const getVaultKeyFromStorage = async (accountId: string) => { export const deleteVaultKeyFromStorage = async (accountId: string) => { await window.main.secretStorage.deleteSecret(getVaultSecretKey(accountId)); }; - -export const encryptSecretValue = (rawValue: string, symmetricKey: JsonWebKey) => { - if (typeof symmetricKey !== 'object' || Object.keys(symmetricKey).length === 0) { - // invalid symmetricKey - return rawValue; - } - try { - const encryptResult = encryptAES(symmetricKey, rawValue); - const encryptedValue = base64encode(encryptResult); - return encryptedValue; - } catch { - // return original value if encryption fails - return rawValue; - } -}; - -export const decryptSecretValue = (encryptedValue: string, symmetricKey: JsonWebKey) => { - if (typeof symmetricKey !== 'object' || Object.keys(symmetricKey).length === 0) { - // invalid symmetricKey - return encryptedValue; - } - try { - const jsonWebKey = base64decode(encryptedValue, true) as AESMessage; - return decryptAES(symmetricKey, jsonWebKey); - } catch { - // return origin value if failed to decrypt - return encryptedValue; - } -}; From ed4dea83b301d0fc0a3d59391e70f948f0219a83 Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 22:28:42 +0200 Subject: [PATCH 23/47] add crypto bridges --- packages/insomnia/src/account/session.ts | 15 ++--- packages/insomnia/src/entry.preload.ts | 20 +++++++ packages/insomnia/src/main/ipc/electron.ts | 11 +++- packages/insomnia/src/main/ipc/main.ts | 55 +++++++++++++++++++ .../src/ui/auth-session-provider.client.ts | 15 +++-- .../modals/invite-modal/encryption.ts | 49 +++++++++-------- packages/insomnia/vite.config.ts | 1 + 7 files changed, 127 insertions(+), 39 deletions(-) diff --git a/packages/insomnia/src/account/session.ts b/packages/insomnia/src/account/session.ts index 509cffa089e3..73399ba29a82 100644 --- a/packages/insomnia/src/account/session.ts +++ b/packages/insomnia/src/account/session.ts @@ -1,10 +1,11 @@ import { getEncryptionKeys, getUserProfile, logout as logoutAPI } from 'insomnia-api'; -import type { GitRepository, Project, WorkspaceMeta } from 'insomnia-data'; -import { models, services } from 'insomnia-data'; + +import type { GitRepository, Project, WorkspaceMeta } from '~/insomnia-data'; +import type { AESMessage } from '~/insomnia-data'; +import { models, services } from '~/insomnia-data'; import { AI_PLUGIN_NAME, LLM_BACKENDS } from '../common/constants'; import { database } from '../common/database'; -import * as crypt from './crypt'; export interface SessionData { accountId: string; @@ -14,7 +15,7 @@ export interface SessionData { lastName: string; symmetricKey: JsonWebKey; publicKey: JsonWebKey; - encPrivateKey: crypt.AESMessage; + encPrivateKey: AESMessage; } /** Creates a session from a sessionId and derived symmetric key. */ @@ -27,7 +28,7 @@ export async function absorbKey(sessionId: string, key: string) { ]); const { public_key: publicKey, enc_private_key: encPrivateKey, enc_symmetric_key: encSymmetricKey } = keys; const { email, id: accountId, first_name: firstName, last_name: lastName } = profile; - const symmetricKeyStr = crypt.decryptAES(key, JSON.parse(encSymmetricKey)); + const symmetricKeyStr = await window.main.crypt.decryptAES(key, JSON.parse(encSymmetricKey)); // Store the information for later await setSessionData( @@ -57,7 +58,7 @@ export async function getPrivateKey() { throw new Error("Can't get private key: session is missing keys."); } - const privateKeyStr = crypt.decryptAES(symmetricKey, encPrivateKey); + const privateKeyStr = await window.main.crypt.decryptAES(symmetricKey, encPrivateKey); return JSON.parse(privateKeyStr) as JsonWebKey; } @@ -104,7 +105,7 @@ export async function setSessionData( email: string, symmetricKey: JsonWebKey, publicKey: JsonWebKey, - encPrivateKey: crypt.AESMessage, + encPrivateKey: AESMessage, ) { const sessionData: SessionData = { id, diff --git a/packages/insomnia/src/entry.preload.ts b/packages/insomnia/src/entry.preload.ts index d9165ac3a648..15350acbab1f 100644 --- a/packages/insomnia/src/entry.preload.ts +++ b/packages/insomnia/src/entry.preload.ts @@ -351,6 +351,26 @@ const main: Window['main'] = { decryptSecretValue: (encryptedValue, symmetricKey) => invokeWithNormalizedError('vault.decryptSecretValue', encryptedValue, symmetricKey), }, + crypt: { + encryptRSAWithJWK: (publicKeyJWK: JsonWebKey, plaintext: string) => + invokeWithNormalizedError('crypt.encryptRSAWithJWK', publicKeyJWK, plaintext), + decryptRSAWithJWK: (privateJWK: JsonWebKey, encryptedBlob: string) => + invokeWithNormalizedError('crypt.decryptRSAWithJWK', privateJWK, encryptedBlob), + encryptAESBuffer: (jwkOrKey: string | JsonWebKey, buff: number[], additionalData?: string) => + invokeWithNormalizedError('crypt.encryptAESBuffer', jwkOrKey, buff, additionalData), + encryptAES: (jwkOrKey: string | JsonWebKey, plaintext: string, additionalData?: string) => + invokeWithNormalizedError('crypt.encryptAES', jwkOrKey, plaintext, additionalData), + decryptAES: (jwkOrKey: string | JsonWebKey, encryptedResult: object) => + invokeWithNormalizedError('crypt.decryptAES', jwkOrKey, encryptedResult), + decryptAESToBuffer: (jwkOrKey: string | JsonWebKey, encryptedResult: object) => + invokeWithNormalizedError('crypt.decryptAESToBuffer', jwkOrKey, encryptedResult), + generateAES256Key: () => invokeWithNormalizedError('crypt.generateAES256Key'), + }, + sealedBox: { + keyPair: () => invokeWithNormalizedError('sealedbox.keyPair'), + open: (sealedbox: Uint8Array, pk: Uint8Array, sk: Uint8Array) => + invokeWithNormalizedError('sealedbox.open', sealedbox, pk, sk), + }, extractJsonFileFromPostmanDataDumpArchive: archivePath => invokeWithNormalizedError('extractJsonFileFromPostmanDataDumpArchive', archivePath), syncNewWorkspaceIfNeeded: options => invokeWithNormalizedError('syncNewWorkspaceIfNeeded', options), diff --git a/packages/insomnia/src/main/ipc/electron.ts b/packages/insomnia/src/main/ipc/electron.ts index 3021d27215fa..ff49705936cd 100644 --- a/packages/insomnia/src/main/ipc/electron.ts +++ b/packages/insomnia/src/main/ipc/electron.ts @@ -174,7 +174,16 @@ export type HandleChannels = | 'deleteRulesetFile' | 'writeResponseBodyToFile' | 'vault.encryptSecretValue' - | 'vault.decryptSecretValue'; + | 'vault.decryptSecretValue' + | 'crypt.encryptRSAWithJWK' + | 'crypt.decryptRSAWithJWK' + | 'crypt.encryptAESBuffer' + | 'crypt.encryptAES' + | 'crypt.decryptAES' + | 'crypt.decryptAESToBuffer' + | 'crypt.generateAES256Key' + | 'sealedbox.keyPair' + | 'sealedbox.open'; export const ipcMainHandle = ( channel: HandleChannels, diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index c2f6e9548b9f..672b2ac1814d 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -38,9 +38,11 @@ import type { ModelConfig, } from '~/plugins/types'; +import * as crypt from '../../account/crypt'; import type { HiddenBrowserWindowBridgeAPI } from '../../entry.hidden-window'; import type { PluginsBridgeAPI } from '../../plugins/bridge-types'; import type { RenderedRequest } from '../../templating/types'; +import { keyPair as sealedboxKeyPair, open as sealedboxOpen } from '../../utils/sealedbox'; import { decryptSecretValue, encryptSecretValue } from '../../utils/vault-adapter'; import type { AnalyticsEvent } from '../analytics'; import { setCurrentOrganizationId, trackAnalyticsEvent, trackPageView } from '../analytics'; @@ -298,6 +300,27 @@ export interface RendererToMainBridgeAPI { encryptSecretValue: (rawValue: string, symmetricKey: JsonWebKey) => Promise; decryptSecretValue: (encryptedValue: string, symmetricKey: JsonWebKey) => Promise; }; + crypt: { + encryptRSAWithJWK: (publicKeyJWK: JsonWebKey, plaintext: string) => Promise; + decryptRSAWithJWK: (privateJWK: JsonWebKey, encryptedBlob: string) => Promise; + encryptAESBuffer: ( + jwkOrKey: string | JsonWebKey, + buff: number[], + additionalData?: string, + ) => Promise; + encryptAES: ( + jwkOrKey: string | JsonWebKey, + plaintext: string, + additionalData?: string, + ) => Promise; + decryptAES: (jwkOrKey: string | JsonWebKey, encryptedResult: crypt.AESMessage) => Promise; + decryptAESToBuffer: (jwkOrKey: string | JsonWebKey, encryptedResult: crypt.AESMessage) => Promise; + generateAES256Key: () => Promise; + }; + sealedBox: { + keyPair: () => Promise<{ publicKey: Uint8Array; secretKey: Uint8Array }>; + open: (sealedbox: Uint8Array, pk: Uint8Array, sk: Uint8Array) => Promise; + }; timeline: { getPath: (responseId: string) => Promise; appendToFile: (options: { timelinePath: string; data: string }) => Promise; @@ -826,6 +849,38 @@ export function registerMainHandlers() { return decryptSecretValue(encryptedValue, symmetricKey); }); + ipcMainHandle('crypt.encryptRSAWithJWK', (_, publicKeyJWK: JsonWebKey, plaintext: string) => { + return crypt.encryptRSAWithJWK(publicKeyJWK, plaintext); + }); + ipcMainHandle('crypt.decryptRSAWithJWK', (_, privateJWK: JsonWebKey, encryptedBlob: string) => { + return crypt.decryptRSAWithJWK(privateJWK, encryptedBlob); + }); + ipcMainHandle( + 'crypt.encryptAESBuffer', + (_, jwkOrKey: string | JsonWebKey, buff: number[], additionalData?: string) => { + return crypt.encryptAESBuffer(jwkOrKey, Buffer.from(buff), additionalData); + }, + ); + ipcMainHandle('crypt.encryptAES', (_, jwkOrKey: string | JsonWebKey, plaintext: string, additionalData?: string) => { + return crypt.encryptAES(jwkOrKey, plaintext, additionalData); + }); + ipcMainHandle('crypt.decryptAES', (_, jwkOrKey: string | JsonWebKey, encryptedResult: crypt.AESMessage) => { + return crypt.decryptAES(jwkOrKey, encryptedResult); + }); + ipcMainHandle('crypt.decryptAESToBuffer', (_, jwkOrKey: string | JsonWebKey, encryptedResult: crypt.AESMessage) => { + return Array.from(crypt.decryptAESToBuffer(jwkOrKey, encryptedResult)); + }); + ipcMainHandle('crypt.generateAES256Key', _ => { + return crypt.generateAES256Key(); + }); + + ipcMainHandle('sealedbox.keyPair', _ => { + return sealedboxKeyPair(); + }); + ipcMainHandle('sealedbox.open', (_, sealedbox: Uint8Array, pk: Uint8Array, sk: Uint8Array) => { + return sealedboxOpen(sealedbox, pk, sk); + }); + ipcMainHandle('run-tests', async (_, src: string) => { const { runTests } = await import('insomnia-testing'); const sendRequest = getSendRequestCallback(); diff --git a/packages/insomnia/src/ui/auth-session-provider.client.ts b/packages/insomnia/src/ui/auth-session-provider.client.ts index cf33fbfee5c7..6d0bc7026ebe 100644 --- a/packages/insomnia/src/ui/auth-session-provider.client.ts +++ b/packages/insomnia/src/ui/auth-session-provider.client.ts @@ -1,24 +1,23 @@ import * as session from '../account/session'; import { getAppWebsiteBaseURL, getInsomniaPublicKey, getInsomniaSecretKey } from '../common/constants'; import { invariant } from '../utils/invariant'; -import { keyPair, open } from '../utils/sealedbox'; interface AuthBox { token: string; key: string; } -const sessionKeyPair = keyPair(); -encodeBase64(sessionKeyPair.publicKey).then(res => { +const sessionKeyPairPromise = window.main.sealedBox.keyPair(); +sessionKeyPairPromise.then(async kp => { try { - window.localStorage.setItem('insomnia.publicKey', getInsomniaPublicKey() || res); + const pub = await encodeBase64(kp.publicKey); + window.localStorage.setItem('insomnia.publicKey', getInsomniaPublicKey() || pub); } catch { console.error('Failed to store public key in localStorage.'); } -}); -encodeBase64(sessionKeyPair.secretKey).then(res => { try { - window.localStorage.setItem('insomnia.secretKey', getInsomniaSecretKey() || res); + const sec = await encodeBase64(kp.secretKey); + window.localStorage.setItem('insomnia.secretKey', getInsomniaSecretKey() || sec); } catch { console.error('Failed to store secret key in localStorage.'); } @@ -68,7 +67,7 @@ export async function submitAuthCode(code: string) { const rawBox = await decodeBase64(code.trim()); const publicKey = await decodeBase64(window.localStorage.getItem('insomnia.publicKey') || ''); const secretKey = await decodeBase64(window.localStorage.getItem('insomnia.secretKey') || ''); - const boxData = open(rawBox, publicKey, secretKey); + const boxData = await window.main.sealedBox.open(rawBox, publicKey, secretKey); invariant(boxData, 'Invalid authentication code.'); const decoder = new TextDecoder(); diff --git a/packages/insomnia/src/ui/components/modals/invite-modal/encryption.ts b/packages/insomnia/src/ui/components/modals/invite-modal/encryption.ts index cbd33a776b12..3765f8fda983 100644 --- a/packages/insomnia/src/ui/components/modals/invite-modal/encryption.ts +++ b/packages/insomnia/src/ui/components/modals/invite-modal/encryption.ts @@ -7,7 +7,6 @@ import { startAddingCollaborators, } from 'insomnia-api'; -import { decryptRSAWithJWK, encryptRSAWithJWK } from '../../../../account/crypt'; import { getCurrentSessionId, getPrivateKey } from '../../../../account/session'; import { invariant } from '../../../../utils/invariant'; @@ -37,21 +36,23 @@ interface Invite { inviteeId: string; } -export function buildInviteByInstruction( +export async function buildInviteByInstruction( instruction: InviteInstruction, rawProjectKeys: DecryptedProjectKey[], -): Invite { +): Promise { let inviteKeys: InviteKey[] = []; if (rawProjectKeys?.length) { const inviteePublicKey = JSON.parse(instruction.inviteePublicKey); - inviteKeys = rawProjectKeys.map(key => { - const reEncryptedSymmetricKey = encryptRSAWithJWK(inviteePublicKey, key.symmetricKey); - return { - projectId: key.projectId, - encSymmetricKey: reEncryptedSymmetricKey, - autoLinked: instruction.inviteeAutoLinked, - }; - }); + inviteKeys = await Promise.all( + rawProjectKeys.map(async key => { + const reEncryptedSymmetricKey = await window.main.crypt.encryptRSAWithJWK(inviteePublicKey, key.symmetricKey); + return { + projectId: key.projectId, + encSymmetricKey: reEncryptedSymmetricKey, + autoLinked: instruction.inviteeAutoLinked, + }; + }), + ); } return { inviteeId: instruction.inviteeId, @@ -60,17 +61,17 @@ export function buildInviteByInstruction( }; } -function buildMemberProjectKey( +async function buildMemberProjectKey( accountId: string, projectId: string, publicKey: string, rawProjectKey?: string, -): MemberProjectKey | null { +): Promise { if (!rawProjectKey) { return null; } const acctPublicKey = JSON.parse(publicKey); - const encSymmetricKey = encryptRSAWithJWK(acctPublicKey, rawProjectKey); + const encSymmetricKey = await window.main.crypt.encryptRSAWithJWK(acctPublicKey, rawProjectKey); return { projectId, accountId, @@ -86,8 +87,8 @@ async function decryptProjectKeys( decryptionKey: JsonWebKey, projectKeys: EncryptedProjectKey[], ): Promise { - const promises = projectKeys.map(key => { - const symmetricKey = decryptRSAWithJWK(decryptionKey, key.encKey); + const promises = projectKeys.map(async key => { + const symmetricKey = await window.main.crypt.decryptRSAWithJWK(decryptionKey, key.encKey); return { projectId: key.projectId, symmetricKey, @@ -139,11 +140,13 @@ export async function startInvite({ emails, teamIds, organizationId, roleId }: S }, keyMap); // This is to reconcile any users in bad standing - memberKeys = myKeysInfo.members - .map((member: ProjectMember) => - buildMemberProjectKey(member.accountId, member.projectId, member.publicKey, keyMap[member.projectId]), + memberKeys = ( + await Promise.all( + myKeysInfo.members.map((member: ProjectMember) => + buildMemberProjectKey(member.accountId, member.projectId, member.publicKey, keyMap[member.projectId]), + ), ) - .filter(Boolean) as MemberProjectKey[]; + ).filter(Boolean) as MemberProjectKey[]; } if (memberKeys.length) { @@ -165,9 +168,9 @@ export async function startInvite({ emails, teamIds, organizationId, roleId }: S keys[acctId] = {}; } - projectKeys.forEach(key => { + for (const key of projectKeys) { const pubKey = instruction[acctId].publicKey; - const newKey = buildMemberProjectKey(acctId, key.projectId, pubKey, key.symmetricKey); + const newKey = await buildMemberProjectKey(acctId, key.projectId, pubKey, key.symmetricKey); if (newKey) { keys[acctId][key.projectId] = { @@ -176,7 +179,7 @@ export async function startInvite({ emails, teamIds, organizationId, roleId }: S encKey: newKey.encSymmetricKey, }; } - }); + } } } await finishAddingCollaborators({ diff --git a/packages/insomnia/vite.config.ts b/packages/insomnia/vite.config.ts index 200e38252cf1..54d8c6165ff9 100644 --- a/packages/insomnia/vite.config.ts +++ b/packages/insomnia/vite.config.ts @@ -54,6 +54,7 @@ export default defineConfig(({ mode }) => { // These must appear before the '~' catch-all so the specific path wins. '~/network/network-adapter': path.resolve(__dirname, './src/network/network-adapter.renderer'), '~/templating/render-adapter': path.resolve(__dirname, './src/templating/render-adapter.renderer'), + '~/utils/vault-adapter': path.resolve(__dirname, './src/utils/vault-adapter.renderer'), '~': path.resolve(__dirname, './src'), // Shim Node's `path` module for browser-safe dependencies (e.g. mime-types uses path.extname). 'path': path.resolve(__dirname, './src/path-shim.ts'), From 71e9030e30b4b837a24ba1055f852cd155f3503a Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 22:39:26 +0200 Subject: [PATCH 24/47] tough-cookie ipc --- packages/insomnia/src/common/cookies.ts | 2 +- packages/insomnia/src/entry.main.ts | 2 + packages/insomnia/src/entry.preload.ts | 9 +++ packages/insomnia/src/main/ipc/cookies.ts | 64 +++++++++++++++++++ packages/insomnia/src/main/ipc/electron.ts | 6 +- packages/insomnia/src/main/ipc/main.ts | 2 + .../ui/components/modals/cookies-modal.tsx | 55 +++++++++------- packages/insomnia/vite.config.ts | 2 +- 8 files changed, 115 insertions(+), 27 deletions(-) create mode 100644 packages/insomnia/src/main/ipc/cookies.ts diff --git a/packages/insomnia/src/common/cookies.ts b/packages/insomnia/src/common/cookies.ts index 2e8fe55ecd14..bb469b0cc1d9 100644 --- a/packages/insomnia/src/common/cookies.ts +++ b/packages/insomnia/src/common/cookies.ts @@ -8,7 +8,7 @@ export const cookiesFromJar = (cookieJar: CookieJar): Promise => { return new Promise(resolve => { cookieJar.store.getAllCookies((err, cookies) => { if (err) { - console.warn('Failed to get cookies form jar', err); + console.warn('Failed to get cookies from jar', err); resolve([]); } else { // NOTE: Perform toJSON so we have a plain JS object instead of Cookie instance diff --git a/packages/insomnia/src/entry.main.ts b/packages/insomnia/src/entry.main.ts index b4b6ca35317d..6ecaf885c926 100644 --- a/packages/insomnia/src/entry.main.ts +++ b/packages/insomnia/src/entry.main.ts @@ -26,6 +26,7 @@ import { registerInsomniaProtocols } from './main/api.protocol'; import { backupIfNewerVersionAvailable } from './main/backup'; import { registerSyncHandlers } from './main/cloud-sync/ipc'; import { registerGitServiceAPI } from './main/git-service'; +import { registerCookieHandlers } from './main/ipc/cookies'; import { ipcMainOn, ipcMainOnce, registerElectronHandlers } from './main/ipc/electron'; import { registerElectronStorageHandlers } from './main/ipc/electron-storage'; import { registergRPCHandlers } from './main/ipc/grpc'; @@ -89,6 +90,7 @@ app.on('ready', async () => { registerMainHandlers(); registerPathHandlers(); registergRPCHandlers(); + registerCookieHandlers(); registerGitServiceAPI(); registerLLMConfigServiceAPI(); registerWebSocketHandlers(); diff --git a/packages/insomnia/src/entry.preload.ts b/packages/insomnia/src/entry.preload.ts index 15350acbab1f..ab2e6bf42fd2 100644 --- a/packages/insomnia/src/entry.preload.ts +++ b/packages/insomnia/src/entry.preload.ts @@ -9,6 +9,7 @@ import { servicesProxy } from '~/ui/renderer-services-proxy'; import type { SyncBridgeAPI } from './main/cloud-sync/ipc'; import type { GitServiceAPI } from './main/git-service'; +import type { CookiesBridgeAPI } from './main/ipc/cookies'; import type { electronStorageBridgeAPI } from './main/ipc/electron-storage'; import type { gRPCBridgeAPI } from './main/ipc/grpc'; import type { secretStorageBridgeAPI } from './main/ipc/secret-storage'; @@ -115,6 +116,13 @@ const mcp: McpBridgeAPI = { }, }; +const cookies: CookiesBridgeAPI = { + fromJSON: cookie => invokeWithNormalizedError('cookies.fromJSON', cookie), + parse: cookie => invokeWithNormalizedError('cookies.parse', cookie), + toString: cookie => invokeWithNormalizedError('cookies.toString', cookie), + getCookiesForUrl: args => invokeWithNormalizedError('cookies.getCookiesForUrl', args), +}; + const grpc: gRPCBridgeAPI = { start: options => ipcRenderer.send('grpc.start', options), sendMessage: options => ipcRenderer.send('grpc.sendMessage', options), @@ -305,6 +313,7 @@ const main: Window['main'] = { ipcRenderer.on(channel, listener); return () => ipcRenderer.removeListener(channel, listener); }, + cookies, webSocket, socketIO, mcp, diff --git a/packages/insomnia/src/main/ipc/cookies.ts b/packages/insomnia/src/main/ipc/cookies.ts new file mode 100644 index 000000000000..11eb4f7ca91a --- /dev/null +++ b/packages/insomnia/src/main/ipc/cookies.ts @@ -0,0 +1,64 @@ +import { Cookie as ToughCookie, CookieJar } from 'tough-cookie'; + +import type { Cookie } from '~/insomnia-data'; + +import { ipcMainHandle } from './electron'; + +type CookieInput = Cookie | string; + +const parseCookieFromJSON = (cookie: CookieInput) => { + return typeof cookie === 'string' ? ToughCookie.fromJSON(cookie) : ToughCookie.fromJSON(cookie); +}; + +const cookieToString = (cookie: CookieInput) => { + const parsedCookie = parseCookieFromJSON(cookie); + + if (parsedCookie === null) { + throw new Error(`Unable to read cookie: ${cookie}`); + } + + let value = parsedCookie.toString(); + + if (parsedCookie.domain && parsedCookie.hostOnly) { + value += `; Domain=${parsedCookie.domain}`; + } + + return value; +}; + +const getCookiesForUrl = (cookies: Cookie[], url: string): Cookie[] => { + try { + const sanitized = cookies.map(c => ({ + ...c, + expires: c.expires === null || c.expires === undefined ? 'Infinity' : c.expires, + })); + const jar = CookieJar.fromJSON(JSON.stringify({ cookies: sanitized })); + jar.rejectPublicSuffixes = false; + jar.looseMode = true; + return jar.getCookiesSync(url).map(c => c.toJSON() as Cookie); + } catch { + return []; + } +}; + +export interface CookiesBridgeAPI { + fromJSON: (cookie: CookieInput) => Promise; + parse: (cookie: string) => Promise; + toString: (cookie: CookieInput) => Promise; + getCookiesForUrl: (args: { cookies: Cookie[]; url: string }) => Promise; +} + +export function registerCookieHandlers() { + ipcMainHandle('cookies.fromJSON', (_, cookie: CookieInput) => { + return parseCookieFromJSON(cookie)?.toJSON() as Cookie | null; + }); + ipcMainHandle('cookies.parse', (_, cookie: string) => { + return ToughCookie.parse(cookie, { loose: true })?.toJSON() as Cookie | null; + }); + ipcMainHandle('cookies.toString', (_, cookie: CookieInput) => { + return cookieToString(cookie); + }); + ipcMainHandle('cookies.getCookiesForUrl', (_, { cookies, url }: { cookies: Cookie[]; url: string }) => { + return getCookiesForUrl(cookies, url); + }); +} diff --git a/packages/insomnia/src/main/ipc/electron.ts b/packages/insomnia/src/main/ipc/electron.ts index ff49705936cd..d33da82a22d0 100644 --- a/packages/insomnia/src/main/ipc/electron.ts +++ b/packages/insomnia/src/main/ipc/electron.ts @@ -183,7 +183,11 @@ export type HandleChannels = | 'crypt.decryptAESToBuffer' | 'crypt.generateAES256Key' | 'sealedbox.keyPair' - | 'sealedbox.open'; + | 'sealedbox.open' + | 'cookies.fromJSON' + | 'cookies.parse' + | 'cookies.toString' + | 'cookies.getCookiesForUrl'; export const ipcMainHandle = ( channel: HandleChannels, diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index 672b2ac1814d..d6d632854f76 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -72,6 +72,7 @@ import { import type { SocketIOBridgeAPI } from '../network/socket-io'; import type { WebSocketBridgeAPI } from '../network/websocket'; import { registerPluginIpcHandlers } from '../plugin-window'; +import type { CookiesBridgeAPI } from './cookies'; import { ipcMainHandle, ipcMainOn, type RendererOnChannels } from './electron'; import type { electronStorageBridgeAPI } from './electron-storage'; import extractPostmanDataDumpHandler from './extract-postman-data-dump'; @@ -229,6 +230,7 @@ export interface RendererToMainBridgeAPI { cancelCurlRequest: typeof cancelCurlRequest; curlRequest: typeof curlRequest; on: (channel: RendererOnChannels, listener: (event: IpcRendererEvent, ...args: any[]) => void) => () => void; + cookies: CookiesBridgeAPI; webSocket: WebSocketBridgeAPI; socketIO: SocketIOBridgeAPI; mcp: McpBridgeAPI; diff --git a/packages/insomnia/src/ui/components/modals/cookies-modal.tsx b/packages/insomnia/src/ui/components/modals/cookies-modal.tsx index f06a626eb153..80baa249cec1 100644 --- a/packages/insomnia/src/ui/components/modals/cookies-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/cookies-modal.tsx @@ -1,7 +1,7 @@ import clone from 'clone'; import { isValid } from 'date-fns'; import type { Cookie, CookieJar } from 'insomnia-data'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Button, Dialog, @@ -19,13 +19,11 @@ import { TextField, } from 'react-aria-components'; import { useParams } from 'react-router'; -import { Cookie as ToughCookie } from 'tough-cookie'; import { v4 as uuidv4 } from 'uuid'; import { useUpdateCookieJarActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.update-cookie-jar'; import { OneLineEditor } from '~/ui/components/.client/codemirror/one-line-editor'; -import { cookieToString } from '../../../common/cookies'; import { fuzzyMatch } from '../../../common/misc'; import { useWorkspaceLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId'; import { useNunjucks } from '../../context/nunjucks/use-nunjucks'; @@ -269,13 +267,27 @@ export interface CookieListProps { const CookieList = ({ cookies, onCookieDelete, onUpdateCookie }: CookieListProps) => { const [cookieToEdit, setCookieToEdit] = useState(null); + const [cookieStrings, setCookieStrings] = useState>({}); + + useEffect(() => { + let cancelled = false; + Promise.all( + cookies.map(async cookie => ({ id: cookie.id, str: await window.main.cookies.toString(cookie).catch(() => '') })), + ).then(results => { + if (!cancelled) { + setCookieStrings(Object.fromEntries(results.map(({ id, str }) => [id, str]))); + } + }); + return () => { + cancelled = true; + }; + }, [cookies]); return ( <> {cookies.map((cookie, index) => { - const cookieJSON = ToughCookie.fromJSON(cookie); - const cookieString = cookieJSON ? cookieToString(cookieJSON) : ''; + const cookieString = cookieStrings[cookie.id] || ''; if (cookie.expires && !isValid(new Date(cookie.expires))) { cookie.expires = null; @@ -401,25 +413,20 @@ interface CookieModifyModalProps { const CookieModifyModal = ({ cookie, isOpen, setIsOpen, onUpdateCookie }: CookieModifyModalProps) => { const [editCookie, setEditCookie] = useState(cookie); + const [rawValue, setRawValue] = useState(''); + + useEffect(() => { + window.main.cookies + .toString(cookie) + .then(str => setRawValue(str)) + .catch(() => setRawValue('')); + }, [cookie]); let localDateTime: string; if (editCookie && editCookie.expires && isValid(new Date(editCookie.expires))) { localDateTime = new Date(editCookie.expires).toISOString().slice(0, 16); } - let rawDefaultValue; - if (!editCookie) { - rawDefaultValue = ''; - } else { - try { - const c = ToughCookie.fromJSON(JSON.stringify(editCookie)); - rawDefaultValue = c ? cookieToString(c) : ''; - } catch (err) { - console.warn('Failed to parse cookie string', err); - rawDefaultValue = ''; - } - } - return ( { + value={rawValue} + onChange={async event => { + const str = event.target.value; + setRawValue(str); try { - // NOTE: Perform toJSON so we have a plain JS object instead of Cookie instance - const parsed = ToughCookie.parse(event.target.value, { loose: true })?.toJSON(); + const parsed = await window.main.cookies.parse(str); if (parsed) { // Make sure cookie has an id and keep its host-only-flag parsed.id = editCookie.id; @@ -565,11 +574,9 @@ const CookieModifyModal = ({ cookie, isOpen, setIsOpen, onUpdateCookie }: Cookie setEditCookie(parsed as Cookie); } } catch (err) { - console.warn(`Failed to parse cookie string "${event.target.value}"`, err); - return; + console.warn(`Failed to parse cookie string "${str}"`, err); } }} - defaultValue={rawDefaultValue} /> diff --git a/packages/insomnia/vite.config.ts b/packages/insomnia/vite.config.ts index 54d8c6165ff9..9661110cce3d 100644 --- a/packages/insomnia/vite.config.ts +++ b/packages/insomnia/vite.config.ts @@ -7,7 +7,7 @@ import { defaultServerConditions, defineConfig } from 'vite'; import pkg from './package.json'; //These will be excluded from the bundle and remain as runtime dependencies -export const externalDependencies = ['@apidevtools/swagger-parser', 'mocha', 'tough-cookie']; +export const externalDependencies = ['@apidevtools/swagger-parser', 'mocha']; export default defineConfig(({ mode }) => { const __DEV__ = mode !== 'production'; From bcbf1fce7c324e9e00fa6e8cc1c0f7e3e23c7746 Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 22:45:58 +0200 Subject: [PATCH 25/47] util stub --- packages/insomnia/src/common/har.ts | 62 ++++++++++++++++++----------- packages/insomnia/vite.config.ts | 4 +- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/packages/insomnia/src/common/har.ts b/packages/insomnia/src/common/har.ts index 8ab9bece16eb..27b559b426bb 100644 --- a/packages/insomnia/src/common/har.ts +++ b/packages/insomnia/src/common/har.ts @@ -1,8 +1,7 @@ import type * as Har from 'har-format'; -import type { BaseModel, Environment, Request, RequestGroup, Response, Workspace } from 'insomnia-data'; -import { models, services } from 'insomnia-data'; -import { Cookie as ToughCookie } from 'tough-cookie'; +import type { BaseModel, Cookie, Environment, Request, RequestGroup, Response, Workspace } from '~/insomnia-data'; +import { models, services } from '~/insomnia-data'; import { applyRequestHooks } from '~/network/network-adapter'; import { RenderError } from '../templating/render-error'; @@ -10,7 +9,6 @@ import type { RenderedRequest } from '../templating/types'; import { parseGraphQLReqeustBody } from '../utils/graph-ql'; import { smartEncodeUrl } from '../utils/url/querystring'; import { getAppVersion } from './constants'; -import { jarFromCookies } from './cookies'; import { database } from './database'; import { filterHeaders, getSetCookieHeaders, hasAuthHeader } from './misc'; import { getRenderedRequestAndContext } from './render'; @@ -207,7 +205,7 @@ export async function exportHarResponse(response?: Response) { status: response.statusCode, statusText: response.statusMessage, httpVersion: 'HTTP/1.1', - cookies: getResponseCookies(response), + cookies: await getResponseCookies(response), headers: getResponseHeaders(response), content: await getResponseContent(response), redirectURL: '', @@ -292,7 +290,7 @@ export async function exportHarWithRenderedRequest(renderedRequest: RenderedRequ method: renderedRequest.method, url, httpVersion: 'HTTP/1.1', - cookies: getRequestCookies(renderedRequest), + cookies: await getRequestCookies(renderedRequest), headers: getRequestHeaders(renderedRequest), queryString: getRequestQueryString(renderedRequest), postData: await getRequestPostData(renderedRequest), @@ -301,37 +299,55 @@ export async function exportHarWithRenderedRequest(renderedRequest: RenderedRequ }; return harRequest; } - -function getRequestCookies(renderedRequest: RenderedRequest) { - // filter out invalid cookies to avoid getCookiesSync complaining +async function getRequestCookies(renderedRequest: RenderedRequest): Promise { + if (!renderedRequest.url) { + return []; + } + if (typeof window !== 'undefined' && window.main?.cookies) { + const domainCookies = await window.main.cookies.getCookiesForUrl({ + cookies: renderedRequest.cookieJar.cookies, + url: renderedRequest.url, + }); + return domainCookies.map(mapCookieToHar); + } + // Fallback for non-renderer contexts (tests, plugin window) + const { jarFromCookies } = await import('./cookies'); const jar = jarFromCookies(renderedRequest.cookieJar.cookies); - const domainCookies = renderedRequest.url ? jar.getCookiesSync(renderedRequest.url) : []; - const harCookies: Har.Cookie[] = domainCookies.map(mapCookie); - return harCookies; + const domainCookies = jar.getCookiesSync(renderedRequest.url); + return domainCookies.map(c => mapCookieToHar(c.toJSON() as Cookie)); } -export function getResponseCookiesFromHeaders(headers: Har.Cookie[]) { - return getSetCookieHeaders(headers).reduce((accumulator, harCookie) => { - let cookie: null | undefined | ToughCookie = null; - +export async function getResponseCookiesFromHeaders(headers: Har.Cookie[]): Promise { + const setCookieHeaders = getSetCookieHeaders(headers); + if (typeof window !== 'undefined' && window.main?.cookies) { + const results: Har.Cookie[] = []; + for (const harCookie of setCookieHeaders) { + const cookie = await window.main.cookies.parse(harCookie.value || ''); + if (cookie) { + results.push(mapCookieToHar(cookie)); + } + } + return results; + } + // Fallback for non-renderer contexts (tests, plugin window) + const { Cookie: ToughCookie } = await import('tough-cookie'); + return setCookieHeaders.reduce((accumulator, harCookie) => { + let cookie = null; try { cookie = ToughCookie.parse(harCookie.value || '', { loose: true }); } catch {} - - if (cookie === null || cookie === undefined) { + if (!cookie) { return accumulator; } - - return [...accumulator, mapCookie(cookie)]; + return [...accumulator, mapCookieToHar(cookie.toJSON() as Cookie)]; }, [] as Har.Cookie[]); } -function getResponseCookies(response: Response) { +async function getResponseCookies(response: Response): Promise { const headers = response.headers.filter(Boolean); return getResponseCookiesFromHeaders(headers); } - -function mapCookie(cookie: ToughCookie) { +function mapCookieToHar(cookie: ToughCookie): Har.Cookie { const harCookie: Har.Cookie = { name: cookie.key, value: cookie.value, diff --git a/packages/insomnia/vite.config.ts b/packages/insomnia/vite.config.ts index 9661110cce3d..fc26fa896512 100644 --- a/packages/insomnia/vite.config.ts +++ b/packages/insomnia/vite.config.ts @@ -60,6 +60,8 @@ export default defineConfig(({ mode }) => { 'path': path.resolve(__dirname, './src/path-shim.ts'), // Shim Node's `events` module for browser-safe dependencies (e.g. jshint uses EventEmitter). 'events': path.resolve(__dirname, '../../node_modules/events'), + // Shim Node's `util` module for browser-safe dependencies (e.g. tough-cookie uses util.inherits). + 'util': path.resolve(__dirname, '../../node_modules/util'), }, }, plugins: [ @@ -70,7 +72,7 @@ export default defineConfig(({ mode }) => { modules: [ 'electron', ...externalDependencies, - ...builtinModules.filter(m => m !== 'buffer' && m !== 'path'), + ...builtinModules.filter(m => m !== 'buffer' && m !== 'path' && m !== 'util'), ...builtinModules.map(m => `node:${m}`), ], }), From ed77673dfa360fa3e46ace07bd02f77631d9142f Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 23:13:48 +0200 Subject: [PATCH 26/47] split cookie into network adapter --- .../insomnia/src/common/__tests__/har.test.ts | 40 ++++++++++++- packages/insomnia/src/common/har.ts | 54 +++++++---------- packages/insomnia/src/entry.preload.ts | 1 + packages/insomnia/src/main/ipc/cookies.ts | 60 +++++++++++++++++++ packages/insomnia/src/main/ipc/electron.ts | 3 +- .../src/network/__tests__/network.test.ts | 5 ++ .../src/network/network-adapter.node.ts | 27 ++++++++- .../src/network/network-adapter.renderer.ts | 27 ++++++++- .../insomnia/src/network/network-adapter.ts | 1 + packages/insomnia/src/network/network.ts | 44 +++----------- ...Id.mock-server.mock-route.$mockRouteId.tsx | 8 +-- ...ionId.project.$projectId.workspace.new.tsx | 2 +- 12 files changed, 191 insertions(+), 81 deletions(-) diff --git a/packages/insomnia/src/common/__tests__/har.test.ts b/packages/insomnia/src/common/__tests__/har.test.ts index 9a0620973418..8858effe3980 100644 --- a/packages/insomnia/src/common/__tests__/har.test.ts +++ b/packages/insomnia/src/common/__tests__/har.test.ts @@ -1,6 +1,6 @@ import path from 'node:path'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('~/network/network-adapter', () => ({ getTimelinePath: () => Promise.resolve(''), @@ -12,20 +12,43 @@ vi.mock('~/network/network-adapter', () => ({ applyRequestHooks: (request: any) => Promise.resolve(request), applyResponseHooks: (response: any) => Promise.resolve(response), })); -import type { Cookie, Request, Response } from 'insomnia-data'; -import { models, services } from 'insomnia-data'; +vi.mock('~/utils/vault-adapter', () => ({ + decryptSecretValue: (value: any) => value, + encryptSecretValue: (value: any) => value, +})); + +import type { Cookie, Request, Response } from '~/insomnia-data'; +import { models, services } from '~/insomnia-data'; import { database as db } from '../../common/database'; import { exportHar, exportHarResponse, exportHarWithRequest } from '../har'; import { getRenderedRequestAndContext } from '../render'; +let cookieBridge: any; + describe('export', () => { beforeEach(async () => { + cookieBridge = { + fromJSON: vi.fn(), + parse: vi.fn().mockResolvedValue(null), + toString: vi.fn(), + getCookiesForUrl: vi.fn().mockResolvedValue([]), + addSetCookies: vi.fn().mockResolvedValue({ cookies: [], rejectedCookies: [] }), + }; + vi.stubGlobal('window', { + main: { + cookies: cookieBridge, + }, + }); await db.init({ inMemoryOnly: true }, true); await services.project.list(); await services.settings.getOrCreate(); }); + afterEach(() => { + vi.unstubAllGlobals(); + }); + describe('exportHar()', () => { it('exports single requests', async () => { const wrk = await services.workspace.create({ @@ -360,6 +383,16 @@ describe('export', () => { contentType: 'application/json', bodyPath: path.join(__dirname, '../__fixtures__/har/test-response.json'), }; + cookieBridge.parse.mockResolvedValueOnce({ + id: '', + key: 'sessionid', + value: '12345', + expires: null, + domain: '', + path: '/', + secure: false, + httpOnly: true, + }); const harResponse = await exportHarResponse(response); expect(harResponse).toMatchObject({ status: 200, @@ -454,6 +487,7 @@ describe('export', () => { }, }; const { request: renderedRequest } = await getRenderedRequestAndContext({ request }); + cookieBridge.getCookiesForUrl.mockResolvedValue(cookies); const har = await exportHarWithRequest(renderedRequest); expect(har.cookies.length).toBe(1); expect(har).toEqual({ diff --git a/packages/insomnia/src/common/har.ts b/packages/insomnia/src/common/har.ts index 27b559b426bb..50deed740d00 100644 --- a/packages/insomnia/src/common/har.ts +++ b/packages/insomnia/src/common/har.ts @@ -303,51 +303,31 @@ async function getRequestCookies(renderedRequest: RenderedRequest): Promise mapCookieToHar(c.toJSON() as Cookie)); + const domainCookies = await getCookieBridge().getCookiesForUrl({ + cookies: renderedRequest.cookieJar.cookies, + url: renderedRequest.url, + }); + return domainCookies.map(mapCookieToHar); } export async function getResponseCookiesFromHeaders(headers: Har.Cookie[]): Promise { const setCookieHeaders = getSetCookieHeaders(headers); - if (typeof window !== 'undefined' && window.main?.cookies) { - const results: Har.Cookie[] = []; - for (const harCookie of setCookieHeaders) { - const cookie = await window.main.cookies.parse(harCookie.value || ''); - if (cookie) { - results.push(mapCookieToHar(cookie)); - } + const results: Har.Cookie[] = []; + const cookiesBridge = getCookieBridge(); + for (const harCookie of setCookieHeaders) { + const cookie = await cookiesBridge.parse(harCookie.value || ''); + if (cookie) { + results.push(mapCookieToHar(cookie)); } - return results; } - // Fallback for non-renderer contexts (tests, plugin window) - const { Cookie: ToughCookie } = await import('tough-cookie'); - return setCookieHeaders.reduce((accumulator, harCookie) => { - let cookie = null; - try { - cookie = ToughCookie.parse(harCookie.value || '', { loose: true }); - } catch {} - if (!cookie) { - return accumulator; - } - return [...accumulator, mapCookieToHar(cookie.toJSON() as Cookie)]; - }, [] as Har.Cookie[]); + return results; } async function getResponseCookies(response: Response): Promise { const headers = response.headers.filter(Boolean); return getResponseCookiesFromHeaders(headers); } -function mapCookieToHar(cookie: ToughCookie): Har.Cookie { +function mapCookieToHar(cookie: Cookie): Har.Cookie { const harCookie: Har.Cookie = { name: cookie.key, value: cookie.value, @@ -389,6 +369,14 @@ function mapCookieToHar(cookie: ToughCookie): Har.Cookie { return harCookie; } +function getCookieBridge() { + if (typeof window === 'undefined' || !window.main?.cookies) { + throw new Error('window.main.cookies is required for cookie handling'); + } + + return window.main.cookies; +} + async function getResponseContent(response: Response) { let body = await services.helpers.getResponseBodyBuffer(response); diff --git a/packages/insomnia/src/entry.preload.ts b/packages/insomnia/src/entry.preload.ts index ab2e6bf42fd2..b4f0642503f4 100644 --- a/packages/insomnia/src/entry.preload.ts +++ b/packages/insomnia/src/entry.preload.ts @@ -121,6 +121,7 @@ const cookies: CookiesBridgeAPI = { parse: cookie => invokeWithNormalizedError('cookies.parse', cookie), toString: cookie => invokeWithNormalizedError('cookies.toString', cookie), getCookiesForUrl: args => invokeWithNormalizedError('cookies.getCookiesForUrl', args), + addSetCookies: args => invokeWithNormalizedError('cookies.addSetCookies', args), }; const grpc: gRPCBridgeAPI = { diff --git a/packages/insomnia/src/main/ipc/cookies.ts b/packages/insomnia/src/main/ipc/cookies.ts index 11eb4f7ca91a..78690916aa31 100644 --- a/packages/insomnia/src/main/ipc/cookies.ts +++ b/packages/insomnia/src/main/ipc/cookies.ts @@ -6,6 +6,17 @@ import { ipcMainHandle } from './electron'; type CookieInput = Cookie | string; +interface AddSetCookiesArgs { + setCookieStrings: string[]; + currentUrl: string; + cookieJar: Cookie[]; +} + +interface AddSetCookiesResult { + cookies: Cookie[]; + rejectedCookies: string[]; +} + const parseCookieFromJSON = (cookie: CookieInput) => { return typeof cookie === 'string' ? ToughCookie.fromJSON(cookie) : ToughCookie.fromJSON(cookie); }; @@ -41,11 +52,57 @@ const getCookiesForUrl = (cookies: Cookie[], url: string): Cookie[] => { } }; +const addSetCookiesToToughCookieJar = ({ + setCookieStrings, + currentUrl, + cookieJar, +}: AddSetCookiesArgs): AddSetCookiesResult => { + const rejectedCookies: string[] = []; + try { + const cookieJarWithDefaults = CookieJar.fromJSON( + JSON.stringify({ + cookies: cookieJar.map(c => ({ + ...c, + expires: c.expires === null || c.expires === undefined ? 'Infinity' : c.expires, + })), + }), + ); + + cookieJarWithDefaults.rejectPublicSuffixes = false; + cookieJarWithDefaults.looseMode = true; + + for (const setCookieStr of setCookieStrings) { + try { + cookieJarWithDefaults.setCookieSync(setCookieStr, currentUrl); + } catch (err) { + if (err instanceof Error) { + rejectedCookies.push(err.message); + } + } + } + + return { + cookies: cookieJarWithDefaults.getCookiesSync(currentUrl).map(c => c.toJSON() as Cookie), + rejectedCookies, + }; + } catch (error) { + if (error instanceof Error) { + rejectedCookies.push(error.message); + } + + return { + cookies: [], + rejectedCookies, + }; + } +}; + export interface CookiesBridgeAPI { fromJSON: (cookie: CookieInput) => Promise; parse: (cookie: string) => Promise; toString: (cookie: CookieInput) => Promise; getCookiesForUrl: (args: { cookies: Cookie[]; url: string }) => Promise; + addSetCookies: (args: AddSetCookiesArgs) => Promise; } export function registerCookieHandlers() { @@ -61,4 +118,7 @@ export function registerCookieHandlers() { ipcMainHandle('cookies.getCookiesForUrl', (_, { cookies, url }: { cookies: Cookie[]; url: string }) => { return getCookiesForUrl(cookies, url); }); + ipcMainHandle('cookies.addSetCookies', (_, args: AddSetCookiesArgs) => { + return addSetCookiesToToughCookieJar(args); + }); } diff --git a/packages/insomnia/src/main/ipc/electron.ts b/packages/insomnia/src/main/ipc/electron.ts index d33da82a22d0..81d0a6eacc12 100644 --- a/packages/insomnia/src/main/ipc/electron.ts +++ b/packages/insomnia/src/main/ipc/electron.ts @@ -187,7 +187,8 @@ export type HandleChannels = | 'cookies.fromJSON' | 'cookies.parse' | 'cookies.toString' - | 'cookies.getCookiesForUrl'; + | 'cookies.getCookiesForUrl' + | 'cookies.addSetCookies'; export const ipcMainHandle = ( channel: HandleChannels, diff --git a/packages/insomnia/src/network/__tests__/network.test.ts b/packages/insomnia/src/network/__tests__/network.test.ts index 6a9a2c790fd2..a2c65100ec40 100644 --- a/packages/insomnia/src/network/__tests__/network.test.ts +++ b/packages/insomnia/src/network/__tests__/network.test.ts @@ -17,6 +17,11 @@ import { DEFAULT_BOUNDARY } from '../multipart-constants'; import * as networkUtils from '../network'; import { getAuthQueryParams, getSetCookiesFromResponseHeaders } from '../network'; +vi.mock('~/utils/vault-adapter', () => ({ + decryptSecretValue: (value: any) => value, + encryptSecretValue: (value: any) => value, +})); + const getRenderedRequest = async (args: Parameters[0]) => (await getRenderedRequestAndContext(args)).request; describe('getAuthQueryParams', () => { diff --git a/packages/insomnia/src/network/network-adapter.node.ts b/packages/insomnia/src/network/network-adapter.node.ts index ee830298fe84..e1a351ce9bec 100644 --- a/packages/insomnia/src/network/network-adapter.node.ts +++ b/packages/insomnia/src/network/network-adapter.node.ts @@ -2,8 +2,8 @@ import fs from 'node:fs'; import nodePath from 'node:path'; import clone from 'clone'; -import type { RequestHeader } from 'insomnia-data'; +import type { Cookie, RequestHeader } from '~/insomnia-data'; import type { RenderedRequest } from '~/templating/types'; import type { RequestContext } from '../../../insomnia-scripting-environment/src/objects'; @@ -18,6 +18,7 @@ import * as pluginResponse from '../plugins/context/response'; import * as pluginStore from '../plugins/context/store'; import { runScript as executeScript } from '../script-executor'; import { applyDefaultHeaders } from './apply-default-headers'; +import { addSetCookiesToToughCookieJar } from './set-cookie-util'; export const getTimelinePath = async (responseId: string): Promise => { const electron = require('electron') as { app: { getPath: (name: string) => string } }; @@ -42,6 +43,30 @@ export const getAuthHeader = (r: RenderedRequest, u: string): Promise => curlRequest(options); +export async function extractCookies({ + setCookieStrings, + currentUrl, + cookieJar, + settingStoreCookies, +}: { + setCookieStrings: string[]; + currentUrl: string; + cookieJar: { cookies: Cookie[] }; + settingStoreCookies: boolean; +}) { + if (!settingStoreCookies || !setCookieStrings.length) { + return { cookies: [], rejectedCookies: [], totalSetCookies: 0 }; + } + + const { cookies, rejectedCookies } = await addSetCookiesToToughCookieJar({ + setCookieStrings, + currentUrl, + cookieJar, + }); + + return { cookies, rejectedCookies, totalSetCookies: setCookieStrings.length }; +} + export const runScript = (options: { script: string; context: RequestContext; diff --git a/packages/insomnia/src/network/network-adapter.renderer.ts b/packages/insomnia/src/network/network-adapter.renderer.ts index 1cb1ade79168..821fb3850d3c 100644 --- a/packages/insomnia/src/network/network-adapter.renderer.ts +++ b/packages/insomnia/src/network/network-adapter.renderer.ts @@ -1,5 +1,4 @@ -import type { RequestHeader } from 'insomnia-data'; - +import type { Cookie, RequestHeader } from '~/insomnia-data'; import { plugins as pluginsBridge } from '~/plugins/renderer-bridge'; import type { RenderedRequest } from '~/templating/types'; @@ -22,6 +21,30 @@ export const getAuthHeader = (r: RenderedRequest, u: string): Promise cancellableCurlRequest(options); +export async function extractCookies({ + setCookieStrings, + currentUrl, + cookieJar, + settingStoreCookies, +}: { + setCookieStrings: string[]; + currentUrl: string; + cookieJar: { cookies: Cookie[] }; + settingStoreCookies: boolean; +}) { + if (!settingStoreCookies || !setCookieStrings.length) { + return { cookies: [], rejectedCookies: [], totalSetCookies: 0 }; + } + + const { cookies, rejectedCookies } = await window.main.cookies.addSetCookies({ + setCookieStrings, + currentUrl, + cookieJar: cookieJar.cookies, + }); + + return { cookies, rejectedCookies, totalSetCookies: setCookieStrings.length }; +} + export const runScript = (options: { script: string; context: RequestContext; diff --git a/packages/insomnia/src/network/network-adapter.ts b/packages/insomnia/src/network/network-adapter.ts index d1593da4f33b..037ced29f96a 100644 --- a/packages/insomnia/src/network/network-adapter.ts +++ b/packages/insomnia/src/network/network-adapter.ts @@ -12,6 +12,7 @@ export const { appendTimelineLines, getAuthHeader, executeCurlRequest, + extractCookies, runScript, applyRequestHooks, applyResponseHooks, diff --git a/packages/insomnia/src/network/network.ts b/packages/insomnia/src/network/network.ts index 60ee8c1b5ec9..57c4ebbbe43a 100644 --- a/packages/insomnia/src/network/network.ts +++ b/packages/insomnia/src/network/network.ts @@ -30,6 +30,7 @@ import { applyRequestHooks, applyResponseHooks, executeCurlRequest, + extractCookies, getAuthHeader, getTimelinePath, runScript, @@ -43,7 +44,7 @@ import { database as db } from '../common/database'; import { generateId, getContentTypeHeader, getLocationHeader, getSetCookieHeaders } from '../common/misc'; import { getRenderedRequestAndContext } from '../common/render'; import { ascendingFirstIndexStringSort } from '../common/sorting'; -import type { HeaderResult, ResponsePatch } from '../main/network/libcurl-promise'; +import type { ResponsePatch } from '../main/network/libcurl-promise'; import { RenderError } from '../templating/render-error'; import type { RenderedRequest, RenderPurpose } from '../templating/types'; import { maskOrDecryptVaultDataIfNecessary } from '../templating/utils'; @@ -52,7 +53,6 @@ import { QUERY_PARAMS } from './api-key/constants'; import { getAuthObjectOrNull, isAuthEnabled } from './authentication'; import { filterClientCertificates } from './certificate'; import type { TransformedExecuteScriptContext } from './concurrency'; -import { addSetCookiesToToughCookieJar } from './set-cookie-util'; const { isRequest } = models.request; const { isRequestGroup } = models.requestGroup; @@ -893,12 +893,12 @@ export async function sendCurlAndWriteTimeline( // todo: move to main process debugTimeline.forEach(entry => timeline.push(entry)); // transform output - const { cookies, rejectedCookies, totalSetCookies } = await extractCookies( - headerResults, - renderedRequest.cookieJar, - finalUrl, - renderedRequest.settingStoreCookies, - ); + const { cookies, rejectedCookies, totalSetCookies } = await extractCookies({ + setCookieStrings: headerResults.flatMap(({ headers }: any) => getSetCookiesFromResponseHeaders(headers)), + currentUrl: getCurrentUrl({ headerResults, finalUrl }), + cookieJar: renderedRequest.cookieJar, + settingStoreCookies: renderedRequest.settingStoreCookies, + }); rejectedCookies.forEach(errorMessage => timeline.push({ value: `Rejected cookie: ${errorMessage}`, name: 'Text', timestamp: Date.now() }), ); @@ -1006,34 +1006,6 @@ export const transformUrl = ( return { finalUrl: `${protocol}//${socketUrl}`, socketPath }; }; -const extractCookies = async ( - headerResults: HeaderResult[], - cookieJar: any, - finalUrl: string, - settingStoreCookies: boolean, -) => { - // add set-cookie headers to file(cookiejar) and database - if (settingStoreCookies) { - // supports many set-cookies over many redirects - const redirects: string[][] = headerResults.map(({ headers }: any) => getSetCookiesFromResponseHeaders(headers)); - const setCookieStrings: string[] = redirects.flat(); - const totalSetCookies = setCookieStrings.length; - if (totalSetCookies) { - const currentUrl = getCurrentUrl({ headerResults, finalUrl }); - const { cookies, rejectedCookies } = await addSetCookiesToToughCookieJar({ - setCookieStrings, - currentUrl, - cookieJar, - }); - const hasCookiesToPersist = totalSetCookies > rejectedCookies.length; - if (hasCookiesToPersist) { - return { cookies, rejectedCookies, totalSetCookies }; - } - } - } - return { cookies: [], rejectedCookies: [], totalSetCookies: 0 }; -}; - export const getSetCookiesFromResponseHeaders = (headers: any[]) => getSetCookieHeaders(headers).map(h => h.value); export const getCurrentUrl = ({ headerResults, finalUrl }: { headerResults: any; finalUrl: string }): string => { diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.tsx index 7191ac28baed..d7f7ba3c8138 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.tsx @@ -87,7 +87,7 @@ export const isInMockContentTypeList = (contentType: string): boolean => Boolean(contentType && mockContentTypes.includes(contentType)); // mockbin expect a HAR response structure -export const mockRouteToHar = ({ +export const mockRouteToHar = async ({ statusCode, statusText, mimeType, @@ -99,14 +99,14 @@ export const mockRouteToHar = ({ mimeType: string; headersArray: RequestHeader[]; body: string; -}): Har.Response => { +}): Promise => { const validHeaders = headersArray.filter(({ name }) => !!name); return { status: +statusCode, statusText: statusText || RESPONSE_CODE_REASONS[+statusCode] || '', httpVersion: 'HTTP/1.1', headers: validHeaders, - cookies: getResponseCookiesFromHeaders(validHeaders), + cookies: await getResponseCookiesFromHeaders(validHeaders), content: { size: Buffer.byteLength(body), mimeType, @@ -168,7 +168,7 @@ export const MockRouteRoute = () => { organizationId, sessionId: userSession.id, method: mockRoute.method, - data: mockRouteToHar({ + data: await mockRouteToHar({ statusCode: mockRoute.statusCode, statusText: mockRoute.statusText, headersArray: mockRoute.headers, diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.new.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.new.tsx index 654ecf827bd3..cb0a7aca6621 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.new.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.new.tsx @@ -479,7 +479,7 @@ async function createMockRoutes( organizationId, sessionId, method: route.method, - data: mockRouteToHar({ + data: await mockRouteToHar({ statusCode: mockRoute.statusCode, statusText: mockRoute.statusText || '', headersArray: mockRoute.headers, From 799e2bcb31c664b4802ce297ea06f9ad096d486b Mon Sep 17 00:00:00 2001 From: jackkav Date: Mon, 1 Jun 2026 23:22:55 +0200 Subject: [PATCH 27/47] assert --- package-lock.json | 30 ++++++++++++++++++++++++++++++ packages/insomnia/package.json | 1 + packages/insomnia/vite.config.ts | 24 ++++++++++++++++++++---- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4b8bcd6ee55c..50c210024823 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11986,6 +11986,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -18974,6 +18987,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-natural-number": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", @@ -29190,6 +29219,7 @@ "acorn-walk": "^8.3.5", "ajv": "^8.17.1", "apiconnect-wsdl": "2.0.36", + "assert": "^2.1.0", "aws4": "^1.13.2", "blakejs": "^1.2.1", "buffer": "^6.0.3", diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index 0141550301ac..53ae7bee3153 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -72,6 +72,7 @@ "acorn": "^8.16.0", "acorn-walk": "^8.3.5", "ajv": "^8.17.1", + "assert": "^2.1.0", "apiconnect-wsdl": "2.0.36", "aws4": "^1.13.2", "blakejs": "^1.2.1", diff --git a/packages/insomnia/vite.config.ts b/packages/insomnia/vite.config.ts index fc26fa896512..661964173cba 100644 --- a/packages/insomnia/vite.config.ts +++ b/packages/insomnia/vite.config.ts @@ -10,6 +10,8 @@ import pkg from './package.json'; export const externalDependencies = ['@apidevtools/swagger-parser', 'mocha']; export default defineConfig(({ mode }) => { const __DEV__ = mode !== 'production'; + const browserSafeBuiltinModules = new Set(['assert', 'buffer', 'events', 'path', 'util']); + const nodeBuiltinModules = builtinModules.filter(m => !browserSafeBuiltinModules.has(m)); return { define: { @@ -58,10 +60,19 @@ export default defineConfig(({ mode }) => { '~': path.resolve(__dirname, './src'), // Shim Node's `path` module for browser-safe dependencies (e.g. mime-types uses path.extname). 'path': path.resolve(__dirname, './src/path-shim.ts'), + 'node:path': path.resolve(__dirname, './src/path-shim.ts'), + // Shim Node's `assert` module for browser-safe dependencies that still use runtime invariants. + 'assert': path.resolve(__dirname, '../../node_modules/assert'), + 'node:assert': path.resolve(__dirname, '../../node_modules/assert'), // Shim Node's `events` module for browser-safe dependencies (e.g. jshint uses EventEmitter). 'events': path.resolve(__dirname, '../../node_modules/events'), + 'node:events': path.resolve(__dirname, '../../node_modules/events'), // Shim Node's `util` module for browser-safe dependencies (e.g. tough-cookie uses util.inherits). 'util': path.resolve(__dirname, '../../node_modules/util'), + 'node:util': path.resolve(__dirname, '../../node_modules/util'), + // Buffer is also browser-safe in this renderer bundle, so keep it bundled instead of externalized. + 'buffer': path.resolve(__dirname, '../../node_modules/buffer'), + 'node:buffer': path.resolve(__dirname, '../../node_modules/buffer'), }, }, plugins: [ @@ -72,8 +83,8 @@ export default defineConfig(({ mode }) => { modules: [ 'electron', ...externalDependencies, - ...builtinModules.filter(m => m !== 'buffer' && m !== 'path' && m !== 'util'), - ...builtinModules.map(m => `node:${m}`), + ...nodeBuiltinModules, + ...nodeBuiltinModules.map(m => `node:${m}`), ], }), reactRouter(), @@ -108,6 +119,7 @@ export interface Options { */ export function electronNodeRequire(options: Options): Plugin { const { modules = [] } = options; + const getExternalId = (id: string) => id.split('virtual:external:')[1]?.split('?')[0]; return { name: 'vite-plugin-electron-node-require', @@ -137,7 +149,7 @@ export function electronNodeRequire(options: Options): Plugin { return conf; }, resolveId(id, _importer, options) { - const externalId = id.split('virtual:external:')[1]; + const externalId = getExternalId(id); if (externalId && modules.includes(externalId)) { if (options.ssr) { @@ -153,7 +165,11 @@ export function electronNodeRequire(options: Options): Plugin { }, load(id, options) { if (id.includes('virtual:external:')) { - const externalId = id.split('virtual:external:')[1]; + const externalId = getExternalId(id); + + if (!externalId) { + return null; + } // We need to handle electron because it's different when required in the renderer process if (externalId === 'electron') { From 3fb117456867e417408def3aa92a7fdca7889f07 Mon Sep 17 00:00:00 2001 From: jackkav Date: Tue, 2 Jun 2026 14:03:59 +0200 Subject: [PATCH 28/47] fix plugin index import --- packages/insomnia-inso/esbuild.ts | 14 +++++++++++++- packages/insomnia/esbuild.entrypoints.ts | 11 +++++++++++ packages/insomnia/src/network/network-adapter.ts | 14 ++++---------- packages/insomnia/src/plugins/index.ts | 14 +++++++++----- packages/insomnia/src/templating/render-adapter.ts | 13 +++---------- packages/insomnia/src/utils/vault-adapter.ts | 12 +++--------- packages/insomnia/vite.config.ts | 3 +-- 7 files changed, 44 insertions(+), 37 deletions(-) diff --git a/packages/insomnia-inso/esbuild.ts b/packages/insomnia-inso/esbuild.ts index 1c74bb67f3fb..7dcff5d82881 100644 --- a/packages/insomnia-inso/esbuild.ts +++ b/packages/insomnia-inso/esbuild.ts @@ -1,11 +1,22 @@ import fs from 'node:fs'; +import path from 'node:path'; -import { analyzeMetafile, build, type BuildOptions, context } from 'esbuild'; +import { analyzeMetafile, build, type BuildOptions, context, type Plugin } from 'esbuild'; const isProd = Boolean(process.env.NODE_ENV === 'production'); const watch = Boolean(process.env.ESBUILD_WATCH); const isDebug = Boolean(process.env.DEBUG); const version = process.env.VERSION || 'dev'; +// Redirects *.renderer imports to their *.node equivalents for node/CLI builds. +const rendererToNodePlugin: Plugin = { + name: 'renderer-to-node', + setup(build) { + build.onResolve({ filter: /\.renderer$/ }, args => ({ + path: path.resolve(args.resolveDir, args.path.replace('.renderer', '.node') + '.ts'), + })); + }, +}; + const config: BuildOptions = { outfile: './dist/index.js', bundle: true, @@ -20,6 +31,7 @@ const config: BuildOptions = { electron: '../insomnia/send-request/electron', }, plugins: [ + rendererToNodePlugin, // taken from https://github.com/tjx666/awesome-vscode-extension-boilerplate/blob/main/scripts/esbuild.ts { name: 'umd2esm', diff --git a/packages/insomnia/esbuild.entrypoints.ts b/packages/insomnia/esbuild.entrypoints.ts index 9ee37e20b17a..7aa918fe4e7b 100644 --- a/packages/insomnia/esbuild.entrypoints.ts +++ b/packages/insomnia/esbuild.entrypoints.ts @@ -4,6 +4,16 @@ import path from 'node:path'; import esbuild, { type BuildOptions, type Plugin } from 'esbuild'; +// Redirects *.renderer imports to their *.node equivalents for node/main-process builds. +const rendererToNodePlugin: Plugin = { + name: 'renderer-to-node', + setup(build) { + build.onResolve({ filter: /\.renderer$/ }, args => ({ + path: path.resolve(args.resolveDir, args.path.replace('.renderer', '.node') + '.ts'), + })); + }, +}; + import pkg from './package.json'; interface Options { mode?: 'development' | 'production'; @@ -105,6 +115,7 @@ export default async function build(options: Options) { platform: 'node', sourcemap: true, format: 'cjs', + plugins: [rendererToNodePlugin], define: { ...env, // Electron main = "browser" diff --git a/packages/insomnia/src/network/network-adapter.ts b/packages/insomnia/src/network/network-adapter.ts index 037ced29f96a..2baf1ff69bc0 100644 --- a/packages/insomnia/src/network/network-adapter.ts +++ b/packages/insomnia/src/network/network-adapter.ts @@ -1,12 +1,6 @@ -// Runtime adapter selection: renderer uses IPC bridge, node uses libcurl directly. -// Vite production inlines process.type='renderer' so Rollup tree-shakes the node branch. -import type * as AdapterType from './network-adapter.renderer'; - -const impl = ( - (process as any).type === 'renderer' ? require('./network-adapter.renderer') : require('./network-adapter.node') -) as typeof AdapterType; - -export const { +// Imports the renderer implementation by default. +// esbuild node builds alias this to network-adapter.node via the renderer-to-node plugin. +export { getTimelinePath, appendToTimelineOnError, appendTimelineLines, @@ -16,4 +10,4 @@ export const { runScript, applyRequestHooks, applyResponseHooks, -} = impl; +} from './network-adapter.renderer'; diff --git a/packages/insomnia/src/plugins/index.ts b/packages/insomnia/src/plugins/index.ts index cc7ad7fa31be..381f8bbb12a5 100644 --- a/packages/insomnia/src/plugins/index.ts +++ b/packages/insomnia/src/plugins/index.ts @@ -128,9 +128,10 @@ export async function getPlugins(force = false): Promise { }); // Make sure the default directories exist - // imported lazy becuase this should only be read in plugin hidden window - - const pluginPath = path.resolve(process.env['INSOMNIA_DATA_PATH'] || electron.app.getPath('userData'), 'plugins'); + const pluginPath = path.resolve( + process.env['INSOMNIA_DATA_PATH'] || (process.type === 'renderer' ? window : electron).app.getPath('userData'), + 'plugins', + ); // Also look in node_modules folder in each directory const basePaths = [pluginPath, ...extraPaths]; @@ -148,7 +149,7 @@ export async function getPlugins(force = false): Promise { return plugins; } -export function getBundlePluginMap() { +function getBundlePluginMap() { const appBundlePlugins = getAppBundlePlugins(); const bundlePluginMap: Record = {}; appBundlePlugins.forEach(({ name: pluginName }) => { @@ -297,7 +298,10 @@ export function getPluginCommonContext({ ...pluginStore.init(plugin), ...pluginNetwork.init(), util: { - openInBrowser: (url: string) => electron.shell.openExternal(url), + openInBrowser: async (url: string) => + process.type === 'renderer' || process.type === 'worker' + ? window.main.openInBrowser(url) + : electron.shell.openExternal(url), models: { request: { getById: services.request.getById, diff --git a/packages/insomnia/src/templating/render-adapter.ts b/packages/insomnia/src/templating/render-adapter.ts index 60c3c8e85596..9d3f5af1ae7d 100644 --- a/packages/insomnia/src/templating/render-adapter.ts +++ b/packages/insomnia/src/templating/render-adapter.ts @@ -1,10 +1,3 @@ -// Runtime adapter selection: renderer delegates to the templating worker, node/CLI uses the node implementation. -// Vite inlines process.type at build time so Rollup tree-shakes the unused branch from each bundle. -// process.type is 'renderer' in Electron renderer builds and undefined in Node.js/inso — no cast needed at runtime. -import type * as AdapterType from './render-adapter.node'; - -const impl = ( - (process as any).type === 'renderer' ? require('./render-adapter.renderer') : require('./render-adapter.node') -) as typeof AdapterType; - -export const { renderTemplate } = impl; +// Imports the renderer implementation by default. +// esbuild node builds alias this to render-adapter.node via the renderer-to-node plugin. +export { renderTemplate } from './render-adapter.renderer'; diff --git a/packages/insomnia/src/utils/vault-adapter.ts b/packages/insomnia/src/utils/vault-adapter.ts index 3e75d3dfdbc4..a85286b0f609 100644 --- a/packages/insomnia/src/utils/vault-adapter.ts +++ b/packages/insomnia/src/utils/vault-adapter.ts @@ -1,9 +1,3 @@ -// Runtime adapter selection: renderer delegates to IPC, node/CLI uses direct crypto. -// Vite inlines process.type at build time so Rollup tree-shakes the unused branch from each bundle. -import type * as AdapterType from './vault-adapter.node'; - -const impl = ( - (process as any).type === 'renderer' ? require('./vault-adapter.renderer') : require('./vault-adapter.node') -) as typeof AdapterType; - -export const { encryptSecretValue, decryptSecretValue } = impl; +// Imports the renderer implementation by default. +// esbuild node builds alias this to vault-adapter.node via the renderer-to-node plugin. +export { encryptSecretValue, decryptSecretValue } from './vault-adapter.renderer'; diff --git a/packages/insomnia/vite.config.ts b/packages/insomnia/vite.config.ts index 661964173cba..0fce97446745 100644 --- a/packages/insomnia/vite.config.ts +++ b/packages/insomnia/vite.config.ts @@ -51,8 +51,7 @@ export default defineConfig(({ mode }) => { }, resolve: { alias: { - // Resolve these adapters to their renderer variants so both client and server - // builds inline the module directly (avoids runtime require() in server bundle). + // Short-circuit the adapter wrappers to the renderer implementation directly. // These must appear before the '~' catch-all so the specific path wins. '~/network/network-adapter': path.resolve(__dirname, './src/network/network-adapter.renderer'), '~/templating/render-adapter': path.resolve(__dirname, './src/templating/render-adapter.renderer'), From 3b2ef888bd43017f1134c74bef6fe7b79fea2a29 Mon Sep 17 00:00:00 2001 From: jackkav Date: Tue, 2 Jun 2026 18:49:06 +0200 Subject: [PATCH 29/47] serialise cookie --- .../insomnia/src/main/templating-worker-database.ts | 2 +- .../insomnia/src/templating/liquid-extension.ts | 2 +- packages/insomnia/src/templating/types.ts | 13 ++++++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/insomnia/src/main/templating-worker-database.ts b/packages/insomnia/src/main/templating-worker-database.ts index 36254d8e4302..1d523d7aba12 100644 --- a/packages/insomnia/src/main/templating-worker-database.ts +++ b/packages/insomnia/src/main/templating-worker-database.ts @@ -103,7 +103,7 @@ const pluginToMainAPI: Record Promise< 'cookieJar.getCookiesForUrl': async (body: { parentId: string; url: string }) => { const cookies = await services.cookieJar.getOrCreateForParentId(body.parentId); const jar = jarFromCookies(cookies.cookies); - return jar.getCookiesSync(body.url); + return jar.getCookiesSync(body.url).map(c => c.toJSON()); }, 'response.getLatestForRequestId': async (body: { requestId: string; environmentId: string }) => { return await services.response.getLatestForRequestId(body.requestId, body.environmentId); diff --git a/packages/insomnia/src/templating/liquid-extension.ts b/packages/insomnia/src/templating/liquid-extension.ts index 818222d7a229..1cbbfb21c8f9 100644 --- a/packages/insomnia/src/templating/liquid-extension.ts +++ b/packages/insomnia/src/templating/liquid-extension.ts @@ -96,7 +96,7 @@ export function createLiquidTag( getCookiesForUrl: async (parentId: string, url: string) => { const cookies = await services.cookieJar.getOrCreateForParentId(parentId); const jar = jarFromCookies(cookies.cookies); - return jar.getCookiesSync(url); + return jar.getCookiesSync(url).map(c => c.toJSON()); }, }, response: { diff --git a/packages/insomnia/src/templating/types.ts b/packages/insomnia/src/templating/types.ts index e788e78ac66a..fe6acb255309 100644 --- a/packages/insomnia/src/templating/types.ts +++ b/packages/insomnia/src/templating/types.ts @@ -51,6 +51,17 @@ export interface PluginStore { >; } +export interface SerializedCookie { + key?: string; + value?: string; + domain?: string; + path?: string; + expires?: string | Date; + httpOnly?: boolean; + secure?: boolean; + hostOnly?: boolean; +} + export type RenderPurpose = 'send' | 'general' | 'preview' | 'script' | 'no-render'; export type PluginToMainAPIPaths = | 'readFile' @@ -320,7 +331,7 @@ export interface PluginTemplateTagContext { oAuth2Token: { getByRequestId: (id: string) => Promise }; cookieJar: { getOrCreateForParentId: (parentId: string) => Promise; - getCookiesForUrl: (parentId: string, url: string) => Promise; + getCookiesForUrl: (parentId: string, url: string) => Promise; }; response: { getLatestForRequestId: Services['response']['getLatestForRequestId']; From 58ef319cb40a9618c50aadf1a98586dbfd1cf77a Mon Sep 17 00:00:00 2001 From: jackkav Date: Tue, 2 Jun 2026 19:52:49 +0200 Subject: [PATCH 30/47] decouple renderer from scripting --- .../package.json | 3 +- .../scripts/generate-autocomplete.ts | 135 ++++++++++++++++++ .../src/autocomplete-snippets.json | 88 ++++++++++++ ...projectId.workspace.$workspaceId.debug.tsx | 2 +- .../editors/request-script-editor.tsx | 134 +---------------- .../components/panes/request-group-pane.tsx | 4 +- .../src/ui/components/panes/request-pane.tsx | 2 - 7 files changed, 229 insertions(+), 139 deletions(-) create mode 100644 packages/insomnia-scripting-environment/scripts/generate-autocomplete.ts create mode 100644 packages/insomnia-scripting-environment/src/autocomplete-snippets.json diff --git a/packages/insomnia-scripting-environment/package.json b/packages/insomnia-scripting-environment/package.json index dfb36ed593bf..15a20f9bc52b 100644 --- a/packages/insomnia-scripting-environment/package.json +++ b/packages/insomnia-scripting-environment/package.json @@ -8,7 +8,8 @@ "scripts": { "lint": "eslint . --ext .js,.ts,.tsx --cache", "test": "vitest run", - "type-check": "tsc --project tsconfig.json" + "type-check": "tsc --project tsconfig.json", + "generate:autocomplete": "node --experimental-strip-types scripts/generate-autocomplete.ts" }, "repository": { "type": "git", diff --git a/packages/insomnia-scripting-environment/scripts/generate-autocomplete.ts b/packages/insomnia-scripting-environment/scripts/generate-autocomplete.ts new file mode 100644 index 000000000000..b022f5c17383 --- /dev/null +++ b/packages/insomnia-scripting-environment/scripts/generate-autocomplete.ts @@ -0,0 +1,135 @@ +// Regenerate src/autocomplete-snippets.json from the live scripting API. +// Run from the package root: node --experimental-strip-types scripts/generate-autocomplete.ts +// +// This script instantiates the scripting classes in Node.js (where tough-cookie is fine) +// and walks the object graph to derive autocomplete snippets. The output is committed as a +// static JSON file so the renderer never has to import the scripting classes. +// +// Re-run this script whenever the public scripting API surface changes. + +import { writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// These imports are Node.js-only (via tough-cookie etc.) — safe here, never in the renderer. + +const { + CookieObject, + Environment, + Execution, + InsomniaObject, + Request: ScriptRequest, + RequestInfo, + Response: ScriptResponse, + Url, + Variables, + Vault, +} = require('../src/objects/index.ts'); + +const { ParentFolders } = require('../src/objects/folders.ts'); + +interface Snippet { + displayValue: string; + name: string; + value: string; +} + +function walk(obj: object, path: string): Snippet[] { + let snippets: Snippet[] = []; + const refs = new Set(); + const record = obj as Record; + + for (const key in obj) { + if (key.startsWith('_')) { + continue; + } + + const value = record[key]; + + if (typeof value === 'object' && value !== null) { + if (refs.has(value)) { + continue; + } + refs.add(value); + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + snippets.push({ displayValue: `${path}.${value}`, name: `${path}.${key}`, value: `${path}.${key}` }); + } else if (typeof value === 'function') { + snippets.push({ displayValue: `${path}.${key}()`, name: `${path}.${key}()`, value: `${path}.${key}()` }); + } else if (Array.isArray(value)) { + for (const item of value) { + snippets = snippets.concat(walk(item, `${path}.${key}`)); + } + } else if (value !== null && typeof value === 'object') { + snippets = snippets.concat(walk(value as object, `${path}.${key}`)); + } + } + + return snippets; +} + +const settings: any = { enableVaultInScripts: true }; +const req = new ScriptRequest({ url: new Url('http://placeholder.com') }); + +const insomnia = new InsomniaObject({ + globals: new Environment('globals', {}), + baseGlobals: new Environment('baseGlobals', {}), + iterationData: new Environment('iterationData', {}), + environment: new Environment('environment', {}), + baseEnvironment: new Environment('baseEnvironment', {}), + variables: new Variables({ + baseGlobalVars: new Environment('baseGlobals', {}), + globalVars: new Environment('globals', {}), + environmentVars: new Environment('environment', {}), + collectionVars: new Environment('collection', {}), + iterationDataVars: new Environment('data', {}), + folderLevelVars: [], + localVars: new Environment('data', {}), + }), + vault: new Vault('vault', {}, true), + request: req, + response: new ScriptResponse({ + code: 200, + reason: 'OK', + header: [ + { key: 'header1', value: 'val1' }, + { key: 'header2', value: 'val2' }, + ], + cookie: [ + { key: 'header1', value: 'val1' }, + { key: 'header2', value: 'val2' }, + ], + body: '{"key": 888}', + stream: undefined, + responseTime: 100, + originalRequest: req, + }), + settings, + clientCertificates: [], + cookies: new CookieObject({ + _id: '', + type: 'CookieJar', + parentId: '', + modified: 0, + created: 0, + isPrivate: false, + name: '', + cookies: [], + }), + requestInfo: new RequestInfo({ + eventName: 'prerequest', + iteration: 1, + iterationCount: 1, + requestName: '', + requestId: '', + }), + execution: new Execution({ location: ['path'] }), + parentFolders: new ParentFolders([]), +}); + +const snippets = walk(insomnia, 'insomnia'); + +const outputPath = join(dirname(fileURLToPath(import.meta.url)), '../src/autocomplete-snippets.json'); +writeFileSync(outputPath, JSON.stringify(snippets, null, 2) + '\n'); +console.log(`Wrote ${snippets.length} snippets to src/autocomplete-snippets.json`); diff --git a/packages/insomnia-scripting-environment/src/autocomplete-snippets.json b/packages/insomnia-scripting-environment/src/autocomplete-snippets.json new file mode 100644 index 000000000000..524907b4f9a7 --- /dev/null +++ b/packages/insomnia-scripting-environment/src/autocomplete-snippets.json @@ -0,0 +1,88 @@ +[ + { "displayValue": "insomnia.environment.has()", "name": "insomnia.environment.has()", "value": "insomnia.environment.has()" }, + { "displayValue": "insomnia.environment.get()", "name": "insomnia.environment.get()", "value": "insomnia.environment.get()" }, + { "displayValue": "insomnia.environment.set()", "name": "insomnia.environment.set()", "value": "insomnia.environment.set()" }, + { "displayValue": "insomnia.environment.unset()", "name": "insomnia.environment.unset()", "value": "insomnia.environment.unset()" }, + { "displayValue": "insomnia.environment.clear()", "name": "insomnia.environment.clear()", "value": "insomnia.environment.clear()" }, + { "displayValue": "insomnia.environment.replaceIn()", "name": "insomnia.environment.replaceIn()", "value": "insomnia.environment.replaceIn()" }, + { "displayValue": "insomnia.environment.toObject()", "name": "insomnia.environment.toObject()", "value": "insomnia.environment.toObject()" }, + + { "displayValue": "insomnia.baseEnvironment.has()", "name": "insomnia.baseEnvironment.has()", "value": "insomnia.baseEnvironment.has()" }, + { "displayValue": "insomnia.baseEnvironment.get()", "name": "insomnia.baseEnvironment.get()", "value": "insomnia.baseEnvironment.get()" }, + { "displayValue": "insomnia.baseEnvironment.set()", "name": "insomnia.baseEnvironment.set()", "value": "insomnia.baseEnvironment.set()" }, + { "displayValue": "insomnia.baseEnvironment.unset()", "name": "insomnia.baseEnvironment.unset()", "value": "insomnia.baseEnvironment.unset()" }, + { "displayValue": "insomnia.baseEnvironment.clear()", "name": "insomnia.baseEnvironment.clear()", "value": "insomnia.baseEnvironment.clear()" }, + { "displayValue": "insomnia.baseEnvironment.replaceIn()", "name": "insomnia.baseEnvironment.replaceIn()", "value": "insomnia.baseEnvironment.replaceIn()" }, + { "displayValue": "insomnia.baseEnvironment.toObject()", "name": "insomnia.baseEnvironment.toObject()", "value": "insomnia.baseEnvironment.toObject()" }, + + { "displayValue": "insomnia.collectionVariables.has()", "name": "insomnia.collectionVariables.has()", "value": "insomnia.collectionVariables.has()" }, + { "displayValue": "insomnia.collectionVariables.get()", "name": "insomnia.collectionVariables.get()", "value": "insomnia.collectionVariables.get()" }, + { "displayValue": "insomnia.collectionVariables.set()", "name": "insomnia.collectionVariables.set()", "value": "insomnia.collectionVariables.set()" }, + { "displayValue": "insomnia.collectionVariables.unset()", "name": "insomnia.collectionVariables.unset()", "value": "insomnia.collectionVariables.unset()" }, + { "displayValue": "insomnia.collectionVariables.clear()", "name": "insomnia.collectionVariables.clear()", "value": "insomnia.collectionVariables.clear()" }, + { "displayValue": "insomnia.collectionVariables.replaceIn()", "name": "insomnia.collectionVariables.replaceIn()", "value": "insomnia.collectionVariables.replaceIn()" }, + { "displayValue": "insomnia.collectionVariables.toObject()", "name": "insomnia.collectionVariables.toObject()", "value": "insomnia.collectionVariables.toObject()" }, + + { "displayValue": "insomnia.variables.has()", "name": "insomnia.variables.has()", "value": "insomnia.variables.has()" }, + { "displayValue": "insomnia.variables.get()", "name": "insomnia.variables.get()", "value": "insomnia.variables.get()" }, + { "displayValue": "insomnia.variables.set()", "name": "insomnia.variables.set()", "value": "insomnia.variables.set()" }, + { "displayValue": "insomnia.variables.replaceIn()", "name": "insomnia.variables.replaceIn()", "value": "insomnia.variables.replaceIn()" }, + + { "displayValue": "insomnia.vault.has()", "name": "insomnia.vault.has()", "value": "insomnia.vault.has()" }, + { "displayValue": "insomnia.vault.get()", "name": "insomnia.vault.get()", "value": "insomnia.vault.get()" }, + { "displayValue": "insomnia.vault.set()", "name": "insomnia.vault.set()", "value": "insomnia.vault.set()" }, + { "displayValue": "insomnia.vault.unset()", "name": "insomnia.vault.unset()", "value": "insomnia.vault.unset()" }, + { "displayValue": "insomnia.vault.clear()", "name": "insomnia.vault.clear()", "value": "insomnia.vault.clear()" }, + { "displayValue": "insomnia.vault.replaceIn()", "name": "insomnia.vault.replaceIn()", "value": "insomnia.vault.replaceIn()" }, + { "displayValue": "insomnia.vault.toObject()", "name": "insomnia.vault.toObject()", "value": "insomnia.vault.toObject()" }, + + { "displayValue": "insomnia.request.method", "name": "insomnia.request.method", "value": "insomnia.request.method" }, + { "displayValue": "insomnia.request.url", "name": "insomnia.request.url", "value": "insomnia.request.url" }, + { "displayValue": "insomnia.request.url.addQueryParams()", "name": "insomnia.request.url.addQueryParams()", "value": "insomnia.request.url.addQueryParams()" }, + { "displayValue": "insomnia.request.url.getQueryString()", "name": "insomnia.request.url.getQueryString()", "value": "insomnia.request.url.getQueryString()" }, + { "displayValue": "insomnia.request.body", "name": "insomnia.request.body", "value": "insomnia.request.body" }, + { "displayValue": "insomnia.request.body.update()", "name": "insomnia.request.body.update()", "value": "insomnia.request.body.update()" }, + { "displayValue": "insomnia.request.headers", "name": "insomnia.request.headers", "value": "insomnia.request.headers" }, + { "displayValue": "insomnia.request.auth", "name": "insomnia.request.auth", "value": "insomnia.request.auth" }, + { "displayValue": "insomnia.request.auth.update()", "name": "insomnia.request.auth.update()", "value": "insomnia.request.auth.update()" }, + { "displayValue": "insomnia.request.addHeader()", "name": "insomnia.request.addHeader()", "value": "insomnia.request.addHeader()" }, + { "displayValue": "insomnia.request.removeHeader()", "name": "insomnia.request.removeHeader()", "value": "insomnia.request.removeHeader()" }, + { "displayValue": "insomnia.request.getHeader()", "name": "insomnia.request.getHeader()", "value": "insomnia.request.getHeader()" }, + { "displayValue": "insomnia.request.upsertHeader()", "name": "insomnia.request.upsertHeader()", "value": "insomnia.request.upsertHeader()" }, + { "displayValue": "insomnia.request.toObject()", "name": "insomnia.request.toObject()", "value": "insomnia.request.toObject()" }, + + { "displayValue": "insomnia.response.code", "name": "insomnia.response.code", "value": "insomnia.response.code" }, + { "displayValue": "insomnia.response.status", "name": "insomnia.response.status", "value": "insomnia.response.status" }, + { "displayValue": "insomnia.response.responseTime", "name": "insomnia.response.responseTime", "value": "insomnia.response.responseTime" }, + { "displayValue": "insomnia.response.headers", "name": "insomnia.response.headers", "value": "insomnia.response.headers" }, + { "displayValue": "insomnia.response.cookies", "name": "insomnia.response.cookies", "value": "insomnia.response.cookies" }, + { "displayValue": "insomnia.response.json()", "name": "insomnia.response.json()", "value": "insomnia.response.json()" }, + { "displayValue": "insomnia.response.text()", "name": "insomnia.response.text()", "value": "insomnia.response.text()" }, + { "displayValue": "insomnia.response.toObject()", "name": "insomnia.response.toObject()", "value": "insomnia.response.toObject()" }, + + { "displayValue": "insomnia.cookies.jar()", "name": "insomnia.cookies.jar()", "value": "insomnia.cookies.jar()" }, + { "displayValue": "insomnia.cookies.toObject()", "name": "insomnia.cookies.toObject()", "value": "insomnia.cookies.toObject()" }, + + { "displayValue": "insomnia.info.eventName", "name": "insomnia.info.eventName", "value": "insomnia.info.eventName" }, + { "displayValue": "insomnia.info.iteration", "name": "insomnia.info.iteration", "value": "insomnia.info.iteration" }, + { "displayValue": "insomnia.info.iterationCount", "name": "insomnia.info.iterationCount", "value": "insomnia.info.iterationCount" }, + { "displayValue": "insomnia.info.requestName", "name": "insomnia.info.requestName", "value": "insomnia.info.requestName" }, + { "displayValue": "insomnia.info.requestId", "name": "insomnia.info.requestId", "value": "insomnia.info.requestId" }, + { "displayValue": "insomnia.info.toObject()", "name": "insomnia.info.toObject()", "value": "insomnia.info.toObject()" }, + + { "displayValue": "insomnia.execution.location", "name": "insomnia.execution.location", "value": "insomnia.execution.location" }, + { "displayValue": "insomnia.execution.skipRequest()", "name": "insomnia.execution.skipRequest()", "value": "insomnia.execution.skipRequest()" }, + { "displayValue": "insomnia.execution.setNextRequest()", "name": "insomnia.execution.setNextRequest()", "value": "insomnia.execution.setNextRequest()" }, + { "displayValue": "insomnia.execution.toObject()", "name": "insomnia.execution.toObject()", "value": "insomnia.execution.toObject()" }, + + { "displayValue": "insomnia.parentFolders.get()", "name": "insomnia.parentFolders.get()", "value": "insomnia.parentFolders.get()" }, + { "displayValue": "insomnia.parentFolders.getById()", "name": "insomnia.parentFolders.getById()", "value": "insomnia.parentFolders.getById()" }, + { "displayValue": "insomnia.parentFolders.getByName()", "name": "insomnia.parentFolders.getByName()", "value": "insomnia.parentFolders.getByName()" }, + { "displayValue": "insomnia.parentFolders.findValue()", "name": "insomnia.parentFolders.findValue()", "value": "insomnia.parentFolders.findValue()" }, + { "displayValue": "insomnia.parentFolders.toObject()", "name": "insomnia.parentFolders.toObject()", "value": "insomnia.parentFolders.toObject()" }, + + { "displayValue": "insomnia.test()", "name": "insomnia.test()", "value": "insomnia.test()" }, + { "displayValue": "insomnia.expect()", "name": "insomnia.expect()", "value": "insomnia.expect()" }, + { "displayValue": "insomnia.sendRequest()", "name": "insomnia.sendRequest()", "value": "insomnia.sendRequest()" }, + { "displayValue": "insomnia.toObject()", "name": "insomnia.toObject()", "value": "insomnia.toObject()" } +] diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.tsx index b902f8d8ea9a..74ab0992ad2c 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.tsx @@ -1150,7 +1150,7 @@ const Debug = () => { {workspaceId ? ( - {isRequestGroupId(requestGroupId) && } + {isRequestGroupId(requestGroupId) && } {models.grpcRequest.isGrpcRequestId(requestId) && grpcState && activeRequest?._id === requestId && ( diff --git a/packages/insomnia/src/ui/components/editors/request-script-editor.tsx b/packages/insomnia/src/ui/components/editors/request-script-editor.tsx index 3575fbd53f83..a2a8c64777c3 100644 --- a/packages/insomnia/src/ui/components/editors/request-script-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/request-script-editor.tsx @@ -16,19 +16,7 @@ import { import { translateHandlersInScript } from '~/main/importers/importers/translate-postman-script'; import { CodeEditor, type CodeEditorHandle } from '~/ui/components/.client/codemirror/code-editor'; -import { - CookieObject, - Environment, - Execution, - InsomniaObject, - Request as ScriptRequest, - RequestInfo, - Response as ScriptResponse, - Url, - Variables, - Vault, -} from '../../../../../insomnia-scripting-environment/src/objects'; -import { ParentFolders } from '../../../../../insomnia-scripting-environment/src/objects/folders'; +import autocompleteSnippets from '../../../../../insomnia-scripting-environment/src/autocomplete-snippets.json'; import { Icon } from '../icon'; interface Props { @@ -36,7 +24,6 @@ interface Props { defaultValue: string; uniquenessKey: string; className?: string; - settings: Settings; onSnippetAdded?: (snippetName: string) => void; } @@ -147,58 +134,6 @@ const lintOptions = { esversion: 11, }; -// TODO: We probably don't want to expose every property like .toObject() so we need a way to filter those out -// or make those properties private -// TODO: introduce this functionality for other objects, such as Url, UrlMatchPattern and so on -// TODO: introduce function arguments -// TODO: provide snippets for environment keys if possible -function getRequestScriptSnippets(insomniaObject: InsomniaObject, path: string): Snippet[] { - let snippets: Snippet[] = []; - - const refs = new Set(); - const insomniaRecords = insomniaObject as Record; - - for (const key in insomniaObject) { - const isPrivate = typeof key === 'string' && key.startsWith('_'); - if (isPrivate) { - continue; - } - - const value = insomniaRecords[key]; - - if (typeof key === 'object') { - if (refs.has(value)) { - // avoid cyclic referring - continue; - } else { - refs.add(value); - } - } - - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - snippets.push({ - displayValue: `${path}.${value}`, - name: `${path}.${key}`, - value: `${path}.${key}`, - }); - } else if (typeof value === 'function') { - snippets.push({ - displayValue: `${path}.${key}()`, - name: `${path}.${key}()`, - value: `${path}.${key}()`, - }); - } else if (Array.isArray(value)) { - for (const item of value) { - snippets = snippets.concat(getRequestScriptSnippets(item, `${path}.${key}`)); - } - } else { - snippets = snippets.concat(getRequestScriptSnippets(value, `${path}.${key}`)); - } - } - - return snippets; -} - interface SnippetMenuItem { id: string; name: string; @@ -538,7 +473,6 @@ export const RequestScriptEditor: FC = ({ defaultValue, onChange, uniquenessKey, - settings, onSnippetAdded, }) => { const editorRef = useRef(null); @@ -558,71 +492,7 @@ export const RequestScriptEditor: FC = ({ onSnippetAdded?.(snippet); }; - const req = new ScriptRequest({ - url: new Url('http://placeholder.com'), - }); - // TODO(george): Add more to this object to provide improved autocomplete - const requestScriptSnippets = getRequestScriptSnippets( - new InsomniaObject({ - globals: new Environment('globals', {}), - baseGlobals: new Environment('baseGlobals', {}), - iterationData: new Environment('iterationData', {}), - environment: new Environment('environment', {}), - baseEnvironment: new Environment('baseEnvironment', {}), - variables: new Variables({ - baseGlobalVars: new Environment('baseGlobals', {}), - globalVars: new Environment('globals', {}), - environmentVars: new Environment('environment', {}), - collectionVars: new Environment('collection', {}), - iterationDataVars: new Environment('data', {}), - folderLevelVars: [], // folderLevelVars - localVars: new Environment('data', {}), - }), - vault: settings.enableVaultInScripts ? new Vault('vault', {}, settings.enableVaultInScripts) : undefined, - request: req, - response: new ScriptResponse({ - code: 200, - reason: 'OK', - header: [ - { key: 'header1', value: 'val1' }, - { key: 'header2', value: 'val2' }, - ], - cookie: [ - { key: 'header1', value: 'val1' }, - { key: 'header2', value: 'val2' }, - ], - body: '{"key": 888}', - stream: undefined, - responseTime: 100, - originalRequest: req, - }), - settings, - clientCertificates: [], - cookies: new CookieObject({ - _id: '', - type: 'CookieJar', - parentId: '', - modified: 0, - created: 0, - isPrivate: false, - name: '', - cookies: [], - }), - requestInfo: new RequestInfo({ - // @TODO - Look into this event name when we introduce iteration data - eventName: 'prerequest', - iteration: 1, - iterationCount: 1, - requestName: '', - requestId: '', - }), - execution: new Execution({ - location: ['path'], - }), - parentFolders: new ParentFolders([]), - }), - 'insomnia', - ); + const requestScriptSnippets = autocompleteSnippets as Snippet[]; return (
diff --git a/packages/insomnia/src/ui/components/panes/request-group-pane.tsx b/packages/insomnia/src/ui/components/panes/request-group-pane.tsx index eea0fd4f0a66..24d67221259e 100644 --- a/packages/insomnia/src/ui/components/panes/request-group-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/request-group-pane.tsx @@ -21,7 +21,7 @@ import { Icon } from '../icon'; import { MarkdownEditor } from '../markdown-editor'; import { RequestGroupSettingsModal } from '../modals/request-group-settings-modal'; -export const RequestGroupPane: FC<{ settings: Settings }> = ({ settings }) => { +export const RequestGroupPane: FC = () => { const { activeRequestGroup } = useRequestGroupLoaderData()!; const { activeEnvironment, vcsVersion } = useWorkspaceLoaderData()!; const [isRequestGroupSettingsModalOpen, setIsRequestGroupSettingsModalOpen] = useState(false); @@ -177,7 +177,6 @@ export const RequestGroupPane: FC<{ settings: Settings }> = ({ settings }) => { uniquenessKey={`${activeRequestGroup._id}:pre-request-script`} defaultValue={activeRequestGroup.preRequestScript || ''} onChange={preRequestScript => patchRequestGroup(activeRequestGroup._id, { preRequestScript })} - settings={settings} /> @@ -187,7 +186,6 @@ export const RequestGroupPane: FC<{ settings: Settings }> = ({ settings }) => { uniquenessKey={`${activeRequestGroup._id}:after-response-script`} defaultValue={activeRequestGroup.afterResponseScript || ''} onChange={afterResponseScript => patchRequestGroup(activeRequestGroup._id, { afterResponseScript })} - settings={settings} /> diff --git a/packages/insomnia/src/ui/components/panes/request-pane.tsx b/packages/insomnia/src/ui/components/panes/request-pane.tsx index a5fc04afb3cc..cf8f60a4d61b 100644 --- a/packages/insomnia/src/ui/components/panes/request-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/request-pane.tsx @@ -368,7 +368,6 @@ export const RequestPane: FC = ({ environmentId, settings, onPaste }) => uniquenessKey={`${activeRequest._id}:pre-request-script`} defaultValue={activeRequest.preRequestScript || ''} onChange={preRequestScript => patchRequest(requestId, { preRequestScript })} - settings={settings} onSnippetAdded={snippetName => { window.main.trackAnalyticsEvent({ event: AnalyticsEvent.requestScriptsPreScriptSnippetAdded, @@ -384,7 +383,6 @@ export const RequestPane: FC = ({ environmentId, settings, onPaste }) => uniquenessKey={`${activeRequest._id}:after-response-script`} defaultValue={activeRequest.afterResponseScript || ''} onChange={afterResponseScript => patchRequest(requestId, { afterResponseScript })} - settings={settings} onSnippetAdded={snippetName => { window.main.trackAnalyticsEvent({ event: AnalyticsEvent.requestScriptsPostScriptSnippetAdded, From 475c8b61705abbfde5d54dcb4aa1d3ce8d75f177 Mon Sep 17 00:00:00 2001 From: jackkav Date: Tue, 2 Jun 2026 21:27:35 +0200 Subject: [PATCH 31/47] Fix rebase conflicts and import path issues - Fix incorrect ~/insomnia-data imports (should be insomnia-data package) - Remove non-existent mime utility imports and provide simple fallback - Remove incorrect analytics call from main process - Remove unused imports (Settings, Cookie) - Fix Response type annotation for getResponseBodyBuffer --- packages/insomnia/src/account/session.ts | 6 +++--- .../insomnia/src/common/__tests__/har.test.ts | 4 ++-- packages/insomnia/src/common/har.ts | 4 ++-- packages/insomnia/src/main/ipc/cookies.ts | 2 +- .../src/main/templating-worker-database.ts | 6 +++--- packages/insomnia/src/main/window-utils.ts | 1 - .../src/network/network-adapter.node.ts | 2 +- .../src/network/network-adapter.renderer.ts | 2 +- packages/insomnia/src/plugins/index.ts | 7 +++---- packages/insomnia/src/plugins/types.ts | 2 +- ...orkspaceId.debug.request.$requestId.send.tsx | 3 +-- packages/insomnia/src/templating/types.ts | 1 - .../ui/components/editors/body/body-editor.tsx | 17 ++++++++++++++++- .../editors/request-script-editor.tsx | 1 - .../ui/components/panes/request-group-pane.tsx | 2 +- 15 files changed, 35 insertions(+), 25 deletions(-) diff --git a/packages/insomnia/src/account/session.ts b/packages/insomnia/src/account/session.ts index 73399ba29a82..4172aa37f3c1 100644 --- a/packages/insomnia/src/account/session.ts +++ b/packages/insomnia/src/account/session.ts @@ -1,8 +1,8 @@ import { getEncryptionKeys, getUserProfile, logout as logoutAPI } from 'insomnia-api'; -import type { GitRepository, Project, WorkspaceMeta } from '~/insomnia-data'; -import type { AESMessage } from '~/insomnia-data'; -import { models, services } from '~/insomnia-data'; +import type { GitRepository, Project, WorkspaceMeta } from 'insomnia-data'; +import type { AESMessage } from 'insomnia-data'; +import { models, services } from 'insomnia-data'; import { AI_PLUGIN_NAME, LLM_BACKENDS } from '../common/constants'; import { database } from '../common/database'; diff --git a/packages/insomnia/src/common/__tests__/har.test.ts b/packages/insomnia/src/common/__tests__/har.test.ts index 8858effe3980..8756380ed314 100644 --- a/packages/insomnia/src/common/__tests__/har.test.ts +++ b/packages/insomnia/src/common/__tests__/har.test.ts @@ -17,8 +17,8 @@ vi.mock('~/utils/vault-adapter', () => ({ encryptSecretValue: (value: any) => value, })); -import type { Cookie, Request, Response } from '~/insomnia-data'; -import { models, services } from '~/insomnia-data'; +import type { Cookie, Request, Response } from 'insomnia-data'; +import { models, services } from 'insomnia-data'; import { database as db } from '../../common/database'; import { exportHar, exportHarResponse, exportHarWithRequest } from '../har'; diff --git a/packages/insomnia/src/common/har.ts b/packages/insomnia/src/common/har.ts index 50deed740d00..9ca6f69b83f4 100644 --- a/packages/insomnia/src/common/har.ts +++ b/packages/insomnia/src/common/har.ts @@ -1,7 +1,7 @@ import type * as Har from 'har-format'; -import type { BaseModel, Cookie, Environment, Request, RequestGroup, Response, Workspace } from '~/insomnia-data'; -import { models, services } from '~/insomnia-data'; +import type { BaseModel, Cookie, Environment, Request, RequestGroup, Response, Workspace } from 'insomnia-data'; +import { models, services } from 'insomnia-data'; import { applyRequestHooks } from '~/network/network-adapter'; import { RenderError } from '../templating/render-error'; diff --git a/packages/insomnia/src/main/ipc/cookies.ts b/packages/insomnia/src/main/ipc/cookies.ts index 78690916aa31..1b5f54648bc5 100644 --- a/packages/insomnia/src/main/ipc/cookies.ts +++ b/packages/insomnia/src/main/ipc/cookies.ts @@ -1,6 +1,6 @@ import { Cookie as ToughCookie, CookieJar } from 'tough-cookie'; -import type { Cookie } from '~/insomnia-data'; +import type { Cookie } from 'insomnia-data'; import { ipcMainHandle } from './electron'; diff --git a/packages/insomnia/src/main/templating-worker-database.ts b/packages/insomnia/src/main/templating-worker-database.ts index 1d523d7aba12..4b4eadf47eab 100644 --- a/packages/insomnia/src/main/templating-worker-database.ts +++ b/packages/insomnia/src/main/templating-worker-database.ts @@ -7,8 +7,8 @@ import iconv from 'iconv-lite'; import { v4 as uuidv4 } from 'uuid'; import { jarFromCookies } from '~/common/cookies'; -import type { AllTypes, CloudProviderCredential, Request as DBRequest, RequestGroup, Workspace } from '~/insomnia-data'; -import { services } from '~/insomnia-data'; +import type { AllTypes, CloudProviderCredential, Request as DBRequest, RequestGroup, Workspace } from 'insomnia-data'; +import { services } from 'insomnia-data'; import { getPluginCommonContext } from '~/plugins'; import { getAppBundlePlugins, RESPONSE_CODE_REASONS } from '../common/constants'; @@ -108,7 +108,7 @@ const pluginToMainAPI: Record Promise< 'response.getLatestForRequestId': async (body: { requestId: string; environmentId: string }) => { return await services.response.getLatestForRequestId(body.requestId, body.environmentId); }, - 'response.getBodyBuffer': async (body: { response: Response; readFailureValue: string }) => { + 'response.getBodyBuffer': async (body: { response?: { bodyPath?: string; bodyCompression?: any }; readFailureValue?: string }) => { return await services.helpers.getResponseBodyBuffer(body.response, body.readFailureValue); }, 'pluginData.hasItem': async (body: { pluginName: string; key: string }) => { diff --git a/packages/insomnia/src/main/window-utils.ts b/packages/insomnia/src/main/window-utils.ts index afeca6fe5772..2511f3525bde 100644 --- a/packages/insomnia/src/main/window-utils.ts +++ b/packages/insomnia/src/main/window-utils.ts @@ -269,7 +269,6 @@ export function createWindow(): ElectronBrowserWindow { { label: `${MNEMONIC_SYM}Preferences`, click: () => { - trackAnalyticsEvent(AnalyticsEvent.AppMenuPreferencesClicked); mainBrowserWindow.webContents?.send('toggle-preferences'); }, }, diff --git a/packages/insomnia/src/network/network-adapter.node.ts b/packages/insomnia/src/network/network-adapter.node.ts index e1a351ce9bec..18780ae476da 100644 --- a/packages/insomnia/src/network/network-adapter.node.ts +++ b/packages/insomnia/src/network/network-adapter.node.ts @@ -3,7 +3,7 @@ import nodePath from 'node:path'; import clone from 'clone'; -import type { Cookie, RequestHeader } from '~/insomnia-data'; +import type { Cookie, RequestHeader } from 'insomnia-data'; import type { RenderedRequest } from '~/templating/types'; import type { RequestContext } from '../../../insomnia-scripting-environment/src/objects'; diff --git a/packages/insomnia/src/network/network-adapter.renderer.ts b/packages/insomnia/src/network/network-adapter.renderer.ts index 821fb3850d3c..497b37527d4e 100644 --- a/packages/insomnia/src/network/network-adapter.renderer.ts +++ b/packages/insomnia/src/network/network-adapter.renderer.ts @@ -1,4 +1,4 @@ -import type { Cookie, RequestHeader } from '~/insomnia-data'; +import type { Cookie, RequestHeader } from 'insomnia-data'; import { plugins as pluginsBridge } from '~/plugins/renderer-bridge'; import type { RenderedRequest } from '~/templating/types'; diff --git a/packages/insomnia/src/plugins/index.ts b/packages/insomnia/src/plugins/index.ts index 381f8bbb12a5..7cc617a43a32 100644 --- a/packages/insomnia/src/plugins/index.ts +++ b/packages/insomnia/src/plugins/index.ts @@ -2,10 +2,9 @@ import fs from 'node:fs'; import path from 'node:path'; import electron from 'electron'; - -import type { Request, RequestGroup, Workspace } from '~/insomnia-data'; -import { database as db, models, services } from '~/insomnia-data'; -import type { PluginConfigMap } from '~/insomnia-data/common'; +import type { Request, RequestGroup, Workspace } from 'insomnia-data'; +import { database as db, models, services } from 'insomnia-data'; +import type { PluginConfigMap } from 'insomnia-data/common'; import { fetchFromTemplateWorkerDatabase } from '~/templating/liquid-extension-worker'; import { getAppBundlePlugins, isDevelopment } from '../common/constants'; diff --git a/packages/insomnia/src/plugins/types.ts b/packages/insomnia/src/plugins/types.ts index cc3c83a0ff1a..62477b524b9d 100644 --- a/packages/insomnia/src/plugins/types.ts +++ b/packages/insomnia/src/plugins/types.ts @@ -1,4 +1,4 @@ -import type { GrpcRequest, Request, RequestGroup, SocketIORequest, WebSocketRequest, Workspace } from '~/insomnia-data'; +import type { GrpcRequest, Request, RequestGroup, SocketIORequest, WebSocketRequest, Workspace } from 'insomnia-data'; import type { ParsedApiSpec } from '../common/api-specs'; import type { PluginTemplateTag } from '../templating/types'; diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx index aac941b38071..185da8288ce6 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx @@ -14,7 +14,6 @@ import { href, redirect } from 'react-router'; import { v4 as uuidv4 } from 'uuid'; import { CONTENT_TYPE_GRAPHQL } from '~/common/constants'; -import { mimeTypeExtension as mimeExtension } from '~/common/mime'; import { getContentDispositionHeader } from '~/common/misc'; import type { ResponsePatch } from '~/main/network/libcurl-promise'; import type { TimingStep } from '~/main/network/request-timing'; @@ -314,7 +313,7 @@ export const sendActionImplementation = async (options: { const header = getContentDispositionHeader(responsePatch.headers || []); const name = header ? contentDisposition.parse(header.value).parameters.filename - : `${requestData.request.name.replace(/\s/g, '-').toLowerCase()}.${(responsePatch.contentType && mimeExtension(responsePatch.contentType)) || 'unknown'}`; + : `${requestData.request.name.replace(/\s/g, '-').toLowerCase()}.unknown`; await writeToDownloadPath( window.path.join(requestMeta.downloadPath, name), responsePatch, diff --git a/packages/insomnia/src/templating/types.ts b/packages/insomnia/src/templating/types.ts index fe6acb255309..b978c00c5d59 100644 --- a/packages/insomnia/src/templating/types.ts +++ b/packages/insomnia/src/templating/types.ts @@ -18,7 +18,6 @@ import type { WebSocketRequest, Workspace, } from 'insomnia-data'; -import type { Cookie } from 'tough-cookie'; type NodeCurlRequestType = Pick & Partial>; diff --git a/packages/insomnia/src/ui/components/editors/body/body-editor.tsx b/packages/insomnia/src/ui/components/editors/body/body-editor.tsx index d71351aa43e7..e6ce05435342 100644 --- a/packages/insomnia/src/ui/components/editors/body/body-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/body/body-editor.tsx @@ -8,8 +8,23 @@ import { useParams } from 'react-router'; import { CONTENT_TYPE_FILE, CONTENT_TYPE_FORM_DATA } from '../../../../common/constants'; import { documentationLinks } from '../../../../common/documentation'; -import { lookupMimeType } from '../../../../common/mime'; import { getContentTypeHeader } from '../../../../common/misc'; + +const lookupMimeType = (path: string) => { + const ext = path.split('.').pop()?.toLowerCase(); + const mimeMap: Record = { + 'json': 'application/json', + 'xml': 'application/xml', + 'txt': 'text/plain', + 'html': 'text/html', + 'png': 'image/png', + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'gif': 'image/gif', + 'pdf': 'application/pdf', + }; + return mimeMap[ext || '']; +}; import { useRequestPatcher } from '../../../hooks/use-request'; import { ContentTypeDropdown } from '../../dropdowns/content-type-dropdown'; import { AskModal } from '../../modals/ask-modal'; diff --git a/packages/insomnia/src/ui/components/editors/request-script-editor.tsx b/packages/insomnia/src/ui/components/editors/request-script-editor.tsx index a2a8c64777c3..5b27e136d9ec 100644 --- a/packages/insomnia/src/ui/components/editors/request-script-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/request-script-editor.tsx @@ -1,5 +1,4 @@ import type { Snippet } from 'codemirror'; -import type { Settings } from 'insomnia-data'; import React, { type FC, useRef } from 'react'; import { Button, diff --git a/packages/insomnia/src/ui/components/panes/request-group-pane.tsx b/packages/insomnia/src/ui/components/panes/request-group-pane.tsx index 24d67221259e..f507ca2e1da1 100644 --- a/packages/insomnia/src/ui/components/panes/request-group-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/request-group-pane.tsx @@ -1,4 +1,4 @@ -import type { EnvironmentKvPairData, Settings } from 'insomnia-data'; +import type { EnvironmentKvPairData } from 'insomnia-data'; import { EnvironmentType } from 'insomnia-data'; import React, { type FC, useRef, useState } from 'react'; import { Heading, Tab, TabList, TabPanel, Tabs, ToggleButton } from 'react-aria-components'; From bf2299cd24b58a58b61b83199cfe5c6114ad5925 Mon Sep 17 00:00:00 2001 From: jackkav Date: Tue, 2 Jun 2026 21:41:34 +0200 Subject: [PATCH 32/47] lint --- .../scripts/generate-autocomplete.ts | 4 ++-- packages/insomnia/src/account/session.ts | 1 - packages/insomnia/src/common/har.ts | 2 +- packages/insomnia/src/main/ipc/cookies.ts | 3 +-- packages/insomnia/src/main/templating-worker-database.ts | 9 ++++++--- packages/insomnia/src/main/window-utils.ts | 2 -- packages/insomnia/src/network/network-adapter.node.ts | 2 +- .../insomnia/src/network/network-adapter.renderer.ts | 1 + packages/insomnia/src/plugins/index.ts | 1 + packages/insomnia/src/templating/utils.ts | 1 + 10 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/insomnia-scripting-environment/scripts/generate-autocomplete.ts b/packages/insomnia-scripting-environment/scripts/generate-autocomplete.ts index b022f5c17383..32252a963bba 100644 --- a/packages/insomnia-scripting-environment/scripts/generate-autocomplete.ts +++ b/packages/insomnia-scripting-environment/scripts/generate-autocomplete.ts @@ -8,7 +8,7 @@ // Re-run this script whenever the public scripting API surface changes. import { writeFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; +import path from 'node:path'; import { fileURLToPath } from 'node:url'; // These imports are Node.js-only (via tough-cookie etc.) — safe here, never in the renderer. @@ -130,6 +130,6 @@ const insomnia = new InsomniaObject({ const snippets = walk(insomnia, 'insomnia'); -const outputPath = join(dirname(fileURLToPath(import.meta.url)), '../src/autocomplete-snippets.json'); +const outputPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../src/autocomplete-snippets.json'); writeFileSync(outputPath, JSON.stringify(snippets, null, 2) + '\n'); console.log(`Wrote ${snippets.length} snippets to src/autocomplete-snippets.json`); diff --git a/packages/insomnia/src/account/session.ts b/packages/insomnia/src/account/session.ts index 4172aa37f3c1..b92f574a78ff 100644 --- a/packages/insomnia/src/account/session.ts +++ b/packages/insomnia/src/account/session.ts @@ -1,5 +1,4 @@ import { getEncryptionKeys, getUserProfile, logout as logoutAPI } from 'insomnia-api'; - import type { GitRepository, Project, WorkspaceMeta } from 'insomnia-data'; import type { AESMessage } from 'insomnia-data'; import { models, services } from 'insomnia-data'; diff --git a/packages/insomnia/src/common/har.ts b/packages/insomnia/src/common/har.ts index 9ca6f69b83f4..d325148906cd 100644 --- a/packages/insomnia/src/common/har.ts +++ b/packages/insomnia/src/common/har.ts @@ -1,7 +1,7 @@ import type * as Har from 'har-format'; - import type { BaseModel, Cookie, Environment, Request, RequestGroup, Response, Workspace } from 'insomnia-data'; import { models, services } from 'insomnia-data'; + import { applyRequestHooks } from '~/network/network-adapter'; import { RenderError } from '../templating/render-error'; diff --git a/packages/insomnia/src/main/ipc/cookies.ts b/packages/insomnia/src/main/ipc/cookies.ts index 1b5f54648bc5..27450d758dde 100644 --- a/packages/insomnia/src/main/ipc/cookies.ts +++ b/packages/insomnia/src/main/ipc/cookies.ts @@ -1,6 +1,5 @@ -import { Cookie as ToughCookie, CookieJar } from 'tough-cookie'; - import type { Cookie } from 'insomnia-data'; +import { Cookie as ToughCookie, CookieJar } from 'tough-cookie'; import { ipcMainHandle } from './electron'; diff --git a/packages/insomnia/src/main/templating-worker-database.ts b/packages/insomnia/src/main/templating-worker-database.ts index 4b4eadf47eab..32ed81fc6e62 100644 --- a/packages/insomnia/src/main/templating-worker-database.ts +++ b/packages/insomnia/src/main/templating-worker-database.ts @@ -4,11 +4,11 @@ import os from 'node:os'; import { shell } from 'electron'; import iconv from 'iconv-lite'; +import type { AllTypes, CloudProviderCredential, Request as DBRequest, RequestGroup, Workspace } from 'insomnia-data'; +import { services } from 'insomnia-data'; import { v4 as uuidv4 } from 'uuid'; import { jarFromCookies } from '~/common/cookies'; -import type { AllTypes, CloudProviderCredential, Request as DBRequest, RequestGroup, Workspace } from 'insomnia-data'; -import { services } from 'insomnia-data'; import { getPluginCommonContext } from '~/plugins'; import { getAppBundlePlugins, RESPONSE_CODE_REASONS } from '../common/constants'; @@ -108,7 +108,10 @@ const pluginToMainAPI: Record Promise< 'response.getLatestForRequestId': async (body: { requestId: string; environmentId: string }) => { return await services.response.getLatestForRequestId(body.requestId, body.environmentId); }, - 'response.getBodyBuffer': async (body: { response?: { bodyPath?: string; bodyCompression?: any }; readFailureValue?: string }) => { + 'response.getBodyBuffer': async (body: { + response?: { bodyPath?: string; bodyCompression?: any }; + readFailureValue?: string; + }) => { return await services.helpers.getResponseBodyBuffer(body.response, body.readFailureValue); }, 'pluginData.hasItem': async (body: { pluginName: string; key: string }) => { diff --git a/packages/insomnia/src/main/window-utils.ts b/packages/insomnia/src/main/window-utils.ts index 2511f3525bde..e92bb2277024 100644 --- a/packages/insomnia/src/main/window-utils.ts +++ b/packages/insomnia/src/main/window-utils.ts @@ -17,8 +17,6 @@ import { } from 'electron'; import { isLinux, isMac } from 'insomnia-data/common'; - - import { getAppBuildDate, getAppVersion, getProductName, isDevelopment, MNEMONIC_SYM } from '../common/constants'; import { docsBase } from '../common/documentation'; import { invariant } from '../utils/invariant'; diff --git a/packages/insomnia/src/network/network-adapter.node.ts b/packages/insomnia/src/network/network-adapter.node.ts index 18780ae476da..b7d8338e7ba3 100644 --- a/packages/insomnia/src/network/network-adapter.node.ts +++ b/packages/insomnia/src/network/network-adapter.node.ts @@ -2,8 +2,8 @@ import fs from 'node:fs'; import nodePath from 'node:path'; import clone from 'clone'; - import type { Cookie, RequestHeader } from 'insomnia-data'; + import type { RenderedRequest } from '~/templating/types'; import type { RequestContext } from '../../../insomnia-scripting-environment/src/objects'; diff --git a/packages/insomnia/src/network/network-adapter.renderer.ts b/packages/insomnia/src/network/network-adapter.renderer.ts index 497b37527d4e..0584c7d3d164 100644 --- a/packages/insomnia/src/network/network-adapter.renderer.ts +++ b/packages/insomnia/src/network/network-adapter.renderer.ts @@ -1,4 +1,5 @@ import type { Cookie, RequestHeader } from 'insomnia-data'; + import { plugins as pluginsBridge } from '~/plugins/renderer-bridge'; import type { RenderedRequest } from '~/templating/types'; diff --git a/packages/insomnia/src/plugins/index.ts b/packages/insomnia/src/plugins/index.ts index 7cc617a43a32..18cc66cf2793 100644 --- a/packages/insomnia/src/plugins/index.ts +++ b/packages/insomnia/src/plugins/index.ts @@ -5,6 +5,7 @@ import electron from 'electron'; import type { Request, RequestGroup, Workspace } from 'insomnia-data'; import { database as db, models, services } from 'insomnia-data'; import type { PluginConfigMap } from 'insomnia-data/common'; + import { fetchFromTemplateWorkerDatabase } from '~/templating/liquid-extension-worker'; import { getAppBundlePlugins, isDevelopment } from '../common/constants'; diff --git a/packages/insomnia/src/templating/utils.ts b/packages/insomnia/src/templating/utils.ts index b8d0a8030ffb..d3fd7253214f 100644 --- a/packages/insomnia/src/templating/utils.ts +++ b/packages/insomnia/src/templating/utils.ts @@ -2,6 +2,7 @@ import type { EditorFromTextArea, MarkerRange } from 'codemirror'; import { models, services } from 'insomnia-data'; import { decryptSecretValue } from '~/utils/vault-adapter'; + import type { NunjucksParsedTag, NunjucksParsedTagArg, RenderPurpose } from '../templating/types'; import { decryptVaultKeyFromSession } from '../utils/vault'; import { tokenizeArgs } from './tokenize-args'; From e1f38ced27343de4fa496ff2a7a37a797d524b3a Mon Sep 17 00:00:00 2001 From: jackkav Date: Tue, 2 Jun 2026 21:55:19 +0200 Subject: [PATCH 33/47] fix tests --- .../src/account/__tests__/session.test.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/insomnia/src/account/__tests__/session.test.ts b/packages/insomnia/src/account/__tests__/session.test.ts index f4ee90e6cd96..d63439d7293d 100644 --- a/packages/insomnia/src/account/__tests__/session.test.ts +++ b/packages/insomnia/src/account/__tests__/session.test.ts @@ -24,6 +24,9 @@ vi.mock('../crypt', () => ({ interface MockWindowMain { loginStateChange: ReturnType; + crypt: { + decryptAES: ReturnType; + }; } const getWindowMain = () => (window as unknown as { main: MockWindowMain }).main; @@ -62,7 +65,14 @@ beforeEach(() => { vi.mocked(insomniaApi.getEncryptionKeys).mockResolvedValue(mockEncryptionKeys); vi.mocked(crypt.decryptAES).mockReturnValue(JSON.stringify(MOCK_SYMMETRIC_KEY)); - vi.stubGlobal('window', { main: { loginStateChange: vi.fn() } }); + vi.stubGlobal('window', { + main: { + loginStateChange: vi.fn(), + crypt: { + decryptAES: vi.fn().mockReturnValue(JSON.stringify(MOCK_SYMMETRIC_KEY)), + }, + }, + }); }); describe('absorbKey', () => { @@ -76,7 +86,7 @@ describe('absorbKey', () => { it('decrypts the symmetric key using the provided raw key and encSymmetricKey', async () => { await absorbKey(SESSION_ID, RAW_KEY); - expect(crypt.decryptAES).toHaveBeenCalledWith(RAW_KEY, MOCK_ENC_SYMMETRIC_KEY); + expect(getWindowMain().crypt.decryptAES).toHaveBeenCalledWith(RAW_KEY, MOCK_ENC_SYMMETRIC_KEY); }); it('stores session data with mapped fields from profile and encryption keys', async () => { @@ -123,7 +133,9 @@ describe('absorbKey', () => { describe('getPrivateKey', () => { it('decrypts and returns the private key from session, and throws when keys are missing', async () => { const mockPrivateKey = { kty: 'RSA', d: 'private' }; - vi.mocked(crypt.decryptAES).mockReturnValue(JSON.stringify(mockPrivateKey)); + (getWindowMain().crypt.decryptAES as ReturnType).mockReturnValue( + JSON.stringify(mockPrivateKey), + ); await setSessionData( SESSION_ID, @@ -138,7 +150,7 @@ describe('getPrivateKey', () => { const privateKey = await getPrivateKey(); - expect(crypt.decryptAES).toHaveBeenCalledWith(MOCK_SYMMETRIC_KEY, MOCK_ENC_PRIVATE_KEY); + expect(getWindowMain().crypt.decryptAES).toHaveBeenCalledWith(MOCK_SYMMETRIC_KEY, MOCK_ENC_PRIVATE_KEY); expect(privateKey).toEqual(mockPrivateKey); await setSessionData( From f4e0f0cbddc06ce30eef89c3fe5b4766c6ea112b Mon Sep 17 00:00:00 2001 From: jackkav Date: Tue, 2 Jun 2026 22:00:01 +0200 Subject: [PATCH 34/47] fix: use dynamic import for crypt in session.ts for main process compatibility The session.ts module is used in both renderer and main process contexts (via sync.invoke IPC handlers). When running in the main process, window.main is undefined, causing TypeError when trying to access window.main.crypt.decryptAES(). Changes: - Use dynamic import of crypt module (only loaded in main process context) - In renderer: window.main.crypt is always available so dynamic import never executes - In main process: dynamic import loads crypt with node:crypto support - Protect loginStateChange() calls with window existence checks This avoids bundling node:crypto in the Vite renderer build while still supporting both execution contexts. Fixes E2E test failures in sync operations (remoteBackendProjects, _assertSession, etc.) caused by disabled nodeIntegration. --- packages/insomnia/src/account/session.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/insomnia/src/account/session.ts b/packages/insomnia/src/account/session.ts index b92f574a78ff..f61d85f3697a 100644 --- a/packages/insomnia/src/account/session.ts +++ b/packages/insomnia/src/account/session.ts @@ -6,6 +6,17 @@ import { models, services } from 'insomnia-data'; import { AI_PLUGIN_NAME, LLM_BACKENDS } from '../common/constants'; import { database } from '../common/database'; +async function decryptAESString( + symmetricKey: string | JsonWebKey, + encryptedResult: AESMessage, +): Promise { + if (typeof window !== 'undefined' && window.main?.crypt?.decryptAES) { + return window.main.crypt.decryptAES(symmetricKey, encryptedResult); + } + const crypt = await import('./crypt'); + return crypt.decryptAES(symmetricKey, encryptedResult); +} + export interface SessionData { accountId: string; id: string; @@ -27,7 +38,7 @@ export async function absorbKey(sessionId: string, key: string) { ]); const { public_key: publicKey, enc_private_key: encPrivateKey, enc_symmetric_key: encSymmetricKey } = keys; const { email, id: accountId, first_name: firstName, last_name: lastName } = profile; - const symmetricKeyStr = await window.main.crypt.decryptAES(key, JSON.parse(encSymmetricKey)); + const symmetricKeyStr = await decryptAESString(key, JSON.parse(encSymmetricKey)); // Store the information for later await setSessionData( @@ -41,7 +52,9 @@ export async function absorbKey(sessionId: string, key: string) { JSON.parse(encPrivateKey), ); - window.main.loginStateChange(true); + if (typeof window !== 'undefined' && window.main?.loginStateChange) { + window.main.loginStateChange(true); + } } export async function getPrivateKey() { @@ -57,7 +70,7 @@ export async function getPrivateKey() { throw new Error("Can't get private key: session is missing keys."); } - const privateKeyStr = await window.main.crypt.decryptAES(symmetricKey, encPrivateKey); + const privateKeyStr = await decryptAESString(symmetricKey, encPrivateKey); return JSON.parse(privateKeyStr) as JsonWebKey; } @@ -92,7 +105,9 @@ export async function logout(clearCredentials = false) { if (clearCredentials) { await _removeAllCredentials(); } - window.main.loginStateChange(false); + if (typeof window !== 'undefined' && window.main?.loginStateChange) { + window.main.loginStateChange(false); + } } /** Set data for the new session and store it encrypted with the sessionId */ From bf302419b1d8eab44542c0b401ba2d4151d667b8 Mon Sep 17 00:00:00 2001 From: jackkav Date: Tue, 2 Jun 2026 22:12:40 +0200 Subject: [PATCH 35/47] fix: add aria-label to template tag preview and browser-safe encoding fallback - Add aria-label="Live Preview" to textarea in TagEditor for better Playwright accessibility - Add atob() fallback for decodeEncoding in browser contexts where Buffer isn't available - Fixes smoke test element discovery for template tag preview modal --- packages/insomnia/src/templating/utils.ts | 6 +++++- .../insomnia/src/ui/components/templating/tag-editor.tsx | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/insomnia/src/templating/utils.ts b/packages/insomnia/src/templating/utils.ts index d3fd7253214f..a8b8cd790e93 100644 --- a/packages/insomnia/src/templating/utils.ts +++ b/packages/insomnia/src/templating/utils.ts @@ -137,7 +137,11 @@ export function decodeEncoding(value: T) { const results = value.match(/^b64::(.+)::46b$/); if (results) { - return Buffer.from(results[1], 'base64').toString('utf8'); + if (typeof Buffer !== 'undefined') { + return Buffer.from(results[1], 'base64').toString('utf8'); + } + // Fallback for browser environments + return atob(results[1]); } return value; diff --git a/packages/insomnia/src/ui/components/templating/tag-editor.tsx b/packages/insomnia/src/ui/components/templating/tag-editor.tsx index f28e2a346de5..210ba2d98897 100644 --- a/packages/insomnia/src/ui/components/templating/tag-editor.tsx +++ b/packages/insomnia/src/ui/components/templating/tag-editor.tsx @@ -269,12 +269,12 @@ export const TagEditor: FC = props => {
) : ( -