mirror of
https://github.com/Kong/insomnia.git
synced 2026-08-04 11:52:33 -04:00
Split out high-risk runtime changes
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
@@ -47,6 +47,9 @@ jobs:
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Check renderer Node import baseline
|
||||
run: npm run check:renderer-node-imports
|
||||
|
||||
- name: Type checks
|
||||
run: npm run type-check
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"scripts": {
|
||||
"dev": "npm start -w insomnia",
|
||||
"dev:autoRestart": "npm run start:autoRestart -w insomnia",
|
||||
"check:renderer-node-imports": "npm run check:renderer-node-imports -w insomnia",
|
||||
"lint": "npm run lint --workspaces --if-present",
|
||||
"type-check": "npm run type-check --workspaces --if-present",
|
||||
"test": "npm run test --workspaces --if-present",
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
"verify-bundle-plugins": "esr --cache ./scripts/verify-bundle-plugins.ts",
|
||||
"install-x64-native-dependencies": "esr --cache ./scripts/install-x64-native-dependencies.ts",
|
||||
"build": "react-router build && esr --cache ./scripts/build.ts --noErrorTruncation",
|
||||
"analyze:renderer-node-imports": "cross-env NODE_OPTIONS=--max-old-space-size=8192 INSOMNIA_NODE_IMPORT_REPORT=1 react-router build",
|
||||
"check:renderer-node-imports": "npm run analyze:renderer-node-imports && esr --cache ./scripts/check-renderer-node-imports.ts",
|
||||
"update:renderer-node-import-baseline": "npm run analyze:renderer-node-imports && esr --cache ./scripts/check-renderer-node-imports.ts --write-baseline",
|
||||
"build:react-router": "react-router build",
|
||||
"generate:schema": "esr ./src/schema.ts",
|
||||
"build:electron-entrypoints": "cross-env NODE_ENV=development esr esbuild.entrypoints.ts",
|
||||
|
||||
@@ -26,7 +26,6 @@ import { registerInsomniaProtocols } from './main/api.protocol';
|
||||
import { backupIfNewerVersionAvailable } from './main/backup';
|
||||
import { registerSyncHandlers } from './main/cloud-sync/ipc';
|
||||
import { registerGitServiceAPI } from './main/git-service';
|
||||
import { registerCookieHandlers } from './main/ipc/cookies';
|
||||
import { ipcMainOn, ipcMainOnce, registerElectronHandlers } from './main/ipc/electron';
|
||||
import { registerElectronStorageHandlers } from './main/ipc/electron-storage';
|
||||
import { registergRPCHandlers } from './main/ipc/grpc';
|
||||
@@ -89,7 +88,6 @@ app.on('ready', async () => {
|
||||
// @TODO - Maybe move the register stuff in the registerMainHandlers function
|
||||
registerMainHandlers();
|
||||
registerPathHandlers();
|
||||
registerCookieHandlers();
|
||||
registergRPCHandlers();
|
||||
registerGitServiceAPI();
|
||||
registerLLMConfigServiceAPI();
|
||||
|
||||
@@ -3,9 +3,6 @@ import { ipcRenderer } from 'electron';
|
||||
// Provide window.app so plugin-loading code (which checks process.type === 'renderer')
|
||||
// can resolve the userData path without needing the main renderer's full preload.
|
||||
window.app = {
|
||||
env: Object.fromEntries(
|
||||
Object.entries(process.env).filter(([key, value]) => value !== undefined && key.startsWith('INSOMNIA_')),
|
||||
),
|
||||
getPath: (name: string) => ipcRenderer.sendSync('getPath', name) as string,
|
||||
getAppPath: () => ipcRenderer.sendSync('getAppPath') as string,
|
||||
process: { platform: process.platform as NodeJS.Platform },
|
||||
|
||||
@@ -9,7 +9,6 @@ import { servicesProxy } from '~/ui/renderer-services-proxy';
|
||||
|
||||
import type { SyncBridgeAPI } from './main/cloud-sync/ipc';
|
||||
import type { GitServiceAPI } from './main/git-service';
|
||||
import type { CookiesBridgeAPI } from './main/ipc/cookies';
|
||||
import type { electronStorageBridgeAPI } from './main/ipc/electron-storage';
|
||||
import type { gRPCBridgeAPI } from './main/ipc/grpc';
|
||||
import type { secretStorageBridgeAPI } from './main/ipc/secret-storage';
|
||||
@@ -124,17 +123,9 @@ const grpc: gRPCBridgeAPI = {
|
||||
closeAll: () => ipcRenderer.send('grpc.closeAll'),
|
||||
loadMethods: options => invokeWithNormalizedError('grpc.loadMethods', options),
|
||||
loadMethodsFromReflection: options => invokeWithNormalizedError('grpc.loadMethodsFromReflection', options),
|
||||
validateProtoFile: filePath => invokeWithNormalizedError('grpc.validateProtoFile', filePath),
|
||||
writeProtoFile: protoFileId => invokeWithNormalizedError('grpc.writeProtoFile', protoFileId),
|
||||
};
|
||||
|
||||
const cookies: CookiesBridgeAPI = {
|
||||
fromJSON: cookie => invokeWithNormalizedError('cookies.fromJSON', cookie),
|
||||
parse: cookie => invokeWithNormalizedError('cookies.parse', cookie),
|
||||
toString: cookie => invokeWithNormalizedError('cookies.toString', cookie),
|
||||
getCookiesForUrl: args => invokeWithNormalizedError('cookies.getCookiesForUrl', args),
|
||||
};
|
||||
|
||||
const secretStorage: secretStorageBridgeAPI = {
|
||||
setSecret: (key, secret) => invokeWithNormalizedError('secretStorage.setSecret', key, secret),
|
||||
getSecret: key => invokeWithNormalizedError('secretStorage.getSecret', key),
|
||||
@@ -261,30 +252,6 @@ const llm: LLMConfigServiceAPI = {
|
||||
invokeWithNormalizedError('llm.setAIFeatureEnabled', feature, enabled),
|
||||
};
|
||||
|
||||
const rendererProcessEnv = Object.fromEntries(
|
||||
Object.entries(process.env).filter(
|
||||
([key, value]) =>
|
||||
value !== undefined &&
|
||||
(key.startsWith('INSOMNIA_') ||
|
||||
key === 'BUILD_DATE' ||
|
||||
key === 'NODE_ENV' ||
|
||||
key === 'PLAYWRIGHT_TEST' ||
|
||||
key === 'PORTABLE_EXECUTABLE_DIR'),
|
||||
),
|
||||
);
|
||||
|
||||
const rendererProcess = {
|
||||
env: rendererProcessEnv,
|
||||
platform: process.platform as NodeJS.Platform,
|
||||
type: 'renderer' as const,
|
||||
versions: {
|
||||
chrome: process.versions.chrome,
|
||||
electron: process.versions.electron,
|
||||
node: process.versions.node,
|
||||
v8: process.versions.v8,
|
||||
},
|
||||
};
|
||||
|
||||
const main: Window['main'] = {
|
||||
startExecution: options => ipcRenderer.send('startExecution', options),
|
||||
addExecutionStep: options => ipcRenderer.send('addExecutionStep', options),
|
||||
@@ -338,7 +305,6 @@ const main: Window['main'] = {
|
||||
webSocket,
|
||||
socketIO,
|
||||
mcp,
|
||||
cookies,
|
||||
git,
|
||||
llm,
|
||||
grpc,
|
||||
@@ -447,7 +413,6 @@ const dialog: Window['dialog'] = {
|
||||
showSaveDialog: options => invokeWithNormalizedError('showSaveDialog', options),
|
||||
};
|
||||
const app: Window['app'] = {
|
||||
env: rendererProcessEnv,
|
||||
getPath: options => ipcRenderer.sendSync('getPath', options),
|
||||
getAppPath: () => ipcRenderer.sendSync('getAppPath'),
|
||||
process: {
|
||||
@@ -474,8 +439,6 @@ const database: Window['database'] = {
|
||||
|
||||
if (process.contextIsolated) {
|
||||
contextBridge.exposeInMainWorld('main', main);
|
||||
contextBridge.exposeInMainWorld('process', rendererProcess);
|
||||
contextBridge.exposeInMainWorld('global', globalThis);
|
||||
contextBridge.exposeInMainWorld('dialog', dialog);
|
||||
contextBridge.exposeInMainWorld('app', app);
|
||||
contextBridge.exposeInMainWorld('shell', shell);
|
||||
@@ -486,14 +449,6 @@ if (process.contextIsolated) {
|
||||
contextBridge.exposeInMainWorld('_dataServices', servicesProxy);
|
||||
} else {
|
||||
window.main = main;
|
||||
Object.defineProperty(window, 'process', {
|
||||
configurable: true,
|
||||
value: rendererProcess,
|
||||
});
|
||||
Object.defineProperty(window, 'global', {
|
||||
configurable: true,
|
||||
value: window,
|
||||
});
|
||||
window.dialog = dialog;
|
||||
window.app = app;
|
||||
window.shell = shell;
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { Cookie as ToughCookie, CookieJar } from 'tough-cookie';
|
||||
|
||||
import type { Cookie } from '~/insomnia-data';
|
||||
|
||||
import { ipcMainHandle } from './electron';
|
||||
|
||||
type CookieInput = Cookie | string;
|
||||
|
||||
const parseCookieFromJSON = (cookie: CookieInput) => {
|
||||
return typeof cookie === 'string' ? ToughCookie.fromJSON(cookie) : ToughCookie.fromJSON(cookie);
|
||||
};
|
||||
|
||||
const cookieToString = (cookie: CookieInput) => {
|
||||
const parsedCookie = parseCookieFromJSON(cookie);
|
||||
|
||||
if (parsedCookie === null) {
|
||||
throw new Error(`Unable to read cookie: ${cookie}`);
|
||||
}
|
||||
|
||||
let value = parsedCookie.toString();
|
||||
|
||||
if (parsedCookie.domain && parsedCookie.hostOnly) {
|
||||
value += `; Domain=${parsedCookie.domain}`;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const getCookiesForUrl = (cookies: Cookie[], url: string): Cookie[] => {
|
||||
try {
|
||||
const sanitized = cookies.map(c => ({
|
||||
...c,
|
||||
expires: c.expires === null || c.expires === undefined ? 'Infinity' : c.expires,
|
||||
}));
|
||||
const jar = CookieJar.fromJSON(JSON.stringify({ cookies: sanitized }));
|
||||
jar.rejectPublicSuffixes = false;
|
||||
jar.looseMode = true;
|
||||
return jar.getCookiesSync(url).map(c => c.toJSON() as Cookie);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export interface CookiesBridgeAPI {
|
||||
fromJSON: (cookie: CookieInput) => Promise<Cookie | null>;
|
||||
parse: (cookie: string) => Promise<Cookie | null>;
|
||||
toString: (cookie: CookieInput) => Promise<string>;
|
||||
getCookiesForUrl: (args: { cookies: Cookie[]; url: string }) => Promise<Cookie[]>;
|
||||
}
|
||||
|
||||
export function registerCookieHandlers() {
|
||||
ipcMainHandle('cookies.fromJSON', (_, cookie: CookieInput) => {
|
||||
return parseCookieFromJSON(cookie)?.toJSON() as Cookie | null;
|
||||
});
|
||||
ipcMainHandle('cookies.parse', (_, cookie: string) => {
|
||||
return ToughCookie.parse(cookie, { loose: true })?.toJSON() as Cookie | null;
|
||||
});
|
||||
ipcMainHandle('cookies.toString', (_, cookie: CookieInput) => {
|
||||
return cookieToString(cookie);
|
||||
});
|
||||
ipcMainHandle('cookies.getCookiesForUrl', (_, { cookies, url }: { cookies: Cookie[]; url: string }) => {
|
||||
return getCookiesForUrl(cookies, url);
|
||||
});
|
||||
}
|
||||
@@ -26,10 +26,6 @@ export type HandleChannels =
|
||||
| 'curl.event.findMany'
|
||||
| 'curl.open'
|
||||
| 'curl.readyState'
|
||||
| 'cookies.fromJSON'
|
||||
| 'cookies.getCookiesForUrl'
|
||||
| 'cookies.parse'
|
||||
| 'cookies.toString'
|
||||
| 'createPlugin'
|
||||
| 'curlRequest'
|
||||
| 'database.caCertificate.create'
|
||||
@@ -84,7 +80,6 @@ export type HandleChannels =
|
||||
| 'git.getGitProviderEmails'
|
||||
| 'grpc.loadMethods'
|
||||
| 'grpc.loadMethodsFromReflection'
|
||||
| 'grpc.validateProtoFile'
|
||||
| 'grpc.writeProtoFile'
|
||||
| 'initializeWorkspaceBackendProject'
|
||||
| 'insecureReadFile'
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
FileDescriptorSet as ProtobufEsFileDescriptorSet,
|
||||
MethodIdempotency,
|
||||
@@ -62,7 +60,6 @@ export interface gRPCBridgeAPI {
|
||||
cancel: typeof cancel;
|
||||
loadMethods: typeof loadMethods;
|
||||
loadMethodsFromReflection: typeof loadMethodsFromReflection;
|
||||
validateProtoFile: (filePath: string) => Promise<void>;
|
||||
closeAll: typeof closeAll;
|
||||
writeProtoFile: (protoFileId: string) => Promise<{ filePath: string; dirs: string[] }>;
|
||||
}
|
||||
@@ -94,17 +91,9 @@ export function registergRPCHandlers() {
|
||||
ipcMainOn('grpc.closeAll', closeAll);
|
||||
ipcMainHandle('grpc.loadMethods', (_, requestId) => loadMethods(requestId));
|
||||
ipcMainHandle('grpc.loadMethodsFromReflection', (_, requestId) => loadMethodsFromReflection(requestId));
|
||||
ipcMainHandle('grpc.validateProtoFile', (_, filePath: string) => validateProtoFile(filePath));
|
||||
ipcMainHandle('grpc.writeProtoFile', (_, protoFileId: string) => writeProtoFileById(protoFileId));
|
||||
}
|
||||
|
||||
const validateProtoFile = async (filePath: string): Promise<void> => {
|
||||
await protoLoader.load(filePath, {
|
||||
...grpcOptions,
|
||||
includeDirs: [path.dirname(filePath)],
|
||||
});
|
||||
};
|
||||
|
||||
const loadMethodsFromFilePath = async (filePath: string, includeDirs: string[]): Promise<MethodDefs[]> => {
|
||||
const definition = await protoLoader.load(filePath, {
|
||||
...grpcOptions,
|
||||
|
||||
@@ -67,7 +67,6 @@ import {
|
||||
import type { SocketIOBridgeAPI } from '../network/socket-io';
|
||||
import type { WebSocketBridgeAPI } from '../network/websocket';
|
||||
import { registerPluginIpcHandlers } from '../plugin-window';
|
||||
import type { CookiesBridgeAPI } from './cookies';
|
||||
import { ipcMainHandle, ipcMainOn, type RendererOnChannels } from './electron';
|
||||
import type { electronStorageBridgeAPI } from './electron-storage';
|
||||
import extractPostmanDataDumpHandler from './extract-postman-data-dump';
|
||||
@@ -227,7 +226,6 @@ export interface RendererToMainBridgeAPI {
|
||||
webSocket: WebSocketBridgeAPI;
|
||||
socketIO: SocketIOBridgeAPI;
|
||||
mcp: McpBridgeAPI;
|
||||
cookies: CookiesBridgeAPI;
|
||||
grpc: gRPCBridgeAPI;
|
||||
curl: CurlBridgeAPI;
|
||||
git: GitServiceAPI;
|
||||
|
||||
@@ -201,7 +201,7 @@ export function createWindow(): ElectronBrowserWindow {
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'entry.preload.min.js'),
|
||||
zoomFactor: getZoomFactor(),
|
||||
nodeIntegration: false,
|
||||
nodeIntegration: true,
|
||||
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
|
||||
|
||||
@@ -4,7 +4,6 @@ import nodePath from 'node:path';
|
||||
import clone from 'clone';
|
||||
|
||||
import type { RequestHeader } from '~/insomnia-data';
|
||||
import { services } from '~/insomnia-data';
|
||||
import type { RenderedRequest } from '~/templating/types';
|
||||
|
||||
import type { RequestContext } from '../../../insomnia-scripting-environment/src/objects';
|
||||
@@ -48,23 +47,10 @@ export const runScript = (options: {
|
||||
context: RequestContext;
|
||||
}): Promise<RequestContext | { error: string }> => executeScript(options);
|
||||
|
||||
async function canRunPluginHooksInNodeAdapter() {
|
||||
if (!process.versions?.electron || typeof global.require !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const settings = await services.settings.get();
|
||||
return Boolean(settings);
|
||||
}
|
||||
|
||||
export async function applyRequestHooks(
|
||||
newRenderedRequest: RenderedRequest,
|
||||
renderedContext: Record<string, any>,
|
||||
): Promise<RenderedRequest> {
|
||||
if (!await canRunPluginHooksInNodeAdapter()) {
|
||||
return newRenderedRequest;
|
||||
}
|
||||
|
||||
const pluginIndex = require('../plugins/index');
|
||||
for (const { plugin, hook } of await pluginIndex.getRequestHooks()) {
|
||||
const context = {
|
||||
@@ -90,10 +76,6 @@ export async function applyResponseHooks(
|
||||
renderedRequest: RenderedRequest,
|
||||
renderedContext: Record<string, any>,
|
||||
): Promise<ResponsePatch> {
|
||||
if (!await canRunPluginHooksInNodeAdapter()) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const newResponse = clone(response);
|
||||
const newRequest = clone(renderedRequest);
|
||||
const pluginIndex = require('../plugins/index');
|
||||
|
||||
@@ -31,10 +31,6 @@ export async function applyRequestHooks(
|
||||
newRenderedRequest: RenderedRequest,
|
||||
renderedContext: Record<string, any>,
|
||||
): Promise<RenderedRequest> {
|
||||
if (!globalThis.window?.main?.plugins) {
|
||||
return newRenderedRequest;
|
||||
}
|
||||
|
||||
if (!await pluginsBridge.hasRequestHooks()) {
|
||||
return newRenderedRequest;
|
||||
}
|
||||
@@ -50,10 +46,6 @@ export async function applyResponseHooks(
|
||||
renderedRequest: RenderedRequest,
|
||||
renderedContext: Record<string, any>,
|
||||
): Promise<ResponsePatch> {
|
||||
if (!globalThis.window?.main?.plugins) {
|
||||
return response;
|
||||
}
|
||||
|
||||
if (!await pluginsBridge.hasResponseHooks()) {
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,7 @@ import type * as AdapterType from './network-adapter.renderer';
|
||||
|
||||
|
||||
const impl = (
|
||||
(process as any).type === 'renderer' &&
|
||||
globalThis.window !== undefined &&
|
||||
globalThis.window.main !== undefined &&
|
||||
globalThis.window.main.plugins !== undefined
|
||||
(process as any).type === 'renderer'
|
||||
? require('./network-adapter.renderer')
|
||||
: require(/* @vite-ignore */ './network-adapter.node')
|
||||
) as typeof AdapterType;
|
||||
|
||||
@@ -3,10 +3,8 @@ import Color from 'color';
|
||||
import type { ThemeSettings } from '~/insomnia-data';
|
||||
import { getAppDefaultTheme } from '~/insomnia-data/common';
|
||||
|
||||
import type { SerializableTheme } from './bridge-types';
|
||||
import { plugins } from './renderer-bridge';
|
||||
|
||||
export type ColorScheme = 'default' | 'light' | 'dark';
|
||||
import type { Theme } from './index';
|
||||
import { type ColorScheme, getThemes } from './index';
|
||||
|
||||
export type HexColor = `#${string}`;
|
||||
export type RGBColor = `rgb(${string})`;
|
||||
@@ -333,7 +331,7 @@ export async function setTheme(themeName: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
const themes: SerializableTheme[] = await plugins.getThemes();
|
||||
const themes: Theme[] = await getThemes();
|
||||
let selectedTheme = themes.find(t => t.theme.name === themeName);
|
||||
|
||||
if (!selectedTheme) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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).
|
||||
@@ -6,17 +7,15 @@ import type { PluginBridgeMetrics, PluginsBridgeAPI } from './bridge-types';
|
||||
// plugin-system deps it pulls in don't inflate the preload.
|
||||
const bridgeEnabled = process.env.INSOMNIA_ENABLE_PLUGIN_BRIDGE !== 'false';
|
||||
|
||||
async function call<M extends keyof Omit<PluginsBridgeAPI, 'getBridgeMetrics'>>(
|
||||
function call<M extends keyof Omit<PluginsBridgeAPI, 'getBridgeMetrics'>>(
|
||||
method: M,
|
||||
args?: Parameters<PluginsBridgeAPI[M]>[0],
|
||||
): Promise<Awaited<ReturnType<PluginsBridgeAPI[M]>>> {
|
||||
): ReturnType<PluginsBridgeAPI[M]> {
|
||||
if (bridgeEnabled) {
|
||||
const fn = (window.main.plugins[method] as (...a: any[]) => any);
|
||||
return fn(args) as Promise<Awaited<ReturnType<PluginsBridgeAPI[M]>>>;
|
||||
return fn(args) as ReturnType<PluginsBridgeAPI[M]>;
|
||||
}
|
||||
|
||||
const { invokePluginMethod } = await import('./invoke-method');
|
||||
return invokePluginMethod(method as any, args) as Promise<Awaited<ReturnType<PluginsBridgeAPI[M]>>>;
|
||||
return invokePluginMethod(method as any, args) as ReturnType<PluginsBridgeAPI[M]>;
|
||||
}
|
||||
|
||||
const emptyBridgeMetrics: PluginBridgeMetrics = {
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
import type { Environment } from 'nunjucks';
|
||||
import nunjucks from 'nunjucks';
|
||||
|
||||
import { localTemplateTags } from '~/templating/local-template-tags';
|
||||
|
||||
import type { TemplateTag } from '../plugins';
|
||||
import BaseExtension from './base-extension';
|
||||
import { extractUndefinedVariableKey, RenderError } from './render-error';
|
||||
|
||||
// Some constants
|
||||
export const NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME = '_';
|
||||
|
||||
type NunjucksEnvironment = Environment & {
|
||||
extensions: Record<string, any>;
|
||||
};
|
||||
|
||||
// Cached globals
|
||||
let nunjucksAll: NunjucksEnvironment | null = null;
|
||||
|
||||
/**
|
||||
* Render text based on stuff
|
||||
* @param {String} text - Nunjucks template in text form
|
||||
* @param {Object} [config] - Config options for rendering
|
||||
* @param {Object} [config.context] - Context to render with
|
||||
* @param {Object} [config.path] - Path to include in the error message
|
||||
*/
|
||||
export function render(
|
||||
text: string,
|
||||
config: {
|
||||
context?: Record<string, any>;
|
||||
path?: string;
|
||||
ignoreUndefinedEnvVariable?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
const hasNunjucksInterpolationSymbols = text.includes('{{') && text.includes('}}');
|
||||
const hasNunjucksCustomTagSymbols = text.includes('{%') && text.includes('%}');
|
||||
const hasNunjucksCommentSymbols = text.includes('{#') && text.includes('#}');
|
||||
if (!hasNunjucksInterpolationSymbols && !hasNunjucksCustomTagSymbols && !hasNunjucksCommentSymbols) {
|
||||
return text;
|
||||
}
|
||||
const context = config.context || {};
|
||||
// context needs to exist on the root for the old templating syntax, and in _ for the new templating syntax
|
||||
// old: {{ arr[0].prop }}
|
||||
// new: {{ _['arr-name-with-dash'][0].prop }}
|
||||
const templatingContext = { ...context, [NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME]: context };
|
||||
const path = config.path || null;
|
||||
return new Promise<string | null>(async (resolve, reject) => {
|
||||
// NOTE: this is added as a breadcrumb because renderString sometimes hangs
|
||||
const id = setTimeout(() => console.log('[templating] Warning: nunjucks failed to respond within 5 seconds'), 5000);
|
||||
const nj = await getNunjucks(config.ignoreUndefinedEnvVariable);
|
||||
nj?.renderString(text, templatingContext, (err: Error | null, result: any) => {
|
||||
clearTimeout(id);
|
||||
if (!err) {
|
||||
return resolve(result);
|
||||
}
|
||||
console.warn('[templating] Error rendering template', err);
|
||||
const sanitizedMsg = err.message
|
||||
.replace(/\(unknown path\)\s/, '')
|
||||
.replace(/\[Line \d+, Column \d*]/, '')
|
||||
.replace(/^\s*Error:\s*/, '')
|
||||
.trim();
|
||||
const location = err.message.match(/\[Line (\d+), Column (\d+)*]/);
|
||||
const line = location ? Number.parseInt(location[1]) : 1;
|
||||
const column = location ? Number.parseInt(location[2]) : 1;
|
||||
const reason = err.message.includes('attempted to output null or undefined value') ? 'undefined' : 'error';
|
||||
const newError = new RenderError(sanitizedMsg);
|
||||
newError.path = path || '';
|
||||
newError.message = sanitizedMsg;
|
||||
newError.location = {
|
||||
line,
|
||||
column,
|
||||
};
|
||||
newError.type = 'render';
|
||||
newError.reason = reason;
|
||||
// regard as environment variable missing
|
||||
if (hasNunjucksInterpolationSymbols && reason === 'undefined') {
|
||||
newError.extraInfo = {
|
||||
subType: 'environmentVariable',
|
||||
undefinedEnvironmentVariables: extractUndefinedVariableKey(text, templatingContext),
|
||||
};
|
||||
}
|
||||
reject(newError);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload Nunjucks environments. Useful for if plugins change.
|
||||
*/
|
||||
export function reload() {
|
||||
nunjucksAll = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get definitions of template tags
|
||||
*/
|
||||
export async function getTagDefinitions() {
|
||||
const env = await getNunjucks();
|
||||
|
||||
return Object.keys(env.extensions)
|
||||
.map(k => env.extensions[k])
|
||||
.filter(ext => !ext.isDeprecated())
|
||||
.sort((a, b) => (a.getPriority() > b.getPriority() ? 1 : -1))
|
||||
.map(ext => ({
|
||||
name: ext.getTag() || '',
|
||||
displayName: ext.getName() || '',
|
||||
liveDisplayName: ext.getLiveDisplayName(),
|
||||
description: ext.getDescription(),
|
||||
disablePreview: ext.getDisablePreview(),
|
||||
args: ext.getArgs(),
|
||||
actions: ext.getActions(),
|
||||
}));
|
||||
}
|
||||
|
||||
async function getNunjucks(ignoreUndefinedEnvVariable?: boolean): Promise<NunjucksEnvironment> {
|
||||
let throwOnUndefined = true;
|
||||
if (ignoreUndefinedEnvVariable) {
|
||||
throwOnUndefined = false;
|
||||
} else if (nunjucksAll) {
|
||||
return nunjucksAll;
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~ //
|
||||
// Setup Config //
|
||||
// ~~~~~~~~~~~~ //
|
||||
const config = {
|
||||
autoescape: false,
|
||||
// Don't escape HTML
|
||||
throwOnUndefined,
|
||||
// Strict mode
|
||||
tags: {
|
||||
blockStart: '{%',
|
||||
blockEnd: '%}',
|
||||
variableStart: '{{',
|
||||
variableEnd: '}}',
|
||||
commentStart: '{#',
|
||||
commentEnd: '#}',
|
||||
},
|
||||
};
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// Create Env with Extensions //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
const nunjucksEnvironment = nunjucks.configure(config) as NunjucksEnvironment;
|
||||
nunjucksEnvironment.addGlobal('range', () => {});
|
||||
nunjucksEnvironment.addGlobal('cycler', () => {});
|
||||
nunjucksEnvironment.addGlobal('joiner', () => {});
|
||||
const pluginTemplateTags: TemplateTag[] = [];
|
||||
|
||||
const allExtensions = [
|
||||
...localTemplateTags,
|
||||
|
||||
// Spread after local tags to allow plugins to override them.
|
||||
// TODO: Determine if this is in fact the behavior we've explicitly decided to support.
|
||||
...pluginTemplateTags,
|
||||
];
|
||||
|
||||
for (const extension of allExtensions) {
|
||||
const { templateTag, plugin } = extension;
|
||||
templateTag.priority = templateTag.priority || allExtensions.indexOf(extension);
|
||||
const instance = new BaseExtension(templateTag, plugin);
|
||||
nunjucksEnvironment.addExtension(instance.getTag() || '', instance);
|
||||
// Hidden helper filter to debug complicated things
|
||||
// eg. `{{ foo | urlencode | debug | upper }}`
|
||||
nunjucksEnvironment.addFilter('debug', (o: any) => o);
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~ //
|
||||
// Cache Env and Return (when ignoreUndefinedEnvVariable is false) //
|
||||
// ~~~~~~~~~~~~~~~~~~~~ //
|
||||
if (ignoreUndefinedEnvVariable) {
|
||||
return nunjucksEnvironment;
|
||||
}
|
||||
|
||||
nunjucksAll = nunjucksEnvironment;
|
||||
|
||||
return nunjucksEnvironment;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { EditorFromTextArea, MarkerRange } from 'codemirror';
|
||||
|
||||
import { models, services } from '~/insomnia-data';
|
||||
import { decryptSecretValue } from '~/utils/vault';
|
||||
|
||||
import type { NunjucksParsedTag, NunjucksParsedTagArg, RenderPurpose } from '../templating/types';
|
||||
import { decryptVaultKeyFromSession } from '../utils/vault';
|
||||
@@ -156,7 +157,6 @@ export async function maskOrDecryptVaultDataIfNecessary(vaultEnvironmentData: an
|
||||
const { vaultKey, vaultSalt } = await services.userSession.get();
|
||||
const isVaultEnabled = !!vaultSalt;
|
||||
if (isVaultEnabled && vaultKey) {
|
||||
const { decryptSecretValue } = await import('~/utils/vault-crypto');
|
||||
const symmetricKey = (await decryptVaultKeyFromSession(vaultKey, true)) as JsonWebKey;
|
||||
// decrypt all secret values under vaultEnvironmentPath property in context
|
||||
Object.keys(vaultEnvironmentData).forEach(vaultContextKey => {
|
||||
|
||||
6
packages/insomnia/types/global.d.ts
vendored
6
packages/insomnia/types/global.d.ts
vendored
@@ -10,14 +10,10 @@ declare global {
|
||||
main: RendererToMainBridgeAPI;
|
||||
bridge: HiddenBrowserWindowToMainBridgeAPI;
|
||||
database: DatabaseBridgeAPI;
|
||||
global: typeof globalThis;
|
||||
// This is a temporary measure to provide access to services on the global window object. It will be removed in the future once all usages are updated to import services directly from the insomnia-data package.
|
||||
_dataServices?: Services;
|
||||
dialog: Pick<Electron.Dialog, 'showOpenDialog' | 'showSaveDialog'>;
|
||||
app: Pick<Electron.App, 'getPath' | 'getAppPath'> & {
|
||||
env: Record<string, string | undefined>;
|
||||
process: { platform: NodeJS.Platform };
|
||||
};
|
||||
app: Pick<Electron.App, 'getPath' | 'getAppPath'> & { process: { platform: NodeJS.Platform } };
|
||||
shell: Pick<Electron.Shell, 'showItemInFolder' | 'openPath'>;
|
||||
clipboard: Pick<Electron.Clipboard, 'readText' | 'writeText' | 'clear'>;
|
||||
webUtils: Pick<Electron.WebUtils, 'getPathForFile'>;
|
||||
|
||||
Reference in New Issue
Block a user