diff --git a/.claude/skills/fix-scripting-feature/SKILL.md b/.claude/skills/fix-scripting-feature/SKILL.md new file mode 100644 index 00000000000..1560c356349 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/SKILL.md @@ -0,0 +1,120 @@ +--- +name: fix-scripting-feature +description: 'Debug and fix issues in Insomnia''s pre-request/after-response/test scripting feature (the pm/insomnia API surface). Use when scripts throw sandbox violations, timeouts, module import errors, or produce wrong request/response/variable mutations.' +argument-hint: 'Provide the failing script snippet, the error message, and whether it happened in the app (hidden window) or inso (CLI)' +--- + +# Fix Scripting Feature Issues + +## When to Use + +- A pre-request script, after-response script, or test script (`pm.*` / `insomnia.*` API) fails, throws, or produces incorrect results. +- Errors like `SECURITY_POLICY_VIOLATION`, `no module is found for "..."`, `Timeout: Running script took too long`, or `insomnia object is invalid or script returns earlier than expected.` +- `insomnia-scripting-environment` SDK objects (`Request`, `Response`, `Environment`, `Variables`, `Collection`, `test()`) behave unexpectedly. + +## Architecture (read this first) + +Pre-request/after-response/test scripts (the Postman-compatible `pm`/`insomnia` API) have **three execution paths** depending on where the script runs: + +| Path | Entry point | Sandboxing | +|---|---|---| +| Electron app (default) | `packages/insomnia/src/entry.hidden-window.ts` → `packages/insomnia/src/scripting/run-script.ts`, run in a hidden `BrowserWindow` (see `packages/insomnia/src/main/window-utils.ts`) | AST-based via `packages/insomnia/src/scripting/sandbox.ts` + `script-security-rules.ts` + `require-interceptor.ts`, toggled by `context.settings.scriptSandboxEnabled` (default on) | +| Electron app, opt-in QuickJS sandbox | `packages/insomnia/src/network/concurrency.renderer.ts` → `packages/insomnia/src/scripting/run-script-quickjs.ts`, run in a Web Worker over a WASM QuickJS VM (`quickjs-script-engine.ts`) | Gated by `settings.useQuickJsScriptSandbox` (default off). PoC with a **deliberately minimal, hand-rolled `insomnia`/`$` API** — not the shared SDK below — with no `test()`/`pm.test()`, `collectionVariables`, vault, cookies, client certificates, or request mutation, and a `sendRequest()` limited to plain-text bodies | +| `inso` CLI and other Node-runtime callers (e.g. `packages/insomnia/src/runtimes/network/network-adapter.node.ts`) | `packages/insomnia/src/script-executor.ts` (plain `AsyncFunction`/`eval`, no Electron) | None — `script-executor.ts` never calls `sandbox.ts`'s `prepareSandbox`, it's a structurally separate implementation. (Don't confuse this with `canSandbox = !!process.type` in `network-adapter.node.ts` — that gates **plugin** request/response hook sandboxing, an unrelated subsystem.) | + +The full SDK object model (`InsomniaObject`, `Request`, `Response`, `Environment`, `Variables`, `Collection`, `Test`, console/send-request shims) lives in `packages/insomnia-scripting-environment/src/objects/` and is shared across the Electron (default) and `inso`/Node paths only — the QuickJS sandbox does not use it. + +## `insomnia-scripting-environment` Folder Hierarchy + +The SDK package (`packages/insomnia-scripting-environment/`) is where the `pm`/`insomnia` object model lives — start here for any bug about a specific script API's behavior: + +``` +insomnia-scripting-environment/ +├── src/ +│ ├── objects/ # the pm/insomnia API surface — one file per SDK concept +│ │ ├── index.ts # public re-exports +│ │ ├── insomnia.ts # InsomniaObject — the top-level `insomnia`/`pm` instance scripts see +│ │ ├── interfaces.ts # RequestContext, IEnvironment — the context shape passed in/out of a script run +│ │ ├── execution.ts # Execution — pm.execution (setNextRequest, skipRequest, location) +│ │ ├── request.ts # Request, RequestBody — pm.request +│ │ ├── request-info.ts # RequestInfo — pm.info (event name, iteration data) +│ │ ├── response.ts # Response — pm.response +│ │ ├── send-request.ts # pm.sendRequest() implementation +│ │ ├── environments.ts # Environment, Variables — pm.environment / pm.globals +│ │ ├── variables.ts # Variable, VariableList — underlying key/value model shared by environments/collection +│ │ ├── collection.ts # Collection — pm.collectionVariables / folder & collection variable resolution +│ │ ├── folders.ts # Folder, ParentFolders — collection folder hierarchy walked for variable resolution +│ │ ├── properties.ts # PropertyBase, PropertyList — base classes most SDK objects extend +│ │ ├── headers.ts # Header, HeaderList +│ │ ├── cookies.ts # Cookie, CookieJar, CookieList — pm.cookies +│ │ ├── auth.ts # RequestAuth — pm.request.auth +│ │ ├── certificates.ts # Certificate — client cert modeling +│ │ ├── proxy-configs.ts # ProxyConfig, ProxyConfigList +│ │ ├── urls.ts # Url, QueryParam, UrlMatchPattern — URL parsing/manipulation +│ │ ├── console.ts # Console — console.log capture surfaced back to the app +│ │ ├── test.ts # pm.test()/test() handler registration + TestHandler, waitForAllTestsDone +│ │ ├── async-objects.ts # ProxiedPromise — Promise plumbing so async script code can be awaited by the host +│ │ ├── interpolator.ts # template-tag ({{ }}) interpolation used when resolving variable values +│ │ ├── utils.ts # misc helpers (e.g. checkIfUrlIncludesTag) +│ │ └── __tests__/ # tests for selected objects above (request.test.ts, response.test.ts, etc.) +│ └── autocomplete-snippets.json # generated editor autocomplete data — see scripts/generate-autocomplete.ts +├── scripts/ +│ └── generate-autocomplete.ts # regenerates autocomplete-snippets.json from the objects/ source (CI-checked) +├── docs/ # TypeDoc-generated API reference (generated from objects/, don't hand-edit) +├── typedoc.json # TypeDoc config for docs/ generation +└── vitest.config.ts # test runner config for `npm test -w insomnia-scripting-environment` +``` + +If a bug is about *how* a script executes (sandboxing, timeouts, hidden window vs. `inso`), look in `packages/insomnia/src/scripting/` (see Architecture above). If it's about *what a specific API returns or mutates* (`pm.request.headers`, `pm.environment.get()`, `pm.response.json()`, etc.), it's almost always in this package's `src/objects/`. + +For property/method-level detail on any single object above (real signatures, script-facing surface, gotchas), see `references/objects/.md` in this skill folder — one file per source file in `src/objects/` (e.g. `references/objects/request.md` for `request.ts`, `references/objects/environments.md` for `environments.ts`). + +## Procedure + +1. **Identify the execution path first** — ask/check whether the failure is in the Electron app or `inso`. Fixes and even error messages differ per path (see table above). +2. **Reproduce with the narrowest test** before touching app code: + - Unit tests for the sandbox/security logic: + ```bash + npm test -w insomnia -- src/scripting + ``` + (not `npx vitest run src/scripting -w insomnia` — Vitest's own CLI already owns `-w`/`--watch`, so that form drops into watch mode instead of selecting the `insomnia` workspace.) + Relevant files: `packages/insomnia/src/scripting/__tests__/sandbox.test.ts`, `__tests__/script-security-policy.test.ts`, `__tests__/require-interceptor.test.ts`. + - SDK object-model unit tests: + ```bash + npm test -w insomnia-scripting-environment + ``` + - Full app unit suite (if narrowing further isn't obvious): + ```bash + npm test -w insomnia + ``` +3. **If the bug only shows up end-to-end** (real app, real request lifecycle), run the Playwright smoke tests: + ```bash + npm run test:dev -w insomnia-smoke-test -- --grep "pre-request" + npm run test:dev -w insomnia-smoke-test -- --grep "after-response" + ``` + Relevant specs in `packages/insomnia-smoke-test/tests/smoke/`: `pre-request-script-features.test.ts`, `after-response-script-features.test.ts`, `pre-request-script-window.test.ts`. +4. **Locate the fix by symptom** (see Notes below for the mapping from error message to file). +5. **If you change the SDK's public API surface** (new/changed methods on `Request`/`Response`/`Environment`/etc. in `insomnia-scripting-environment`), regenerate autocomplete snippets — CI checks this is committed: + ```bash + npm run generate:autocomplete -w insomnia-scripting-environment + ``` + +## Notes + +- Common failure patterns and where to look: + - `SECURITY_POLICY_VIOLATION` / script blocked for using `this`, `globalThis`, `__proto__`, `constructor`, or `import` → AST rule in `packages/insomnia/src/scripting/sandbox.ts` (`checkSandboxViolations`) or the rule lists in `script-security-rules.ts`. These are Electron-app-only; `inso` has no AST sandbox at all. + - `no module is found for "..."` → the module isn't on the allowlist in `packages/insomnia/src/scripting/require-interceptor.ts`. Currently allowed: + - Full-access Node builtins: `path`, `assert`, `url`, `punycode`, `querystring`, `string_decoder`, `stream`, `events` + - Method-restricted Node builtins (some methods throw): `timers` (`setImmediate` blocked), `buffer` (`allocUnsafe`/`allocUnsafeSlow` blocked), `util` (`inherits`/`debuglog` blocked) + - Shims: `atob`, `btoa` + - External/npm modules: `ajv`, `chai`, `cheerio`, `crypto-js`, `csv-parse/lib/sync` (note: not bare `csv-parse`), `lodash` (backed by `es-toolkit/compat`), `moment`, `tv4`, `uuid`, `xml2js` + - Special-cased: `insomnia-collection` / `postman-collection` (resolves to the SDK's own `Collection` module) + + Adding a module means adding it here, not just installing the npm package. + - `Timeout: Running script took too long` (Electron app) → default 5000ms in `entry.hidden-window.ts:39` (`data.context.timeout`). + - `insomnia object is invalid or script returns earlier than expected.` → thrown when the wrapped `AsyncFunction` doesn't resolve to an `InsomniaObject` instance; check both `run-script.ts` and `script-executor.ts` since the same invariant is duplicated in each path. + - Hidden window "closed unexpectedly" / "froze" / busy errors → restart/health-check race conditions in `packages/insomnia/src/main/window-utils.ts` (`hiddenWindowIsBusy`, `createHiddenBrowserWindow`). + - Works in unit tests but not in CI → confirm `npm run generate:autocomplete -w insomnia-scripting-environment` output matches the committed `autocomplete-snippets.json`; `test.yml` diffs this file and fails the build if it's stale. +- The script behavior should be as consistent as possible with [the original implementation](https://www.postmanlabs.com/postman-collection/tutorial-concepts.html). The implementation is hosted in this [repo](https://github.com/postmanlabs/postman-collection). +- CI workflows relevant to this feature: `.github/workflows/test.yml` (unit tests + autocomplete snippet check, runs on PR/push to develop), `.github/workflows/test-e2e.yml` (Playwright smoke tests, same triggers). +- Success criteria: the targeted vitest/Playwright command passes, and if the SDK surface changed, the autocomplete snippet diff is clean. diff --git a/.claude/skills/fix-scripting-feature/references/objects/README.md b/.claude/skills/fix-scripting-feature/references/objects/README.md new file mode 100644 index 00000000000..c6ec877e1d1 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/README.md @@ -0,0 +1,46 @@ +# `insomnia-scripting-environment/src/objects` Reference + +One doc per source file in `packages/insomnia-scripting-environment/src/objects/`. Each covers: public API (real signatures), how a script actually reaches it (`pm.*`/`insomnia.*`), and gotchas found while reading the code. + +## Core / context +- [insomnia.md](insomnia.md) — `InsomniaObject`, the top-level `insomnia`/`pm` instance +- [interfaces.md](interfaces.md) — `RequestContext`, `IEnvironment` +- [execution.md](execution.md) — `Execution` (`pm.execution`) +- [request-info.md](request-info.md) — `RequestInfo` (`pm.info`) +- [async-objects.md](async-objects.md) — `ProxiedPromise` and async-task tracking +- [interpolator.md](interpolator.md) — `{{ }}` template interpolation +- [utils.md](utils.md) — misc helpers + +## Request / response +- [request.md](request.md) — `Request`, `RequestBody` (`pm.request`) +- [response.md](response.md) — `Response` (`pm.response`) +- [headers.md](headers.md) — `Header`, `HeaderList` +- [urls.md](urls.md) — `Url`, `QueryParam`, `UrlMatchPattern` + +## Environment / variables / collection +- [environments.md](environments.md) — `Environment`, `Variables` (`pm.environment` / `pm.globals`, variable resolution precedence) +- [variables.md](variables.md) — `Variable`, `VariableList` +- [collection.md](collection.md) — re-export barrel only; no `Collection` class or logic — see `environments.md`/`insomnia.md` instead +- [folders.md](folders.md) — `Folder`, `ParentFolders` +- [properties.md](properties.md) — `PropertyBase`, `PropertyList` base classes + +## Auth / cookies / certs / proxy +- [auth.md](auth.md) — `RequestAuth` (`pm.request.auth`) +- [cookies.md](cookies.md) — `Cookie`, `CookieJar`, `CookieList` (`pm.cookies`) +- [certificates.md](certificates.md) — `Certificate` (`pm.request.certificate`) +- [proxy-configs.md](proxy-configs.md) — `ProxyConfig`, `ProxyConfigList` + +## Test / console / send-request +- [test.md](test.md) — `pm.test()`/`test()` handler registration +- [console.md](console.md) — `Console` (script `console.log` capture) +- [send-request.md](send-request.md) — `pm.sendRequest()` + +## Flagged during writing (worth a closer look if you hit related symptoms) +- `insomnia.settings` always returns `undefined` in scripts (getter unconditionally returns nothing) — see `insomnia.md`. +- `pm.collectionVariables` and `pm.baseEnvironment` are the same object reference, not a copy — see `insomnia.md`/`environments.md`. +- Writes to `insomnia.info.*` (`RequestInfo`) are never read back by the script-run merge logic — silently discarded — see `request-info.md`. +- `pm.sendRequest(url, callback)` resolves (does not reject) on network/parse errors when a callback is passed — only the promise-only form rejects — see `send-request.md`. +- `Response.dataURI()` has a typo bug: emits `baseg4` instead of `base64` — see `response.md`. +- `Certificate.update()` doesn't update `disabled`, unlike the constructor — see `certificates.md`. +- `ProxyConfig` has no `key` property despite `_index = 'key'`, breaking base-class `one()`/`indexOf()`/`upsert()` lookups — see `proxy-configs.md`. +- `Vault` is read-only from scripts even when `enableVaultInScripts` is set — `set`/`unset`/`clear` always throw — see `environments.md`. diff --git a/.claude/skills/fix-scripting-feature/references/objects/async-objects.md b/.claude/skills/fix-scripting-feature/references/objects/async-objects.md new file mode 100644 index 00000000000..24f9bc44acc --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/async-objects.md @@ -0,0 +1,84 @@ +# Async plumbing (`ProxiedPromise`) + +**Source:** `packages/insomnia-scripting-environment/src/objects/async-objects.ts` + +## Purpose +Internal plumbing (not part of the `pm`/`insomnia` script-facing API by name) that lets the sandbox +track every `Promise` a user script creates, so it can drain/await fire-and-forget promises before +handing control back to the host app. This is what prevents an un-awaited `.then()` chain or a +stray `setTimeout`-driven promise inside a script from silently continuing (or erroring) after the +script has already "finished". + +## Public API + +- `export const OriginalPromise = Promise;` — a stashed reference to the real native `Promise` + class, captured at module load before anything replaces the global `Promise`. +- `export class ProxiedPromise extends Promise` — drop-in `Promise` replacement: + - `constructor(executor)` — behaves exactly like `Promise`, and additionally pushes `this` into + the module-level `scriptPromises` array if `monitoring` is `true`. + - `static override all(promises: Promise[])` — delegates to `super.all(promises)`; pushes + the resulting promise into `scriptPromises` if `monitoring`. + - `static override allSettled(promises: Promise[])` — delegates to `super.allSettled(promises)` only; does **not** push the result into `scriptPromises` (comment: "promise will be counted in Promise.resolve"). + - `static any(_: Promise[])` — **not actually overridden** (no `override` keyword, does not call `super.any`); always returns `super.reject("'super.any' not supported")`. Comment notes `Promise.any` isn't supported for the ES2021 compile target. + - `static override race(promises: Promise[])` — delegates to `super.race(promises)`; pushes the result if `monitoring`. + - `static override reject(value: any)` — delegates to `super.reject(value)`; pushes the result if `monitoring`. + - `static override resolve(value?: T | PromiseLike)` — delegates to `super.resolve(value)`; pushes the result if `monitoring`. + - `static withResolvers()` — always returns `super.reject("'Promise.withResolvers' not supported")`; same ES2021 compile-target limitation noted in a comment. +- `export const asyncTasksAllSettled = async () => { await Promise.allSettled(scriptPromises); scriptPromises = []; }` — awaits every currently-tracked promise, then clears the tracked list. +- `export const stopMonitorAsyncTasks = () => { monitoring = false; }` — disables tracking of any *new* promises created after this call. +- `export const resetAsyncTasks = async () => { scriptPromises = []; monitoring = true; }` — clears the tracked list and re-enables monitoring; called at the start of a fresh script run. + +Module-level (shared, not per-instance) state: `let monitoring = true;` and +`let scriptPromises = new Array>();`. Importing this module also calls +`resetTestPromises()` (from `./test`) once, as a side effect at load time. + +## Script-facing surface +None of these names are called directly by a user script. However, the mechanism *is* in the +script-facing surface indirectly: `packages/insomnia/src/entry.hidden-window-preload.ts` replaces +the sandbox's global `Promise` with `ProxiedPromise` +(`contextBridge.exposeInMainWorld('Promise', ProxiedPromise); window.Promise = ProxiedPromise;`), +so `new Promise(...)`, `Promise.resolve(...)`, `Promise.all(...)`, etc. — anything that references +the (now-reassigned) global `Promise` binding — is transparently tracked. **`async function` is not +in that list**: per spec, an async function's returned promise is created against the realm's +intrinsic `%Promise%` (`NewPromiseCapability(%Promise%)`), not whatever the mutable global `Promise` +binding currently points to, so reassigning `window.Promise` does not make it a `ProxiedPromise`. +`packages/insomnia/src/scripting/run-script.ts` injects +`resetAsyncTasks`/`stopMonitorAsyncTasks`/`asyncTasksAllSettled` into the executed script function +as `__bridgeReset__`/`__bridgeStop__`/`__bridgeSettle__`, calling `__bridgeReset__()` before the +user script body runs and `await __bridgeSettle__()` after it, right before returning the mutated +`insomnia` object. This drains explicitly-tracked promises, but an un-awaited `pm.sendRequest(...)` +call or stray `.then()` chain is only guaranteed to resolve before the script's result is used if +every promise in its chain was created via a tracked constructor/helper — not guaranteed just +because it originated from an `async function`. + +## Gotchas / notable behavior +- `Promise.any(...)` and `Promise.withResolvers()` are **explicitly unsupported**: calling either + from a script returns a promise that rejects with the literal string `"'super.any' not supported"` + or `"'Promise.withResolvers' not supported"` (not a real `Error` object) — worth checking for if a + script's failure message looks like that. +- `allSettled` results are deliberately *not* double-counted into `scriptPromises` — the comment + claims this is safe because "promise will be counted in Promise.resolve" (i.e. the engine's + internal `allSettled` implementation is assumed to route through `Promise.resolve` on the inputs, + which are already tracked). This is an implicit coupling to Promise/A+ implementation details. +- `scriptPromises`/`monitoring` are **module-level singletons**, not scoped per script execution — + correctness across sequential script runs depends on `resetAsyncTasks()` (`__bridgeReset__`) + being called at the start of every run; if it's ever skipped, promises from a previous run could + leak into the next run's settle-and-drain step. +- Constructing a promise while `monitoring === false` silently skips tracking — no error or warning is raised. +- Promises returned by an `async function` are **not guaranteed to be added to `scriptPromises`**, + even though `window.Promise`/`Promise` was replaced with `ProxiedPromise` — the JS engine builds + an async function's result promise from the realm's intrinsic `%Promise%`, bypassing the + reassigned global binding entirely. `send-request.ts`'s exported `sendRequest` is itself an + `async function` that internally does `return new Promise(async (resolve, reject) => {...})`; the + inner explicit `new Promise(...)` is tracked, but the promise actually returned to the caller of + `sendRequest(...)` (i.e. what `pm.sendRequest()` hands back to a script when not passed a callback) + is not. Symptom: a script that fires `pm.sendRequest(...)` without `await`ing it (or awaiting a + `.then()` derived from it) may see the request silently abandoned or racing past + `asyncTasksAllSettled()`'s drain, instead of being reliably completed first. + +## Related +- `test.ts` — `resetTestPromises()` is called once at this module's import time; test assertion promises are tracked separately from `scriptPromises`. +- `packages/insomnia/src/scripting/run-script.ts` — wires `resetAsyncTasks`/`stopMonitorAsyncTasks`/`asyncTasksAllSettled` into the sandboxed script's `__bridgeReset__`/`__bridgeStop__`/`__bridgeSettle__` calls. +- `packages/insomnia/src/entry.hidden-window-preload.ts` — replaces the sandbox's global `Promise` with `ProxiedPromise`. +- `packages/insomnia/src/scripting/sandbox.ts` — prepares the broader sandbox execution context that this async tracking runs inside. + \ No newline at end of file diff --git a/.claude/skills/fix-scripting-feature/references/objects/auth.md b/.claude/skills/fix-scripting-feature/references/objects/auth.md new file mode 100644 index 00000000000..5bddf9c4400 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/auth.md @@ -0,0 +1,61 @@ +# RequestAuth + +**Source:** `packages/insomnia-scripting-environment/src/objects/auth.ts` + +## Purpose +Models the authentication settings attached to a request (`pm.request.auth`). It stores auth options per-type in a map (so switching `type` doesn't destroy other types' saved options), and provides the two-way transform functions used to convert between Insomnia's native `RequestAuthentication` model and the script-facing `AuthOptions` shape. `RequestAuth` extends `Property` (see `properties.ts`). + +## Public API + +### Types +- `type AuthOptionTypes = 'noauth' | 'basic' | 'bearer' | 'jwt' | 'digest' | 'oauth1' | 'oauth2' | 'hawk' | 'awsv4' | 'ntlm' | 'apikey' | 'edgegrid' | 'asap' | 'netrc'` — the full set of auth type strings the SDK models are aware of, but not all are reachable from scripts (see Gotchas). +- `const AuthTypes: Set` — a `Set` containing the same string values as `AuthOptionTypes`, used by `RequestAuth.isValidType`. +- `interface AuthOption { key: string; value: string; type?: string }` — a single key/value auth parameter (e.g. `{ key: 'username', value: 'user1' }`). +- `interface OAuth2AuthOption { key: string; value: string | OAuth2Param[]; type?: string }` — like `AuthOption` but OAuth2 params can themselves be arrays of `OAuth2Param` (used for nested `tokenRequestParams` etc.). +- `interface OAuth2Param { key: string; value: string; enabled: boolean; send_as: string }`. +- Per-auth-type option interfaces (shape of the fields you'd set for `.use()`/`.update()` when constructing raw options rather than key/value arrays): `BasicOptions`, `BearerOptions`, `JWTOptions`, `DigestOptions`, `OAuth1Options`, `OAuth2Options`, `HAWKOptions`, `AWSV4Options`, `NTLMOptions`, `APIKeyOptions`, `EdgegridOptions`, `ASAPOptions`. Each mirrors the field names used by the corresponding Insomnia auth UI (e.g. `OAuth1Options` has `consumerKey`, `consumerSecret`, `signatureMethod`, etc.) and all have an optional `id?: string`. +- `interface AuthOptions { type: AuthOptionTypes; basic?: AuthOption[]; bearer?: AuthOption[]; jwt?: AuthOption[]; digest?: AuthOption[]; oauth1?: AuthOption[]; oauth2?: OAuth2AuthOption[]; hawk?: AuthOption[]; awsv4?: AuthOption[]; ntlm?: AuthOption[]; apikey?: AuthOption[]; edgegrid?: AuthOption[]; asap?: AuthOption[] }` — the JSON shape used to construct a `RequestAuth` and returned by `toJSON()`/`toPreRequestAuth()`. Each type's key holds an array of `AuthOption` (or `OAuth2AuthOption` for oauth2). + +### Functions +- `authOptionsToParams(authMethod: BasicOptions | BearerOptions | ... | ASAPOptions)` — converts a flat options object (e.g. `{ username, password }`) into an array of `{ type: 'any', key, value }` entries. Not used internally by `RequestAuth`; appears to be a helper for callers building `AuthOption[]` from a typed options object. +- `fromPreRequestAuth(auth: RequestAuth): RequestAuthentication` — converts a script-side `RequestAuth` into Insomnia's native `RequestAuthentication` model (used when a pre-request script finishes and Insomnia needs to write the mutated auth back onto the real request). Calls `auth.toJSON()` then switches on `type`. +- `toPreRequestAuth(auth: RequestAuthentication | {}): AuthOptions` — the reverse transform: converts Insomnia's native auth model into the `AuthOptions` shape used to construct the script's `RequestAuth` (called when initializing `pm.request.auth` before a script runs). + +### `class RequestAuth extends Property` +- `constructor(options: AuthOptions, parent?: Property)` — throws `Error('invalid auth type ${options.type}')` if `options.type` isn't a recognized type. Populates an internal `Map>` (`authOptions`) with one entry per auth-type key present in `options` (so if you pass an `AuthOptions` with only `type: 'basic'` and `basic: [...]`, only the `'basic'` entry is populated — other types are simply absent from the map, not defaulted). +- `static isValidType(authType: string): boolean` — checks membership in `AuthTypes`. +- `clear(type: string): void` — removes the stored options for `type` from the internal map (no-op, silently, if `type` is invalid). +- `parameters(): VariableList | undefined` — returns the `VariableList` of options for the *currently active* `type` (i.e. whatever `this.type` is set to), or `undefined` if nothing is stored for that type. +- `toJSON(): AuthOptions` — serializes back to the `AuthOptions` shape. For `type === 'noauth'` or `'netrc'` it returns just `{ type }` with no options array. Otherwise returns `{ type, [type]: }`. +- `update(options: VariableList | Variable[] | AuthOptions, type?: AuthOptionTypes): void` — replaces the options for `type` (or the current `this.type` if `type` omitted) and switches `this.type` to it. Throws `Error('no valid RequestAuth options is found')` if no variable list could be derived from `options`. +- `use(type: AuthOptionTypes, options: VariableList | Variable[] | AuthOptions): void` — same as `update` but `type` is required; throws `Error('invalid type (...)')` if `type` isn't in `AuthTypes`, and the same "no valid options" error as `update` otherwise. This is the method `Request.authorizeUsing()` calls to switch a request's auth type from a script. + +### Internal helper (not exported) +- `rawOptionsToVariables(options, targetType?)` — normalizes the three accepted input shapes (`VariableList`, `Variable[]`, or a full `AuthOptions` object) into `VariableList[]`. Throws `Error('options is not valid: it must be VariableList | Variable[] | object')` if none of the shapes match. + +## Script-facing surface +- `pm.request.auth` is a `RequestAuth` instance (constructed in `request.ts` from `options.auth || { type: 'noauth' }`). +- `pm.request.auth.parameters()` — read the currently active auth's key/value options. +- `pm.request.auth.update(newOptions)` — mutate the current auth type's options in place. +- `pm.request.authorizeUsing(type, options)` (on `Request`, in `request.ts`) delegates to `this.auth.use(type, options || { type: 'noauth' })` — this is the documented way scripts switch auth type. +- `pm.request.auth.clear(type)` to drop a stored auth-type's options. +- When a pre-request script finishes, `fromPreRequestAuth(request.auth)` converts the mutated `RequestAuth` back to Insomnia's native `RequestAuthentication`, which is what actually gets sent on the wire (see `request.ts`, `authentication: fromPreRequestAuth(updatedReq.auth)`). + +## Gotchas / notable behavior +- **`netrc` is a dead end**: `AuthTypes` includes `'netrc'` and `RequestAuth` will happily accept `type: 'netrc'`, but `fromPreRequestAuth` throws `Error('netrc is not supported yet')` when it sees `type === 'netrc'`, and `toPreRequestAuth` throws `Error('netrc auth is not supported in scripting yet')` for the native `'netrc'` type. So a script that sets `type: 'netrc'` will fail only when Insomnia tries to convert it back, not immediately. +- **`singleToken` is unsupported**: `toPreRequestAuth` throws for native auth type `'singleToken'` — there is no path from this native auth type into the script's `RequestAuth` at all. +- **`edgegrid` has interfaces but no transform**: `EdgegridOptions` and `'edgegrid'` are defined in `AuthOptionTypes`/`AuthTypes`, but neither `fromPreRequestAuth` nor `toPreRequestAuth` has a `case 'edgegrid'` — passing this type through those functions falls into the `default` branch and throws `Error('unknown auth type: ...')`. +- **oauth1 `signatureMethod` restrictions**: `fromPreRequestAuth`'s oauth1 branch only supports `HMAC-SHA1`, `HMAC-SHA256`, `RSA-SHA1`, and `PLAINTEXT`. `HMAC-SHA512`, `RSA-SHA256`, and `RSA-SHA512` are recognized strings but explicitly throw `Error('...unsupported signatureMethod type for oauth1: ...')`. +- **oauth1 `privateKey` is a one-way/unsupported field**: comment in code says "it is not supported in the script side" — it's read but not written back into the native auth model in `toPreRequestAuth`. +- **oauth2 has several fields marked "not supported yet in the script side"**: `tokenPrefix`, `responseType`, `origin` are passed through in `fromPreRequestAuth`/`toPreRequestAuth` for round-tripping but are not meaningfully used elsewhere per the inline comments. +- **hawk loses several fields on the round trip to native Insomnia auth**: `timestamp`, `delegation`, `app`, `nonce`, `user` are commented as "some keys are lost here" in `fromPreRequestAuth`'s hawk branch, and `toPreRequestAuth`'s hawk branch hardcodes them back to empty string / `'false'` since "these fields are not supported in Insomnia side". +- **Booleans are stored as strings internally**: all the `AuthOption`/`OAuth2AuthOption` values are `string`, so booleans like `disabled` are represented as `'true'`/`'false'` strings inside the `VariableList`, and compared with `=== 'true'` when converting back. +- **Constructor does not default missing types**: if you construct `new RequestAuth({ type: 'bearer' })` (no `bearer` key), `parameters()` will return `undefined` immediately — there's no implicit empty array. +- **`update`/`use` fully replace, not merge**: calling `update()`/`use()` replaces the entire options list for a type; it does not merge in partial fields. + +## Related +- `properties.ts` — `Property` (base class) and `Variable`/`VariableList` container semantics used to store auth options. +- `variables.ts` — `Variable`, `VariableList` (the underlying storage for each auth type's key/value pairs). +- `request.ts` — constructs `pm.request.auth` from `AuthOptions`, exposes `authorizeUsing()`, and calls `fromPreRequestAuth` when finalizing the request. +- `insomnia.ts` — calls `toPreRequestAuth(rawObj.request.authentication)` to seed `pm.request.auth` before a script runs. +- `insomnia-data` package — source of the native `RequestAuthentication` and `OAuth2ResponseType` types that `fromPreRequestAuth`/`toPreRequestAuth` convert to/from. diff --git a/.claude/skills/fix-scripting-feature/references/objects/certificates.md b/.claude/skills/fix-scripting-feature/references/objects/certificates.md new file mode 100644 index 00000000000..afee8b59a77 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/certificates.md @@ -0,0 +1,43 @@ +# Certificate + +**Source:** `packages/insomnia-scripting-environment/src/objects/certificates.ts` + +## Purpose +Models a single client (mTLS) certificate for the scripting environment — the script-facing representation surfaced as `pm.request.certificate`. It is deliberately simple: unlike Insomnia's native model (which supports a list of client certificates matched by host), the scripting SDK only ever models **one** certificate per request, matched via `UrlMatchPattern`s (from `urls.ts`). + +## Public API + +### Types +- `interface SrcRef { src: string }` — a file-path reference; used for `key`, `cert`, and `pfx` since certificate content is loaded from disk, not embedded inline. +- `interface CertificateOptions { name?: string; matches?: string[]; key?: SrcRef; cert?: SrcRef; passphrase?: string; pfx?: SrcRef; disabled?: boolean }` — constructor/`update()` input. `pfx` is documented as "PFX or PKCS12 Certificate". + +### `class Certificate extends Property` +- `override _kind = 'Certificate'` +- `override name?: string` +- `matches?: UrlMatchPatternList` — constructed internally from the `matches: string[]` option; each string becomes a `UrlMatchPattern`. +- `key?: SrcRef` +- `cert?: SrcRef` +- `passphrase?: string` +- `pfx?: SrcRef` +- `constructor(options: CertificateOptions)` — sets all fields directly from `options`, including `this.disabled = options.disabled` (inherited `Property.disabled` field). `matches` defaults to an empty `UrlMatchPatternList` if `options.matches` is not provided. +- `static isCertificate(obj: object): boolean` — checks `obj._kind === 'Certificate'`. +- `canApplyTo(url: string): boolean` — returns `this.matches ? this.matches.test(url) : false`, i.e. whether any of the certificate's match patterns matches the given URL. Delegates to `UrlMatchPatternList.test()`. +- `update(options: CertificateOptions): void` — fully overwrites `name`, `matches` (rebuilt the same way as in the constructor), `key`, `cert`, `passphrase`, `pfx`. Note: **does not** update `disabled` (unlike the constructor, which does set it) — see Gotchas. + +## Script-facing surface +- `pm.request.certificate` is a `Certificate` instance. Per `insomnia.ts`, it's initialized either as an empty placeholder certificate (`name: 'Default Certificate'`, no `key`/`cert`/`pfx`) when the request URL contains unrendered template tags or `filterClientCertificates` returns zero matches, or as `{ name: 'The first matched certificate from Settings', matches: [matchedCertificates[0].host], key: {src: ...}, cert: {src: ...}, passphrase, pfx: {src: ...} }` — built from `matchedCertificates[0]` whenever the match list is **non-empty**, i.e. one or more matches, not only when there's exactly one. If more than one client certificate matches, everything past the first is silently dropped rather than surfaced as the empty-certificate path — a direct consequence of `Certificate` only ever modeling a single certificate (see Gotchas below). +- Scripts can read `pm.request.certificate.key`, `.cert`, `.pfx`, `.passphrase`, `.name`, call `.canApplyTo(url)`, or call `.update({...})` to replace the certificate used for the request. Per `request.ts`, `Request.certificate` is a `Certificate` built the same way (`options.certificate ? new Certificate(options.certificate) : undefined`), and the certificate is serialized back out (`toJSON()`-style plain object with `name`, `matches` stringified, `key`, `cert`, `passphrase`, `pfx`) when the script finishes so Insomnia can merge it into the request's actual client-certificate list via `mergeClientCertificates` (in `request.ts`). + +## Gotchas / notable behavior +- **Only one certificate is modeled, even though Insomnia supports several.** `request.ts`'s comment: "Pre-request script request only supports one certificate while Insomnia supports configuring multiple ones." When a script sets a certificate, `mergeClientCertificates` prepends it as a new entry ahead of the request's existing native client certificates rather than replacing the whole list. +- **`update()` does not update `disabled`.** The constructor sets `this.disabled = options.disabled`, but `update()` has no such line — calling `cert.update({ disabled: true, ... })` will not actually change `cert.disabled`. This looks like an inconsistency/bug relative to the constructor's behavior. +- **`cert+key` and `pfx` are mutually exclusive downstream.** `request.ts`'s `mergeClientCertificates` throws `Error('Invalid certificate configuration: "cert+key" and "pfx" can not be set at the same time')` if a script sets both a PFX and a cert/key pair on `pm.request.certificate`. +- **Empty/placeholder certificate is normal, not an error state.** If the request URL contains a template tag (e.g. `{{ baseUrl }}`) or there's no matching client certificate configured in Settings, `pm.request.certificate` is initialized to an empty certificate and `insomnia.ts` logs this via `getExistingConsole().warn(...)` — a warning-level timeline entry, not debug — so it's visible when tracing why the default/empty certificate was selected. Scripts should not assume `key`/`cert`/`pfx` are populated. +- **`matches` is derived only from certificate `host`, singular** — even though `UrlMatchPatternList` supports multiple patterns, `insomnia.ts` only ever seeds `matches: [matchedCertificates[0].host]` (a single-element array) from the native model. + +## Related +- `properties.ts` — `Property` base class (provides `id`, `name`, `disabled`, `toJSON()`/`toString()` defaults). +- `urls.ts` — `UrlMatchPattern`, `UrlMatchPatternList` (used for `matches` and `canApplyTo()`). +- `request.ts` — constructs/serializes `pm.request.certificate`, and contains `mergeClientCertificates()` which reconciles the script's single certificate back into Insomnia's native multi-certificate list. +- `insomnia.ts` — seeds the initial `pm.request.certificate` value from Insomnia Settings' configured client certificates, using `filterClientCertificates` from `insomnia/src/network/certificate`. +- `insomnia-data` package — source of the native `ClientCertificate` type. diff --git a/.claude/skills/fix-scripting-feature/references/objects/collection.md b/.claude/skills/fix-scripting-feature/references/objects/collection.md new file mode 100644 index 00000000000..6504fa02ac2 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/collection.md @@ -0,0 +1,53 @@ +# collection.ts (re-export barrel — no `Collection` class) + +**Source:** `packages/insomnia-scripting-environment/src/objects/collection.ts` + +## Purpose +**This file does not define a `Collection` class, and has no logic of its own.** Its entire content is a JSDoc `@module` comment plus eleven `export { ... } from '...'` re-export statements: +```ts +export { RequestAuth } from './auth'; +export { Certificate } from './certificates'; +export { Cookie, CookieList } from './cookies'; +export { Header, HeaderList } from './headers'; +export { Property, PropertyBase, PropertyList } from './properties'; +export { ProxyConfig, ProxyConfigList } from './proxy-configs'; +export { FormParam, Request, RequestBody } from './request'; +export { Response } from './response'; +export { QueryParam, Url, UrlMatchPattern, UrlMatchPatternList } from './urls'; +export { Variable, VariableList } from './variables'; +export { Folder } from './folders'; +``` +It exists purely so that `index.ts` can do `export * as Collection from './collection'`, giving SDK/type consumers a `Collection.Property`, `Collection.Variable`, `Collection.Folder`, etc. namespace (mirroring the Postman `postman-collection` package's `Collection.*` type namespace for compatibility/familiarity). There is no runtime behavior, no constructor, and nothing related to collection-scoped *variable storage* in this file. + +## Public API +No classes/functions of its own. It re-exports (unchanged) the following, whose real definitions and docs live in their own files: +- `RequestAuth` (`auth.ts`) +- `Certificate` (`certificates.ts`) +- `Cookie`, `CookieList` (`cookies.ts`) +- `Header`, `HeaderList` (`headers.ts`) +- `Property`, `PropertyBase`, `PropertyList` (`properties.ts` — see `properties.md`) +- `ProxyConfig`, `ProxyConfigList` (`proxy-configs.ts`) +- `FormParam`, `Request`, `RequestBody` (`request.ts`) +- `Response` (`response.ts`) +- `QueryParam`, `Url`, `UrlMatchPattern`, `UrlMatchPatternList` (`urls.ts`) +- `Variable`, `VariableList` (`variables.ts` — see `variables.md`) +- `Folder` (`folders.ts` — see `folders.md`) + +Note this list is nearly identical to `index.ts`'s own top-level JSDoc module comment (both files carry the same doc block), but `index.ts` itself re-exports far more (all of `environments.ts`, `insomnia.ts`, `test.ts`, `execution.ts`, etc.) in addition to wrapping this file as the `Collection` namespace. + +## Script-facing surface +No direct script-facing surface of its own. **If you are looking for how `pm.collectionVariables` actually works** (collection-level variable storage and resolution), that logic lives in: +- `environments.ts` — the `Environment` class backing the actual store. +- `insomnia.ts` — `InsomniaObject` explicitly maps `this.collectionVariables = this.baseEnvironment` (i.e. `pm.collectionVariables` **is** `pm.baseEnvironment`, the same `Environment` instance, not a separate object) and constructs the `Variables` hierarchy (`collectionVars: baseEnvironment`) used by `pm.variables`. +See `environments.md` for the full precedence/resolution behavior. + +## Gotchas / notable behavior +- **Naming trap**: a file named `collection.ts` in a scripting/variable-resolution context strongly suggests a `Collection` class with collection-variable logic — it has none. Anyone debugging "collection variables" behavior should look at `environments.ts` (`Environment`) and `insomnia.ts` (`collectionVariables = baseEnvironment` aliasing), not this file. +- Diffed byte-for-byte against `index.ts`'s doc comment: the JSDoc header text is duplicated between the two files, but the actual export lists differ — `collection.ts` only re-exports a subset (the "Postman-collection-shaped" types), while `index.ts` re-exports everything including `insomnia.ts`, `environments.ts`, `console.ts`, `execution.ts`, `test.ts`, `async-objects.ts`, `request-info.ts`. +- `index.ts` wraps this file's exports under a namespace: `export * as Collection from './collection';` — so external consumers see `Collection.Variable`, `Collection.Folder`, etc., but that `Collection` namespace object is not related to `pm.collectionVariables` at runtime; it's purely a type/value re-export grouping. + +## Related +- `index.ts` — re-exports this file's contents as the `Collection` namespace (`export * as Collection from './collection'`). +- `environments.ts` — actual implementation backing `pm.collectionVariables`/`pm.baseEnvironment` (see `environments.md`). +- `insomnia.ts` — wires `collectionVariables` to `baseEnvironment` and builds the `Variables` hierarchy. +- `properties.ts`, `variables.ts`, `folders.ts`, `urls.ts`, `request.ts`, `response.ts`, `cookies.ts`, `headers.ts`, `auth.ts`, `certificates.ts`, `proxy-configs.ts` — the actual modules whose types this file re-exports. diff --git a/.claude/skills/fix-scripting-feature/references/objects/console.md b/.claude/skills/fix-scripting-feature/references/objects/console.md new file mode 100644 index 00000000000..08702ee98b2 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/console.md @@ -0,0 +1,62 @@ +# Console + +**Source:** `packages/insomnia-scripting-environment/src/objects/console.ts` + +## Purpose +Implements a sandbox-safe replacement for the global `console` object used inside pre-request/after-response/test scripts. Instead of writing to a real stdout/devtools console, every call is captured into an in-memory row buffer that the execution harness later dumps and surfaces back to the Insomnia app (e.g. the response timeline). + +## Public API + +### `type LogLevel` +```ts +type LogLevel = 'debug' | 'info' | 'log' | 'warn' | 'error'; +``` +Not exported — internal tag for which console method produced a row. + +### `interface Row` +```ts +export interface Row { + value: string; + name: string; + timestamp: number; +} +``` +- `value` — the rendered `": "` string. +- `name` — always `'Text'` in current usage. +- `timestamp` — `Date.now()` at log time. + +### `class Console` +No explicit constructor (uses field initializers); instantiated with `new Console()`. + +- `rows: Row[]` — accumulated log entries, in call order. +- `printLog = (rows: Row[], level: LogLevel, ...values: any) => void` — core formatter (marked `@ignore`, not really meant to be called directly by users). Maps each value to a string (`typeof value === 'string' ? value : JSON.stringify(value, null, 2)`), joins with `' '`, and pushes `{ value: `${level}: ${content}`, name: 'Text', timestamp: Date.now() }` onto `rows`. If formatting throws, it instead pushes a row whose `value` is `'error: ' + JSON.stringify(e, null, 2)`. +- `log = (...values: any[]) => void` — calls `printLog(this.rows, 'log', ...values)`. +- `warn = (...values: any[]) => void` — calls `printLog(this.rows, 'warn', ...values)`. +- `debug = (...values: any[]) => void` — calls `printLog(this.rows, 'debug', ...values)`. +- `info = (...values: any[]) => void` — calls `printLog(this.rows, 'info', ...values)`. +- `error = (...values: any[]) => void` — calls `printLog(this.rows, 'error', ...values)`. +- `clear = (_level: LogLevel, _message?: any, ..._optionalParams: any[]) => void` — **always throws** `new Error('currently "clear" is not supported for the timeline')`. Calling `console.clear()` in a script will throw. +- `dumpLogs = () => string` — returns all rows serialized as `JSON.stringify(row) + '\n'`, joined with `'\n'` (i.e. one JSON blob per row, double-newline separated). +- `dumpLogsAsArray = () => string[]` — same per-row serialization (`JSON.stringify(row) + '\n'`) but returned as an array instead of one joined string. + +### Module-level singleton helpers +- `let builtInConsole = new Console();` (module-private). +- `getExistingConsole(): Console` — returns the current singleton without creating a new one. +- `getNewConsole(): Console` — replaces the singleton with a fresh `new Console()` and returns it (used to reset log state between script executions). + +## Script-facing surface +- `console.log(...)`, `console.warn(...)`, `console.debug(...)`, `console.info(...)`, `console.error(...)` — available as the script's global `console` inside pre-request/after-response/test scripts (the sandbox injects a `Console` instance as the `console` parameter, not the real JS console). +- `console.clear()` — present but always throws if called from a script. + +## Gotchas / notable behavior +- **`clear()` is a hard error, not a no-op.** Any script calling `console.clear()` will throw `Error('currently "clear" is not supported for the timeline')`, which will surface as a script failure. +- **Object logging uses `JSON.stringify(value, null, 2)`**, not `util.inspect`-style formatting — circular references or values with `toJSON()` quirks will affect what shows up. If `JSON.stringify` itself throws (e.g. circular reference), the whole `printLog` call falls back to a single `'error: ' + JSON.stringify(e, null, 2)` row rather than losing the log entirely — but the original values are lost in that fallback. +- **No format-string substitution (`%s`, `%d`, etc.)** — the `// TODO: support replacing substitution` comment confirms this isn't implemented; values are just stringified and space-joined. +- **State is a module-level singleton**, not per-instance-only: `getExistingConsole()`/`getNewConsole()` share one `builtInConsole` variable across the module. The harness must call `getNewConsole()` before each script run to avoid leaking log rows from a previous execution into the next one's dump. +- **Two different dump shapes exist** (`dumpLogs()` joined string vs. `dumpLogsAsArray()` array of strings) — different call sites in the harness use one or the other (see Related), so a log line "disappearing" could be a symptom of reading from the wrong dump method or a stale singleton reference. + +## Related +- `packages/insomnia-scripting-environment/src/objects/insomnia.ts` — imports `getExistingConsole` and uses it only for a one-off warning log during `initInsomniaObject` (an empty-certificate warning); it is not the general per-script logging path. +- `packages/insomnia/src/scripting/sandbox.ts` (`prepareSandbox`) — calls `getNewConsole()` to obtain a fresh `Console` instance for each script run and passes `scriptConsole.log` into `initInsomniaObject(sandboxContext, scriptConsole.log)`. +- `packages/insomnia/src/scripting/run-script.ts` — injects the `Console` instance itself as the sandboxed script's `console` global (part of the generated function's parameter list), and at the end calls `scriptConsole.dumpLogsAsArray()`, including the result (`logs`) in the returned `RequestContext`. +- Legacy path `packages/insomnia/src/script-executor.ts` — calls `scriptConsole.dumpLogs()` (joined-string form) and appends it to a timeline file via `fs.promises.appendFile`, which is how logs end up in Insomnia's response timeline view in that code path. diff --git a/.claude/skills/fix-scripting-feature/references/objects/cookies.md b/.claude/skills/fix-scripting-feature/references/objects/cookies.md new file mode 100644 index 00000000000..d19e38ad50e --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/cookies.md @@ -0,0 +1,70 @@ +# Cookie, CookieJar, CookieList + +**Source:** `packages/insomnia-scripting-environment/src/objects/cookies.ts` + +## Purpose +Models HTTP cookies for the scripting environment. `Cookie` wraps a `tough-cookie` cookie plus Insomnia-specific extension fields; `CookieList` is a keyed `PropertyList`; `CookieObject` (a `CookieList` subclass) is the concrete class backing `pm.cookies` and pairs the list with a `CookieJar`; `CookieJar` is a custom (non-tough-cookie) jar keyed by domain used for `pm.cookies.jar()`. `mergeCookieJar` reconciles a script-produced jar back into Insomnia's native `CookieJar` model. + +## Public API + +### Types +- `interface InsomniaCookieExtensions { creation?: Date; creationIndex?: number; lastAccessed?: Date; pathIsDefault?: boolean }` — Insomnia-only metadata carried alongside the standard cookie fields. +- `interface CookieOptions extends InsomniaCookieExtensions { id?: string; key: string; value: string; expires?: Date | string | null; maxAge?: number | 'Infinity' | '-Infinity'; domain?: string; path?: string; secure?: boolean; httpOnly?: boolean; hostOnly?: boolean; session?: boolean; extensions?: { key: string; value: string }[] }` — constructor input for `Cookie`. + +### `class Cookie extends Property` +- `constructor(cookieDef: CookieOptions | string)` — if given a string, parses it via `Cookie.parse()` first (throws `Error('failed to parse cookie, the cookie string seems invalid')` if parsing fails or the tough-cookie construction fails). Wraps the result in a `tough-cookie` `Cookie` (`ToughCookie.fromJSON`). `extensions` are stored separately from the tough-cookie object (tough-cookie only handles `string[]` extensions natively). +- `static override _index = 'key'` — `PropertyList.one()`/`indexOf()` look items up by `key` for cookie lists. +- `static isCookie(obj: Property): boolean` — checks `obj._kind === 'Cookie'`. +- `static parse(cookieStr: string): CookieOptions` — parses a `Set-Cookie`-style string (loose mode) via tough-cookie, pulls `HostOnly`/`Session` out of the extensions list into dedicated booleans, and converts remaining string extensions (`key=value` or bare flags, which become `{key, value: 'true'}`) into `{key, value}` objects. Throws if the string can't be parsed by tough-cookie. +- `static stringify(cookie: Cookie): string` — alias for `cookie.toString()`. +- `static unparseSingle(cookieOpt: CookieOptions): string` — constructs a `Cookie` from options and returns its string form. +- `static unparse(cookies: Cookie[]): string` — joins each cookie's `toString()` with `'; '`. +- `toString(): string` — tough-cookie's own `toString()` plus `; HostOnly`, `; Session`, and `; key=value` segments appended for any extensions/inso flags present. +- `valueOf(): string` — returns just `cookie.value` (used by `PropertyList.one()` for lookups and by `toObject()`). +- `get key(): string` — reads through to `this.cookie.toJSON().key`. +- `toJSON()` — returns the full plain-object shape: `{ id, key, value, expires, maxAge, domain, path, secure, httpOnly, hostOnly, session, extensions, creation, creationIndex, lastAccessed, pathIsDefault }`. Note `expires` is coerced to `undefined` when tough-cookie reports `'Infinity'`. + +### `class CookieList extends PropertyList` +- `constructor(cookies: Cookie[])` — always constructs with `typeClass = Cookie`, no `parent`. +- `static isCookieList(obj: object): boolean`. +- `override toObject(excludeDisabled?, _caseSensitive?, multiValue?, _sanitizeKeys?): Record` — builds a plain `key -> value` map via `Object.create(null)` (so no prototype — dangerous keys like `__proto__`/`constructor` land as own properties, not pollute the prototype). `excludeDisabled` skips cookies where `cookie.disabled` is true. `multiValue` collects same-key cookies into an array instead of overwriting (last value wins when `multiValue` is falsy). + +### `class CookieObject extends CookieList` +This is the concrete class instantiated as `pm.cookies`. +- `constructor(cookieJar: InsomniaCookieJar | null)` — maps each native Insomnia cookie into a script `Cookie` (translating numeric `expires` into a `Date`, and passing through `maxAge: undefined`/`session: undefined`/`extensions: undefined` since those fields aren't tracked by Insomnia's native cookie model). Also builds an internal `CookieJar` (named from `cookieJar.name`, or `''`/empty if `cookieJar` is `null`). +- `jar(): CookieJar` — returns the internal `CookieJar` instance backing `pm.cookies.jar()`. + +### `class CookieJar` (custom, not `tough-cookie`'s jar) +Comment: "CookieJar from tough-cookie can not be used, as it will fail in comparing context location and cookies' domain as it reads location from the browser window, it is 'localhost'". Internally a `Map>`. +- `constructor(jarName: string, cookies?: Cookie[])` — indexes each cookie by its `domain` (read via `cookie.toJSON().domain`). If a cookie has no `domain`, it's **dropped** with a console warning: `` `domain is not specified for the cookie "${cookie.key}" so it is omitted` `` (via `getExistingConsole().warn`). +- `set(url: string, key: string, value: string | CookieOptions, cb: (error?: Error, cookie?: Cookie) => void): void` — `url` is actually used as the domain key (not parsed as a URL). If `value` is a plain string, constructs a minimal `Cookie({ key, value, domain: url })`; otherwise treats `value` as full `CookieOptions` and constructs `new Cookie(value)`. Always calls `cb(undefined, cookie)` — this implementation never produces an error. +- `get(url: string, name: string, cb: (error?: Error, cookie?: Cookie) => void): void` — looks up `name` within the `url` domain bucket; calls back with `undefined` cookie if not found (not an error). +- `getAll(url: string, cb: (error?: Error, cookies?: Cookie[]) => void): void` — returns all cookies for that domain bucket (empty array if none). +- `unset(url: string, name: string, cb: (error?: Error | null) => void): void` — deletes `name` from the domain bucket if the bucket exists; always calls back with no error. +- `clear(url: string, cb: (error?: Error | null) => void): void` — deletes the entire domain bucket. +- `toInsomniaCookieJar(): { name: string; cookies: Partial[] }` — flattens the map back into Insomnia's native cookie-jar shape. `expires` is defaulted back to `'Infinity'` if falsy ("avoid edge cases"). + +### Module-level function +- `mergeCookieJar(originalCookieJar: InsomniaCookieJar, updatedCookieJar: { name: string; cookies: Partial[] }): InsomniaCookieJar` — assigns a fresh `uuidv4()` id to any cookie missing one (mirroring the id-generation approach in Insomnia's `cookie-list.tsx`), and returns `{ ...originalCookieJar, cookies: cookiesWithId }`. This is the function that reconciles a script's mutated jar back into Insomnia's persisted model after a script runs. + +## Script-facing surface +- `pm.cookies` is a `CookieObject` (constructed in `insomnia.ts` via `new CookieObject(rawObj.cookieJar)`), so scripts get all `CookieList`/`PropertyList` methods directly: `pm.cookies.get(key)` / `pm.cookies.one(key)` (via inherited `PropertyList.get`/`one`, which for `Cookie` returns `cookie.valueOf()` — i.e. just the string value, not the `Cookie` object, because `Cookie.valueOf` is defined), `pm.cookies.toObject()`, `pm.cookies.all()`, `pm.cookies.count()`, `pm.cookies.each()`, `pm.cookies.add()`, etc. +- `pm.cookies.jar()` returns the `CookieJar`, giving access to `set(url, key, value, cb)`, `get(url, name, cb)`, `getAll(url, cb)`, `unset(url, name, cb)`, `clear(url, cb)` — all Postman-style callback-based cookie-jar operations, scoped by domain (the `url` argument is treated as a plain domain string, not parsed). +- After a script runs, Insomnia calls `this.cookies.jar().toInsomniaCookieJar()` (see `insomnia.ts`) to get the jar back out, which is then merged via `mergeCookieJar`. + +## Gotchas / notable behavior +- **`PropertyList.one()`/`.get()` return the raw value, not a `Cookie`**: because `Cookie` defines `valueOf()`, and `PropertyList.one()` special-cases items with a `valueOf` method — `pm.cookies.get('foo')` returns the cookie's string value, not the `Cookie` instance. To get the full object, use `pm.cookies.find(...)` or `pm.cookies.all()` instead. +- **`toObject()` uses `Object.create(null)`** specifically to avoid prototype pollution when cookie keys are things like `__proto__` or `constructor` — confirmed by a dedicated test (`toObject does not pollute the prototype for dangerous cookie keys`). +- **Cookies without a `domain` are silently dropped** when building a `CookieJar` (constructor only; a warning is logged, not thrown). +- **`maxAge` and `session` are not persisted from Insomnia's native model**: `CookieObject`'s constructor always sets `maxAge: undefined` and `session: undefined` when converting from `InsomniaCookieJar`, with inline comments "not supported in Insomnia". +- **`extensions` format from Insomnia is unknown** per an inline `TODO` — `CookieObject`'s constructor always passes `extensions: undefined`. +- **`CookieJar.set()` never reports an error** — the callback signature allows an `Error`, but the implementation has no failure path (a malformed `CookieOptions` would throw inside `new Cookie(...)` before the callback is ever reached, rather than being passed to `cb`). +- **Duplicate keys in `toObject()` collapse to the last value** unless `multiValue` is passed as `true`, in which case duplicates accumulate into an array. +- **`Cookie.parse()` of a malformed string like `'=gingerale'`** still succeeds with `key: ''`, `value: 'gingerale'`, `expires: 'Infinity'` (per the test) — it does not throw for merely unusual input, only for input tough-cookie's parser rejects entirely. + +## Related +- `properties.ts` — `Property` (base class for `Cookie`) and `PropertyList` (base class for `CookieList`). +- `console.ts` — `getExistingConsole()`, used by `CookieJar`'s constructor to warn about domain-less cookies. +- `insomnia.ts` — constructs `pm.cookies` as `new CookieObject(rawObj.cookieJar)` and reads back `this.cookies.jar().toInsomniaCookieJar()` after script execution. +- `insomnia-data` package — source of the native `Cookie`/`CookieJar` types this file converts to/from. +- `tough-cookie` (external) — underlying cookie parse/stringify implementation wrapped by `Cookie`. diff --git a/.claude/skills/fix-scripting-feature/references/objects/environments.md b/.claude/skills/fix-scripting-feature/references/objects/environments.md new file mode 100644 index 00000000000..a7d0015a767 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/environments.md @@ -0,0 +1,85 @@ +# Environment / Variables / Vault + +**Source:** `packages/insomnia-scripting-environment/src/objects/environments.ts` + +## Purpose +This file defines `Environment` (a single flat key-value store), `Variables` (a hierarchical read/write facade over several `Environment` scopes, resolving lookups in precedence order), and `Vault` (a locked-down `Environment` subclass for secrets). `InsomniaObject` (`insomnia.ts`) instantiates these and exposes them as `insomnia.environment`, `insomnia.globals`/`insomnia.baseGlobals` (private), `insomnia.collectionVariables` (aliased to `baseEnvironment`), `insomnia.variables`, and `insomnia.vault` — the core variable model behind pre-request/after-response/test scripts. + +## Public API + +### `class Environment` +```ts +constructor(name: string, jsonObject: object | undefined) +``` +Initializes `_name` and a `Map` from `Object.entries(jsonObject || {})`. + +- `get name(): string` — returns `_name` (read-only getter; no setter). +- `has = (variableName: string) => boolean` — `Map.has`. +- `get = (variableName: string) => boolean|number|string|null|undefined` — `Map.get`; returns `undefined` if absent. +- `set = (variableName: string, variableValue: boolean|number|string|undefined|null) => void` — sets the value; if `Number.isNaN(variableValue)` is true, logs a warning via `getExistingConsole().warn` and **does not** set the value. +- `unset = (variableName: string) => void` — `Map.delete`. +- `clear = () => void` — `Map.clear`. +- `replaceIn = async (template: string | object) => Promise` — coerces an object template via `.toString()`, throws `TypeError` if not a string/object, then renders it through `getInterpolator().render(template, this.toObject())`. Supports `{{$randomUUID}}` and `{{varName}}` placeholders. +- `toObject = () => Record` — `Object.fromEntries(this.kvs.entries())`. +- `toJSON()` — returns `this.toObject()` (drives `JSON.stringify(environment)` serialization). + +### `function mergeFolderLevelVars(folderLevelVars: Environment[]): Environment` *(not exported, `@ignore`)* +Reduces an array of folder `Environment`s into one merged `Environment` named `'mergedFolderLevelVars'`, with **later folders in the array winning** on key collisions (`{ ...merged, ...folderLevelEnv.toObject() }`). + +### `class Variables` +```ts +constructor(args: { + baseGlobalVars: Environment; + globalVars: Environment; + collectionVars: Environment; + environmentVars: Environment; + iterationDataVars: Environment; + folderLevelVars: Environment[]; + localVars: Environment; +}) +``` +Stores each scope as a private `Environment` (or `Environment[]` for folder levels). + +- `has = (variableName: string) => boolean` — true if any of the 7 scopes has the variable (local, folder-level via `.some`, iterationData, environment, collection, global, baseGlobal — all evaluated eagerly, then OR'd). +- `get = (variableName: string) => value` — builds an ordered array `[localVars, mergeFolderLevelVars(folderLevelVars), iterationDataVars, environmentVars, collectionVars, globalVars, baseGlobalVars]`, uses `.find(vars => vars.has(variableName))` to pick the **first scope (highest precedence) that has the key**, and returns `scope?.get(variableName)`. +- `set = (variableName, variableValue) => void` — **always writes only to `localVars`** (same NaN-guard/warning as `Environment.set`). There is no way to write directly to environment/collection/global scope through `Variables.set`. +- `replaceIn = async (template: string | object) => Promise` — same contract as `Environment.replaceIn`, but the context is `this.toObject()` (the fully merged view across all scopes). +- `toObject = () => Record` — maps `[baseGlobalVars, globalVars, collectionVars, environmentVars, iterationDataVars, mergeFolderLevelVars(folderLevelVars), localVars]` to plain objects and folds them left-to-right with `{...ctx, ...obj}`, so **later entries in that array win** — i.e. `localVars` has final say, matching the precedence order used by `get`. +- `localVarsToObject = () => Record` *(`@ignore`)* — returns just `localVars.toObject()`; used by `InsomniaObject.toObject()` to serialize `insomnia.variables`. + +### `class Vault extends Environment` +```ts +constructor(name: string, jsonObject: object | undefined, enableVaultInScripts: boolean) +``` +Calls `super(name, jsonObject)`, then **returns a `Proxy(this, ...)`** (note: the constructor return value, not `this`, is what callers get). The proxy's `get`/`set` traps throw `new Error('Vault is disabled in script')` for **every property/method access** (including inherited `Environment` methods like `get`/`has`) whenever `enableVaultInScripts` is falsy. +- `unset = () => { throw new Error('Vault can not be unset in script'); }` — overridden to always throw, regardless of `enableVaultInScripts`. +- `clear = () => { throw new Error('Vault can not be cleared in script'); }` — always throws. +- `set = () => { throw new Error('Vault can not be set in script'); }` — always throws (vault is read-only from scripts even when enabled). +- Inherited `get`, `has`, `toObject`, `toJSON`, `replaceIn`, `name` still work normally when `enableVaultInScripts` is true (subject to the proxy's guard, which only blocks when the flag is false). + +## Script-facing surface +- `insomnia.environment` → an `Environment` instance (the selected sub-environment, or the base environment itself if none is selected — see `initInsomniaObject` in `insomnia.ts`). Scripts call `insomnia.environment.get/set/unset/has/clear/replaceIn/toObject`. +- `insomnia.baseEnvironment` → the collection's base `Environment`. +- `insomnia.collectionVariables` → in `insomnia.ts`, this is literally assigned `this.baseEnvironment` (`this.collectionVariables = this.baseEnvironment;`) — it is not a distinct store, it's the *same* `Environment` object as `insomnia.baseEnvironment`. +- `insomnia.variables` → a `Variables` instance built from all scopes (`insomnia.ts`'s `initInsomniaObject`: `baseGlobalVars: baseGlobals, globalVars: globals, environmentVars: environment, collectionVars: baseEnvironment, iterationDataVars: iterationData, folderLevelVars: parentFolders.getEnvironments(), localVars: localVariables`). Scripts use `insomnia.variables.get/set/has/replaceIn/toObject`. `insomnia.variables.set(...)` only ever affects the transient local scope. +- `insomnia.vault` → a `Vault` instance; `insomnia.vault.get()` is the documented usage. Gated by the `enableVaultInScripts` setting. +- `insomnia.globals`/`insomnia.baseGlobals` are declared `private` on `InsomniaObject`, but that's a TypeScript-only, compile-time restriction — `private` is erased at runtime, and `InsomniaObject`'s constructor returns `new Proxy(this, { get: (target, prop, receiver) => ... Reflect.get(target, prop, receiver) })` (see `insomnia.ts`), which forwards any property name through to the real field. A script can read `insomnia.globals`/`insomnia.baseGlobals` directly at runtime; they're just absent from the TS-declared public API, not actually inaccessible. Treat this as accidental exposure, not a supported surface — don't build fixes or features on scripts reading these, and any change here should aim to actually restrict access (e.g. a proxy guard like `Vault`'s) rather than expose them further. +- `{{$randomUUID}}`-style and `{{variableName}}` templates in request URLs/bodies/headers are ultimately rendered through `Environment.replaceIn` / `Variables.replaceIn`. + +## Gotchas / notable behavior +- **NaN guard**: both `Environment.set` and `Variables.set` silently refuse to store `NaN` and log a console warning instead of throwing — `null` and `undefined` are accepted just fine. +- **Precedence order** (highest to lowest) used consistently by `Variables.has`, `.get`, and `.toObject`: local → folder-level (the **last** entry in the `folderLevelVars: Environment[]` array wins on collisions — see the merge-order gotcha below, not necessarily the nearest folder) → iteration data → environment → collection (`baseEnvironment`) → global → base global. +- **Falsy values are not skipped**: `Variables.get` uses `.has()` to pick the scope, not truthiness, so a local value of `0`, `''`, or `false` is correctly returned instead of falling through to a lower-precedence scope (verified by a parameterized test in `environments.test.ts`). +- **`collectionVariables` is an alias, not a separate object** — mutating `insomnia.collectionVariables` also mutates `insomnia.baseEnvironment` and vice versa, since they reference the same `Environment` instance. +- **`Variables.set` cannot target non-local scopes** — there is no API on `Variables` to write into environment/collection/global; scripts must use `insomnia.environment.set(...)`, `insomnia.collectionVariables.set(...)`, etc. directly for those scopes. +- **Folder-level merge order**: `mergeFolderLevelVars` merges the given `Environment[]` left-to-right with later entries winning; since `ParentFolders.getEnvironments()` returns folders "from bottom to top" is *not* guaranteed here — actual nearest-wins behavior depends on the order `folderLevelVars` is constructed in by the caller (see `folders.md`). +- **`Vault` returns a `Proxy` from its constructor** — `new Vault(...)` does not yield a plain `Vault` instance; every property access goes through the proxy trap, so `enableVaultInScripts` is (re-)checked on *every* access, not just once at construction. +- **`Vault` is effectively write-protected even when enabled**: `set`, `unset`, and `clear` are all overridden to unconditionally throw, so scripts can only ever read from the vault, never write to it, regardless of the `enableVaultInScripts` flag. +- Deep/nested object values are not directly supported by `Environment`'s type signature (`boolean | number | string | null | undefined`); nothing in this file coerces or validates that at runtime, so anything else the caller passes is stored as-is. + +## Related +- `properties.ts` — not used directly here, but `Variables`/`Environment` sit at the same conceptual layer that the `Variable`/`VariableList` classes (in `variables.ts`) also use for interpolation (via `getInterpolator()` in `interpolator.ts`). +- `folders.ts` — `Folder.environment` is an `Environment` instance; `ParentFolders.getEnvironments()` supplies the `folderLevelVars: Environment[]` array consumed by `Variables`. +- `console.ts` — `getExistingConsole()` is used to emit the NaN warning. +- `interpolator.ts` — `getInterpolator().render(...)` implements `{{...}}` template substitution for `replaceIn`. +- `insomnia.ts` — constructs and wires up all the `Environment`/`Variables`/`Vault` instances exposed as `insomnia.environment`, `insomnia.collectionVariables`, `insomnia.baseEnvironment`, `insomnia.variables`, `insomnia.vault`. diff --git a/.claude/skills/fix-scripting-feature/references/objects/execution.md b/.claude/skills/fix-scripting-feature/references/objects/execution.md new file mode 100644 index 00000000000..c1819f31f51 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/execution.md @@ -0,0 +1,61 @@ +# Execution (`insomnia.execution` / `pm.execution`) + +**Source:** `packages/insomnia-scripting-environment/src/objects/execution.ts` + +## Purpose +Models `pm.execution`: where the currently-running request sits in the collection tree, plus two +collection-runner controls — skipping the request and rerouting to a different next request. This +is one of the fields the collection runner reads back after a script finishes. + +## Public API + +### `interface ExecutionOption` +```ts +interface ExecutionOption { + location: string[]; + skipRequest?: boolean; + nextRequestIdOrName?: string; +} +``` +Plain-object shape used to construct an `Execution` (and the shape stored on `RequestContext.execution`, see `interfaces.md`). + +### `class Execution` +```ts +constructor(options: ExecutionOption) +``` +- `public location: string[]` — **not a plain array**: the constructor wraps a shallow copy + (`[...location]`) in a `Proxy` whose `get` trap adds a `current` accessor returning + `target.length > 0 ? target[target.length - 1] : ''` (the last path segment, or `''` if empty), + mirroring Postman's `execution.location.current` usage. All other property/index access passes + through via `Reflect.get`. Throws `TypeError('Location input must be array of string')` if + `options.location` is not an array. +- `private _skipRequest: boolean` (default `false`), `private _nextRequestIdOrName: string` (default `''`) — no direct getters; only visible via `toObject()`. +- `skipRequest = () => { this._skipRequest = true; }` — one-way flag setter; there is no method to un-set it. +- `setNextRequest = (requestIdOrName: string) => { this._nextRequestIdOrName = requestIdOrName; }` — records which request the collection runner should execute next. +- `toObject = () => ({ location: Array.from(this.location), skipRequest: this._skipRequest, nextRequestIdOrName: this._nextRequestIdOrName })` — serializes state to a plain object; `Array.from` strips the `Proxy`, so the resulting `location` array **loses the `.current` accessor**. + +## Script-facing surface +- `insomnia.execution.location` — array of path segments (e.g. `['project', 'workspace', 'file', 'requestname']`). +- `insomnia.execution.location.current` — last segment of the location path (Postman-compat). +- `insomnia.execution.skipRequest()` — marks the current request to be skipped. +- `insomnia.execution.setNextRequest(requestIdOrName)` — reroutes the collection runner to a specific request by id or name. + +## Gotchas / notable behavior +- `location.current` only works on the live `Proxy`-wrapped array returned from the constructor; + once serialized via `toObject()` (which uses `Array.from`), the plain array no longer has + `.current`. This only matters for the runtime maintainers reading `toObject()`'s output — script + authors reading `insomnia.execution.location.current` directly are unaffected. +- Passing a non-array `location` (e.g. a string) throws a `TypeError` synchronously from the + constructor — this is a `initInsomniaObject`/context-building bug, not something a script can + trigger directly, since scripts only read `insomnia.execution`, they don't construct it. +- There's no public getter for `skipRequest`/`nextRequestIdOrName` state from the script side — + scripts can only set them, not read them back; only `toObject()` (used internally when the + runtime merges results into the persisted request context) exposes the current values. +- Comment references Postman's docs on "using variables in scripts" for the `location.current` + design: https://learning.postman.com/docs/tests-and-scripts/write-scripts/postman-sandbox-api-reference/#using-variables-in-scripts + +## Related +- `insomnia.ts` — `InsomniaObject.execution: Execution`; `toObject()` calls `execution.toObject()`. +- `interfaces.ts` — `ExecutionOption` is embedded as `RequestContext.execution`. +- `packages/insomnia/src/scripting/run-script.ts` — merges `mutatedContextObject.execution` (i.e. `Execution.toObject()`'s output) back into the returned `RequestContext` so the collection runner can act on `skipRequest`/`nextRequestIdOrName`. +- `__tests__/execution.test.ts` — covers the `location`/`current` proxy behavior, `skipRequest()`/`setNextRequest()`, and the invalid-`location` throw. diff --git a/.claude/skills/fix-scripting-feature/references/objects/folders.md b/.claude/skills/fix-scripting-feature/references/objects/folders.md new file mode 100644 index 00000000000..dbb63be8199 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/folders.md @@ -0,0 +1,53 @@ +# Folder / ParentFolders + +**Source:** `packages/insomnia-scripting-environment/src/objects/folders.ts` + +## Purpose +Models the folder hierarchy a request lives in, each folder carrying its own folder-level `Environment` of variables. `ParentFolders` is a container/lookup helper over an ordered array of `Folder`s, and its `getEnvironments()` is what feeds the `folderLevelVars: Environment[]` array consumed by `Variables` (`environments.ts`) — i.e. this is the source of folder-scoped variable resolution for `insomnia.variables`. + +## Public API + +### `class Folder` +```ts +constructor(id: string, name: string, environmentObject: object | undefined) +``` +- `id: string` +- `name: string` +- `environment: Environment` — constructed as `new Environment(`${id}.environment`, environmentObject)` (see `environments.md`). +- `toObject = () => { id: string; name: string; environment: Record }` — returns `id`, `name`, and `environment.toObject()`. + +### `class ParentFolders` +```ts +constructor(private folders: Folder[]) +``` +Per the constructor JSDoc, `folders` is expected "from bottom to top" (nearest-ancestor folder first). + +- `get = (idOrName: string) => Folder` — finds the first folder whose `name` or `id` matches; **throws** `Error('Folder "" not found')` if none match. +- `getById = (id: string) => Folder` — finds by `id` only; throws the same style of error if not found. +- `getByName = (folderName: string) => Folder` — finds by `name` only; throws the same style of error if not found. +- `findValue = (valueKey: string) => value | undefined` — reverses a **copy** of `this.folders` (`[...this.folders].reverse()`) and returns the first folder (after reversal) whose `environment.has(valueKey)`, then returns that folder's `environment.get(valueKey)`; returns `undefined` if no folder has it. +- `toObject = () => object[]` — maps every folder to `folder.toObject()`, preserving the original (non-reversed) array order. +- `getEnvironments = () => Environment[]` — maps every folder to `folder.environment`, preserving the original array order. This is the array handed to `Variables({ folderLevelVars: ... })`. + +## Script-facing surface +Not directly exposed as a `pm.*` object itself — `ParentFolders` is internal plumbing constructed in `insomnia.ts`'s `initInsomniaObject`: +```ts +const parentFolders = new ParentFolders( + rawObj.parentFolders.map(folderObj => new Folder(folderObj.id, folderObj.name, folderObj.environment)), +); +``` +and consumed two ways: +- `parentFolders.getEnvironments()` → passed as `folderLevelVars` into `insomnia.variables` (`Variables`), so scripts reading `insomnia.variables.get('someFolderVar')` are indirectly reading through `Folder.environment`. +- `parentFolders.toObject()` → included in `InsomniaObject.toObject()`'s `parentFolders` field (used for serialization/debugging, not typically read directly by user scripts). +There is no `insomnia.parentFolders` / `insomnia.folders` public property — `parentFolders` is a private field on `InsomniaObject`. + +## Gotchas / notable behavior +- **The two folder-ordering JSDoc comments in this codebase appear to disagree, and `findValue` is not what scripts actually go through for `insomnia.variables`.** `ParentFolders`'s constructor doc says its `folders` array is ordered "from bottom to top" (i.e. index `0` = nearest/bottom, last index = topmost ancestor). `findValue`'s own doc says it searches "starting from the nearest ancestor folder and moving towards the top ancestor folder." But the implementation does `[...this.folders].reverse().find(...)` — reversing a bottom-to-top array produces a top-to-bottom traversal, which is the *opposite* of what `findValue`'s own docstring claims. Treat `findValue`'s precedence as unverified/possibly inverted from its documented intent, and confirm against the actual call order at the construction site (`packages/insomnia/src/network/network.ts`, outside this SDK package) before relying on it to debug a specific precedence question. Note also that `findValue` is a standalone utility on `ParentFolders` — it is **not** what `insomnia.variables.get(...)` uses. +- **`insomnia.variables.get(...)`'s folder precedence is governed by `mergeFolderLevelVars` in `environments.ts`, not by `ParentFolders.findValue`.** `mergeFolderLevelVars` merges the `folderLevelVars: Environment[]` array left-to-right with **later array entries winning** (`{...merged, ...folderLevelEnv.toObject()}`), and `ParentFolders.getEnvironments()` (used to build that array) preserves the original, non-reversed `this.folders` order. Per `environments.test.ts`'s `'variables operations'` test, `folders.getEnvironments()` is passed directly as `folderLevelVars`, and mutating the folder passed **second** to the `ParentFolders` constructor (`folder2`) is what wins in `variables.get('value')` — i.e. whichever folder appears later in the array given to `new ParentFolders([...])` wins ties in `insomnia.variables`, regardless of what "nearest"/"topmost" means in that particular caller's ordering convention. +- `get`, `getById`, and `getByName` all throw plain `Error`s (not a custom error type) with a template string — safe to catch with a generic `try/catch` but there's no error code to switch on. +- `Folder.environment`'s name is derived (`${id}.environment`), not the folder's own `name` — don't assume `folder.environment.name === folder.name`. + +## Related +- `environments.ts` — `Folder.environment` is an `Environment` instance; `ParentFolders.getEnvironments()` supplies `Variables`'s `folderLevelVars`, and `mergeFolderLevelVars` there is what actually implements the effective folder-variable precedence used by `insomnia.variables`. +- `insomnia.ts` — constructs `ParentFolders` from `rawObj.parentFolders` and wires `getEnvironments()` into the `Variables` constructor; exposes `parentFolders.toObject()` via `InsomniaObject.toObject()`. +- `index.ts` — re-exports `Folder` (but not `ParentFolders`) at the top level; `collection.ts` also re-exports `Folder`. diff --git a/.claude/skills/fix-scripting-feature/references/objects/headers.md b/.claude/skills/fix-scripting-feature/references/objects/headers.md new file mode 100644 index 00000000000..274d141390b --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/headers.md @@ -0,0 +1,50 @@ +# Header, HeaderList + +**Source:** `packages/insomnia-scripting-environment/src/objects/headers.ts` + +## Purpose + +Models a single HTTP header (`Header`) and an ordered collection of headers (`HeaderList`). Used by `Request.headers` and `Response.headers` (`pm.request.headers` / `pm.response.headers`), and internally by `Request`'s header-merge logic. `HeaderList` is a thin specialization of the generic `PropertyList`. + +## Public API + +### `HeaderDefinition` (interface) +`{ key: string; value: string; id?: string; name?: string; disabled?: boolean }` + +### `Header` (extends `Property`) +- `override _kind = 'Header'` +- `key: string`, `value: string` +- `constructor(opts: HeaderDefinition | string, name?: string)` — if `opts` is a string, it's parsed via `Header.parseSingle` (only `key`/`value` are set — `id`/`name`/`disabled` come from `Property`'s defaults, not from the string). If `opts` is an object, `id`, `key`, `name` (overridden by the `name` param if given), `value`, and `disabled` are all read from it (each falling back to `''`/`false` if absent). +- `static override _index = 'key'` — tells `PropertyList` to index/dedupe `Header`s by `key` (used by `one`, `indexOf`, `upsert`). +- `static create(input?: {key, value} | string, name?: string): Header` — defaults `input` to `{key: '', value: ''}`. +- `static isHeader(obj: object): boolean` — checks `_kind === 'Header'`. +- `static parse(headerString: string): {key, value}[]` — splits on `\n`, ignores blank lines, parses each via `parseSingle`. +- `static parseSingle(headerStr: string): {key, value}` — splits on the **first** colon. **Throws** `Error('Header.parseSingle: the header string seems invalid')` if there's no colon or it's at position 0. +- `static unparse(headers: {key,value}[] | PropertyList
, separator?: string): string` — maps each header through `unparseSingle` and joins (default separator `'\n'`). +- `static unparseSingle(header: {key,value} | Header): string` — `"key: value"`. +- `update(newHeader: {key, value})` — mutates `this.key`/`this.value` in place. +- `override valueOf()` — returns `this.value`. + +### `HeaderList` (extends `PropertyList`) +- `constructor(parent: PropertyList | undefined, populate: T[])` — internally always constructs the underlying `PropertyList` with `Header` as the type class (regardless of `T`). +- `static isHeaderList(obj: any): boolean` — checks `_kind === 'HeaderList'`. +- `contentSize(): number` — sum of `header.toString().length` (i.e. `"key: value"`.length) across all headers; comment notes special characters aren't handled. +- `override toObject(excludeDisabled?: boolean, _caseSensitive?: boolean, multiValue?: boolean, _sanitizeKeys?: boolean): Record` — builds a plain key→value(s) map. `excludeDisabled` (default falsy) skips headers with `disabled` truthy. `multiValue` (default falsy) collects same-key headers into an array instead of overwriting with the last value. `_caseSensitive` and `_sanitizeKeys` are accepted but **unused**. + +## Script-facing surface + +`pm.request.headers` and `pm.response.headers` are `HeaderList
` instances. Beyond the methods above, all generic `PropertyList` methods are available: `add`/`append`, `all()`, `assimilate()`, `clear()`, `count()`, `each()`, `filter()`, `find()`, `get(key)` / `one(key)` (keyed by `key` since `Header._index = 'key'`), `has()`, `idx()`, `indexOf()`, `insert()`/`insertAfter()`, `map()`, `populate()`, `prepend()`, `reduce()`, `remove()`, `repopulate()`, `toString()`, `upsert()`. Typical script usage: `pm.request.headers.add({key, value})`, `pm.request.headers.get('Content-Type')`, `pm.response.headers.has(...)`, `pm.request.headers.upsert(new Header({...}))`. + +## Gotchas / notable behavior + +- `new Header(stringOpts)` only populates `key`/`value` from the parsed string — `id`, `name`, and `disabled` are left at `Property`'s constructor defaults (`''`, `''`, `false`), never read from the string form. +- `Header.parseSingle` throws on malformed input (no colon) — a raw header string with a typo can throw instead of silently producing an empty/garbage header. +- `PropertyList.one(key)` (inherited, used for `HeaderList.get`) iterates **from the end of the list backwards**, so when duplicate keys exist, the **last-added** matching header wins — relevant when a script both sets a default header and later overrides it with the same key. +- `HeaderList.toObject()`'s `_caseSensitive` and `_sanitizeKeys` parameters exist in the signature (for compatibility with Postman's original API shape) but have **no effect** — only `excludeDisabled` and `multiValue` are actually implemented. +- `HeaderList`'s constructor ignores its own `T` type parameter internally — it always builds the base `PropertyList` using the concrete `Header` class, not whatever `T` was passed. + +## Related + +- `packages/insomnia-scripting-environment/src/objects/properties.ts` — `Property` (base class for `Header`), `PropertyList` (base class for `HeaderList`). +- `packages/insomnia-scripting-environment/src/objects/request.ts` — `Request.headers: HeaderList
`; also reuses `Header`/`HeaderList` in `addHeader`/`removeHeader`/`upsertHeader`/`getHeaders`. +- `packages/insomnia-scripting-environment/src/objects/response.ts` — `Response.headers: HeaderList
`; used in `contentInfo()` to find `Content-Type`/`Content-Disposition`. diff --git a/.claude/skills/fix-scripting-feature/references/objects/insomnia.md b/.claude/skills/fix-scripting-feature/references/objects/insomnia.md new file mode 100644 index 00000000000..72b4da3b12a --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/insomnia.md @@ -0,0 +1,160 @@ +# InsomniaObject (`insomnia` / `pm`) + +**Source:** `packages/insomnia-scripting-environment/src/objects/insomnia.ts` + +## Purpose +This is the top-level scripting object: the single instance bound into the sandbox as `insomnia` +(with `$` as a Postman-compat alias — see Script-facing surface). It aggregates every other object +in this module (environments, variables, request, response, cookies, execution, request info, +vault, client certificates, test assertions) into one value that a pre-request / after-response / +test script reads and mutates, and that the runtime serializes back out via `toObject()` after the +script finishes. + +## Public API + +### `class InsomniaObject` + +```ts +constructor(rawObj: { + globals: Environment; + baseGlobals: Environment; + iterationData: Environment; + environment: Environment; + baseEnvironment: Environment; + variables: Variables; + request: ScriptRequest; + settings: Settings; + clientCertificates: ClientCertificate[]; + cookies: CookieObject; + requestInfo: RequestInfo; + execution: Execution; + response?: ScriptResponse; + parentFolders: ParentFolders; + vault?: Vault; +}) +``` + +Public/exposed properties: +- `environment: Environment` — the currently-selected environment. +- `collectionVariables: Environment` — **same object reference as `baseEnvironment`** (see Gotchas). +- `baseEnvironment: Environment` — the base (root) environment. +- `variables: Variables` — layered variable resolver (globals/env/collection/iteration/folder/local). +- `request: ScriptRequest` — the request being built/sent (`pm.request`). +- `cookies: CookieObject` — cookie jar wrapper (`pm.cookies`). +- `info: RequestInfo` — event/iteration metadata (`pm.info`). +- `response?: ScriptResponse` — present only in after-response/test scripts. +- `execution: Execution` — location/skip/next-request control (`pm.execution`). +- `vault?: Vault` — secret vault accessor, gated by `settings.enableVaultInScripts`. +- `clientCertificates: ClientCertificate[]` — raw client certs available to the request. + +Private/internal properties (TypeScript-only — see Gotchas for why this doesn't restrict scripts at +runtime): `_expect` (chai `expect`), `_test`/`_skip` (from `./test`), `iterationData: Environment`, +`globals: Environment`, `baseGlobals: Environment` (marked +`// TODO: follows will be enabled after Insomnia supports them`), `_settings: Settings`, +`requestTestResults: RequestTestResult[]`, `parentFolders: ParentFolders`. + +Methods: +- `sendRequest(request: string | ScriptRequest, cb: (error?: string, response?: ScriptResponse) => void)` — delegates to `sendRequest()` from `./send-request`, passing the internal `_settings`. +- `test = () => {}` — a no-op placeholder property; the *actual* behavior is installed by the + constructor's `Proxy` (see below). Declaring this field exists mainly so `test` shows up as an + own property. +- `expect = (exp: boolean | number | string | object) => this._expect(exp)` — thin pass-through to chai's `expect`. +- `get settings()` — **always returns `undefined`** (see Gotchas). +- `toObject = () => {...}` — serializes the whole object graph to a plain object: `globals`, + `baseGlobals`, `environment`, `baseEnvironment`, `iterationData` (all via each `Environment`'s + `toObject()`), `variables` (via `variables.localVarsToObject()`), `request`, `settings` (calls + `this.settings`, i.e. always `undefined`), `clientCertificates`, `cookieJar` (via + `cookies.jar().toInsomniaCookieJar()`), `info` (via `info.toObject()`), `response` (via + `response.toObject()` or `undefined`), `requestTestResults`, `execution` (via + `execution.toObject()`), `parentFolders` (via `parentFolders.toObject()`). + +The constructor returns `new Proxy(this, { get: ... })`: any property access other than `'test'` +passes straight through via `Reflect.get`. Accessing `.test` instead returns a freshly-built +`TestHandler` function that calls `this._test(msg, fn, this.pushRequestTestResult)`, with a +`.skip` method that calls `this._skip(msg, fn, this.pushRequestTestResult)`. `pushRequestTestResult` +appends each `RequestTestResult` to the private `requestTestResults` array (immutably, via spread). + +### `async function initInsomniaObject(rawObj: RequestContext, log: (...args: any[]) => void): Promise` + +Factory used by the script runner to build an `InsomniaObject` from a `RequestContext` snapshot +(see `interfaces.md`). Responsibilities, in order: +- Maps `globals`/`baseGlobals` — if the same environment id is selected for both, `globals` and + `baseGlobals` become the *same* `Environment` instance; otherwise separate instances are created. +- Maps `environment`/`baseEnvironment` the same way; if no sub-environment is selected (ids equal), + logs a warning via `log(...)` that mutations to `insomnia.environment` will apply to the base + environment. +- Builds `iterationData` and `transientVariables`-backed local variables `Environment`s (defaulting + to empty named environments when absent). +- Builds a `Vault` from `rawObj.vault` (or an empty object), gated by + `rawObj.settings?.enableVaultInScripts` (defaults `false`). +- Builds `CookieObject` from `rawObj.cookieJar`. +- Builds `RequestInfo` from `rawObj.requestInfo` plus `rawObj.request.name`/`_id`. +- Builds `ParentFolders` from `rawObj.parentFolders`, then a `Variables` instance that layers + base-global/global/environment/collection/iteration-data/folder-level/local variables. +- Resolves client certificates: uses `checkIfUrlIncludesTag` (from `./utils`) and + `filterClientCertificates` (from `insomnia/src/network/certificate`) against `rawObj.request.url`. + If the URL contains template tags or no certificate matches, initializes an **empty** default + certificate and logs a warning via `getExistingConsole().warn(...)`; otherwise uses the first + matched certificate. +- Builds the request URL (`toUrlObject`), proxy options (`transformToSdkProxyOptions`, + `resolveProtocolForProxy`), adds query params, builds the auth via `toPreRequestAuth`, and + constructs a `ScriptRequest`. +- Constructs an `Execution` from `rawObj.execution`. +- Reads the response body via `readBodyFromPath` and builds a `ScriptResponse` via + `toScriptResponse` if `rawObj.response` is present. +- Returns `new InsomniaObject({...})` with everything above. + +## Script-facing surface +`InsomniaObject` *is* what a script sees as the global `insomnia` object (and `$`, a Postman-compat +alias set up as `const $ = insomnia;` by the script wrapper in +`packages/insomnia/src/scripting/run-script.ts`). Nearly every `pm.*`/`insomnia.*` call in a script +routes through this object's properties/methods directly: +- `insomnia.environment`, `insomnia.collectionVariables`, `insomnia.baseEnvironment`, `insomnia.variables` +- `insomnia.request`, `insomnia.response`, `insomnia.cookies`, `insomnia.info`, `insomnia.execution`, `insomnia.vault` +- `insomnia.test(name, fn)` / `insomnia.test.skip(name, fn)` +- `insomnia.expect(value)` +- `insomnia.sendRequest(request, callback)` + +`run-script.ts` requires the script's `insomnia` result to be `instanceof InsomniaObject` — if the +user script returns early or otherwise breaks that invariant, `runScript` throws +`'insomnia object is invalid or script returns earlier than expected.'`. After the script runs, +`InsomniaObject.toObject()` is used to merge mutations back into the persisted `RequestContext`. + +## Gotchas / notable behavior +- **`collectionVariables` is not a copy** — `this.collectionVariables = this.baseEnvironment;` in + the constructor means `insomnia.collectionVariables` and `insomnia.baseEnvironment` are the exact + same `Environment` instance. Mutating one via a script mutates the other. +- **`insomnia.settings` is always `undefined`** — the `get settings()` accessor unconditionally + `return;`s nothing, even though the real `Settings` object is held internally as `_settings` (used + only for `sendRequest`'s proxy/certificate resolution). `toObject().settings` is therefore always + `undefined` too, regardless of what `Settings` were passed in. +- **`globals`/`baseGlobals` are `private` in name only** — `private` is TypeScript-only and erased at runtime, and the constructor's `Proxy` forwards them (via `Reflect.get`, no `set` trap) same as any other property, so scripts can read *and* write `insomnia.globals`/`insomnia.baseGlobals` despite them being absent from the TS-declared public surface. Accidental exposure, not a supported feature. +- **The `test` proxy is easy to miss when reading the class** — the `test = () => {}` field looks + like the real implementation, but any actual call to `insomnia.test(...)` is intercepted by the + constructor's `Proxy` `get` trap before it ever reaches that field. +- **Certificate fallback is silent unless you check logs** — if the request URL contains + `{{`/`}}`/`{%`/`%}` template tags, or no client certificate matches the host, an *empty* default + certificate is substituted and a warning is written via the script console rather than thrown as + an error. +- **No-environment-selected warning** — selecting the base environment (rather than a + sub-environment) causes `initInsomniaObject` to log a warning that `insomnia.environment` + mutations will land on the base environment, because `environment` and `baseEnvironment` are the + same instance in that case. + +## Related +- `interfaces.ts` — defines `RequestContext`, the input shape `initInsomniaObject` consumes. +- `execution.ts` — `Execution`, held as `insomnia.execution`. +- `request-info.ts` — `RequestInfo`, held as `insomnia.info`. +- `environments.ts` — `Environment`, `Variables`, `Vault` classes used throughout. +- `cookies.ts` — `CookieObject`, held as `insomnia.cookies`. +- `folders.ts` — `Folder`, `ParentFolders`, used for folder-level variables. +- `request.ts` — `Request`/`RequestOptions`/`toScriptRequestBody`, builds `insomnia.request`. +- `response.ts` — `toScriptResponse`, `readBodyFromPath`, builds `insomnia.response`. +- `send-request.ts` — implements `insomnia.sendRequest(...)`. +- `test.ts` — `test`/`skip`/`TestHandler`, implements `insomnia.test(...)`. +- `auth.ts` — `toPreRequestAuth`, used when building the request's auth config. +- `proxy-configs.ts` — `transformToSdkProxyOptions`, used for request proxy settings. +- `urls.ts` — `toUrlObject`, `resolveProtocolForProxy`. +- `utils.ts` — `checkIfUrlIncludesTag`, used for certificate-fallback detection. +- `console.ts` — `getExistingConsole`, used to log warnings. +- `packages/insomnia/src/scripting/run-script.ts` — binds this object as the sandbox global `insomnia` and calls `toObject()` after the script runs. diff --git a/.claude/skills/fix-scripting-feature/references/objects/interfaces.md b/.claude/skills/fix-scripting-feature/references/objects/interfaces.md new file mode 100644 index 00000000000..95c5a8646cb --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/interfaces.md @@ -0,0 +1,80 @@ +# Shared interfaces (`RequestContext`, `IEnvironment`) + +**Source:** `packages/insomnia-scripting-environment/src/objects/interfaces.ts` + +## Purpose +Pure type-only file (no runtime code) defining the shared shapes used to pass state into and out +of the scripting environment. `RequestContext` is the full serialized state snapshot that the host +app (`packages/insomnia`) builds before running a script and that `initInsomniaObject` (in +`insomnia.ts`) turns into a live `InsomniaObject`. + +## Public API + +### `interface IEnvironment` +```ts +interface IEnvironment { + id: string; + name: string; + data: object; +} +``` +Minimal serialized shape for any environment-like blob (globals, base globals, environment, base +environment, vault). Marked `/** @ignore */` (excluded from generated public docs). + +### `interface RequestContext` +```ts +interface RequestContext { + request: Request; + timelinePath: string; + environment: IEnvironment; + baseEnvironment: IEnvironment; + vault?: IEnvironment; + collectionVariables?: object; + globals?: IEnvironment; + baseGlobals?: IEnvironment; + iterationData?: Omit; + timeout: number; + settings: Settings; + clientCertificates: ClientCertificate[]; + cookieJar: CookieJar; + response?: any; + requestTestResults?: RequestTestResult[]; + requestInfo: RequestInfoOption; + execution: ExecutionOption; + logs: string[]; + transientVariables?: Omit; + parentFolders: { id: string; name: string; environment: Record }[]; +} +``` +Full input/output snapshot for one script execution. Fields of note: +- `request: Request` — the raw request model (from `insomnia-data`), not the script-facing `ScriptRequest` wrapper in `request.ts`. +- `globals`/`baseGlobals` — optional; per the inline comment, "activated only when selected". +- `iterationData`/`transientVariables` — typed as `Omit` (no `id` field needed for these). +- `response?: any` — deliberately untyped; comment notes "Callback types defined elsewhere to avoid circular imports". +- `requestInfo: RequestInfoOption` and `execution: ExecutionOption` — imported from `./request-info` and `./execution` respectively; these are the plain-object option shapes consumed by the `RequestInfo`/`Execution` constructors (see `request-info.md`/`execution.md`). +- `parentFolders` — array of plain folder descriptors (`id`, `name`, `environment` data), consumed by `ParentFolders`/`Folder` in `folders.ts`. + +Both interfaces are marked `/** @ignore */`, meaning they're internal/plumbing types not meant to +appear in the generated public SDK reference. + +## Script-facing surface +None directly. A user's pre-request/after-response/test script never sees a `RequestContext` or +`IEnvironment` value — these are internal transport types used by the host application +(`packages/insomnia/src/scripting/run-script.ts`) to hand state into `initInsomniaObject` and to +receive mutated state back out via `InsomniaObject.toObject()`. + +## Gotchas / notable behavior +- `globals`/`baseGlobals`/`vault` are all optional — code reading them elsewhere (e.g. + `initInsomniaObject`) must handle `undefined` explicitly (it does, via `?.` and fallbacks). +- `response` being typed `any` means no compile-time safety on the response shape passed into the + scripting environment; the real shape is validated only implicitly by how `toScriptResponse` (in + `response.ts`) consumes it. +- This file has no runtime exports (interfaces only) — importing it has zero side effects. + +## Related +- `insomnia.ts` — `initInsomniaObject(rawObj: RequestContext, log)` is the sole consumer that turns this shape into a live `InsomniaObject`. +- `execution.ts` — supplies `ExecutionOption`, embedded as `RequestContext.execution`. +- `request-info.ts` — supplies `RequestInfoOption`, embedded as `RequestContext.requestInfo`. +- `environments.ts` — `Environment`/`Vault` classes are constructed from `IEnvironment`-shaped data. +- `folders.ts` — `Folder`/`ParentFolders` are constructed from `RequestContext.parentFolders`. +- `packages/insomnia/src/scripting/run-script.ts` — builds and consumes `RequestContext` around each script run. diff --git a/.claude/skills/fix-scripting-feature/references/objects/interpolator.md b/.claude/skills/fix-scripting-feature/references/objects/interpolator.md new file mode 100644 index 00000000000..5ffe42f7095 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/interpolator.md @@ -0,0 +1,67 @@ +# Interpolator (template-tag rendering) + +**Source:** `packages/insomnia-scripting-environment/src/objects/interpolator.ts` + +## Purpose +Internal helper providing `{{variable}}`-style template rendering (plus `{{$fakerFn}}` dynamic +value generation) on top of LiquidJS. It backs every "replace variables in this string" operation +exposed by the environment/property objects (e.g. `insomnia.environment.replaceIn(...)`). + +## Public API + +### `class Interpolator` (not exported — module-private) +```ts +constructor() +``` +Builds a `Liquid` engine configured with: +- `outputDelimiterLeft: '{{'`, `outputDelimiterRight: '}}'` — variable interpolation delimiters. +- `tagDelimiterLeft: '{%'`, `tagDelimiterRight: '%}'` — Liquid tag delimiters (e.g. `{% if %}`). +- `strictVariables: true` — referencing an undefined variable causes a render error rather than silently rendering empty. +- `jsTruthy: true` — use JS truthiness semantics instead of Liquid's default truthy rules. +- `ownPropertyOnly: false` — allows resolving inherited/prototype properties on the context object, not just own properties. + +Methods: +- `render = async (template: string, context: object): Promise` — runs `renderWithFaker(template)` first (to substitute any `{{$fakerFn}}` tags with generated values), then parses/renders the result through the Liquid engine against `context`. Comment: `// TODO: support plugins`. +- `renderWithFaker = (template: string) => string` — a pre-pass over the template string, done via + manual string splitting (not Liquid) rather than regex/AST parsing: + 1. Splits `template` on `'}}'`. + 2. For each segment, finds the last `'{{'` before the end of the segment. + 3. If the found tag name starts with `'$'`, strips the `$` prefix, looks it up in + `fakerFunctions` (from `insomnia/src/common/templating/faker-functions`), calls it, and + splices the generated value in place of the tag. + 4. If the tag name does not start with `$`, the segment (and its `}}`) is left untouched for the + real Liquid engine to interpolate as a normal variable. + 5. Throws `Error('replaceIn: no faker function is found: ${funcName}')` if the `$`-prefixed name + isn't a recognized faker function. + +### `function getInterpolator(): Interpolator` +Returns the single module-level `Interpolator` instance (`const interpolator = new Interpolator();`), constructed once at module load. This is the only exported symbol. + +## Script-facing surface +No direct surface — scripts never call `getInterpolator()` themselves. It is reached indirectly +through higher-level methods on other objects, all of which delegate to +`getInterpolator().render(...)`: +- `insomnia.environment.replaceIn(template)` / same method on `baseEnvironment`, `collectionVariables`, `variables`, etc. (defined in `environments.ts`) — e.g. `insomnia.environment.replaceIn("My id is {{$randomUUID}}")` or `insomnia.environment.replaceIn("Visiting URL: {{urlValueFromEnvironment}}")`. +- `Property.replaceSubstitutions(content, ...variables)` and `Property.replaceSubstitutionsIn(obj, ...variables)` (static methods in `properties.ts`) — used internally wherever a `Property`-derived object needs variable substitution against multiple variable-scope objects merged together. + +## Gotchas / notable behavior +- `renderWithFaker` is a hand-rolled string scan, not a real parser — it looks for the *last* + `'{{'` before each `'}}'` boundary in each split segment. Malformed or nested template syntax + could confuse this pass before Liquid ever sees the template. +- `strictVariables: true` means referencing a variable name that isn't present in `context` will + cause the Liquid render to throw/reject, not silently produce an empty string — a common source + of "my `{{someVar}}` template failed" script errors when the variable isn't actually defined in + any active scope. +- `$`-prefixed faker tags are resolved by this module's own pre-pass, *before* Liquid — an unknown + `$xyz` faker function name throws synchronously from `renderWithFaker` with the message + `replaceIn: no faker function is found: xyz`. +- `getInterpolator()` always returns the same singleton `Interpolator` instance across the whole + process — there is no way to get a differently-configured interpolator or to reset its Liquid + engine mid-run. +- The `Interpolator` class itself is not exported; only `getInterpolator()` is, so consumers cannot + construct their own instance or subclass it. + +## Related +- `environments.ts` — `Environment`/`Variables`/`Vault`'s `replaceIn(...)` methods call `getInterpolator().render(...)`. +- `properties.ts` — `Property.replaceSubstitutions`/`replaceSubstitutionsIn` call `getInterpolator().render(...)`. +- `insomnia/src/common/templating/faker-functions` (outside this package) — supplies the `fakerFunctions` map used for `{{$fakerFn}}` tags. diff --git a/.claude/skills/fix-scripting-feature/references/objects/properties.md b/.claude/skills/fix-scripting-feature/references/objects/properties.md new file mode 100644 index 00000000000..575b5295274 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/properties.md @@ -0,0 +1,100 @@ +# PropertyBase / Property / PropertyList + +**Source:** `packages/insomnia-scripting-environment/src/objects/properties.ts` + +## Purpose +Base classes that most other SDK objects extend, mirroring Postman's `postman-collection` `Property`/`PropertyList` model: `PropertyBase` provides parent-chain traversal and JSON serialization, `Property` adds `id`/`name`/`disabled` plus static template-substitution helpers, and `PropertyList` is a generic ordered/keyed collection with Postman-compatible methods (`add`, `each`, `filter`, `find`, `one`, `upsert`, `toObject`, etc.). Concrete subclasses (`Variable`/`VariableList`, `Header`/`HeaderList`, `Cookie`/`CookieList`, `ProxyConfig`/`ProxyConfigList`, etc.) build on top of these. + +## Public API + +### `unsupportedError(featureName: string, alternative?: string): Error` +Builds an `Error` with message `` `${featureName} is not supported yet` `` (optionally appending `, please use ${alternative} instead temporarily.` when `alternative` is given). Used throughout the SDK (e.g. `Variable.types()`, `PropertyList.eachParent`) to mark stubbed-out Postman-compat methods. + +### `class PropertyBase` +```ts +constructor(description?: string) +``` +- `public _kind = 'PropertyBase'` — used as a lightweight runtime type tag (checked via `'_kind' in obj` elsewhere, e.g. `VariableList.isVariableList`). +- `protected _parent: PropertyBase | undefined` — set only via subclasses/external assignment; no setter method is provided on the base class itself. +- `protected description?: string`. +- `static propertyIsMeta(_value: any, key: string): boolean` — `key && key.startsWith('_')` (keys starting with `_` are treated as "meta"). +- `static propertyUnprefixMeta(_value: any, key: string): string` — strips a leading `_` from `key` if present. +- `meta(): {}` — always returns an empty object (not implemented beyond the stub). +- `parent(): PropertyBase | undefined` — returns `this._parent`. +- `forEachParent(_options: { withRoot?: boolean }, iterator: (obj: PropertyBase) => boolean): PropertyBase[] | undefined` — BFS-style walk up the parent chain (via `parent()`), cloning each ancestor with the `clone` package before passing it to `iterator`; stops as soon as `iterator` returns falsy, and always returns the array of cloned ancestors visited so far (or `undefined` if there is no parent at all). `_options` is accepted but never read. +- `findInParents(property: string, customizer?: (ancestor: PropertyBase) => boolean): PropertyBase | undefined` — walks the (cloned) parent chain looking for the first ancestor whose `meta()` keys include `property`; if `customizer` is given, keeps walking until `customizer(ancestor)` returns truthy, otherwise returns the first ancestor with that meta key. Returns `undefined` if no parent, or none matches. +- `toJSON(): Record` — returns `Object.entries(this)` filtered to drop function-valued entries, `undefined` values, and the `_kind` key. +- `toObject(): Record` — returns `this.toJSON()`. +- `toString(): string` — `JSON.stringify(this.toJSON())`. + +### `class Property extends PropertyBase` +```ts +constructor(id?: string, name?: string, disabled?: boolean, info?: { id?: string; name?: string }) +``` +`info.id`/`info.name` take priority over the positional `id`/`name` params if both are given; `this._kind` is set to `'Property'`; `disabled` defaults to `false`. +- `id: string`, `name?: string`, `disabled?: boolean`. +- `static _index = 'id'` — the field name `PropertyList` uses to key/index items of this type; overridden by subclasses (e.g. `Variable._index = 'key'`). +- `static async replaceSubstitutions(content: string, ...variables: object[]): Promise` — throws `TypeError` if `variables` isn't an array or `content` isn't a string. Merges `variables` into a single context object: the array is `.reverse()`d first, then folded left-to-right with `{...context, ...variable}`, so the reversed (originally-last) entries are applied first and get overwritten by earlier ones — net effect: **the leftmost/first argument passed to `replaceSubstitutions` wins** on key collisions, matching the inline comment ("the searching priority of rendering is from left to right"). Renders `content` against that merged context via `getInterpolator().render(...)`. +- `static async replaceSubstitutionsIn(obj: object, ...variables: object[]): Promise` — same signature/merge semantics as `replaceSubstitutions`, but stringifies `obj` with `JSON.stringify`, renders it, then `JSON.parse`s the result. Throws `TypeError` for bad args up front; wraps any other error (e.g. a `JSON.parse` failure on non-JSON-safe rendered output) in a new `Error('replaceSubstitutionsIn: ' + e.toString())`. +- `describe(content: string, typeName: string): void` — sets `this._kind = typeName` and `this.description = content`. + +### `class PropertyList` +```ts +constructor( + protected typeClass: { _index?: string }, + protected parent: Property | PropertyList | undefined, + populate: T[], +) +``` +- `protected _kind = 'PropertyList'`, `protected list: T[]`. +- `static isPropertyList(obj: object): boolean` — `'_kind' in obj && obj._kind === 'PropertyList'`. +- `add(item: T): void` — pushes to the end of `list`. +- `all(): Record[]` — `list.map(pp => pp.toJSON())`. +- `append(item: T): void` — alias for `add`. +- `assimilate(source: T[] | PropertyList, prune?: boolean): void` — if `prune`, calls `clear()` first; then pushes all of `source`'s items (`source.list` if it's a `PropertyList`, else the array itself) onto `list`. Per an inline comment, "it doesn't update values from a source list" (i.e. this appends, it does not merge/replace by key). +- `clear(): void` — `list = []`. +- `count(): number` — `list.length`. +- `each(iterator: (item: T) => void, context: object): void` — calls `list.forEach(iterator)`; `context` is stashed as `iterator.context` (assigned onto the function) but the iterator itself is not invoked with that context bound — purely a Postman-compat artifact. +- `eachParent(_iterator, _context?): never` — **always throws** `unsupportedError('eachParent')`; not implemented ("properties are not organized as hierarchy" per the TODO comment). +- `filter(rule: (item: T) => boolean, context: object): T[]` — same `context`-stash pattern as `each`; returns `list.filter(rule)`. +- `find(rule: (item: T) => boolean, context?: object): T | undefined` — same pattern; `list.find(rule)`. +- `get(key: string): T | undefined` — alias for `one(key)`. +- `has(item: T, _value?: any): boolean` — `indexOf(item) >= 0`; `_value` is accepted but unused ("its usage is unknown" per comment). +- `idx(index: number): T | undefined` — returns `list[index]` if `index <= list.length - 1`, else `undefined`. +- `indexOf(item: string | T): number` — looks up the index field via `typeClass._index || 'id'`; if `item` is a string, matches `record[indexFieldName] === item`; otherwise matches `record[indexFieldName] === (item as Record)[indexFieldName]`. Returns `-1` if not found. +- `insert(item: T, before?: number): void` — splices `item` in before index `before` if valid (`before != null && before >= 0 && before <= list.length - 1`), else falls back to `append(item)`. +- `insertAfter(item: T, after?: number): void` — splices `item` in right after index `after` under the same bounds check, else falls back to `append(item)`. +- `map(iterator: (item: T) => any, context: object): any[]` — same `context`-stash pattern; `list.map(iterator)`. +- `one(id: string): T | undefined` — scans `list` **backwards** (`for (let i = list.length - 1; i >= 0; i--)`) for the first record whose index field equals `id`; if the matched item has a callable `valueOf`, returns `item.valueOf()` instead of the item itself; else returns the item as-is. Returns `undefined` if not found. +- `populate(items: T[]): void` — `list = [...list, ...items]`. +- `prepend(item: T): void` — `list = [item, ...list]`. +- `reduce(iterator: (acc: any, item: T) => any, accumulator: any, context: object): any` — same `context`-stash pattern; `list.reduce(iterator, accumulator)`. +- `remove(predicate: T | ((item: T) => boolean), context: object): void` — if `predicate` is a function, keeps everything that does *not* match it (via `filter` with the negated predicate); if `predicate` is a value, keeps everything not `deep-equal` to it. +- `repopulate(items: T[]): void` — `clear()` then `populate(items)`. +- `toObject(_excludeDisabled?, _caseSensitive?, _multiValue?, _sanitizeKeys?): Record | Record[]` — base implementation ignores all four arguments and returns `list.map(elem => elem.toJSON())` (an array, not a keyed object) — it's the fallback for lists whose items have no natural key (e.g. `UrlMatchPatternList`, or a raw `PropertyList` not used through a subclass). Subclasses backed by keyed items (`CookieList`, `HeaderList`, `VariableList`, `ProxyConfigList`) override this with a real key→value map that supports `excludeDisabled`/`multiValue`; `caseSensitive`/`sanitizeKeys` remain unsupported even in those overrides. +- `toString(): string` — `` `[${list.map(item => item.toString()).join('; ')}]` ``. +- `upsert(item: T): boolean` — returns `false` immediately if `item == null`. If `indexOf(item)` finds an existing entry, **splices it out and reinserts the new item at the same position** (returns `false`, meaning "updated, not inserted"); otherwise calls `add(item)` and returns `true` ("inserted new"). Note: the "splice out, then splice again" implementation calls `this.list.splice(...)` twice on the *same* array reference inside one expression (`[...this.list.splice(0, itemIdx), item, ...this.list.splice(itemIdx + 1)]`) — the first `splice` call mutates `this.list` in place before the second one runs against the now-shortened array. + +## Script-facing surface +Not directly exposed as `pm.*`/`insomnia.*` itself — these are base classes. Scripts interact with them only through concrete subclasses' public APIs, e.g.: +- `pm.variables`/collections of variables → `VariableList` (extends `PropertyList`), `Variable` (extends `Property`) — see `variables.md`. +- `pm.request.headers`, `pm.response.headers` → `HeaderList`/`Header`. +- `pm.request.url.query` → `UrlMatchPatternList`/similar list types. +- Any script calling `.get()`, `.one()`, `.each()`, `.filter()`, `.upsert()`, `.toObject()`, etc. on a collection-like SDK object is going through `PropertyList`'s implementation documented above. +- `Property.replaceSubstitutions`/`replaceSubstitutionsIn` back the SDK's `{{variable}}` template rendering wherever it's invoked as a static helper (distinct from, but functionally similar to, `Environment.replaceIn`/`Variables.replaceIn` in `environments.ts`). + +- **`replaceSubstitutions`/`replaceSubstitutionsIn` mutate their own `variables` rest-array via `.reverse()`** before merging — this is an implementation detail (not observable to callers since `variables` is a fresh rest-parameter array each call), but worth knowing if you're stepping through this code: the reversal is what makes the final left-to-right merge produce "leftmost argument wins" precedence. +- **`eachParent` always throws** `unsupportedError('eachParent')` — calling it from a script will break, regardless of arguments. +- **`each`/`filter`/`find`/`map`/`reduce`'s `context` parameter does nothing functionally** — it's stashed as a property on the callback function object (`it.context = context`) purely for Postman API-shape compatibility, but the callback is invoked with `Array.prototype`'s normal (unbound) semantics, so relying on `context` for `this`-binding inside the iterator will not work as it might in Postman. +- **`one(id)` scans backwards and unwraps `valueOf()`** — if two items share the same index-field value, `one()` returns the *last*-added one (matching most recently `add`/`upsert`ed semantics), and if that item defines a custom `valueOf`, the returned object is `item.valueOf()`, not the raw list item. +- **`upsert(item)` replaces the item at the same index** via `[...this.list.slice(0, itemIdx), item, ...this.list.slice(itemIdx + 1)]`, leaving every other item in place. +- **`toObject()`'s base implementation returns an array, not an object**, unlike its keyed subclass overrides (`VariableList.toObject()` etc., which return `Record`) — code that assumes every `PropertyList.toObject()` yields a key/value map will break for list types that don't override it (e.g. `UrlMatchPatternList`). +- `PropertyBase.forEachParent`/`findInParents` both `clone()` each ancestor before handing it to the caller/comparing — mutations the iterator/customizer performs on the passed-in ancestor object do not affect the real parent chain. +- `Property`'s constructor lets `info.id`/`info.name` silently override the positional `id`/`name` arguments — passing both can be surprising if not intentional. + +## Related +- `variables.ts` — `Variable extends Property`, `VariableList extends PropertyList` (see `variables.md`). +- `environments.ts` — a separate, non-`Property`-based variable model (`Environment`/`Variables`) used for `insomnia.environment`/`insomnia.variables`/`insomnia.collectionVariables`; don't conflate the two when tracing a variable bug. +- `interpolator.ts` — `getInterpolator().render(...)` is what `Property.replaceSubstitutions`/`replaceSubstitutionsIn` delegate to for `{{...}}` template rendering. +- `headers.ts`, `cookies.ts`, `proxy-configs.ts`, `urls.ts` — other concrete `Property`/`PropertyList` subclasses that override `toObject()` with their own keyed semantics. +- `collection.ts` / `index.ts` — re-export `Property`, `PropertyBase`, `PropertyList` as part of the SDK's public surface. diff --git a/.claude/skills/fix-scripting-feature/references/objects/proxy-configs.md b/.claude/skills/fix-scripting-feature/references/objects/proxy-configs.md new file mode 100644 index 00000000000..1175ffbcab6 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/proxy-configs.md @@ -0,0 +1,65 @@ +# ProxyConfig, ProxyConfigList + +**Source:** `packages/insomnia-scripting-environment/src/objects/proxy-configs.ts` + +## Purpose +Models a single proxy configuration (`pm.request.proxy`) and a keyed list of them (`ProxyConfigList`, exported for scripts to build their own lists, e.g. via `require('insomnia-collection')`). Also provides `transformToSdkProxyOptions`, the function that converts Insomnia's native proxy settings (`httpProxy`/`httpsProxy`/`proxyEnabled`/`noProxy` strings from Settings) into the SDK's `ProxyConfigOptions` shape used to seed `pm.request.proxy`. + +## Public API + +### Types +- `interface ProxyConfigOptions { match: string; host: string; port?: number; tunnel: boolean; disabled?: boolean; authenticate: boolean; username: string; password: string; bypass?: string[]; protocol: string }` — constructor input for `ProxyConfig`. `match` is a URL match pattern string (see `urls.ts`) initializing a `UrlMatchPattern` internally, e.g. `'http+https://example.com/*'`. `bypass` and `protocol` are called out in comments as "for compatibility with Insomnia". + +### `class ProxyConfig extends Property` +- `override _kind = 'ProxyConfig'` +- `host: string` +- `match: string` — the raw match-pattern string (not a `UrlMatchPattern` instance on the instance property, though a `UrlMatchPattern` is constructed on the fly inside `getProtocols()`/`test()`). +- `port?: number` +- `tunnel: boolean` +- `authenticate: boolean` +- `username: string` +- `password: string` +- `bypass: string[]` — list of hosts/URLs to bypass the proxy for; comment: "for compatibility with Insomnia's bypass list". +- `protocol: string` — e.g. `"http"`/`"https"`. +- `static authenticate = false`, `static bypass: UrlMatchPatternList = new UrlMatchPatternList(undefined, [])`, `static host = ''`, `static match = ''`, `static password = ''`, `static port?: number = undefined`, `static tunnel = false` (comment: "unsupported"), `static username = ''`, `static protocol = 'https:'` — all marked `@ignore` in JSDoc with the comment "following properties are hidden as they are not used while must be exposed". These are class-level defaults, distinct from the instance properties of the same name. +- `constructor(def: { id?: string; name?: string; match: string; host: string; port?: number; tunnel: boolean; disabled?: boolean; authenticate: boolean; username: string; password: string; bypass?: string[]; protocol: string })` — sets `id`/`name` from `def` or defaults to `''`, `disabled` defaults to `false`, `bypass` defaults to `[]` if omitted. All other fields copied straight through (no defaulting). +- `static override _index = 'key'` — Note: `ProxyConfig` has no instance field literally named `key`; `PropertyList.indexOf()`/`.one()` would look for a `key` property on the record, which doesn't exist on `ProxyConfig` — see Gotchas. (`ProxyConfigList.toObject()` keys its map by `match`, not by this `_index`.) +- `static isProxyConfig(obj: object): boolean` — checks `obj._kind === 'ProxyConfig'`. +- `getProtocols(): string[]` — builds a `UrlMatchPattern(this.match)` and returns `.getProtocols()` (parses the `'+'`-delimited protocol prefix of the match string, e.g. `'http+https://...'` → `['http', 'https']`). +- `getProxyUrl(): string` — builds the full proxy URL: `` `${protocol}//${username}:${password}@${host}${port}` `` if `authenticate` is true, otherwise `` `${protocol}//${host}${port}` `` (port segment omitted entirely if `port === undefined`). +- `test(url?: string): boolean` — returns `false` immediately if `url` is falsy (comment: "TODO: it is confusing in which case url arg is optional"). Returns `false` if `url` is literally in `this.bypass` (exact string match, not pattern matching). Otherwise delegates to `new UrlMatchPattern(this.match).test(url)`. +- `update(options: Omit): void` — updates `host`, `match`, `port`, `tunnel`, `authenticate`, `username`, `password`. Does **not** allow updating `bypass` or `protocol` (both omitted from the type, and untouched by the method body). +- `updateProtocols(_protocols: string[]): never` — always throws `Error('updateProtocols is not supported in Insomnia')`. Comment: "In Insomnia there is no whitelist while there is a blacklist." + +### `class ProxyConfigList extends PropertyList` +- `constructor(parent: PropertyList | undefined, populate: T[])` — constructs the underlying `PropertyList` with `typeClass = ProxyConfig`. +- `static isProxyConfigList(obj: any): boolean` — checks `obj._kind === 'ProxyConfigList'`. +- `resolve(url?: Url): object | null` — returns `null` if `url` is falsy. Otherwise stringifies `url`, filters the list to configs whose `.test(urlStr)` is true, maps matches to `.toJSON()`, and returns the **first** match's JSON (or `null` if none match). Comment: "It only returns the first one if multiple matches are found." +- `override toObject(excludeDisabled?, _caseSensitive?, multiValue?, _sanitizeKeys?): Record` — builds a plain object keyed by each config's `match` string, using `Object.create(null)` (no prototype). `excludeDisabled` skips `disabled` configs. `multiValue` collects same-`match` configs into an array; otherwise duplicate `match` values collapse to the last one. + +### Module-level function +- `transformToSdkProxyOptions(protocol: string, httpProxy: string, httpsProxy: string, proxyEnabled: boolean, noProxy: string): ProxyConfigOptions` — computes `proxyHost` (the string actually parsed into `host`/`port`/etc. below) as `httpsProxy` or `httpProxy` based on whether `protocol === 'https:'`. But `enabledProxy` is computed separately, from `proxyEnabled && (httpsProxy || httpProxy || '').trim() !== ''` — **not** from `proxyHost` — so it's `true` whenever *either* proxy string is non-empty, regardless of which one the current `protocol` selected (see Gotchas). Splits `noProxy` on commas (trimmed) into the `bypass` list. Always returns `match: ''` (`UrlMatchPattern.MATCH_ALL_URLS`). If the proxy is enabled and a host string is present, parses it with the built-in `URL` class (prefixing `${protocol}//` if the string has no `://`), extracting `port` (only set if non-empty, then `Number.parseInt(..., 10)`), `protocol`, `host` (hostname), `username`, `password`; sets `authenticate = true` if either `username` or `password` came through; logs `` `Using proxy: ${sanitizedProxy}` `` via `getExistingConsole().warn`. Throws `Error('Failed to parse proxy (${protocol}//${proxyHost}): ${e.message}')` if URL parsing fails **and the proxy is enabled** (parsing is skipped entirely, no throw, when the proxy is disabled). + +## Script-facing surface +- `pm.request.proxy` is a single `ProxyConfig` instance (per `request.ts`: `const proxy = options.proxy ? new ProxyConfig(options.proxy) : undefined;`), seeded from Insomnia's Settings-level proxy configuration via `transformToSdkProxyOptions` (called in `insomnia.ts`). It is `undefined` if no `proxy` option was supplied when constructing the request. +- Scripts read/write `pm.request.proxy.host`, `.port`, `.username`, `.password`, `.authenticate`, `.tunnel`, `.bypass`, `.protocol` directly, call `.getProxyUrl()` to see the resolved proxy URL, `.test(url)` to check whether a given URL should go through this proxy, and `.update({...})` to change host/match/port/tunnel/authenticate/username/password. +- `ProxyConfigList` and `ProxyConfig` are also exported from the `insomnia-collection` module (via `collection.ts`) so scripts can `require('insomnia-collection')` and construct their own lists directly, e.g.: `new ProxyConfigList(undefined, [{match: 'https://example.com/*', host: 'proxy.com', port: 8080, tunnel: true}, ...])` (per the inline example comment in the source). There is no evidence in the SDK object graph that `pm.request` itself ever holds a `ProxyConfigList` — only a single `ProxyConfig`. + +## Gotchas / notable behavior +- **`_index = 'key'` but `ProxyConfig` has no `key` property.** Base `PropertyList.one()`/`.indexOf()`/`.upsert()` look up items by `typeClass._index` (here `'key'`), but `ProxyConfig` instances never define a `key` field — so `ProxyConfigList.one('...')`/`.get('...')`/`.indexOf(...)`/`.upsert(...)` would compare against `undefined` for every item and effectively never index correctly by identity (though `.toObject()` and `.resolve()` work fine since they key/filter explicitly by `match` instead of relying on `_index`). +- **`test(url)` returns `false` silently if `url` is omitted** — the source itself flags this as confusing ("TODO: it is confusing in which case url arg is optional"). +- **`bypass` matching is exact string equality**, not pattern-based — a URL must be an exact match to an entry in `bypass` to be excluded; nothing is normalized (no protocol/trailing-slash handling). +- **`update()` cannot change `bypass` or `protocol`** — these are deliberately excluded from `ProxyConfigOptions` in the `update()` signature's `Omit<...>`. +- **`updateProtocols()` always throws** — it exists on the class but is not a supported operation in Insomnia. +- **`getProxyUrl()` omits the port segment entirely when `port === undefined`** (not just falsy — `port: 0` would still render `:0`). +- **`transformToSdkProxyOptions` swallows parse errors when the proxy is disabled**: a malformed proxy host string does not throw as long as `proxyEnabled` is `false`; it only throws when the proxy would actually be used. +- **`enabled`/`disabled` can disagree with the protocol-selected proxy string.** `enabledProxy` is derived from `httpsProxy || httpProxy` (https checked first, regardless of `protocol`), while the proxy actually parsed into `host`/`port`/etc. is `proxyHost` (`httpsProxy` only if `protocol === 'https:'`, else `httpProxy`). If `protocol` is `'http:'` with `httpProxy` empty but `httpsProxy` set (or vice versa), `enabledProxy` comes out `true` from the other, unused string, `proxy.disabled` is set to `false`, yet `proxyHost === ''` skips the parsing block entirely — so `pm.request.proxy` ends up `disabled: false` with an empty `host`. +- **The class-level `static` fields shadow real instance field names** (`static host = ''`, `static match = ''`, etc., separate from `this.host`, `this.match` on instances) — marked `@ignore` and described only as "hidden as they are not used while must be exposed"; do not confuse `ProxyConfig.host` (static, always `''`) with `someProxyConfigInstance.host`. + +## Related +- `properties.ts` — `Property` (base class for `ProxyConfig`) and `PropertyList` (base class for `ProxyConfigList`), including the shared `toObject()`/`_index` lookup semantics referenced above. +- `urls.ts` — `UrlMatchPattern`/`UrlMatchPatternList` (used internally by `getProtocols()`/`test()`) and `Url` (the type accepted by `ProxyConfigList.resolve()`). +- `console.ts` — `getExistingConsole()`, used by `transformToSdkProxyOptions` to log which proxy is being used. +- `request.ts` — constructs `pm.request.proxy` from `ProxyConfigOptions` and serializes it back out when building the final request. +- `insomnia.ts` — calls `transformToSdkProxyOptions` using Insomnia Settings (`proxyEnabled`, http/https proxy strings, `noProxy`) to seed the proxy config before a script runs. +- `collection.ts` — re-exports `ProxyConfig`/`ProxyConfigList` for direct use via `require('insomnia-collection')`. diff --git a/.claude/skills/fix-scripting-feature/references/objects/request-info.md b/.claude/skills/fix-scripting-feature/references/objects/request-info.md new file mode 100644 index 00000000000..e1b2dce656b --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/request-info.md @@ -0,0 +1,69 @@ +# RequestInfo (`insomnia.info` / `pm.info`) + +**Source:** `packages/insomnia-scripting-environment/src/objects/request-info.ts` + +## Purpose +Models `pm.info`: metadata about which script phase is executing and, for the collection runner, +which iteration is in progress. Read-oriented — scripts typically branch on `insomnia.info.eventName` +or log `insomnia.info.iteration`/`iterationCount`. + +## Public API + +### `type EventName = 'prerequest' | 'test'` +The name of the event that triggered the script: `'prerequest'` (before a request is sent) or +`'test'` (during the testing/after-response phase). + +### `interface RequestInfoOption` +```ts +interface RequestInfoOption { + eventName?: EventName; + iteration?: number; + iterationCount?: number; + requestName?: string; + requestId?: string; +} +``` +Plain-object shape used to construct a `RequestInfo` (and stored on `RequestContext.requestInfo`, see `interfaces.md`). + +### `class RequestInfo` +```ts +constructor(options: RequestInfoOption) +``` +Public properties (plain, mutable, non-readonly): +- `eventName: EventName` — defaults to `'prerequest'` if not provided (`options.eventName || 'prerequest'`). +- `iteration: number` — defaults to `1` (`options.iteration || 1`). +- `iterationCount: number` — defaults to `1` (`options.iterationCount || 1`). +- `requestName: string` — defaults to `''`. +- `requestId: string` — defaults to `''`. + +Method: +- `toObject = () => ({ eventName, iteration, iterationCount, requestName, requestId })` — plain serialization, same shape as `RequestInfoOption` with all fields always populated. + +## Script-facing surface +- `insomnia.info.eventName` — `'prerequest'` or `'test'`, useful for scripts shared between pre-request and test tabs. +- `insomnia.info.iteration` — current collection-runner iteration number (1-based). +- `insomnia.info.iterationCount` — total number of iterations configured for the run. +- `insomnia.info.requestName` — display name of the request being executed. +- `insomnia.info.requestId` — the request's unique id. + +## Gotchas / notable behavior +- All five properties are **plain public fields**, not readonly — a script can reassign + `insomnia.info.iteration = 99` and nothing in this class prevents it. In practice this has no + effect on the actual collection-runner state: `packages/insomnia/src/scripting/run-script.ts`'s + merge logic (building the returned `RequestContext`) does not read `requestInfo` back out of the + mutated `InsomniaObject` at all, so any script-side mutation of `insomnia.info` is silently + discarded after the script finishes. +- The `||` fallback pattern means an explicit `0` for `iteration`/`iterationCount` is treated the + same as "not provided" and coerced to `1`. Notably, `initInsomniaObject` (in `insomnia.ts`) + builds the options with `iterationCount: rawObj.requestInfo.iterationCount || 0` — but since `0` + is falsy, the `RequestInfo` constructor's own `|| 1` fallback still turns that back into `1`, so + `iterationCount` effectively can never legitimately be `0` through this path. +- A `// TODO: update follows when post-request script and iterationData are introduced` comment on + the call site in `insomnia.ts` suggests the `eventName`/iteration wiring for after-response + scripts may still be incomplete. + +## Related +- `insomnia.ts` — `InsomniaObject.info: RequestInfo`; `initInsomniaObject` builds it from `rawObj.requestInfo` plus `rawObj.request.name`/`_id`. +- `interfaces.ts` — `RequestInfoOption` is embedded as `RequestContext.requestInfo`. +- `packages/insomnia/src/scripting/run-script.ts` — supplies the initial `RequestInfoOption` per script phase (pre-request vs. test) and — notably — does not persist script-side mutations back into `RequestContext`. +- `__tests__/request-info.test.ts` — covers default values and `toObject()` shape for both single-request and collection-runner (`iteration`/`iterationCount`) cases. diff --git a/.claude/skills/fix-scripting-feature/references/objects/request.md b/.claude/skills/fix-scripting-feature/references/objects/request.md new file mode 100644 index 00000000000..803e54a8771 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/request.md @@ -0,0 +1,94 @@ +# Request, RequestBody + +**Source:** `packages/insomnia-scripting-environment/src/objects/request.ts` + +## Purpose + +Models the outgoing HTTP request as seen/mutated by pre-request and test scripts: `pm.request` / `insomnia.request`. Also holds the merge/serialization logic that converts between this script-facing `Request` shape and Insomnia's own `insomnia-data` request model (`mergeRequests`, `mergeRequestBody`, `mergeSettings`, `mergeClientCertificates`), and payload-size calculation used by `Request.size()` / `Response.size()`. + +## Public API + +### `FormParam` (extends `Property`) +- `constructor(options: { key: string; value: string; type?: string; disabled?: boolean })` +- `key: string`, `value: string`, `type?: string` +- `static _postman_propertyAllowsMultipleValues()` — **always throws** `Error('unsupported')`. +- `static _postman_propertyIndexKey()` — **always throws** `Error('unsupported')`. +- `toJSON()` — `{ key, value, type, disabled }`. +- `toString()` — `"key=value"` with both parts `encodeURIComponent`-escaped. +- `valueOf()` — returns `value`. + +### `RequestBodyOptions` (interface) +`{ mode: RequestBodyMode; file?: string; formdata?: {key,value,type?,disabled?}[]; graphql?: {query,operationName,variables,disabled?}; raw?: string; urlencoded?: {key,value?,type?,disabled?,multiline?,fileName?}[]; options?: object }` +`RequestBodyMode = undefined | 'formdata' | 'urlencoded' | 'raw' | 'file' | 'graphql'`. + +### `RequestBody` (extends `PropertyBase`) +- `constructor(opts: RequestBodyOptions)` +- `mode: RequestBodyMode`, `file?: string`, `formdata?: PropertyList`, `graphql?: {query,operationName,variables}`, `raw?: string`, `urlencoded?: PropertyList`, `options?: object` +- `isEmpty(): boolean` — switches on `mode` and checks whether the matching field is `null`; **throws** `Error` if `mode` isn't one of the five known modes. +- `toString()` — renders body as a string per `mode` (`formdata`/`urlencoded` join their `PropertyList` entries with `&`, `graphql` is `JSON.stringify`-ed); wraps any internal error as `Error("toString: ...")`; returns `''` if `mode` is `undefined`. +- `update(opts: RequestBodyOptions)` — re-derives and replaces all fields (same logic as constructor). + +### `RequestOptions` (interface) +`{ url: string | Url; method?: string; header?: HeaderDefinition[] | object; body?: RequestBodyOptions; auth?: AuthOptions; proxy?: ProxyConfigOptions; certificate?: CertificateOptions; pathParameters?: RequestPathParameter[]; name?: string }` + +### `RequestSize` (interface) +`{ body: number; header: number; total: number; source: string }` + +### `Request` (extends `Property`) +- `constructor(options: RequestOptions)` — sets `_kind = 'Request'`; `method` defaults to `'GET'` if omitted; `header` accepts either an array of `HeaderDefinition` or a plain `{key: value}` object (converted to `Header`s); `auth` defaults to `{ type: 'noauth' }`. +- Properties: `name: string`, `url: Url`, `method: string`, `headers: HeaderList
`, `body?: RequestBody`, `auth: RequestAuth`, `proxy?: ProxyConfig`, `certificate?: Certificate`, `pathParameters: RequestPathParameter[]`. +- `static isRequest(obj: object): boolean` — checks `_kind === 'Request'`. +- `addHeader(header: Header | object)` — accepts a `Header` instance or `{key, value}`; **throws** `Error` otherwise. +- `addQueryParams(params: QueryParam[] | string)` — delegates to `this.url.addQueryParams`. +- `authorizeUsing(authType: AuthOptionTypes | AuthOptions, options?: VariableList)` — delegates to `this.auth.use(...)`. +- `clone(): Request` — `new Request({ ...this.toJSON() })` (JSON round-trip clone, not a deep object clone). +- `forEachHeader(callback: (header: Header, context?: object) => void)` — delegates to `this.headers.each`. +- `getHeaders(options?: { ignoreCase, enabled, multiValue, sanitizeKeys }): Record` — merges headers with the same key into an array; `ignoreCase` lowercases keys before merging; `enabled` filters out headers where `disabled` is truthy; `sanitizeKeys` drops headers with a falsy `value`. +- `removeHeader(toRemove: string | Header, options?: { ignoreCase: boolean })` — rebuilds `this.headers` as a new `HeaderList` excluding matches; **throws** if `toRemove` isn't `string | Header`. +- `removeQueryParams(params: string | string[] | QueryParam[])` — delegates to `this.url.removeQueryParams`. +- `size(): RequestSize` — `calculatePayloadSize(this.body?.toString() ?? '', this.headers)`. +- `toJSON()` — plain object snapshot (`url` as string, `header` array, `body`, `auth`, `proxy`, `certificate`). +- `update(options: RequestOptions)` — re-derives and replaces every field. +- `upsertHeader(header: HeaderDefinition)` — removes any existing header with the same `key` (case-sensitive), then appends a new `Header`. + +### Module-level functions +- `mergeSettings(originalSettings: Settings, updatedReq: Request): Settings` — if `updatedReq.proxy` is enabled (not disabled and has a non-empty proxy URL), overrides both `httpProxy` and `httpsProxy` in the returned `Settings` with the same proxy URL; otherwise returns `originalSettings` unchanged. +- `mergeClientCertificates(originalClientCertificates: ClientCertificate[], updatedReq: Request): ClientCertificate[]` — maps the script's single `updatedReq.certificate` onto Insomnia's certificate list (which supports multiple). Returns originals unchanged if no certificate was set (or it's empty). **Throws** `Error('Invalid certificate configuration: "cert+key" and "pfx" can not be set at the same time')` if neither a valid `pfx` nor a valid `cert`+`key` pair is present in the certificate. +- `toScriptRequestBody(insomniaReqBody: InsomniaRequestBody): RequestBodyOptions` — converts Insomnia's native request body (`text` / `fileName` / `params`) into a `RequestBodyOptions` (`raw` / `file` / `urlencoded`); `formdata` and `graphql` modes are not produced here. +- `mergeRequestBody(updatedReqBody: RequestBody | undefined, originalReqBody: InsomniaRequestBody): InsomniaRequestBody` — infers `mimeType` from `updatedReqBody.mode` (falls back to `originalReqBody.mimeType` if set); **throws** on an unknown `mode`; wraps any other failure as `Error("failed to update body: ...")`. +- `mergeRequests(originalReq: InsomniaRequest, updatedReq: Request): InsomniaRequest` — builds the outgoing Insomnia request from the script's mutated `Request`: `url` via `toStringWithoutQuery()`, `parameters` from `url.query`, `headers` from `headers`, `authentication` via `fromPreRequestAuth`, `pathParameters` copied as-is, and **hardcodes `preRequestScript: ''`** (i.e., the merged request never carries a pre-request script forward). +- `calculatePayloadSize(body: string, headers: HeaderList
): RequestSize` — body size via `new Blob([body]).size`; `source` is always `'COMPUTED'`. +- `calculateHeadersSize(headers: HeaderList
): number` — `Blob` size of all headers joined as `"key: value\n"` lines (via each header's `toString()`, which is `Header`'s inherited/overridden behavior). + +## Script-facing surface + +`insomnia.request` / `pm.request` is a live `Request` instance (constructed in `insomnia.ts#initInsomniaObject`). Common script usage: +- `pm.request.url`, `pm.request.method`, `pm.request.headers`, `pm.request.body` +- `pm.request.headers.add(...)`, `pm.request.addHeader({key, value})`, `pm.request.upsertHeader({key, value})`, `pm.request.removeHeader('X-Foo')` +- `pm.request.url.addQueryParams(...)` / `pm.request.addQueryParams(...)`, `pm.request.removeQueryParams(...)` +- `pm.request.auth`, `pm.request.authorizeUsing('basic', ...)` +- `pm.request.size()` — payload size info +- Any mutation a pre-request script makes to `pm.request` is read back and merged into the real outgoing request via `mergeRequests` / `mergeRequestBody` / `mergeSettings` / `mergeClientCertificates` after the script finishes. + +## Gotchas / notable behavior + +- `FormParam._postman_propertyAllowsMultipleValues()` and `_postman_propertyIndexKey()` are stubs that **always throw** — calling them from a script (unlikely, but they exist statically) will always fail. A commented-out `static parse` is also noted as "not supported yet in existing scripts". +- `RequestBody.isEmpty()` / `toString()` both `throw` if `mode` doesn't match one of the five known literals — an unexpected/typo'd `mode` string surfaces as a runtime error, not silently. +- `Request.getHeaders()`'s `multiValue` option is part of the signature but **is never referenced in the function body** — the implementation always accumulates same-key headers into an array regardless of what `multiValue` is set to. +- `Request.removeHeader` and `upsertHeader` rebuild `this.headers` as an entirely new `HeaderList` rather than mutating in place. +- `Request.clone()` is a JSON round-trip (`toJSON()` → `new Request(...)`), not a structural/deep clone — anything not captured by `toJSON()` is lost on clone. +- `mergeRequests` **always sets `preRequestScript: ''`** on the merged request — a deliberate design choice worth knowing if debugging "script disappeared" reports downstream. +- `mergeClientCertificates` throws if a script sets both `cert`+`key` and `pfx` on `pm.request.certificate` at the same time — Insomnia only supports one or the other. +- `toScriptRequestBody` only produces `raw` / `file` / `urlencoded` modes; there's no path from Insomnia's native body model to script `formdata`/`graphql` modes in this function. + +## Related + +- `packages/insomnia-scripting-environment/src/objects/headers.ts` — `Header`, `HeaderList` used for `Request.headers`. +- `packages/insomnia-scripting-environment/src/objects/urls.ts` — `Url`, `QueryParam`, `toUrlObject` used for `Request.url` and `RequestBody.urlencoded`. +- `packages/insomnia-scripting-environment/src/objects/auth.ts` — `RequestAuth`, `fromPreRequestAuth` used for `Request.auth`. +- `packages/insomnia-scripting-environment/src/objects/certificates.ts` — `Certificate` used for `Request.certificate`. +- `packages/insomnia-scripting-environment/src/objects/proxy-configs.ts` — `ProxyConfig` used for `Request.proxy`. +- `packages/insomnia-scripting-environment/src/objects/properties.ts` — `Property`, `PropertyBase`, `PropertyList` base classes. +- `packages/insomnia-scripting-environment/src/objects/variables.ts` — `Variable`, `VariableList` used by `authorizeUsing`. +- `packages/insomnia-scripting-environment/src/objects/response.ts` — imports `calculateHeadersSize`; `Response.originalRequest` is a `Request`. +- `packages/insomnia-scripting-environment/src/objects/insomnia.ts` — constructs `pm.request` in `initInsomniaObject`. diff --git a/.claude/skills/fix-scripting-feature/references/objects/response.md b/.claude/skills/fix-scripting-feature/references/objects/response.md new file mode 100644 index 00000000000..4e35db150e8 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/response.md @@ -0,0 +1,58 @@ +# Response + +**Source:** `packages/insomnia-scripting-environment/src/objects/response.ts` + +## Purpose + +Models the HTTP response as seen by after-response and test scripts: `pm.response` / `insomnia.response`. It wraps the raw body/headers/cookies/timing data returned by the network layer, provides parsing/introspection helpers (`json()`, `contentInfo()`, `size()`), and extends `chai`'s assertion API with Postman-style response assertions (`pm.response.to.have.status(200)`, etc.). `pm.response` is only populated for after-response/test scripts — it's `undefined` during pre-request scripts. + +## Public API + +### `ResponseOptions` (interface) +`{ code: number; reason?: string; header?: HeaderDefinition[]; cookie?: CookieOptions[]; body?: string; stream?: Buffer | ArrayBuffer; responseTime: number; originalRequest: Request; bytesRead?: number }` + +### `ResponseContentInfo` (interface) +`{ mimeType: string; mimeFormat: string; charset: string; fileExtension: string; fileName: string; contentType: string }` + +### `Response` (extends `Property`) +- `constructor(options: ResponseOptions)` — sets `_kind = 'Response'`; `body` defaults to `''`; `status` defaults to `options.reason`, falling back to `RESPONSE_CODE_REASONS[options.code]`, falling back to `''`; `bytesRead` (private) defaults to `0`. +- Properties: `body: string`, `code: number`, `cookies: CookieList`, `headers: HeaderList
`, `originalRequest: Request`, `responseTime: number`, `status: string`, `stream?: Buffer | ArrayBuffer`; `bytesRead` is **private**. +- `static createFromNode(response: {...}, cookies: CookieOptions[]): Response` — builds a `Response` from a raw Node-style response object (`body`, `headers`, `statusCode`, `statusMessage`, `elapsedTime`, `originalRequest`, `stream`). +- `static isResponse(obj: object): boolean` — checks `_kind === 'Response'`. +- `contentInfo(): ResponseContentInfo` — parses the `Content-Type` header for `mimeType`/`charset` (defaults: `application/octet-stream` / `utf8`) and the `Content-Disposition` header for `fileName`/`fileExtension`. **Throws** if a `Content-Type` header exists but its value is blank, or its mime-type segment is empty. +- `dataURI(): string` — builds a `data:` URI string from `contentInfo().contentType` and `this.stream || this.body`. **Throws** if neither `stream` nor `body` is set. +- `json(reviver?: (key, value) => any, _strict?: boolean): any` — `JSON.parse(this.body.toString(), reviver)`; wraps parse failures as `Error("json: failed to parse: ...")`. `_strict` is accepted but unused ("TODO: enable strict after common module is introduced"). +- `jsonp(_reviver?, _strict?)` — **always throws** `unsupportedError('jsonp()')`. +- `reason(): string` — returns `this.status`. +- `size(): { body: number; header: number; total: number; source: 'COMPUTED' }` — `body` is the private `bytesRead` value (not derived from `this.body`'s actual length); `header` via `calculateHeadersSize(this.headers)`. +- `text(): string` — `this.body.toString()`. +- `get to()` — returns a `chai.Assertion` wrapping `this`, with Postman-style properties/methods registered onto `chai.Assertion.prototype` on every access: properties `withBody`, `error` (true if `code` is within 400–500 inclusive), `ok` (true if `code === 200`), `json` (true if the body parses to an object); methods `status(val)`, `header(headerName)`, `body(bodyContent)`, `jsonBody(propName)`, `jsonSchema(schema, options?)` (uses `ajv` to validate `this.json()` against `schema`). + +### Module-level functions +- `toScriptResponse(originalRequest: Request, partialInsoResponse: sendCurlAndWriteTimelineResponse | sendCurlAndWriteTimelineError, responseBody: string): Response | undefined` — returns `undefined` if `partialInsoResponse` is an error result (network/curl failure); otherwise builds a `Response`, extracting `Set-Cookie` headers into `Cookie.parse(...)` entries for `cookie`. +- `readBodyFromPath(response: sendCurlAndWriteTimelineResponse | sendCurlAndWriteTimelineError | undefined): Promise` — returns `''` if `response` is missing, an error, or has no `bodyPath`; otherwise reads and decompresses the body from disk via `services.helpers.readCurlResponse`. **Throws** if that read reports an error. + +## Script-facing surface + +`insomnia.response` / `pm.response` is a `Response` instance, present only in after-response/test scripts (constructed in `insomnia.ts#initInsomniaObject` via `toScriptResponse`; `undefined` if the request errored or during pre-request scripts). Common script usage: +- `pm.response.code`, `pm.response.status`, `pm.response.headers`, `pm.response.body` +- `pm.response.json()`, `pm.response.text()`, `pm.response.contentInfo()` +- `pm.response.to.have.status(200)`, `pm.response.to.have.header('Content-Type')`, `pm.response.to.have.jsonBody('key')`, `pm.response.to.not.have.status(404)`, `pm.response.to.have.jsonSchema({...})` + +## Gotchas / notable behavior + +- `dataURI()` has a typo in the returned string: it emits `` `data:${contentType};baseg4, ${bodyInBase64}` `` — **`baseg4` instead of `base64`**. Any consumer relying on `dataURI()` producing a spec-compliant data URI will get a malformed one. +- `jsonp()` is a stub that always throws `unsupportedError('jsonp()')` — it exists on the type but is never functional. +- `size().body` comes from the **private `bytesRead`** field (set once at construction from `options.bytesRead`, defaulting to `0`), not from measuring `this.body`. If `bytesRead` wasn't passed in, `size()` reports `0` regardless of actual body length. (The test file has a commented-out assertion noting `resp.size()` doesn't fully work yet: `"this will work after PropertyList.one is improved"`.) +- The `error` assertion (`pm.response.to.have.error` / `.to.be.error`) only covers status codes **400–500 inclusive** — not the broader 4xx/5xx range one might expect. +- **`get to()`'s re-registration is a real correctness risk, not just wasted work.** Every `.to` access calls `chai.use((_chai, utils) => {...})` with a freshly-created arrow function; chai dedupes plugins by callback identity (`used.indexOf(fn)`), so a new closure is never recognized as already-registered and the plugin body reruns every time. Worse, every `withBody`/`error`/`ok`/`json`/`status`/`header`/`body`/`jsonBody`/`jsonSchema` callback is itself an arrow function that reads `utils.flag(respAssertion, 'object')`, where `respAssertion` is closed over from whichever `.to` access most recently ran — not derived from `this`/the assertion instance actually being evaluated (arrow functions ignore the `this` chai binds via `getter.call(this)`/`method.apply(this, ...)`). So if a script holds a `.to`-derived assertion chain and triggers another `.to` access (same or different response) before invoking a method/property on that held chain, the held chain silently asserts against the *other* response instead. Single-statement chains like `pm.response.to.have.status(200)` are unaffected since no `.to` access can intervene mid-expression. +- `contentInfo()` throws if a `Content-Type` header is present but empty/malformed; scripts that blindly call `pm.response.contentInfo()` on unusual responses can throw. +- `toScriptResponse` returns `undefined` on network error — scripts must handle `pm.response` potentially being `undefined` (though in practice after-response/test scripts only run when a response exists). + +## Related + +- `packages/insomnia-scripting-environment/src/objects/request.ts` — `Response.originalRequest: Request`; imports `calculateHeadersSize` from here. +- `packages/insomnia-scripting-environment/src/objects/headers.ts` — `Header`, `HeaderList` for `Response.headers`. +- `packages/insomnia-scripting-environment/src/objects/cookies.ts` — `Cookie`, `CookieList` for `Response.cookies`. +- `packages/insomnia-scripting-environment/src/objects/properties.ts` — `Property`, `unsupportedError`. +- `packages/insomnia-scripting-environment/src/objects/insomnia.ts` — constructs `pm.response` in `initInsomniaObject` via `toScriptResponse`/`readBodyFromPath`. diff --git a/.claude/skills/fix-scripting-feature/references/objects/send-request.md b/.claude/skills/fix-scripting-feature/references/objects/send-request.md new file mode 100644 index 00000000000..398f60c15c3 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/send-request.md @@ -0,0 +1,70 @@ +# sendRequest (pm.sendRequest) + +**Source:** `packages/insomnia-scripting-environment/src/objects/send-request.ts` + +## Purpose +Implements the standalone `sendRequest()` function that backs `pm.sendRequest()` / `insomnia.sendRequest()` — letting a pre-request/after-response script fire an ad-hoc HTTP request (via libcurl) and get a `Response` back, either through a Node-style callback or a returned promise. It also contains the translation layer from the SDK's `Request`/string/`RequestOptions` shapes into the curl request options consumed by Insomnia's network layer, and from the raw curl output back into a `Response` object. + +## Public API + +### `sendRequest(request, cb, settings)` +```ts +export async function sendRequest( + request: string | Request | RequestOptions, + cb: (error?: string, response?: Response) => void, + settings: Settings, +): Promise +``` +- Builds curl options from `request` via `requestToCurlOptions(request, settings)`. +- Picks the curl execution path based on environment: in the renderer (`__IS_RENDERER__` true) it uses `window.bridge.curlRequest`; otherwise it dynamically imports `curlRequest` from `insomnia/src/main/network/libcurl-promise`. +- Awaits the curl call, converts the raw `CurlRequestOutput` to a `Response` via `curlOutputToResponse(output, request)`. +- **Success:** if `cb` is provided, calls `cb(undefined, transformedOutput)`; either way resolves the returned promise with `transformedOutput`. +- **Failure:** if `cb` is provided, calls `cb(e)` and resolves the promise with `undefined` (does **not** reject); if no `cb` is provided, rejects the promise with `e`. + +### `requestToCurlOptions(req, settings)` (not exported) +```ts +function requestToCurlOptions(req: string | Request | RequestOptions, settings: Settings) +``` +- If `req` is a `string`: treats it as a bare URL, builds a minimal GET request object (`no body`, `noauth`, empty headers/cookies, `settingRebuildPath: true`, `settingSendCookies: true`, `settingFollowRedirects` derived from `settings.followRedirects ? 'on' : 'off'`), with `requestId` prefixed `pre-request-script-adhoc-req-simple:`. +- If `req` is a `Request` instance or a plain object (`RequestOptions`): coerces to a `Request` (`new Request(req)` if not already one), derives `mimeType` from `finalReq.body.mode` (`raw`→`text/plain`, `file`→`application/octet-stream`, `formdata`→`multipart/form-data`, `urlencoded`→`application/x-www-form-urlencoded`, `graphql`→`application/json`, otherwise `text/plain`), maps headers/body/certificate/auth into the curl request shape, with `requestId` = `finalReq.id` or a generated `pre-request-script-adhoc-req-custom:`. +- Auth mapping beyond `noauth` is largely commented out / marked `TODO` (see Gotchas). +- Throws `Error('the request type must be: string | Request | RequestOptions.')` for any other input shape. + +### `curlOutputToResponse(result, request)` (not exported) +```ts +async function curlOutputToResponse( + result: CurlRequestOutput, + request: string | Request | RequestOptions, +): Promise +``` +- Throws if `result.headerResults` is empty, or if `result.patch.error` is set (re-throws that error), or if there's no last redirect entry. +- Normalizes `request` into a `Request` instance for `originalRequest`. +- Extracts headers from the last redirect's `headerResults` entry; parses any `Set-Cookie` headers via `tough-cookie`'s `Cookie.parse(..., { loose: true })` into cookie option objects (filtering out unparsable ones). +- If `result.responseBodyPath` is absent, returns a `Response` with `body: ''`. +- Otherwise reads the body via `services.helpers.readCurlResponse({ bodyPath, bodyCompression })`; throws if that read reports an error; otherwise returns a fully populated `Response` (`code`, `reason`, `header`, `cookie`, `body`, `responseTime: result.patch.elapsedTime`, `originalRequest`). `stream` is always left `undefined` ("because it is inaccurate to differentiate if body is binary"). + +## Script-facing surface +- `pm.sendRequest(url, (err, response) => { ... })` — string URL, GET request, callback style. +- `pm.sendRequest({ url, method, header, body, auth, ... }, (err, response) => { ... })` — full `RequestOptions`-shaped object. +- `pm.sendRequest(requestInstance, (err, response) => { ... })` — passing an existing `Request` instance (e.g. built via `new Request(...)` or `pm.request`). +- Can also be used promise-style without a callback: `const response = await pm.sendRequest(url)` (see Gotchas for the resulting error-handling difference). +- Exposed on the object model as `insomnia.sendRequest(request, cb)`, which is a plain instance method (not proxy-trapped) that forwards to this file's `sendRequest(request, cb, this._settings)`. + +## Gotchas / notable behavior +- **Callback vs. promise error handling differ:** if a `cb` is supplied, network/parsing errors are *swallowed* into the callback (`cb(e)`) and the returned promise still resolves (with `undefined`) — it will not reject. If no `cb` is supplied, the same error instead rejects the returned promise. A script mixing `await pm.sendRequest(url, cb)` with `try/catch` will not catch curl errors, because the promise resolves successfully even on failure when a callback is present. +- **Environment branching:** `__IS_RENDERER__ ? window.bridge.curlRequest : (await import('insomnia/src/main/network/libcurl-promise')).curlRequest` — the actual network call goes through completely different code paths depending on whether the script executes in the renderer (Electron IPC bridge) or Node/main context. Bugs that only reproduce in one context (e.g. inso CLI vs. the desktop app) may live in this branch. +- **Auth beyond `noauth` is not wired up** for the custom-`Request` branch: there's a large commented-out block (`// const authHeaders = ...`) showing API-key/bearer header injection was planned but not implemented; `fromPreRequestAuth(finalReq.auth)` is still called and passed through as `authentication`, but the `authHeader` field of the curl options is always `undefined` with a `// TODO: add this for bearer and other auth methods` comment. +- **Certificates:** only a single client certificate is forwarded (`finalReq.certificate`), built directly into the curl options' `certificates` array; several fields (`disabled`, `isPrivate`, `_id`, `type`, `parentId`, `modified`, `created`, `name`) are hardcoded to empty/`false`/`0` since they're "unused fields because they are not persisted". +- **Cookies are not populated from a jar** — `cookieJar: { cookies: [] }` and `cookies: []` are always empty in the outgoing request; the comment notes "currently cookies should be handled by user in headers". Response `Set-Cookie` headers are still parsed back out via `tough-cookie`, though. +- **`suppressUserAgent` detection** only checks for a *disabled* `User-Agent` header (`h.key.toLowerCase() === 'user-agent' && h.disabled === true`) — an enabled custom `User-Agent` header does not suppress the default one; it presumably just overrides via normal header precedence. +- **Response body/stream:** `stream` is always `undefined` in the returned `Response`, even though `CurlRequestOutput`/`ResponseOptions` support it, specifically to avoid ambiguity between binary and text bodies — callers should rely on `body` (a string) only. + +## Related +- `packages/insomnia-scripting-environment/src/objects/request.ts` — `Request`, `RequestOptions`, `RequestBody`/`RequestBodyOptions` types this function consumes as input. +- `packages/insomnia-scripting-environment/src/objects/response.ts` — `Response` class this function constructs as output. +- `packages/insomnia-scripting-environment/src/objects/auth.ts` — `RequestAuth`, `fromPreRequestAuth` used to translate the SDK's auth model into the curl request's `authentication` field. +- `packages/insomnia-scripting-environment/src/objects/cookies.ts` — `CookieOptions` type used for parsed `Set-Cookie` results. +- `packages/insomnia-scripting-environment/src/objects/insomnia.ts` — exposes this function as `insomnia.sendRequest(request, cb)`, supplying `this._settings` as the `settings` argument. +- `insomnia/src/main/network/libcurl-promise` (`curlRequest`, `CurlRequestOutput`) — the main/Node-side curl execution path, dynamically imported when not running in the renderer. +- `window.bridge.curlRequest` — the renderer-side IPC bridge equivalent used when `__IS_RENDERER__` is true. +- `insomnia-data` (`services.helpers.readCurlResponse`, `Settings` type) — used to read the response body from disk (`responseBodyPath`) and for the `Settings` (e.g. `followRedirects`) passed in. diff --git a/.claude/skills/fix-scripting-feature/references/objects/test.md b/.claude/skills/fix-scripting-feature/references/objects/test.md new file mode 100644 index 00000000000..6db341f67ed --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/test.md @@ -0,0 +1,79 @@ +# Test handler (pm.test / test()) + +**Source:** `packages/insomnia-scripting-environment/src/objects/test.ts` + +## Purpose +Implements the `test()`/`skip()` handler that backs `pm.test(...)` (and `insomnia.test(...)`), plus the bookkeeping used by the script-execution harness to know when all in-flight test callbacks have settled before the script finishes. This is the only file responsible for turning a user's test callback into a `RequestTestResult` record. + +## Public API + +### `test(msg, fn, log)` +```ts +export async function test( + msg: string, + fn: () => Promise, + log: (testResult: RequestTestResult) => void +): Promise +``` +- Wraps `fn` in an async closure (`wrapFn`), times its execution with `performance.now()`, awaits it, and calls `log(...)` with a `RequestTestResult`: + - On success: `{ testCase: msg, status: 'passed', executionTime, category: 'unknown' }`. + - On thrown error `e`: `{ testCase: msg, status: 'failed', executionTime, errorMessage: `error: ${e} | ACTUAL: ${e.actual} | EXPECTED: ${e.expected}`, category: 'unknown' }`. +- Immediately invokes `wrapFn()`, registers the resulting promise with `startTestObserver`, and returns that same promise (`testPromise`) — i.e. `test()` itself resolves once the wrapped test body (pass or fail) has finished running. +- `category` is always hardcoded to `'unknown'` here even though `RequestTestResult.category` (in `insomnia-data`) also allows `'pre-request'` and `'after-response'`. + +### `skip(msg, _, log)` +```ts +export async function skip( + msg: string, + _: () => Promise, + log: (testResult: RequestTestResult) => void +): Promise +``` +- Does **not** call `fn` (the second argument is ignored). Immediately calls `log({ testCase: msg, status: 'skipped', executionTime: 0, category: 'unknown' })`. + +### `resetTestPromises()` +```ts +export function resetTestPromises(): void +``` +- Clears the module-level `testPromises` array (`testPromises = []`). Used to reset state between script executions so promises from a previous run aren't awaited again. + +### `waitForAllTestsDone()` +```ts +export async function waitForAllTestsDone(): Promise +``` +- Awaits `Promise.allSettled(testPromises)` (captured as `NativePromise` at module load, so it isn't affected by any sandbox-level `Promise` patching), then resets `testPromises` back to `[]`. +- This is the drain point the execution harness calls at the end of a script run to make sure every `pm.test(...)` callback (including ones the script itself didn't `await`) has finished before results are collected. + +### `startTestObserver(promise)` (not exported) +```ts +function startTestObserver(promise: Promise): void +``` +- Pushes `promise` onto the module-level `testPromises` array. Called once per `test()` invocation. + +### `TestHandler` interface +```ts +export interface TestHandler { + (msg: string, fn: () => Promise): Promise; + skip?: (msg: string, fn: () => Promise) => void; +} +``` +- Callable interface shape used by the object model to type the value returned for `pm.test` — a function that also carries an optional `.skip` method. + +## Script-facing surface +- `pm.test('name', async () => { ... })` / `insomnia.test('name', fn)` — registers and immediately runs a test, recording a pass/fail result. +- `pm.test.skip('name', fn)` — records a `skipped` result without running `fn`. +- There is no bare global `test`/`skip` exposed directly by this file; script-facing exposure happens through the `insomnia`/`pm` object (see Related). + +## Gotchas / notable behavior +- **Fire-and-forget by default:** `test()` starts executing the test body synchronously (well, as soon as the async function runs) and returns a promise, but scripts are not required to `await pm.test(...)`. Because every test promise is also pushed into the shared `testPromises` array via `startTestObserver`, un-awaited tests are still tracked and will be waited on by `waitForAllTestsDone()` before the script finishes — this is what prevents test results from being lost when a script fires multiple `pm.test()` calls without awaiting them. +- **Module-level mutable state:** `testPromises` is a module-level array, not per-execution-context state. `resetTestPromises()` must be called before a script runs and `waitForAllTestsDone()` after, or results/timing from different executions could leak into each other (relevant if concurrent script executions ever share this module instance). +- **Error message shape assumes chai-style errors:** the failure branch does `` `error: ${e} | ACTUAL: ${e.actual} | EXPECTED: ${e.expected}` ``, which assumes `e` has `.actual`/`.expected` (true for chai `AssertionError`s from `insomnia.expect(...)`). For a plain thrown `Error` or non-chai exception, `ACTUAL`/`EXPECTED` will stringify as `undefined`. +- **`category` is never anything but `'unknown'`** from this module — any pre-request vs. after-response distinction in `RequestTestResult.category` must be set elsewhere (or is currently unused/always `'unknown'` for script-originated tests). +- **`NativePromise` capture:** `const NativePromise = Promise;` is captured at module import time specifically so `waitForAllTestsDone`'s `allSettled` call uses the real, un-sandboxed `Promise`, not a possibly-patched one from the script sandbox. + +## Related +- `packages/insomnia-scripting-environment/src/objects/insomnia.ts` — wires `test`/`skip` into the `InsomniaObject`. The constructor returns a `Proxy` whose `get` trap intercepts `prop === 'test'` and returns a `TestHandler` closure calling `this._test(msg, fn, this.pushRequestTestResult)` (and `.skip` calling `this._skip(...)`); `pushRequestTestResult` appends to `this.requestTestResults: RequestTestResult[]`, later returned from `toObject()`. +- `packages/insomnia-scripting-environment/src/objects/async-objects.ts` — calls `resetTestPromises()` once at module load alongside its own promise-tracking logic for `ProxiedPromise`. +- `packages/insomnia/src/scripting/sandbox.ts` (`prepareSandbox`) and `packages/insomnia/src/scripting/run-script.ts` — the harness that injects `__waitForAllTestsDone__` into the generated script function and awaits it (`await __waitForAllTestsDone__();`) before returning the mutated `insomnia` object; `run-script.ts` then reads `mutatedInsomniaObject.toObject().requestTestResults` and includes it in the returned `RequestContext`. +- `packages/insomnia-data` (`RequestTestResult`, `TestStatus`, `TestCategory` in `src/models/runner-test-result.ts`) — the result shape this file produces. +- UI consumers of the resulting test results: `packages/insomnia/src/ui/components/panes/request-test-result-pane.tsx` and `request-result-card.tsx`. diff --git a/.claude/skills/fix-scripting-feature/references/objects/urls.md b/.claude/skills/fix-scripting-feature/references/objects/urls.md new file mode 100644 index 00000000000..eb61456aba8 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/urls.md @@ -0,0 +1,111 @@ +# Url, QueryParam, UrlMatchPattern + +**Source:** `packages/insomnia-scripting-environment/src/objects/urls.ts` + +## Purpose + +Models a request URL (`Url`), its individual query-string parameters (`QueryParam`), and Chrome-extension-style match patterns (`UrlMatchPattern` / `UrlMatchPatternList`) used to test whether a URL matches a proxy bypass rule or a client-certificate host rule. `pm.request.url` is a `Url`; `UrlMatchPattern` backs `ProxyConfig.match` and `Certificate.matches`. + +## Public API + +### `setUrlSearchParams(provider: any)` +Module-level override hook: replaces the `URLSearchParams` implementation used internally by `QueryParam` (defaults to the global `URLSearchParams`). Test-only utility, not script-facing. + +### `QueryParamOptions` (interface) +`{ key: string; value?: string; type?: string; multiline?: string | boolean; disabled?: boolean; fileName?: string }` + +### `QueryParam` (extends `Property`) +- `override _kind = 'QueryParam'` +- `key: string`, `value?: string`, `type?: string`, `multiline?: string | boolean`, `fileName?: string` (the latter two are noted in a comment as "properties from Insomnia... added here to avoid being dropped"). +- `constructor(options: QueryParamOptions | string)` — if `options` is a string, it's `JSON.parse`d (wraps parse errors as `Error('invalid QueryParam options ...')`); if it's an object with `key`+`value`, fields are copied directly; otherwise **throws** `Error('unknown options for new QueryParam')`. +- `static override _index = 'key'` +- `static parse(queryStr: string): {key, value}[]` — parses a query string via `URLSearchParams`. +- `static parseSingle(paramStr: string, _idx?, _all?): {key, value}` — parses a single `key=value` pair; **throws** if `QueryParam.parse` yields nothing. +- `static unparse(params: QueryParamOptions[] | Record): string` — builds a query string via `URLSearchParams`, URL-encoding as it goes. +- `static unparseSingle(obj: {key, value}): string | {}` — returns `''`-joined encoded `"key=value"` if the input has both `key` and `value`, otherwise returns `{}` (an empty object, not a string — inconsistent return type). +- `toString(): string` — URL-encodes via `URLSearchParams` (e.g. spaces become `+`). +- `toRawString(): string` — `"key=value"` with **no encoding**. +- `update(param: string | {key, value, type?})` — parses a string form via `parseSingle` (only sets `key`/`value`, coercing non-string results to `''`) or copies `key`/`value`/`type` from an object; **throws** on any other input shape. + +### `UrlOptions` (interface) +`{ id?: string; auth?: {username, password}; hash?: string; host: string[]; path?: string[]; port?: string; protocol: string; query: {key, value}[]; variables: {key, value}[] }` + +### `Url` (extends `PropertyBase`) +- `override _kind = 'Url'` +- `id?: string` +- Getters (all derived from an internal `URL` object, `this.urlObject`, which is `undefined` if the input couldn't be parsed as a URL — e.g. contains unrendered `{{ }}`/`{% %}` template tags): `auth` (`{username, password}` or `undefined`), `hash` (without leading `#`), `host` (hostname split on `.`), `path` (pathname segments, empty ones filtered out), `port`, `protocol`, `query: PropertyList` (a **new** `PropertyList` wrapping the current query params on every access), `variables: string[]` (**always returns `[]`** — "TODO: it's usage is unknown"). +- `constructor(def: UrlOptions | string)` +- `private initFields(urlOptions)` — parses `def`; if it's a string containing a template tag (`checkIfUrlIncludesTag`), it's kept as an opaque `origin` string and `urlObject` stays `undefined` (to avoid mangling `{% uuid 'v4' %}`-style tags); if it's an object, a URL string is assembled from `protocol`/`auth`/`host`/`port`/`path`/`query`/`hash` (protocol defaults to `'https://'` if blank). Query params are always parsed out into `this.queryParams` (a private array) separately from the `URL` object (whose own `search` is cleared) — "query params are handled separately as URL object encodes content". +- `static _index = 'id'` +- `static isUrl(obj: object): boolean` +- `static parse(urlStr: string): UrlOptions | undefined` — returns `undefined` if `URL.canParse` fails; `variables` is always `[]` in the result. +- `addQueryParams(params: QueryParamOptions[] | string)` — string form splits on `&` then `=` (not URL-decoded); array form copies each entry into a new `QueryParam`. **Throws** `TypeError` on other input. +- `getHost(): string` — `''` if the URL didn't parse. +- `getPath(_unresolved?): string` — `_unresolved` param is accepted but unused; `''` if unparsed. +- `getPathWithQuery(): string` — path + `?` + query string (or just the query string if path is blank). +- `getQueryString(): string` — joins **enabled** (`!disabled`) query params via `toRawString()` (i.e. **unencoded**, unlike `QueryParam.toString()`). +- `getRemote(_forcePort?): string` — `_forcePort` is accepted but unused; returns `urlObject.host` (which already includes the port when non-default) or `''`. +- `removeQueryParams(params: QueryParam[] | string[] | string)` — filters `this.queryParams` by key; **throws** `TypeError` on unrecognized input shape. +- `override toString(_forceProtocol?): string` — `_forceProtocol` accepted but unused; rebuilds the URL string with the current query string; falls back to the opaque `origin` string if unparsed; special-cases avoiding an added trailing `/` for root-path URLs. +- `toStringWithoutQuery(_forceProtocol?): string` — same as `toString()` but with `search` cleared. +- `update(url: UrlOptions | string)` — re-runs `initFields`. + +### `UrlMatchPattern` (extends `Property`) +Implements [Chrome extension match patterns](https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns) (`scheme://host/path`, wildcards, ``); comment notes it doesn't support top-level-domain wildcards, and that URLs can't start with `-` (unenforced). +- `override id = ''` +- `constructor(pattern: string)` +- `static override _index = 'id'` +- `static readonly MATCH_ALL_URLS = ''` +- `static pattern: string | undefined = undefined` — declared but unused ("TODO: its usage is unknown"); shadowed by (unrelated to) the instance's private `pattern`. +- `static readonly PROTOCOL_DELIMITER = '+'` — multiple schemes are written as `http+https+custom://...`. +- `getProtocols(): string[]` — `['http', 'https', 'file']` for ``; otherwise splits the scheme segment on `+`. +- `test(urlStr: string): boolean` — true only if protocol, host, path, and port all match. +- `testHost(hostStr: string): boolean` — segment-by-segment comparison against the pattern's host (split on `.`); a segment of `*` matches anything; segment **counts must match exactly** (no subdomain-wildcard-prefix support beyond single-segment `*`). +- `testPath(pathStr: string): boolean` — segment-by-segment comparison (split on `/`); segment counts must match exactly; `*` matches any single segment. +- `testPort(port: string, protocol: string): boolean` — returns `false` immediately if `testProtocol(protocol)` fails; `*` in the pattern matches any port; if no port is specified in either the pattern or input, falls back to protocol-default-port logic for `http`/`https` only. +- `testProtocol(protocol: string): boolean` — `*` in the pattern matches any protocol. +- `override toString(): string` — returns the raw pattern string. +- `update(pattern: string)` + +### `UrlMatchPatternList` (extends `PropertyList`) +- `override _kind = 'UrlMatchPatternList'` +- `constructor(parent: PropertyList | undefined, populate: T[])` +- `static isUrlMatchPatternList(obj: any): boolean` +- `test(urlStr: string): boolean` — true if **any** pattern in the list matches. + +### Module-level functions +- `toUrlObject(url: string | Url): Url` — **throws** `Error('Request URL is not specified')` if `url` is falsy; passes through an existing `Url` unchanged, otherwise constructs a new one from a string. +- `resolveProtocolForProxy(rawUrl: string): string` — resolves the protocol to use for proxy selection when the URL may still contain unrendered template tags (pre-request scripts run before rendering); tries `new URL(rawUrl).protocol` first, falling back to `'https:'` on parse failure. + +## Script-facing surface + +`pm.request.url` is a `Url` instance. Common script usage: +- `pm.request.url.toString()`, `pm.request.url.getHost()`, `pm.request.url.getPath()`, `pm.request.url.query` +- `pm.request.url.addQueryParams([{key, value}])` / `pm.request.addQueryParams(...)` (delegates) +- `pm.request.url.removeQueryParams('key')` / `pm.request.removeQueryParams(...)` (delegates) +- `pm.request.url.hash`, `.host`, `.port`, `.protocol`, `.auth` + +`QueryParam` instances populate `pm.request.url.query` and `RequestBody.urlencoded`. + +`UrlMatchPattern`/`UrlMatchPatternList` aren't constructed directly from a typical script, but are reachable via `pm.request.certificate.matches` (see `certificates.ts`), which stores its match rule(s) as a `UrlMatchPatternList` directly. `pm.request.proxy.match` (see `proxy-configs.ts`) is different: it's a raw `string` on the instance, with a `UrlMatchPattern` built on the fly, temporarily, inside `ProxyConfig.test()`/`.getProtocols()` — and `ProxyConfig.bypass` is a plain `string[]` checked by exact string equality, not a `UrlMatchPatternList` at all (that type only appears as an unused `static bypass` field on the class, separate from the instance field of the same name). + +## Gotchas / notable behavior + +- `Url.variables` **always returns `[]`**, even though `UrlOptions.variables` and `Url.parse()`'s return both have a `variables` field — the field is accepted on input but silently discarded; there is no way to read it back out. +- `Url`'s internal `urlObject` (a real `URL`) is `undefined` whenever the input string contains a template tag (`{{ }}` / `{% %}`) or otherwise fails `URL.canParse` — in that case nearly every getter/method (`host`, `path`, `port`, `protocol`, `getHost()`, `getRemote()`, etc.) silently returns an empty string/array instead of throwing. Only `toString()`/`toStringWithoutQuery()` fall back to the raw `origin` string. +- Query-string encoding is inconsistent by design: `QueryParam.toString()` URL-encodes (via `URLSearchParams`), but `QueryParam.toRawString()` and `Url.getQueryString()` (which uses `toRawString()`) do **not** encode. `Url.addQueryParams(stringForm)` also splits on raw `&`/`=` without decoding. +- `QueryParam.unparseSingle()` returns `{}` (not a string) when the input lacks `key`/`value` — inconsistent return type (`string | {}`) that callers must handle. +- Several methods accept parameters that are **entirely unused**: `Url.getPath(_unresolved)`, `Url.getRemote(_forcePort)`, `Url.toString(_forceProtocol)`, `Url.toStringWithoutQuery(_forceProtocol)`, `QueryParam.parseSingle(_idx, _all)`. Passing anything for these has no effect. +- `UrlMatchPattern.static pattern` (class-level) is unused/dead and easy to confuse with the instance's own private `pattern` field. +- `UrlMatchPattern.testHost`/`testPath` require the **segment counts to match exactly** — a pattern like `*.insomnia.com` (2 segments) will not match `bin.download.insomnia.com` (4 segments), confirmed by the test suite (`urls.test.ts`). +- `UrlMatchPattern` has no protocol-agnostic short-circuit: calling `testProtocol` on a pattern with no `://` returns `[]` for `getProtocols()`, meaning `testProtocol` always returns `false` for such a malformed pattern (per `urls.test.ts`, "no protocol" case). + +## Related + +- `packages/insomnia-scripting-environment/src/objects/properties.ts` — `Property`, `PropertyBase`, `PropertyList` base classes; `PropertyList.toObject()`'s fallback comment specifically calls out `UrlMatchPatternList` as a list with no natural key. +- `packages/insomnia-scripting-environment/src/objects/utils.ts` — `checkIfUrlIncludesTag`, used by `Url.initFields` to avoid mangling template tags. +- `packages/insomnia-scripting-environment/src/objects/request.ts` — `Request.url: Url`; `toUrlObject` used when constructing a `Request`; `RequestBody.urlencoded: PropertyList`. +- `packages/insomnia-scripting-environment/src/objects/certificates.ts` — `Certificate.matches: UrlMatchPatternList`. +- `packages/insomnia-scripting-environment/src/objects/proxy-configs.ts` — `ProxyConfig.match: string` is used to construct a `UrlMatchPattern` on demand inside `test()`/`getProtocols()`; the instance's `ProxyConfig.bypass` is a plain `string[]` (exact-match only), not a `UrlMatchPatternList`. +- `packages/insomnia-scripting-environment/src/objects/collection.ts` — re-exports `QueryParam`, `Url`, `UrlMatchPattern`, `UrlMatchPatternList` as part of the public collection API surface. +- `packages/insomnia-scripting-environment/src/objects/insomnia.ts` — uses `resolveProtocolForProxy` and `toUrlObject` in `initInsomniaObject`. diff --git a/.claude/skills/fix-scripting-feature/references/objects/utils.md b/.claude/skills/fix-scripting-feature/references/objects/utils.md new file mode 100644 index 00000000000..dc14ca21332 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/utils.md @@ -0,0 +1,35 @@ +# Utils (misc helpers) + +**Source:** `packages/insomnia-scripting-environment/src/objects/utils.ts` + +## Purpose +Single-function utility module. Currently contains only a helper used to detect whether a request +URL still contains unresolved template-tag syntax, which affects client-certificate matching in +`insomnia.ts`. + +## Public API + +### `function checkIfUrlIncludesTag(url: string): boolean` +```ts +export function checkIfUrlIncludesTag(url: string): boolean { + return /{%/.test(`${url}`) || /%}/.test(`${url}`) || /{{/.test(`${url}`) || /}}/.test(`${url}`); +} +``` +Returns `true` if the given `url` (coerced to a string via template literal) contains any of the +four Liquid/interpolation delimiter substrings: `{%`, `%}`, `{{`, `}}`. Used as a proxy for "this +URL hasn't been fully rendered yet" / "this URL has dynamic template parts that can't be resolved +against a fixed hostname". + +## Script-facing surface +None. This is internal plumbing — it is not exposed on `insomnia`/`pm` and scripts cannot call it. + +## Gotchas / notable behavior +- The check is purely textual (four independent regex tests, not a real template parser) — it will + return `true` for a URL that merely contains a literal `{{` or `}}` substring even if it isn't + actually meant as an interpolation tag. +- `` `${url}` `` coercion means passing `undefined`/`null`/non-string values won't throw; they'll be + stringified first (e.g. `undefined` becomes the string `"undefined"`, which does not match any + pattern, so the function returns `false`). + +## Related +- `insomnia.ts` — the only current caller. `initInsomniaObject` uses `checkIfUrlIncludesTag(rawObj.request.url)` together with `filterClientCertificates` (from `insomnia/src/network/certificate`) to decide whether to fall back to an empty default client certificate (since a templated URL's real host can't be matched against certificate rules yet). diff --git a/.claude/skills/fix-scripting-feature/references/objects/variables.md b/.claude/skills/fix-scripting-feature/references/objects/variables.md new file mode 100644 index 00000000000..b63d50cf8d6 --- /dev/null +++ b/.claude/skills/fix-scripting-feature/references/objects/variables.md @@ -0,0 +1,68 @@ +# Variable / VariableList + +**Source:** `packages/insomnia-scripting-environment/src/objects/variables.ts` + +## Purpose +This file defines the low-level key/value model — `Variable` (a single named, typed value) and `VariableList` (an ordered, keyed collection of `Variable`s) — built on top of `Property`/`PropertyList` from `properties.ts`. Unlike `Environment` (a `Map`-backed flat store used for `pm.environment`/`pm.globals`), `Variable`/`VariableList` is the object-per-entry model used elsewhere in the SDK wherever a Postman-compatible "list of variable-shaped items" is needed (e.g. request path parameters); it is not the type behind `insomnia.environment` or `insomnia.variables`. + +## Public API + +### `interface VariableDefinition` +```ts +interface VariableDefinition { + id?: string; + key: string; + name?: string; + value: string; + type?: string; + disabled?: boolean; +} +``` +Shape used to construct a `Variable`. + +### `class Variable extends Property` +```ts +constructor(def?: VariableDefinition) +``` +Calls `super()` (no args passed to `Property`'s constructor, so `Property`'s own `id`/`name`/`disabled` defaults are set first, then overwritten below). When `def` is provided: `id = def.id || ''`, `key = def.key`, `name = def.name`, `value = def.value`, `type = def.type || 'Variable'`, `disabled = def.disabled`. When `def` is omitted: `id = ''`, `key = ''`, `name = undefined`, `value = ''`, `type = 'Variable'`, `disabled = false`. + +- `key: string` — the variable's lookup key. +- `value: any` — the stored value (any type, despite `VariableDefinition.value` being typed `string`). +- `type: string` — defaults to `'Variable'`. +- `override _kind = 'Variable'` *(`@ignore`)*. +- `static override _index = 'key'` *(`@ignore`)* — tells `PropertyList`/`indexOf`/`one` to index `Variable`s by `key` instead of the default `id`. +- `static types()` — **throws** `unsupportedError('types')` unconditionally; not implemented. +- `cast(value: any)` — if `value` has `_kind === 'Variable'`, returns `value.value` (unwraps a `Variable` to its raw value); otherwise returns `undefined`. +- `get()` — returns `this.value`. +- `set(value: any)` — sets `this.value = value`. + +### `class VariableList extends PropertyList` +```ts +constructor(parent: PropertyList | undefined, populate: T[]) +``` +Calls `super(Variable, undefined, populate)` (always uses `Variable` as `typeClass`, ignoring `T`'s actual class for indexing purposes), then sets `this.parent = parent`. + +- `override _kind = 'VariableList'` *(`@ignore`)*. +- `static isVariableList(obj: any): boolean` — `'_kind' in obj && obj._kind === 'VariableList'`. +- `override toObject(excludeDisabled?: boolean, _caseSensitive?: boolean, multiValue?: boolean, _sanitizeKeys?: boolean): Record` — builds a plain object keyed by each `Variable.key`: + - skips entries where `excludeDisabled && variable.disabled`. + - if `multiValue` is true and the key already exists in the output, coalesces values into an array (`[existing, value]`, growing it on further duplicates). + - otherwise, later entries with the same key overwrite earlier ones (last-write-wins) when `multiValue` is falsy. + - `_caseSensitive` and `_sanitizeKeys` are accepted for signature compatibility but unused. + +All other list operations (`add`, `all`, `append`, `assimilate`, `clear`, `count`, `each`, `filter`, `find`, `get`, `has`, `idx`, `indexOf`, `insert`, `insertAfter`, `map`, `one`, `populate`, `prepend`, `reduce`, `remove`, `repopulate`, `toString`, `upsert`) are inherited unmodified from `PropertyList` — see `properties.md`. Because `Variable._index = 'key'`, `indexOf`/`one`/`upsert`/`has` on a `VariableList` match by `key`, not `id`. + +## Script-facing surface +Not directly exposed as `pm.*`/`insomnia.*` on its own — `insomnia.environment` and `insomnia.variables` are backed by `Environment`/`Variables` (see `environments.md`), not by `Variable`/`VariableList`. `Variable`/`VariableList` are the generic building blocks other collection-shaped properties reuse (e.g. anywhere the SDK needs an ordered, keyed set of `{key, value}` items with Postman-style list semantics such as `.upsert()`/`.toObject()`). Exported from the package's public surface via `index.ts` (`export { Variable, VariableList } from './variables'`) and re-exported under the `Collection` namespace via `collection.ts`, so consumers of the SDK's type declarations can reference `Collection.Variable`/`Collection.VariableList`. + +## Gotchas / notable behavior +- `Variable.types()` is a stub that always throws `unsupportedError('types')` — calling it will break a script; it exists only for API-shape completeness. +- `Variable.cast(value)` assumes `value` is an object with `in` support (`'_kind' in value`) — passing a primitive (e.g. `cast(5)`) will throw a `TypeError` since `in` requires an object operand on the right-hand side. There's no type guard before that check. +- `VariableList`'s constructor hardcodes `Variable` as the `typeClass` regardless of the generic `T`, so subclasses of `Variable` (if any) still get indexed as plain `Variable` for `_index` purposes. +- `VariableList.toObject()` **includes disabled variables by default** — callers must explicitly pass `excludeDisabled = true` to filter them out (confirmed by `variables.test.ts`: `toObject()` returns `{h1: 'v1', h2: 'v2'}` even when `h2` is disabled). +- Duplicate keys: `toObject()` silently drops earlier duplicates unless `multiValue` is passed, in which case duplicates become an array under a single key — this can be surprising if a script assumes object-key uniqueness maps 1:1 to list length. + +## Related +- `properties.ts` — `Variable extends Property`, `VariableList extends PropertyList`; inherits nearly all list/property behavior from there. +- `environments.ts` — conceptually parallel/alternate model (`Environment`/`Variables`) that is what `insomnia.environment`/`insomnia.variables`/`insomnia.collectionVariables` actually use; do not confuse the two when debugging variable-resolution issues. +- `index.ts` / `collection.ts` — re-export `Variable`/`VariableList` as part of the SDK's public/namespaced surface. diff --git a/packages/insomnia-scripting-environment/src/objects/folders.ts b/packages/insomnia-scripting-environment/src/objects/folders.ts index 7a799fb8bfe..bf289bbe60e 100644 --- a/packages/insomnia-scripting-environment/src/objects/folders.ts +++ b/packages/insomnia-scripting-environment/src/objects/folders.ts @@ -70,7 +70,7 @@ export class ParentFolders { /** * Creates an instance of the class with a list of folders. * - * @param folders - An array of `Folder` objects to initialize the instance with, from bottom to top. + * @param folders - An array of `Folder` objects to initialize the instance with, from top to bottom. */ constructor(private folders: Folder[]) {} diff --git a/packages/insomnia-scripting-environment/src/objects/properties.ts b/packages/insomnia-scripting-environment/src/objects/properties.ts index 8c53bdf0897..06c729f311f 100644 --- a/packages/insomnia-scripting-environment/src/objects/properties.ts +++ b/packages/insomnia-scripting-environment/src/objects/properties.ts @@ -418,7 +418,7 @@ export class PropertyList { const itemIdx = this.indexOf(item); if (itemIdx !== -1) { - this.list = [...this.list.splice(0, itemIdx), item, ...this.list.splice(itemIdx + 1)]; + this.list = [...this.list.slice(0, itemIdx), item, ...this.list.slice(itemIdx + 1)]; return false; }