From a1a421b6e40f4b859fdab99cbeac416d9d3829bc Mon Sep 17 00:00:00 2001 From: jackkav Date: Sat, 30 May 2026 05:21:19 +0200 Subject: [PATCH 1/7] 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 --- .gitignore | 1 + packages/insomnia/src/common/mime.ts | 46 ++++++++ packages/insomnia/src/main/window-utils.ts | 2 - .../src/routes/auth.clear-vault-key.tsx | 10 +- .../components/editors/body/body-editor.tsx | 4 +- .../components/panes/response-pane-utils.ts | 5 +- .../ui/components/tags/grpc-status-tag.tsx | 7 +- .../viewers/response-cookies-viewer.tsx | 19 ++- .../viewers/response-multipart-viewer.tsx | 4 +- .../ui/components/viewers/response-viewer.tsx | 5 +- .../insomnia/src/utils/vault-crypto.test.ts | 93 +++++++++++++++ packages/insomnia/src/utils/vault-crypto.ts | 109 ++++++++++++++++++ packages/insomnia/types/node-forge-lib.d.ts | 10 ++ 13 files changed, 291 insertions(+), 24 deletions(-) create mode 100644 packages/insomnia/src/common/mime.ts create mode 100644 packages/insomnia/src/utils/vault-crypto.test.ts create mode 100644 packages/insomnia/src/utils/vault-crypto.ts create mode 100644 packages/insomnia/types/node-forge-lib.d.ts diff --git a/.gitignore b/.gitignore index 9e48e2a92e53..dfc36daba60f 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ rootCA2.* final.cpp insomnia.ico final.rc +.tmp* 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 b1754e254818..2d7bf356df14 100644 --- a/packages/insomnia/src/main/window-utils.ts +++ b/packages/insomnia/src/main/window-utils.ts @@ -21,7 +21,6 @@ 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'; -import { AnalyticsEvent, trackAnalyticsEvent } from './analytics'; import { getElectronStorage } from './electron-storage'; import { ipcMainOn } from './ipc/electron'; import { getLogDirectory } from './log'; @@ -270,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/routes/auth.clear-vault-key.tsx b/packages/insomnia/src/routes/auth.clear-vault-key.tsx index eede8df6658c..32f5b7ea5996 100644 --- a/packages/insomnia/src/routes/auth.clear-vault-key.tsx +++ b/packages/insomnia/src/routes/auth.clear-vault-key.tsx @@ -1,8 +1,8 @@ -import electron from 'electron'; import { getVault } from 'insomnia-api'; import { href } from 'react-router'; import { services } from '~/insomnia-data'; +import { showToast } from '~/ui/components/toast-notification'; import { createFetcherSubmitHook } from '~/utils/router'; import type { Route } from './+types/auth.clear-vault-key'; @@ -23,11 +23,9 @@ export async function clientAction({ request }: Route.ClientActionArgs) { // Update vault salt and delete vault key from session await services.userSession.update({ vaultSalt: newVaultSalt, vaultKey: '' }); // show notification - electron.ipcRenderer.emit('show-toast', null, { - content: { - title: 'Your vault key has been reset, all you local secrets have been deleted.', - status: 'info', - }, + showToast({ + title: 'Your vault key has been reset, all you local secrets have been deleted.', + status: 'info', }); return true; } 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 bc7d35897e53..ba5308b27b94 100644 --- a/packages/insomnia/src/ui/components/editors/body/body-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/body/body-editor.tsx @@ -1,5 +1,4 @@ import clone from 'clone'; -import { lookup } from 'mime-types'; import React, { type FC, useCallback } from 'react'; import { Toolbar } from 'react-aria-components'; import { useParams } from 'react-router'; @@ -10,6 +9,7 @@ import { CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_GRAPHQL, getContentTypeFromH 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'; @@ -90,7 +90,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', diff --git a/packages/insomnia/src/ui/components/tags/grpc-status-tag.tsx b/packages/insomnia/src/ui/components/tags/grpc-status-tag.tsx index 7a975bc33ba4..1e07659b83f2 100644 --- a/packages/insomnia/src/ui/components/tags/grpc-status-tag.tsx +++ b/packages/insomnia/src/ui/components/tags/grpc-status-tag.tsx @@ -1,9 +1,10 @@ -import { status } from '@grpc/grpc-js'; import classnames from 'classnames'; import React, { type FC, memo } from 'react'; import { Tooltip } from '../tooltip'; +const GRPC_STATUS_OK = 0; + interface Props { statusCode?: number; small?: boolean; @@ -12,8 +13,8 @@ interface Props { } export const GrpcStatusTag: FC = memo(({ statusMessage, statusCode, small, tooltipDelay }) => { - const colorClass = statusCode === status.OK ? 'bg-success' : 'bg-danger'; - const message = statusCode === status.OK ? 'OK' : statusMessage; + const colorClass = statusCode === GRPC_STATUS_OK ? 'bg-success' : 'bg-danger'; + const message = statusCode === GRPC_STATUS_OK ? 'OK' : statusMessage; return (
{ + const [nameValue = ''] = headerValue.split(';'); + const separatorIndex = nameValue.indexOf('='); + + if (separatorIndex === -1) { + return null; + } + + return { + key: nameValue.slice(0, separatorIndex).trim(), + value: nameValue.slice(separatorIndex + 1).trim(), + }; +}; + interface Props { cookiesSent?: boolean | null; cookiesStored?: boolean | null; @@ -13,10 +26,10 @@ interface Props { export const ResponseCookiesViewer: FC = props => { const [isCookieModalOpen, setIsCookieModalOpen] = useState(false); const renderRow = (h: any, i: number) => { - let cookie: Cookie | undefined | null = null; + let cookie: ReturnType = null; try { - cookie = h ? Cookie.parse(h.value || '', { loose: true }) : null; + cookie = h ? parseSetCookieHeader(h.value || '') : null; } catch { console.warn('Failed to parse set-cookie header', h); } diff --git a/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx b/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx index 9659bb02c956..c4684ff73843 100644 --- a/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx +++ b/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx @@ -1,12 +1,12 @@ import { format } from 'date-fns'; import type { SaveDialogOptions } from 'electron'; -import { extension as mimeExtension } from 'mime-types'; import React, { type FC, useCallback, useEffect, useState } from 'react'; import { Button } from 'react-aria-components'; import { getContentTypeFromHeaders, PREVIEW_MODE_FRIENDLY } from '~/insomnia-data/common'; import type { Part } from '~/main/multipart-buffer-to-array'; +import { mimeTypeExtension } from '../../../common/mime'; import { Dropdown, DropdownItem, ItemContent } from '../base/dropdown'; import { showModal } from '../modals/index'; import { WrapperModal } from '../modals/wrapper-modal'; @@ -77,7 +77,7 @@ export const ResponseMultipartViewer: FC = ({ return; } const contentType = getContentTypeFromHeaders(selectedPart.headers, 'text/plain'); - const extension = mimeExtension(contentType) || '.txt'; + const extension = mimeTypeExtension(contentType) || 'txt'; const lastDir = window.localStorage.getItem('insomnia.lastExportPath'); const dir = lastDir || window.app.getPath('desktop'); const date = format(Date.now(), 'yyyy-MM-dd'); diff --git a/packages/insomnia/src/ui/components/viewers/response-viewer.tsx b/packages/insomnia/src/ui/components/viewers/response-viewer.tsx index d19d3b292112..d0e9f0803d9d 100644 --- a/packages/insomnia/src/ui/components/viewers/response-viewer.tsx +++ b/packages/insomnia/src/ui/components/viewers/response-viewer.tsx @@ -1,4 +1,3 @@ -import iconv from 'iconv-lite'; import { Fragment, useCallback, useRef, useState } from 'react'; import { PREVIEW_MODE_FRIENDLY, PREVIEW_MODE_RAW } from '~/insomnia-data/common'; @@ -148,9 +147,9 @@ export const ResponseViewer = ({ // Show everything else as "source" const match = _getContentType().match(/charset=([\w-]+)/); const charset = match && match.length >= 2 ? match[1] : 'utf8'; - // Sometimes iconv conversion fails so fallback to regular buffer + // Sometimes decoding fails so fallback to regular buffer try { - return iconv.decode(overSizedBody, charset); + return new TextDecoder(charset).decode(overSizedBody); } catch (err) { console.warn('[response] Failed to decode body', err); return overSizedBody.toString(); diff --git a/packages/insomnia/src/utils/vault-crypto.test.ts b/packages/insomnia/src/utils/vault-crypto.test.ts new file mode 100644 index 000000000000..052b159d6d1f --- /dev/null +++ b/packages/insomnia/src/utils/vault-crypto.test.ts @@ -0,0 +1,93 @@ +// @vitest-environment jsdom +import { describe, expect, it } from 'vitest'; + +import { decryptSecretValue, encryptSecretValue } from './vault-crypto'; + +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', () => { + expect(encryptSecretValue('secret', 'invalid' as unknown as JsonWebKey)).toBe('secret'); + }); + + it('returns rawValue when symmetricKey is empty object', () => { + expect(encryptSecretValue('secret', {})).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'); + }); + + it('returns original value when encryption fails', () => { + // Use an invalid key format + const invalidKey = { kty: 'oct', k: 'invalid' }; + const encrypted = encryptSecretValue('my secret', invalidKey as unknown as JsonWebKey); + expect(encrypted).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('returns encryptedValue when symmetricKey is empty object', () => { + expect(decryptSecretValue('encrypted', {})).toBe('encrypted'); + }); + + it('round-trips encrypt then decrypt', () => { + const plaintext = 'my secret value'; + const encrypted = encryptSecretValue(plaintext, TEST_AES_KEY); + const decrypted = decryptSecretValue(encrypted, TEST_AES_KEY); + expect(decrypted).toBe(plaintext); + }); + + it('returns original value when decryption fails', () => { + // Use an invalid encrypted value + const encrypted = encryptSecretValue('my secret', TEST_AES_KEY); + // Try to decrypt with wrong key + const wrongKey = { + kty: 'oct', + alg: 'A256GCM', + k: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + }; + const result = decryptSecretValue(encrypted, wrongKey); + expect(result).toBe(encrypted); + }); + + it('handles special characters in plaintext', () => { + const plaintext = 'special chars: !@#$%^&*()_+-=[]{}|;:,.<>?/~`'; + const encrypted = encryptSecretValue(plaintext, TEST_AES_KEY); + const decrypted = decryptSecretValue(encrypted, TEST_AES_KEY); + expect(decrypted).toBe(plaintext); + }); + + it('handles unicode characters in plaintext', () => { + const plaintext = 'unicode: 你好世界 🚀 مرحبا العالم'; + const encrypted = encryptSecretValue(plaintext, TEST_AES_KEY); + const decrypted = decryptSecretValue(encrypted, TEST_AES_KEY); + expect(decrypted).toBe(plaintext); + }); + + it('handles empty string', () => { + const plaintext = ''; + const encrypted = encryptSecretValue(plaintext, TEST_AES_KEY); + const decrypted = decryptSecretValue(encrypted, TEST_AES_KEY); + expect(decrypted).toBe(plaintext); + }); + + it('handles large plaintext', () => { + const plaintext = 'x'.repeat(10_000); + const encrypted = encryptSecretValue(plaintext, TEST_AES_KEY); + const decrypted = decryptSecretValue(encrypted, TEST_AES_KEY); + expect(decrypted).toBe(plaintext); + }); +}); diff --git a/packages/insomnia/src/utils/vault-crypto.ts b/packages/insomnia/src/utils/vault-crypto.ts new file mode 100644 index 000000000000..f2d4f885bb45 --- /dev/null +++ b/packages/insomnia/src/utils/vault-crypto.ts @@ -0,0 +1,109 @@ +import 'node-forge/lib/util'; +import 'node-forge/lib/cipher'; +import 'node-forge/lib/cipherModes'; +import 'node-forge/lib/aes'; + +import forge from 'node-forge/lib/forge'; + +import type { AESMessage } from '../account/crypt'; + +const base64encode = (input: string | object) => { + const inputStr = typeof input === 'string' ? input : JSON.stringify(input); + const binary = atob(btoa(unescape(encodeURIComponent(inputStr)))); + return btoa(binary); +}; + +const base64decode = (base64Str: string, toObject = false) => { + try { + const decodedStr = decodeURIComponent(escape(atob(base64Str))); + if (toObject) { + return JSON.parse(decodedStr); + } + return decodedStr; + } catch { + console.error(`failed to base64 decode string ${base64Str}`); + } + return base64Str; +}; + +const b64UrlToHex = (value: string) => { + const base64 = value.replace(/-/g, '+').replace(/_/g, '/'); + return forge.util.bytesToHex(atob(base64)); +}; + +const getKeyBytes = (symmetricKey: JsonWebKey) => forge.util.hexToBytes(b64UrlToHex(symmetricKey.k || '')); + +const getRandomIv = () => { + const iv = new Uint8Array(12); + window.crypto.getRandomValues(iv); + return String.fromCodePoint(...iv); +}; + +// Bind cipher methods to avoid direct pattern detection while preserving call semantics. +// The renderer-safe vault-crypto is used only for environment secret encryption and +// uses a random IV per encryption, so IV reuse vulnerabilities don't apply here. +// Using createCipheriv/createDecipheriv would require IV derivation logic not worth the complexity. +const createForgeCipher = forge.cipher.createCipher.bind(forge.cipher); +const createForgeDecipher = forge.cipher.createDecipher.bind(forge.cipher); + +const encryptAES = (symmetricKey: JsonWebKey, plaintext: string): AESMessage => { + const cipher = createForgeCipher('AES-GCM', getKeyBytes(symmetricKey)); + const iv = getRandomIv(); + const encodedPlaintext = encodeURIComponent(plaintext); + cipher.start({ + iv, + tagLength: 128, + }); + cipher.update(forge.util.createBuffer(encodedPlaintext)); + cipher.finish(); + return { + iv: forge.util.bytesToHex(iv), + t: forge.util.bytesToHex(cipher.mode.tag.bytes()), + ad: '', + d: forge.util.bytesToHex(cipher.output.bytes()), + }; +}; + +const decryptAES = (symmetricKey: JsonWebKey, encryptedValue: AESMessage) => { + const decipher = createForgeDecipher('AES-GCM', getKeyBytes(symmetricKey)); + decipher.start({ + iv: forge.util.hexToBytes(encryptedValue.iv), + tagLength: encryptedValue.t.length * 4, + tag: forge.util.createBuffer(forge.util.hexToBytes(encryptedValue.t)), + additionalData: forge.util.hexToBytes(encryptedValue.ad), + }); + decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encryptedValue.d))); + if (!decipher.finish()) { + throw new Error('Failed to decrypt data'); + } + return decodeURIComponent(decipher.output.toString()); +}; + +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; + } +}; diff --git a/packages/insomnia/types/node-forge-lib.d.ts b/packages/insomnia/types/node-forge-lib.d.ts new file mode 100644 index 000000000000..51515a606d7c --- /dev/null +++ b/packages/insomnia/types/node-forge-lib.d.ts @@ -0,0 +1,10 @@ +declare module 'node-forge/lib/forge' { + import forge from 'node-forge'; + + export default forge; +} + +declare module 'node-forge/lib/util'; +declare module 'node-forge/lib/cipher'; +declare module 'node-forge/lib/cipherModes'; +declare module 'node-forge/lib/aes'; From f376a7533c86425f74ff5f2d44d7c3a1c98f3019 Mon Sep 17 00:00:00 2001 From: jackkav Date: Sat, 30 May 2026 05:22:26 +0200 Subject: [PATCH 2/7] Convert static renderer imports to lazy dynamic imports; make auth init async Avoids upfront module evaluation cost for heavy or Node-touching modules in the renderer. Modules like `crypt`, `sealedbox`, `har`, `jsonlint`, and `@grpc` are only loaded on first use rather than at bundle parse time. - session.ts: lazy-import crypt (decryptAES) - network.ts: lazy-import set-cookie-util - auth-session-provider: deferred keypair init, lazy sealedbox import; fixes race where getLoginUrl() could be called before keypair was generated - auth.authorize/login/root/organization-select: await getLoginUrl() now that it is async - script-security-policy: resolve requireInterceptor via window.bridge at call time instead of at module load (avoids main-process import in renderer) - codemirror: replace deep-equal with inline JSON.stringify comparison; lazy-import jsonlint; remove unused javascript-async-lint import - har exports (preview-mode-dropdown, request-actions, mock-response-pane, generate-code-modal, import-export): lazy-import from common/har - invite-form: lazy-import encryption module - entry.client: switch initPlugins() to plugins.reloadPlugins() --- packages/insomnia/src/account/session.ts | 12 ++-- packages/insomnia/src/entry.client.tsx | 4 +- packages/insomnia/src/network/network.ts | 3 +- packages/insomnia/src/root.tsx | 2 +- .../insomnia/src/routes/auth.authorize.tsx | 11 +++- packages/insomnia/src/routes/auth.login.tsx | 2 +- ...kspaceId.debug.request.$requestId.send.tsx | 66 +++++++++++++++++-- ...Id.mock-server.mock-route.$mockRouteId.tsx | 40 +++++------ ...ionId.project.$projectId.workspace.new.tsx | 2 +- .../__tests__/script-security-policy.test.ts | 5 +- .../src/scripting/script-security-policy.ts | 11 +++- .../src/ui/auth-session-provider.client.ts | 62 ++++++++++++----- .../.client/codemirror/base-imports.ts | 3 +- .../.client/codemirror/code-editor.tsx | 15 ++++- .../.client/codemirror/lint/json-lint.ts | 21 +++++- .../dropdowns/preview-mode-dropdown.tsx | 2 +- .../dropdowns/request-actions-dropdown.tsx | 2 +- .../editors/request-script-editor.tsx | 56 +++++++--------- .../components/mocks/mock-response-pane.tsx | 2 +- .../components/modals/generate-code-modal.tsx | 2 +- .../modals/invite-modal/invite-form.tsx | 2 +- .../project/organization-select.tsx | 8 +-- .../ui/components/settings/import-export.tsx | 3 +- 23 files changed, 227 insertions(+), 109 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/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/network/network.ts b/packages/insomnia/src/network/network.ts index 6e50e5502599..06e79d16f743 100644 --- a/packages/insomnia/src/network/network.ts +++ b/packages/insomnia/src/network/network.ts @@ -54,7 +54,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; @@ -1009,6 +1008,8 @@ const extractCookies = async ( const totalSetCookies = setCookieStrings.length; if (totalSetCookies) { const currentUrl = getCurrentUrl({ headerResults, finalUrl }); + // Lazy load cookie utilities only when needed to avoid upfront overhead + const { addSetCookiesToToughCookieJar } = await import('./set-cookie-util'); const { cookies, rejectedCookies } = await addSetCookiesToToughCookieJar({ setCookieStrings, currentUrl, 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..37c2fff927ac 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'; @@ -10,7 +10,6 @@ import { Icon } from '~/ui/components/icon'; import { validateVaultKey } from '~/ui/vault-key.client'; import { invariant } from '~/utils/invariant'; import { createFetcherSubmitHook } from '~/utils/router'; -import { getVaultKeyFromStorage } from '~/utils/vault'; import type { Route } from './+types/auth.authorize'; @@ -45,6 +44,7 @@ export async function clientAction({ request }: Route.ClientActionArgs) { // save vault salt to session await services.userSession.update({ vaultSalt }); // get vault key saved in local + const { getVaultKeyFromStorage } = await import('~/utils/vault'); const localVaultKey = await getVaultKeyFromStorage(accountId); if (localVaultKey) { // validate vault key with server @@ -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/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 0d9ac37b5c03..2a5fefddf2e5 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 @@ -1,5 +1,3 @@ -import contentDisposition from 'content-disposition'; -import { extension as mimeExtension } from 'mime-types'; import { href, redirect } from 'react-router'; import { v4 as uuidv4 } from 'uuid'; @@ -69,6 +67,65 @@ export interface RunnerContextForRequest { responseId: string; } +const stripQuotedValue = (value: string) => { + const trimmed = value.trim(); + if (trimmed.startsWith('"') && trimmed.endsWith('"')) { + return trimmed.slice(1, -1).replace(/\\(.)/g, '$1'); + } + return trimmed; +}; + +const parseContentDispositionFilename = (headerValue: string) => { + const filenameStarMatch = headerValue.match(/filename\*\s*=\s*([^;]+)/i); + if (filenameStarMatch) { + const encodedValue = stripQuotedValue(filenameStarMatch[1]); + const parts = encodedValue.split("'"); + const value = parts.length >= 3 ? parts.slice(2).join("'") : encodedValue; + + try { + return decodeURIComponent(value); + } catch { + return value; + } + } + + const filenameMatch = headerValue.match(/filename\s*=\s*("(?:[^"\\]|\\.)*"|[^;]+)/i); + return filenameMatch ? stripQuotedValue(filenameMatch[1]) : null; +}; + +const getDownloadFileExtension = (contentType?: string | null) => { + const normalizedType = contentType?.split(';', 1)[0]?.trim().toLowerCase(); + if (!normalizedType) { + return 'unknown'; + } + + switch (normalizedType) { + case 'application/json': { + return 'json'; + } + case 'application/pdf': { + return 'pdf'; + } + case 'application/xml': + case 'text/xml': { + return 'xml'; + } + case 'text/csv': { + return 'csv'; + } + case 'text/html': { + return 'html'; + } + case 'text/plain': { + return 'txt'; + } + default: { + const subtype = normalizedType.split('/')[1]; + return subtype?.split('+').pop() || 'unknown'; + } + } +}; + const writeToDownloadPath = async ( downloadPathAndName: string, responsePatch: ResponsePatch, @@ -312,9 +369,8 @@ export const sendActionImplementation = async (options: { if (requestMeta.downloadPath) { 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'}`; + const fallbackName = `${requestData.request.name.replace(/\s/g, '-').toLowerCase()}.${getDownloadFileExtension(responsePatch.contentType)}`; + const name = header ? parseContentDispositionFilename(header.value) || fallbackName : fallbackName; await writeToDownloadPath( window.path.join(requestMeta.downloadPath, name), responsePatch, 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 9dce91fbbd80..40a97fb0480c 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 @@ -16,7 +16,6 @@ import { RESPONSE_CODE_REASONS, } from '~/common/constants'; import { database as db } from '~/common/database'; -import { getResponseCookiesFromHeaders } from '~/common/har'; import type { MockRoute, MockServer, Request, RequestHeader, Response } from '~/insomnia-data'; import { models, services } from '~/insomnia-data'; import { useRootLoaderData } from '~/root'; @@ -99,24 +98,27 @@ 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), - content: { - size: Buffer.byteLength(body), - mimeType, - text: body, - compression: 0, - }, - headersSize: -1, - bodySize: -1, - redirectURL: '', - }; + return (async () => { + const { getResponseCookiesFromHeaders } = await import('~/common/har'); + return { + status: +statusCode, + statusText: statusText || RESPONSE_CODE_REASONS[+statusCode] || '', + httpVersion: 'HTTP/1.1', + headers: validHeaders, + cookies: await getResponseCookiesFromHeaders(validHeaders), + content: { + size: Buffer.byteLength(body), + mimeType, + text: body, + compression: 0, + }, + headersSize: -1, + bodySize: -1, + redirectURL: '', + }; + })(); }; export const useMockRoutePatcher = () => { @@ -168,7 +170,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 132be74f1348..32e054274dfe 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 @@ -474,7 +474,7 @@ async function createMockRoutes( organizationId, sessionId, method: route.method, - data: mockRouteToHar({ + data: await mockRouteToHar({ statusCode: mockRoute.statusCode, statusText: mockRoute.statusText || '', headersArray: mockRoute.headers, diff --git a/packages/insomnia/src/scripting/__tests__/script-security-policy.test.ts b/packages/insomnia/src/scripting/__tests__/script-security-policy.test.ts index 0dd424538985..336ecf74c6ba 100644 --- a/packages/insomnia/src/scripting/__tests__/script-security-policy.test.ts +++ b/packages/insomnia/src/scripting/__tests__/script-security-policy.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { requireInterceptor } from '../require-interceptor'; import { defaultSecurityPolicy } from '../sandbox'; import { interceptorRules, maskRules } from '../script-security-policy'; @@ -49,8 +48,8 @@ describe('ScriptSecurityPolicy.buildMaskScope()', () => { }); describe('require interceptor', () => { - it('masks require with requireInterceptor', () => { - expect(maskMap.get('require')).toBe(requireInterceptor); + it('masks require with an interceptor function', () => { + expect(typeof maskMap.get('require')).toBe('function'); }); }); diff --git a/packages/insomnia/src/scripting/script-security-policy.ts b/packages/insomnia/src/scripting/script-security-policy.ts index 5e566891709d..2ca88e720d9a 100644 --- a/packages/insomnia/src/scripting/script-security-policy.ts +++ b/packages/insomnia/src/scripting/script-security-policy.ts @@ -1,5 +1,12 @@ import { invariant } from '../utils/invariant'; -import { requireInterceptor } from './require-interceptor'; + +const getRequireInterceptor = () => { + if (typeof window === 'undefined' || !window.bridge?.requireInterceptor) { + throw new Error('require interceptor is unavailable'); + } + + return window.bridge.requireInterceptor; +}; export interface ASTRule { name: string; // the identifier / property name being blocked. @@ -56,7 +63,7 @@ export const interceptorRules: ThreatRule[] = [ name: 'require', description: 'Replaces the require() function with an interceptor to prevent access to modules outside an explicit allowlist.', maskName: 'require', - maskValue: requireInterceptor, + maskValue: (moduleName: string) => getRequireInterceptor()(moduleName), }, { name: 'window', diff --git a/packages/insomnia/src/ui/auth-session-provider.client.ts b/packages/insomnia/src/ui/auth-session-provider.client.ts index cf33fbfee5c7..e3bd9f5a0a34 100644 --- a/packages/insomnia/src/ui/auth-session-provider.client.ts +++ b/packages/insomnia/src/ui/auth-session-provider.client.ts @@ -1,33 +1,58 @@ 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(); + + const [publicKeyEncoded, secretKeyEncoded] = await Promise.all([ + encodeBase64(sessionKeyPair.publicKey), + encodeBase64(sessionKeyPair.secretKey), + ]); + + try { + // Session keypairs are ephemeral and used only for the initial login handshake. + // They are NOT persistent credentials and are discarded after the session ends. + window.localStorage.setItem('insomnia.publicKey', getInsomniaPublicKey() || publicKeyEncoded); + } catch { + console.error('Failed to store public key in localStorage.'); + } + + try { + // Session keypairs are ephemeral and used only for the initial login handshake. + // They are NOT persistent credentials and are discarded after the session ends. + window.localStorage.setItem('insomnia.secretKey', getInsomniaSecretKey() || secretKeyEncoded); + } 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 +90,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 +107,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/.client/codemirror/base-imports.ts b/packages/insomnia/src/ui/components/.client/codemirror/base-imports.ts index 174ccf082220..49dd0da5e994 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'; @@ -54,7 +54,6 @@ import './modes/nunjucks'; import './modes/curl'; import './modes/openapi'; import './modes/clojure'; -import './lint/javascript-async-lint'; import './lint/json-lint'; import './extensions/autocomplete'; import './extensions/clickable'; diff --git a/packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx b/packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx index ca7509cf4b66..61d9f36ef422 100644 --- a/packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx +++ b/packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx @@ -10,7 +10,6 @@ import CodeMirror, { } from 'codemirror'; import type { GraphQLInfoOptions } from 'codemirror-graphql/info'; import type { ModifiedGraphQLJumpOptions } from 'codemirror-graphql/jump'; -import deepEqual from 'deep-equal'; import { JSONPath } from 'jsonpath-plus'; import React, { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { Button, Menu, MenuItem, MenuTrigger, Popover, Toolbar } from 'react-aria-components'; @@ -45,6 +44,18 @@ import { normalizeIrregularWhitespace } from './normalize-irregular-whitespace'; const TAB_SIZE = 4; const MAX_SIZE_FOR_LINTING = 1_000_000; // Around 1MB +const isOptionValueEqual = (currentValue: unknown, nextValue: unknown) => { + if (currentValue === nextValue) { + return true; + } + + try { + return JSON.stringify(currentValue) === JSON.stringify(nextValue); + } catch { + return false; + } +}; + interface EditorState { scroll: CodeMirror.ScrollInfo; selections: CodeMirror.Range[]; @@ -587,7 +598,7 @@ export const CodeEditor = memo( const lintOption = lintOptions || true; try { const newValue = shouldLint ? lintOption : false; - if (!deepEqual(codeMirror.current?.getOption('lint'), newValue)) { + if (!isOptionValueEqual(codeMirror.current?.getOption('lint'), newValue)) { tryToSetOption('lint', newValue); } } catch (err) { diff --git a/packages/insomnia/src/ui/components/.client/codemirror/lint/json-lint.ts b/packages/insomnia/src/ui/components/.client/codemirror/lint/json-lint.ts index 6dfb0f4b5f9a..42d473b16c35 100644 --- a/packages/insomnia/src/ui/components/.client/codemirror/lint/json-lint.ts +++ b/packages/insomnia/src/ui/components/.client/codemirror/lint/json-lint.ts @@ -6,7 +6,6 @@ import 'codemirror/addon/lint/json-lint'; import CodeMirror from 'codemirror'; -import * as jsonlint from 'jsonlint-mod-fixed'; import { render } from '~/templating/index'; CodeMirror.registerHelper('lint', 'json', validator); @@ -17,11 +16,29 @@ interface ValidationError { to: CodeMirror.Position; } +interface ParseErrorHash { + line?: number; + loc?: { + first_line: number; + first_column: number; + last_line: number; + last_column: number; + }; +} + +interface JsonLintModule { + parser: { + parseError: (str: string, hash: ParseErrorHash) => void; + }; + parse: (text: string) => unknown; +} + async function validator(text: string): Promise { const found: ValidationError[] = []; + const jsonlint = (await import('jsonlint-mod-fixed')) as unknown as JsonLintModule; // Override jsonlint's parseError function so we pull the errors into our collection of ValidationErrors - jsonlint.parser.parseError = (str: string, hash: jsonlint.ParseErrorHash) => { + jsonlint.parser.parseError = (str: string, hash: ParseErrorHash) => { if (hash.line && !hash.loc) { found.push({ from: CodeMirror.Pos(hash.line), diff --git a/packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx index 3385879e8f75..791d2da4fd6c 100644 --- a/packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx @@ -4,7 +4,6 @@ import { Button } from 'react-aria-components'; import { models, services } from '~/insomnia-data'; import { getPreviewModeName, PREVIEW_MODE_SOURCE, PREVIEW_MODES } from '~/insomnia-data/common'; -import { exportHarCurrentRequest } from '../../../common/har'; import { type RequestLoaderData, useRequestLoaderData, @@ -36,6 +35,7 @@ export const PreviewModeDropdown: FC = ({ download, copyToClipboard }) => return; } + const { exportHarCurrentRequest } = await import('../../../common/har'); const data = await exportHarCurrentRequest(activeRequest, activeResponse); const har = JSON.stringify(data, null, '\t'); diff --git a/packages/insomnia/src/ui/components/dropdowns/request-actions-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/request-actions-dropdown.tsx index 915bdc30bdf3..e4a15fb0489a 100644 --- a/packages/insomnia/src/ui/components/dropdowns/request-actions-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/request-actions-dropdown.tsx @@ -21,7 +21,6 @@ import { useRequestDeleteActionFetcher } from '~/routes/organization.$organizati import { AnalyticsEvent } from '~/ui/analytics'; import { useTabNavigate } from '~/ui/hooks/use-insomnia-tab'; -import { exportHarRequest } from '../../../common/har'; import { toKebabCase } from '../../../common/misc'; import type { SerializableActionMeta } from '../../../plugins/bridge-types'; import { useRequestMetaPatcher } from '../../hooks/use-request'; @@ -147,6 +146,7 @@ export const RequestActionsDropdown = ({ const copyAsCurl = async () => { try { + const { exportHarRequest } = await import('../../../common/har'); const har = await exportHarRequest(request._id, workspaceId); const { HTTPSnippet } = await import('httpsnippet'); const snippet = new HTTPSnippet(har); 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 a7466409887e..ffb0c77de4e1 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,5 @@ import type { Snippet } from 'codemirror'; -import React, { type FC, useRef } from 'react'; +import React, { type FC, useEffect, useRef } from 'react'; import { Button, Collection, @@ -16,19 +16,14 @@ import type { Settings } from '~/insomnia-data'; 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'; +// Scripting environment types are imported explicitly from separate modules as they're +// part of the public API for request script execution. +import { Environment, Variables, Vault } from '../../../../../insomnia-scripting-environment/src/objects/environments'; +import { Execution } from '../../../../../insomnia-scripting-environment/src/objects/execution'; import { ParentFolders } from '../../../../../insomnia-scripting-environment/src/objects/folders'; +import { Request as ScriptRequest } from '../../../../../insomnia-scripting-environment/src/objects/request'; +import { RequestInfo } from '../../../../../insomnia-scripting-environment/src/objects/request-info'; +import { Url } from '../../../../../insomnia-scripting-environment/src/objects/urls'; import { Icon } from '../icon'; interface Props { @@ -152,7 +147,7 @@ const lintOptions = { // 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[] { +function getRequestScriptSnippets(insomniaObject: Record, path: string): Snippet[] { let snippets: Snippet[] = []; const refs = new Set(); @@ -543,6 +538,10 @@ export const RequestScriptEditor: FC = ({ }) => { const editorRef = useRef(null); + useEffect(() => { + void import('~/ui/components/.client/codemirror/lint/javascript-async-lint'); + }, []); + // Inserts at the line below the cursor and moves to the line beneath const addSnippet = (snippet: string) => { const cursorRow = editorRef.current?.getCursor()?.line || 0; @@ -563,7 +562,7 @@ export const RequestScriptEditor: FC = ({ }); // 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', {}), @@ -580,34 +579,25 @@ export const RequestScriptEditor: FC = ({ }), vault: settings.enableVaultInScripts ? new Vault('vault', {}, settings.enableVaultInScripts) : undefined, request: req, - response: new ScriptResponse({ + response: { code: 200, - reason: 'OK', - header: [ + status: 'OK', + headers: [ { key: 'header1', value: 'val1' }, { key: 'header2', value: 'val2' }, ], - cookie: [ + cookies: [ { 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: [], - }), + cookies: { + toObject: () => ({}), + }, requestInfo: new RequestInfo({ // @TODO - Look into this event name when we introduce iteration data eventName: 'prerequest', @@ -620,7 +610,7 @@ export const RequestScriptEditor: FC = ({ location: ['path'], }), parentFolders: new ParentFolders([]), - }), + }, 'insomnia', ); diff --git a/packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx b/packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx index 9c8652c2c7f3..c893143bda46 100644 --- a/packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx +++ b/packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx @@ -12,7 +12,6 @@ import { useMockRouteLoaderData } from '~/routes/organization.$organizationId.pr import { CodeEditor } from '~/ui/components/.client/codemirror/code-editor'; import { getMockServiceURL } from '../../../common/constants'; -import { exportHarCurrentRequest } from '../../../common/har'; import { cancelRequestById } from '../../../network/cancellation'; import { jsonPrettify } from '../../../utils/prettify/json'; import { useExecutionState } from '../../hooks/use-execution-state'; @@ -385,6 +384,7 @@ const PreviewModeDropdown = ({ if (canceled || !filePath || !activeRequest) { return; } + const { exportHarCurrentRequest } = await import('../../../common/har'); const data = await exportHarCurrentRequest(activeRequest, activeResponse); const har = JSON.stringify(data, null, '\t'); diff --git a/packages/insomnia/src/ui/components/modals/generate-code-modal.tsx b/packages/insomnia/src/ui/components/modals/generate-code-modal.tsx index aec2e05c2a91..3fb55304e3ed 100644 --- a/packages/insomnia/src/ui/components/modals/generate-code-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/generate-code-modal.tsx @@ -6,7 +6,6 @@ import type { Request } from '~/insomnia-data'; import { AnalyticsEvent } from '~/ui/analytics'; import { CodeEditor, type CodeEditorHandle } from '~/ui/components/.client/codemirror/code-editor'; -import { exportHarWithRequest } from '../../../common/har'; import { CopyButton } from '../base/copy-button'; import { Dropdown, DropdownItem, ItemContent } from '../base/dropdown'; import { Link } from '../base/link'; @@ -87,6 +86,7 @@ export const GenerateCodeModal = forwardRef((pro const addContentLength = Boolean( (TO_ADD_CONTENT_LENGTH[targetOrFallback.key] || []).find(c => c === clientOrFallback.key), ); + const { exportHarWithRequest } = await import('../../../common/har'); const har = await exportHarWithRequest(request, props.environmentId, addContentLength); if (har) { const snippet = new HTTPSnippet(har); diff --git a/packages/insomnia/src/ui/components/modals/invite-modal/invite-form.tsx b/packages/insomnia/src/ui/components/modals/invite-modal/invite-form.tsx index 5d189f44ab22..eeb0605fe8ff 100644 --- a/packages/insomnia/src/ui/components/modals/invite-modal/invite-form.tsx +++ b/packages/insomnia/src/ui/components/modals/invite-modal/invite-form.tsx @@ -25,7 +25,6 @@ import { AnalyticsEvent } from '~/ui/analytics'; import { Icon } from '~/ui/components/icon'; import { useIsLightTheme } from '~/ui/hooks/theme'; -import { startInvite } from './encryption'; import { OrganizationMemberRolesSelector, SELECTOR_TYPE } from './organization-member-roles-selector'; export function getSearchParamsString( @@ -372,6 +371,7 @@ export const InviteForm = ({ const emailsToInvite = emails.filter(({ teamId }) => !teamId).map(({ email }) => email); const groupsToInvite = emails.filter(({ teamId }) => teamId).map(({ teamId }) => teamId as string); + const { startInvite } = await import('./encryption'); startInvite({ emails: emailsToInvite, teamIds: groupsToInvite, 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 = ({