feat: lift network.ts fs/path behind window.main.timeline IPC bridge (#9945)

* updated plan

* feat: lift network.ts fs/path use behind window.main.timeline IPC bridge

Removes `node:fs` and `node:path` from the renderer-reachable
`src/network/network.ts`. Three timeline-path constructions and two
`appendFile` calls are replaced with narrow `window.main.timeline.getPath`
(sync IPC) and `window.main.timeline.appendToFile` (async IPC) helpers
that live in main, where Node builtins belong.

Path validation in `appendToTimeline` mirrors `writeResponseBodyToFile`:
only paths inside the `responses/` directory ending in `.timeline` are
accepted, preventing a compromised renderer from writing arbitrary files.

Updates `config/renderer-node-import-baseline.json` to remove the two
`src/network/network.ts` entries — the baseline shrinks as intended.

Part of the nodeIntegration: false migration (PR B).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: address Aikido path-traversal feedback and mock window.main in network tests

- `getTimelinePath`: use `path.resolve` + `path.relative` check instead of
  `path.join` to prevent path-traversal attacks (Aikido medium severity finding)
- `network.test.ts`: add `vi.stubGlobal('window', ...)` mock for
  `window.main.timeline` so tests don't throw "window is not defined" now
  that `defaultSendActionRuntime` calls `window.main.timeline.appendToFile`

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* chore: suppress echoServer stdout in playwright config

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: update insomnia-inso logger for consola v3 compatibility

FancyReporter and BasicReporter were removed in consola v3; LogLevel became
a type-only export and the runtime enum is now LogLevels. Replace with
createConsola + a local BasicReporter shim, and import LogLevels in cli.ts.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* refactor: address timeline IPC review concerns

- Convert timeline.getPath from sendSync to invoke (async) to avoid
  blocking the renderer thread; path construction has no I/O
- Extract getResponsesDir() shared helper so both getTimelinePath and
  appendToTimeline read the same source of truth, eliminating env-drift
  between the two calls
- Guard mkdir with a Set so the responses directory is only created once
  per process rather than on every appendFile call

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: upgrade consola to v3 and fix type/import issues in insomnia-inso

- Bump consola from ^2.15.3 to ^3.4.2 to match logger.ts which already used v3 API (createConsola)
- Fix logType → LogType (renamed in v3)
- Remove fancy option (removed in v3 ConsolaOptions)
- Use ConsolaInstance instead of Consola in result-report.ts so .log() resolves correctly
- Fix import sort in cli.ts

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: restore Node.js-safe fallbacks in network.ts for inso CLI

The timeline IPC bridge introduced window.main.timeline calls without
guarding against the inso CLI context where window is not defined.
The electron shim (aliased in the inso bundle) provides app.getPath
as a fallback, matching the pre-bridge behaviour.

- getTimelinePath: check typeof window before using IPC; fall back to
  the electron shim path (os.tmpdir()/insomnia-send-request/responses)
- defaultSendActionRuntime.appendTimeline: fall back to fs.promises.appendFile
- tryToExecutePreRequestScript catch block: skip IPC appendToFile in
  Node.js context

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: sort node: imports before third-party and replace if/else with ternary in network.ts

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: replace static fs/path imports with inline require() to pass renderer baseline check

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* refactor: replace process.type branching in network.ts with build-time adapter modules

Eliminates all runtime process.type and typeof window checks by introducing
network-adapter.renderer.ts and network-adapter.node.ts. Vite and Vitest resolve
the import to the renderer adapter; inso esbuild resolves to the node adapter.
No branching code remains in network.ts itself.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: restore object alias format in vite.config.ts to fix rollup build

Array-form alias with find:'~' was not matching prefix imports like
~/common/insomnia-fetch in the react-router production build. Object
form behaves correctly in Vite 7.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: resolve merge conflicts in logger.ts and cli.ts, restore AGENTS.md indented tree

- logger.ts: keep LogType (consola v3 casing), drop duplicate conflict markers
- cli.ts: remove stashed duplicate LogLevels import from conflict block
- AGENTS.md: restore indented hierarchy in Repository Structure and Data Model sections

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* remove cx

* fix: add path traversal guard to getTimelinePath in node adapter

Mirrors the same defence-in-depth check already present in the IPC handler.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix tests which use network from main

* flake

* refactor: replace bundler aliases with process.type runtime detection in network-adapter

Instead of three separate bundler aliases (Vite, esbuild main, esbuild inso),
network-adapter.ts now selects the correct adapter at runtime using
process.type === 'renderer'.

- Vite prod: process.type is already inlined as 'renderer' via define, so
  Rollup tree-shakes the node branch
- esbuild main: define process.type='browser' so esbuild tree-shakes renderer branch
- esbuild inso: define process.type=undefined so esbuild tree-shakes renderer branch
- Vitest (insomnia): existing renderer alias kept for test environment
- Vitest (inso): add renderer alias to match pre-existing test behaviour and
  avoid loading native node-libcurl module in tests

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: restore vite alias for network-adapter to fix server bundle build

The react-router build produces both client and server bundles. Without
the alias, the server bundle encountered a runtime require() for
'./network-adapter.renderer' that couldn't resolve (Vite inlines
process.type='renderer' via define for the server build too, so Rollup
tree-shakes to the renderer branch, but the module gets externalized in
the server bundle rather than inlined, leaving a broken runtime require).

