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

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,13 @@
1
1
  # @scalar/scripts
2
2
 
3
+ ## 0.4.20
4
+
5
+ ## 0.4.19
6
+
7
+ ### Patch Changes
8
+
9
+ - [#9338](https://github.com/scalar/scalar/pull/9338): perf: warm up the request scripts sandbox on mount when scripts are present, so the first request no longer pays the sandbox cold-start cost
10
+
3
11
  ## 0.4.18
4
12
 
5
13
  ### Patch Changes
@@ -1,3 +1,4 @@
1
1
  export { type TestResult, executePostResponseScript } from './execute-post-response-script.js';
2
2
  export { executePreRequestScript } from './execute-pre-request-script.js';
3
+ export { prewarmSandboxFrame } from './postman-adapter/sandbox-adapter.js';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/libs/execute-scripts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAA;AAC3F,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/libs/execute-scripts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAA;AAC3F,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AACtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAA"}
@@ -35,6 +35,18 @@ export declare const sandboxFrameUrl: () => string;
35
35
  type SandboxTransport = (request: SandboxExecuteRequest, onMessage: (message: SandboxOutboundMessage) => void) => void;
36
36
  /** Override how the host reaches the sandbox (e.g. an in-process transport for tests). */
37
37
  export declare const setSandboxTransport: (next: SandboxTransport) => void;
38
+ /**
39
+ * Eagerly creates the sandbox iframe so the first script execution does not pay the cold-start
40
+ * cost of loading `sandbox.html` and the `postman-sandbox` bundle (noticeably slow on Electron).
41
+ *
42
+ * This is a best-effort optimization: it only runs in a DOM context, reuses the same module-cached
43
+ * frame {@link executeInPostmanSandbox} later awaits, and swallows failures so a warm-up problem can
44
+ * never surface to the caller — the real execution path retries and reports errors on its own.
45
+ *
46
+ * Call this only when scripts are actually present; warming the sandbox for a request without
47
+ * scripts would defeat the lazy creation that keeps script-free requests cheap.
48
+ */
49
+ export declare const prewarmSandboxFrame: () => void;
38
50
  export declare const executeInPostmanSandbox: ({ script, type, context: { requestBuilder, response, variablesStore, scriptConsole }, onTestResultsUpdate, }: {
39
51
  script: string;
40
52
  type: "pre-request" | "post-response";
@@ -1 +1 @@
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
+ {"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;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,QAAO,IAQtC,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"}
@@ -191,6 +191,21 @@ var iframeTransport = (request, onMessage) => {
191
191
  });
192
192
  };
193
193
  var transport = iframeTransport;
194
+ /**
195
+ * Eagerly creates the sandbox iframe so the first script execution does not pay the cold-start
196
+ * cost of loading `sandbox.html` and the `postman-sandbox` bundle (noticeably slow on Electron).
197
+ *
198
+ * This is a best-effort optimization: it only runs in a DOM context, reuses the same module-cached
199
+ * frame {@link executeInPostmanSandbox} later awaits, and swallows failures so a warm-up problem can
200
+ * never surface to the caller — the real execution path retries and reports errors on its own.
201
+ *
202
+ * Call this only when scripts are actually present; warming the sandbox for a request without
203
+ * scripts would defeat the lazy creation that keeps script-free requests cheap.
204
+ */
205
+ var prewarmSandboxFrame = () => {
206
+ if (typeof document === "undefined") return;
207
+ ensureSandboxFrame().catch(() => {});
208
+ };
194
209
  var nextExecutionId = 0;
195
210
  var executeInPostmanSandbox = async ({ script, type, context: { requestBuilder, response, variablesStore, scriptConsole }, onTestResultsUpdate }) => {
196
211
  const requestDefinition = requestBuilder ? createPostmanRequestFromFactory(requestBuilder).toJSON() : void 0;
@@ -233,6 +248,6 @@ var executeInPostmanSandbox = async ({ script, type, context: { requestBuilder,
233
248
  return variablesStore;
234
249
  };
235
250
  //#endregion
236
- export { executeInPostmanSandbox };
251
+ export { executeInPostmanSandbox, prewarmSandboxFrame };
237
252
 
238
253
  //# sourceMappingURL=sandbox-adapter.js.map
@@ -1 +1 @@
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
+ {"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\n/**\n * Eagerly creates the sandbox iframe so the first script execution does not pay the cold-start\n * cost of loading `sandbox.html` and the `postman-sandbox` bundle (noticeably slow on Electron).\n *\n * This is a best-effort optimization: it only runs in a DOM context, reuses the same module-cached\n * frame {@link executeInPostmanSandbox} later awaits, and swallows failures so a warm-up problem can\n * never surface to the caller — the real execution path retries and reports errors on its own.\n *\n * Call this only when scripts are actually present; warming the sandbox for a request without\n * scripts would defeat the lazy creation that keeps script-free requests cheap.\n */\nexport const prewarmSandboxFrame = (): void => {\n if (typeof document === 'undefined') {\n return\n }\n\n ensureSandboxFrame().catch(() => {\n // Ignored on purpose: warm-up is opportunistic and the execution path handles real errors.\n })\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":"request-scripts-plugin.d.ts","sourceRoot":"","sources":["../../../src/plugins/request-scripts/request-scripts-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAS7D;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,QAAO,YAwCvC,CAAA"}
1
+ {"version":3,"file":"request-scripts-plugin.d.ts","sourceRoot":"","sources":["../../../src/plugins/request-scripts/request-scripts-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAc7D;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,QAAO,YAuDvC,CAAA"}
@@ -1,4 +1,5 @@
1
1
  import { getScript } from "../../helpers/get-script.js";
2
+ import { prewarmSandboxFrame } from "../../libs/execute-scripts/postman-adapter/sandbox-adapter.js";
2
3
  import { executePostResponseScript } from "../../libs/execute-scripts/execute-post-response-script.js";
3
4
  import { executePreRequestScript } from "../../libs/execute-scripts/execute-pre-request-script.js";
4
5
  import TestResults_default from "./components/TestResults/TestResults.vue.js";
@@ -26,6 +27,9 @@ var requestScriptsPlugin = () => {
26
27
  }
27
28
  },
28
29
  hooks: {
30
+ onRequestMount: ({ document, operation }) => {
31
+ if (getScript(document["x-pre-request"], operation["x-pre-request"], document["x-post-response"], operation["x-post-response"])) prewarmSandboxFrame();
32
+ },
29
33
  beforeRequest: async ({ requestBuilder, document, operation, variablesStore }) => {
30
34
  results.value = [];
31
35
  await executePreRequestScript(getScript(document["x-pre-request"], operation["x-pre-request"]), {
@@ -1 +1 @@
1
- {"version":3,"file":"request-scripts-plugin.js","names":[],"sources":["../../../src/plugins/request-scripts/request-scripts-plugin.ts"],"sourcesContent":["import type { ClientPlugin } from '@scalar/oas-utils/helpers'\nimport { ref } from 'vue'\n\nimport { getScript } from '@/helpers/get-script'\nimport { type TestResult, executePostResponseScript, executePreRequestScript } from '@/libs/execute-scripts'\nimport { TestResults } from '@/plugins/request-scripts/components/TestResults'\n\nimport ScriptsSection from './components/ScriptsSection.vue'\n\n/**\n * Unified request scripts plugin for client V2. Provides a single \"Scripts\"\n * section with:\n * - Pre-request script editor (x-pre-request) — runs before each request\n * - Post-response script editor (x-post-response) — runs after each response\n * - Test results from post-response assertions, shown in the response section\n *\n * Hooks: beforeRequest runs pre-request scripts and resets results;\n * responseReceived runs post-response scripts and updates test results.\n */\nexport const requestScriptsPlugin = (): ClientPlugin => {\n const results = ref<TestResult[]>([])\n\n return {\n components: {\n request: {\n component: ScriptsSection,\n },\n response: {\n component: TestResults,\n additionalProps: { results },\n },\n },\n\n hooks: {\n beforeRequest: async ({ requestBuilder, document, operation, variablesStore }) => {\n // Reset test results when a new request is sent\n results.value = []\n const script = getScript(document['x-pre-request'], operation['x-pre-request'])\n await executePreRequestScript(script, {\n requestBuilder,\n variablesStore,\n onTestResultsUpdate: (newResults) => (results.value = [...newResults]),\n })\n },\n\n responseReceived: async ({ requestBuilder, response, operation, document, variablesStore }) => {\n const script = getScript(document['x-post-response'], operation['x-post-response'])\n // Snapshot pre-request results before running post-response script\n const preRequestResults = [...results.value]\n await executePostResponseScript(script, {\n requestBuilder,\n response,\n onTestResultsUpdate: (postResponseResults) =>\n (results.value = [...preRequestResults, ...postResponseResults]),\n variablesStore,\n })\n },\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAmBA,IAAa,6BAA2C;CACtD,MAAM,UAAU,IAAkB,EAAE,CAAC;AAErC,QAAO;EACL,YAAY;GACV,SAAS,EACP,WAAW,wBACZ;GACD,UAAU;IACR,WAAW;IACX,iBAAiB,EAAE,SAAS;IAC7B;GACF;EAED,OAAO;GACL,eAAe,OAAO,EAAE,gBAAgB,UAAU,WAAW,qBAAqB;AAEhF,YAAQ,QAAQ,EAAE;AAElB,UAAM,wBADS,UAAU,SAAS,kBAAkB,UAAU,iBAAiB,EACzC;KACpC;KACA;KACA,sBAAsB,eAAgB,QAAQ,QAAQ,CAAC,GAAG,WAAW;KACtE,CAAC;;GAGJ,kBAAkB,OAAO,EAAE,gBAAgB,UAAU,WAAW,UAAU,qBAAqB;IAC7F,MAAM,SAAS,UAAU,SAAS,oBAAoB,UAAU,mBAAmB;IAEnF,MAAM,oBAAoB,CAAC,GAAG,QAAQ,MAAM;AAC5C,UAAM,0BAA0B,QAAQ;KACtC;KACA;KACA,sBAAsB,wBACnB,QAAQ,QAAQ,CAAC,GAAG,mBAAmB,GAAG,oBAAoB;KACjE;KACD,CAAC;;GAEL;EACF"}
1
+ {"version":3,"file":"request-scripts-plugin.js","names":[],"sources":["../../../src/plugins/request-scripts/request-scripts-plugin.ts"],"sourcesContent":["import type { ClientPlugin } from '@scalar/oas-utils/helpers'\nimport { ref } from 'vue'\n\nimport { getScript } from '@/helpers/get-script'\nimport {\n type TestResult,\n executePostResponseScript,\n executePreRequestScript,\n prewarmSandboxFrame,\n} from '@/libs/execute-scripts'\nimport { TestResults } from '@/plugins/request-scripts/components/TestResults'\n\nimport ScriptsSection from './components/ScriptsSection.vue'\n\n/**\n * Unified request scripts plugin for client V2. Provides a single \"Scripts\"\n * section with:\n * - Pre-request script editor (x-pre-request) — runs before each request\n * - Post-response script editor (x-post-response) — runs after each response\n * - Test results from post-response assertions, shown in the response section\n *\n * Hooks: beforeRequest runs pre-request scripts and resets results;\n * responseReceived runs post-response scripts and updates test results.\n */\nexport const requestScriptsPlugin = (): ClientPlugin => {\n const results = ref<TestResult[]>([])\n\n return {\n components: {\n request: {\n component: ScriptsSection,\n },\n response: {\n component: TestResults,\n additionalProps: { results },\n },\n },\n\n hooks: {\n onRequestMount: ({ document, operation }) => {\n // Only warm up the sandbox when this operation actually has scripts. Requests without\n // scripts never touch the sandbox, so creating the iframe for them would waste work.\n const script = getScript(\n document['x-pre-request'],\n operation['x-pre-request'],\n document['x-post-response'],\n operation['x-post-response'],\n )\n\n if (script) {\n prewarmSandboxFrame()\n }\n },\n\n beforeRequest: async ({ requestBuilder, document, operation, variablesStore }) => {\n // Reset test results when a new request is sent\n results.value = []\n const script = getScript(document['x-pre-request'], operation['x-pre-request'])\n await executePreRequestScript(script, {\n requestBuilder,\n variablesStore,\n onTestResultsUpdate: (newResults) => (results.value = [...newResults]),\n })\n },\n\n responseReceived: async ({ requestBuilder, response, operation, document, variablesStore }) => {\n const script = getScript(document['x-post-response'], operation['x-post-response'])\n // Snapshot pre-request results before running post-response script\n const preRequestResults = [...results.value]\n await executePostResponseScript(script, {\n requestBuilder,\n response,\n onTestResultsUpdate: (postResponseResults) =>\n (results.value = [...preRequestResults, ...postResponseResults]),\n variablesStore,\n })\n },\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAwBA,IAAa,6BAA2C;CACtD,MAAM,UAAU,IAAkB,EAAE,CAAC;AAErC,QAAO;EACL,YAAY;GACV,SAAS,EACP,WAAW,wBACZ;GACD,UAAU;IACR,WAAW;IACX,iBAAiB,EAAE,SAAS;IAC7B;GACF;EAED,OAAO;GACL,iBAAiB,EAAE,UAAU,gBAAgB;AAU3C,QAPe,UACb,SAAS,kBACT,UAAU,kBACV,SAAS,oBACT,UAAU,mBACX,CAGC,sBAAqB;;GAIzB,eAAe,OAAO,EAAE,gBAAgB,UAAU,WAAW,qBAAqB;AAEhF,YAAQ,QAAQ,EAAE;AAElB,UAAM,wBADS,UAAU,SAAS,kBAAkB,UAAU,iBAAiB,EACzC;KACpC;KACA;KACA,sBAAsB,eAAgB,QAAQ,QAAQ,CAAC,GAAG,WAAW;KACtE,CAAC;;GAGJ,kBAAkB,OAAO,EAAE,gBAAgB,UAAU,WAAW,UAAU,qBAAqB;IAC7F,MAAM,SAAS,UAAU,SAAS,oBAAoB,UAAU,mBAAmB;IAEnF,MAAM,oBAAoB,CAAC,GAAG,QAAQ,MAAM;AAC5C,UAAM,0BAA0B,QAAQ;KACtC;KACA;KACA,sBAAsB,wBACnB,QAAQ,QAAQ,CAAC,GAAG,mBAAmB,GAAG,oBAAoB;KACjE;KACD,CAAC;;GAEL;EACF"}
package/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "post-response scripts",
20
20
  "api client"
21
21
  ],
22
- "version": "0.4.18",
22
+ "version": "0.4.20",
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/helpers": "0.8.0",
63
- "@scalar/oas-utils": "0.17.3",
64
- "@scalar/components": "0.26.0",
62
+ "@scalar/oas-utils": "0.18.1",
63
+ "@scalar/components": "0.27.0",
64
+ "@scalar/helpers": "0.8.1",
65
65
  "@scalar/use-codemirror": "0.14.11",
66
66
  "@scalar/use-hooks": "0.4.6",
67
- "@scalar/workspace-store": "0.52.0"
67
+ "@scalar/workspace-store": "0.54.0"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@vitejs/plugin-vue": "^6.0.3",