first pass

This commit is contained in:
jackkav
2026-05-28 15:44:55 +02:00
parent e038e317f7
commit 6357df7d3d
11 changed files with 75 additions and 66 deletions

View File

@@ -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,

View File

@@ -1,14 +1,10 @@
import clone from 'clone';
import type * as Har from 'har-format';
import { Cookie as ToughCookie } from 'tough-cookie';
import type { BaseModel, Environment, Request, RequestGroup, Response, Workspace } from '~/insomnia-data';
import { models, services } from '~/insomnia-data';
import { applyRequestHooks } from '~/network/network-adapter';
import * as plugins from '../plugins';
import * as pluginApp from '../plugins/context/app';
import * as pluginRequest from '../plugins/context/request';
import * as pluginStore from '../plugins/context/store';
import { RenderError } from '../templating/render-error';
import type { RenderedRequest } from '../templating/types';
import { parseGraphQLReqeustBody } from '../utils/graph-ql';
@@ -264,25 +260,7 @@ async function _applyRequestPluginHooks(
renderedRequest: RenderedRequest,
renderedContext: Record<string, any>,
): Promise<RenderedRequest> {
let newRenderedRequest = renderedRequest;
for (const { plugin, hook } of await plugins.getRequestHooks()) {
newRenderedRequest = clone(newRenderedRequest);
const context = {
...(pluginApp.init() as Record<string, any>),
...(pluginRequest.init(newRenderedRequest, renderedContext) as Record<string, any>),
...(pluginStore.init(plugin) as Record<string, any>),
};
try {
await hook(context);
} catch (err) {
err.plugin = plugin;
throw err;
}
}
return newRenderedRequest;
return applyRequestHooks(renderedRequest, renderedContext);
}
export async function exportHarWithRenderedRequest(renderedRequest: RenderedRequest, addContentLength = false) {

View File

@@ -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();

View File

@@ -201,7 +201,7 @@ export function createWindow(): ElectronBrowserWindow {
webPreferences: {
preload: path.join(__dirname, 'entry.preload.min.js'),
zoomFactor: getZoomFactor(),
nodeIntegration: true,
nodeIntegration: false,
nodeIntegrationInWorker: false, // must remain false to ensure the nunjucks web worker sandbox does not have access to Node.js APIs
webviewTag: true,
// TODO: enable context isolation

View File

@@ -1,10 +1,11 @@
import Color from 'color';
import type { ThemeSettings } from '~/insomnia-data';
import { getAppDefaultTheme } from '~/insomnia-data/common';
import { getAppDefaultTheme } from '../common/constants';
import type { SerializableTheme } from './bridge-types';
import { plugins } from './renderer-bridge';
import type { Theme } from './index';
import { type ColorScheme, getThemes } from './index';
export type ColorScheme = 'default' | 'light' | 'dark';
export type HexColor = `#${string}`;
export type RGBColor = `rgb(${string})`;
@@ -331,7 +332,7 @@ export async function setTheme(themeName: string) {
return;
}
const themes: Theme[] = await getThemes();
const themes: SerializableTheme[] = await plugins.getThemes();
let selectedTheme = themes.find(t => t.theme.name === themeName);
if (!selectedTheme) {

View File

@@ -1,5 +1,4 @@
import type { PluginBridgeMetrics, PluginsBridgeAPI } from './bridge-types';
import { invokePluginMethod } from './invoke-method';
// Phase 1a rollback switch: set INSOMNIA_ENABLE_PLUGIN_BRIDGE=false to fall
// back to running plugins directly in the renderer (legacy behaviour).
@@ -7,15 +6,17 @@ import { invokePluginMethod } from './invoke-method';
// plugin-system deps it pulls in don't inflate the preload.
const bridgeEnabled = process.env.INSOMNIA_ENABLE_PLUGIN_BRIDGE !== 'false';
function call<M extends keyof Omit<PluginsBridgeAPI, 'getBridgeMetrics'>>(
async function call<M extends keyof Omit<PluginsBridgeAPI, 'getBridgeMetrics'>>(
method: M,
args?: Parameters<PluginsBridgeAPI[M]>[0],
): ReturnType<PluginsBridgeAPI[M]> {
): Promise<Awaited<ReturnType<PluginsBridgeAPI[M]>>> {
if (bridgeEnabled) {
const fn = (window.main.plugins[method] as (...a: any[]) => any);
return fn(args) as ReturnType<PluginsBridgeAPI[M]>;
return fn(args) as Promise<Awaited<ReturnType<PluginsBridgeAPI[M]>>>;
}
return invokePluginMethod(method as any, args) as ReturnType<PluginsBridgeAPI[M]>;
const { invokePluginMethod } = await import('./invoke-method');
return invokePluginMethod(method as any, args) as Promise<Awaited<ReturnType<PluginsBridgeAPI[M]>>>;
}
const emptyBridgeMetrics: PluginBridgeMetrics = {

View File

@@ -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'));

View File

@@ -1,5 +1,5 @@
import { getVault } from 'insomnia-api';
import { Fragment } from 'react';
import { Fragment, useEffect, useState } from 'react';
import { Button, Heading } from 'react-aria-components';
import { href, redirect, useFetchers, useNavigate } from 'react-router';
@@ -75,7 +75,12 @@ export const useAuthorizeActionFetcher = createFetcherSubmitHook(
);
const Component = () => {
const url = getLoginUrl();
const [url, setUrl] = useState('');
useEffect(() => {
void getLoginUrl().then(setUrl);
}, []);
const copyUrl = () => {
window.clipboard.writeText(url);
};

View File

@@ -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);

View File

@@ -1,33 +1,52 @@
import * as session from '../account/session';
import { getAppWebsiteBaseURL, getInsomniaPublicKey, getInsomniaSecretKey } from '../common/constants';
import { invariant } from '../utils/invariant';
import { keyPair, open } from '../utils/sealedbox';
interface AuthBox {
token: string;
key: string;
}
const sessionKeyPair = keyPair();
encodeBase64(sessionKeyPair.publicKey).then(res => {
try {
window.localStorage.setItem('insomnia.publicKey', getInsomniaPublicKey() || res);
} catch {
console.error('Failed to store public key in localStorage.');
}
});
encodeBase64(sessionKeyPair.secretKey).then(res => {
try {
window.localStorage.setItem('insomnia.secretKey', getInsomniaSecretKey() || res);
} catch {
console.error('Failed to store secret key in localStorage.');
}
});
/**
* Keypair used for the login handshake.
* This keypair can be re-used for the entire session.
*/
interface SessionKeyPair {
publicKey: Uint8Array;
secretKey: Uint8Array;
}
let sessionKeyPairPromise: Promise<SessionKeyPair> | null = null;
async function getSessionKeyPair() {
if (!sessionKeyPairPromise) {
sessionKeyPairPromise = (async () => {
const { keyPair } = await import('../utils/sealedbox');
const sessionKeyPair = keyPair();
encodeBase64(sessionKeyPair.publicKey).then(res => {
try {
window.localStorage.setItem('insomnia.publicKey', getInsomniaPublicKey() || res);
} catch {
console.error('Failed to store public key in localStorage.');
}
});
encodeBase64(sessionKeyPair.secretKey).then(res => {
try {
window.localStorage.setItem('insomnia.secretKey', getInsomniaSecretKey() || res);
} catch {
console.error('Failed to store secret key in localStorage.');
}
});
return sessionKeyPair;
})();
}
return sessionKeyPairPromise;
}
export async function decodeBase64(base64: string): Promise<Uint8Array> {
try {
let uri = 'data:application/octet-binary;base64,';
@@ -65,9 +84,11 @@ export async function encodeBase64(data: Uint8Array): Promise<string> {
export async function submitAuthCode(code: string) {
try {
await getSessionKeyPair();
const rawBox = await decodeBase64(code.trim());
const publicKey = await decodeBase64(window.localStorage.getItem('insomnia.publicKey') || '');
const secretKey = await decodeBase64(window.localStorage.getItem('insomnia.secretKey') || '');
const { open } = await import('../utils/sealedbox');
const boxData = open(rawBox, publicKey, secretKey);
invariant(boxData, 'Invalid authentication code.');
@@ -80,7 +101,8 @@ export async function submitAuthCode(code: string) {
}
}
export function getLoginUrl() {
export async function getLoginUrl() {
await getSessionKeyPair();
const publicKey = window.localStorage.getItem('insomnia.publicKey');
if (!publicKey) {
console.log('[auth] No public key found');

View File

@@ -68,9 +68,9 @@ export const OrganizationSelect = ({
<div className="my-1 border-t border-(--hl-sm)" />
<Button
className="flex h-(--line-height-xs) w-full items-center gap-2 bg-transparent px-(--padding-md) whitespace-nowrap text-(--color-font) transition-colors hover:bg-(--hl-sm) focus:bg-(--hl-xs) focus:outline-hidden"
onPress={() => {
onPress={async () => {
setIsOpen(false);
window.main.openInBrowser(getLoginUrl());
window.main.openInBrowser(await getLoginUrl());
}}
>
<Icon icon="sign-in-alt" />
@@ -78,11 +78,11 @@ export const OrganizationSelect = ({
</Button>
<Button
className="flex h-(--line-height-xs) w-full items-center gap-2 bg-transparent px-(--padding-md) whitespace-nowrap text-(--color-font) transition-colors hover:bg-(--hl-sm) focus:bg-(--hl-xs) focus:outline-hidden"
onPress={() => {
onPress={async () => {
setIsOpen(false);
// If user is in the scratchpad workspace redirect them to the login page
if (isScratchpadWorkspace) {
return window.main.openInBrowser(getLoginUrl());
return window.main.openInBrowser(await getLoginUrl());
}
if (!currentPlan) {