@scalar/pre-post-request-scripts 0.4.17 → 0.4.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @scalar/scripts
2
2
 
3
+ ## 0.4.18
4
+
5
+ ### Patch Changes
6
+
7
+ - [#9332](https://github.com/scalar/scalar/pull/9332): fix: running scripts on electron
8
+
3
9
  ## 0.4.17
4
10
 
5
11
  ## 0.4.16
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox-adapter.d.ts","sourceRoot":"","sources":["../../../../src/libs/execute-scripts/postman-adapter/sandbox-adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAA;AAE7F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAGjE,OAAO,EACL,KAAK,yBAAyB,EAE9B,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC5B,MAAM,oBAAoB,CAAA;AAE3B;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,GAAU,UAAU,QAAQ,KAAG,OAAO,CAAC,yBAAyB,CAiB7F,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,QAAO,MAKlC,CAAA;AAeD;;;;;;;GAOG;AACH,KAAK,gBAAgB,GAAG,CAAC,OAAO,EAAE,qBAAqB,EAAE,SAAS,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,IAAI,KAAK,IAAI,CAAA;AAqJtH,0FAA0F;AAC1F,eAAO,MAAM,mBAAmB,GAAI,MAAM,gBAAgB,KAAG,IAE5D,CAAA;AAID,eAAO,MAAM,uBAAuB,GAAU,8GAK3C;IACD,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,aAAa,GAAG,eAAe,CAAA;IACrC,OAAO,EAAE;QACP,uFAAuF;QACvF,cAAc,CAAC,EAAE,cAAc,CAAA;QAC/B,QAAQ,CAAC,EAAE,QAAQ,CAAA;QACnB,cAAc,CAAC,EAAE,cAAc,CAAA;QAC/B,aAAa,EAAE,cAAc,CAAA;KAC9B,CAAA;IACD,mBAAmB,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;CACpE,KAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CA4DrC,CAAA"}
1
+ {"version":3,"file":"sandbox-adapter.d.ts","sourceRoot":"","sources":["../../../../src/libs/execute-scripts/postman-adapter/sandbox-adapter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAA;AAE7F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAIjE,OAAO,EACL,KAAK,yBAAyB,EAE9B,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC5B,MAAM,oBAAoB,CAAA;AAE3B;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,GAAU,UAAU,QAAQ,KAAG,OAAO,CAAC,yBAAyB,CAiB7F,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,QAAO,MAKlC,CAAA;AAED;;;;;;;GAOG;AACH,KAAK,gBAAgB,GAAG,CAAC,OAAO,EAAE,qBAAqB,EAAE,SAAS,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,IAAI,KAAK,IAAI,CAAA;AAkNtH,0FAA0F;AAC1F,eAAO,MAAM,mBAAmB,GAAI,MAAM,gBAAgB,KAAG,IAE5D,CAAA;AAID,eAAO,MAAM,uBAAuB,GAAU,8GAK3C;IACD,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,aAAa,GAAG,eAAe,CAAA;IACrC,OAAO,EAAE;QACP,uFAAuF;QACvF,cAAc,CAAC,EAAE,cAAc,CAAA;QAC/B,QAAQ,CAAC,EAAE,QAAQ,CAAA;QACnB,cAAc,CAAC,EAAE,cAAc,CAAA;QAC/B,aAAa,EAAE,cAAc,CAAA;KAC9B,CAAA;IACD,mBAAmB,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;CACpE,KAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CA4DrC,CAAA"}
@@ -1,6 +1,8 @@
1
1
  import { getVariableScopesFromStore, toVariableEntries } from "../variables-store.js";
2
2
  import { createPostmanRequestFromFactory, syncPlainPostmanRequestToRequestFactory } from "./request-factory-adapter.js";
3
+ import { getSandboxOrigins } from "./sandbox-origins.js";
3
4
  import { SANDBOX_CHANNEL } from "./sandbox-protocol.js";
5
+ import { normalizeError } from "@scalar/helpers/errors/normalize-error";
4
6
  import { isElectron } from "@scalar/helpers/general/is-electron";
5
7
  //#region src/libs/execute-scripts/postman-adapter/sandbox-adapter.ts
6
8
  /**
@@ -43,19 +45,9 @@ var sandboxFrameUrl = () => {
43
45
  if (isElectron()) return new URL("sandbox.html", document.baseURI).href;
44
46
  return new URL("/sandbox.html", window.location.origin).href;
45
47
  };
46
- /**
47
- * Origin we pin `postMessage` exchanges to.
48
- *
49
- * The sandbox iframe is served from the same origin as the host (see `sandbox.html`), so we lock
50
- * every send and receive to `window.location.origin`. Pinning the send origin prevents the payload
51
- * from leaking if the frame is ever navigated away to another origin; pinning the receive origin
52
- * prevents another window (for example a cross-origin opener) from spoofing sandbox messages.
53
- *
54
- * For `file://` documents (Electron), `window.location.origin` is the string `'file://'`, which is
55
- * the same value `postMessage` reports on `event.origin`, so the pin still holds.
56
- */
57
- var sandboxOrigin = () => window.location.origin;
58
48
  var framePromise;
49
+ var activeFrameId;
50
+ var nextFrameId = 0;
59
51
  /**
60
52
  * Maximum time we wait for a freshly created sandbox iframe to post its `ready` handshake.
61
53
  *
@@ -69,15 +61,51 @@ var framePromise;
69
61
  * broken rather than slow.
70
62
  */
71
63
  var SANDBOX_READY_TIMEOUT_MS = 30 * 1e3;
64
+ var clearActiveFrame = (id) => {
65
+ if (activeFrameId === id) {
66
+ framePromise = void 0;
67
+ activeFrameId = void 0;
68
+ }
69
+ };
70
+ var invalidateSandboxFrame = ({ element, id }) => {
71
+ clearActiveFrame(id);
72
+ element.remove();
73
+ };
74
+ var assertSandboxFrameLocation = (sandboxFrame) => {
75
+ const { element, url, window: frameWindow } = sandboxFrame;
76
+ if (!element.isConnected || !document.body.contains(element)) {
77
+ invalidateSandboxFrame(sandboxFrame);
78
+ throw new Error("Sandbox iframe was detached before script execution");
79
+ }
80
+ if (element.contentWindow !== frameWindow) {
81
+ invalidateSandboxFrame(sandboxFrame);
82
+ throw new Error("Sandbox iframe window changed before script execution");
83
+ }
84
+ let currentUrl;
85
+ try {
86
+ currentUrl = frameWindow.location.href;
87
+ } catch (error) {
88
+ invalidateSandboxFrame(sandboxFrame);
89
+ const message = normalizeError(error).message;
90
+ throw new Error(`Could not verify sandbox iframe location before script execution: ${message}`);
91
+ }
92
+ if (currentUrl !== url) {
93
+ invalidateSandboxFrame(sandboxFrame);
94
+ throw new Error("Sandbox iframe navigated before script execution");
95
+ }
96
+ };
72
97
  /** Lazily creates a single hidden sandbox iframe and resolves once it reports readiness. */
73
98
  var ensureSandboxFrame = () => {
74
99
  if (framePromise) return framePromise;
75
100
  framePromise = new Promise((resolve, reject) => {
101
+ const frameId = nextFrameId++;
102
+ activeFrameId = frameId;
76
103
  const iframe = document.createElement("iframe");
77
104
  iframe.setAttribute("aria-hidden", "true");
78
105
  iframe.style.display = "none";
79
- iframe.src = sandboxFrameUrl();
80
- const expectedOrigin = sandboxOrigin();
106
+ const iframeUrl = sandboxFrameUrl();
107
+ iframe.src = iframeUrl;
108
+ const { receive: expectedOrigin } = getSandboxOrigins();
81
109
  let readyTimeoutId;
82
110
  const stopWaiting = () => {
83
111
  clearTimeout(readyTimeoutId);
@@ -90,22 +118,27 @@ var ensureSandboxFrame = () => {
90
118
  stopWaiting();
91
119
  if (!iframe.contentWindow) {
92
120
  iframe.remove();
93
- framePromise = void 0;
121
+ clearActiveFrame(frameId);
94
122
  reject(/* @__PURE__ */ new Error("Sandbox iframe has no content window"));
95
123
  return;
96
124
  }
97
- resolve(iframe.contentWindow);
125
+ resolve({
126
+ element: iframe,
127
+ id: frameId,
128
+ url: iframeUrl,
129
+ window: iframe.contentWindow
130
+ });
98
131
  };
99
132
  iframe.addEventListener("error", () => {
100
133
  stopWaiting();
101
134
  iframe.remove();
102
- framePromise = void 0;
135
+ clearActiveFrame(frameId);
103
136
  reject(/* @__PURE__ */ new Error("Failed to load the script sandbox iframe"));
104
137
  });
105
138
  readyTimeoutId = setTimeout(() => {
106
139
  window.removeEventListener("message", onReady);
107
140
  iframe.remove();
108
- framePromise = void 0;
141
+ clearActiveFrame(frameId);
109
142
  reject(/* @__PURE__ */ new Error(`Sandbox iframe did not report readiness within ${SANDBOX_READY_TIMEOUT_MS}ms`));
110
143
  }, SANDBOX_READY_TIMEOUT_MS);
111
144
  window.addEventListener("message", onReady);
@@ -123,8 +156,10 @@ var ensureSandboxFrame = () => {
123
156
  */
124
157
  var SANDBOX_EXECUTION_TIMEOUT_MS = 300 * 1e3;
125
158
  var iframeTransport = (request, onMessage) => {
126
- ensureSandboxFrame().then((frame) => {
127
- const expectedOrigin = sandboxOrigin();
159
+ ensureSandboxFrame().then((sandboxFrame) => {
160
+ assertSandboxFrameLocation(sandboxFrame);
161
+ const frame = sandboxFrame.window;
162
+ const { receive: expectedOrigin, send: targetOrigin } = getSandboxOrigins();
128
163
  const timeoutId = setTimeout(() => {
129
164
  window.removeEventListener("message", handleMessage);
130
165
  onMessage({
@@ -145,7 +180,7 @@ var iframeTransport = (request, onMessage) => {
145
180
  onMessage(data);
146
181
  };
147
182
  window.addEventListener("message", handleMessage);
148
- frame.postMessage(request, expectedOrigin);
183
+ frame.postMessage(request, targetOrigin);
149
184
  }).catch((error) => {
150
185
  onMessage({
151
186
  channel: SANDBOX_CHANNEL,
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox-adapter.js","names":[],"sources":["../../../../src/libs/execute-scripts/postman-adapter/sandbox-adapter.ts"],"sourcesContent":["import { isElectron } from '@scalar/helpers/general/is-electron'\nimport type { RequestFactory, VariablesStore } from '@scalar/workspace-store/request-example'\n\nimport type { ConsoleContext } from '../context/console'\nimport type { TestResult } from '../execute-post-response-script'\nimport { getVariableScopesFromStore, toVariableEntries } from '../variables-store'\nimport { createPostmanRequestFromFactory, syncPlainPostmanRequestToRequestFactory } from './request-factory-adapter'\nimport {\n type PostmanResponseDefinition,\n SANDBOX_CHANNEL,\n type SandboxExecuteRequest,\n type SandboxOutboundMessage,\n} from './sandbox-protocol'\n\n/**\n * Host-side adapter for pre/post-request scripts.\n *\n * The actual script execution happens in `postman-sandbox`, which requires `eval`. To keep\n * `'unsafe-eval'` out of the main application CSP, that execution is delegated to an iframe loaded\n * from a real same-origin `.html` file with its own permissive CSP (see `sandbox-frame-server.ts`).\n * This module therefore must NOT import `postman-sandbox`; it only serializes inputs, talks to the\n * iframe over `postMessage`, and applies the results back to the live store/request.\n */\nexport const toPostmanResponse = async (response: Response): Promise<PostmanResponseDefinition> => {\n // Read as ArrayBuffer (not text) so binary payloads — gzipped JSON, images, PDFs, protobuf —\n // survive the trip through `postMessage` byte-for-byte. Round-tripping through `response.text()`\n // + `TextEncoder` corrupts any sequence that is not valid UTF-8 because the decoder replaces\n // invalid units with U+FFFD before we ever encode them back to bytes.\n const buffer = await response.arrayBuffer()\n const responseBytes = Array.from(new Uint8Array(buffer))\n\n return {\n code: response.status,\n status: response.statusText || String(response.status),\n header: Array.from(response.headers.entries()).map(([key, value]) => ({ key, value })),\n stream: {\n type: 'Buffer',\n data: responseBytes,\n },\n }\n}\n\n/**\n * Resolves the sandbox iframe URL.\n *\n * On the web we anchor to the origin root because the SPA router owns the current path: a\n * relative `sandbox.html` would resolve under the active route and get rewritten to `index.html`\n * by the history fallback. In Electron the document path is literal, so a sibling resolution\n * works — and we use the `isElectron` helper rather than a `file://` check because Electron's\n * dev mode is served over `http://localhost`.\n *\n * Exported for tests; production code reaches it through `ensureSandboxFrame`.\n */\nexport const sandboxFrameUrl = (): string => {\n if (isElectron()) {\n return new URL('sandbox.html', document.baseURI).href\n }\n return new URL('/sandbox.html', window.location.origin).href\n}\n\n/**\n * Origin we pin `postMessage` exchanges to.\n *\n * The sandbox iframe is served from the same origin as the host (see `sandbox.html`), so we lock\n * every send and receive to `window.location.origin`. Pinning the send origin prevents the payload\n * from leaking if the frame is ever navigated away to another origin; pinning the receive origin\n * prevents another window (for example a cross-origin opener) from spoofing sandbox messages.\n *\n * For `file://` documents (Electron), `window.location.origin` is the string `'file://'`, which is\n * the same value `postMessage` reports on `event.origin`, so the pin still holds.\n */\nconst sandboxOrigin = (): string => window.location.origin\n\n/**\n * Delivers a single execute request to the sandbox and forwards every message it produces (test\n * results, console output, and the final `done`) to `onMessage`.\n *\n * The default transport is the iframe (see {@link iframeTransport}). It is injectable so that node\n * unit tests can run the real execution in-process (`runSandboxExecution`) without a DOM, and without\n * the production host bundle ever importing `postman-sandbox`.\n */\ntype SandboxTransport = (request: SandboxExecuteRequest, onMessage: (message: SandboxOutboundMessage) => void) => void\n\nlet framePromise: Promise<Window> | undefined\n\n/**\n * Maximum time we wait for a freshly created sandbox iframe to post its `ready` handshake.\n *\n * The iframe `error` event fires only when the `.html` document itself fails to load. If the\n * document loads (HTTP 200) but the script inside never reaches `startSandboxFrameServer` — a\n * 404 on the bundle, an import error, a CSP block, a crash during boot — no `ready` message is\n * posted and no `error` event fires. Without this ceiling the readiness promise hangs forever;\n * because it is module-cached, every later call awaits the same dead promise and all script\n * execution freezes for the session. The window is generous: readiness only requires loading one\n * same-origin HTML file plus the sandbox bundle, so timing out means the bundle is genuinely\n * broken rather than slow.\n */\nconst SANDBOX_READY_TIMEOUT_MS = 30 * 1000\n\n/** Lazily creates a single hidden sandbox iframe and resolves once it reports readiness. */\nconst ensureSandboxFrame = (): Promise<Window> => {\n if (framePromise) {\n return framePromise\n }\n\n framePromise = new Promise<Window>((resolve, reject) => {\n const iframe = document.createElement('iframe')\n iframe.setAttribute('aria-hidden', 'true')\n iframe.style.display = 'none'\n iframe.src = sandboxFrameUrl()\n\n const expectedOrigin = sandboxOrigin()\n\n // Cleanup shared by the success, load-error, and readiness-timeout paths: cancel the timer and\n // drop the window listener so exactly one of the three settles the promise without leaking.\n let readyTimeoutId: ReturnType<typeof setTimeout>\n const stopWaiting = () => {\n clearTimeout(readyTimeoutId)\n window.removeEventListener('message', onReady)\n }\n\n const onReady = (event: MessageEvent) => {\n // Same-origin iframe: reject anything that does not come from our own origin and our own frame.\n if (event.origin !== expectedOrigin || event.source !== iframe.contentWindow) {\n return\n }\n const data = event.data as SandboxOutboundMessage | null\n if (!data || data.channel !== SANDBOX_CHANNEL || data.kind !== 'ready') {\n return\n }\n stopWaiting()\n if (!iframe.contentWindow) {\n // Tear the orphaned node down so a retry can append a fresh frame without piling up.\n iframe.remove()\n framePromise = undefined\n reject(new Error('Sandbox iframe has no content window'))\n return\n }\n resolve(iframe.contentWindow)\n }\n\n iframe.addEventListener('error', () => {\n stopWaiting()\n // Drop the broken iframe from the DOM. Without this, a transient failure (network blip, bad\n // bundle path, etc.) leaves a dead `<iframe>` attached, and every retry appends another one,\n // growing the DOM unboundedly across the session.\n iframe.remove()\n framePromise = undefined\n reject(new Error('Failed to load the script sandbox iframe'))\n })\n\n // Catch the silent failure mode the `error` event cannot see: the document loaded but its\n // script never announced readiness. Drop the dead frame and clear the cache so the next call\n // retries with a fresh iframe instead of awaiting this one forever.\n readyTimeoutId = setTimeout(() => {\n window.removeEventListener('message', onReady)\n iframe.remove()\n framePromise = undefined\n reject(new Error(`Sandbox iframe did not report readiness within ${SANDBOX_READY_TIMEOUT_MS}ms`))\n }, SANDBOX_READY_TIMEOUT_MS)\n\n window.addEventListener('message', onReady)\n document.body.appendChild(iframe)\n })\n\n return framePromise\n}\n\n/**\n * Maximum time we wait for the iframe to emit `kind: 'done'` for a given execute request.\n *\n * postman-sandbox enforces its own per-script timeout inside the sandbox, so reaching this ceiling\n * means the iframe itself wedged (renderer OOM, hard crash, blocking infinite loop in a worker,\n * messages dropped). The default is generous on purpose — this is a circuit-breaker for crashes,\n * not a budget for user scripts.\n */\nconst SANDBOX_EXECUTION_TIMEOUT_MS = 5 * 60 * 1000\n\nconst iframeTransport: SandboxTransport = (request, onMessage) => {\n ensureSandboxFrame()\n .then((frame) => {\n const expectedOrigin = sandboxOrigin()\n\n // Defensive circuit-breaker: if the iframe dies mid-execution we would otherwise wait on a\n // `done` message that will never arrive, leaking the listener and leaving the caller's\n // promise unresolved. The timer is cancelled the moment `done` is observed below.\n const timeoutId = setTimeout(() => {\n window.removeEventListener('message', handleMessage)\n onMessage({\n channel: SANDBOX_CHANNEL,\n kind: 'done',\n id: request.id,\n error: `Script execution timed out after ${SANDBOX_EXECUTION_TIMEOUT_MS}ms`,\n })\n }, SANDBOX_EXECUTION_TIMEOUT_MS)\n\n const handleMessage = (event: MessageEvent) => {\n // Reject anything that is not from our own origin and our own frame. Without this guard,\n // any window in the same browsing context group could send fabricated `done`/`test-results`\n // messages and trick the host into applying attacker-controlled variables and requests.\n if (event.origin !== expectedOrigin || event.source !== frame) {\n return\n }\n const data = event.data as SandboxOutboundMessage | null\n if (!data || data.channel !== SANDBOX_CHANNEL || !('id' in data) || data.id !== request.id) {\n return\n }\n if (data.kind === 'done') {\n clearTimeout(timeoutId)\n window.removeEventListener('message', handleMessage)\n }\n onMessage(data)\n }\n\n window.addEventListener('message', handleMessage)\n // Pin the target origin: the execute payload contains environments, headers, secrets, and\n // potentially auth tokens, so we must never broadcast it with `'*'`.\n frame.postMessage(request, expectedOrigin)\n })\n .catch((error: unknown) => {\n onMessage({\n channel: SANDBOX_CHANNEL,\n kind: 'done',\n id: request.id,\n error: error instanceof Error ? error.message : String(error),\n })\n })\n}\nlet transport: SandboxTransport = iframeTransport\n\n/** Override how the host reaches the sandbox (e.g. an in-process transport for tests). */\nexport const setSandboxTransport = (next: SandboxTransport): void => {\n transport = next\n}\n\nlet nextExecutionId = 0\n\nexport const executeInPostmanSandbox = async ({\n script,\n type,\n context: { requestBuilder, response, variablesStore, scriptConsole },\n onTestResultsUpdate,\n}: {\n script: string\n type: 'pre-request' | 'post-response'\n context: {\n /** Postman Collection request for `pm.request` (not the browser Fetch API Request). */\n requestBuilder?: RequestFactory\n response?: Response\n variablesStore?: VariablesStore\n scriptConsole: ConsoleContext\n }\n onTestResultsUpdate?: ((results: TestResult[]) => void) | undefined\n}): Promise<VariablesStore | undefined> => {\n // Serialize everything that crosses the iframe boundary on the host, so the iframe only deals with\n // plain, structured-cloneable data. The response body is read here (it requires the live Response).\n const requestDefinition = requestBuilder ? createPostmanRequestFromFactory(requestBuilder).toJSON() : undefined\n const responseDefinition = response ? await toPostmanResponse(response) : undefined\n const scopes = variablesStore ? getVariableScopesFromStore(variablesStore) : undefined\n\n const request: SandboxExecuteRequest = {\n channel: SANDBOX_CHANNEL,\n kind: 'execute',\n id: String(nextExecutionId++),\n listen: type === 'pre-request' ? 'prerequest' : 'test',\n script,\n scopes,\n request: requestDefinition,\n response: responseDefinition,\n }\n\n await new Promise<void>((resolve) => {\n const onMessage = (data: SandboxOutboundMessage) => {\n if (data.kind === 'test-results') {\n onTestResultsUpdate?.(data.results)\n return\n }\n\n if (data.kind === 'console') {\n const consoleMethod = scriptConsole[data.level] ?? scriptConsole.log\n ;(consoleMethod as (...params: unknown[]) => void)(...data.args)\n return\n }\n\n if (data.kind === 'done') {\n if (variablesStore && data.variables) {\n const { local, collection, globals, environment } = data.variables\n if (local) {\n variablesStore.setLocalVariables(toVariableEntries(local))\n }\n if (collection) {\n variablesStore.setCollectionVariables?.(toVariableEntries(collection))\n }\n if (globals) {\n variablesStore.setGlobals?.(toVariableEntries(globals))\n }\n if (environment) {\n variablesStore.setEnvironment?.(toVariableEntries(environment))\n }\n }\n\n if (!data.error && type === 'pre-request' && requestBuilder && data.request !== undefined) {\n syncPlainPostmanRequestToRequestFactory(data.request, requestBuilder)\n }\n\n resolve()\n }\n }\n\n transport(request, onMessage)\n })\n\n return variablesStore\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sandbox-adapter.js","names":[],"sources":["../../../../src/libs/execute-scripts/postman-adapter/sandbox-adapter.ts"],"sourcesContent":["import { normalizeError } from '@scalar/helpers/errors/normalize-error'\nimport { isElectron } from '@scalar/helpers/general/is-electron'\nimport type { RequestFactory, VariablesStore } from '@scalar/workspace-store/request-example'\n\nimport type { ConsoleContext } from '../context/console'\nimport type { TestResult } from '../execute-post-response-script'\nimport { getVariableScopesFromStore, toVariableEntries } from '../variables-store'\nimport { createPostmanRequestFromFactory, syncPlainPostmanRequestToRequestFactory } from './request-factory-adapter'\nimport { getSandboxOrigins } from './sandbox-origins'\nimport {\n type PostmanResponseDefinition,\n SANDBOX_CHANNEL,\n type SandboxExecuteRequest,\n type SandboxOutboundMessage,\n} from './sandbox-protocol'\n\n/**\n * Host-side adapter for pre/post-request scripts.\n *\n * The actual script execution happens in `postman-sandbox`, which requires `eval`. To keep\n * `'unsafe-eval'` out of the main application CSP, that execution is delegated to an iframe loaded\n * from a real same-origin `.html` file with its own permissive CSP (see `sandbox-frame-server.ts`).\n * This module therefore must NOT import `postman-sandbox`; it only serializes inputs, talks to the\n * iframe over `postMessage`, and applies the results back to the live store/request.\n */\nexport const toPostmanResponse = async (response: Response): Promise<PostmanResponseDefinition> => {\n // Read as ArrayBuffer (not text) so binary payloads — gzipped JSON, images, PDFs, protobuf —\n // survive the trip through `postMessage` byte-for-byte. Round-tripping through `response.text()`\n // + `TextEncoder` corrupts any sequence that is not valid UTF-8 because the decoder replaces\n // invalid units with U+FFFD before we ever encode them back to bytes.\n const buffer = await response.arrayBuffer()\n const responseBytes = Array.from(new Uint8Array(buffer))\n\n return {\n code: response.status,\n status: response.statusText || String(response.status),\n header: Array.from(response.headers.entries()).map(([key, value]) => ({ key, value })),\n stream: {\n type: 'Buffer',\n data: responseBytes,\n },\n }\n}\n\n/**\n * Resolves the sandbox iframe URL.\n *\n * On the web we anchor to the origin root because the SPA router owns the current path: a\n * relative `sandbox.html` would resolve under the active route and get rewritten to `index.html`\n * by the history fallback. In Electron the document path is literal, so a sibling resolution\n * works — and we use the `isElectron` helper rather than a `file://` check because Electron's\n * dev mode is served over `http://localhost`.\n *\n * Exported for tests; production code reaches it through `ensureSandboxFrame`.\n */\nexport const sandboxFrameUrl = (): string => {\n if (isElectron()) {\n return new URL('sandbox.html', document.baseURI).href\n }\n return new URL('/sandbox.html', window.location.origin).href\n}\n\n/**\n * Delivers a single execute request to the sandbox and forwards every message it produces (test\n * results, console output, and the final `done`) to `onMessage`.\n *\n * The default transport is the iframe (see {@link iframeTransport}). It is injectable so that node\n * unit tests can run the real execution in-process (`runSandboxExecution`) without a DOM, and without\n * the production host bundle ever importing `postman-sandbox`.\n */\ntype SandboxTransport = (request: SandboxExecuteRequest, onMessage: (message: SandboxOutboundMessage) => void) => void\n\ntype SandboxFrame = {\n element: HTMLIFrameElement\n id: number\n url: string\n window: Window\n}\n\nlet framePromise: Promise<SandboxFrame> | undefined\nlet activeFrameId: number | undefined\nlet nextFrameId = 0\n\n/**\n * Maximum time we wait for a freshly created sandbox iframe to post its `ready` handshake.\n *\n * The iframe `error` event fires only when the `.html` document itself fails to load. If the\n * document loads (HTTP 200) but the script inside never reaches `startSandboxFrameServer` — a\n * 404 on the bundle, an import error, a CSP block, a crash during boot — no `ready` message is\n * posted and no `error` event fires. Without this ceiling the readiness promise hangs forever;\n * because it is module-cached, every later call awaits the same dead promise and all script\n * execution freezes for the session. The window is generous: readiness only requires loading one\n * same-origin HTML file plus the sandbox bundle, so timing out means the bundle is genuinely\n * broken rather than slow.\n */\nconst SANDBOX_READY_TIMEOUT_MS = 30 * 1000\n\nconst clearActiveFrame = (id: number): void => {\n if (activeFrameId === id) {\n framePromise = undefined\n activeFrameId = undefined\n }\n}\n\nconst invalidateSandboxFrame = ({ element, id }: SandboxFrame): void => {\n clearActiveFrame(id)\n element.remove()\n}\n\nconst assertSandboxFrameLocation = (sandboxFrame: SandboxFrame): void => {\n const { element, url, window: frameWindow } = sandboxFrame\n if (!element.isConnected || !document.body.contains(element)) {\n invalidateSandboxFrame(sandboxFrame)\n throw new Error('Sandbox iframe was detached before script execution')\n }\n\n if (element.contentWindow !== frameWindow) {\n invalidateSandboxFrame(sandboxFrame)\n throw new Error('Sandbox iframe window changed before script execution')\n }\n\n let currentUrl: string\n\n try {\n currentUrl = frameWindow.location.href\n } catch (error) {\n invalidateSandboxFrame(sandboxFrame)\n const message = normalizeError(error).message\n throw new Error(`Could not verify sandbox iframe location before script execution: ${message}`)\n }\n\n if (currentUrl !== url) {\n invalidateSandboxFrame(sandboxFrame)\n throw new Error('Sandbox iframe navigated before script execution')\n }\n}\n\n/** Lazily creates a single hidden sandbox iframe and resolves once it reports readiness. */\nconst ensureSandboxFrame = (): Promise<SandboxFrame> => {\n if (framePromise) {\n return framePromise\n }\n\n framePromise = new Promise<SandboxFrame>((resolve, reject) => {\n const frameId = nextFrameId++\n activeFrameId = frameId\n const iframe = document.createElement('iframe')\n iframe.setAttribute('aria-hidden', 'true')\n iframe.style.display = 'none'\n const iframeUrl = sandboxFrameUrl()\n iframe.src = iframeUrl\n\n const { receive: expectedOrigin } = getSandboxOrigins()\n\n // Cleanup shared by the success, load-error, and readiness-timeout paths: cancel the timer and\n // drop the window listener so exactly one of the three settles the promise without leaking.\n let readyTimeoutId: ReturnType<typeof setTimeout>\n const stopWaiting = () => {\n clearTimeout(readyTimeoutId)\n window.removeEventListener('message', onReady)\n }\n\n const onReady = (event: MessageEvent) => {\n // Same-origin iframe: reject anything that does not come from our own origin and our own frame.\n if (event.origin !== expectedOrigin || event.source !== iframe.contentWindow) {\n return\n }\n const data = event.data as SandboxOutboundMessage | null\n if (!data || data.channel !== SANDBOX_CHANNEL || data.kind !== 'ready') {\n return\n }\n stopWaiting()\n if (!iframe.contentWindow) {\n // Tear the orphaned node down so a retry can append a fresh frame without piling up.\n iframe.remove()\n clearActiveFrame(frameId)\n reject(new Error('Sandbox iframe has no content window'))\n return\n }\n resolve({\n element: iframe,\n id: frameId,\n url: iframeUrl,\n window: iframe.contentWindow,\n })\n }\n\n iframe.addEventListener('error', () => {\n stopWaiting()\n // Drop the broken iframe from the DOM. Without this, a transient failure (network blip, bad\n // bundle path, etc.) leaves a dead `<iframe>` attached, and every retry appends another one,\n // growing the DOM unboundedly across the session.\n iframe.remove()\n clearActiveFrame(frameId)\n reject(new Error('Failed to load the script sandbox iframe'))\n })\n\n // Catch the silent failure mode the `error` event cannot see: the document loaded but its\n // script never announced readiness. Drop the dead frame and clear the cache so the next call\n // retries with a fresh iframe instead of awaiting this one forever.\n readyTimeoutId = setTimeout(() => {\n window.removeEventListener('message', onReady)\n iframe.remove()\n clearActiveFrame(frameId)\n reject(new Error(`Sandbox iframe did not report readiness within ${SANDBOX_READY_TIMEOUT_MS}ms`))\n }, SANDBOX_READY_TIMEOUT_MS)\n\n window.addEventListener('message', onReady)\n document.body.appendChild(iframe)\n })\n\n return framePromise\n}\n\n/**\n * Maximum time we wait for the iframe to emit `kind: 'done'` for a given execute request.\n *\n * postman-sandbox enforces its own per-script timeout inside the sandbox, so reaching this ceiling\n * means the iframe itself wedged (renderer OOM, hard crash, blocking infinite loop in a worker,\n * messages dropped). The default is generous on purpose — this is a circuit-breaker for crashes,\n * not a budget for user scripts.\n */\nconst SANDBOX_EXECUTION_TIMEOUT_MS = 5 * 60 * 1000\n\nconst iframeTransport: SandboxTransport = (request, onMessage) => {\n ensureSandboxFrame()\n .then((sandboxFrame) => {\n assertSandboxFrameLocation(sandboxFrame)\n\n const frame = sandboxFrame.window\n const { receive: expectedOrigin, send: targetOrigin } = getSandboxOrigins()\n\n // Defensive circuit-breaker: if the iframe dies mid-execution we would otherwise wait on a\n // `done` message that will never arrive, leaking the listener and leaving the caller's\n // promise unresolved. The timer is cancelled the moment `done` is observed below.\n const timeoutId = setTimeout(() => {\n window.removeEventListener('message', handleMessage)\n onMessage({\n channel: SANDBOX_CHANNEL,\n kind: 'done',\n id: request.id,\n error: `Script execution timed out after ${SANDBOX_EXECUTION_TIMEOUT_MS}ms`,\n })\n }, SANDBOX_EXECUTION_TIMEOUT_MS)\n\n const handleMessage = (event: MessageEvent) => {\n // Reject anything that is not from our own origin and our own frame. Without this guard,\n // any window in the same browsing context group could send fabricated `done`/`test-results`\n // messages and trick the host into applying attacker-controlled variables and requests.\n if (event.origin !== expectedOrigin || event.source !== frame) {\n return\n }\n const data = event.data as SandboxOutboundMessage | null\n if (!data || data.channel !== SANDBOX_CHANNEL || !('id' in data) || data.id !== request.id) {\n return\n }\n if (data.kind === 'done') {\n clearTimeout(timeoutId)\n window.removeEventListener('message', handleMessage)\n }\n onMessage(data)\n }\n\n window.addEventListener('message', handleMessage)\n // Pin the target origin for web origins. `file://` is the only exception: Chromium does not\n // accept it as a target origin, so Electron file builds use `'*'` and rely on the source-window\n // checks above for receive-side isolation.\n frame.postMessage(request, targetOrigin)\n })\n .catch((error: unknown) => {\n onMessage({\n channel: SANDBOX_CHANNEL,\n kind: 'done',\n id: request.id,\n error: error instanceof Error ? error.message : String(error),\n })\n })\n}\nlet transport: SandboxTransport = iframeTransport\n\n/** Override how the host reaches the sandbox (e.g. an in-process transport for tests). */\nexport const setSandboxTransport = (next: SandboxTransport): void => {\n transport = next\n}\n\nlet nextExecutionId = 0\n\nexport const executeInPostmanSandbox = async ({\n script,\n type,\n context: { requestBuilder, response, variablesStore, scriptConsole },\n onTestResultsUpdate,\n}: {\n script: string\n type: 'pre-request' | 'post-response'\n context: {\n /** Postman Collection request for `pm.request` (not the browser Fetch API Request). */\n requestBuilder?: RequestFactory\n response?: Response\n variablesStore?: VariablesStore\n scriptConsole: ConsoleContext\n }\n onTestResultsUpdate?: ((results: TestResult[]) => void) | undefined\n}): Promise<VariablesStore | undefined> => {\n // Serialize everything that crosses the iframe boundary on the host, so the iframe only deals with\n // plain, structured-cloneable data. The response body is read here (it requires the live Response).\n const requestDefinition = requestBuilder ? createPostmanRequestFromFactory(requestBuilder).toJSON() : undefined\n const responseDefinition = response ? await toPostmanResponse(response) : undefined\n const scopes = variablesStore ? getVariableScopesFromStore(variablesStore) : undefined\n\n const request: SandboxExecuteRequest = {\n channel: SANDBOX_CHANNEL,\n kind: 'execute',\n id: String(nextExecutionId++),\n listen: type === 'pre-request' ? 'prerequest' : 'test',\n script,\n scopes,\n request: requestDefinition,\n response: responseDefinition,\n }\n\n await new Promise<void>((resolve) => {\n const onMessage = (data: SandboxOutboundMessage) => {\n if (data.kind === 'test-results') {\n onTestResultsUpdate?.(data.results)\n return\n }\n\n if (data.kind === 'console') {\n const consoleMethod = scriptConsole[data.level] ?? scriptConsole.log\n ;(consoleMethod as (...params: unknown[]) => void)(...data.args)\n return\n }\n\n if (data.kind === 'done') {\n if (variablesStore && data.variables) {\n const { local, collection, globals, environment } = data.variables\n if (local) {\n variablesStore.setLocalVariables(toVariableEntries(local))\n }\n if (collection) {\n variablesStore.setCollectionVariables?.(toVariableEntries(collection))\n }\n if (globals) {\n variablesStore.setGlobals?.(toVariableEntries(globals))\n }\n if (environment) {\n variablesStore.setEnvironment?.(toVariableEntries(environment))\n }\n }\n\n if (!data.error && type === 'pre-request' && requestBuilder && data.request !== undefined) {\n syncPlainPostmanRequestToRequestFactory(data.request, requestBuilder)\n }\n\n resolve()\n }\n }\n\n transport(request, onMessage)\n })\n\n return variablesStore\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox-frame-server.d.ts","sourceRoot":"","sources":["../../../../src/libs/execute-scripts/postman-adapter/sandbox-frame-server.ts"],"names":[],"mappings":"AAOA,OAAO,EAAmB,KAAK,qBAAqB,EAAE,KAAK,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AA2D7G;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC9B,SAAS,qBAAqB,EAC9B,MAAM,CAAC,OAAO,EAAE,sBAAsB,KAAK,IAAI,KAC9C,OAAO,CAAC,IAAI,CAmHd,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,QAAO,CAAC,MAAM,IAAI,CAgDrD,CAAA"}
1
+ {"version":3,"file":"sandbox-frame-server.d.ts","sourceRoot":"","sources":["../../../../src/libs/execute-scripts/postman-adapter/sandbox-frame-server.ts"],"names":[],"mappings":"AAQA,OAAO,EAAmB,KAAK,qBAAqB,EAAE,KAAK,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AA2D7G;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC9B,SAAS,qBAAqB,EAC9B,MAAM,CAAC,OAAO,EAAE,sBAAsB,KAAK,IAAI,KAC9C,OAAO,CAAC,IAAI,CAmHd,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,QAAO,CAAC,MAAM,IAAI,CA+CrD,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { getSandboxOrigins } from "./sandbox-origins.js";
1
2
  import { SANDBOX_CHANNEL } from "./sandbox-protocol.js";
2
3
  import { buildSandboxContextFromScopes } from "../build-sandbox-context.js";
3
4
  import { Request } from "postman-collection";
@@ -154,12 +155,13 @@ var startSandboxFrameServer = () => {
154
155
  * sandbox output (which can include user secrets returned via `pm.environment`) from leaking
155
156
  * to a different origin if the parent is ever swapped out.
156
157
  *
157
- * For `file://` documents (Electron) this resolves to `'file://'`, which is the same value
158
- * `postMessage` exposes on `event.origin`, so the pin still holds.
158
+ * For `file://` documents (Electron), Chromium reports message origins as `'null'` and does not
159
+ * allow `'file://'` as a `postMessage` target origin. In that case we target `'*'` and rely on the
160
+ * exact parent/source-window checks on both sides for isolation.
159
161
  */
160
- const expectedOrigin = window.location.origin;
162
+ const { receive: expectedOrigin, send: targetOrigin } = getSandboxOrigins();
161
163
  const post = (message) => {
162
- window.parent?.postMessage(message, expectedOrigin);
164
+ window.parent?.postMessage(message, targetOrigin);
163
165
  };
164
166
  const handleMessage = (event) => {
165
167
  if (event.origin !== expectedOrigin || event.source !== window.parent) return;
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox-frame-server.js","names":[],"sources":["../../../../src/libs/execute-scripts/postman-adapter/sandbox-frame-server.ts"],"sourcesContent":["import { Request as PostmanRequest } from 'postman-collection'\nimport type { ExecutionResult, SandboxContext } from 'postman-sandbox'\nimport Sandbox from 'postman-sandbox'\n\nimport { buildSandboxContextFromScopes } from '../build-sandbox-context'\nimport type { ConsoleContext } from '../context/console'\nimport type { TestResult } from '../execute-post-response-script'\nimport { SANDBOX_CHANNEL, type SandboxExecuteRequest, type SandboxOutboundMessage } from './sandbox-protocol'\n\n/**\n * Iframe-side sandbox bridge.\n *\n * This is the ONLY module that imports `postman-sandbox` (and therefore the only place that performs\n * `eval`/`new Function`). It is loaded exclusively by the sandbox `.html` host page, which carries its\n * own permissive CSP. The main application bundle never imports this file, so it stays eval-free and\n * its CSP can drop `'unsafe-eval'`.\n */\n\ntype AssertionEvent = {\n name: string\n index: number\n passed: boolean\n skipped: boolean\n error: { message?: string } | null\n}\n\nconst toErrorMessage = (error: unknown): string => {\n if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') {\n return error.message\n }\n\n return String(error)\n}\n\nconst upsertTestResult = (testResults: TestResult[], assertion: AssertionEvent, duration: number): void => {\n const title = assertion.name || `Assertion ${assertion.index + 1}`\n\n const nextResult: TestResult = {\n title,\n passed: assertion.passed,\n duration,\n error: assertion.error?.message,\n status: assertion.passed ? 'passed' : 'failed',\n }\n\n const existingResultIndex = testResults.findIndex((result) => result.title === title)\n if (existingResultIndex === -1) {\n testResults.push(nextResult)\n return\n }\n\n testResults[existingResultIndex] = nextResult\n}\n\nconst createContext = (): Promise<SandboxContext> =>\n new Promise((resolve, reject) => {\n Sandbox.createContext((error: unknown, context: SandboxContext) => {\n if (error) {\n reject(error)\n return\n }\n\n resolve(context)\n })\n })\n\n/**\n * Runs a single script in the postman-sandbox and reports progress/results via {@link emit}.\n *\n * Inputs arrive as plain, structured-cloneable data (already converted on the host) and are rebuilt\n * into postman-collection objects here. Variable mutations and the mutated request are sent back as\n * plain values; the host applies them to the live `VariablesStore`/`RequestFactory`.\n */\nexport const runSandboxExecution = async (\n request: SandboxExecuteRequest,\n emit: (message: SandboxOutboundMessage) => void,\n): Promise<void> => {\n const { id, listen, script, scopes, request: requestDefinition, response } = request\n\n const testResults: TestResult[] = []\n let lastAssertionTime = 0\n let scriptExecutionStartedAt = 0\n\n const sandboxContext = await createContext()\n\n const handleAssertion = (_cursor: unknown, assertions: AssertionEvent[]) => {\n assertions.forEach((assertion) => {\n const duration = Number((performance.now() - lastAssertionTime).toFixed(2))\n lastAssertionTime = performance.now()\n upsertTestResult(testResults, assertion, duration)\n })\n emit({ channel: SANDBOX_CHANNEL, kind: 'test-results', id, results: [...testResults] })\n }\n\n const handleConsole = (_cursor: unknown, level: string, ...args: unknown[]) => {\n emit({ channel: SANDBOX_CHANNEL, kind: 'console', id, level: level as keyof ConsoleContext, args })\n }\n\n try {\n sandboxContext.on('execution.assertion', handleAssertion)\n sandboxContext.on('console', handleConsole)\n\n const variablesContext = scopes ? buildSandboxContextFromScopes(scopes) : undefined\n\n scriptExecutionStartedAt = performance.now()\n lastAssertionTime = scriptExecutionStartedAt\n\n /**\n * Lodash `_.has(context, 'response')` is true even when the value is `undefined`,\n * which makes the sandbox run `new Response(undefined)` and breaks `pm.request`.\n * Only set keys that are actually present.\n */\n const context: Record<string, unknown> = {\n ...(variablesContext ?? {}),\n }\n\n if (requestDefinition !== undefined) {\n context.request = new PostmanRequest(requestDefinition as ConstructorParameters<typeof PostmanRequest>[0])\n }\n if (response !== undefined) {\n context.response = response\n }\n\n await new Promise<void>((resolve) => {\n sandboxContext.execute(\n {\n listen,\n script: {\n exec: [script],\n },\n },\n {\n disableLegacyAPIs: true,\n context,\n },\n (error: unknown, execution?: ExecutionResult) => {\n const done: SandboxOutboundMessage = {\n channel: SANDBOX_CHANNEL,\n kind: 'done',\n id,\n }\n\n if (execution) {\n done.variables = {\n local: execution._variables?.values,\n collection: execution.collectionVariables?.values,\n globals: execution.globals?.values,\n environment: execution.environment?.values,\n }\n }\n\n if (!error && listen === 'prerequest' && execution?.request !== undefined) {\n done.request = execution.request\n }\n\n if (error) {\n const duration = Number((performance.now() - scriptExecutionStartedAt).toFixed(2))\n const errorMessage = toErrorMessage(error)\n done.error = errorMessage\n\n emit({\n channel: SANDBOX_CHANNEL,\n kind: 'console',\n id,\n level: 'error',\n args: [\n `[${listen === 'prerequest' ? 'PRE-REQUEST' : 'POST-RESPONSE'} Script] Error (${duration}ms):`,\n errorMessage,\n ],\n })\n\n testResults.push({\n title: 'Script Execution',\n passed: false,\n duration,\n error: errorMessage,\n status: 'failed',\n })\n emit({ channel: SANDBOX_CHANNEL, kind: 'test-results', id, results: [...testResults] })\n }\n\n emit(done)\n resolve()\n },\n )\n })\n } finally {\n sandboxContext.off('execution.assertion', handleAssertion)\n sandboxContext.off('console', handleConsole)\n sandboxContext.dispose()\n }\n}\n\n/**\n * Boots the sandbox bridge: listens for {@link SandboxExecuteRequest} messages from the host window\n * and announces readiness. Called by the sandbox `.html` entry point.\n *\n * Returns a teardown function that removes the `message` listener it registered. Production code\n * does not need to call it (the iframe lives for the lifetime of the document), but tests and any\n * future hot-reload path can use it to avoid leaking listeners across reboots.\n */\nexport const startSandboxFrameServer = (): (() => void) => {\n /**\n * Origin we pin `postMessage` exchanges to.\n *\n * The iframe is loaded same-origin with its host (see `sandbox.html`), so both the parent we\n * post to and the parent we accept from are pinned to `window.location.origin`. This prevents\n * a hostile cross-origin opener from feeding fake execute requests into the sandbox, and stops\n * sandbox output (which can include user secrets returned via `pm.environment`) from leaking\n * to a different origin if the parent is ever swapped out.\n *\n * For `file://` documents (Electron) this resolves to `'file://'`, which is the same value\n * `postMessage` exposes on `event.origin`, so the pin still holds.\n */\n const expectedOrigin = window.location.origin\n\n const post = (message: SandboxOutboundMessage) => {\n // Always target the host's origin explicitly instead of `'*'`. The host also validates\n // `event.source` and `event.origin`, but defense-in-depth is cheap here.\n window.parent?.postMessage(message, expectedOrigin)\n }\n\n const handleMessage = (event: MessageEvent) => {\n // Reject anything that is not from our parent on our own origin. Without this guard, any\n // window that obtains a reference to this iframe could send fabricated execute payloads and\n // run arbitrary scripts inside the eval-permitted realm.\n if (event.origin !== expectedOrigin || event.source !== window.parent) {\n return\n }\n\n const data = event.data as Partial<SandboxExecuteRequest> | null\n if (!data || data.channel !== SANDBOX_CHANNEL || data.kind !== 'execute') {\n return\n }\n\n void runSandboxExecution(data as SandboxExecuteRequest, post).catch((error: unknown) => {\n post({\n channel: SANDBOX_CHANNEL,\n kind: 'done',\n id: (data as SandboxExecuteRequest).id,\n error: toErrorMessage(error),\n })\n })\n }\n\n window.addEventListener('message', handleMessage)\n post({ channel: SANDBOX_CHANNEL, kind: 'ready' })\n\n return () => window.removeEventListener('message', handleMessage)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sandbox-frame-server.js","names":[],"sources":["../../../../src/libs/execute-scripts/postman-adapter/sandbox-frame-server.ts"],"sourcesContent":["import { Request as PostmanRequest } from 'postman-collection'\nimport type { ExecutionResult, SandboxContext } from 'postman-sandbox'\nimport Sandbox from 'postman-sandbox'\n\nimport { buildSandboxContextFromScopes } from '../build-sandbox-context'\nimport type { ConsoleContext } from '../context/console'\nimport type { TestResult } from '../execute-post-response-script'\nimport { getSandboxOrigins } from './sandbox-origins'\nimport { SANDBOX_CHANNEL, type SandboxExecuteRequest, type SandboxOutboundMessage } from './sandbox-protocol'\n\n/**\n * Iframe-side sandbox bridge.\n *\n * This is the ONLY module that imports `postman-sandbox` (and therefore the only place that performs\n * `eval`/`new Function`). It is loaded exclusively by the sandbox `.html` host page, which carries its\n * own permissive CSP. The main application bundle never imports this file, so it stays eval-free and\n * its CSP can drop `'unsafe-eval'`.\n */\n\ntype AssertionEvent = {\n name: string\n index: number\n passed: boolean\n skipped: boolean\n error: { message?: string } | null\n}\n\nconst toErrorMessage = (error: unknown): string => {\n if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') {\n return error.message\n }\n\n return String(error)\n}\n\nconst upsertTestResult = (testResults: TestResult[], assertion: AssertionEvent, duration: number): void => {\n const title = assertion.name || `Assertion ${assertion.index + 1}`\n\n const nextResult: TestResult = {\n title,\n passed: assertion.passed,\n duration,\n error: assertion.error?.message,\n status: assertion.passed ? 'passed' : 'failed',\n }\n\n const existingResultIndex = testResults.findIndex((result) => result.title === title)\n if (existingResultIndex === -1) {\n testResults.push(nextResult)\n return\n }\n\n testResults[existingResultIndex] = nextResult\n}\n\nconst createContext = (): Promise<SandboxContext> =>\n new Promise((resolve, reject) => {\n Sandbox.createContext((error: unknown, context: SandboxContext) => {\n if (error) {\n reject(error)\n return\n }\n\n resolve(context)\n })\n })\n\n/**\n * Runs a single script in the postman-sandbox and reports progress/results via {@link emit}.\n *\n * Inputs arrive as plain, structured-cloneable data (already converted on the host) and are rebuilt\n * into postman-collection objects here. Variable mutations and the mutated request are sent back as\n * plain values; the host applies them to the live `VariablesStore`/`RequestFactory`.\n */\nexport const runSandboxExecution = async (\n request: SandboxExecuteRequest,\n emit: (message: SandboxOutboundMessage) => void,\n): Promise<void> => {\n const { id, listen, script, scopes, request: requestDefinition, response } = request\n\n const testResults: TestResult[] = []\n let lastAssertionTime = 0\n let scriptExecutionStartedAt = 0\n\n const sandboxContext = await createContext()\n\n const handleAssertion = (_cursor: unknown, assertions: AssertionEvent[]) => {\n assertions.forEach((assertion) => {\n const duration = Number((performance.now() - lastAssertionTime).toFixed(2))\n lastAssertionTime = performance.now()\n upsertTestResult(testResults, assertion, duration)\n })\n emit({ channel: SANDBOX_CHANNEL, kind: 'test-results', id, results: [...testResults] })\n }\n\n const handleConsole = (_cursor: unknown, level: string, ...args: unknown[]) => {\n emit({ channel: SANDBOX_CHANNEL, kind: 'console', id, level: level as keyof ConsoleContext, args })\n }\n\n try {\n sandboxContext.on('execution.assertion', handleAssertion)\n sandboxContext.on('console', handleConsole)\n\n const variablesContext = scopes ? buildSandboxContextFromScopes(scopes) : undefined\n\n scriptExecutionStartedAt = performance.now()\n lastAssertionTime = scriptExecutionStartedAt\n\n /**\n * Lodash `_.has(context, 'response')` is true even when the value is `undefined`,\n * which makes the sandbox run `new Response(undefined)` and breaks `pm.request`.\n * Only set keys that are actually present.\n */\n const context: Record<string, unknown> = {\n ...(variablesContext ?? {}),\n }\n\n if (requestDefinition !== undefined) {\n context.request = new PostmanRequest(requestDefinition as ConstructorParameters<typeof PostmanRequest>[0])\n }\n if (response !== undefined) {\n context.response = response\n }\n\n await new Promise<void>((resolve) => {\n sandboxContext.execute(\n {\n listen,\n script: {\n exec: [script],\n },\n },\n {\n disableLegacyAPIs: true,\n context,\n },\n (error: unknown, execution?: ExecutionResult) => {\n const done: SandboxOutboundMessage = {\n channel: SANDBOX_CHANNEL,\n kind: 'done',\n id,\n }\n\n if (execution) {\n done.variables = {\n local: execution._variables?.values,\n collection: execution.collectionVariables?.values,\n globals: execution.globals?.values,\n environment: execution.environment?.values,\n }\n }\n\n if (!error && listen === 'prerequest' && execution?.request !== undefined) {\n done.request = execution.request\n }\n\n if (error) {\n const duration = Number((performance.now() - scriptExecutionStartedAt).toFixed(2))\n const errorMessage = toErrorMessage(error)\n done.error = errorMessage\n\n emit({\n channel: SANDBOX_CHANNEL,\n kind: 'console',\n id,\n level: 'error',\n args: [\n `[${listen === 'prerequest' ? 'PRE-REQUEST' : 'POST-RESPONSE'} Script] Error (${duration}ms):`,\n errorMessage,\n ],\n })\n\n testResults.push({\n title: 'Script Execution',\n passed: false,\n duration,\n error: errorMessage,\n status: 'failed',\n })\n emit({ channel: SANDBOX_CHANNEL, kind: 'test-results', id, results: [...testResults] })\n }\n\n emit(done)\n resolve()\n },\n )\n })\n } finally {\n sandboxContext.off('execution.assertion', handleAssertion)\n sandboxContext.off('console', handleConsole)\n sandboxContext.dispose()\n }\n}\n\n/**\n * Boots the sandbox bridge: listens for {@link SandboxExecuteRequest} messages from the host window\n * and announces readiness. Called by the sandbox `.html` entry point.\n *\n * Returns a teardown function that removes the `message` listener it registered. Production code\n * does not need to call it (the iframe lives for the lifetime of the document), but tests and any\n * future hot-reload path can use it to avoid leaking listeners across reboots.\n */\nexport const startSandboxFrameServer = (): (() => void) => {\n /**\n * Origin we pin `postMessage` exchanges to.\n *\n * The iframe is loaded same-origin with its host (see `sandbox.html`), so both the parent we\n * post to and the parent we accept from are pinned to `window.location.origin`. This prevents\n * a hostile cross-origin opener from feeding fake execute requests into the sandbox, and stops\n * sandbox output (which can include user secrets returned via `pm.environment`) from leaking\n * to a different origin if the parent is ever swapped out.\n *\n * For `file://` documents (Electron), Chromium reports message origins as `'null'` and does not\n * allow `'file://'` as a `postMessage` target origin. In that case we target `'*'` and rely on the\n * exact parent/source-window checks on both sides for isolation.\n */\n const { receive: expectedOrigin, send: targetOrigin } = getSandboxOrigins()\n\n const post = (message: SandboxOutboundMessage) => {\n window.parent?.postMessage(message, targetOrigin)\n }\n\n const handleMessage = (event: MessageEvent) => {\n // Reject anything that is not from our parent on our own origin. Without this guard, any\n // window that obtains a reference to this iframe could send fabricated execute payloads and\n // run arbitrary scripts inside the eval-permitted realm.\n if (event.origin !== expectedOrigin || event.source !== window.parent) {\n return\n }\n\n const data = event.data as Partial<SandboxExecuteRequest> | null\n if (!data || data.channel !== SANDBOX_CHANNEL || data.kind !== 'execute') {\n return\n }\n\n void runSandboxExecution(data as SandboxExecuteRequest, post).catch((error: unknown) => {\n post({\n channel: SANDBOX_CHANNEL,\n kind: 'done',\n id: (data as SandboxExecuteRequest).id,\n error: toErrorMessage(error),\n })\n })\n }\n\n window.addEventListener('message', handleMessage)\n post({ channel: SANDBOX_CHANNEL, kind: 'ready' })\n\n return () => window.removeEventListener('message', handleMessage)\n}\n"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ type SandboxOriginConfig = {
2
+ receive: string;
3
+ send: string;
4
+ };
5
+ /**
6
+ * Origin pair used for sandbox `postMessage` exchanges.
7
+ *
8
+ * Chromium reports `file://` message origins as `'null'` and rejects `file://` as a target origin,
9
+ * so packaged Electron builds must receive from `'null'` and send with `'*'`. Other schemes stay
10
+ * pinned to their exact origin to avoid leaking script payloads or accepting spoofed messages.
11
+ */
12
+ export declare const getSandboxOrigins: () => SandboxOriginConfig;
13
+ export {};
14
+ //# sourceMappingURL=sandbox-origins.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox-origins.d.ts","sourceRoot":"","sources":["../../../../src/libs/execute-scripts/postman-adapter/sandbox-origins.ts"],"names":[],"mappings":"AAAA,KAAK,mBAAmB,GAAG;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,QAAO,mBAepC,CAAA"}
@@ -0,0 +1,23 @@
1
+ //#region src/libs/execute-scripts/postman-adapter/sandbox-origins.ts
2
+ /**
3
+ * Origin pair used for sandbox `postMessage` exchanges.
4
+ *
5
+ * Chromium reports `file://` message origins as `'null'` and rejects `file://` as a target origin,
6
+ * so packaged Electron builds must receive from `'null'` and send with `'*'`. Other schemes stay
7
+ * pinned to their exact origin to avoid leaking script payloads or accepting spoofed messages.
8
+ */
9
+ var getSandboxOrigins = () => {
10
+ const { origin, protocol } = window.location;
11
+ if (protocol === "file:" || origin === "file://") return {
12
+ receive: "null",
13
+ send: "*"
14
+ };
15
+ return {
16
+ receive: origin,
17
+ send: origin
18
+ };
19
+ };
20
+ //#endregion
21
+ export { getSandboxOrigins };
22
+
23
+ //# sourceMappingURL=sandbox-origins.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox-origins.js","names":[],"sources":["../../../../src/libs/execute-scripts/postman-adapter/sandbox-origins.ts"],"sourcesContent":["type SandboxOriginConfig = {\n receive: string\n send: string\n}\n\n/**\n * Origin pair used for sandbox `postMessage` exchanges.\n *\n * Chromium reports `file://` message origins as `'null'` and rejects `file://` as a target origin,\n * so packaged Electron builds must receive from `'null'` and send with `'*'`. Other schemes stay\n * pinned to their exact origin to avoid leaking script payloads or accepting spoofed messages.\n */\nexport const getSandboxOrigins = (): SandboxOriginConfig => {\n const { origin, protocol } = window.location\n const isFileProtocol = protocol === 'file:' || origin === 'file://'\n\n if (isFileProtocol) {\n return {\n receive: 'null',\n send: '*',\n }\n }\n\n return {\n receive: origin,\n send: origin,\n }\n}\n"],"mappings":";;;;;;;;AAYA,IAAa,0BAA+C;CAC1D,MAAM,EAAE,QAAQ,aAAa,OAAO;AAGpC,KAFuB,aAAa,WAAW,WAAW,UAGxD,QAAO;EACL,SAAS;EACT,MAAM;EACP;AAGH,QAAO;EACL,SAAS;EACT,MAAM;EACP"}
package/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "post-response scripts",
20
20
  "api client"
21
21
  ],
22
- "version": "0.4.17",
22
+ "version": "0.4.18",
23
23
  "engines": {
24
24
  "node": ">=22"
25
25
  },
@@ -59,12 +59,12 @@
59
59
  "postman-collection": "^5.3.0",
60
60
  "postman-sandbox": "^6.6.1",
61
61
  "vue": "^3.5.30",
62
- "@scalar/components": "0.26.0",
63
62
  "@scalar/helpers": "0.8.0",
64
- "@scalar/oas-utils": "0.17.2",
63
+ "@scalar/oas-utils": "0.17.3",
64
+ "@scalar/components": "0.26.0",
65
65
  "@scalar/use-codemirror": "0.14.11",
66
66
  "@scalar/use-hooks": "0.4.6",
67
- "@scalar/workspace-store": "0.51.2"
67
+ "@scalar/workspace-store": "0.52.0"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@vitejs/plugin-vue": "^6.0.3",