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/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/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/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..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/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.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/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/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/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 831f094dcc2a..187f96ec2681 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'; +import { decryptSecretValue, encryptSecretValue } from '../../../../utils/vault-crypto'; import { PromptButton } from '../../base/prompt-button'; import { Icon } from '../../icon'; import { showModal } from '../../modals'; 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/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/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 = ({