Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions examples/insomnia-plugin-sandbox-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# insomnia-plugin-sandbox-demo

Manual test fixture for the QuickJS template-tag sandbox (Milestone 2).

The `sandboxprobe` tag reports **where** it executed and exercises an async host bridge:

- Sandbox flag **off** → `hello | ran in: main-process | arch via bridge: <arch>`
- Sandbox flag **on** → `hello | ran in: sandbox | arch via bridge: <arch>`

`ran in` flips because Node's `process` global exists in the legacy main-process path but is
absent inside QuickJS. `arch via bridge` proves `context.util.nodeOS()` round-tripped through
`__hostBridge` → `pluginToMainAPI['nodeOS']` and back.

## Install (dev)

1. Run the app: `npm run dev` (repo root).
2. Preferences → Plugins → **Reveal Plugins Folder**.
3. Copy this `insomnia-plugin-sandbox-demo` folder into that directory.
4. Click **Reload Plugins**.
5. Preferences → Scripting → toggle **Run template tags in sandbox (experimental)**.
6. In a request URL/header, insert the `Sandbox Probe` template tag (or type `{% sandboxprobe 'hi' %}`),
and watch the preview change as you toggle the flag.

Note: this cut's `require` shim only supports `path` and `crypto`. A plugin that `require()`s
an npm package, another builtin, or a relative file will throw a clear
`Cannot find module ... in sandbox` — broader coverage is follow-up work.
28 changes: 28 additions & 0 deletions examples/insomnia-plugin-sandbox-demo/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// eslint-disable-next-line no-undef
module.exports.templateTags = [
{
name: 'sandboxprobe',
displayName: 'Sandbox Probe',
description: 'Reports whether it executed inside the QuickJS sandbox, and exercises an async bridge',
args: [
{ displayName: 'Label', type: 'string', defaultValue: 'hello' },
],
async run(context, label = 'hello') {
// In the QuickJS sandbox, Node globals like `process` are absent; in the legacy main-process
// path they exist. This makes the chosen execution path directly observable in the output.
const ranIn = typeof process === 'undefined' ? 'sandbox' : 'main-process';

// Exercise an async host bridge — proves __hostBridge + the executePendingJobs driver loop
// round-trip work end-to-end (context.util.nodeOS -> pluginToMainAPI['nodeOS']).
let arch = 'n/a';
try {
const os = await context.util.nodeOS();
arch = os.arch;
} catch (err) {
arch = 'bridge-error:' + err.message;
}

return `${label} | ran in: ${ranIn} | arch via bridge: ${arch}`;
},
},
];
10 changes: 10 additions & 0 deletions examples/insomnia-plugin-sandbox-demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "insomnia-plugin-sandbox-demo",
"version": "1.0.0",
"description": "Demo plugin to manually verify the QuickJS template-tag sandbox path",
"main": "index.js",
"insomnia": {
"name": "sandbox-demo",
"description": "Demo plugin to manually verify the QuickJS template-tag sandbox path"
}
}
68 changes: 68 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/insomnia-data/common-src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ export interface Settings {
scriptSandboxEnabled: boolean;
// Wraps the user script in 'use strict', preventing accidental globals and making `this` undefined.
scriptStrictModeEnabled: boolean;
// Experimental: execute plugin template tags inside the QuickJS-WASM sandbox instead of directly in the main process.
templateTagSandboxEnabled: boolean;
// Names of security rules that have been individually disabled.
disabledSecurityRules: string[];
// AST blocked-property names that have been individually disabled.
Expand Down
1 change: 1 addition & 0 deletions packages/insomnia-data/src/models/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export function init(): BaseSettings {
dataFolders: [],
scriptSandboxEnabled: true,
scriptStrictModeEnabled: true,
templateTagSandboxEnabled: false,
disabledSecurityRules: [],
disabledBlockedProperties: [],
disabledBlockedRoots: [],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
type: collection.insomnia.rest/5.0
schema_version: "5.1"
name: Sandbox Probe Collection
meta:
id: wrk_5a4db0c5000000000000000000000001
created: 1751800000000
modified: 1751800000000
collection:
- url: http://127.0.0.1:4010/echo
name: Sandbox Probe
meta:
id: req_5a4db0c5000000000000000000000002
created: 1751800000001
modified: 1751800000001
isPrivate: false
sortKey: -1751800000001
method: GET
body:
mimeType: text/plain
text: |
{% sandboxprobe 'e2e' %}
{% cryptoparity 'insomnia-test' %}
headers:
- name: Content-Type
value: text/plain
129 changes: 129 additions & 0 deletions packages/insomnia-smoke-test/tests/smoke/sandbox-template-tags.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';

import { expect } from '@playwright/test';

import { loadFixture } from '../../playwright/paths';
import { test } from '../../playwright/test';

const PLUGIN_NAME = 'insomnia-plugin-sandbox-probe';

// Write a probe plugin into the data-path plugins directory (same pattern as plugin-bridge.test.ts).
// - sandboxprobe: reports which execution path ran it (Node's `process` global exists in the legacy
// main-process path but is absent inside QuickJS) and exercises an async host-bridge round-trip.
// - cryptoparity: a deterministic require('crypto') workload whose output must be byte-identical
// between the legacy path and the sandbox path.
const installProbePlugin = (dataPath: string) => {
const pluginDir = path.join(dataPath, 'plugins', PLUGIN_NAME);
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(
path.join(pluginDir, 'package.json'),
// The 'insomnia' key is required — the loader skips packages that lack it.
JSON.stringify({ name: PLUGIN_NAME, version: '1.0.0', main: 'index.js', insomnia: {} }),
);
fs.writeFileSync(
path.join(pluginDir, 'index.js'),
`
module.exports.templateTags = [
{
name: 'sandboxprobe',
displayName: 'Sandbox Probe',
description: 'Reports whether it executed inside the QuickJS sandbox',
args: [{ displayName: 'Label', type: 'string', defaultValue: 'hello' }],
async run(context, label = 'hello') {
const ranIn = typeof process === 'undefined' ? 'sandbox' : 'main-process';
let arch = 'n/a';
try {
const hostOS = await context.util.nodeOS();
arch = hostOS.arch;
} catch (err) {
arch = 'bridge-error:' + err.message;
}
return label + ' | ran in: ' + ranIn + ' | arch via bridge: ' + arch;
},
},
{
name: 'cryptoparity',
displayName: 'Crypto Parity',
description: 'Deterministic crypto workload for flag-on/off parity',
args: [{ displayName: 'Input', type: 'string', defaultValue: 'insomnia-test' }],
async run(context, input = 'insomnia-test') {
return require('crypto').createHash('sha256').update(String(input)).digest('hex');
},
},
];
`,
);
};

test('Template tag sandbox: flag routes plugin tag execution into the QuickJS sandbox', async ({
page,
app,
dataPath,
insomnia,
}) => {
installProbePlugin(dataPath);

// The persistent "Plugin system updated" notification fires from a Root mount effect whenever
// user plugins exist on disk, and its toast region intercepts pointer events over the page.
// Seed its once-only guard so route remounts can't re-raise it, then clear any toast already
// in flight before driving the UI.
await page.getByLabel('Import').waitFor();
await page.evaluate(() => localStorage.setItem('plugin-system-changes-toast-shown', 'true'));
const dismissButtons = page.getByRole('button', { name: 'Dismiss' });
await dismissButtons
.first()
.waitFor({ timeout: 3000 })
.catch(() => {});
while ((await dismissButtons.count()) > 0) {
await dismissButtons.first().click();
}

// Import a collection whose request body contains the probe tags (the tags render as pills
// lexically, so importing before the plugin is registered is fine).
const fixture = await loadFixture('sandbox-probe-collection.yaml');
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), fixture);
await page.getByLabel('Import').click();
await page.locator('[data-test-id="import-from-clipboard"]').click();
await page.getByRole('button', { name: 'Scan' }).click();
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();

// Register the probe plugin through the bridge.
await page.evaluate(() => (window as any).main.plugins.reloadPlugins());

await insomnia.navigationSidebar.clickRequestOrFolder('Sandbox Probe');
await page.getByText('Body', { exact: true }).click();

// Open a tag pill in the body editor and assert its Live Preview output (auto-retries until the
// render completes), then close the modal.
const assertTagPreview = async (tagPrefix: string, expected: string) => {
await page.locator(`[data-template^="${tagPrefix}"]`).click();
const modal = page.getByRole('dialog');
await expect.soft(modal.getByLabel('Live Preview')).toContainText(expected);
await modal.getByRole('button', { name: 'Done' }).click();
};

const expectedHash = crypto.createHash('sha256').update('insomnia-test').digest('hex');

// Flag off (default): tags run on the legacy main-process path.
await assertTagPreview('{% sandboxprobe', 'e2e | ran in: main-process');
await assertTagPreview('{% cryptoparity', expectedHash);

// Toggle the sandbox on: Preferences → Scripting → "Run template tags in sandbox".
await page.getByTestId('settings-button').click();
await page.getByRole('tab', { name: 'Scripting' }).click();
const sandboxToggle = page.getByTestId('toggle-template-tag-sandbox');
await sandboxToggle.click();
await expect.soft(sandboxToggle.getByRole('switch')).toBeChecked();
await page.locator('.app').press('Escape');

// Canary: the same tag now reports sandbox execution, and the async host bridge still round-trips.
// Derive the expected arch from the Electron main process (where pluginToMainAPI runs) rather
// than the Playwright runner, which can differ in cross-arch setups.
const electronArch = await app.evaluate(() => process.arch);
await assertTagPreview('{% sandboxprobe', `e2e | ran in: sandbox | arch via bridge: ${electronArch}`);

// Parity: the sandboxed require('crypto') workload is byte-identical to the legacy render above.
await assertTagPreview('{% cryptoparity', expectedHash);
});
5 changes: 5 additions & 0 deletions packages/insomnia/esbuild.entrypoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ export default async function build(options: Options) {
external: [
'electron',
'@getinsomnia/node-libcurl',
// QuickJS loads its .wasm relative to its own __dirname; bundling it breaks that path
// resolution. Keep it external so it's required from node_modules at runtime (like node-libcurl).
'quickjs-emscripten',
'quickjs-emscripten-core',
'@jitl/*',
'fsevents',
'@node-llama-cpp/mac-arm64-metal',
'@node-llama-cpp/mac-x64',
Expand Down
3 changes: 2 additions & 1 deletion packages/insomnia/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@
"acorn": "^8.16.0",
"acorn-walk": "^8.3.5",
"ajv": "^8.17.1",
"assert": "^2.1.0",
"apiconnect-wsdl": "2.0.36",
"assert": "^2.1.0",
"aws4": "^1.13.2",
"blakejs": "^1.2.1",
"buffer": "^6.0.3",
Expand Down Expand Up @@ -130,6 +130,7 @@
"oauth-1.0a": "^2.2.6",
"objectpath": "^2.0.0",
"papaparse": "^5.5.2",
"quickjs-emscripten": "^0.32.0",
"react": "^18.3.1",
"react-aria": "3.43.2",
"react-aria-components": "^1.12.2",
Expand Down
Loading
Loading