From 6357df7d3d877f63637bbab00fa7076ce0b9b341 Mon Sep 17 00:00:00 2001 From: jackkav Date: Thu, 28 May 2026 15:44:55 +0200 Subject: [PATCH 01/14] first pass --- packages/insomnia/src/account/session.ts | 12 ++-- packages/insomnia/src/common/har.ts | 26 +-------- packages/insomnia/src/entry.client.tsx | 4 +- packages/insomnia/src/main/window-utils.ts | 2 +- packages/insomnia/src/plugins/misc.ts | 9 +-- .../insomnia/src/plugins/renderer-bridge.ts | 11 ++-- packages/insomnia/src/root.tsx | 2 +- .../insomnia/src/routes/auth.authorize.tsx | 9 ++- packages/insomnia/src/routes/auth.login.tsx | 2 +- .../src/ui/auth-session-provider.client.ts | 56 +++++++++++++------ .../project/organization-select.tsx | 8 +-- 11 files changed, 75 insertions(+), 66 deletions(-) diff --git a/packages/insomnia/src/account/session.ts b/packages/insomnia/src/account/session.ts index 5fd9ee5c7f1c..9d573bc6ed82 100644 --- a/packages/insomnia/src/account/session.ts +++ b/packages/insomnia/src/account/session.ts @@ -5,7 +5,7 @@ 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'; +import type { AESMessage } from './crypt'; export interface SessionData { accountId: string; @@ -15,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. */ @@ -28,7 +28,8 @@ 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 { decryptAES } = await import('./crypt'); + const symmetricKeyStr = decryptAES(key, JSON.parse(encSymmetricKey)); // Store the information for later await setSessionData( @@ -58,7 +59,8 @@ export async function getPrivateKey() { throw new Error("Can't get private key: session is missing keys."); } - const privateKeyStr = crypt.decryptAES(symmetricKey, encPrivateKey); + const { decryptAES } = await import('./crypt'); + const privateKeyStr = decryptAES(symmetricKey, encPrivateKey); return JSON.parse(privateKeyStr) as JsonWebKey; } @@ -105,7 +107,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/common/har.ts b/packages/insomnia/src/common/har.ts index f18893c37b2b..5324a7fec958 100644 --- a/packages/insomnia/src/common/har.ts +++ b/packages/insomnia/src/common/har.ts @@ -1,14 +1,10 @@ -import clone from 'clone'; import type * as Har from 'har-format'; import { Cookie as ToughCookie } from 'tough-cookie'; import type { BaseModel, Environment, Request, RequestGroup, Response, Workspace } from '~/insomnia-data'; import { models, services } from '~/insomnia-data'; +import { applyRequestHooks } from '~/network/network-adapter'; -import * as plugins from '../plugins'; -import * as pluginApp from '../plugins/context/app'; -import * as pluginRequest from '../plugins/context/request'; -import * as pluginStore from '../plugins/context/store'; import { RenderError } from '../templating/render-error'; import type { RenderedRequest } from '../templating/types'; import { parseGraphQLReqeustBody } from '../utils/graph-ql'; @@ -264,25 +260,7 @@ async function _applyRequestPluginHooks( renderedRequest: RenderedRequest, renderedContext: Record, ): Promise { - let newRenderedRequest = renderedRequest; - - for (const { plugin, hook } of await plugins.getRequestHooks()) { - newRenderedRequest = clone(newRenderedRequest); - const context = { - ...(pluginApp.init() as Record), - ...(pluginRequest.init(newRenderedRequest, renderedContext) as Record), - ...(pluginStore.init(plugin) as Record), - }; - - try { - await hook(context); - } catch (err) { - err.plugin = plugin; - throw err; - } - } - - return newRenderedRequest; + return applyRequestHooks(renderedRequest, renderedContext); } export async function exportHarWithRenderedRequest(renderedRequest: RenderedRequest, addContentLength = false) { diff --git a/packages/insomnia/src/entry.client.tsx b/packages/insomnia/src/entry.client.tsx index 580274be7234..5d574d2cd578 100644 --- a/packages/insomnia/src/entry.client.tsx +++ b/packages/insomnia/src/entry.client.tsx @@ -8,12 +8,12 @@ import { HydratedRouter } from 'react-router/dom'; import { insomniaFetch } from '~/common/insomnia-fetch'; import { initDatabase, initServices, services } from '~/insomnia-data'; +import { plugins } from '~/plugins/renderer-bridge'; import { database as clientDatabase } from '~/ui/database.client'; import { clearOAuthWindowSessionId } from '~/ui/spawn-oauth-window'; import { migrateFromLocalStorage, type SessionData, setSessionData, setVaultSessionData } from './account/session'; import { getInsomniaSession, getInsomniaVaultKey, getInsomniaVaultSalt, getSkipOnboarding } from './common/constants'; -import { init as initPlugins } from './plugins'; import { applyColorScheme } from './plugins/misc'; import { registerSyncMergeConflictListener } from './sync/vcs/insomnia-sync'; import { HtmlElementWrapper } from './ui/components/html-element-wrapper'; @@ -40,7 +40,7 @@ delete window._dataServices; configureFetch(options => insomniaFetch({ ...options, onDeepLink: (uri: string) => window.main.openDeepLink(uri) })); -await initPlugins(); +await plugins.reloadPlugins(); await migrateFromLocalStorage(); registerSyncMergeConflictListener(); diff --git a/packages/insomnia/src/main/window-utils.ts b/packages/insomnia/src/main/window-utils.ts index b1754e254818..c9a0f27dc084 100644 --- a/packages/insomnia/src/main/window-utils.ts +++ b/packages/insomnia/src/main/window-utils.ts @@ -201,7 +201,7 @@ export function createWindow(): ElectronBrowserWindow { webPreferences: { preload: path.join(__dirname, 'entry.preload.min.js'), zoomFactor: getZoomFactor(), - nodeIntegration: true, + nodeIntegration: false, nodeIntegrationInWorker: false, // must remain false to ensure the nunjucks web worker sandbox does not have access to Node.js APIs webviewTag: true, // TODO: enable context isolation diff --git a/packages/insomnia/src/plugins/misc.ts b/packages/insomnia/src/plugins/misc.ts index f5794a366ae4..5e6f86dd13d7 100644 --- a/packages/insomnia/src/plugins/misc.ts +++ b/packages/insomnia/src/plugins/misc.ts @@ -1,10 +1,11 @@ import Color from 'color'; import type { ThemeSettings } from '~/insomnia-data'; -import { getAppDefaultTheme } from '~/insomnia-data/common'; +import { getAppDefaultTheme } from '../common/constants'; +import type { SerializableTheme } from './bridge-types'; +import { plugins } from './renderer-bridge'; -import type { Theme } from './index'; -import { type ColorScheme, getThemes } from './index'; +export type ColorScheme = 'default' | 'light' | 'dark'; export type HexColor = `#${string}`; export type RGBColor = `rgb(${string})`; @@ -331,7 +332,7 @@ export async function setTheme(themeName: string) { return; } - const themes: Theme[] = await getThemes(); + const themes: SerializableTheme[] = await plugins.getThemes(); let selectedTheme = themes.find(t => t.theme.name === themeName); if (!selectedTheme) { diff --git a/packages/insomnia/src/plugins/renderer-bridge.ts b/packages/insomnia/src/plugins/renderer-bridge.ts index 7b4c4c3652e4..85c1768b57e1 100644 --- a/packages/insomnia/src/plugins/renderer-bridge.ts +++ b/packages/insomnia/src/plugins/renderer-bridge.ts @@ -1,5 +1,4 @@ import type { PluginBridgeMetrics, PluginsBridgeAPI } from './bridge-types'; -import { invokePluginMethod } from './invoke-method'; // Phase 1a rollback switch: set INSOMNIA_ENABLE_PLUGIN_BRIDGE=false to fall // back to running plugins directly in the renderer (legacy behaviour). @@ -7,15 +6,17 @@ import { invokePluginMethod } from './invoke-method'; // plugin-system deps it pulls in don't inflate the preload. const bridgeEnabled = process.env.INSOMNIA_ENABLE_PLUGIN_BRIDGE !== 'false'; -function call>( +async function call>( method: M, args?: Parameters[0], -): ReturnType { +): Promise>> { if (bridgeEnabled) { const fn = (window.main.plugins[method] as (...a: any[]) => any); - return fn(args) as ReturnType; + return fn(args) as Promise>>; } - return invokePluginMethod(method as any, args) as ReturnType; + + const { invokePluginMethod } = await import('./invoke-method'); + return invokePluginMethod(method as any, args) as Promise>>; } const emptyBridgeMetrics: PluginBridgeMetrics = { diff --git a/packages/insomnia/src/root.tsx b/packages/insomnia/src/root.tsx index 35118b6900e6..29d16b1a11d7 100644 --- a/packages/insomnia/src/root.tsx +++ b/packages/insomnia/src/root.tsx @@ -568,7 +568,7 @@ const Root = () => { // gracefully handle open org in app from browser const userSession = await services.userSession.get(); if (!userSession.id || userSession.id === '') { - const url = new URL(getLoginUrl()); + const url = new URL(await getLoginUrl()); window.main.openInBrowser(url.toString()); window.localStorage.setItem('specificOrgRedirectAfterAuthorize', params.organizationId); return navigate(href('/auth/authorize')); diff --git a/packages/insomnia/src/routes/auth.authorize.tsx b/packages/insomnia/src/routes/auth.authorize.tsx index a7532fce336d..e6a197aa015e 100644 --- a/packages/insomnia/src/routes/auth.authorize.tsx +++ b/packages/insomnia/src/routes/auth.authorize.tsx @@ -1,5 +1,5 @@ import { getVault } from 'insomnia-api'; -import { Fragment } from 'react'; +import { Fragment, useEffect, useState } from 'react'; import { Button, Heading } from 'react-aria-components'; import { href, redirect, useFetchers, useNavigate } from 'react-router'; @@ -75,7 +75,12 @@ export const useAuthorizeActionFetcher = createFetcherSubmitHook( ); const Component = () => { - const url = getLoginUrl(); + const [url, setUrl] = useState(''); + + useEffect(() => { + void getLoginUrl().then(setUrl); + }, []); + const copyUrl = () => { window.clipboard.writeText(url); }; diff --git a/packages/insomnia/src/routes/auth.login.tsx b/packages/insomnia/src/routes/auth.login.tsx index ad5651f5cf99..63c25e0e5483 100644 --- a/packages/insomnia/src/routes/auth.login.tsx +++ b/packages/insomnia/src/routes/auth.login.tsx @@ -37,7 +37,7 @@ const GoogleIcon = (props: React.ReactSVGElement['props']) => { export async function clientAction({ request }: Route.ClientActionArgs) { const data = await request.formData(); const provider = data.get('provider'); - const url = new URL(getLoginUrl()); + const url = new URL(await getLoginUrl()); if (typeof provider === 'string' && provider) { url.searchParams.set('provider', provider); diff --git a/packages/insomnia/src/ui/auth-session-provider.client.ts b/packages/insomnia/src/ui/auth-session-provider.client.ts index cf33fbfee5c7..a1cc84134034 100644 --- a/packages/insomnia/src/ui/auth-session-provider.client.ts +++ b/packages/insomnia/src/ui/auth-session-provider.client.ts @@ -1,33 +1,52 @@ 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 => { - try { - window.localStorage.setItem('insomnia.publicKey', getInsomniaPublicKey() || res); - } catch { - console.error('Failed to store public key in localStorage.'); - } -}); -encodeBase64(sessionKeyPair.secretKey).then(res => { - try { - window.localStorage.setItem('insomnia.secretKey', getInsomniaSecretKey() || res); - } catch { - console.error('Failed to store secret key in localStorage.'); - } -}); /** * Keypair used for the login handshake. * This keypair can be re-used for the entire session. */ +interface SessionKeyPair { + publicKey: Uint8Array; + secretKey: Uint8Array; +} + +let sessionKeyPairPromise: Promise | null = null; + +async function getSessionKeyPair() { + if (!sessionKeyPairPromise) { + sessionKeyPairPromise = (async () => { + const { keyPair } = await import('../utils/sealedbox'); + const sessionKeyPair = keyPair(); + + encodeBase64(sessionKeyPair.publicKey).then(res => { + try { + window.localStorage.setItem('insomnia.publicKey', getInsomniaPublicKey() || res); + } catch { + console.error('Failed to store public key in localStorage.'); + } + }); + encodeBase64(sessionKeyPair.secretKey).then(res => { + try { + window.localStorage.setItem('insomnia.secretKey', getInsomniaSecretKey() || res); + } catch { + console.error('Failed to store secret key in localStorage.'); + } + }); + + return sessionKeyPair; + })(); + } + + return sessionKeyPairPromise; +} + export async function decodeBase64(base64: string): Promise { try { let uri = 'data:application/octet-binary;base64,'; @@ -65,9 +84,11 @@ export async function encodeBase64(data: Uint8Array): Promise { export async function submitAuthCode(code: string) { try { + await getSessionKeyPair(); 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 { open } = await import('../utils/sealedbox'); const boxData = open(rawBox, publicKey, secretKey); invariant(boxData, 'Invalid authentication code.'); @@ -80,7 +101,8 @@ export async function submitAuthCode(code: string) { } } -export function getLoginUrl() { +export async function getLoginUrl() { + await getSessionKeyPair(); const publicKey = window.localStorage.getItem('insomnia.publicKey'); if (!publicKey) { console.log('[auth] No public key found'); diff --git a/packages/insomnia/src/ui/components/project/organization-select.tsx b/packages/insomnia/src/ui/components/project/organization-select.tsx index 5101ad1bda47..00102a099fd6 100644 --- a/packages/insomnia/src/ui/components/project/organization-select.tsx +++ b/packages/insomnia/src/ui/components/project/organization-select.tsx @@ -68,9 +68,9 @@ export const OrganizationSelect = ({