mirror of
https://github.com/Kong/insomnia.git
synced 2026-08-04 03:42:20 -04:00
* fix prompt * refactor: implement app context methods via fetch bridge (#10034) Replace stub app context methods (alert, dialog, prompt, getPath, clipboard operations, showSaveDialog) with actual implementations using the fetch bridge to the main process. - Add 8 new entries to pluginToMainAPI in templating-worker-database.ts - Update AppContext interface to use async methods (matching worker reality) - Replace worker-side throws with fetchFromTemplateWorkerDatabase calls - Update renderer-side app context to return promises --------- Co-authored-by: jackkav <jackkav@gmail.com> Co-authored-by: Jay Wu <jay.wu@konghq.com>
This commit is contained in:
@@ -150,7 +150,4 @@ test('Critical Path For Template Tags Interactions', async ({ page, app, insomni
|
||||
const { tagPrefix } = templateTagTestCases.prompt[0];
|
||||
await page.locator(`[data-template^="${tagPrefix}"]`).isVisible();
|
||||
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
|
||||
// prompt tag is blocked in the sandboxed render-adapter worker
|
||||
await expect.soft(page.getByText('Unexpected Request Failure')).toBeVisible();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'OK' }).click();
|
||||
});
|
||||
|
||||
@@ -442,8 +442,8 @@ const main: Window['main'] = {
|
||||
applyResponseHooks: (args: ApplyResponseHooksArgs) => invokePluginBridgeMethod('applyResponseHooks', args),
|
||||
getBridgeMetrics: () => invokeWithNormalizedError('plugins.getBridgeMetrics'),
|
||||
},
|
||||
notifyPluginPromptResult: (id: string, value: string | null) =>
|
||||
ipcRenderer.send('plugins.uiPromptResult', { id, value }),
|
||||
notifyPromptResult: (id: string, value: string | null) =>
|
||||
ipcRenderer.send('ui.promptResult', { id, value }),
|
||||
timeline: {
|
||||
getPath: (responseId: string) => invokeWithNormalizedError('timeline.getPath', responseId) as Promise<string>,
|
||||
appendToFile: (options: { timelinePath: string; data: string }) =>
|
||||
|
||||
@@ -145,7 +145,6 @@ export type HandleChannels =
|
||||
| 'plugins.hasResponseHooks'
|
||||
| 'plugins.reloadPlugins'
|
||||
| 'plugins.runTemplateTagAction'
|
||||
| 'plugins.uiPrompt'
|
||||
| 'openPath'
|
||||
| 'parseImport'
|
||||
| 'readCurlResponse'
|
||||
@@ -251,7 +250,7 @@ export type MainOnChannels =
|
||||
| 'sync.cancelConflict'
|
||||
| 'sync.resolveConflict'
|
||||
| 'mcp.sendMCPRequest'
|
||||
| 'plugins.uiPromptResult'
|
||||
| 'ui.promptResult'
|
||||
| 'writeText';
|
||||
|
||||
export type RendererOnChannels =
|
||||
@@ -259,7 +258,7 @@ export type RendererOnChannels =
|
||||
| 'db.changes'
|
||||
| 'plugins.uiAlert'
|
||||
| 'plugins.uiDialog'
|
||||
| 'plugins.uiPrompt'
|
||||
| 'ui.prompt'
|
||||
| 'grpc.data'
|
||||
| 'grpc.end'
|
||||
| 'grpc.error'
|
||||
|
||||
@@ -313,7 +313,7 @@ export interface RendererToMainBridgeAPI {
|
||||
>;
|
||||
syncNewWorkspaceIfNeeded: typeof syncNewWorkspaceIfNeeded;
|
||||
plugins: PluginsBridgeAPI;
|
||||
notifyPluginPromptResult: (id: string, value: string | null) => void;
|
||||
notifyPromptResult: (id: string, value: string | null) => void;
|
||||
vault: {
|
||||
encryptSecretValue: (rawValue: string, symmetricKey: JsonWebKey) => Promise<string>;
|
||||
decryptSecretValue: (encryptedValue: string, symmetricKey: JsonWebKey) => Promise<string>;
|
||||
|
||||
@@ -3,6 +3,9 @@ import path from 'node:path';
|
||||
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
import { requestPromptFromRenderer } from './prompt-bridge';
|
||||
import { getMainWindow } from './window-utils';
|
||||
|
||||
let pluginWindow: BrowserWindow | null = null;
|
||||
let windowReady = false;
|
||||
const pendingRequests = new Map<
|
||||
@@ -12,7 +15,6 @@ const pendingRequests = new Map<
|
||||
|
||||
let cachedHasRequestHooks: boolean | null = null;
|
||||
let cachedHasResponseHooks: boolean | null = null;
|
||||
const promptPendingRequests = new Map<string, (value: string | null) => void>();
|
||||
|
||||
// Bridge observability counters. Kept in-memory and exposed via the
|
||||
// `plugins.getBridgeMetrics` IPC handler so devs / smoke tests / support
|
||||
@@ -67,9 +69,6 @@ export function getBridgeMetricsSnapshot() {
|
||||
};
|
||||
}
|
||||
|
||||
function getMainWindow() {
|
||||
return BrowserWindow.getAllWindows().find(w => !w.isDestroyed() && w.getTitle() === 'Insomnia');
|
||||
}
|
||||
|
||||
// Registered once so that persistent `ipcMain.on` handlers don't accumulate across window recreations.
|
||||
let ipcListenersRegistered = false;
|
||||
@@ -109,35 +108,7 @@ function ensureIpcListeners() {
|
||||
if (event.sender !== pluginWindow?.webContents) {
|
||||
return null;
|
||||
}
|
||||
const mainWindow = getMainWindow();
|
||||
if (!mainWindow) {
|
||||
return null;
|
||||
}
|
||||
const id = randomUUID();
|
||||
return new Promise<string | null>(resolve => {
|
||||
const timeout = setTimeout(() => {
|
||||
promptPendingRequests.delete(id);
|
||||
resolve(null);
|
||||
}, 60_000);
|
||||
promptPendingRequests.set(id, value => {
|
||||
clearTimeout(timeout);
|
||||
resolve(value);
|
||||
});
|
||||
mainWindow.webContents.send('plugins.uiPrompt', id, options);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('plugins.uiPromptResult', (event, { id, value }: { id: string; value: string | null }) => {
|
||||
const mainWindow = getMainWindow();
|
||||
if (!mainWindow || event.sender !== mainWindow.webContents) {
|
||||
return;
|
||||
}
|
||||
const resolve = promptPendingRequests.get(id);
|
||||
if (!resolve) {
|
||||
return;
|
||||
}
|
||||
promptPendingRequests.delete(id);
|
||||
resolve(value);
|
||||
return requestPromptFromRenderer(options as { title: string; label?: string; defaultValue?: string });
|
||||
});
|
||||
|
||||
ipcMain.on(
|
||||
|
||||
42
packages/insomnia/src/main/prompt-bridge.ts
Normal file
42
packages/insomnia/src/main/prompt-bridge.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
import { getMainWindow } from './window-utils';
|
||||
|
||||
const promptPendingRequests = new Map<string, (value: string | null) => void>();
|
||||
|
||||
export function requestPromptFromRenderer(options: {
|
||||
title: string;
|
||||
label?: string;
|
||||
defaultValue?: string;
|
||||
}): Promise<string | null> {
|
||||
const mainWindow = getMainWindow();
|
||||
if (!mainWindow) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
const id = randomUUID();
|
||||
return new Promise(resolve => {
|
||||
const timeout = setTimeout(() => {
|
||||
promptPendingRequests.delete(id);
|
||||
resolve(null);
|
||||
}, 60_000);
|
||||
promptPendingRequests.set(id, value => {
|
||||
clearTimeout(timeout);
|
||||
resolve(value);
|
||||
});
|
||||
mainWindow.webContents.send('ui.prompt', id, options);
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.on('ui.promptResult', (event, { id, value }: { id: string; value: string | null }) => {
|
||||
if (event.sender !== getMainWindow()?.webContents) {
|
||||
return;
|
||||
}
|
||||
const resolve = promptPendingRequests.get(id);
|
||||
if (!resolve) {
|
||||
return;
|
||||
}
|
||||
promptPendingRequests.delete(id);
|
||||
resolve(value);
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import type { BinaryToTextEncoding } from 'node:crypto';
|
||||
import crypto from 'node:crypto';
|
||||
import os from 'node:os';
|
||||
|
||||
import { shell } from 'electron';
|
||||
import { app, clipboard, dialog, shell } from 'electron';
|
||||
import iconv from 'iconv-lite';
|
||||
import type { AllTypes, CloudProviderCredential, Request as DBRequest, RequestGroup, Workspace } from 'insomnia-data';
|
||||
import { services } from 'insomnia-data';
|
||||
@@ -18,6 +18,7 @@ import { fetchRequestData, sendCurlAndWriteTimeline, tryToInterpolateRequest } f
|
||||
import { type Plugin, type TemplateTag } from '../plugins/types';
|
||||
import type { PluginTemplateTag, PluginTemplateTagContext, PluginToMainAPIPaths } from '../templating/types';
|
||||
import { curlRequest } from './network/libcurl-promise';
|
||||
import { requestPromptFromRenderer } from './prompt-bridge';
|
||||
import { secureReadFile } from './secure-read-file';
|
||||
|
||||
const bundlePluginModuleMap: Record<string, Plugin['module']> = {};
|
||||
@@ -308,4 +309,33 @@ const pluginToMainAPI: Record<PluginToMainAPIPaths, (...args: any[]) => Promise<
|
||||
}
|
||||
throw new Error(`Unsupported action named ${actionName} for plugin ${pluginName}`);
|
||||
},
|
||||
'app.alert': async (body: { title: string; message?: string }) => {
|
||||
await dialog.showMessageBox({ type: 'info', title: body.title, message: body.message || '' });
|
||||
},
|
||||
'app.dialog': async (body: { title: string; message?: string }) => {
|
||||
await dialog.showMessageBox({ type: 'info', title: body.title, message: body.message || '' });
|
||||
},
|
||||
'app.prompt': async (body: { title: string; options?: { label?: string; defaultValue?: string } }) => {
|
||||
return requestPromptFromRenderer({
|
||||
title: body.title,
|
||||
label: body.options?.label ?? body.title,
|
||||
defaultValue: body.options?.defaultValue ?? '',
|
||||
});
|
||||
},
|
||||
'app.getPath': async (body: { name: string }) => {
|
||||
return app.getPath(body.name as Parameters<typeof app.getPath>[0]);
|
||||
},
|
||||
'app.showSaveDialog': async (body: { options?: { defaultPath?: string } }) => {
|
||||
const result = await dialog.showSaveDialog(body.options ?? {});
|
||||
return result.canceled ? null : result.filePath;
|
||||
},
|
||||
'app.clipboard.readText': async () => {
|
||||
return clipboard.readText();
|
||||
},
|
||||
'app.clipboard.writeText': async (body: { text: string }) => {
|
||||
clipboard.writeText(body.text);
|
||||
},
|
||||
'app.clipboard.clear': async () => {
|
||||
clipboard.clear();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -32,6 +32,9 @@ const DEFAULT_HEIGHT = 720;
|
||||
const MINIMUM_WIDTH = 500;
|
||||
const MINIMUM_HEIGHT = 400;
|
||||
const browserWindows = new Map<'Insomnia' | 'HiddenBrowserWindow', ElectronBrowserWindow>();
|
||||
export function getMainWindow(): ElectronBrowserWindow | null {
|
||||
return browserWindows.get('Insomnia') ?? null;
|
||||
}
|
||||
let hiddenWindowIsBusy = false;
|
||||
interface Bounds {
|
||||
height?: number;
|
||||
|
||||
@@ -8,12 +8,12 @@ const isRenderer = process.type === 'renderer';
|
||||
|
||||
export const init = (renderPurpose: RenderPurpose = 'general'): { app: AppContext } => ({
|
||||
app: {
|
||||
alert: (title: string, message?: string) => {
|
||||
alert: async (title: string, message?: string) => {
|
||||
if (isRenderer) {
|
||||
return window.showAlert({ title, message });
|
||||
}
|
||||
},
|
||||
dialog: (title, body, options = {}) => {
|
||||
dialog: async (title, body, options = {}) => {
|
||||
if (isRenderer) {
|
||||
window.showWrapper({
|
||||
...options,
|
||||
@@ -41,7 +41,7 @@ export const init = (renderPurpose: RenderPurpose = 'general'): { app: AppContex
|
||||
});
|
||||
},
|
||||
|
||||
getPath: (name: string) => {
|
||||
getPath: async (name: string) => {
|
||||
invariant(name.toLowerCase() === 'desktop', `Unknown path name ${name}`);
|
||||
return window.app.getPath('desktop');
|
||||
},
|
||||
@@ -63,9 +63,9 @@ export const init = (renderPurpose: RenderPurpose = 'general'): { app: AppContex
|
||||
},
|
||||
|
||||
clipboard: {
|
||||
readText: () => window.clipboard.readText(),
|
||||
writeText: text => window.clipboard.writeText(text),
|
||||
clear: () => window.clipboard.clear(),
|
||||
readText: async () => window.clipboard.readText(),
|
||||
writeText: async (text: string) => window.clipboard.writeText(text),
|
||||
clear: async () => window.clipboard.clear(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -49,7 +49,6 @@ export const fetchFromTemplateWorkerDatabase = async (path: PluginToMainAPIPaths
|
||||
return result;
|
||||
};
|
||||
|
||||
const legacyModeErrorMessage = `This version improves the security around plugins by limiting scope of access by default. This may break some plugins which rely on having the same kind of access Insomnia does. You can still grant elevated access to plugins, should your workflow absolutely require it, by navigating to Preferences > Plugins and checking the box enabling elevated access for plugins.`;
|
||||
|
||||
function resolveArg(arg: ReturnType<typeof tokenizeArgs>[number], scope: Record<string, any>): any {
|
||||
if (arg.type === 'variable') {
|
||||
@@ -85,32 +84,24 @@ export function createLiquidTagWorker(
|
||||
|
||||
const helperContext: PluginTemplateTagContext = {
|
||||
app: {
|
||||
alert: () => {
|
||||
throw new Error(legacyModeErrorMessage);
|
||||
},
|
||||
dialog: () => {
|
||||
throw new Error(legacyModeErrorMessage);
|
||||
},
|
||||
prompt: () => {
|
||||
throw new Error(legacyModeErrorMessage);
|
||||
},
|
||||
getPath: () => {
|
||||
throw new Error(legacyModeErrorMessage);
|
||||
},
|
||||
alert: async (title: string, message?: string) =>
|
||||
fetchFromTemplateWorkerDatabase('app.alert', { title, message }),
|
||||
dialog: async (title: string) =>
|
||||
fetchFromTemplateWorkerDatabase('app.dialog', { title }),
|
||||
prompt: async (title: string, options?: { label?: string; defaultValue?: string; submitName?: string; inputType?: string }) =>
|
||||
fetchFromTemplateWorkerDatabase('app.prompt', { title, options }),
|
||||
getPath: async (name: string) =>
|
||||
fetchFromTemplateWorkerDatabase('app.getPath', { name }),
|
||||
getInfo: () => ({ version: packageJson.version, platform }),
|
||||
showSaveDialog: async () => {
|
||||
throw new Error(legacyModeErrorMessage);
|
||||
},
|
||||
showSaveDialog: async (options?: { defaultPath?: string }) =>
|
||||
fetchFromTemplateWorkerDatabase('app.showSaveDialog', { options }),
|
||||
clipboard: {
|
||||
readText: () => {
|
||||
throw new Error(legacyModeErrorMessage);
|
||||
},
|
||||
writeText: () => {
|
||||
throw new Error(legacyModeErrorMessage);
|
||||
},
|
||||
clear: () => {
|
||||
throw new Error(legacyModeErrorMessage);
|
||||
},
|
||||
readText: async () =>
|
||||
fetchFromTemplateWorkerDatabase('app.clipboard.readText', {}),
|
||||
writeText: async (text: string) =>
|
||||
fetchFromTemplateWorkerDatabase('app.clipboard.writeText', { text }),
|
||||
clear: async () =>
|
||||
fetchFromTemplateWorkerDatabase('app.clipboard.clear', {}),
|
||||
},
|
||||
},
|
||||
store: {
|
||||
|
||||
@@ -89,7 +89,15 @@ export type PluginToMainAPIPaths =
|
||||
| 'network.sendRequestWithoutSideEffects'
|
||||
| 'plugin.getBundlePluginTemplateTags'
|
||||
| 'plugin.executeBundlePluginTag'
|
||||
| 'plugin.executeBundlePluginMainAction';
|
||||
| 'plugin.executeBundlePluginMainAction'
|
||||
| 'app.alert'
|
||||
| 'app.dialog'
|
||||
| 'app.prompt'
|
||||
| 'app.getPath'
|
||||
| 'app.showSaveDialog'
|
||||
| 'app.clipboard.readText'
|
||||
| 'app.clipboard.writeText'
|
||||
| 'app.clipboard.clear';
|
||||
|
||||
export type RenderedRequest = Request & {
|
||||
cookies: {
|
||||
@@ -270,20 +278,20 @@ interface PromptModalOptions {
|
||||
}
|
||||
|
||||
export interface AppContext {
|
||||
alert: (title: string, message?: string) => void;
|
||||
alert: (title: string, message?: string) => Promise<void>;
|
||||
dialog: (
|
||||
title: string,
|
||||
body: HTMLElement,
|
||||
options?: { onHide?: () => void; tall?: boolean; skinny?: boolean; wide?: boolean },
|
||||
) => void;
|
||||
) => Promise<void>;
|
||||
prompt: (
|
||||
title: string,
|
||||
options?: Pick<PromptModalOptions, 'label' | 'defaultValue' | 'submitName' | 'inputType'>,
|
||||
) => Promise<string>;
|
||||
getPath: (name: string) => string;
|
||||
getPath: (name: string) => Promise<string>;
|
||||
getInfo: () => { version: string; platform: NodeJS.Platform };
|
||||
showSaveDialog: (options?: { defaultPath?: string }) => Promise<string | null>;
|
||||
clipboard: { readText(): string; writeText(text: string): void; clear(): void };
|
||||
clipboard: { readText(): Promise<string>; writeText(text: string): Promise<void>; clear(): Promise<void> };
|
||||
}
|
||||
export interface PluginTemplateTagContext {
|
||||
app: AppContext;
|
||||
|
||||
@@ -36,11 +36,11 @@ window.main.on('plugins.uiDialog', (_, options: Record<string, any>) => {
|
||||
window.showWrapper?.(options);
|
||||
});
|
||||
|
||||
window.main.on('plugins.uiPrompt', (_, id: string, options: Record<string, any>) => {
|
||||
window.main.on('ui.prompt', (_, id: string, options: Record<string, any>) => {
|
||||
window.showPrompt?.({
|
||||
...options,
|
||||
onComplete: (value: string) => {
|
||||
window.main.notifyPluginPromptResult(id, value);
|
||||
window.main.notifyPromptResult(id, value);
|
||||
},
|
||||
onHide: () => {},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user