Restoring the alias ensures both builds inline network-adapter.renderer
directly, which is safe because the module only defines functions —
window.main is never called at module init time.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* feedback

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Jack Kavanagh
2026-05-27 09:44:48 +00:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent 5c39d6557b
commit 7cd8854f24
24 changed files with 402 additions and 921 deletions
+24 -45
View File
@@ -1,6 +1,7 @@
# AGENTS.md
## Tech Stack
- **UI:** React with React Router (loaders/actions pattern)
- **Components:** React Aria Components
- **Desktop Shell:** Electron (main + renderer processes)
@@ -9,14 +10,17 @@
- **Database:** NeDB (`@seald-io/nedb`) — embedded NoSQL
- **Build/Dev:** Vite, npm workspaces monorepo
*See `package.json` for current versions and `.nvmrc` for the Node version.*
_See `package.json` for current versions and `.nvmrc` for the Node version._
## Strict Rules
- **No unsolicited formatting.** Rely on ESLint/Prettier. Do not reformat existing code.
- **Strict scoping.** Only modify code directly related to the prompt. Do not refactor adjacent code unless asked.
## Command Output
Prefer quiet command variants to minimise output volume:
- `git log --oneline -20` not `git log`
- `git diff --stat` not `git diff`
- `npm test --silent` not `npm test`
@@ -25,6 +29,7 @@ Prefer quiet command variants to minimise output volume:
- Use `Grep` with `head_limit` rather than unrestricted searches
## Validation Commands
Run from repo root before considering work complete:
```bash
@@ -33,7 +38,22 @@ npm run type-check # TypeScript check all workspaces
npm test # Tests all workspaces (or: npm test -w packages/insomnia)
```
## Worktree Setup
- New git worktrees may not have `node_modules` yet. Before installing or validating, switch to the repo's required runtime from the worktree root:
```bash
fnm use "$(cat .nvmrc)"
node -v
npm -v
```
- This repo expects the `.nvmrc` Node version and npm 11+. If `fnm` is unavailable, manually use an equivalent Node/npm version before running any `npm` commands.
- After switching versions in a fresh worktree, install dependencies from repo root with `npm ci`.
- Do **not** use `npm ci --ignore-scripts` for normal worktree setup. It leaves Electron partially installed, which later breaks builds, renderer import checks, and other validation commands.
## Repository Structure
`packages/`
`insomnia/` ← Main Electron app
`src/`
@@ -52,6 +72,7 @@ npm test # Tests all workspaces (or: npm test -w packages/insomnia)
`insomnia-testing/` ← Test framework
## Data Model Hierarchy
Organization
→ Project (local | remote/cloud | git-backed)
→ Workspace (scope: 'collection' | 'design')
@@ -64,6 +85,7 @@ Organization
**Note:** A Workspace with `scope: 'collection'` IS the collection.
## Key Patterns
- **Route-Based Actions:** Mutations use React Router's `clientAction` (`src/routes/`).
- **CRITICAL:** `clientAction` blocks navigation. For long-running UI operations, use plain async functions instead.
- **Database Buffering:** Always buffer bulk writes (`database.bufferChangesIndefinitely()`, then `flushChanges()`). Unbuffered writes fire UI revalidation per operation, causing severe lag.
@@ -79,49 +101,6 @@ Organization
- New test imports: `import { test } from '../../playwright/test'` and `import { expect } from '@playwright/test'`.
## Sensitive Data
- **Vault system (AES-GCM):** For environment secrets (`EnvironmentKvPairDataType.SECRET`).
- **Electron safeStorage:** Platform-native encryption (`window.main.secretStorage`).
## cx — Semantic Code Navigation
Prefer cx over reading files. Escalate: overview → symbols → definition/references → Read tool.
### Quick reference
```
cx overview PATH file or directory table of contents
cx overview DIR --full directory overview with signatures
cx symbols [--kind K] [--name GLOB] [--file PATH] search symbols project-wide
cx symbols --kinds [--file PATH] list distinct kinds with counts
cx definition --name NAME [--from PATH] [--kind K] get a function/type body
cx references --name NAME [--file PATH] [--unique] find all usages (--unique: one per caller)
cx lang list show supported languages
cx lang add LANG [LANG...] install language grammars
```
Aliases: `cx o`, `cx s`, `cx d`, `cx r`
Kinds: fn, struct, enum, trait, type, const, class, interface, module, event
### Key patterns
- Start with `cx overview .`, drill into subdirectories — cheaper than ls + reading files
- `cx definition --name X` gives exact text for Edit tool's `old_string` without reading the whole file
- `cx references --name X --unique` shows one row per caller — use before refactoring to check blast radius
- After context compression, use `cx overview` / `cx definition` to re-orient — don't re-read full files
- Check signatures for `pub`/`export` to identify public API without reading the file
### Pagination
Default limits: definition 3, symbols 100, references 50. When truncated, stderr shows:
```
cx: 3/32 definitions for "X" | --from PATH to narrow | --offset 3 for more | --all
```
`--offset N` pages forward, `--all` bypasses, `--limit N` overrides. Narrowing with `--from`/`--file`/`--kind` is usually better than paging.
JSON: paginated → `{total, offset, limit, results: [...]}`, non-paginated → bare array.
### Missing grammars
If cx reports a missing grammar, install with `cx lang add <lang>`. Run `cx lang list` to see what's installed.
+1 -5
View File
@@ -29365,7 +29365,7 @@
"@stoplight/spectral-rulesets": "^1.22.1",
"@stoplight/types": "^14.1.1",
"commander": "^12.1.0",
"consola": "^2.15.3",
"consola": "^3.4.2",
"cosmiconfig": "^9.0.0",
"enquirer": "^2.4.1",
"picocolors": "^1.1.1",
@@ -29398,10 +29398,6 @@
"node": "^12.20 || >=14.13"
}
},
"packages/insomnia-inso/node_modules/consola": {
"version": "2.15.3",
"license": "MIT"
},
"packages/insomnia-scripting-environment": {
"version": "12.5.1-alpha.0",
"license": "Apache-2.0",
+1
View File
@@ -39,6 +39,7 @@ const config: BuildOptions = {
'process.env.DEFAULT_APP_NAME': JSON.stringify(isProd ? 'Insomnia' : 'insomnia-app'),
'process.env.VERSION': JSON.stringify(isProd ? version : 'dev'),
'__DEV__': JSON.stringify(!isProd),
'process.type': 'undefined',
},
// node-llama-cpp is not included here because inso does not need it
external: ['@getinsomnia/node-libcurl', 'fsevents', 'mocha'],
+1 -1
View File
@@ -58,7 +58,7 @@
"@stoplight/spectral-rulesets": "^1.22.1",
"@stoplight/types": "^14.1.1",
"commander": "^12.1.0",
"consola": "^2.15.3",
"consola": "^3.4.2",
"cosmiconfig": "^9.0.0",
"enquirer": "^2.4.1",
"picocolors": "^1.1.1",
+3 -2
View File
@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises';
import nodePath from 'node:path';
import * as commander from 'commander';
import { LogLevels } from 'consola';
import { cosmiconfig } from 'cosmiconfig';
// @ts-expect-error the enquirer types are incomplete https://github.com/enquirer/enquirer/pull/307
import { Confirm } from 'enquirer';
@@ -37,7 +38,7 @@ import { matchIdIsh } from './db/models/util';
import { loadWorkspace, promptWorkspace } from './db/models/workspace';
import type { Database } from './db/types';
import { InsoError } from './errors';
import { BasicReporter, logger, LogLevel } from './logger';
import { BasicReporter, logger } from './logger';
import { logTestResult, logTestResultSummary, reporterTypes, type TestReporter } from './reporter';
import { generateDocumentation } from './scripts/docs';
import { getAppDataDir, getDefaultProductName } from './util';
@@ -296,7 +297,7 @@ export const go = (args?: string[]) => {
...commandOptions,
configFileContent: __configFile,
};
logger.level = options.verbose ? LogLevel.Verbose : LogLevel.Info;
logger.level = options.verbose ? LogLevels.verbose : LogLevels.info;
options.ci && logger.setReporters([new BasicReporter()]);
options.printOptions && logger.log('Loaded options', options, '\n');
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import nodePath from 'node:path';
import type { Consola } from 'consola';
import type { ConsolaInstance } from 'consola';
import { pick } from 'es-toolkit';
import type {
@@ -65,7 +65,7 @@ export class RunCollectionResultReport {
outputFilePath: string;
includeFullData?: 'redact' | 'plaintext';
},
private logger: Consola,
private logger: ConsolaInstance,
init?: Partial<ReportData>,
) {
Object.assign(this, init);
+14 -14
View File
@@ -1,22 +1,22 @@
import type { logType } from 'consola';
import consola, { BasicReporter, FancyReporter, LogLevel } from 'consola';
import type { ConsolaOptions, LogObject, LogType } from 'consola';
import { createConsola } from 'consola';
type LogsByType = Partial<Record<logType, string[]>>;
type LogsByType = Partial<Record<LogType, string[]>>;
type ModifiedConsola = ReturnType<typeof consola.create> & { __getLogs: () => LogsByType };
type ModifiedConsola = ReturnType<typeof createConsola> & { __getLogs: () => LogsByType };
const consolaLogger = consola.create({
reporters: [
new FancyReporter({
formatOptions: {
// @ts-expect-error something is wrong here, ultimately these types come from https://nodejs.org/api/util.html#util_util_inspect_object_options and `date` doesn't appear to be one of the options.
date: false,
},
}),
],
const consolaLogger = createConsola({
formatOptions: {
date: false,
},
});
(consolaLogger as ModifiedConsola).__getLogs = () => ({});
export const logger = consolaLogger as ModifiedConsola;
export { LogLevel, BasicReporter };
export class BasicReporter {
log(logObj: LogObject, _ctx: { options: ConsolaOptions }) {
process.stdout.write(logObj.args.join(' ') + '\n');
}
}
+1
View File
@@ -4,6 +4,7 @@ export default defineConfig({
test: {
hideSkippedTests: true,
alias: {
'~/network/network-adapter': new URL('../insomnia/src/network/network-adapter.node.ts', import.meta.url).pathname,
'~/': new URL('../insomnia/src/', import.meta.url).pathname,
},
env: {
@@ -8,7 +8,7 @@ const echoServer: PlaywrightTestConfig['webServer'] = {
url: 'http://localhost:4010',
timeout: 20 * 1000,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stdout: 'ignore',
stderr: 'pipe',
wait: {
stdout: /Listening at http/,
@@ -112,8 +112,8 @@ test.describe('Environment Editor', () => {
const bodyEditor = page.getByRole('dialog').getByTestId('CodeEditor').getByRole('textbox');
// move cursor right and input json string
await bodyEditor.focus();
await bodyEditor.press('ArrowRight');
await bodyEditor.fill('"anotherString":"kvAnotherStr","anotherNumber": 12345');
await page.keyboard.press('ControlOrMeta+a');
await page.keyboard.type('{"anotherString":"kvAnotherStr","anotherNumber": 12345}');
// Submit and wait for the JSON modal to close before proceeding
await page.getByRole('button', { name: 'Modal Submit' }).click();
await expect.soft(page.getByRole('dialog', { name: 'Modal' })).toBeHidden();
@@ -1,771 +1,123 @@
# Node Integration Migration PR Plan
This document breaks the renderer `nodeIntegration: false` migration into deliverable slices that can move in parallel without creating excessive merge conflict risk.
This document tracks the renderer `nodeIntegration: false` migration. The original PR-by-PR plan in this file was written when the baseline contained ~30 entries and many subsystems still owned filesystem and crypto code in renderer-reachable modules. Most of those candidates have since landed via other workstreams. This refresh reflects the actual current state of `packages/insomnia/src/`.
Update: PR 1 through PR 3 are merged. The remaining work is re-scoped around single feature flows instead of broad subsystem buckets so each PR can land with tighter review, clearer ownership, and faster iteration.
## Goal
The plan assumes the current guardrails are already in place:
Flip the main BrowserWindow in `src/main/window-utils.ts:199-208` from `nodeIntegration: true` to `nodeIntegration: false`. Phase 2 (later) flips `contextIsolation: false` to `true`. The hidden BrowserWindow keeps `nodeIntegration: true` for plugin and script execution.
- renderer import analyzer in `vite.config.ts`
- baseline comparison in `scripts/check-renderer-node-imports.ts`
- baseline snapshot in `config/renderer-node-import-baseline.json`
- CI enforcement through `npm run check:renderer-node-imports`
## Guardrails (already in place)
## Delivery Rules
- Renderer import analyzer in `vite.config.ts`
- Baseline comparison in `scripts/check-renderer-node-imports.ts`
- Baseline snapshot in `config/renderer-node-import-baseline.json`
- CI via `npm run check:renderer-node-imports`
1. Each PR should remove baseline entries or add guardrails. It should not add new renderer Node builtin imports.
2. If a PR removes offenders, update `config/renderer-node-import-baseline.json` in the same PR.
3. Prefer moving privileged behavior behind existing preload or `window.main` APIs before inventing new bridge surface.
4. Do not combine route cleanup with subsystem redesign unless the route is blocked on the subsystem boundary.
5. Remaining candidates should stay scoped to one user-visible feature flow or one tightly bounded privileged service.
6. Each PR should carry an explicit test automation plan before implementation starts.
7. If a feature depends on sync or storage behavior, land the sync/storage boundary first instead of mixing the dependency into the same PR.
Note: both `config/renderer-node-import-baseline.json` and `.reports/renderer-node-imports.json` are stale relative to the working tree. Run `npm run update:renderer-node-import-baseline` before opening the next PR.
## Reviewer Lanes
## What is actually left (verified against current code)
- Electron/runtime: people familiar with preload, IPC, and main process boundaries
- Router/UI: people familiar with route loaders, actions, and UI flows
- Network/gRPC: people familiar with request execution, file access, and gRPC flows
- Sync/storage: people familiar with VCS, project storage, and compression flows
- Plugins/templating: people familiar with plugin loading and templating execution
Seven files in `packages/insomnia/src/` still import Node builtins:
## Parallelization Summary
| File | Builtins | Notes |
|---|---|---|
| `src/plugins/index.ts` | `fs`, `path` | Plugin discovery |
| `src/plugins/create.ts` | `fs`, `path` | Plugin filesystem writes |
| `src/plugins/context/response.ts` | `fs`, `zlib`, `stream` (type only) | Plugin response API |
| `src/utils/plugin.ts` | `fs`, `path` | Plugin helpers |
| `src/network/network.ts` | `fs`, `path` | Request execution pipeline |
| `src/script-executor.ts` | `fs/promises` | Script execution file IO |
| `src/templating/base-extension.ts` | `crypto`, `os` | Templating bootstrap |
Already merged:
Plus two carve-out modules under `packages/insomnia-testing/src/` (`generate/generate.ts`, `run/run.ts`) which the analyzer counts but which are not loaded by the renderer at runtime — they ship with the CLI.
- PR 1: Route path-only cleanup
- PR 2: Route fs-backed cleanup
- PR 3: Shared browser-safe helper cleanup
## Already completed since the original plan
Next PR candidates:
No follow-up action needed for any of these:
- Candidate: Sync storage boundary foundation
- Candidate: Import parsing and persistence boundary
- Candidate: gRPC proto asset boundary **(quick win candidate)**
- Candidate: Plugin discovery boundary
- Candidate: Templating bootstrap boundary
- Candidate: OAuth token crypto cleanup **(quick win candidate)**
- Candidate: Response archival and compression boundary
- Candidate: Script executor boundary
- PRs 1–3 of the original plan (route path/fs cleanup, shared helper cleanup) — merged
- Response archival — `response-operations.ts` migrated to `insomnia-data/node-src/`
- `src/network/url-matches-cert-host.ts` — cleaned
- `src/scripting/require-interceptor.ts` — cleaned
- OAuth token crypto — files moved out of renderer-reachable paths
- Sync storage / VCS — moved to main / `insomnia-data`
- gRPC `proto-directory-loader.tsx` — moved (only `write-proto-file.ts` still lives in `src/network/grpc/`; verify before assuming clean)
- Import parsing (`src/common/import.ts`) — cleaned
- Plugin execution (Phase 1a of the plugin POC) — PR #9889 plus follow-ups; all plugin invocations now cross an IPC bridge to a hidden BrowserWindow
Dependencies:
## Remaining PRs
- Import depends on sync storage because import creates or updates persisted workspace state.
- Plugin discovery and templating should stay split so one does not become a catch-all runtime PR.
- gRPC proto asset work can likely move ahead once the sync-storage bridge pattern is clear.
- OAuth cleanup looks like the smallest remaining isolated boundary and is a good candidate for a fast iteration.
- Response archival and `script-executor.ts` should stay separate from the feature-scoped candidates unless a later PR proves they are truly part of the same flow.
### PR A: Plugins (largest cluster)
## Candidate Backlog
Files: `src/plugins/index.ts`, `src/plugins/create.ts`, `src/plugins/context/response.ts`, `src/utils/plugin.ts`.
## PR 0: Guardrails and Baseline
This is Phase 1b of the plugin POC (`PLUGIN_SYSTEM_POC.md`). Phase 1a already routes plugin *execution* through `window.main.plugins.*` IPC into a hidden BrowserWindow. Phase 1b moves plugin *discovery, loading, and the response context helper* fully into that hidden window so the renderer holds only metadata and bridge proxies. Re-use the existing `window.main.plugins.*` channel surface; do not invent a parallel discovery bridge.
Status: already in place
Risk: high. Longest renderer code path that survived the earlier cleanup.
Purpose:
Suggested reviewers: Plugins/templating, Electron/runtime.
- Keep new debt from being introduced while the migration is underway.
### PR B: `src/network/network.ts`
Primary files:
Last network offender. Two options:
- `packages/insomnia/vite.config.ts`
- `packages/insomnia/scripts/check-renderer-node-imports.ts`
- `packages/insomnia/config/renderer-node-import-baseline.json`
- `eslint.config.mjs`
- `.github/workflows/test.yml`
1. Lift `fs` / `path` use to a narrow main-side helper (`writeMultipartBody`, `resolveBodyFilePath`, etc.) exposed via existing `window.main` surface.
2. Or finish the pipeline-to-main move: push `sendCurlAndWriteTimeline`, `tryToInterpolateRequest`, and `tryToExecute*Script` into main and let the renderer call a single `window.main.executeRequest`. Cleaner endpoint, much larger PR.
Expected risk: low
Recommendation: do option 1 to unblock the flag flip; defer option 2 to post-flip cleanup.
Suggested reviewers:
Risk: medium.
- Electron/runtime
- Repo maintainers
Suggested reviewers: Network/gRPC, Electron/runtime.
## PR 1: Route Path-Only Cleanup
### PR C: `src/script-executor.ts`
Status: merged
One `appendFile` from `node:fs/promises`. Move the file-write behind an existing or new narrow `window.main.scriptLog.append` bridge. Keep the rest of script orchestration in place.
Purpose:
Risk: low.
- Remove route-local `node:path` usage where existing `window.path` is already sufficient.
Suggested reviewers: Plugins/templating, Electron/runtime.
Primary files:
### PR D: `src/templating/base-extension.ts`
- `src/routes/import.scan.tsx`
- `src/routes/organization.$organizationId.project.$projectId.workspace.update.tsx`
- `src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.tsx`
- `src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.generate-request-collection.tsx`
Replace `crypto` hashing with Web Crypto (`crypto.subtle`) where the algorithm allows, or expose a `window.main.hash` helper for legacy algorithms. Replace `os.hostname()` / `os.userInfo()` with values fetched once from main on startup and cached in renderer.
Likely implementation:
Risk: low.
- Replace `path.basename`, `path.dirname`, `path.join`, and similar calls with `window.path.*`.
- Keep behavior identical.
- Avoid introducing new preload methods.
Suggested reviewers: Plugins/templating, Electron/runtime.
Expected risk: low
### PR E: inso carve-out
Suggested reviewers:
Decide between:
- Router/UI
- Electron/runtime
(a) adding `packages/insomnia-testing` to the analyzer's allow-list — it never runs in the renderer; or
(b) restructuring the package so the renderer-imported entrypoint does not transitively pull `run.ts` / `generate.ts`.
Baseline entries to remove:
Option (a) is simpler and matches reality.
- `src/routes/import.scan.tsx -> path`
- `src/routes/organization.$organizationId.project.$projectId.workspace.update.tsx -> path`
- `src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.tsx -> path`
- `src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.generate-request-collection.tsx -> path`
Risk: low.
Dependencies:
Suggested reviewers: Electron/runtime, repo maintainers.
- none beyond PR 0
## The flip
Concurrent with:
After PRs A–E land and the baseline file is empty (or reduced to intentionally allowed entries):
- PR 2
- PR 3
- later dependency-clearing candidate
- **File:** `src/main/window-utils.ts:199-208`
- **Change:** `nodeIntegration: true` → `false`. Leave `contextIsolation: false` for now (Phase 2). Keep `nodeIntegrationInWorker: false` (already correct — protects the Nunjucks worker sandbox). Hidden window stays on `nodeIntegration: true`.
- **Audit:** any preload surface added during the migration, particularly anything that writes to disk on behalf of the renderer (e.g. `writeResponseBodyToFile`).
## PR 2: Route FS-Backed Cleanup
## Verification
Status: merged
- After each offender-removing PR: re-run `npm run update:renderer-node-import-baseline`, confirm the baseline shrinks, and commit the refreshed JSON in the same PR.
- Per-PR: `npm run lint`, `npm run type-check`, `npm test`.
- Before the flag flip: full smoke run (`npm run test:smoke:dev`) covering plugin load, send-request, gRPC, OAuth, scripting, templating.
- After the flag flip: in a dev build, confirm `typeof process === 'undefined'` (or `process.type === undefined`) in the main renderer DevTools console, and that the hidden window still has full Node access. Re-run smoke.
Purpose:
## Exit criteria
- Remove route-level `node:fs` and remaining `node:path` usage that touches downloads or file reads.
Primary files:
- `src/routes/organization.$organizationId.project.$projectId.workspace.new.tsx`
- `src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx`
Likely implementation:
- Replace file reads and writes with existing `window.main` APIs where possible.
- Reuse `window.path` for path manipulation.
- If a missing bridge is required, keep it minimal and specific.
Expected risk: medium
Suggested reviewers:
- Router/UI
- Electron/runtime
- Network/gRPC for the debug send route
Baseline entries to remove:
- `src/routes/organization.$organizationId.project.$projectId.workspace.new.tsx -> fs`
- `src/routes/organization.$organizationId.project.$projectId.workspace.new.tsx -> path`
- `src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx -> fs`
- `src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx -> path`
Dependencies:
- may touch preload if a new minimal bridge is needed
Concurrent with:
- PR 1
- PR 3
## PR 3: Shared Browser-Safe Helper Cleanup
Status: merged
Purpose:
- Remove Node builtin usage from helper modules that should be safe to load in the renderer.
Primary files:
- `src/common/misc.ts`
- `src/common/significant-diff-detection.ts`
- `src/utils/url/querystring.ts`
Secondary candidates if they can be made browser-safe without boundary work:
- `src/models/helpers/response-operations.ts`
Likely implementation:
- Replace Node URL and path helpers with browser or shared alternatives where possible.
- If compression remains privileged, split the pure helper from the privileged implementation.
Expected risk: medium
Suggested reviewers:
- Router/UI
- Electron/runtime
Baseline entries to remove:
- `src/common/misc.ts -> path`
- `src/common/misc.ts -> zlib`
- `src/common/significant-diff-detection.ts -> path`
- `src/utils/url/querystring.ts -> url`
Optional stretch target:
- `src/models/helpers/response-operations.ts -> fs`
- `src/models/helpers/response-operations.ts -> zlib`
Dependencies:
- none, unless compression or file IO must be pushed behind a bridge
Concurrent with:
- PR 1
- PR 2
- later dependency-clearing candidate
## Candidate: Sync Storage Boundary Foundation
Status: candidate
Purpose:
- Move local project sync storage so renderer code stops owning filesystem, compression, and VCS-path details.
Single feature scope:
- Local project persistence for sync-backed workspaces.
Primary files:
- `src/sync/store/drivers/file-system-driver.ts`
- `src/sync/store/drivers/graceful-rename.ts`
- `src/sync/store/hooks/compress.ts`
- `src/sync/store/index.ts`
- `src/sync/vcs/util.ts`
- `src/sync/vcs/vcs.ts`
- `src/sync/vcs/create-vcs.ts`
Implementation notes:
- Introduce a narrow storage-oriented bridge for read, write, rename, compression, and VCS-adjacent path work.
- Avoid mixing import, plugin, or network behavior into this PR.
- Move the vcs class entirely to main, simplify it down so its clear what internal state it has, expose its functions over IPC.
- create an event listener in the renderer to handle events from the conflictHandler function passed into vcs.
Expected risk: high
Quick win: no
Suggested reviewers:
- Sync/storage
- Electron/runtime
Baseline entries to remove:
- `src/sync/store/drivers/file-system-driver.ts -> fs/promises`
- `src/sync/store/drivers/file-system-driver.ts -> path`
- `src/sync/store/drivers/graceful-rename.ts -> fs/promises`
- `src/sync/store/hooks/compress.ts -> zlib`
- `src/sync/store/index.ts -> path`
- `src/sync/vcs/util.ts -> crypto`
- `src/sync/vcs/vcs.ts -> crypto`
- `src/sync/vcs/vcs.ts -> path`
Dependencies:
- none
Test automation plan:
- Extend `src/sync/store/hooks/__tests__/compress.test.ts` and related sync store tests to cover the new privileged boundary behavior.
- Add focused unit coverage for any new main-process sync bridge or IPC handlers.
- Add a renderer-side contract test that proves sync store calls delegate through the bridge instead of importing Node-backed code directly.
- Keep smoke coverage limited to one sync-backed roundtrip so this PR stays quick to iterate.
Enables:
- Import parsing and persistence boundary
- gRPC proto asset boundary
- Plugin discovery boundary
- Templating bootstrap boundary
Out of scope:
- import parsing or import persistence
- gRPC proto temp-file handling
- plugin discovery or templating runtime changes
## Candidate: Import Parsing and Persistence Boundary
Status: candidate
Purpose:
- Make import a self-contained feature flow that relies on the sync boundary instead of importing `src/main` helpers or privileged file access into renderer-reachable modules.
Single feature scope:
- Scan, parse, and persist imported resources.
Primary files:
- `src/common/import.ts`
- `src/routes/import.scan.tsx`
- `src/routes/import.resources.tsx`
- `src/ui/components/modals/import-modal/import-modal.tsx`
- `src/main/importers/convert.ts`
- `src/main/importers/importers/curl.ts`
- `src/main/importers/importers/openapi-3.ts`
- `src/main/importers/importers/swagger-2.ts`
- `src/main/network/parse-header-strings.ts`
- `src/main/secure-read-file.ts`
Implementation notes:
- Keep importer execution and privileged file reads behind explicit main-process entrypoints.
- Extract pure importer helpers into shared modules only where they are genuinely renderer-safe.
- Make the import flow consume the sync/storage candidate instead of mixing storage work into this candidate.
Expected risk: high
Quick win: no
Suggested reviewers:
- Router/UI
- Sync/storage
- Electron/runtime
Baseline entries to remove:
- `src/main/importers/importers/curl.ts -> url`
- `src/main/importers/importers/openapi-3.ts -> crypto`
- `src/main/importers/importers/openapi-3.ts -> url`
- `src/main/importers/importers/swagger-2.ts -> crypto`
- `src/main/network/parse-header-strings.ts -> url`
- potentially `src/main/secure-read-file.ts -> fs`
- potentially `src/main/secure-read-file.ts -> os`
- potentially `src/main/secure-read-file.ts -> path`
Dependencies:
- Sync storage boundary foundation
Test automation plan:
- Extend `src/common/__tests__/import.test.ts` to cover scan and persist paths after the boundary cleanup.
- Add route-level coverage for `src/routes/import.scan.tsx` and `src/routes/import.resources.tsx` where the bridge contract changes.
- Keep one UI-level import smoke path for a representative source such as curl or file import.
- Update the renderer-node-import baseline in the same PR once the import offenders are removed.
Enables:
- import follow-up polish, if needed
Out of scope:
- generic OAuth cleanup
- gRPC proto temp files
- plugin loading and templating
## Candidate: gRPC Proto Asset Boundary
Status: candidate
Purpose:
- Isolate the gRPC proto file preparation flow behind a privileged boundary without expanding the PR into broader network or response persistence cleanup.
Single feature scope:
- Preparing proto directories and temp proto files for gRPC request execution.
Primary files:
- `src/network/grpc/proto-directory-loader.tsx`
- `src/network/grpc/write-proto-file.ts`
- any minimal preload or IPC additions needed to support the proto write path
Implementation notes:
- Move proto temp-file creation and path work to main.
- Keep request assembly in the renderer, but remove direct `fs`, `os`, and `path` ownership from the gRPC preparation path.
- Do not mix in generic request execution, OAuth, or response archival work.
Expected risk: medium
Quick win: yes
Suggested reviewers:
- Network/gRPC
- Electron/runtime
Baseline entries to remove:
- `src/network/grpc/proto-directory-loader.tsx -> fs`
- `src/network/grpc/proto-directory-loader.tsx -> path`
- `src/network/grpc/write-proto-file.ts -> fs`
- `src/network/grpc/write-proto-file.ts -> os`
- `src/network/grpc/write-proto-file.ts -> path`
Dependencies:
- Sync storage boundary foundation patterns should be available first
Test automation plan:
- Extend or relocate unit coverage for `write-proto-file` so the privileged file-write path stays directly tested.
- Add a contract test proving the renderer gRPC flow uses the new bridge instead of direct Node imports.
- Keep one gRPC-focused smoke or integration path that confirms proto-backed requests still resolve correctly.
Out of scope:
- OAuth token helpers
- generic network file IO
- response body archival helpers
## Candidate: Plugin Discovery Boundary
Status: candidate
Purpose:
- Move plugin discovery and plugin file access onto a privileged boundary without coupling the work to templating bootstrap or unrelated runtime refactors.
Single feature scope:
- Plugin discovery, metadata loading, and plugin file access.
Primary files:
- `src/plugins/context/response.ts`
- `src/plugins/create.ts`
- `src/plugins/index.ts`
- `src/utils/plugin.ts`
- any preload or IPC additions needed for plugin discovery and metadata handoff
Implementation notes:
- Keep plugin metadata and UI-facing types renderer-safe.
- Move plugin discovery and plugin file traversal to main.
- Avoid adding templating bootstrap, plugin install, package management, or unrelated runtime redesign to this candidate.
Expected risk: high
Quick win: no
Suggested reviewers:
- Plugins/templating
- Electron/runtime
Baseline entries to remove:
- `src/plugins/context/response.ts -> fs`
- `src/plugins/create.ts -> fs`
- `src/plugins/create.ts -> path`
- `src/plugins/index.ts -> fs`
- `src/plugins/index.ts -> path`
- `src/utils/plugin.ts -> fs`
- `src/utils/plugin.ts -> path`
Dependencies:
- Sync storage boundary foundation should land first
- Prefer after import and gRPC candidate patterns are established so plugin work can reuse the same bridge shape
Test automation plan:
- Extend plugin discovery or plugin creation unit tests around the new privileged entrypoints.
- Add a renderer contract test for plugin metadata loading so the UI path remains fast and explicit.
- Keep one plugin-focused smoke path to prove discovery still works end to end.
Out of scope:
- templating bootstrap
- package installation and lifecycle management
- generic network cleanup
- import persistence changes
## Candidate: Templating Bootstrap Boundary
Status: candidate
Purpose:
- Move templating bootstrap helpers off the renderer path without coupling the work to plugin discovery or broader templating redesign.
Single feature scope:
- Templating startup and privileged helper access.
Primary files:
- `src/templating/base-extension.ts`
- any preload or IPC additions needed for templating bootstrap helpers
Implementation notes:
- Keep template evaluation behavior unchanged while moving privileged helper access behind explicit main-process entrypoints.
- Avoid folding plugin discovery or plugin package management into this candidate.
- Prefer to reuse any plugin-side metadata bridge rather than inventing parallel surfaces if the contracts line up cleanly.
Expected risk: medium
Quick win: maybe
Suggested reviewers:
- Plugins/templating
- Electron/runtime
Baseline entries to remove:
- `src/templating/base-extension.ts -> crypto`
- `src/templating/base-extension.ts -> os`
Dependencies:
- Prefer after plugin discovery boundary if templating still relies on shared plugin bootstrap behavior
Test automation plan:
- Extend templating-focused unit tests around the new privileged entrypoints.
- Add a renderer contract test for templating bootstrap helpers.
- Keep one templating smoke path to prove the startup flow still works end to end.
Out of scope:
- plugin discovery
- package installation and lifecycle management
- generic network cleanup
## Candidate: OAuth Token Crypto Cleanup
Status: candidate
Purpose:
- Remove isolated OAuth crypto usage from renderer-reachable code without pulling in unrelated network or storage work.
Single feature scope:
- OAuth token helper crypto operations.
Primary files:
- `src/network/o-auth-1/get-token.ts`
- `src/network/o-auth-2/get-token.ts`
- `src/network/o-auth-2/utils.ts`
Implementation notes:
- Treat this as a small privileged-helper cleanup, not a broad request-execution refactor.
- Prefer a narrow bridge for hashing, PKCE, and token helper crypto rather than a generic network bridge.
- Keep request sending and response handling out of scope.
Expected risk: low to medium
Quick win: yes
Suggested reviewers:
- Network/gRPC
- Electron/runtime
Baseline entries to remove:
- `src/network/o-auth-1/get-token.ts -> crypto`
- `src/network/o-auth-2/get-token.ts -> crypto`
- `src/network/o-auth-2/utils.ts -> crypto`
Dependencies:
- none
Test automation plan:
- Extend OAuth unit tests around PKCE and token helper behavior.
- Add a renderer contract test for any new crypto bridge surface.
- Keep this candidate free of smoke-test expansion unless an existing auth smoke path already covers the flow.
Out of scope:
- generic request execution
- response archival
- import or sync persistence
## Candidate: Response Archival and Compression Boundary
Status: candidate
Purpose:
- Isolate response body archival and compression so renderer-safe helpers stop owning file and compression concerns.
Single feature scope:
- Response archival, file writes, and compression helpers.
Primary files:
- `src/models/helpers/response-operations.ts`
Implementation notes:
- Split pure response metadata shaping from privileged file-write and compression behavior.
- Avoid bundling this candidate with sync compression just because both touch compression.
- Reuse existing preload APIs if they are already close to the needed shape.
Expected risk: medium
Quick win: maybe
Suggested reviewers:
- Network/gRPC
- Electron/runtime
Baseline entries to remove:
- `src/models/helpers/response-operations.ts -> fs`
- `src/models/helpers/response-operations.ts -> zlib`
Dependencies:
- none, though sync-storage patterns may provide a good template
Test automation plan:
- Extend unit tests for response archival helpers around the boundary split.
- Add a renderer contract test for any file-write bridge used by response exports.
- Keep one focused integration path for exporting or persisting a response body.
Out of scope:
- sync store compression
- generic request execution
- plugin or templating work
## Candidate: Script Executor Boundary
Status: candidate
Purpose:
- Move `script-executor.ts` file access behind a privileged boundary without broadening the work into general scripting or plugin changes.
Single feature scope:
- Script execution file access.
Primary files:
- `src/script-executor.ts`
Implementation notes:
- Keep the candidate narrowly focused on the file-access boundary.
- Do not combine this with plugin loading or broader execution-runtime redesign.
- Reuse existing bridge patterns from sync or response file operations where possible.
Expected risk: medium
Quick win: maybe
Suggested reviewers:
- Electron/runtime
- Sync/storage
Baseline entries to remove:
- `src/script-executor.ts -> fs/promises`
Dependencies:
- none
Test automation plan:
- Extend targeted script-executor tests around the file-access boundary.
- Add a renderer contract test if a new bridge surface is introduced.
- Avoid growing this into a smoke-heavy candidate unless an existing script-execution smoke path already exists.
Out of scope:
- plugin runtime redesign
- sync store persistence
- response archival
## Candidate: Baseline Ratchet Follow-Ups
Purpose:
- Keep the baseline moving downward as soon as functional PRs merge.
Primary files:
- `config/renderer-node-import-baseline.json`
- `.reports/renderer-node-imports.json`
Implementation notes:
- Re-run `npm run update:renderer-node-import-baseline` after each offender-removing PR.
- Confirm the baseline only drops entries already removed from the analyzer output.
Expected risk: low
Suggested reviewers:
- whoever reviewed the functional PR
Dependencies:
- any PR that removes offenders
Concurrent with:
- usually folded into the offender-removing PR rather than done separately
## Suggested Candidate Sequencing
1. Sync storage boundary foundation
2. Import parsing and persistence boundary
3. gRPC proto asset boundary
4. Plugin discovery boundary
5. Templating bootstrap boundary
6. OAuth token crypto cleanup
7. Response archival and compression boundary
8. Script executor boundary
Notes:
- PR 1, PR 2, and PR 3 should produce the fastest visible reduction in route and helper debt.
- Sync storage remains the dependency-clearing candidate because import should not move until sync storage is on the privileged side of the boundary.
- gRPC proto asset work and OAuth token crypto cleanup look like the clearest quick-win candidates.
- Plugin discovery and templating are intentionally separate candidates. If they naturally converge later, that should be a conscious decision rather than the default plan.
- If a candidate starts collecting unrelated offenders, split it again instead of broadening it.
## Ownership Template
For each PR, capture the following in the PR description:
- Purpose
- Single feature scope
- Implementation notes
- Files in scope
- Baseline entries expected to be removed
- Test automation plan
- Any new preload or IPC surface added
- Any deliberate deferrals to later PRs
## Exit Criteria
This migration is complete when:
1. The analyzer report no longer contains renderer Node builtin imports that are not explicitly allowed.
2. The baseline file is empty or reduced to intentionally permitted entries.
3. Lint restrictions can be tightened by removing temporary offender exclusions.
4. The main BrowserWindow runs with `nodeIntegration: false` without renderer regressions.
5. Security audit of changes is complete, including the writeResponseBodyToFile preload function.
1. The analyzer report contains no renderer Node builtin imports outside explicit allow-list entries.
2. `config/renderer-node-import-baseline.json` is empty or only contains intentionally permitted entries.
3. The main BrowserWindow runs with `nodeIntegration: false` without renderer regressions.
4. Security audit of any new preload surface introduced during the migration is complete.
5. Stretch follow-up: tighten `vite.config.ts` to fail rather than baseline; remove the baseline file entirely.
@@ -16,14 +16,6 @@
"importer": "../insomnia-testing/src/run/run.ts",
"builtin": "path"
},
{
"importer": "src/network/network.ts",
"builtin": "fs"
},
{
"importer": "src/network/network.ts",
"builtin": "path"
},
{
"importer": "src/plugins/context/response.ts",
"builtin": "fs"
+21 -3
View File
@@ -105,7 +105,11 @@ export default async function build(options: Options) {
platform: 'node',
sourcemap: true,
format: 'cjs',
define: env,
define: {
...env,
// Electron main = "browser"
'process.type': '"browser"',
},
external: [
'electron',
'@getinsomnia/node-libcurl',
@@ -215,7 +219,14 @@ export default async function build(options: Options) {
const hiddenWindowPreloadWatch = await hiddenPreloadContext.watch();
const pluginWindowWatch = await pluginWindowContext.watch();
const pluginWindowPreloadWatch = await pluginWindowPreloadContext.watch();
return Promise.all([preloadWatch, hiddenWindowPreloadWatch, mainWatch, hiddenWindowWatch, pluginWindowWatch, pluginWindowPreloadWatch]);
return Promise.all([
preloadWatch,
hiddenWindowPreloadWatch,
mainWatch,
hiddenWindowWatch,
pluginWindowWatch,
pluginWindowPreloadWatch,
]);
}
const preload = esbuild.build(preloadBuildOptions);
const hiddenBrowserWindow = esbuild.build(hiddenBrowserWindowBuildOptions);
@@ -223,7 +234,14 @@ export default async function build(options: Options) {
const pluginWindow = esbuild.build(pluginWindowBuildOptions);
const pluginWindowPreload = esbuild.build(pluginWindowPreloadBuildOptions);
const main = esbuild.build(mainBuildOptions);
return Promise.all([main, preload, hiddenBrowserWindow, hiddenBrowserWindowPreload, pluginWindow, pluginWindowPreload]).catch(err => {
return Promise.all([
main,
preload,
hiddenBrowserWindow,
hiddenBrowserWindowPreload,
pluginWindow,
pluginWindowPreload,
]).catch(err => {
console.error('[Build] Build failed:', err);
});
}
+5
View File
@@ -388,6 +388,11 @@ const main: Window['main'] = {
},
notifyPluginPromptResult: (id: string, value: string | null) =>
ipcRenderer.send('plugins.uiPromptResult', { id, value }),
timeline: {
getPath: (responseId: string) => invokeWithNormalizedError('timeline.getPath', responseId) as Promise<string>,
appendToFile: (options: { timelinePath: string; data: string }) =>
invokeWithNormalizedError('timeline.appendToFile', options),
},
};
ipcRenderer.on('hidden-browser-window-response-listener', event => {
@@ -163,6 +163,8 @@ export type HandleChannels =
| 'webSocket.event.send'
| 'webSocket.open'
| 'webSocket.readyState'
| 'timeline.appendToFile'
| 'timeline.getPath'
| 'writeFile'
| 'writeResponseBodyToFile';
+41
View File
@@ -136,6 +136,40 @@ const writeResponseBodyToFile = async (
}
};
const getResponsesDir = () =>
path.join(process.env.INSOMNIA_DATA_PATH || app.getPath('userData'), 'responses');
const responsesDirCreated = new Set<string>();
const getTimelinePath = (_: unknown, responseId: string) => {
const base = path.resolve(getResponsesDir());
const target = path.resolve(base, responseId + '.timeline');
const relative = path.relative(base, target);
if (relative.startsWith('..') || path.isAbsolute(relative)) {
throw new Error('Invalid response ID');
}
return target;
};
const appendToTimeline = async (
_: unknown,
options: { timelinePath: string; data: string },
) => {
const allowedResponsesDir = getResponsesDir();
const resolvedPath = path.resolve(options.timelinePath);
if (!resolvedPath.startsWith(path.resolve(allowedResponsesDir) + path.sep) || !resolvedPath.endsWith('.timeline')) {
throw new Error(
'appendToTimeline: timelinePath is outside the allowed responses directory or does not end in .timeline',
);
}
const dir = path.dirname(resolvedPath);
if (!responsesDirCreated.has(dir)) {
await fs.promises.mkdir(dir, { recursive: true });
responsesDirCreated.add(dir);
}
await fs.promises.appendFile(resolvedPath, options.data);
};
export interface RendererToMainBridgeAPI {
loginStateChange: () => void;
openInBrowser: (url: string) => void;
@@ -243,6 +277,10 @@ export interface RendererToMainBridgeAPI {
syncNewWorkspaceIfNeeded: typeof syncNewWorkspaceIfNeeded;
plugins: PluginsBridgeAPI;
notifyPluginPromptResult: (id: string, value: string | null) => void;
timeline: {
getPath: (responseId: string) => Promise<string>;
appendToFile: (options: { timelinePath: string; data: string }) => Promise<void>;
};
}
export function registerMainHandlers() {
@@ -677,5 +715,8 @@ export function registerMainHandlers() {
});
});
ipcMainHandle('timeline.getPath', getTimelinePath);
ipcMainHandle('timeline.appendToFile', appendToTimeline);
registerPluginIpcHandlers();
}
@@ -3,7 +3,7 @@ import fs from 'node:fs';
import nodePath from 'node:path';
import { CurlHttpVersion, CurlNetrc } from '@getinsomnia/node-libcurl';
import { beforeEach, describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { models, services } from '~/insomnia-data';
@@ -11,7 +11,8 @@ import { CONTENT_TYPE_FILE, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED
import { filterHeaders } from '../../common/misc';
import { getRenderedRequestAndContext } from '../../common/render';
import { HttpVersions } from '../../common/settings';
import { _parseHeaders, getHttpVersion } from '../../main/network/libcurl-promise';
import { getAuthHeader } from '../../main/network/get-auth-header';
import { _parseHeaders, curlRequest, getHttpVersion } from '../../main/network/libcurl-promise';
import { _getAwsAuthHeaders } from '../../network/parse-header-strings';
import { DEFAULT_BOUNDARY } from '../multipart-constants';
import * as networkUtils from '../network';
@@ -36,6 +37,20 @@ describe('getAuthQueryParams', () => {
});
});
describe('sendCurlAndWriteTimeline()', () => {
beforeEach(() => {
vi.stubGlobal('window', {
main: {
timeline: {
getPath: (responseId: string) => Promise.resolve(`/tmp/${responseId}.timeline`),
appendToFile: vi.fn().mockResolvedValue(null),
},
getAuthHeader,
curlRequest,
cancelCurlRequest: vi.fn(),
},
});
});
it('sends a generic request', async () => {
const workspace = await services.workspace.create();
const settings = await services.settings.getOrCreate();
@@ -24,12 +24,6 @@ Object.defineProperty(globalThis, 'window', {
configurable: true,
});
Object.defineProperty(process, 'type', {
value: 'renderer',
writable: true,
configurable: true,
});
import { _applyRequestPluginHooks, _applyResponsePluginHooks } from '../network';
const mockRenderedRequest = {
@@ -0,0 +1,93 @@
import fs from 'node:fs';
import nodePath from 'node:path';
import clone from 'clone';
import type { RequestHeader } from '~/insomnia-data';
import type { RenderedRequest } from '~/templating/types';
import { getAuthHeader as getAuthHeaderFromMain } from '../main/network/get-auth-header';
import type { CurlRequestOptions, CurlRequestOutput, ResponsePatch } from '../main/network/libcurl-promise';
import { curlRequest } from '../main/network/libcurl-promise';
import * as pluginApp from '../plugins/context/app';
import * as pluginData from '../plugins/context/data';
import * as pluginNetwork from '../plugins/context/network';
import * as pluginRequest from '../plugins/context/request';
import * as pluginResponse from '../plugins/context/response';
import * as pluginStore from '../plugins/context/store';
export const getTimelinePath = async (responseId: string): Promise<string> => {
const electron = require('electron') as { app: { getPath: (name: string) => string } };
const dataDir = process.env['INSOMNIA_DATA_PATH'] || electron.app.getPath('userData');
const base = nodePath.resolve(dataDir, 'responses');
const target = nodePath.resolve(base, responseId + '.timeline');
const relative = nodePath.relative(base, target);
if (relative.startsWith('..') || nodePath.isAbsolute(relative)) {
throw new Error('Invalid response ID');
}
return target;
};
export const appendToTimelineOnError = (timelinePath: string, data: string): Promise<void> =>
fs.promises.appendFile(timelinePath, data);
export const appendTimelineLines = (timelinePath: string, logs: string[]): Promise<void> =>
fs.promises.appendFile(timelinePath, logs.join('\n'));
export const getAuthHeader = (r: RenderedRequest, u: string): Promise<RequestHeader | undefined> =>
getAuthHeaderFromMain(r, u);
export const executeCurlRequest = (options: CurlRequestOptions): Promise<CurlRequestOutput> =>
curlRequest(options);
export async function applyRequestHooks(
newRenderedRequest: RenderedRequest,
renderedContext: Record<string, any>,
): Promise<RenderedRequest> {
const pluginIndex = require('../plugins/index');
for (const { plugin, hook } of await pluginIndex.getRequestHooks()) {
const context = {
...(pluginApp.init() as Record<string, any>),
...pluginData.init(renderedContext.getProjectId()),
...(pluginStore.init(plugin) as Record<string, any>),
...(pluginRequest.init(newRenderedRequest, renderedContext) as Record<string, any>),
...(pluginNetwork.init() as Record<string, any>),
};
try {
await hook(context);
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
(error as any).plugin = plugin;
throw error;
}
}
return newRenderedRequest;
}
export async function applyResponseHooks(
response: ResponsePatch,
renderedRequest: RenderedRequest,
renderedContext: Record<string, any>,
): Promise<ResponsePatch> {
const newResponse = clone(response);
const newRequest = clone(renderedRequest);
const pluginIndex = require('../plugins/index');
for (const { plugin, hook } of await pluginIndex.getResponseHooks()) {
const context = {
...(pluginApp.init() as Record<string, any>),
...pluginData.init(renderedContext.getProjectId()),
...(pluginStore.init(plugin) as Record<string, any>),
...(pluginResponse.init(newResponse) as Record<string, any>),
...(pluginRequest.init(newRequest, renderedContext, true) as Record<string, any>),
...(pluginNetwork.init() as Record<string, any>),
};
try {
await hook(context);
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
(error as any).plugin = plugin;
throw error;
}
}
return newResponse;
}
@@ -0,0 +1,51 @@
import type { RequestHeader } from '~/insomnia-data';
import { plugins as pluginsBridge } from '~/plugins/renderer-bridge';
import type { RenderedRequest } from '~/templating/types';
import type { CurlRequestOptions, ResponsePatch } from '../main/network/libcurl-promise';
import { cancellableCurlRequest } from './cancellation';
export const getTimelinePath = (responseId: string): Promise<string> =>
window.main.timeline.getPath(responseId);
export const appendToTimelineOnError = (timelinePath: string, data: string): Promise<void> =>
window.main.timeline.appendToFile({ timelinePath, data });
export const appendTimelineLines = (timelinePath: string, logs: string[]): Promise<void> =>
window.main.timeline.appendToFile({ timelinePath, data: logs.join('\n') });
export const getAuthHeader = (r: RenderedRequest, u: string): Promise<RequestHeader | undefined> =>
window.main.getAuthHeader(r, u);
export const executeCurlRequest = (options: CurlRequestOptions) =>
cancellableCurlRequest(options);
export async function applyRequestHooks(
newRenderedRequest: RenderedRequest,
renderedContext: Record<string, any>,
): Promise<RenderedRequest> {
if (!await pluginsBridge.hasRequestHooks()) {
return newRenderedRequest;
}
return pluginsBridge.applyRequestHooks({
renderedRequest: newRenderedRequest,
projectId: renderedContext.getProjectId(),
environment: renderedContext,
});
}
export async function applyResponseHooks(
response: ResponsePatch,
renderedRequest: RenderedRequest,
renderedContext: Record<string, any>,
): Promise<ResponsePatch> {
if (!await pluginsBridge.hasResponseHooks()) {
return response;
}
return pluginsBridge.applyResponseHooks({
response,
renderedRequest,
projectId: renderedContext.getProjectId(),
environment: renderedContext,
});
}
@@ -0,0 +1,20 @@
// Runtime adapter selection: renderer uses IPC bridge, node uses libcurl directly.
// Vite production inlines process.type='renderer' so Rollup tree-shakes the node branch.
import type * as AdapterType from './network-adapter.renderer';
const impl = (
(process as any).type === 'renderer'
? require('./network-adapter.renderer')
: require(/* @vite-ignore */ './network-adapter.node')
) as typeof AdapterType;
export const {
getTimelinePath,
appendToTimelineOnError,
appendTimelineLines,
getAuthHeader,
executeCurlRequest,
applyRequestHooks,
applyResponseHooks,
} = impl;
+22 -106
View File
@@ -1,6 +1,3 @@
import fs from 'node:fs';
import nodePath from 'node:path';
import clone from 'clone';
import orderedJSON from 'json-order';
@@ -25,7 +22,15 @@ import type {
Workspace,
} from '~/insomnia-data';
import { EnvironmentType, models, services } from '~/insomnia-data';
import { plugins as pluginsBridge } from '~/plugins/renderer-bridge';
import {
appendTimelineLines,
appendToTimelineOnError,
applyRequestHooks,
applyResponseHooks,
executeCurlRequest,
getAuthHeader,
getTimelinePath,
} from '~/network/network-adapter';
import { getKVPairFromData } from '~/utils/environment-utils';
import type {
@@ -40,13 +45,7 @@ import { generateId, getContentTypeHeader, getLocationHeader, getSetCookieHeader
import { getRenderedRequestAndContext } from '../common/render';
import { ascendingFirstIndexStringSort } from '../common/sorting';
import type { HeaderResult, ResponsePatch, ResponseTimelineEntry } from '../main/network/libcurl-promise';
import * as pluginApp from '../plugins/context/app';
import * as pluginData from '../plugins/context/data';
import * as pluginNetwork from '../plugins/context/network';
import * as pluginRequest from '../plugins/context/request';
import * as pluginResponse from '../plugins/context/response';
import * as pluginStore from '../plugins/context/store';
import * as plugins from '../plugins/index';
import { RenderError } from '../templating/render-error';
import type { RenderedRequest, RenderPurpose } from '../templating/types';
import { maskOrDecryptVaultDataIfNecessary } from '../templating/utils';
@@ -55,7 +54,7 @@ import { serializeNDJSON } from '../utils/ndjson';
import { buildQueryStringFromParams, joinUrlAndQueryString, smartEncodeUrl } from '../utils/url/querystring';
import { QUERY_PARAMS } from './api-key/constants';
import { getAuthObjectOrNull, isAuthEnabled } from './authentication';
import { cancellableCurlRequest, cancellableRunScript } from './cancellation';
import { cancellableRunScript } from './cancellation';
import { filterClientCertificates } from './certificate';
import { runScriptConcurrently, type TransformedExecuteScriptContext } from './concurrency';
import { addSetCookiesToToughCookieJar } from './set-cookie-util';
@@ -63,6 +62,7 @@ import { addSetCookiesToToughCookieJar } from './set-cookie-util';
const { isRequest } = models.request;
const { isRequestGroup } = models.requestGroup;
export interface SendActionRuntime {
appendTimeline: (timelinePath: string, logs: string[]) => Promise<void>;
}
@@ -155,11 +155,7 @@ export const fetchRequestGroupData = async (requestGroupId: string) => {
const clientCertificates = await services.clientCertificate.findByParentId(workspaceId);
const caCert = await services.caCertificate.getByParentId(workspaceId);
const responseId = generateId('res');
const responsesDir = nodePath.join(
(process.type === 'renderer' ? window : require('electron')).app.getPath('userData'),
'responses',
);
const timelinePath = nodePath.join(responsesDir, responseId + '.timeline');
const timelinePath = await getTimelinePath(responseId);
return { environment, settings, clientCertificates, caCert, activeEnvironmentId, timelinePath, responseId };
};
@@ -222,12 +218,7 @@ export const fetchRequestData = async (
const caCert = await services.caCertificate.getByParentId(workspaceId);
const responseId = generateId('res');
const responsesDir = nodePath.join(
process.env['INSOMNIA_DATA_PATH'] ||
(process.type === 'renderer' ? window : require('electron')).app.getPath('userData'),
'responses',
);
const timelinePath = nodePath.join(responsesDir, responseId + '.timeline');
const timelinePath = await getTimelinePath(responseId);
return {
request,
@@ -266,12 +257,7 @@ export const fetchMcpRequestData = async (mcpRequestId: string) => {
invariant(settings, 'failed to create settings');
const responseId = generateId('res');
const responsesDir = nodePath.join(
process.env['INSOMNIA_DATA_PATH'] ||
(process.type === 'renderer' ? window : require('electron')).app.getPath('userData'),
'responses',
);
const timelinePath = nodePath.join(responsesDir, responseId + '.timeline');
const timelinePath = await getTimelinePath(responseId);
return {
environment,
@@ -667,7 +653,7 @@ const tryToExecuteScript = async (context: RequestAndContextAndOptionalResponse)
parentFolders: output.parentFolders,
};
} catch (err) {
await fs.promises.appendFile(
await appendToTimelineOnError(
timelinePath,
serializeNDJSON([{ value: err.message, name: 'Text', timestamp: Date.now() }]),
);
@@ -878,11 +864,7 @@ export async function sendCurlAndWriteTimeline(
if (!renderedRequest.settingSendCookies) {
timeline.push({ value: 'Disable cookie sending due to user setting', name: 'Text', timestamp: Date.now() });
}
const getRenderedRequestAuthHeader =
process.type === 'renderer'
? (r: RenderedRequest, u: string) => window.main.getAuthHeader(r, u)
: (await import('../main/network/get-auth-header')).getAuthHeader;
const authHeader = await getRenderedRequestAuthHeader(renderedRequest, finalUrl);
const authHeader = await getAuthHeader(renderedRequest, finalUrl);
const requestOptions = {
requestId,
req: renderedRequest,
@@ -894,12 +876,7 @@ export async function sendCurlAndWriteTimeline(
authHeader,
};
// NOTE: conditionally use ipc bridge, renderer cannot import native modules directly
const nodejsCurlRequest =
process.type === 'renderer'
? cancellableCurlRequest
: (await import('../main/network/libcurl-promise')).curlRequest;
const output = await nodejsCurlRequest(requestOptions);
const output = await executeCurlRequest(requestOptions);
if ('error' in output) {
if (runtime) {
@@ -1090,35 +1067,7 @@ export async function _applyRequestPluginHooks(renderedRequest: RenderedRequest,
}
}
if (process.type !== 'renderer') {
for (const { plugin, hook } of await plugins.getRequestHooks()) {
const context = {
...(pluginApp.init() as Record<string, any>),
...pluginData.init(renderedContext.getProjectId()),
...(pluginStore.init(plugin) as Record<string, any>),
...(pluginRequest.init(newRenderedRequest, renderedContext) as Record<string, any>),
...(pluginNetwork.init() as Record<string, any>),
};
try {
await hook(context);
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
(error as any).plugin = plugin;
throw error;
}
}
return newRenderedRequest;
}
if (!await pluginsBridge.hasRequestHooks()) {
return newRenderedRequest;
}
return pluginsBridge.applyRequestHooks({
renderedRequest: newRenderedRequest,
projectId: renderedContext.getProjectId(),
environment: renderedContext,
});
return applyRequestHooks(newRenderedRequest, renderedContext);
}
export async function _applyResponsePluginHooks(
@@ -1127,39 +1076,7 @@ export async function _applyResponsePluginHooks(
renderedContext: Record<string, any>,
): Promise<ResponsePatch> {
try {
if (process.type !== 'renderer') {
const newResponse = clone(response);
const newRequest = clone(renderedRequest);
for (const { plugin, hook } of await plugins.getResponseHooks()) {
const context = {
...(pluginApp.init() as Record<string, any>),
...pluginData.init(renderedContext.getProjectId()),
...(pluginStore.init(plugin) as Record<string, any>),
...(pluginResponse.init(newResponse) as Record<string, any>),
...(pluginRequest.init(newRequest, renderedContext, true) as Record<string, any>),
...(pluginNetwork.init() as Record<string, any>),
};
try {
await hook(context);
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
(error as any).plugin = plugin;
throw error;
}
}
return newResponse;
}
if (!await pluginsBridge.hasResponseHooks()) {
return response;
}
return await pluginsBridge.applyResponseHooks({
response,
renderedRequest,
projectId: renderedContext.getProjectId(),
environment: renderedContext,
});
return await applyResponseHooks(response, renderedRequest, renderedContext);
} catch (err) {
console.log('[plugin] Response hook failed', err, response);
return {
@@ -1172,8 +1089,7 @@ export async function _applyResponsePluginHooks(
};
}
}
export const defaultSendActionRuntime = {
appendTimeline: async (timelinePath: string, logs: string[]) => {
await fs.promises.appendFile(timelinePath, logs.join('\n'));
},
export const defaultSendActionRuntime: SendActionRuntime = {
appendTimeline: appendTimelineLines,
};
+3
View File
@@ -52,6 +52,9 @@ export default defineConfig(({ mode }) => {
},
resolve: {
alias: {
// Resolve network-adapter to the renderer variant so both client and server
// builds inline the module directly (avoids runtime require() in server bundle).
'~/network/network-adapter': path.resolve(__dirname, './src/network/network-adapter.renderer'),
'~': path.resolve(__dirname, './src'),
},
},
+1
View File
@@ -11,6 +11,7 @@ export default defineConfig({
},
exclude: ['src/routes/**.*.tsx', '.react-router', 'node_modules'],
alias: {
'~/network/network-adapter': path.resolve(__dirname, './src/network/network-adapter.renderer'),
'~': path.resolve(__dirname, './src'),
'electron/main': 'electron',
},