/** * Sandboxed Worker bootstrap for external adapter UI parsers. * * Security boundary: parser code runs inside a dedicated Web Worker with * network and DOM APIs explicitly disabled. Communication uses a narrow * postMessage protocol (see {@link SandboxRequest} / {@link SandboxResponse}). * * The worker is created from an inline Blob URL so no extra file needs to * be served. On initialisation the main thread sends the parser source; * the bootstrap evaluates it in a scope where dangerous globals are shadowed * by `undefined`, then responds to parse requests. */ // ── Message protocol ──────────────────────────────────────────────────────── /** Messages sent from the main thread to the worker. */ export type SandboxRequest = | { type: "init"; source: string } | { type: "parse"; id: number; line: string; ts: string }; /** Messages sent from the worker back to the main thread. */ export type SandboxResponse = | { type: "ready" } | { type: "error"; message: string } | { type: "result"; id: number; entries: unknown[] }; // ── Worker bootstrap source ───────────────────────────────────────────────── /** * Inline JS that runs inside the Worker. It: * 1. Shadows dangerous globals (`fetch`, `XMLHttpRequest`, `WebSocket`, * `importScripts`, `EventSource`, `navigator.sendBeacon`, etc.) with * no-ops or `undefined`. * 2. Waits for an `init` message carrying the adapter's parser source. * 3. Evaluates the source via `new Function()` and extracts exports. * 4. Responds to `parse` messages with `TranscriptEntry[]` results. */ const WORKER_BOOTSTRAP = ` "use strict"; // ── 1. Lock down dangerous globals ────────────────────────────────────────── // Workers have no DOM, but they still have network and import APIs. const _undefined = void 0; // Network self.fetch = _undefined; self.XMLHttpRequest = _undefined; self.WebSocket = _undefined; self.EventSource = _undefined; self.RTCPeerConnection = _undefined; self.RTCDataChannel = _undefined; self.Request = _undefined; self.Response = _undefined; self.Headers = _undefined; self.Cache = _undefined; self.CacheStorage = _undefined; self.caches = _undefined; // Import / eval escape hatches self.importScripts = _undefined; self.Worker = _undefined; self.SharedWorker = _undefined; self.Blob = _undefined; if (self.URL) { try { Object.defineProperty(self.URL, "createObjectURL", { value: _undefined, writable: false, configurable: false }); } catch {} try { Object.defineProperty(self.URL, "revokeObjectURL", { value: _undefined, writable: false, configurable: false }); } catch {} } // Beacon / reporting if (self.navigator) { try { Object.defineProperty(self.navigator, "sendBeacon", { value: _undefined, writable: false, configurable: false }); } catch {} } // Service worker / broadcast channel self.BroadcastChannel = _undefined; // IndexedDB (prevents persistent state exfiltration) self.indexedDB = _undefined; self.IDBFactory = _undefined; // ── 2. Parser state ───────────────────────────────────────────────────────── let parseStdoutLine = null; let createStdoutParser = null; let fallbackParser = null; // ── 3. Message handler ────────────────────────────────────────────────────── self.onmessage = function (e) { const msg = e.data; if (msg.type === "init") { try { // Evaluate the parser source in a constrained scope. // We use a Function constructor to avoid giving the source access to // our local variables. The only value we inject is a module-like // \`exports\` object so both CJS-style and ESM-compiled code works. // // ESM sources compiled to IIFE typically assign to an \`exports\` param // or use \`export\`. Since we can't use real ESM import() here (the // source is a string, not a URL), we wrap it. const exports = {}; const module = { exports }; // Build a function that receives common CJS shims. // \`self\` is shadowed to prevent the parser from un-deleting globals. const factory = new Function( "exports", "module", "self", "globalThis", // Wrap in a block to prevent hoisted declarations from leaking. "\\"use strict\\";\\n{\\n" + msg.source + "\\n}" ); factory(exports, module, _undefined, _undefined); // Resolve exports — try module.exports first (CJS), then named exports. const resolved = module.exports && typeof module.exports === "object" && Object.keys(module.exports).length > 0 ? module.exports : exports; if (typeof resolved.parseStdoutLine === "function") { parseStdoutLine = resolved.parseStdoutLine; } if (typeof resolved.createStdoutParser === "function") { createStdoutParser = resolved.createStdoutParser; } if (!parseStdoutLine && createStdoutParser) { fallbackParser = createStdoutParser(); if (fallbackParser && typeof fallbackParser.parseLine === "function") { parseStdoutLine = (line, ts) => fallbackParser.parseLine(line, ts); } } if (!parseStdoutLine) { self.postMessage({ type: "error", message: "Parser module exports no usable parseStdoutLine or createStdoutParser" }); return; } self.postMessage({ type: "ready" }); } catch (err) { self.postMessage({ type: "error", message: "Parser init failed: " + (err && err.message || String(err)) }); } return; } if (msg.type === "parse") { try { const entries = parseStdoutLine ? parseStdoutLine(msg.line, msg.ts) : []; self.postMessage({ type: "result", id: msg.id, entries: entries || [] }); } catch (err) { self.postMessage({ type: "result", id: msg.id, entries: [] }); } return; } }; `; // ── Public API ─────────────────────────────────────────────────────────────── /** * Return the inline Worker bootstrap source. * Exported for testing (so test code can verify the lockdown behaviour). */ export function getWorkerBootstrapSource(): string { return WORKER_BOOTSTRAP; } /** * Create a sandboxed Web Worker from the inline bootstrap. * The caller must send an `init` message with the parser source before * sending parse requests. */ export function createSandboxedWorker(): Worker { const blob = new Blob([WORKER_BOOTSTRAP], { type: "application/javascript" }); const url = URL.createObjectURL(blob); try { return new Worker(url); } finally { // Revoke after construction; the Worker has already captured the Blob URL source. URL.revokeObjectURL(url); } }