Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ rootCA2.*
final.cpp
insomnia.ico
final.rc
.tmp*
12 changes: 7 additions & 5 deletions packages/insomnia/src/account/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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. */
Expand All @@ -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(
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -105,7 +107,7 @@ export async function setSessionData(
email: string,
symmetricKey: JsonWebKey,
publicKey: JsonWebKey,
encPrivateKey: crypt.AESMessage,
encPrivateKey: AESMessage,
) {
const sessionData: SessionData = {
id,
Expand Down
46 changes: 46 additions & 0 deletions packages/insomnia/src/common/mime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const extensionToMimeType: Record<string, string> = {
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',
};
Comment on lines +1 to +16
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Addressed in latest commits


const mimeTypeToExtension: Record<string, string> = {
...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;
};
4 changes: 2 additions & 2 deletions packages/insomnia/src/entry.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();
Expand Down
2 changes: 0 additions & 2 deletions packages/insomnia/src/main/window-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -270,7 +269,6 @@ export function createWindow(): ElectronBrowserWindow {
{
label: `${MNEMONIC_SYM}Preferences`,
click: () => {
trackAnalyticsEvent(AnalyticsEvent.AppMenuPreferencesClicked);
mainBrowserWindow.webContents?.send('toggle-preferences');
},
},
Expand Down
3 changes: 2 additions & 1 deletion packages/insomnia/src/network/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we use dynamic import here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Addressed in latest commits

const { cookies, rejectedCookies } = await addSetCookiesToToughCookieJar({
setCookieStrings,
currentUrl,
Expand Down
2 changes: 1 addition & 1 deletion packages/insomnia/src/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
11 changes: 8 additions & 3 deletions packages/insomnia/src/routes/auth.authorize.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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';

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
};
Expand Down
10 changes: 4 additions & 6 deletions packages/insomnia/src/routes/auth.clear-vault-key.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/insomnia/src/routes/auth.login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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';
}
Comment on lines +122 to +125
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Addressed in latest commits

}
};

const writeToDownloadPath = async (
downloadPathAndName: string,
responsePatch: ResponsePatch,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -99,24 +98,27 @@ export const mockRouteToHar = ({
mimeType: string;
headersArray: RequestHeader[];
body: string;
}): Har.Response => {
}): Promise<Har.Response> => {
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 = () => {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading