Replace IPC-based database/services bridge (database.main,
database.client,
services-proxy) with a MessagePort RPC layer backed by an Electron
utilityProcess (entry.data.ts).
- Add PortRpc transport, data-process server, and crash-restart manager
- Unify all entry points to use initDataBridge(invokeDataPort)
* feat(templating): (H1) in-sandbox response-hook API + marshaling
First increment of PR 10b (response hooks). Adds the sandbox-side machinery to
run a user plugin's response hook, without yet wiring the network adapters:
- __buildResponseApi(resp): faithful ES5 rebuild of plugins/context/response.ts
getters (status/headers/time, case-insensitive getHeader returning
string|string[]|null). getBody() bridges to the existing response.getBodyBuffer
path; getBodyStream() throws (a Node Readable can't cross the sandbox); setBody()
base64-encodes the bytes over a new response.setBody bridge path (no fs in the
sandbox) and updates bytesContent locally.
- __invokeHook: for response hooks, attaches context.response and a READ-ONLY
context.request (matches pluginRequest.init(..., true)), and marshals both the
request and response back out.
- host-bridge: response.setBody mapped at baseline (models.read, grouped with the
other response ops) — a bounded write to the host-set bodyPath, matching the
ungated in-process behavior; not a general fs write.
Unit test drives a response hook that reads status/headers, reads the body via
the bridge, and rewrites it via setBody (asserting the base64 round-trip and the
read-only request). Host runner + adapter integration + e2e are the next increment.
* feat(templating): (H1) route user-plugin response hooks through the sandbox
Wires the response-hook sandbox core into both hook paths so a user plugin's
response hook runs in QuickJS (bundle plugins and the flag-off path unchanged).
- templating-worker-database: runResponseHookInSandbox + the
plugin.runUserResponseHook handler; the response.setBody bridge handler
(base64-decode -> fs.writeFileSync) guarded to the responses directory as
defense in depth (bodyPath is host-set); pickHookResponseFields.
- network-adapter.node.ts (main/CLI): runs the response hook in the sandbox for
user plugins, merging the returned response fields onto newResponse.
- invoke-method.ts (plugin window): reaches the same runner over the protocol.
E2E: a user plugin response hook rewrites the body via setBody; the echo
response pane shows the rewrite. Flag off it reports ranin-mainprocess (control),
flag on ranin-sandboxed. Completes H1 (request hooks in PR 10a).
* fix(templating): (H1) revive bridged Buffer in response getBody + reject empty setBody
Addresses Copilot review on #10286:
- in-sandbox-bootstrap.ts: context.response.getBody() now revives the JSON-marshaled
Buffer shape ({ type: 'Buffer', data: [...] }) back into a (shimmed) Buffer, so hooks
consume it exactly like the in-process response API instead of a plain object.
- templating-worker-database.ts: response.setBody rejects a missing/non-string bodyBase64
(TypeError) instead of defaulting to '' — a write primitive must not silently truncate
the body to zero bytes. An empty string stays a valid empty body.
- sandbox-hooks.test.ts: the getBodyBuffer stub now returns the real marshaled Buffer
shape and the hook decodes with toString('utf8'), exercising the revive end-to-end.
* feat: show full value on hover for truncated single-line fields
* chore: clean up
* feat: show full value on hover for truncated single-line fields
* feat: show full value on hover for truncated single-line fields
* feat: show full value on hover for truncated single-line fields
* feat: show full value on hover for truncated single-line fields
* chore: address copilot feedback
* chore: more copilot feedback
* feat: handle cases to render tooltip when there is a combination of nunjucks tags as well as text in a cell
* feat: show raw nunjucks template string if a template is invalid
* feat(templating): (H1) in-sandbox request-hook execution + marshaling
First increment of PR 10 (hooks). Adds the sandbox-side machinery to run a
user plugin's request hook inside QuickJS and marshal the mutated request back
out, without yet wiring the network adapters (next increment):
- __buildRequestApi(req, readOnly): faithful ES5 rebuild of
plugins/context/request.ts (getters + mutators, case-insensitive header /
case-sensitive parameter matching, readOnly deletions) operating on the
copied-in renderedRequest — no host calls.
- __invokeHook(): builds the (capability-gated) hook context via __buildContext,
attaches context.request, runs the hook at hookIndex, returns the mutated
request as JSON. HOOK_RUNNER drives it; runTagInSandbox picks it when the
envelope carries hookKind.
- ContextEnvelope gains hookKind/hookIndex/hookRequest/hookResponse; new
HookResult type for the marshaled-out data.
Unit tests cover header/url/method/body mutation round-trip, case-insensitive
header parity, env-var reads, and capability gating (network absent when
ungranted, present when granted). Host-side runner + adapter integration + e2e
land in the next increment.
* feat(templating): (H1) route user-plugin request hooks through the sandbox
Wires the request-hook sandbox core into both hook-execution paths so a user
plugin's request hook runs in QuickJS instead of in-process (bundle plugins and
the flag-off path are unchanged). Gated on templateTagSandboxEnabled.
- templating-worker-database: runRequestHookInSandbox (reuses the capability
bridge + grants + host crypto; marshals the mutated request field subset back)
and the plugin.runUserRequestHook bridge handler.
- network-adapter.node.ts (main/CLI): calls runRequestHookInSandbox directly for
user plugins; merges the returned mutation onto the request, chaining hook-to-hook.
- invoke-method.ts (plugin window): reaches the same runner over the
templating-worker-database protocol; per-plugin counter recovers each hook's index.
E2E (sandbox-hook-collection.yaml): a user plugin request hook injects headers;
the echo server reflects them. Flag off the hook reports main-process (control);
flag on it reports sandbox and the injected header reaches the wire.
Response hooks (with the disk-writing response.setBody) follow as PR 10b.
* test(templating): assert H1 hook e2e on header values, not lowercased names
The echo server lowercases request-header names but preserves values, so the
prior assertion on the 'X-Sandbox-Hook' name failed. Assert on unique header
values instead (ranin-sandboxed / ranin-mainprocess canary + hook-marker), also
removing the ambiguous 'sandbox' substring match. The sandbox hook path itself
was working; only the assertion was wrong.
* fix(templating): guard node-runtime hook sandbox to Electron; fail-fast on missing hookRequest
Two review fixes:
- network-adapter.node.ts backs both the Electron main process AND the pure-Node
inso CLI. The sandbox host (templating-worker-database) statically imports
electron, so importing it from the CLI crashes when the flag is on with request
hooks present. Gate the sandbox path on `process.type` (Electron only); the CLI
runs hooks in-process as it always has (the sandbox host is Electron-resident).
- __invokeHook: pass env.hookRequest through directly instead of `|| {}`, so a
missing request throws (matching in-process pluginRequest.init) and the object
marshaled back is always the one the hook mutated.
* feat(templating): (H1) route user-plugin request hooks through the sandbox (PR 10a) - sec request hooks review (#10290)
* fix(templating): authenticate plugin IPC dispatch and sanitize hook marshal boundary
H1's request-hook capability gating was moot from outside the sandbox: plugin-window.ts
dispatched plugins.applyRequestHooks (and siblings) to the plugin window for any IPC sender,
with no check that the caller was the legitimate main window, letting forged renderedRequest/
environment data trigger every installed plugin's hooks and read the mutated result back.
Route every ipcMain.handle/.on registration through one of three sender-checked wrappers
(handleFromMainWindow/onFromPluginWindow/handleFromPluginWindow), with a static guardrail
(no bare ipcMain call outside the wrappers) and a dynamic per-channel test so a future
channel is protected automatically instead of by convention.
Also harden the hook-mutation marshal boundary: JSON.parse now strips __proto__/constructor/
prototype keys at any depth via a reviver, and the merge onto the live request object copies
only the HOOK_REQUEST_FIELDS allowlist instead of a blanket Object.assign of whatever the
sandbox returned. Not an active exploit today, but removes a latent prototype-pollution
primitive at the exact point untrusted sandbox output re-enters host memory.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* test(smoke): bound the plugin-toast dismiss loop instead of clicking forever
clearPluginToast used a plain .click() inside an unbounded while loop to dismiss
the "Plugin system updated" toast. The toast's DOM node gets swapped during its
exit transition, so Playwright's actionability wait saw it go stable -> detached
and retried indefinitely, eventually blowing the test's timeout. Bound the loop
to a wall-clock deadline and use force clicks that don't wait on stability.
Also routes the first test's copy-pasted instance of the same dismiss pattern
through the shared (now-fixed) helper instead of duplicating it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(templating): close remaining hook-control bypasses in the templating db protocol
The prior fix authenticated plugins.applyRequestHooks over ipcMain, but the
same capability (running a user plugin's request hook, executing a plugin
tag, discovering a plugin's exports, and the whole DB bridge) was dispatched
by resolveDbByKey behind the insomnia-templating-worker-database:// protocol
with no caller check at all. Anything able to fetch() that scheme could run
any installed plugin's request hook with forged data and read the result
back. Generate a per-process secret in main, hand it out only to the main
window and plugin window over a sender-checked IPC channel, and reject any
protocol call missing or mismatching it before it reaches a handler.
Two of those handlers also took the on-disk plugin directory straight from
the request body, letting a forged call point hook or discovery execution
at any folder with a package.json. Both now resolve directory server-side
from the trusted plugin registry, keyed by name, and reject unknown names.
stripDangerousKeysReviver was applied to the hook-result path but not to
the manifest-discovery parse or resolveDbByKey's own request-body parse.
Route both through it so untrusted sandbox output is sanitized at every
point it re-enters host memory, not just one.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* test(templating): send a valid auth token in the app.prompt db-protocol test
The templating db protocol now requires TEMPLATING_DB_AUTH_HEADER on every
request; this pre-existing test predated that requirement and was failing
with a 401 instead of exercising the prompt-bridge path.
* docs(templating): condense verbose comments added by the hook-security fixes
Trims the review-finding-style prose in the new sandbox auth/marshal code
down to plain, single-purpose comments, and drops em dashes and references
to internal plan/finding documents.
* security(templating): document + regression-test permissions-trust gap in H1 hook dispatch
F2' resolved a plugin's on-disk `directory` server-side from the trusted registry, but
left `permissions` as a caller-supplied request-body field for both
plugin.runUserRequestHook and plugin.discoverUserPluginExports. Since C1/C2/C3 already
make insomnia.permissions a live capability boundary, a caller reaching the protocol
with a valid auth token can run any registered plugin's hook with capabilities beyond
its own declared manifest, up to the template-tag profile ceiling.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(templating): resolve permissions from the trusted registry in hook/discovery dispatch
plugin.discoverUserPluginExports and plugin.runUserRequestHook trusted a
caller-supplied `permissions` object instead of the plugin's registered
manifest, letting any caller that reached the (now-authenticated) protocol
grant a plugin broader capabilities than its own package.json declares. Mirror
the same trusted-registry resolution F2' already applies to `directory`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* test(templating): cover the directory/permissions axes on both dispatch handlers
Each trust-resolution guard (directory, permissions) previously covered only
one of the two handlers that call resolveTrustedPlugin: directory-forgery was
tested only via plugin.discoverUserPluginExports, permissions-forgery only via
plugin.runUserRequestHook. That asymmetry is exactly the shape of gap that let
the permissions-trust bug slip past the directory fix's own regression test.
Add the missing pairing: a directory-forgery case for runUserRequestHook, and
a permissions.modules-forgery case for discoverUserPluginExports.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: kwburns-kong <kyle.burns@konghq.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* feat(templating): (L1) discover user-plugin exports in the sandbox, not via nodeRequire
Phase 2, PR 9. With the sandbox enabled, a user plugin's exports are now
discovered by evaluating its source INSIDE the QuickJS sandbox and returning a
serializable manifest, instead of nodeRequire-ing the module in the plugin
window. Installing/enabling a user plugin therefore no longer runs its
top-level code with host (Node) privileges — top-level code that reaches for
require('child_process')/require('fs') is default-denied in the sandbox.
- in-sandbox-bootstrap: add __describeExports() (enumerates templateTags
metadata, hook counts, action labels/icons, theme data) + DESCRIBE_RUNNER.
- plugin-tag-sandbox: runTagInSandbox gains a "discover" mode.
- templating-worker-database: discoverPluginExportsInSandbox + the
plugin.discoverUserPluginExports handler; factor the shared host crypto +
capability bridge out of runPluginTagInSandbox.
- plugins/index.ts: traversePluginPath discovers user plugins via the sandbox
when templateTagSandboxEnabled is on (bundle plugins keep nodeRequire), and
builds Plugin.module from the manifest descriptors.
Tags execute through the existing sandbox bridge and themes are data, so both
work. Hook/action EXECUTION isn't sandbox-routed yet (H1/A1, PR 10/11): they
still enumerate from the manifest, but invoking one throws a clear error until
then. Gated on the existing templateTagSandboxEnabled flag.
Unit: sandbox-export-discovery (manifest shape, multi-file discovery, throwing
top-level contained, ungranted-require denied). E2E: sentinel test proving
top-level code doesn't run on the host at load while the tag still enumerates
and runs.
* fix(templating): retry L1 discovery on transient "fetch failed" during reload
The sandbox export-discovery call rides the insomnia-templating-worker-database
protocol from the plugin window. The plugin reload that flips the sandbox on is
triggered by a settings change, and the protocol fetch can briefly fail
("fetch failed", transport-level) while the app re-renders around that change —
dropping the plugin so its tags don't enumerate. Retry the discovery call up to
5 times with a short backoff on that transport error only; handler errors (a
denied require, a syntax error) still come back as a resolved 500 and surface
immediately. A persistent failure still throws to the per-plugin catch.
* fix(templating): call L1 discovery directly in main, not via the renderer protocol
getPlugins/traversePluginPath runs in the main process too (templating-worker-
database imports getTemplateTags from ~/plugins), so when a flag-on render calls
executeUserPluginTag -> getTemplateTags -> getPlugins, discovery ran in main via
fetch('insomnia-templating-worker-database://...'). That scheme is a
renderer<->main protocol handler; main's own fetch can't resolve it, so every
user plugin's discovery failed with "fetch failed" and its tags vanished
("tag X not found"). Not transient — retrying didn't help.
Branch on __IS_RENDERER__: in main call the sandbox discovery directly
(discoverUserPluginExportsForLoader, now shared with the bridge handler); in a
renderer (the plugin window) keep using the protocol. Fixes the sandbox smoke
suite regressions on the flag-on path.
* fix(templating): clamp discovered hook counts to prevent an allocation DoS
The export manifest reports request/response hooks as a count, and the host
builds Array.from({ length: count }) of throw-stubs. A hostile plugin could
export a non-array with a huge `.length` (e.g. { length: 1e12 }) so the count
crosses to the host and drives an enormous allocation — even though the plugin
only ran in the sandbox.
Coerce the count to a finite, non-negative integer capped at 1000 inside the
sandbox (__describeExports), and clamp again host-side before Array.from as
defense in depth. Unit test covers a fake huge-length hooks export.
* feat(templating): (L1) discover user-plugin exports - bridge fix (#10280)
* test(templating): add regression coverage for discovery-time bridge escape
Discovery mode (discover: true) is supposed to be side-effect-free, but
runTagInSandbox wires the real host bridge into the VM unconditionally, and
plugin top-level code can call globalThis.__buildContext(...) itself to reach
it before any tag is ever inserted or run. Adds a fixture-driven regression
test for the exploit shape, a stretch-goal walk+invoke reachability probe over
every globalThis-reachable function, and a completeness tripwire over the
sandbox's internal __-prefixed globals so a new one added later forces review.
Both bridge-escape tests currently fail (red) against the unfixed code.
Supersedes the untracked POC-discovery-bridge-escape.test.ts PoC, now removed.
* fix(templating): substitute a rejecting bridge during L1 discovery
Discovery mode (discover: true) is supposed to be side-effect-free, but
runTagInSandbox wired the real host bridge into the VM unconditionally, and
plugin top-level code could reach it directly via the bare __buildContext
global before any tag was ever inserted or run. Swap in a rejecting bridge
whenever discover is set, so any such call fails inside the sandbox instead
of reaching the host, no matter how it's invoked.
Turns the regression coverage added in f37977f00 green.
---------
Co-authored-by: kwburns-kong <kyle.burns@konghq.com>
* feat(templating): multi-file plugins via in-sandbox module map (M4)
esbuild is dev-only, so instead of bundling plugins at load time this reads the
plugin's own source files into a module map (readPluginModuleMap, host-side)
and teaches the in-sandbox require to resolve RELATIVE specifiers from that map
(./util, ../x, ./dir -> ./dir/index.js) with a CommonJS cache. Bare specifiers
still route through the grant-gated registry.
Security (the poison guarantee): the host walker reads ONLY within the plugin
directory — node_modules and dot-dirs are skipped, symlink-escapes dropped,
file count/size bounded — so a plugin's own node_modules is never consulted and
a bare require('uuid') always binds the vetted registry copy, never a plugin's
substitute. Plugin source now travels as envelope DATA (moduleFiles) compiled by
the loader via new Function; no host-side eval of plugin code. Single-file
callers keep working via a synthesized one-entry map.
Tests: multi-file-plugins.test.ts (7: relative/nested/dir-index resolution,
hybrid bare-uuid, CJS cache, not-found, syntax-error propagation, bare-vs-map
shadow) + readPluginModuleMap tests (map shape, node_modules-excluded poison
guarantee, custom main, entry-escape). E2E: multi-file resolution + poison
(fake node_modules/uuid ignored). Demo converted to multi-file
(require('./lib/greeting')). PERMISSIONS.md documents multi-file + the
require-vetted-libs-inside-run note.
No esbuild runtime dependency added.
* fix(templating): tighten M4 module-map byte guard and relative-path escape
Address review feedback on the multi-file plugin loader:
- readPluginModuleMap counted source.length (UTF-16 code units), which
undercounts multi-byte UTF-8 payloads against MAX_PLUGIN_MODULE_BYTES, and
the custom-"main" entry fallback was added to the map without counting its
bytes at all. Count Buffer.byteLength(...'utf8') via a shared addModule
helper used by both the walk and the entry fallback.
- __normalizeKey clamped above-root '..' traversal back into the plugin root,
so require('../../util') from a shallow dir could masquerade as an in-plugin
module. Return null for above-root traversal (unresolvable), matching Node
semantics; __resolveRelative propagates it as MODULE_NOT_FOUND.
* perf: rewrite command palette search to eliminate UI freezes and stale results
Problems fixed:
- clientLoader blocked navigation on every palette open, freezing the UI during
full sequential DB scan
- no AbortController: stale searches could overwrite newer results mid-type
- no debounce: every keystroke triggered a full DB traversal immediately
- React Aria's default contains filter re-filtered server-side fuzzy results,
silently dropping valid matches and making fuzzy work redundant
- no warm baseline: palette started blank until the entire load completed
Changes:
- move fuzzyMatch/fuzzyMatchAll → insomnia-data/common-src/search.ts (shared,
testable in node)
- move search logic → insomnia-data/node-src/services/helpers/command-search.ts
- per-request AbortController cancellation via abort(requestId)
- recursion depth guard (max 20) on request group traversal
- safeParent() replaces non-null assertions, safe against orphaned documents
- add CommandSearchResult type in insomnia-data/src/command-search-types.ts
- add useCommandSearch hook: 250ms debounce + abort on filter change + warm
baseline on mount
- add defaultFilter={() => true} to ComboBox, delegating all filtering to the
service
- delete routes/commands.tsx
* fix
* fix
* fix: ensure unique blank IDs for environment key-value pairs to prevent stale state
* test: add validation for blank row behavior after deleting committed rows in environment table editor
* chore: emit junit reports per e2e tests shard
* chore: include retries in junit reporter
* chore: test some stuff
* chore: test again
* chore: revert test
* feat(templating): surface profiles + ceiling + manifest-less migration warning (P1)
Add per-surface sandbox profiles (surface-profiles.ts): a profile is the
ceiling of what a plugin on a surface may be granted, effective grant =
floor ∪ (declared ∩ ceiling).
- TEMPLATE_TAG_PROFILE excludes 'credentials' from its capability ceiling —
cloud-credential access is reserved for first-party bundle plugins; a user
template-tag plugin can't obtain it even by declaring it.
- SCRIPTING_PROFILE (broad, enumerated) defined for the later scripting-engine
reuse; unit-tested, not wired to a live surface yet.
- resolveTemplateTag{Modules,Capabilities} move here from module-registry/
host-bridge and delegate to the profile (capabilities enforce the ceiling;
modules pass declared names through so the require gate gives the precise
'not available' vs 'not permitted' message).
- Migration warning: a manifest-less plugin denied a non-baseline module gets a
one-time (per-plugin-per-session) toast via the existing show-toast channel,
naming the module + the insomnia.permissions field to add.
- Docs: PERMISSIONS.md; demo README + capability ceiling note.
Tests: surface-profiles.test.ts (effectiveGrant matrix, floor⊆ceiling, credentials
ceiling exclusion); migration-warning dedup unit test; C2 shape tests decoupled
from the profile (grant caps directly); e2e P1 test (ceiling + one-time warning).
107 unit + 4 smoke green.
* feat(templating): typed module denial (#10254)
* fix(templating): typed module-denial error instead of message regex (P1)
maybeWarnMissingManifest matched the sandbox's thrown error message
("Module 'X' not permitted by manifest") with a regex to decide whether
to show the migration toast. That couples a UI feature to English error
prose crossing the QuickJS VM boundary — any future wrapping or wording
change breaks the toast silently, with no compiler signal.
__require now throws with `code`/`moduleName` own properties
(SANDBOX_MODULE_NOT_PERMITTED vs SANDBOX_MODULE_NOT_AVAILABLE), which
toError() in plugin-tag-sandbox.ts carries through onto the rebuilt
Error. maybeWarnMissingManifest branches on that discriminant instead of
regexing the message text; the message strings remain unchanged and
still asserted verbatim by the existing tests.
* fix(templating): key migration-warning dedup by (plugin, module), add ceiling tripwire
maybeWarnMissingManifest deduped its one-time toast by plugin name alone,
so a manifest-less plugin that trips on two different ungranted modules in
one session only ever saw a hint for the first one it hit — the second,
equally actionable denial was silently suppressed. Key by (plugin, module)
instead so each distinct missing grant still gets its own toast, while a
repeat denial for the same module still doesn't re-toast.
Also added a regression test pinning that a manifest with a malformed axis
(declared:true, per-card warning) is correctly routed away from this toast
path, and a tripwire test on TEMPLATE_TAG_PROFILE.moduleCeiling: resolveTemplateTagModules
deliberately doesn't enforce the ceiling (an unregistered module must still
reach require() for the precise "not available" message), which is only
safe while the ceiling equals the full module registry — this test fails
first if a future change narrows it, as a signal to revisit that function.
---------
Co-authored-by: kwburns-kong <kyle.burns@konghq.com>
__buildContext now builds the full context, then removes branches whose
capability is not granted, so context.network/store/app/util.readFile/
util.openInBrowser/util.models.cloudCredential are undefined (not
present-but-rejecting stubs) unless declared. Driven by __hasCap reading the
captured grantedCapabilities via the pristine __arrIndexOf (unspoofable),
kept in lockstep with C1's BRIDGE_PATH_CAPABILITIES. Baseline caps (render,
models.read, util, crypto) and context/meta/renderPurpose data are never gated.
Plugins can feature-detect (if (context.network)) and degrade gracefully;
touching an ungranted branch fails at the property access in the plugin's own
code, not as a later async rejection. C1's bridge gate remains the backstop.
- unit: context-shape matrix, feature-detect, attach-on-grant, and a C1<->C2
consistency test (branch present iff its bridge-path capability is granted)
- reframed the C1 e2e denial test to prove the bridge backstop (branch granted
to C2 but withheld from the bridge filter); no-manifest denial now covered by
C2's absent-branch test
- smoke: capdenied tag feature-detects (returns 'no-storage-capability'); test
retitled (C1 + C2)