From 5e076b2dc1fba09dcee187d00599c65a656ce8ec Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 20 Mar 2026 01:14:07 -0700 Subject: [PATCH 1/3] fix: add @secure-exec/v8 workspace dep to @secure-exec/node --- packages/secure-exec-browser/package.json | 2 +- packages/secure-exec-browser/src/worker.ts | 14 +- .../src/common/runtime-globals.d.ts | 11 - .../src/inject/bridge-initial-globals.ts | 190 +- .../src/inject/require-setup.ts | 36 +- .../src/inject/setup-dynamic-import.ts | 6 +- .../src/inject/setup-fs-facade.ts | 46 +- packages/secure-exec-core/package.json | 2 +- .../src/bridge/child-process.ts | 24 +- packages/secure-exec-core/src/bridge/fs.ts | 73 +- .../secure-exec-core/src/bridge/module.ts | 15 +- .../secure-exec-core/src/bridge/network.ts | 51 +- .../secure-exec-core/src/bridge/process.ts | 46 +- .../src/generated/isolate-runtime.ts | 8 +- packages/secure-exec-core/src/index.ts | 3 + .../src/shared/bridge-contract.ts | 175 +- .../src/shared/console-formatter.ts | 8 +- packages/secure-exec-node/package.json | 3 +- packages/secure-exec-node/src/bridge-setup.ts | 1592 ++++++++++++++++- packages/secure-exec-node/src/driver.ts | 4 - packages/secure-exec-node/src/esm-compiler.ts | 82 +- .../secure-exec-node/src/execution-driver.ts | 465 ++--- .../src/execution-lifecycle.ts | 39 +- packages/secure-exec-node/src/execution.ts | 64 +- packages/secure-exec-node/src/index.ts | 2 +- .../secure-exec-node/src/isolate-bootstrap.ts | 21 +- packages/secure-exec-node/src/isolate.ts | 18 +- pnpm-lock.yaml | 68 +- 28 files changed, 2153 insertions(+), 915 deletions(-) diff --git a/packages/secure-exec-browser/package.json b/packages/secure-exec-browser/package.json index 6cdfcf13..b22ca13b 100644 --- a/packages/secure-exec-browser/package.json +++ b/packages/secure-exec-browser/package.json @@ -1,6 +1,6 @@ { "name": "@secure-exec/browser", - "version": "0.1.1-rc.2", + "version": "0.1.0", "type": "module", "license": "Apache-2.0", "main": "./dist/index.js", diff --git a/packages/secure-exec-browser/src/worker.ts b/packages/secure-exec-browser/src/worker.ts index f971a119..959eb27f 100644 --- a/packages/secure-exec-browser/src/worker.ts +++ b/packages/secure-exec-browser/src/worker.ts @@ -68,6 +68,9 @@ function getUtf8ByteLength(text: string): number { return encoder.encode(text).byteLength; } +function getBase64EncodedByteLength(rawByteLength: number): number { + return Math.ceil(rawByteLength / 3) * 4; +} function assertPayloadByteLength( payloadLabel: string, @@ -317,14 +320,15 @@ async function initRuntime(payload: BrowserWorkerInitPayload): Promise { const data = await fsOps.readFile(path); assertPayloadByteLength( `fs.readFileBinary ${path}`, - data.byteLength, + getBase64EncodedByteLength(data.byteLength), base64TransferLimitBytes, ); - return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); + return btoa(String.fromCharCode(...data)); }); - const writeFileBinaryRef = makeApplySyncPromise(async (path: string, binaryContent: Uint8Array) => { - assertPayloadByteLength(`fs.writeFileBinary ${path}`, binaryContent.byteLength, base64TransferLimitBytes); - return fsOps.writeFile(path, binaryContent); + const writeFileBinaryRef = makeApplySyncPromise(async (path: string, base64: string) => { + assertTextPayloadSize(`fs.writeFileBinary ${path}`, base64, base64TransferLimitBytes); + const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); + return fsOps.writeFile(path, bytes); }); const readDirRef = makeApplySyncPromise(async (path: string) => { const entries = await fsOps.readDirWithTypes(path); diff --git a/packages/secure-exec-core/isolate-runtime/src/common/runtime-globals.d.ts b/packages/secure-exec-core/isolate-runtime/src/common/runtime-globals.d.ts index 0f0e6d06..9e290423 100644 --- a/packages/secure-exec-core/isolate-runtime/src/common/runtime-globals.d.ts +++ b/packages/secure-exec-core/isolate-runtime/src/common/runtime-globals.d.ts @@ -116,17 +116,6 @@ declare global { var __runtimeCommonJsFileConfig: RuntimeCommonJsFileConfig | undefined; var __runtimeTimingMitigationConfig: RuntimeTimingMitigationConfig | undefined; var __runtimeCustomGlobalPolicy: RuntimeCustomGlobalPolicy | undefined; - var __runtimeJsonPayloadLimitBytes: number | undefined; - var __runtimePayloadLimitErrorCode: string | undefined; - var __runtimeApplyConfig: - | ((config: { - timingMitigation?: string; - frozenTimeMs?: number; - payloadLimitBytes?: number; - payloadLimitErrorCode?: string; - }) => void) - | undefined; - var __runtimeResetProcessState: (() => void) | undefined; var __runtimeProcessCwdOverride: unknown; var __runtimeProcessEnvOverride: unknown; var __runtimeStdinData: unknown; diff --git a/packages/secure-exec-core/isolate-runtime/src/inject/bridge-initial-globals.ts b/packages/secure-exec-core/isolate-runtime/src/inject/bridge-initial-globals.ts index 27a25ae6..ecbf93ca 100644 --- a/packages/secure-exec-core/isolate-runtime/src/inject/bridge-initial-globals.ts +++ b/packages/secure-exec-core/isolate-runtime/src/inject/bridge-initial-globals.ts @@ -1,5 +1,4 @@ import { getRuntimeExposeMutableGlobal } from "../common/global-exposure"; -import { setGlobalValue } from "../common/global-access"; const __runtimeExposeMutableGlobal = getRuntimeExposeMutableGlobal(); @@ -9,15 +8,12 @@ const __initialCwd = typeof __bridgeSetupConfig.initialCwd === "string" ? __bridgeSetupConfig.initialCwd : "/"; - -// Set payload limit defaults on globalThis — read at call time by v8.deserialize, -// overridable via __runtimeApplyConfig for context snapshot restore -globalThis.__runtimeJsonPayloadLimitBytes = +const __jsonPayloadLimitBytes = typeof __bridgeSetupConfig.jsonPayloadLimitBytes === "number" && Number.isFinite(__bridgeSetupConfig.jsonPayloadLimitBytes) ? Math.max(0, Math.floor(__bridgeSetupConfig.jsonPayloadLimitBytes)) : 4 * 1024 * 1024; -globalThis.__runtimePayloadLimitErrorCode = +const __payloadLimitErrorCode = typeof __bridgeSetupConfig.payloadLimitErrorCode === "string" && __bridgeSetupConfig.payloadLimitErrorCode.length > 0 ? __bridgeSetupConfig.payloadLimitErrorCode @@ -241,15 +237,12 @@ if (__moduleCache) { ); }, deserialize: function (buffer: Buffer) { - // Read limits from globals at call time (not captured at setup) for snapshot compatibility - const limit = globalThis.__runtimeJsonPayloadLimitBytes ?? 4 * 1024 * 1024; - const errorCode = globalThis.__runtimePayloadLimitErrorCode ?? "ERR_SANDBOX_PAYLOAD_TOO_LARGE"; // Check raw buffer size BEFORE allocating the decoded string - if (buffer.length > limit) { + if (buffer.length > __jsonPayloadLimitBytes) { throw new Error( - errorCode + + __payloadLimitErrorCode + ": v8.deserialize exceeds " + - String(limit) + + String(__jsonPayloadLimitBytes) + " bytes", ); } @@ -276,176 +269,3 @@ if (__moduleCache) { __runtimeExposeMutableGlobal("_pendingModules", {}); __runtimeExposeMutableGlobal("_currentModule", { dirname: __initialCwd }); - -// Post-restore config application — called after bridge IIFE to apply -// per-session config (timing mitigation, payload limits). Enables context -// snapshot reuse: the IIFE runs once at snapshot creation, this function -// applies session-specific config after restore. -globalThis.__runtimeApplyConfig = function (config: { - timingMitigation?: string; - frozenTimeMs?: number; - payloadLimitBytes?: number; - payloadLimitErrorCode?: string; -}) { - // Apply payload limits - if ( - typeof config.payloadLimitBytes === "number" && - Number.isFinite(config.payloadLimitBytes) - ) { - globalThis.__runtimeJsonPayloadLimitBytes = Math.max( - 0, - Math.floor(config.payloadLimitBytes), - ); - } - if ( - typeof config.payloadLimitErrorCode === "string" && - config.payloadLimitErrorCode.length > 0 - ) { - globalThis.__runtimePayloadLimitErrorCode = - config.payloadLimitErrorCode; - } - - // Apply timing mitigation freeze - if (config.timingMitigation === "freeze") { - const frozenTimeMs = - typeof config.frozenTimeMs === "number" && - Number.isFinite(config.frozenTimeMs) - ? config.frozenTimeMs - : Date.now(); - const frozenDateNow = () => frozenTimeMs; - - // Freeze Date.now - try { - Object.defineProperty(Date, "now", { - value: frozenDateNow, - configurable: false, - writable: false, - }); - } catch { - Date.now = frozenDateNow; - } - - // Patch Date constructor so new Date().getTime() returns degraded time - const OrigDate = Date; - const FrozenDate = function Date( - this: InstanceType, - ...args: unknown[] - ) { - if (new.target) { - if (args.length === 0) { - return new OrigDate(frozenTimeMs); - } - // @ts-expect-error — spread forwarding to variadic Date constructor - return new OrigDate(...args); - } - return OrigDate(); - } as unknown as DateConstructor; - Object.defineProperty(FrozenDate, "prototype", { - value: OrigDate.prototype, - writable: false, - configurable: false, - }); - FrozenDate.now = frozenDateNow; - FrozenDate.parse = OrigDate.parse; - FrozenDate.UTC = OrigDate.UTC; - Object.defineProperty(FrozenDate, "now", { - value: frozenDateNow, - configurable: false, - writable: false, - }); - try { - Object.defineProperty(globalThis, "Date", { - value: FrozenDate, - configurable: false, - writable: false, - }); - } catch { - (globalThis as Record).Date = FrozenDate; - } - - // Freeze performance.now - const frozenPerformanceNow = () => 0; - const origPerf = globalThis.performance; - const frozenPerf = Object.create(null) as Record; - if (typeof origPerf !== "undefined" && origPerf !== null) { - const src = origPerf as unknown as Record; - for (const key of Object.getOwnPropertyNames( - Object.getPrototypeOf(origPerf) ?? origPerf, - )) { - if (key !== "now") { - try { - const val = src[key]; - if (typeof val === "function") { - frozenPerf[key] = val.bind(origPerf); - } else { - frozenPerf[key] = val; - } - } catch { - /* skip inaccessible properties */ - } - } - } - } - Object.defineProperty(frozenPerf, "now", { - value: frozenPerformanceNow, - configurable: false, - writable: false, - }); - Object.freeze(frozenPerf); - try { - Object.defineProperty(globalThis, "performance", { - value: frozenPerf, - configurable: false, - writable: false, - }); - } catch { - (globalThis as Record).performance = frozenPerf; - } - - // Harden SharedArrayBuffer removal - const OrigSAB = globalThis.SharedArrayBuffer; - if (typeof OrigSAB === "function") { - try { - const proto = OrigSAB.prototype; - if (proto) { - for (const key of [ - "byteLength", - "slice", - "grow", - "maxByteLength", - "growable", - ]) { - try { - Object.defineProperty(proto, key, { - get() { - throw new TypeError( - "SharedArrayBuffer is not available in sandbox", - ); - }, - configurable: false, - }); - } catch { - /* property may not exist or be non-configurable */ - } - } - } - } catch { - /* best-effort prototype neutering */ - } - } - try { - Object.defineProperty(globalThis, "SharedArrayBuffer", { - value: undefined, - configurable: false, - writable: false, - enumerable: false, - }); - } catch { - Reflect.deleteProperty(globalThis, "SharedArrayBuffer"); - setGlobalValue("SharedArrayBuffer", undefined); - } - } - - // Clean up — one-shot function - delete globalThis.__runtimeApplyConfig; -}; diff --git a/packages/secure-exec-core/isolate-runtime/src/inject/require-setup.ts b/packages/secure-exec-core/isolate-runtime/src/inject/require-setup.ts index fe4c0e23..fce150ee 100644 --- a/packages/secure-exec-core/isolate-runtime/src/inject/require-setup.ts +++ b/packages/secure-exec-core/isolate-runtime/src/inject/require-setup.ts @@ -1289,7 +1289,26 @@ declare const _loadFileSync: { applySync(recv: undefined, args: [string]): string | null } | undefined; function _resolveFrom(moduleName, fromDir) { - const resolved = _resolveModule(moduleName, fromDir); + const cacheKey = fromDir + '\0' + moduleName; + if (cacheKey in _resolveCache) { + const cached = _resolveCache[cacheKey]; + if (cached === null) { + const err = new Error("Cannot find module '" + moduleName + "'"); + err.code = 'MODULE_NOT_FOUND'; + throw err; + } + return cached; + } + // Use synchronous resolution when available (always works, even inside + // applySync contexts like net socket data callbacks). Fall back to + // applySyncPromise for environments without the sync bridge. + let resolved; + if (typeof _resolveModuleSync !== 'undefined') { + resolved = _resolveModuleSync.applySync(undefined, [moduleName, fromDir]); + } else { + resolved = _resolveModule.applySyncPromise(undefined, [moduleName, fromDir]); + } + _resolveCache[cacheKey] = resolved; if (resolved === null) { const err = new Error("Cannot find module '" + moduleName + "'"); err.code = 'MODULE_NOT_FOUND'; @@ -1653,7 +1672,10 @@ } // Try to load polyfill first (for built-in modules like path, events, etc.) - const polyfillCode = _loadPolyfill(name); + // Skip for relative/absolute paths — they're never polyfills and the + // applySyncPromise call can't run inside applySync contexts. + const isPath = name[0] === '.' || name[0] === '/'; + const polyfillCode = isPath ? null : _loadPolyfill.applySyncPromise(undefined, [name]); if (polyfillCode !== null) { if (__internalModuleCache[name]) return __internalModuleCache[name]; @@ -1704,8 +1726,14 @@ return _pendingModules[cacheKey].exports; } - // Load file content - const source = _loadFile(resolved); + // Load file content. Use synchronous loading when available (works + // inside applySync contexts). Fall back to applySyncPromise otherwise. + let source; + if (typeof _loadFileSync !== 'undefined') { + source = _loadFileSync.applySync(undefined, [resolved]); + } else { + source = _loadFile.applySyncPromise(undefined, [resolved]); + } if (source === null) { const err = new Error("Cannot find module '" + resolved + "'"); err.code = 'MODULE_NOT_FOUND'; diff --git a/packages/secure-exec-core/isolate-runtime/src/inject/setup-dynamic-import.ts b/packages/secure-exec-core/isolate-runtime/src/inject/setup-dynamic-import.ts index ade4cc3a..bafd547b 100644 --- a/packages/secure-exec-core/isolate-runtime/src/inject/setup-dynamic-import.ts +++ b/packages/secure-exec-core/isolate-runtime/src/inject/setup-dynamic-import.ts @@ -23,7 +23,11 @@ const __dynamicImportHandler = async function ( const allowRequireFallback = request.endsWith(".cjs") || request.endsWith(".json"); - const namespace = await globalThis._dynamicImport(request, referrer); + const namespace = await globalThis._dynamicImport.apply( + undefined, + [request, referrer], + { result: { promise: true } }, + ); if (namespace !== null) { return namespace; diff --git a/packages/secure-exec-core/isolate-runtime/src/inject/setup-fs-facade.ts b/packages/secure-exec-core/isolate-runtime/src/inject/setup-fs-facade.ts index d18ed3b5..8950d21d 100644 --- a/packages/secure-exec-core/isolate-runtime/src/inject/setup-fs-facade.ts +++ b/packages/secure-exec-core/isolate-runtime/src/inject/setup-fs-facade.ts @@ -2,30 +2,26 @@ import { getRuntimeExposeCustomGlobal } from "../common/global-exposure"; const __runtimeExposeCustomGlobal = getRuntimeExposeCustomGlobal(); -// Getter-based delegation: each _fs property resolves globalThis._fsXxx at -// call time, not setup time. This allows snapshot-restored contexts to pick -// up replaced bridge function globals after restore. -const __fsFacade: Record = {}; -Object.defineProperties(__fsFacade, { - readFile: { get() { return globalThis._fsReadFile; }, enumerable: true }, - writeFile: { get() { return globalThis._fsWriteFile; }, enumerable: true }, - readFileBinary: { get() { return globalThis._fsReadFileBinary; }, enumerable: true }, - writeFileBinary: { get() { return globalThis._fsWriteFileBinary; }, enumerable: true }, - readDir: { get() { return globalThis._fsReadDir; }, enumerable: true }, - mkdir: { get() { return globalThis._fsMkdir; }, enumerable: true }, - rmdir: { get() { return globalThis._fsRmdir; }, enumerable: true }, - exists: { get() { return globalThis._fsExists; }, enumerable: true }, - stat: { get() { return globalThis._fsStat; }, enumerable: true }, - unlink: { get() { return globalThis._fsUnlink; }, enumerable: true }, - rename: { get() { return globalThis._fsRename; }, enumerable: true }, - chmod: { get() { return globalThis._fsChmod; }, enumerable: true }, - chown: { get() { return globalThis._fsChown; }, enumerable: true }, - link: { get() { return globalThis._fsLink; }, enumerable: true }, - symlink: { get() { return globalThis._fsSymlink; }, enumerable: true }, - readlink: { get() { return globalThis._fsReadlink; }, enumerable: true }, - lstat: { get() { return globalThis._fsLstat; }, enumerable: true }, - truncate: { get() { return globalThis._fsTruncate; }, enumerable: true }, - utimes: { get() { return globalThis._fsUtimes; }, enumerable: true }, -}); +const __fsFacade: Record = { + readFile: globalThis._fsReadFile, + writeFile: globalThis._fsWriteFile, + readFileBinary: globalThis._fsReadFileBinary, + writeFileBinary: globalThis._fsWriteFileBinary, + readDir: globalThis._fsReadDir, + mkdir: globalThis._fsMkdir, + rmdir: globalThis._fsRmdir, + exists: globalThis._fsExists, + stat: globalThis._fsStat, + unlink: globalThis._fsUnlink, + rename: globalThis._fsRename, + chmod: globalThis._fsChmod, + chown: globalThis._fsChown, + link: globalThis._fsLink, + symlink: globalThis._fsSymlink, + readlink: globalThis._fsReadlink, + lstat: globalThis._fsLstat, + truncate: globalThis._fsTruncate, + utimes: globalThis._fsUtimes, +}; __runtimeExposeCustomGlobal("_fs", __fsFacade); diff --git a/packages/secure-exec-core/package.json b/packages/secure-exec-core/package.json index e66d0207..43fd2697 100644 --- a/packages/secure-exec-core/package.json +++ b/packages/secure-exec-core/package.json @@ -1,6 +1,6 @@ { "name": "@secure-exec/core", - "version": "0.1.1-rc.2", + "version": "0.1.0", "type": "module", "license": "Apache-2.0", "main": "./dist/index.js", diff --git a/packages/secure-exec-core/src/bridge/child-process.ts b/packages/secure-exec-core/src/bridge/child-process.ts index e74a015b..09e6b39c 100644 --- a/packages/secure-exec-core/src/bridge/child-process.ts +++ b/packages/secure-exec-core/src/bridge/child-process.ts @@ -496,12 +496,13 @@ function execSync( // Default maxBuffer 1MB (Node.js convention) const maxBuffer = opts.maxBuffer ?? 1024 * 1024; - // Use synchronous bridge call - const result = _childProcessSpawnSync( + // Use synchronous bridge call - result is JSON string + const jsonResult = _childProcessSpawnSync.applySyncPromise(undefined, [ "bash", JSON.stringify(["-c", command]), JSON.stringify({ cwd: opts.cwd, env: opts.env as Record, maxBuffer }), - ); + ]); + const result = JSON.parse(jsonResult) as { stdout: string; stderr: string; code: number; maxBufferExceeded?: boolean }; if (result.maxBufferExceeded) { const err: ExecError = new Error("stdout maxBuffer length exceeded"); @@ -553,11 +554,11 @@ function spawn( const effectiveCwd = opts.cwd ?? (typeof process !== "undefined" ? process.cwd() : "/"); // Streaming mode - spawn immediately - const sessionId = _childProcessSpawnStart( + const sessionId = _childProcessSpawnStart.applySync(undefined, [ command, JSON.stringify(argsArray), JSON.stringify({ cwd: effectiveCwd, env: opts.env }), - ); + ]); activeChildren.set(sessionId, child); @@ -572,13 +573,13 @@ function spawn( if (typeof _childProcessStdinWrite === "undefined") return false; const bytes = typeof data === "string" ? new TextEncoder().encode(data) : (data as Uint8Array); - _childProcessStdinWrite(sessionId, bytes); + _childProcessStdinWrite.applySync(undefined, [sessionId, bytes]); return true; }; child.stdin.end = (): void => { if (typeof _childProcessStdinClose !== "undefined") { - _childProcessStdinClose(sessionId); + _childProcessStdinClose.applySync(undefined, [sessionId]); } child.stdin.writable = false; }; @@ -592,7 +593,7 @@ function spawn( : signal === "SIGINT" || signal === 2 ? 2 : 15; - _childProcessKill(sessionId, sig); + _childProcessKill.applySync(undefined, [sessionId, sig]); child.killed = true; child.signalCode = ( typeof signal === "string" ? signal : "SIGTERM" @@ -663,12 +664,13 @@ function spawnSync( // Pass maxBuffer through to host for enforcement const maxBuffer = opts.maxBuffer as number | undefined; - // Args and options passed as JSON strings for transferability - const result = _childProcessSpawnSync( + // Args passed as JSON string for transferability + const jsonResult = _childProcessSpawnSync.applySyncPromise(undefined, [ command, JSON.stringify(argsArray), JSON.stringify({ cwd: effectiveCwd, env: opts.env as Record, maxBuffer }), - ); + ]); + const result = JSON.parse(jsonResult) as { stdout: string; stderr: string; code: number; maxBufferExceeded?: boolean }; const stdoutBuf = typeof Buffer !== "undefined" ? Buffer.from(result.stdout) : result.stdout; const stderrBuf = typeof Buffer !== "undefined" ? Buffer.from(result.stderr) : result.stderr; diff --git a/packages/secure-exec-core/src/bridge/fs.ts b/packages/secure-exec-core/src/bridge/fs.ts index 77c9b518..c29a2791 100644 --- a/packages/secure-exec-core/src/bridge/fs.ts +++ b/packages/secure-exec-core/src/bridge/fs.ts @@ -1031,12 +1031,12 @@ const fs = { try { if (encoding) { // Text mode - use text read - const content = _fs.readFile(pathStr); + const content = _fs.readFile.applySyncPromise(undefined, [pathStr]); return content; } else { - // Binary mode - host returns raw Uint8Array via MessagePack bin - const binaryData = _fs.readFileBinary(pathStr); - return Buffer.from(binaryData); + // Binary mode - use binary read with base64 encoding + const base64Content = _fs.readFileBinary.applySyncPromise(undefined, [pathStr]); + return Buffer.from(base64Content, "base64"); } } catch (err) { const errMsg = (err as Error).message || String(err); @@ -1079,14 +1079,15 @@ const fs = { if (typeof data === "string") { // Text mode - use text write // Return the result so async callers (fs.promises) can await it. - return _fs.writeFile(pathStr, data); + return _fs.writeFile.applySyncPromise(undefined, [pathStr, data]); } else if (ArrayBuffer.isView(data)) { - // Binary mode - send raw Uint8Array via MessagePack bin + // Binary mode - convert to base64 and use binary write const uint8 = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); - return _fs.writeFileBinary(pathStr, uint8); + const base64 = Buffer.from(uint8).toString("base64"); + return _fs.writeFileBinary.applySyncPromise(undefined, [pathStr, base64]); } else { // Fallback to text mode - return _fs.writeFile(pathStr, String(data)); + return _fs.writeFile.applySyncPromise(undefined, [pathStr, String(data)]); } }, @@ -1105,9 +1106,9 @@ const fs = { readdirSync(path: PathLike, options?: nodeFs.ObjectEncodingOptions & { withFileTypes?: boolean; recursive?: boolean }): string[] | Dirent[] { const rawPath = toPathString(path); const pathStr = rawPath; - let entries: Array<{ name: string; isDirectory: boolean }>; + let entriesJson: string; try { - entries = _fs.readDir(pathStr); + entriesJson = _fs.readDir.applySyncPromise(undefined, [pathStr]); } catch (err) { // Convert "entry not found" and similar errors to proper ENOENT const errMsg = (err as Error).message || String(err); @@ -1121,6 +1122,10 @@ const fs = { } throw err; } + const entries = JSON.parse(entriesJson) as Array<{ + name: string; + isDirectory: boolean; + }>; if (options?.withFileTypes) { return entries.map((e) => new Dirent(e.name, e.isDirectory, rawPath)); } @@ -1131,13 +1136,13 @@ const fs = { const rawPath = toPathString(path); const pathStr = rawPath; const recursive = typeof options === "object" ? options?.recursive ?? false : false; - _fs.mkdir(pathStr, recursive); + _fs.mkdir.applySyncPromise(undefined, [pathStr, recursive]); return recursive ? rawPath : undefined; }, rmdirSync(path: PathLike, _options?: RmDirOptions): void { const pathStr = toPathString(path); - _fs.rmdir(pathStr); + _fs.rmdir.applySyncPromise(undefined, [pathStr]); }, rmSync(path: PathLike, options?: { force?: boolean; recursive?: boolean }): void { @@ -1175,15 +1180,15 @@ const fs = { existsSync(path: PathLike): boolean { const pathStr = toPathString(path); - return _fs.exists(pathStr); + return _fs.exists.applySyncPromise(undefined, [pathStr]); }, statSync(path: PathLike, _options?: nodeFs.StatSyncOptions): Stats { const rawPath = toPathString(path); const pathStr = rawPath; - let stat: { mode: number; size: number; isDirectory: boolean; atimeMs: number; mtimeMs: number; ctimeMs: number; birthtimeMs: number }; + let statJson: string; try { - stat = _fs.stat(pathStr); + statJson = _fs.stat.applySyncPromise(undefined, [pathStr]); } catch (err) { // Convert various "not found" errors to proper ENOENT const errMsg = (err as Error).message || String(err); @@ -1202,24 +1207,42 @@ const fs = { } throw err; } + const stat = JSON.parse(statJson) as { + mode: number; + size: number; + atimeMs?: number; + mtimeMs?: number; + ctimeMs?: number; + birthtimeMs?: number; + }; return new Stats(stat); }, lstatSync(path: PathLike, _options?: nodeFs.StatSyncOptions): Stats { const pathStr = toPathString(path); - const stat = bridgeCall(() => _fs.lstat(pathStr), "lstat", pathStr); + const statJson = bridgeCall(() => _fs.lstat.applySyncPromise(undefined, [pathStr]), "lstat", pathStr); + const stat = JSON.parse(statJson) as { + mode: number; + size: number; + isDirectory: boolean; + isSymbolicLink?: boolean; + atimeMs?: number; + mtimeMs?: number; + ctimeMs?: number; + birthtimeMs?: number; + }; return new Stats(stat); }, unlinkSync(path: PathLike): void { const pathStr = toPathString(path); - _fs.unlink(pathStr); + _fs.unlink.applySyncPromise(undefined, [pathStr]); }, renameSync(oldPath: PathLike, newPath: PathLike): void { const oldPathStr = toPathString(oldPath); const newPathStr = toPathString(newPath); - _fs.rename(oldPathStr, newPathStr); + _fs.rename.applySyncPromise(undefined, [oldPathStr, newPathStr]); }, copyFileSync(src: PathLike, dest: PathLike, _mode?: number): void { @@ -1550,41 +1573,41 @@ const fs = { chmodSync(path: PathLike, mode: Mode): void { const pathStr = toPathString(path); const modeNum = typeof mode === "string" ? parseInt(mode, 8) : mode; - bridgeCall(() => _fs.chmod(pathStr, modeNum), "chmod", pathStr); + bridgeCall(() => _fs.chmod.applySyncPromise(undefined, [pathStr, modeNum]), "chmod", pathStr); }, chownSync(path: PathLike, uid: number, gid: number): void { const pathStr = toPathString(path); - bridgeCall(() => _fs.chown(pathStr, uid, gid), "chown", pathStr); + bridgeCall(() => _fs.chown.applySyncPromise(undefined, [pathStr, uid, gid]), "chown", pathStr); }, linkSync(existingPath: PathLike, newPath: PathLike): void { const existingStr = toPathString(existingPath); const newStr = toPathString(newPath); - bridgeCall(() => _fs.link(existingStr, newStr), "link", newStr); + bridgeCall(() => _fs.link.applySyncPromise(undefined, [existingStr, newStr]), "link", newStr); }, symlinkSync(target: PathLike, path: PathLike, _type?: string | null): void { const targetStr = toPathString(target); const pathStr = toPathString(path); - bridgeCall(() => _fs.symlink(targetStr, pathStr), "symlink", pathStr); + bridgeCall(() => _fs.symlink.applySyncPromise(undefined, [targetStr, pathStr]), "symlink", pathStr); }, readlinkSync(path: PathLike, _options?: nodeFs.EncodingOption): string { const pathStr = toPathString(path); - return bridgeCall(() => _fs.readlink(pathStr), "readlink", pathStr); + return bridgeCall(() => _fs.readlink.applySyncPromise(undefined, [pathStr]), "readlink", pathStr); }, truncateSync(path: PathLike, len?: number | null): void { const pathStr = toPathString(path); - bridgeCall(() => _fs.truncate(pathStr, len ?? 0), "truncate", pathStr); + bridgeCall(() => _fs.truncate.applySyncPromise(undefined, [pathStr, len ?? 0]), "truncate", pathStr); }, utimesSync(path: PathLike, atime: string | number | Date, mtime: string | number | Date): void { const pathStr = toPathString(path); const atimeNum = typeof atime === "number" ? atime : new Date(atime).getTime() / 1000; const mtimeNum = typeof mtime === "number" ? mtime : new Date(mtime).getTime() / 1000; - bridgeCall(() => _fs.utimes(pathStr, atimeNum, mtimeNum), "utimes", pathStr); + bridgeCall(() => _fs.utimes.applySyncPromise(undefined, [pathStr, atimeNum, mtimeNum]), "utimes", pathStr); }, // Async methods - wrap sync methods in callbacks/promises diff --git a/packages/secure-exec-core/src/bridge/module.ts b/packages/secure-exec-core/src/bridge/module.ts index eb842c51..bd5b557c 100644 --- a/packages/secure-exec-core/src/bridge/module.ts +++ b/packages/secure-exec-core/src/bridge/module.ts @@ -115,7 +115,10 @@ export function createRequire(filename: string | URL): RequireFunction { request: string, _options?: { paths?: string[] } ): string { - const resolved = _resolveModule(request, dirname); + const resolved = _resolveModule.applySyncPromise(undefined, [ + request, + dirname, + ]); if (resolved === null) { const err = new Error("Cannot find module '" + request + "'") as NodeJS.ErrnoException; err.code = "MODULE_NOT_FOUND"; @@ -211,7 +214,10 @@ export class Module { (moduleRequire as { resolve?: (request: string) => string }).resolve = ( request: string ): string => { - const resolved = _resolveModule(request, this.path); + const resolved = _resolveModule.applySyncPromise(undefined, [ + request, + this.path, + ]); if (resolved === null) { const err = new Error("Cannot find module '" + request + "'") as NodeJS.ErrnoException; err.code = "MODULE_NOT_FOUND"; @@ -252,7 +258,10 @@ export class Module { _options?: unknown ): string { const parentDir = parent && parent.path ? parent.path : "/"; - const resolved = _resolveModule(request, parentDir); + const resolved = _resolveModule.applySyncPromise(undefined, [ + request, + parentDir, + ]); if (resolved === null) { const err = new Error("Cannot find module '" + request + "'") as NodeJS.ErrnoException; err.code = "MODULE_NOT_FOUND"; diff --git a/packages/secure-exec-core/src/bridge/network.ts b/packages/secure-exec-core/src/bridge/network.ts index b644eec4..93318f3c 100644 --- a/packages/secure-exec-core/src/bridge/network.ts +++ b/packages/secure-exec-core/src/bridge/network.ts @@ -135,7 +135,18 @@ export async function fetch(input: string | URL | Request, options: FetchOptions body: options.body || null, }); - const response = await _networkFetchRaw(String(url), optionsJson); + const responseJson = await _networkFetchRaw.apply(undefined, [resolvedUrl, optionsJson], { + result: { promise: true }, + }); + const response = JSON.parse(responseJson) as { + ok: boolean; + status: number; + statusText: string; + headers?: Record; + url?: string; + redirected?: boolean; + body?: string; + }; // Create Response-like object return { @@ -313,8 +324,10 @@ export const dns = { cb = options as DnsCallback; } - _networkDnsLookupRaw(hostname) - .then((result) => { + _networkDnsLookupRaw + .apply(undefined, [hostname], { result: { promise: true } }) + .then((resultJson) => { + const result = JSON.parse(resultJson) as { error?: string; code?: string; address?: string; family?: number }; if (result.error) { const err: DnsError = new Error(result.error); err.code = result.code || "ENOTFOUND"; @@ -762,7 +775,18 @@ export class ClientRequest { ...tls, }); - const response = await _networkHttpRequestRaw(url, optionsJson); + const responseJson = await _networkHttpRequestRaw.apply(undefined, [url, optionsJson], { + result: { promise: true }, + }); + const response = JSON.parse(responseJson) as { + headers?: Record; + url?: string; + status?: number; + statusText?: string; + body?: string; + trailers?: Record; + upgradeSocketId?: number; + }; this.finished = true; @@ -1391,9 +1415,12 @@ class Server { ); } - const result = await _networkHttpServerListenRaw( - JSON.stringify({ serverId: this._serverId, port, hostname }), + const resultJson = await _networkHttpServerListenRaw.apply( + undefined, + [JSON.stringify({ serverId: this._serverId, port, hostname })], + { result: { promise: true } } ); + const result = JSON.parse(resultJson) as SerializedServerListenResult; this._address = result.address; this.listening = true; this._handleId = `http-server:${this._serverId}`; @@ -1438,7 +1465,9 @@ class Server { await this._listenPromise; } if (this.listening && typeof _networkHttpServerCloseRaw !== "undefined") { - await _networkHttpServerCloseRaw(this._serverId); + await _networkHttpServerCloseRaw.apply(undefined, [this._serverId], { + result: { promise: true }, + }); } this.listening = false; this._address = null; @@ -1522,12 +1551,14 @@ class Server { /** Route an incoming HTTP request to the server's request listener and return the serialized response. */ async function dispatchServerRequest( serverId: number, - request: SerializedServerRequest -): Promise { + requestJson: string +): Promise { const listener = serverRequestListeners.get(serverId); if (!listener) { throw new Error(`Unknown HTTP server: ${serverId}`); } + + const request = JSON.parse(requestJson) as SerializedServerRequest; const incoming = new ServerIncomingMessage(request); const outgoing = new ServerResponseBridge(); @@ -1557,7 +1588,7 @@ async function dispatchServerRequest( } await outgoing.waitForClose(); - return outgoing.serialize(); + return JSON.stringify(outgoing.serialize()); } // Upgrade socket for bidirectional data relay through the host bridge diff --git a/packages/secure-exec-core/src/bridge/process.ts b/packages/secure-exec-core/src/bridge/process.ts index cfccece1..d520c8ae 100644 --- a/packages/secure-exec-core/src/bridge/process.ts +++ b/packages/secure-exec-core/src/bridge/process.ts @@ -113,8 +113,8 @@ function getNowMs(): number { : Date.now(); } -// Start time for uptime calculation (mutable for snapshot restore) -let _processStartTime = getNowMs(); +// Start time for uptime calculation +const _processStartTime = getNowMs(); const BUFFER_MAX_LENGTH = typeof (BufferPolyfill as unknown as { kMaxLength?: unknown }).kMaxLength === @@ -156,19 +156,6 @@ if ( let _exitCode = 0; let _exited = false; -// Expose reset function for snapshot restore — resets mutable state -// captured in this closure so each restored context starts fresh. -(globalThis as Record).__runtimeResetProcessState = - function () { - _processStartTime = - typeof performance !== "undefined" && performance.now - ? performance.now() - : Date.now(); - _exitCode = 0; - _exited = false; - delete (globalThis as Record).__runtimeResetProcessState; - }; - /** * Thrown by `process.exit()` to unwind the sandbox call stack. The host * catches this to extract the exit code without killing the isolate. @@ -229,7 +216,7 @@ function _addListener( const warning = `MaxListenersExceededWarning: Possible EventEmitter memory leak detected. ${total} ${event} listeners added to [process]. MaxListeners is ${_processMaxListeners}. Use emitter.setMaxListeners() to increase limit`; // Use console.error to emit warning without recursion risk if (typeof _error !== "undefined") { - _error(warning); + _error.applySync(undefined, [warning]); } } } @@ -298,7 +285,7 @@ const _stderrIsTTY = (typeof _processConfig !== "undefined" && _processConfig.st const _stdout: StdioWriteStream = { write(data: unknown): boolean { if (typeof _log !== "undefined") { - _log(String(data).replace(/\n$/, "")); + _log.applySync(undefined, [String(data).replace(/\n$/, "")]); } return true; }, @@ -324,7 +311,7 @@ const _stdout: StdioWriteStream = { const _stderr: StdioWriteStream = { write(data: unknown): boolean { if (typeof _error !== "undefined") { - _error(String(data).replace(/\n$/, "")); + _error.applySync(undefined, [String(data).replace(/\n$/, "")]); } return true; }, @@ -498,7 +485,7 @@ const _stdin: StdinStream = { throw new Error("setRawMode is not supported when stdin is not a TTY"); } if (typeof _ptySetRawMode !== "undefined") { - _ptySetRawMode(mode); + _ptySetRawMode.applySync(undefined, [mode]); } return this; }, @@ -625,9 +612,9 @@ const process: Record & { chdir(dir: string): void { // Validate directory exists in VFS before setting cwd - let stat: { isDirectory: boolean }; + let statJson: string; try { - stat = _fs.stat(dir); + statJson = _fs.stat.applySyncPromise(undefined, [dir]); } catch { const err = new Error(`ENOENT: no such file or directory, chdir '${dir}'`) as Error & { code: string; errno: number; syscall: string; path: string }; err.code = "ENOENT"; @@ -636,7 +623,8 @@ const process: Record & { err.path = dir; throw err; } - if (!stat.isDirectory) { + const parsed = JSON.parse(statJson); + if (!parsed.isDirectory) { const err = new Error(`ENOTDIR: not a directory, chdir '${dir}'`) as Error & { code: string; errno: number; syscall: string; path: string }; err.code = "ENOTDIR"; err.errno = -20; @@ -1018,7 +1006,11 @@ export function setTimeout( // Use host timer for actual delays if available and delay > 0 if (typeof _scheduleTimer !== "undefined" && actualDelay > 0) { - _scheduleTimer(actualDelay) + // _scheduleTimer.apply() returns a Promise that resolves after the delay + // Using { result: { promise: true } } tells isolated-vm to wait for the + // host Promise to resolve before resolving the apply() Promise + _scheduleTimer + .apply(undefined, [actualDelay], { result: { promise: true } }) .then(() => { if (_timers.has(id)) { _timers.delete(id); @@ -1073,7 +1065,8 @@ export function setInterval( if (typeof _scheduleTimer !== "undefined" && actualDelay > 0) { // Use host timer for actual delays - _scheduleTimer(actualDelay) + _scheduleTimer + .apply(undefined, [actualDelay], { result: { promise: true } }) .then(() => { if (_intervals.has(id)) { try { @@ -1182,7 +1175,8 @@ export const cryptoPolyfill = { array.byteLength ); try { - const hostBytes = _cryptoRandomFill(bytes.byteLength); + const base64 = _cryptoRandomFill.applySync(undefined, [bytes.byteLength]); + const hostBytes = BufferPolyfill.from(base64, "base64"); if (hostBytes.byteLength !== bytes.byteLength) { throw new Error("invalid host entropy size"); } @@ -1198,7 +1192,7 @@ export const cryptoPolyfill = { throwUnsupportedCryptoApi("randomUUID"); } try { - const uuid = _cryptoRandomUUID(); + const uuid = _cryptoRandomUUID.applySync(undefined, []); if (typeof uuid !== "string") { throw new Error("invalid host uuid"); } diff --git a/packages/secure-exec-core/src/generated/isolate-runtime.ts b/packages/secure-exec-core/src/generated/isolate-runtime.ts index 369a0661..2eb2f44d 100644 --- a/packages/secure-exec-core/src/generated/isolate-runtime.ts +++ b/packages/secure-exec-core/src/generated/isolate-runtime.ts @@ -5,17 +5,17 @@ export const ISOLATE_RUNTIME_SOURCES = { "applyTimingMitigationFreeze": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-access.ts\n function setGlobalValue(name, value) {\n Reflect.set(globalThis, name, value);\n }\n\n // isolate-runtime/src/inject/apply-timing-mitigation-freeze.ts\n var __timingConfig = globalThis.__runtimeTimingMitigationConfig ?? {};\n var __frozenTimeMs = typeof __timingConfig.frozenTimeMs === \"number\" && Number.isFinite(__timingConfig.frozenTimeMs) ? __timingConfig.frozenTimeMs : Date.now();\n var __frozenDateNow = () => __frozenTimeMs;\n try {\n Object.defineProperty(Date, \"now\", {\n get: () => __frozenDateNow,\n set: () => {\n },\n configurable: false\n });\n } catch {\n Date.now = __frozenDateNow;\n }\n var __OrigDate = Date;\n var __FrozenDate = function Date2(...args) {\n if (new.target) {\n if (args.length === 0) {\n return new __OrigDate(__frozenTimeMs);\n }\n return new __OrigDate(...args);\n }\n return __OrigDate();\n };\n Object.defineProperty(__FrozenDate, \"prototype\", {\n value: __OrigDate.prototype,\n writable: false,\n configurable: false\n });\n __FrozenDate.now = __frozenDateNow;\n __FrozenDate.parse = __OrigDate.parse;\n __FrozenDate.UTC = __OrigDate.UTC;\n Object.defineProperty(__FrozenDate, \"now\", {\n get: () => __frozenDateNow,\n set: () => {\n },\n configurable: false\n });\n try {\n Object.defineProperty(globalThis, \"Date\", {\n value: __FrozenDate,\n configurable: false,\n writable: false\n });\n } catch {\n globalThis.Date = __FrozenDate;\n }\n var __frozenPerformanceNow = () => 0;\n var __origPerf = globalThis.performance;\n var __frozenPerf = /* @__PURE__ */ Object.create(null);\n if (typeof __origPerf !== \"undefined\" && __origPerf !== null) {\n const src = __origPerf;\n for (const key of Object.getOwnPropertyNames(\n Object.getPrototypeOf(__origPerf) ?? __origPerf\n )) {\n if (key !== \"now\") {\n try {\n const val = src[key];\n if (typeof val === \"function\") {\n __frozenPerf[key] = val.bind(__origPerf);\n } else {\n __frozenPerf[key] = val;\n }\n } catch {\n }\n }\n }\n }\n Object.defineProperty(__frozenPerf, \"now\", {\n value: __frozenPerformanceNow,\n configurable: false,\n writable: false\n });\n Object.freeze(__frozenPerf);\n try {\n Object.defineProperty(globalThis, \"performance\", {\n value: __frozenPerf,\n configurable: false,\n writable: false\n });\n } catch {\n globalThis.performance = __frozenPerf;\n }\n var __OrigSAB = globalThis.SharedArrayBuffer;\n if (typeof __OrigSAB === \"function\") {\n try {\n const proto = __OrigSAB.prototype;\n if (proto) {\n for (const key of [\n \"byteLength\",\n \"slice\",\n \"grow\",\n \"maxByteLength\",\n \"growable\"\n ]) {\n try {\n Object.defineProperty(proto, key, {\n get() {\n throw new TypeError(\n \"SharedArrayBuffer is not available in sandbox\"\n );\n },\n configurable: false\n });\n } catch {\n }\n }\n }\n } catch {\n }\n }\n try {\n Object.defineProperty(globalThis, \"SharedArrayBuffer\", {\n value: void 0,\n configurable: false,\n writable: false,\n enumerable: false\n });\n } catch {\n Reflect.deleteProperty(globalThis, \"SharedArrayBuffer\");\n setGlobalValue(\"SharedArrayBuffer\", void 0);\n }\n})();\n", "applyTimingMitigationOff": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-access.ts\n function setGlobalValue(name, value) {\n Reflect.set(globalThis, name, value);\n }\n\n // isolate-runtime/src/inject/apply-timing-mitigation-off.ts\n if (typeof globalThis.performance === \"undefined\" || globalThis.performance === null) {\n setGlobalValue(\"performance\", {\n now: () => Date.now()\n });\n }\n})();\n", "bridgeAttach": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-exposure.ts\n function defineRuntimeGlobalBinding(name, value, mutable) {\n Object.defineProperty(globalThis, name, {\n value,\n writable: mutable,\n configurable: mutable,\n enumerable: true\n });\n }\n function createRuntimeGlobalExposer(mutable) {\n return (name, value) => {\n defineRuntimeGlobalBinding(name, value, mutable);\n };\n }\n function getRuntimeExposeCustomGlobal() {\n if (typeof globalThis.__runtimeExposeCustomGlobal === \"function\") {\n return globalThis.__runtimeExposeCustomGlobal;\n }\n return createRuntimeGlobalExposer(false);\n }\n\n // isolate-runtime/src/inject/bridge-attach.ts\n var __runtimeExposeCustomGlobal = getRuntimeExposeCustomGlobal();\n if (typeof globalThis.bridge !== \"undefined\") {\n __runtimeExposeCustomGlobal(\"bridge\", globalThis.bridge);\n }\n})();\n", - "bridgeInitialGlobals": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-exposure.ts\n function defineRuntimeGlobalBinding(name, value, mutable) {\n Object.defineProperty(globalThis, name, {\n value,\n writable: mutable,\n configurable: mutable,\n enumerable: true\n });\n }\n function createRuntimeGlobalExposer(mutable) {\n return (name, value) => {\n defineRuntimeGlobalBinding(name, value, mutable);\n };\n }\n function getRuntimeExposeMutableGlobal() {\n if (typeof globalThis.__runtimeExposeMutableGlobal === \"function\") {\n return globalThis.__runtimeExposeMutableGlobal;\n }\n return createRuntimeGlobalExposer(true);\n }\n\n // isolate-runtime/src/common/global-access.ts\n function setGlobalValue(name, value) {\n Reflect.set(globalThis, name, value);\n }\n\n // isolate-runtime/src/inject/bridge-initial-globals.ts\n var __runtimeExposeMutableGlobal = getRuntimeExposeMutableGlobal();\n var __bridgeSetupConfig = globalThis.__runtimeBridgeSetupConfig ?? {};\n var __initialCwd = typeof __bridgeSetupConfig.initialCwd === \"string\" ? __bridgeSetupConfig.initialCwd : \"/\";\n globalThis.__runtimeJsonPayloadLimitBytes = typeof __bridgeSetupConfig.jsonPayloadLimitBytes === \"number\" && Number.isFinite(__bridgeSetupConfig.jsonPayloadLimitBytes) ? Math.max(0, Math.floor(__bridgeSetupConfig.jsonPayloadLimitBytes)) : 4 * 1024 * 1024;\n globalThis.__runtimePayloadLimitErrorCode = typeof __bridgeSetupConfig.payloadLimitErrorCode === \"string\" && __bridgeSetupConfig.payloadLimitErrorCode.length > 0 ? __bridgeSetupConfig.payloadLimitErrorCode : \"ERR_SANDBOX_PAYLOAD_TOO_LARGE\";\n function __scEncode(value, seen) {\n if (value === null) return null;\n if (value === void 0) return { t: \"undef\" };\n if (typeof value === \"boolean\") return value;\n if (typeof value === \"string\") return value;\n if (typeof value === \"bigint\") return { t: \"bigint\", v: String(value) };\n if (typeof value === \"number\") {\n if (Object.is(value, -0)) return { t: \"-0\" };\n if (Number.isNaN(value)) return { t: \"nan\" };\n if (value === Infinity) return { t: \"inf\" };\n if (value === -Infinity) return { t: \"-inf\" };\n return value;\n }\n const obj = value;\n if (seen.has(obj)) return { t: \"ref\", i: seen.get(obj) };\n const idx = seen.size;\n seen.set(obj, idx);\n if (value instanceof Date)\n return { t: \"date\", v: value.getTime() };\n if (value instanceof RegExp)\n return { t: \"regexp\", p: value.source, f: value.flags };\n if (value instanceof Map) {\n const entries = [];\n value.forEach((v, k) => {\n entries.push([__scEncode(k, seen), __scEncode(v, seen)]);\n });\n return { t: \"map\", v: entries };\n }\n if (value instanceof Set) {\n const elems = [];\n value.forEach((v) => {\n elems.push(__scEncode(v, seen));\n });\n return { t: \"set\", v: elems };\n }\n if (value instanceof ArrayBuffer) {\n return { t: \"ab\", v: Array.from(new Uint8Array(value)) };\n }\n if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {\n return {\n t: \"ta\",\n k: value.constructor.name,\n v: Array.from(\n new Uint8Array(value.buffer, value.byteOffset, value.byteLength)\n )\n };\n }\n if (Array.isArray(value)) {\n return {\n t: \"arr\",\n v: value.map((v) => __scEncode(v, seen))\n };\n }\n const result = {};\n for (const key of Object.keys(value)) {\n result[key] = __scEncode(\n value[key],\n seen\n );\n }\n return { t: \"obj\", v: result };\n }\n function __scDecode(tagged, refs) {\n if (tagged === null) return null;\n if (typeof tagged === \"boolean\" || typeof tagged === \"string\" || typeof tagged === \"number\")\n return tagged;\n const tag = tagged.t;\n if (tag === void 0) return tagged;\n switch (tag) {\n case \"undef\":\n return void 0;\n case \"nan\":\n return NaN;\n case \"inf\":\n return Infinity;\n case \"-inf\":\n return -Infinity;\n case \"-0\":\n return -0;\n case \"bigint\":\n return BigInt(tagged.v);\n case \"ref\":\n return refs[tagged.i];\n case \"date\": {\n const d = new Date(tagged.v);\n refs.push(d);\n return d;\n }\n case \"regexp\": {\n const r = new RegExp(\n tagged.p,\n tagged.f\n );\n refs.push(r);\n return r;\n }\n case \"map\": {\n const m = /* @__PURE__ */ new Map();\n refs.push(m);\n for (const [k, v] of tagged.v) {\n m.set(__scDecode(k, refs), __scDecode(v, refs));\n }\n return m;\n }\n case \"set\": {\n const s = /* @__PURE__ */ new Set();\n refs.push(s);\n for (const v of tagged.v) {\n s.add(__scDecode(v, refs));\n }\n return s;\n }\n case \"ab\": {\n const bytes = tagged.v;\n const ab = new ArrayBuffer(bytes.length);\n const u8 = new Uint8Array(ab);\n for (let i = 0; i < bytes.length; i++) u8[i] = bytes[i];\n refs.push(ab);\n return ab;\n }\n case \"ta\": {\n const { k, v: bytes } = tagged;\n const ctors = {\n Int8Array,\n Uint8Array,\n Uint8ClampedArray,\n Int16Array,\n Uint16Array,\n Int32Array,\n Uint32Array,\n Float32Array,\n Float64Array\n };\n const Ctor = ctors[k] ?? Uint8Array;\n const ab = new ArrayBuffer(bytes.length);\n const u8 = new Uint8Array(ab);\n for (let i = 0; i < bytes.length; i++) u8[i] = bytes[i];\n const ta = new Ctor(ab);\n refs.push(ta);\n return ta;\n }\n case \"arr\": {\n const arr = [];\n refs.push(arr);\n for (const v of tagged.v) {\n arr.push(__scDecode(v, refs));\n }\n return arr;\n }\n case \"obj\": {\n const obj = {};\n refs.push(obj);\n const entries = tagged.v;\n for (const key of Object.keys(entries)) {\n obj[key] = __scDecode(entries[key], refs);\n }\n return obj;\n }\n default:\n return tagged;\n }\n }\n __runtimeExposeMutableGlobal(\"_moduleCache\", {});\n globalThis._moduleCache = globalThis._moduleCache ?? {};\n var __moduleCache = globalThis._moduleCache;\n if (__moduleCache) {\n __moduleCache[\"v8\"] = {\n getHeapStatistics: function() {\n return {\n total_heap_size: 67108864,\n total_heap_size_executable: 1048576,\n total_physical_size: 67108864,\n total_available_size: 67108864,\n used_heap_size: 52428800,\n heap_size_limit: 134217728,\n malloced_memory: 8192,\n peak_malloced_memory: 16384,\n does_zap_garbage: 0,\n number_of_native_contexts: 1,\n number_of_detached_contexts: 0,\n external_memory: 0\n };\n },\n getHeapSpaceStatistics: function() {\n return [];\n },\n getHeapCodeStatistics: function() {\n return {};\n },\n setFlagsFromString: function() {\n },\n serialize: function(value) {\n return Buffer.from(\n JSON.stringify({ $v8sc: 1, d: __scEncode(value, /* @__PURE__ */ new Map()) })\n );\n },\n deserialize: function(buffer) {\n const limit = globalThis.__runtimeJsonPayloadLimitBytes ?? 4 * 1024 * 1024;\n const errorCode = globalThis.__runtimePayloadLimitErrorCode ?? \"ERR_SANDBOX_PAYLOAD_TOO_LARGE\";\n if (buffer.length > limit) {\n throw new Error(\n errorCode + \": v8.deserialize exceeds \" + String(limit) + \" bytes\"\n );\n }\n const text = buffer.toString();\n const envelope = JSON.parse(text);\n if (envelope !== null && typeof envelope === \"object\" && envelope.$v8sc === 1) {\n return __scDecode(envelope.d, []);\n }\n return envelope;\n },\n cachedDataVersionTag: function() {\n return 0;\n }\n };\n }\n __runtimeExposeMutableGlobal(\"_pendingModules\", {});\n __runtimeExposeMutableGlobal(\"_currentModule\", { dirname: __initialCwd });\n globalThis.__runtimeApplyConfig = function(config) {\n if (typeof config.payloadLimitBytes === \"number\" && Number.isFinite(config.payloadLimitBytes)) {\n globalThis.__runtimeJsonPayloadLimitBytes = Math.max(\n 0,\n Math.floor(config.payloadLimitBytes)\n );\n }\n if (typeof config.payloadLimitErrorCode === \"string\" && config.payloadLimitErrorCode.length > 0) {\n globalThis.__runtimePayloadLimitErrorCode = config.payloadLimitErrorCode;\n }\n if (config.timingMitigation === \"freeze\") {\n const frozenTimeMs = typeof config.frozenTimeMs === \"number\" && Number.isFinite(config.frozenTimeMs) ? config.frozenTimeMs : Date.now();\n const frozenDateNow = () => frozenTimeMs;\n try {\n Object.defineProperty(Date, \"now\", {\n value: frozenDateNow,\n configurable: false,\n writable: false\n });\n } catch {\n Date.now = frozenDateNow;\n }\n const OrigDate = Date;\n const FrozenDate = function Date2(...args) {\n if (new.target) {\n if (args.length === 0) {\n return new OrigDate(frozenTimeMs);\n }\n return new OrigDate(...args);\n }\n return OrigDate();\n };\n Object.defineProperty(FrozenDate, \"prototype\", {\n value: OrigDate.prototype,\n writable: false,\n configurable: false\n });\n FrozenDate.now = frozenDateNow;\n FrozenDate.parse = OrigDate.parse;\n FrozenDate.UTC = OrigDate.UTC;\n Object.defineProperty(FrozenDate, \"now\", {\n value: frozenDateNow,\n configurable: false,\n writable: false\n });\n try {\n Object.defineProperty(globalThis, \"Date\", {\n value: FrozenDate,\n configurable: false,\n writable: false\n });\n } catch {\n globalThis.Date = FrozenDate;\n }\n const frozenPerformanceNow = () => 0;\n const origPerf = globalThis.performance;\n const frozenPerf = /* @__PURE__ */ Object.create(null);\n if (typeof origPerf !== \"undefined\" && origPerf !== null) {\n const src = origPerf;\n for (const key of Object.getOwnPropertyNames(\n Object.getPrototypeOf(origPerf) ?? origPerf\n )) {\n if (key !== \"now\") {\n try {\n const val = src[key];\n if (typeof val === \"function\") {\n frozenPerf[key] = val.bind(origPerf);\n } else {\n frozenPerf[key] = val;\n }\n } catch {\n }\n }\n }\n }\n Object.defineProperty(frozenPerf, \"now\", {\n value: frozenPerformanceNow,\n configurable: false,\n writable: false\n });\n Object.freeze(frozenPerf);\n try {\n Object.defineProperty(globalThis, \"performance\", {\n value: frozenPerf,\n configurable: false,\n writable: false\n });\n } catch {\n globalThis.performance = frozenPerf;\n }\n const OrigSAB = globalThis.SharedArrayBuffer;\n if (typeof OrigSAB === \"function\") {\n try {\n const proto = OrigSAB.prototype;\n if (proto) {\n for (const key of [\n \"byteLength\",\n \"slice\",\n \"grow\",\n \"maxByteLength\",\n \"growable\"\n ]) {\n try {\n Object.defineProperty(proto, key, {\n get() {\n throw new TypeError(\n \"SharedArrayBuffer is not available in sandbox\"\n );\n },\n configurable: false\n });\n } catch {\n }\n }\n }\n } catch {\n }\n }\n try {\n Object.defineProperty(globalThis, \"SharedArrayBuffer\", {\n value: void 0,\n configurable: false,\n writable: false,\n enumerable: false\n });\n } catch {\n Reflect.deleteProperty(globalThis, \"SharedArrayBuffer\");\n setGlobalValue(\"SharedArrayBuffer\", void 0);\n }\n }\n delete globalThis.__runtimeApplyConfig;\n };\n})();\n", + "bridgeInitialGlobals": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-exposure.ts\n function defineRuntimeGlobalBinding(name, value, mutable) {\n Object.defineProperty(globalThis, name, {\n value,\n writable: mutable,\n configurable: mutable,\n enumerable: true\n });\n }\n function createRuntimeGlobalExposer(mutable) {\n return (name, value) => {\n defineRuntimeGlobalBinding(name, value, mutable);\n };\n }\n function getRuntimeExposeMutableGlobal() {\n if (typeof globalThis.__runtimeExposeMutableGlobal === \"function\") {\n return globalThis.__runtimeExposeMutableGlobal;\n }\n return createRuntimeGlobalExposer(true);\n }\n\n // isolate-runtime/src/inject/bridge-initial-globals.ts\n var __runtimeExposeMutableGlobal = getRuntimeExposeMutableGlobal();\n var __bridgeSetupConfig = globalThis.__runtimeBridgeSetupConfig ?? {};\n var __initialCwd = typeof __bridgeSetupConfig.initialCwd === \"string\" ? __bridgeSetupConfig.initialCwd : \"/\";\n var __jsonPayloadLimitBytes = typeof __bridgeSetupConfig.jsonPayloadLimitBytes === \"number\" && Number.isFinite(__bridgeSetupConfig.jsonPayloadLimitBytes) ? Math.max(0, Math.floor(__bridgeSetupConfig.jsonPayloadLimitBytes)) : 4 * 1024 * 1024;\n var __payloadLimitErrorCode = typeof __bridgeSetupConfig.payloadLimitErrorCode === \"string\" && __bridgeSetupConfig.payloadLimitErrorCode.length > 0 ? __bridgeSetupConfig.payloadLimitErrorCode : \"ERR_SANDBOX_PAYLOAD_TOO_LARGE\";\n function __scEncode(value, seen) {\n if (value === null) return null;\n if (value === void 0) return { t: \"undef\" };\n if (typeof value === \"boolean\") return value;\n if (typeof value === \"string\") return value;\n if (typeof value === \"bigint\") return { t: \"bigint\", v: String(value) };\n if (typeof value === \"number\") {\n if (Object.is(value, -0)) return { t: \"-0\" };\n if (Number.isNaN(value)) return { t: \"nan\" };\n if (value === Infinity) return { t: \"inf\" };\n if (value === -Infinity) return { t: \"-inf\" };\n return value;\n }\n const obj = value;\n if (seen.has(obj)) return { t: \"ref\", i: seen.get(obj) };\n const idx = seen.size;\n seen.set(obj, idx);\n if (value instanceof Date)\n return { t: \"date\", v: value.getTime() };\n if (value instanceof RegExp)\n return { t: \"regexp\", p: value.source, f: value.flags };\n if (value instanceof Map) {\n const entries = [];\n value.forEach((v, k) => {\n entries.push([__scEncode(k, seen), __scEncode(v, seen)]);\n });\n return { t: \"map\", v: entries };\n }\n if (value instanceof Set) {\n const elems = [];\n value.forEach((v) => {\n elems.push(__scEncode(v, seen));\n });\n return { t: \"set\", v: elems };\n }\n if (value instanceof ArrayBuffer) {\n return { t: \"ab\", v: Array.from(new Uint8Array(value)) };\n }\n if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {\n return {\n t: \"ta\",\n k: value.constructor.name,\n v: Array.from(\n new Uint8Array(value.buffer, value.byteOffset, value.byteLength)\n )\n };\n }\n if (Array.isArray(value)) {\n return {\n t: \"arr\",\n v: value.map((v) => __scEncode(v, seen))\n };\n }\n const result = {};\n for (const key of Object.keys(value)) {\n result[key] = __scEncode(\n value[key],\n seen\n );\n }\n return { t: \"obj\", v: result };\n }\n function __scDecode(tagged, refs) {\n if (tagged === null) return null;\n if (typeof tagged === \"boolean\" || typeof tagged === \"string\" || typeof tagged === \"number\")\n return tagged;\n const tag = tagged.t;\n if (tag === void 0) return tagged;\n switch (tag) {\n case \"undef\":\n return void 0;\n case \"nan\":\n return NaN;\n case \"inf\":\n return Infinity;\n case \"-inf\":\n return -Infinity;\n case \"-0\":\n return -0;\n case \"bigint\":\n return BigInt(tagged.v);\n case \"ref\":\n return refs[tagged.i];\n case \"date\": {\n const d = new Date(tagged.v);\n refs.push(d);\n return d;\n }\n case \"regexp\": {\n const r = new RegExp(\n tagged.p,\n tagged.f\n );\n refs.push(r);\n return r;\n }\n case \"map\": {\n const m = /* @__PURE__ */ new Map();\n refs.push(m);\n for (const [k, v] of tagged.v) {\n m.set(__scDecode(k, refs), __scDecode(v, refs));\n }\n return m;\n }\n case \"set\": {\n const s = /* @__PURE__ */ new Set();\n refs.push(s);\n for (const v of tagged.v) {\n s.add(__scDecode(v, refs));\n }\n return s;\n }\n case \"ab\": {\n const bytes = tagged.v;\n const ab = new ArrayBuffer(bytes.length);\n const u8 = new Uint8Array(ab);\n for (let i = 0; i < bytes.length; i++) u8[i] = bytes[i];\n refs.push(ab);\n return ab;\n }\n case \"ta\": {\n const { k, v: bytes } = tagged;\n const ctors = {\n Int8Array,\n Uint8Array,\n Uint8ClampedArray,\n Int16Array,\n Uint16Array,\n Int32Array,\n Uint32Array,\n Float32Array,\n Float64Array\n };\n const Ctor = ctors[k] ?? Uint8Array;\n const ab = new ArrayBuffer(bytes.length);\n const u8 = new Uint8Array(ab);\n for (let i = 0; i < bytes.length; i++) u8[i] = bytes[i];\n const ta = new Ctor(ab);\n refs.push(ta);\n return ta;\n }\n case \"arr\": {\n const arr = [];\n refs.push(arr);\n for (const v of tagged.v) {\n arr.push(__scDecode(v, refs));\n }\n return arr;\n }\n case \"obj\": {\n const obj = {};\n refs.push(obj);\n const entries = tagged.v;\n for (const key of Object.keys(entries)) {\n obj[key] = __scDecode(entries[key], refs);\n }\n return obj;\n }\n default:\n return tagged;\n }\n }\n __runtimeExposeMutableGlobal(\"_moduleCache\", {});\n globalThis._moduleCache = globalThis._moduleCache ?? {};\n var __moduleCache = globalThis._moduleCache;\n if (__moduleCache) {\n __moduleCache[\"v8\"] = {\n getHeapStatistics: function() {\n return {\n total_heap_size: 67108864,\n total_heap_size_executable: 1048576,\n total_physical_size: 67108864,\n total_available_size: 67108864,\n used_heap_size: 52428800,\n heap_size_limit: 134217728,\n malloced_memory: 8192,\n peak_malloced_memory: 16384,\n does_zap_garbage: 0,\n number_of_native_contexts: 1,\n number_of_detached_contexts: 0,\n external_memory: 0\n };\n },\n getHeapSpaceStatistics: function() {\n return [];\n },\n getHeapCodeStatistics: function() {\n return {};\n },\n setFlagsFromString: function() {\n },\n serialize: function(value) {\n return Buffer.from(\n JSON.stringify({ $v8sc: 1, d: __scEncode(value, /* @__PURE__ */ new Map()) })\n );\n },\n deserialize: function(buffer) {\n if (buffer.length > __jsonPayloadLimitBytes) {\n throw new Error(\n __payloadLimitErrorCode + \": v8.deserialize exceeds \" + String(__jsonPayloadLimitBytes) + \" bytes\"\n );\n }\n const text = buffer.toString();\n const envelope = JSON.parse(text);\n if (envelope !== null && typeof envelope === \"object\" && envelope.$v8sc === 1) {\n return __scDecode(envelope.d, []);\n }\n return envelope;\n },\n cachedDataVersionTag: function() {\n return 0;\n }\n };\n }\n __runtimeExposeMutableGlobal(\"_pendingModules\", {});\n __runtimeExposeMutableGlobal(\"_currentModule\", { dirname: __initialCwd });\n})();\n", "evalScriptResult": "\"use strict\";\n(() => {\n // isolate-runtime/src/inject/eval-script-result.ts\n var __runtimeIndirectEval = globalThis.eval;\n globalThis.__scriptResult__ = __runtimeIndirectEval(\n String(globalThis.__runtimeExecCode)\n );\n})();\n", "globalExposureHelpers": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-exposure.ts\n function defineRuntimeGlobalBinding(name, value, mutable) {\n Object.defineProperty(globalThis, name, {\n value,\n writable: mutable,\n configurable: mutable,\n enumerable: true\n });\n }\n function createRuntimeGlobalExposer(mutable) {\n return (name, value) => {\n defineRuntimeGlobalBinding(name, value, mutable);\n };\n }\n function ensureRuntimeExposureHelpers() {\n if (typeof globalThis.__runtimeExposeCustomGlobal !== \"function\") {\n defineRuntimeGlobalBinding(\n \"__runtimeExposeCustomGlobal\",\n createRuntimeGlobalExposer(false),\n false\n );\n }\n if (typeof globalThis.__runtimeExposeMutableGlobal !== \"function\") {\n defineRuntimeGlobalBinding(\n \"__runtimeExposeMutableGlobal\",\n createRuntimeGlobalExposer(true),\n false\n );\n }\n }\n\n // isolate-runtime/src/inject/global-exposure-helpers.ts\n ensureRuntimeExposureHelpers();\n})();\n", "initCommonjsModuleGlobals": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-exposure.ts\n function defineRuntimeGlobalBinding(name, value, mutable) {\n Object.defineProperty(globalThis, name, {\n value,\n writable: mutable,\n configurable: mutable,\n enumerable: true\n });\n }\n function createRuntimeGlobalExposer(mutable) {\n return (name, value) => {\n defineRuntimeGlobalBinding(name, value, mutable);\n };\n }\n function getRuntimeExposeMutableGlobal() {\n if (typeof globalThis.__runtimeExposeMutableGlobal === \"function\") {\n return globalThis.__runtimeExposeMutableGlobal;\n }\n return createRuntimeGlobalExposer(true);\n }\n\n // isolate-runtime/src/inject/init-commonjs-module-globals.ts\n var __runtimeExposeMutableGlobal = getRuntimeExposeMutableGlobal();\n __runtimeExposeMutableGlobal(\"module\", { exports: {} });\n __runtimeExposeMutableGlobal(\"exports\", globalThis.module.exports);\n})();\n", "overrideProcessCwd": "\"use strict\";\n(() => {\n // isolate-runtime/src/inject/override-process-cwd.ts\n var __cwd = globalThis.__runtimeProcessCwdOverride;\n if (typeof __cwd === \"string\") {\n process.cwd = () => __cwd;\n }\n})();\n", "overrideProcessEnv": "\"use strict\";\n(() => {\n // isolate-runtime/src/inject/override-process-env.ts\n var __envPatch = globalThis.__runtimeProcessEnvOverride;\n if (__envPatch && typeof __envPatch === \"object\") {\n Object.assign(process.env, __envPatch);\n }\n})();\n", - "requireSetup": "\"use strict\";\n(() => {\n // isolate-runtime/src/inject/require-setup.ts\n var __requireExposeCustomGlobal = typeof globalThis.__runtimeExposeCustomGlobal === \"function\" ? globalThis.__runtimeExposeCustomGlobal : function exposeCustomGlobal(name2, value) {\n Object.defineProperty(globalThis, name2, {\n value,\n writable: false,\n configurable: false,\n enumerable: true\n });\n };\n if (typeof globalThis.AbortController === \"undefined\" || typeof globalThis.AbortSignal === \"undefined\") {\n class AbortSignal {\n constructor() {\n this.aborted = false;\n this.reason = void 0;\n this.onabort = null;\n this._listeners = [];\n }\n addEventListener(type, listener) {\n if (type !== \"abort\" || typeof listener !== \"function\") return;\n this._listeners.push(listener);\n }\n removeEventListener(type, listener) {\n if (type !== \"abort\" || typeof listener !== \"function\") return;\n const index = this._listeners.indexOf(listener);\n if (index !== -1) {\n this._listeners.splice(index, 1);\n }\n }\n dispatchEvent(event) {\n if (!event || event.type !== \"abort\") return false;\n if (typeof this.onabort === \"function\") {\n try {\n this.onabort.call(this, event);\n } catch {\n }\n }\n const listeners = this._listeners.slice();\n for (const listener of listeners) {\n try {\n listener.call(this, event);\n } catch {\n }\n }\n return true;\n }\n }\n class AbortController {\n constructor() {\n this.signal = new AbortSignal();\n }\n abort(reason) {\n if (this.signal.aborted) return;\n this.signal.aborted = true;\n this.signal.reason = reason;\n this.signal.dispatchEvent({ type: \"abort\" });\n }\n }\n __requireExposeCustomGlobal(\"AbortSignal\", AbortSignal);\n __requireExposeCustomGlobal(\"AbortController\", AbortController);\n }\n if (typeof globalThis.structuredClone !== \"function\") {\n let structuredClonePolyfill = function(value) {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n if (value instanceof ArrayBuffer) {\n return value.slice(0);\n }\n if (ArrayBuffer.isView(value)) {\n if (value instanceof Uint8Array) {\n return new Uint8Array(value);\n }\n return new value.constructor(value);\n }\n return JSON.parse(JSON.stringify(value));\n };\n structuredClonePolyfill2 = structuredClonePolyfill;\n __requireExposeCustomGlobal(\"structuredClone\", structuredClonePolyfill);\n }\n var structuredClonePolyfill2;\n if (typeof globalThis.btoa !== \"function\") {\n __requireExposeCustomGlobal(\"btoa\", function btoa(input) {\n return Buffer.from(String(input), \"binary\").toString(\"base64\");\n });\n }\n if (typeof globalThis.atob !== \"function\") {\n __requireExposeCustomGlobal(\"atob\", function atob(input) {\n return Buffer.from(String(input), \"base64\").toString(\"binary\");\n });\n }\n function _dirname(p) {\n const lastSlash = p.lastIndexOf(\"/\");\n if (lastSlash === -1) return \".\";\n if (lastSlash === 0) return \"/\";\n return p.slice(0, lastSlash);\n }\n function _patchPolyfill(name2, result2) {\n if (typeof result2 !== \"object\" && typeof result2 !== \"function\" || result2 === null) {\n return result2;\n }\n if (name2 === \"buffer\") {\n const maxLength = typeof result2.kMaxLength === \"number\" ? result2.kMaxLength : 2147483647;\n const maxStringLength = typeof result2.kStringMaxLength === \"number\" ? result2.kStringMaxLength : 536870888;\n if (typeof result2.constants !== \"object\" || result2.constants === null) {\n result2.constants = {};\n }\n if (typeof result2.constants.MAX_LENGTH !== \"number\") {\n result2.constants.MAX_LENGTH = maxLength;\n }\n if (typeof result2.constants.MAX_STRING_LENGTH !== \"number\") {\n result2.constants.MAX_STRING_LENGTH = maxStringLength;\n }\n if (typeof result2.kMaxLength !== \"number\") {\n result2.kMaxLength = maxLength;\n }\n if (typeof result2.kStringMaxLength !== \"number\") {\n result2.kStringMaxLength = maxStringLength;\n }\n const BufferCtor = result2.Buffer;\n if ((typeof BufferCtor === \"function\" || typeof BufferCtor === \"object\") && BufferCtor !== null) {\n if (typeof BufferCtor.kMaxLength !== \"number\") {\n BufferCtor.kMaxLength = maxLength;\n }\n if (typeof BufferCtor.kStringMaxLength !== \"number\") {\n BufferCtor.kStringMaxLength = maxStringLength;\n }\n if (typeof BufferCtor.constants !== \"object\" || BufferCtor.constants === null) {\n BufferCtor.constants = result2.constants;\n }\n }\n return result2;\n }\n if (name2 === \"util\" && typeof result2.formatWithOptions === \"undefined\" && typeof result2.format === \"function\") {\n result2.formatWithOptions = function formatWithOptions(inspectOptions, ...args) {\n return result2.format.apply(null, args);\n };\n return result2;\n }\n if (name2 === \"url\") {\n const OriginalURL = result2.URL;\n if (typeof OriginalURL !== \"function\" || OriginalURL._patched) {\n return result2;\n }\n const PatchedURL = function PatchedURL2(url, base) {\n if (typeof url === \"string\" && url.startsWith(\"file:\") && !url.startsWith(\"file://\") && base === void 0) {\n if (typeof process !== \"undefined\" && typeof process.cwd === \"function\") {\n const cwd = process.cwd();\n if (cwd) {\n try {\n return new OriginalURL(url, \"file://\" + cwd + \"/\");\n } catch (e) {\n }\n }\n }\n }\n return base !== void 0 ? new OriginalURL(url, base) : new OriginalURL(url);\n };\n Object.keys(OriginalURL).forEach(function(key) {\n try {\n PatchedURL[key] = OriginalURL[key];\n } catch {\n }\n });\n Object.setPrototypeOf(PatchedURL, OriginalURL);\n PatchedURL.prototype = OriginalURL.prototype;\n PatchedURL._patched = true;\n const descriptor = Object.getOwnPropertyDescriptor(result2, \"URL\");\n if (descriptor && descriptor.configurable !== true && descriptor.writable !== true && typeof descriptor.set !== \"function\") {\n return result2;\n }\n try {\n result2.URL = PatchedURL;\n } catch {\n try {\n Object.defineProperty(result2, \"URL\", {\n value: PatchedURL,\n writable: true,\n configurable: true,\n enumerable: descriptor?.enumerable ?? true\n });\n } catch {\n }\n }\n return result2;\n }\n if (name2 === \"path\") {\n if (result2.win32 === null || result2.win32 === void 0) {\n result2.win32 = result2.posix || result2;\n }\n if (result2.posix === null || result2.posix === void 0) {\n result2.posix = result2;\n }\n const hasAbsoluteSegment = function(args) {\n return args.some(function(arg) {\n return typeof arg === \"string\" && arg.length > 0 && arg.charAt(0) === \"/\";\n });\n };\n const prependCwd = function(args) {\n if (hasAbsoluteSegment(args)) return;\n if (typeof process !== \"undefined\" && typeof process.cwd === \"function\") {\n const cwd = process.cwd();\n if (cwd && cwd.charAt(0) === \"/\") {\n args.unshift(cwd);\n }\n }\n };\n const originalResolve = result2.resolve;\n if (typeof originalResolve === \"function\" && !originalResolve._patchedForCwd) {\n const patchedResolve = function resolve2() {\n const args = Array.from(arguments);\n prependCwd(args);\n return originalResolve.apply(this, args);\n };\n patchedResolve._patchedForCwd = true;\n result2.resolve = patchedResolve;\n }\n if (result2.posix && typeof result2.posix.resolve === \"function\" && !result2.posix.resolve._patchedForCwd) {\n const originalPosixResolve = result2.posix.resolve;\n const patchedPosixResolve = function resolve2() {\n const args = Array.from(arguments);\n prependCwd(args);\n return originalPosixResolve.apply(this, args);\n };\n patchedPosixResolve._patchedForCwd = true;\n result2.posix.resolve = patchedPosixResolve;\n }\n }\n return result2;\n }\n var _deferredCoreModules = /* @__PURE__ */ new Set([\n \"net\",\n \"tls\",\n \"readline\",\n \"perf_hooks\",\n \"async_hooks\",\n \"worker_threads\",\n \"diagnostics_channel\"\n ]);\n var _unsupportedCoreModules = /* @__PURE__ */ new Set([\n \"dgram\",\n \"cluster\",\n \"wasi\",\n \"inspector\",\n \"repl\",\n \"trace_events\",\n \"domain\"\n ]);\n function _unsupportedApiError(moduleName2, apiName) {\n return new Error(moduleName2 + \".\" + apiName + \" is not supported in sandbox\");\n }\n function _createDeferredModuleStub(moduleName2) {\n const methodCache = {};\n let stub = null;\n stub = new Proxy({}, {\n get(_target, prop) {\n if (prop === \"__esModule\") return false;\n if (prop === \"default\") return stub;\n if (prop === Symbol.toStringTag) return \"Module\";\n if (prop === \"then\") return void 0;\n if (typeof prop !== \"string\") return void 0;\n if (!methodCache[prop]) {\n methodCache[prop] = function deferredApiStub() {\n throw _unsupportedApiError(moduleName2, prop);\n };\n }\n return methodCache[prop];\n }\n });\n return stub;\n }\n var __internalModuleCache = _moduleCache;\n var __require = function require2(moduleName2) {\n return _requireFrom(moduleName2, _currentModule.dirname);\n };\n __requireExposeCustomGlobal(\"require\", __require);\n function _resolveFrom(moduleName2, fromDir2) {\n const resolved2 = _resolveModule(moduleName2, fromDir2);\n if (resolved2 === null) {\n const err = new Error(\"Cannot find module '\" + moduleName2 + \"'\");\n err.code = \"MODULE_NOT_FOUND\";\n throw err;\n }\n return resolved2;\n }\n globalThis.require.resolve = function resolve(moduleName2) {\n return _resolveFrom(moduleName2, _currentModule.dirname);\n };\n function _debugRequire(phase, moduleName2, extra) {\n if (globalThis.__sandboxRequireDebug !== true) {\n return;\n }\n if (moduleName2 !== \"rivetkit\" && moduleName2 !== \"@rivetkit/traces\" && moduleName2 !== \"@rivetkit/on-change\" && moduleName2 !== \"async_hooks\" && !moduleName2.startsWith(\"rivetkit/\") && !moduleName2.startsWith(\"@rivetkit/\")) {\n return;\n }\n if (typeof console !== \"undefined\" && typeof console.log === \"function\") {\n console.log(\n \"[sandbox.require] \" + phase + \" \" + moduleName2 + (extra ? \" \" + extra : \"\")\n );\n }\n }\n function _requireFrom(moduleName, fromDir) {\n _debugRequire(\"start\", moduleName, fromDir);\n const name = moduleName.replace(/^node:/, \"\");\n let cacheKey = name;\n let resolved = null;\n const isRelative = name.startsWith(\"./\") || name.startsWith(\"../\");\n if (!isRelative && __internalModuleCache[name]) {\n _debugRequire(\"cache-hit\", name, name);\n return __internalModuleCache[name];\n }\n if (name === \"fs\") {\n if (__internalModuleCache[\"fs\"]) return __internalModuleCache[\"fs\"];\n const fsModule = globalThis.bridge?.fs || globalThis.bridge?.default || globalThis._fsModule || {};\n __internalModuleCache[\"fs\"] = fsModule;\n _debugRequire(\"loaded\", name, \"fs-special\");\n return fsModule;\n }\n if (name === \"fs/promises\") {\n if (__internalModuleCache[\"fs/promises\"]) return __internalModuleCache[\"fs/promises\"];\n const fsModule = _requireFrom(\"fs\", fromDir);\n __internalModuleCache[\"fs/promises\"] = fsModule.promises;\n _debugRequire(\"loaded\", name, \"fs-promises-special\");\n return fsModule.promises;\n }\n if (name === \"stream/promises\") {\n if (__internalModuleCache[\"stream/promises\"]) return __internalModuleCache[\"stream/promises\"];\n const streamModule = _requireFrom(\"stream\", fromDir);\n const promisesModule = {\n finished(stream, options) {\n return new Promise(function(resolve2, reject) {\n if (typeof streamModule.finished !== \"function\") {\n resolve2();\n return;\n }\n if (options && typeof options === \"object\" && !Array.isArray(options)) {\n streamModule.finished(stream, options, function(error) {\n if (error) {\n reject(error);\n return;\n }\n resolve2();\n });\n return;\n }\n streamModule.finished(stream, function(error) {\n if (error) {\n reject(error);\n return;\n }\n resolve2();\n });\n });\n },\n pipeline() {\n const args = Array.prototype.slice.call(arguments);\n return new Promise(function(resolve2, reject) {\n if (typeof streamModule.pipeline !== \"function\") {\n reject(new Error(\"stream.pipeline is not supported in sandbox\"));\n return;\n }\n args.push(function(error) {\n if (error) {\n reject(error);\n return;\n }\n resolve2();\n });\n streamModule.pipeline.apply(streamModule, args);\n });\n }\n };\n __internalModuleCache[\"stream/promises\"] = promisesModule;\n _debugRequire(\"loaded\", name, \"stream-promises-special\");\n return promisesModule;\n }\n if (name === \"child_process\") {\n if (__internalModuleCache[\"child_process\"]) return __internalModuleCache[\"child_process\"];\n __internalModuleCache[\"child_process\"] = _childProcessModule;\n _debugRequire(\"loaded\", name, \"child-process-special\");\n return _childProcessModule;\n }\n if (name === \"http\") {\n if (__internalModuleCache[\"http\"]) return __internalModuleCache[\"http\"];\n __internalModuleCache[\"http\"] = _httpModule;\n _debugRequire(\"loaded\", name, \"http-special\");\n return _httpModule;\n }\n if (name === \"https\") {\n if (__internalModuleCache[\"https\"]) return __internalModuleCache[\"https\"];\n __internalModuleCache[\"https\"] = _httpsModule;\n _debugRequire(\"loaded\", name, \"https-special\");\n return _httpsModule;\n }\n if (name === \"http2\") {\n if (__internalModuleCache[\"http2\"]) return __internalModuleCache[\"http2\"];\n __internalModuleCache[\"http2\"] = _http2Module;\n _debugRequire(\"loaded\", name, \"http2-special\");\n return _http2Module;\n }\n if (name === \"dns\") {\n if (__internalModuleCache[\"dns\"]) return __internalModuleCache[\"dns\"];\n __internalModuleCache[\"dns\"] = _dnsModule;\n _debugRequire(\"loaded\", name, \"dns-special\");\n return _dnsModule;\n }\n if (name === \"os\") {\n if (__internalModuleCache[\"os\"]) return __internalModuleCache[\"os\"];\n __internalModuleCache[\"os\"] = _osModule;\n _debugRequire(\"loaded\", name, \"os-special\");\n return _osModule;\n }\n if (name === \"module\") {\n if (__internalModuleCache[\"module\"]) return __internalModuleCache[\"module\"];\n __internalModuleCache[\"module\"] = _moduleModule;\n _debugRequire(\"loaded\", name, \"module-special\");\n return _moduleModule;\n }\n if (name === \"process\") {\n _debugRequire(\"loaded\", name, \"process-special\");\n return globalThis.process;\n }\n if (name === \"async_hooks\") {\n if (__internalModuleCache[\"async_hooks\"]) return __internalModuleCache[\"async_hooks\"];\n class AsyncLocalStorage {\n constructor() {\n this._store = void 0;\n }\n run(store, callback) {\n const previousStore = this._store;\n this._store = store;\n try {\n const args = Array.prototype.slice.call(arguments, 2);\n return callback.apply(void 0, args);\n } finally {\n this._store = previousStore;\n }\n }\n enterWith(store) {\n this._store = store;\n }\n getStore() {\n return this._store;\n }\n disable() {\n this._store = void 0;\n }\n exit(callback) {\n const previousStore = this._store;\n this._store = void 0;\n try {\n const args = Array.prototype.slice.call(arguments, 1);\n return callback.apply(void 0, args);\n } finally {\n this._store = previousStore;\n }\n }\n }\n class AsyncResource {\n constructor(type) {\n this.type = type;\n }\n runInAsyncScope(callback, thisArg) {\n const args = Array.prototype.slice.call(arguments, 2);\n return callback.apply(thisArg, args);\n }\n emitDestroy() {\n }\n }\n const asyncHooksModule = {\n AsyncLocalStorage,\n AsyncResource,\n createHook() {\n return {\n enable() {\n return this;\n },\n disable() {\n return this;\n }\n };\n },\n executionAsyncId() {\n return 1;\n },\n triggerAsyncId() {\n return 0;\n },\n executionAsyncResource() {\n return null;\n }\n };\n __internalModuleCache[\"async_hooks\"] = asyncHooksModule;\n _debugRequire(\"loaded\", name, \"async-hooks-special\");\n return asyncHooksModule;\n }\n if (name === \"diagnostics_channel\") {\n let _createChannel2 = function() {\n return {\n hasSubscribers: false,\n publish: function() {\n },\n subscribe: function() {\n },\n unsubscribe: function() {\n }\n };\n };\n var _createChannel = _createChannel2;\n if (__internalModuleCache[name]) return __internalModuleCache[name];\n const dcModule = {\n channel: function() {\n return _createChannel2();\n },\n hasSubscribers: function() {\n return false;\n },\n tracingChannel: function() {\n return {\n start: _createChannel2(),\n end: _createChannel2(),\n asyncStart: _createChannel2(),\n asyncEnd: _createChannel2(),\n error: _createChannel2()\n };\n },\n Channel: function Channel(name2) {\n this.hasSubscribers = false;\n this.publish = function() {\n };\n this.subscribe = function() {\n };\n this.unsubscribe = function() {\n };\n }\n };\n __internalModuleCache[name] = dcModule;\n _debugRequire(\"loaded\", name, \"diagnostics-channel-special\");\n return dcModule;\n }\n if (_deferredCoreModules.has(name)) {\n if (__internalModuleCache[name]) return __internalModuleCache[name];\n const deferredStub = _createDeferredModuleStub(name);\n __internalModuleCache[name] = deferredStub;\n _debugRequire(\"loaded\", name, \"deferred-stub\");\n return deferredStub;\n }\n if (_unsupportedCoreModules.has(name)) {\n throw new Error(name + \" is not supported in sandbox\");\n }\n const polyfillCode = _loadPolyfill(name);\n if (polyfillCode !== null) {\n if (__internalModuleCache[name]) return __internalModuleCache[name];\n const moduleObj = { exports: {} };\n _pendingModules[name] = moduleObj;\n let result = eval(polyfillCode);\n result = _patchPolyfill(name, result);\n if (typeof result === \"object\" && result !== null) {\n Object.assign(moduleObj.exports, result);\n } else {\n moduleObj.exports = result;\n }\n __internalModuleCache[name] = moduleObj.exports;\n delete _pendingModules[name];\n _debugRequire(\"loaded\", name, \"polyfill\");\n return __internalModuleCache[name];\n }\n resolved = _resolveFrom(name, fromDir);\n cacheKey = resolved;\n if (__internalModuleCache[cacheKey]) {\n _debugRequire(\"cache-hit\", name, cacheKey);\n return __internalModuleCache[cacheKey];\n }\n if (_pendingModules[cacheKey]) {\n _debugRequire(\"pending-hit\", name, cacheKey);\n return _pendingModules[cacheKey].exports;\n }\n const source = _loadFile(resolved);\n if (source === null) {\n const err = new Error(\"Cannot find module '\" + resolved + \"'\");\n err.code = \"MODULE_NOT_FOUND\";\n throw err;\n }\n if (resolved.endsWith(\".json\")) {\n const parsed = JSON.parse(source);\n __internalModuleCache[cacheKey] = parsed;\n return parsed;\n }\n const normalizedSource = typeof source === \"string\" ? source.replace(/import\\.meta\\.url/g, \"__filename\").replace(/fileURLToPath\\(__filename\\)/g, \"__filename\").replace(/url\\.fileURLToPath\\(__filename\\)/g, \"__filename\").replace(/fileURLToPath\\.call\\(void 0, __filename\\)/g, \"__filename\") : source;\n const module = {\n exports: {},\n filename: resolved,\n dirname: _dirname(resolved),\n id: resolved,\n loaded: false\n };\n _pendingModules[cacheKey] = module;\n const prevModule = _currentModule;\n _currentModule = module;\n try {\n let wrapper;\n try {\n wrapper = new Function(\n \"exports\",\n \"require\",\n \"module\",\n \"__filename\",\n \"__dirname\",\n \"__dynamicImport\",\n normalizedSource + \"\\n//# sourceURL=\" + resolved\n );\n } catch (error) {\n const details = error && error.stack ? error.stack : String(error);\n throw new Error(\"failed to compile module \" + resolved + \": \" + details);\n }\n const moduleRequire = function(request) {\n return _requireFrom(request, module.dirname);\n };\n moduleRequire.resolve = function(request) {\n return _resolveFrom(request, module.dirname);\n };\n const moduleDynamicImport = function(specifier) {\n if (typeof globalThis.__dynamicImport === \"function\") {\n return globalThis.__dynamicImport(specifier, module.dirname);\n }\n return Promise.reject(new Error(\"Dynamic import is not initialized\"));\n };\n wrapper(\n module.exports,\n moduleRequire,\n module,\n resolved,\n module.dirname,\n moduleDynamicImport\n );\n module.loaded = true;\n } catch (error) {\n const details = error && error.stack ? error.stack : String(error);\n throw new Error(\"failed to execute module \" + resolved + \": \" + details);\n } finally {\n _currentModule = prevModule;\n }\n __internalModuleCache[cacheKey] = module.exports;\n delete _pendingModules[cacheKey];\n _debugRequire(\"loaded\", name, cacheKey);\n return module.exports;\n }\n __requireExposeCustomGlobal(\"_requireFrom\", _requireFrom);\n var __moduleCacheProxy = new Proxy(__internalModuleCache, {\n get(target, prop, receiver) {\n return Reflect.get(target, prop, receiver);\n },\n set(_target, prop) {\n throw new TypeError(\"Cannot set require.cache['\" + String(prop) + \"']\");\n },\n deleteProperty(_target, prop) {\n throw new TypeError(\"Cannot delete require.cache['\" + String(prop) + \"']\");\n },\n defineProperty(_target, prop) {\n throw new TypeError(\"Cannot define property '\" + String(prop) + \"' on require.cache\");\n },\n has(target, prop) {\n return Reflect.has(target, prop);\n },\n ownKeys(target) {\n return Reflect.ownKeys(target);\n },\n getOwnPropertyDescriptor(target, prop) {\n return Reflect.getOwnPropertyDescriptor(target, prop);\n }\n });\n globalThis.require.cache = __moduleCacheProxy;\n Object.defineProperty(globalThis, \"_moduleCache\", {\n value: __moduleCacheProxy,\n writable: false,\n configurable: true,\n enumerable: false\n });\n if (typeof _moduleModule !== \"undefined\") {\n if (_moduleModule.Module) {\n _moduleModule.Module._cache = __moduleCacheProxy;\n }\n _moduleModule._cache = __moduleCacheProxy;\n }\n})();\n", + "requireSetup": "\"use strict\";\n(() => {\n // isolate-runtime/src/inject/require-setup.ts\n var __requireExposeCustomGlobal = typeof globalThis.__runtimeExposeCustomGlobal === \"function\" ? globalThis.__runtimeExposeCustomGlobal : function exposeCustomGlobal(name2, value) {\n Object.defineProperty(globalThis, name2, {\n value,\n writable: false,\n configurable: false,\n enumerable: true\n });\n };\n if (typeof globalThis.AbortController === \"undefined\" || typeof globalThis.AbortSignal === \"undefined\") {\n class AbortSignal {\n constructor() {\n this.aborted = false;\n this.reason = void 0;\n this.onabort = null;\n this._listeners = [];\n }\n addEventListener(type, listener) {\n if (type !== \"abort\" || typeof listener !== \"function\") return;\n this._listeners.push(listener);\n }\n removeEventListener(type, listener) {\n if (type !== \"abort\" || typeof listener !== \"function\") return;\n const index = this._listeners.indexOf(listener);\n if (index !== -1) {\n this._listeners.splice(index, 1);\n }\n }\n dispatchEvent(event) {\n if (!event || event.type !== \"abort\") return false;\n if (typeof this.onabort === \"function\") {\n try {\n this.onabort.call(this, event);\n } catch {\n }\n }\n const listeners = this._listeners.slice();\n for (const listener of listeners) {\n try {\n listener.call(this, event);\n } catch {\n }\n }\n return true;\n }\n }\n class AbortController {\n constructor() {\n this.signal = new AbortSignal();\n }\n abort(reason) {\n if (this.signal.aborted) return;\n this.signal.aborted = true;\n this.signal.reason = reason;\n this.signal.dispatchEvent({ type: \"abort\" });\n }\n }\n __requireExposeCustomGlobal(\"AbortSignal\", AbortSignal);\n __requireExposeCustomGlobal(\"AbortController\", AbortController);\n }\n if (typeof globalThis.structuredClone !== \"function\") {\n let structuredClonePolyfill = function(value) {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n if (value instanceof ArrayBuffer) {\n return value.slice(0);\n }\n if (ArrayBuffer.isView(value)) {\n if (value instanceof Uint8Array) {\n return new Uint8Array(value);\n }\n return new value.constructor(value);\n }\n return JSON.parse(JSON.stringify(value));\n };\n structuredClonePolyfill2 = structuredClonePolyfill;\n __requireExposeCustomGlobal(\"structuredClone\", structuredClonePolyfill);\n }\n var structuredClonePolyfill2;\n if (typeof globalThis.btoa !== \"function\") {\n __requireExposeCustomGlobal(\"btoa\", function btoa(input) {\n return Buffer.from(String(input), \"binary\").toString(\"base64\");\n });\n }\n if (typeof globalThis.atob !== \"function\") {\n __requireExposeCustomGlobal(\"atob\", function atob(input) {\n return Buffer.from(String(input), \"base64\").toString(\"binary\");\n });\n }\n function _dirname(p) {\n const lastSlash = p.lastIndexOf(\"/\");\n if (lastSlash === -1) return \".\";\n if (lastSlash === 0) return \"/\";\n return p.slice(0, lastSlash);\n }\n if (typeof globalThis.TextDecoder === \"function\") {\n _OrigTextDecoder = globalThis.TextDecoder;\n _utf8Aliases = {\n \"utf-8\": true,\n \"utf8\": true,\n \"unicode-1-1-utf-8\": true,\n \"ascii\": true,\n \"us-ascii\": true,\n \"iso-8859-1\": true,\n \"latin1\": true,\n \"binary\": true,\n \"windows-1252\": true,\n \"utf-16le\": true,\n \"utf-16\": true,\n \"ucs-2\": true,\n \"ucs2\": true\n };\n globalThis.TextDecoder = function TextDecoder(encoding, options) {\n var label = encoding !== void 0 ? String(encoding).toLowerCase().replace(/\\s/g, \"\") : \"utf-8\";\n if (_utf8Aliases[label]) {\n return new _OrigTextDecoder(\"utf-8\", options);\n }\n return new _OrigTextDecoder(encoding, options);\n };\n globalThis.TextDecoder.prototype = _OrigTextDecoder.prototype;\n }\n var _OrigTextDecoder;\n var _utf8Aliases;\n function _patchPolyfill(name2, result2) {\n if (typeof result2 !== \"object\" && typeof result2 !== \"function\" || result2 === null) {\n return result2;\n }\n if (name2 === \"buffer\") {\n const maxLength = typeof result2.kMaxLength === \"number\" ? result2.kMaxLength : 2147483647;\n const maxStringLength = typeof result2.kStringMaxLength === \"number\" ? result2.kStringMaxLength : 536870888;\n if (typeof result2.constants !== \"object\" || result2.constants === null) {\n result2.constants = {};\n }\n if (typeof result2.constants.MAX_LENGTH !== \"number\") {\n result2.constants.MAX_LENGTH = maxLength;\n }\n if (typeof result2.constants.MAX_STRING_LENGTH !== \"number\") {\n result2.constants.MAX_STRING_LENGTH = maxStringLength;\n }\n if (typeof result2.kMaxLength !== \"number\") {\n result2.kMaxLength = maxLength;\n }\n if (typeof result2.kStringMaxLength !== \"number\") {\n result2.kStringMaxLength = maxStringLength;\n }\n const BufferCtor = result2.Buffer;\n if ((typeof BufferCtor === \"function\" || typeof BufferCtor === \"object\") && BufferCtor !== null) {\n if (typeof BufferCtor.kMaxLength !== \"number\") {\n BufferCtor.kMaxLength = maxLength;\n }\n if (typeof BufferCtor.kStringMaxLength !== \"number\") {\n BufferCtor.kStringMaxLength = maxStringLength;\n }\n if (typeof BufferCtor.constants !== \"object\" || BufferCtor.constants === null) {\n BufferCtor.constants = result2.constants;\n }\n var bProto = BufferCtor.prototype;\n if (bProto) {\n var encs = [\"utf8\", \"ascii\", \"latin1\", \"binary\", \"hex\", \"base64\", \"ucs2\", \"utf16le\"];\n for (var ei = 0; ei < encs.length; ei++) {\n (function(e) {\n if (typeof bProto[e + \"Slice\"] !== \"function\") {\n bProto[e + \"Slice\"] = function(start, end) {\n return this.toString(e, start, end);\n };\n }\n if (typeof bProto[e + \"Write\"] !== \"function\") {\n bProto[e + \"Write\"] = function(str, offset, length) {\n return this.write(str, offset, length, e);\n };\n }\n })(encs[ei]);\n }\n }\n }\n return result2;\n }\n if (name2 === \"util\" && typeof result2.formatWithOptions === \"undefined\" && typeof result2.format === \"function\") {\n result2.formatWithOptions = function formatWithOptions(inspectOptions, ...args) {\n return result2.format.apply(null, args);\n };\n return result2;\n }\n if (name2 === \"url\") {\n const OriginalURL = result2.URL;\n if (typeof OriginalURL !== \"function\" || OriginalURL._patched) {\n return result2;\n }\n const PatchedURL = function PatchedURL2(url, base) {\n if (typeof url === \"string\" && url.startsWith(\"file:\") && !url.startsWith(\"file://\") && base === void 0) {\n if (typeof process !== \"undefined\" && typeof process.cwd === \"function\") {\n const cwd = process.cwd();\n if (cwd) {\n try {\n return new OriginalURL(url, \"file://\" + cwd + \"/\");\n } catch (e) {\n }\n }\n }\n }\n return base !== void 0 ? new OriginalURL(url, base) : new OriginalURL(url);\n };\n Object.keys(OriginalURL).forEach(function(key) {\n try {\n PatchedURL[key] = OriginalURL[key];\n } catch {\n }\n });\n Object.setPrototypeOf(PatchedURL, OriginalURL);\n PatchedURL.prototype = OriginalURL.prototype;\n PatchedURL._patched = true;\n const descriptor = Object.getOwnPropertyDescriptor(result2, \"URL\");\n if (descriptor && descriptor.configurable !== true && descriptor.writable !== true && typeof descriptor.set !== \"function\") {\n return result2;\n }\n try {\n result2.URL = PatchedURL;\n } catch {\n try {\n Object.defineProperty(result2, \"URL\", {\n value: PatchedURL,\n writable: true,\n configurable: true,\n enumerable: descriptor?.enumerable ?? true\n });\n } catch {\n }\n }\n return result2;\n }\n if (name2 === \"zlib\") {\n if (typeof result2.constants !== \"object\" || result2.constants === null) {\n var zlibConstants = {};\n var constKeys = Object.keys(result2);\n for (var ci = 0; ci < constKeys.length; ci++) {\n var ck = constKeys[ci];\n if (ck.indexOf(\"Z_\") === 0 && typeof result2[ck] === \"number\") {\n zlibConstants[ck] = result2[ck];\n }\n }\n if (typeof zlibConstants.DEFLATE !== \"number\") zlibConstants.DEFLATE = 1;\n if (typeof zlibConstants.INFLATE !== \"number\") zlibConstants.INFLATE = 2;\n if (typeof zlibConstants.GZIP !== \"number\") zlibConstants.GZIP = 3;\n if (typeof zlibConstants.DEFLATERAW !== \"number\") zlibConstants.DEFLATERAW = 4;\n if (typeof zlibConstants.INFLATERAW !== \"number\") zlibConstants.INFLATERAW = 5;\n if (typeof zlibConstants.UNZIP !== \"number\") zlibConstants.UNZIP = 6;\n if (typeof zlibConstants.GUNZIP !== \"number\") zlibConstants.GUNZIP = 7;\n result2.constants = zlibConstants;\n }\n return result2;\n }\n if (name2 === \"crypto\") {\n if (typeof _cryptoHashDigest !== \"undefined\") {\n let SandboxHash2 = function(algorithm) {\n this._algorithm = algorithm;\n this._chunks = [];\n };\n var SandboxHash = SandboxHash2;\n SandboxHash2.prototype.update = function update(data, inputEncoding) {\n if (typeof data === \"string\") {\n this._chunks.push(Buffer.from(data, inputEncoding || \"utf8\"));\n } else {\n this._chunks.push(Buffer.from(data));\n }\n return this;\n };\n SandboxHash2.prototype.digest = function digest(encoding) {\n var combined = Buffer.concat(this._chunks);\n var resultBase64 = _cryptoHashDigest.applySync(void 0, [\n this._algorithm,\n combined.toString(\"base64\")\n ]);\n var resultBuffer = Buffer.from(resultBase64, \"base64\");\n if (!encoding || encoding === \"buffer\") return resultBuffer;\n return resultBuffer.toString(encoding);\n };\n SandboxHash2.prototype.copy = function copy() {\n var c = new SandboxHash2(this._algorithm);\n c._chunks = this._chunks.slice();\n return c;\n };\n SandboxHash2.prototype.write = function write(data, encoding) {\n this.update(data, encoding);\n return true;\n };\n SandboxHash2.prototype.end = function end(data, encoding) {\n if (data) this.update(data, encoding);\n };\n result2.createHash = function createHash(algorithm) {\n return new SandboxHash2(algorithm);\n };\n result2.Hash = SandboxHash2;\n }\n if (typeof _cryptoHmacDigest !== \"undefined\") {\n let SandboxHmac2 = function(algorithm, key) {\n this._algorithm = algorithm;\n if (typeof key === \"string\") {\n this._key = Buffer.from(key, \"utf8\");\n } else if (key && typeof key === \"object\" && key._pem !== void 0) {\n this._key = Buffer.from(key._pem, \"utf8\");\n } else {\n this._key = Buffer.from(key);\n }\n this._chunks = [];\n };\n var SandboxHmac = SandboxHmac2;\n SandboxHmac2.prototype.update = function update(data, inputEncoding) {\n if (typeof data === \"string\") {\n this._chunks.push(Buffer.from(data, inputEncoding || \"utf8\"));\n } else {\n this._chunks.push(Buffer.from(data));\n }\n return this;\n };\n SandboxHmac2.prototype.digest = function digest(encoding) {\n var combined = Buffer.concat(this._chunks);\n var resultBase64 = _cryptoHmacDigest.applySync(void 0, [\n this._algorithm,\n this._key.toString(\"base64\"),\n combined.toString(\"base64\")\n ]);\n var resultBuffer = Buffer.from(resultBase64, \"base64\");\n if (!encoding || encoding === \"buffer\") return resultBuffer;\n return resultBuffer.toString(encoding);\n };\n SandboxHmac2.prototype.copy = function copy() {\n var c = new SandboxHmac2(this._algorithm, this._key);\n c._chunks = this._chunks.slice();\n return c;\n };\n SandboxHmac2.prototype.write = function write(data, encoding) {\n this.update(data, encoding);\n return true;\n };\n SandboxHmac2.prototype.end = function end(data, encoding) {\n if (data) this.update(data, encoding);\n };\n result2.createHmac = function createHmac(algorithm, key) {\n return new SandboxHmac2(algorithm, key);\n };\n result2.Hmac = SandboxHmac2;\n }\n if (typeof _cryptoRandomFill !== \"undefined\") {\n result2.randomBytes = function randomBytes(size, callback) {\n if (typeof size !== \"number\" || size < 0 || size !== (size | 0)) {\n var err = new TypeError('The \"size\" argument must be of type number. Received type ' + typeof size);\n if (typeof callback === \"function\") {\n callback(err);\n return;\n }\n throw err;\n }\n if (size > 2147483647) {\n var rangeErr = new RangeError('The value of \"size\" is out of range. It must be >= 0 && <= 2147483647. Received ' + size);\n if (typeof callback === \"function\") {\n callback(rangeErr);\n return;\n }\n throw rangeErr;\n }\n var buf = Buffer.alloc(size);\n var offset = 0;\n while (offset < size) {\n var chunk = Math.min(size - offset, 65536);\n var base64 = _cryptoRandomFill.applySync(void 0, [chunk]);\n var hostBytes = Buffer.from(base64, \"base64\");\n hostBytes.copy(buf, offset);\n offset += chunk;\n }\n if (typeof callback === \"function\") {\n callback(null, buf);\n return;\n }\n return buf;\n };\n result2.randomFillSync = function randomFillSync(buffer, offset, size) {\n if (offset === void 0) offset = 0;\n var byteLength = buffer.byteLength !== void 0 ? buffer.byteLength : buffer.length;\n if (size === void 0) size = byteLength - offset;\n if (offset < 0 || size < 0 || offset + size > byteLength) {\n throw new RangeError('The value of \"offset + size\" is out of range.');\n }\n var bytes = new Uint8Array(buffer.buffer || buffer, buffer.byteOffset ? buffer.byteOffset + offset : offset, size);\n var filled = 0;\n while (filled < size) {\n var chunk = Math.min(size - filled, 65536);\n var base64 = _cryptoRandomFill.applySync(void 0, [chunk]);\n var hostBytes = Buffer.from(base64, \"base64\");\n bytes.set(hostBytes, filled);\n filled += chunk;\n }\n return buffer;\n };\n result2.randomFill = function randomFill(buffer, offsetOrCb, sizeOrCb, callback) {\n var offset = 0;\n var size;\n var cb;\n if (typeof offsetOrCb === \"function\") {\n cb = offsetOrCb;\n } else if (typeof sizeOrCb === \"function\") {\n offset = offsetOrCb || 0;\n cb = sizeOrCb;\n } else {\n offset = offsetOrCb || 0;\n size = sizeOrCb;\n cb = callback;\n }\n if (typeof cb !== \"function\") {\n throw new TypeError(\"Callback must be a function\");\n }\n try {\n result2.randomFillSync(buffer, offset, size);\n cb(null, buffer);\n } catch (e) {\n cb(e);\n }\n };\n result2.randomInt = function randomInt(minOrMax, maxOrCb, callback) {\n var min, max, cb;\n if (typeof maxOrCb === \"function\" || maxOrCb === void 0) {\n min = 0;\n max = minOrMax;\n cb = maxOrCb;\n } else {\n min = minOrMax;\n max = maxOrCb;\n cb = callback;\n }\n if (!Number.isSafeInteger(min)) {\n var minErr = new TypeError('The \"min\" argument must be a safe integer');\n if (typeof cb === \"function\") {\n cb(minErr);\n return;\n }\n throw minErr;\n }\n if (!Number.isSafeInteger(max)) {\n var maxErr = new TypeError('The \"max\" argument must be a safe integer');\n if (typeof cb === \"function\") {\n cb(maxErr);\n return;\n }\n throw maxErr;\n }\n if (max <= min) {\n var rangeErr2 = new RangeError('The value of \"max\" is out of range. It must be greater than the value of \"min\" (' + min + \")\");\n if (typeof cb === \"function\") {\n cb(rangeErr2);\n return;\n }\n throw rangeErr2;\n }\n var range = max - min;\n var bytes = 6;\n var maxValid = Math.pow(2, 48) - Math.pow(2, 48) % range;\n var val;\n do {\n var base64 = _cryptoRandomFill.applySync(void 0, [bytes]);\n var buf = Buffer.from(base64, \"base64\");\n val = buf.readUIntBE(0, bytes);\n } while (val >= maxValid);\n var result22 = min + val % range;\n if (typeof cb === \"function\") {\n cb(null, result22);\n return;\n }\n return result22;\n };\n }\n if (typeof _cryptoPbkdf2 !== \"undefined\") {\n result2.pbkdf2Sync = function pbkdf2Sync(password, salt, iterations, keylen, digest) {\n var pwBuf = typeof password === \"string\" ? Buffer.from(password, \"utf8\") : Buffer.from(password);\n var saltBuf = typeof salt === \"string\" ? Buffer.from(salt, \"utf8\") : Buffer.from(salt);\n var resultBase64 = _cryptoPbkdf2.applySync(void 0, [\n pwBuf.toString(\"base64\"),\n saltBuf.toString(\"base64\"),\n iterations,\n keylen,\n digest\n ]);\n return Buffer.from(resultBase64, \"base64\");\n };\n result2.pbkdf2 = function pbkdf2(password, salt, iterations, keylen, digest, callback) {\n try {\n var derived = result2.pbkdf2Sync(password, salt, iterations, keylen, digest);\n callback(null, derived);\n } catch (e) {\n callback(e);\n }\n };\n }\n if (typeof _cryptoScrypt !== \"undefined\") {\n result2.scryptSync = function scryptSync(password, salt, keylen, options) {\n var pwBuf = typeof password === \"string\" ? Buffer.from(password, \"utf8\") : Buffer.from(password);\n var saltBuf = typeof salt === \"string\" ? Buffer.from(salt, \"utf8\") : Buffer.from(salt);\n var opts = {};\n if (options) {\n if (options.N !== void 0) opts.N = options.N;\n if (options.r !== void 0) opts.r = options.r;\n if (options.p !== void 0) opts.p = options.p;\n if (options.maxmem !== void 0) opts.maxmem = options.maxmem;\n if (options.cost !== void 0) opts.N = options.cost;\n if (options.blockSize !== void 0) opts.r = options.blockSize;\n if (options.parallelization !== void 0) opts.p = options.parallelization;\n }\n var resultBase64 = _cryptoScrypt.applySync(void 0, [\n pwBuf.toString(\"base64\"),\n saltBuf.toString(\"base64\"),\n keylen,\n JSON.stringify(opts)\n ]);\n return Buffer.from(resultBase64, \"base64\");\n };\n result2.scrypt = function scrypt(password, salt, keylen, optionsOrCb, callback) {\n var opts = optionsOrCb;\n var cb = callback;\n if (typeof optionsOrCb === \"function\") {\n opts = void 0;\n cb = optionsOrCb;\n }\n try {\n var derived = result2.scryptSync(password, salt, keylen, opts);\n cb(null, derived);\n } catch (e) {\n cb(e);\n }\n };\n }\n var _useStatefulCipher = typeof _cryptoCipherivCreate !== \"undefined\";\n if (typeof _cryptoCipheriv !== \"undefined\" || _useStatefulCipher) {\n let SandboxCipher2 = function(algorithm, key, iv) {\n this._algorithm = algorithm;\n this._key = typeof key === \"string\" ? Buffer.from(key, \"utf8\") : Buffer.from(key);\n this._iv = typeof iv === \"string\" ? Buffer.from(iv, \"utf8\") : Buffer.from(iv);\n this._authTag = null;\n this._finalized = false;\n if (_useStatefulCipher) {\n this._sessionId = _cryptoCipherivCreate.applySync(void 0, [\n \"cipher\",\n algorithm,\n this._key.toString(\"base64\"),\n this._iv.toString(\"base64\")\n ]);\n } else {\n this._sessionId = -1;\n this._chunks = [];\n }\n };\n var SandboxCipher = SandboxCipher2;\n SandboxCipher2.prototype.update = function update(data, inputEncoding, outputEncoding) {\n var buf;\n if (typeof data === \"string\") {\n buf = Buffer.from(data, inputEncoding || \"utf8\");\n } else {\n buf = Buffer.from(data);\n }\n if (this._sessionId >= 0) {\n var resultBase64 = _cryptoCipherivUpdate.applySync(void 0, [this._sessionId, buf.toString(\"base64\")]);\n var resultBuffer = Buffer.from(resultBase64, \"base64\");\n if (outputEncoding && outputEncoding !== \"buffer\") return resultBuffer.toString(outputEncoding);\n return resultBuffer;\n }\n this._chunks.push(buf);\n if (outputEncoding && outputEncoding !== \"buffer\") return \"\";\n return Buffer.alloc(0);\n };\n SandboxCipher2.prototype.final = function final(outputEncoding) {\n if (this._finalized) throw new Error(\"Attempting to call final() after already finalized\");\n this._finalized = true;\n if (this._sessionId >= 0) {\n var resultJson = _cryptoCipherivFinal.applySync(void 0, [this._sessionId]);\n var parsed = JSON.parse(resultJson);\n if (parsed.authTag) this._authTag = Buffer.from(parsed.authTag, \"base64\");\n var resultBuffer = Buffer.from(parsed.data, \"base64\");\n if (outputEncoding && outputEncoding !== \"buffer\") return resultBuffer.toString(outputEncoding);\n return resultBuffer;\n }\n var combined = Buffer.concat(this._chunks);\n var resultJson2 = _cryptoCipheriv.applySync(void 0, [\n this._algorithm,\n this._key.toString(\"base64\"),\n this._iv.toString(\"base64\"),\n combined.toString(\"base64\")\n ]);\n var parsed2 = JSON.parse(resultJson2);\n if (parsed2.authTag) this._authTag = Buffer.from(parsed2.authTag, \"base64\");\n var resultBuffer2 = Buffer.from(parsed2.data, \"base64\");\n if (outputEncoding && outputEncoding !== \"buffer\") return resultBuffer2.toString(outputEncoding);\n return resultBuffer2;\n };\n SandboxCipher2.prototype.getAuthTag = function getAuthTag() {\n if (!this._finalized) throw new Error(\"Cannot call getAuthTag before final()\");\n if (!this._authTag) throw new Error(\"Auth tag is only available for GCM ciphers\");\n return this._authTag;\n };\n SandboxCipher2.prototype.setAAD = function setAAD(data) {\n if (this._sessionId >= 0) {\n var buf = typeof data === \"string\" ? Buffer.from(data, \"utf8\") : Buffer.from(data);\n _cryptoCipherivUpdate.applySync(void 0, [this._sessionId, \"\", JSON.stringify({ setAAD: buf.toString(\"base64\") })]);\n }\n return this;\n };\n SandboxCipher2.prototype.setAutoPadding = function setAutoPadding(autoPadding) {\n if (this._sessionId >= 0) {\n _cryptoCipherivUpdate.applySync(void 0, [this._sessionId, \"\", JSON.stringify({ setAutoPadding: autoPadding !== false })]);\n }\n return this;\n };\n result2.createCipheriv = function createCipheriv(algorithm, key, iv) {\n return new SandboxCipher2(algorithm, key, iv);\n };\n result2.Cipheriv = SandboxCipher2;\n }\n if (typeof _cryptoDecipheriv !== \"undefined\" || _useStatefulCipher) {\n let SandboxDecipher2 = function(algorithm, key, iv) {\n this._algorithm = algorithm;\n this._key = typeof key === \"string\" ? Buffer.from(key, \"utf8\") : Buffer.from(key);\n this._iv = typeof iv === \"string\" ? Buffer.from(iv, \"utf8\") : Buffer.from(iv);\n this._authTag = null;\n this._finalized = false;\n if (_useStatefulCipher) {\n this._sessionId = _cryptoCipherivCreate.applySync(void 0, [\n \"decipher\",\n algorithm,\n this._key.toString(\"base64\"),\n this._iv.toString(\"base64\")\n ]);\n } else {\n this._sessionId = -1;\n this._chunks = [];\n }\n };\n var SandboxDecipher = SandboxDecipher2;\n SandboxDecipher2.prototype.update = function update(data, inputEncoding, outputEncoding) {\n var buf;\n if (typeof data === \"string\") {\n buf = Buffer.from(data, inputEncoding || \"utf8\");\n } else {\n buf = Buffer.from(data);\n }\n if (this._sessionId >= 0) {\n var resultBase64 = _cryptoCipherivUpdate.applySync(void 0, [this._sessionId, buf.toString(\"base64\")]);\n var resultBuffer = Buffer.from(resultBase64, \"base64\");\n if (outputEncoding && outputEncoding !== \"buffer\") return resultBuffer.toString(outputEncoding);\n return resultBuffer;\n }\n this._chunks.push(buf);\n if (outputEncoding && outputEncoding !== \"buffer\") return \"\";\n return Buffer.alloc(0);\n };\n SandboxDecipher2.prototype.final = function final(outputEncoding) {\n if (this._finalized) throw new Error(\"Attempting to call final() after already finalized\");\n this._finalized = true;\n if (this._sessionId >= 0) {\n if (this._authTag) {\n _cryptoCipherivUpdate.applySync(void 0, [this._sessionId, \"\", JSON.stringify({ setAuthTag: this._authTag.toString(\"base64\") })]);\n }\n var resultJson = _cryptoCipherivFinal.applySync(void 0, [this._sessionId]);\n var parsed = JSON.parse(resultJson);\n var resultBuffer = Buffer.from(parsed.data, \"base64\");\n if (outputEncoding && outputEncoding !== \"buffer\") return resultBuffer.toString(outputEncoding);\n return resultBuffer;\n }\n var combined = Buffer.concat(this._chunks);\n var options = {};\n if (this._authTag) options.authTag = this._authTag.toString(\"base64\");\n var resultBase64 = _cryptoDecipheriv.applySync(void 0, [\n this._algorithm,\n this._key.toString(\"base64\"),\n this._iv.toString(\"base64\"),\n combined.toString(\"base64\"),\n JSON.stringify(options)\n ]);\n var resultBuffer2 = Buffer.from(resultBase64, \"base64\");\n if (outputEncoding && outputEncoding !== \"buffer\") return resultBuffer2.toString(outputEncoding);\n return resultBuffer2;\n };\n SandboxDecipher2.prototype.setAuthTag = function setAuthTag(tag) {\n this._authTag = typeof tag === \"string\" ? Buffer.from(tag, \"base64\") : Buffer.from(tag);\n return this;\n };\n SandboxDecipher2.prototype.setAAD = function setAAD(data) {\n if (this._sessionId >= 0) {\n var buf = typeof data === \"string\" ? Buffer.from(data, \"utf8\") : Buffer.from(data);\n _cryptoCipherivUpdate.applySync(void 0, [this._sessionId, \"\", JSON.stringify({ setAAD: buf.toString(\"base64\") })]);\n }\n return this;\n };\n SandboxDecipher2.prototype.setAutoPadding = function setAutoPadding(autoPadding) {\n if (this._sessionId >= 0) {\n _cryptoCipherivUpdate.applySync(void 0, [this._sessionId, \"\", JSON.stringify({ setAutoPadding: autoPadding !== false })]);\n }\n return this;\n };\n result2.createDecipheriv = function createDecipheriv(algorithm, key, iv) {\n return new SandboxDecipher2(algorithm, key, iv);\n };\n result2.Decipheriv = SandboxDecipher2;\n }\n if (typeof _cryptoSign !== \"undefined\") {\n result2.sign = function sign(algorithm, data, key) {\n var dataBuf = typeof data === \"string\" ? Buffer.from(data, \"utf8\") : Buffer.from(data);\n var keyPem;\n if (typeof key === \"string\") {\n keyPem = key;\n } else if (key && typeof key === \"object\" && key._pem) {\n keyPem = key._pem;\n } else if (Buffer.isBuffer(key)) {\n keyPem = key.toString(\"utf8\");\n } else {\n keyPem = String(key);\n }\n var sigBase64 = _cryptoSign.applySync(void 0, [\n algorithm,\n dataBuf.toString(\"base64\"),\n keyPem\n ]);\n return Buffer.from(sigBase64, \"base64\");\n };\n }\n if (typeof _cryptoVerify !== \"undefined\") {\n result2.verify = function verify(algorithm, data, key, signature) {\n var dataBuf = typeof data === \"string\" ? Buffer.from(data, \"utf8\") : Buffer.from(data);\n var keyPem;\n if (typeof key === \"string\") {\n keyPem = key;\n } else if (key && typeof key === \"object\" && key._pem) {\n keyPem = key._pem;\n } else if (Buffer.isBuffer(key)) {\n keyPem = key.toString(\"utf8\");\n } else {\n keyPem = String(key);\n }\n var sigBuf = typeof signature === \"string\" ? Buffer.from(signature, \"base64\") : Buffer.from(signature);\n return _cryptoVerify.applySync(void 0, [\n algorithm,\n dataBuf.toString(\"base64\"),\n keyPem,\n sigBuf.toString(\"base64\")\n ]);\n };\n }\n if (typeof _cryptoGenerateKeyPairSync !== \"undefined\") {\n let SandboxKeyObject2 = function(type, pem) {\n this.type = type;\n this._pem = pem;\n };\n var SandboxKeyObject = SandboxKeyObject2;\n SandboxKeyObject2.prototype.export = function exportKey(options) {\n if (!options || options.format === \"pem\") {\n return this._pem;\n }\n if (options.format === \"der\") {\n var lines = this._pem.split(\"\\n\").filter(function(l) {\n return l && l.indexOf(\"-----\") !== 0;\n });\n return Buffer.from(lines.join(\"\"), \"base64\");\n }\n return this._pem;\n };\n SandboxKeyObject2.prototype.toString = function() {\n return this._pem;\n };\n result2.generateKeyPairSync = function generateKeyPairSync(type, options) {\n var opts = {};\n if (options) {\n if (options.modulusLength !== void 0) opts.modulusLength = options.modulusLength;\n if (options.publicExponent !== void 0) opts.publicExponent = options.publicExponent;\n if (options.namedCurve !== void 0) opts.namedCurve = options.namedCurve;\n if (options.divisorLength !== void 0) opts.divisorLength = options.divisorLength;\n if (options.primeLength !== void 0) opts.primeLength = options.primeLength;\n }\n var resultJson = _cryptoGenerateKeyPairSync.applySync(void 0, [\n type,\n JSON.stringify(opts)\n ]);\n var parsed = JSON.parse(resultJson);\n if (options && options.publicKeyEncoding && options.privateKeyEncoding) {\n return { publicKey: parsed.publicKey, privateKey: parsed.privateKey };\n }\n return {\n publicKey: new SandboxKeyObject2(\"public\", parsed.publicKey),\n privateKey: new SandboxKeyObject2(\"private\", parsed.privateKey)\n };\n };\n result2.generateKeyPair = function generateKeyPair(type, options, callback) {\n try {\n var pair = result2.generateKeyPairSync(type, options);\n callback(null, pair.publicKey, pair.privateKey);\n } catch (e) {\n callback(e);\n }\n };\n result2.createPublicKey = function createPublicKey(key) {\n if (typeof key === \"string\") {\n if (key.indexOf(\"-----BEGIN\") === -1) {\n throw new TypeError(\"error:0900006e:PEM routines:OPENSSL_internal:NO_START_LINE\");\n }\n return new SandboxKeyObject2(\"public\", key);\n }\n if (key && typeof key === \"object\" && key._pem) {\n return new SandboxKeyObject2(\"public\", key._pem);\n }\n if (key && typeof key === \"object\" && key.type === \"private\") {\n return new SandboxKeyObject2(\"public\", key._pem);\n }\n if (key && typeof key === \"object\" && key.key) {\n var keyData = typeof key.key === \"string\" ? key.key : key.key.toString(\"utf8\");\n return new SandboxKeyObject2(\"public\", keyData);\n }\n if (Buffer.isBuffer(key)) {\n var keyStr = key.toString(\"utf8\");\n if (keyStr.indexOf(\"-----BEGIN\") === -1) {\n throw new TypeError(\"error:0900006e:PEM routines:OPENSSL_internal:NO_START_LINE\");\n }\n return new SandboxKeyObject2(\"public\", keyStr);\n }\n return new SandboxKeyObject2(\"public\", String(key));\n };\n result2.createPrivateKey = function createPrivateKey(key) {\n if (typeof key === \"string\") {\n if (key.indexOf(\"-----BEGIN\") === -1) {\n throw new TypeError(\"error:0900006e:PEM routines:OPENSSL_internal:NO_START_LINE\");\n }\n return new SandboxKeyObject2(\"private\", key);\n }\n if (key && typeof key === \"object\" && key._pem) {\n return new SandboxKeyObject2(\"private\", key._pem);\n }\n if (key && typeof key === \"object\" && key.key) {\n var keyData = typeof key.key === \"string\" ? key.key : key.key.toString(\"utf8\");\n return new SandboxKeyObject2(\"private\", keyData);\n }\n if (Buffer.isBuffer(key)) {\n var keyStr = key.toString(\"utf8\");\n if (keyStr.indexOf(\"-----BEGIN\") === -1) {\n throw new TypeError(\"error:0900006e:PEM routines:OPENSSL_internal:NO_START_LINE\");\n }\n return new SandboxKeyObject2(\"private\", keyStr);\n }\n return new SandboxKeyObject2(\"private\", String(key));\n };\n result2.createSecretKey = function createSecretKey(key) {\n if (typeof key === \"string\") {\n return new SandboxKeyObject2(\"secret\", key);\n }\n if (Buffer.isBuffer(key) || key instanceof Uint8Array) {\n return new SandboxKeyObject2(\"secret\", Buffer.from(key).toString(\"utf8\"));\n }\n return new SandboxKeyObject2(\"secret\", String(key));\n };\n result2.KeyObject = SandboxKeyObject2;\n }\n if (typeof _cryptoSubtle !== \"undefined\") {\n let SandboxCryptoKey2 = function(keyData) {\n this.type = keyData.type;\n this.extractable = keyData.extractable;\n this.algorithm = keyData.algorithm;\n this.usages = keyData.usages;\n this._keyData = keyData;\n }, toBase642 = function(data) {\n if (typeof data === \"string\") return Buffer.from(data).toString(\"base64\");\n if (data instanceof ArrayBuffer) return Buffer.from(new Uint8Array(data)).toString(\"base64\");\n if (ArrayBuffer.isView(data)) return Buffer.from(new Uint8Array(data.buffer, data.byteOffset, data.byteLength)).toString(\"base64\");\n return Buffer.from(data).toString(\"base64\");\n }, subtleCall2 = function(reqObj) {\n return _cryptoSubtle.applySync(void 0, [JSON.stringify(reqObj)]);\n }, normalizeAlgo2 = function(algorithm) {\n if (typeof algorithm === \"string\") return { name: algorithm };\n return algorithm;\n };\n var SandboxCryptoKey = SandboxCryptoKey2, toBase64 = toBase642, subtleCall = subtleCall2, normalizeAlgo = normalizeAlgo2;\n var SandboxSubtle = {};\n SandboxSubtle.digest = function digest(algorithm, data) {\n return Promise.resolve().then(function() {\n var algo = normalizeAlgo2(algorithm);\n var result22 = JSON.parse(subtleCall2({\n op: \"digest\",\n algorithm: algo.name,\n data: toBase642(data)\n }));\n var buf = Buffer.from(result22.data, \"base64\");\n return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);\n });\n };\n SandboxSubtle.generateKey = function generateKey(algorithm, extractable, keyUsages) {\n return Promise.resolve().then(function() {\n var algo = normalizeAlgo2(algorithm);\n var reqAlgo = Object.assign({}, algo);\n if (reqAlgo.hash) reqAlgo.hash = normalizeAlgo2(reqAlgo.hash);\n if (reqAlgo.publicExponent) {\n reqAlgo.publicExponent = Buffer.from(new Uint8Array(reqAlgo.publicExponent.buffer || reqAlgo.publicExponent)).toString(\"base64\");\n }\n var result22 = JSON.parse(subtleCall2({\n op: \"generateKey\",\n algorithm: reqAlgo,\n extractable,\n usages: Array.from(keyUsages)\n }));\n if (result22.publicKey && result22.privateKey) {\n return {\n publicKey: new SandboxCryptoKey2(result22.publicKey),\n privateKey: new SandboxCryptoKey2(result22.privateKey)\n };\n }\n return new SandboxCryptoKey2(result22.key);\n });\n };\n SandboxSubtle.importKey = function importKey(format, keyData, algorithm, extractable, keyUsages) {\n return Promise.resolve().then(function() {\n var algo = normalizeAlgo2(algorithm);\n var reqAlgo = Object.assign({}, algo);\n if (reqAlgo.hash) reqAlgo.hash = normalizeAlgo2(reqAlgo.hash);\n var serializedKeyData;\n if (format === \"jwk\") {\n serializedKeyData = keyData;\n } else if (format === \"raw\") {\n serializedKeyData = toBase642(keyData);\n } else {\n serializedKeyData = toBase642(keyData);\n }\n var result22 = JSON.parse(subtleCall2({\n op: \"importKey\",\n format,\n keyData: serializedKeyData,\n algorithm: reqAlgo,\n extractable,\n usages: Array.from(keyUsages)\n }));\n return new SandboxCryptoKey2(result22.key);\n });\n };\n SandboxSubtle.exportKey = function exportKey(format, key) {\n return Promise.resolve().then(function() {\n var result22 = JSON.parse(subtleCall2({\n op: \"exportKey\",\n format,\n key: key._keyData\n }));\n if (format === \"jwk\") return result22.jwk;\n var buf = Buffer.from(result22.data, \"base64\");\n return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);\n });\n };\n SandboxSubtle.encrypt = function encrypt(algorithm, key, data) {\n return Promise.resolve().then(function() {\n var algo = normalizeAlgo2(algorithm);\n var reqAlgo = Object.assign({}, algo);\n if (reqAlgo.iv) reqAlgo.iv = toBase642(reqAlgo.iv);\n if (reqAlgo.additionalData) reqAlgo.additionalData = toBase642(reqAlgo.additionalData);\n var result22 = JSON.parse(subtleCall2({\n op: \"encrypt\",\n algorithm: reqAlgo,\n key: key._keyData,\n data: toBase642(data)\n }));\n var buf = Buffer.from(result22.data, \"base64\");\n return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);\n });\n };\n SandboxSubtle.decrypt = function decrypt(algorithm, key, data) {\n return Promise.resolve().then(function() {\n var algo = normalizeAlgo2(algorithm);\n var reqAlgo = Object.assign({}, algo);\n if (reqAlgo.iv) reqAlgo.iv = toBase642(reqAlgo.iv);\n if (reqAlgo.additionalData) reqAlgo.additionalData = toBase642(reqAlgo.additionalData);\n var result22 = JSON.parse(subtleCall2({\n op: \"decrypt\",\n algorithm: reqAlgo,\n key: key._keyData,\n data: toBase642(data)\n }));\n var buf = Buffer.from(result22.data, \"base64\");\n return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);\n });\n };\n SandboxSubtle.deriveBits = function deriveBits(algorithm, baseKey, length) {\n return Promise.resolve().then(function() {\n var algo = normalizeAlgo2(algorithm);\n var reqAlgo = Object.assign({}, algo);\n if (reqAlgo.hash) reqAlgo.hash = normalizeAlgo2(reqAlgo.hash);\n if (reqAlgo.salt) reqAlgo.salt = toBase642(reqAlgo.salt);\n var result22 = JSON.parse(subtleCall2({\n op: \"deriveBits\",\n algorithm: reqAlgo,\n key: baseKey._keyData,\n length\n }));\n var buf = Buffer.from(result22.data, \"base64\");\n return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);\n });\n };\n SandboxSubtle.sign = function sign(algorithm, key, data) {\n return Promise.resolve().then(function() {\n var result22 = JSON.parse(subtleCall2({\n op: \"sign\",\n algorithm: normalizeAlgo2(algorithm),\n key: key._keyData,\n data: toBase642(data)\n }));\n var buf = Buffer.from(result22.data, \"base64\");\n return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);\n });\n };\n SandboxSubtle.verify = function verify(algorithm, key, signature, data) {\n return Promise.resolve().then(function() {\n var result22 = JSON.parse(subtleCall2({\n op: \"verify\",\n algorithm: normalizeAlgo2(algorithm),\n key: key._keyData,\n signature: toBase642(signature),\n data: toBase642(data)\n }));\n return result22.result;\n });\n };\n result2.subtle = SandboxSubtle;\n result2.webcrypto = { subtle: SandboxSubtle, getRandomValues: result2.randomFillSync };\n }\n if (typeof result2.getCurves !== \"function\") {\n result2.getCurves = function getCurves() {\n return [\n \"prime256v1\",\n \"secp256r1\",\n \"secp384r1\",\n \"secp521r1\",\n \"secp256k1\",\n \"secp224r1\",\n \"secp192k1\"\n ];\n };\n }\n if (typeof result2.getCiphers !== \"function\") {\n result2.getCiphers = function getCiphers() {\n return [\n \"aes-128-cbc\",\n \"aes-128-gcm\",\n \"aes-192-cbc\",\n \"aes-192-gcm\",\n \"aes-256-cbc\",\n \"aes-256-gcm\",\n \"aes-128-ctr\",\n \"aes-192-ctr\",\n \"aes-256-ctr\"\n ];\n };\n }\n if (typeof result2.getHashes !== \"function\") {\n result2.getHashes = function getHashes() {\n return [\"md5\", \"sha1\", \"sha256\", \"sha384\", \"sha512\"];\n };\n }\n if (typeof result2.timingSafeEqual !== \"function\") {\n result2.timingSafeEqual = function timingSafeEqual(a, b) {\n if (a.length !== b.length) {\n throw new RangeError(\"Input buffers must have the same byte length\");\n }\n var out = 0;\n for (var i = 0; i < a.length; i++) {\n out |= a[i] ^ b[i];\n }\n return out === 0;\n };\n }\n return result2;\n }\n if (name2 === \"stream\") {\n if (typeof result2 === \"function\" && result2.prototype && typeof result2.Readable === \"function\") {\n var readableProto = result2.Readable.prototype;\n var streamProto = result2.prototype;\n if (readableProto && streamProto && !(readableProto instanceof result2)) {\n var currentParent = Object.getPrototypeOf(readableProto);\n Object.setPrototypeOf(streamProto, currentParent);\n Object.setPrototypeOf(readableProto, streamProto);\n }\n }\n return result2;\n }\n if (name2 === \"path\") {\n if (result2.win32 === null || result2.win32 === void 0) {\n result2.win32 = result2.posix || result2;\n }\n if (result2.posix === null || result2.posix === void 0) {\n result2.posix = result2;\n }\n const hasAbsoluteSegment = function(args) {\n return args.some(function(arg) {\n return typeof arg === \"string\" && arg.length > 0 && arg.charAt(0) === \"/\";\n });\n };\n const prependCwd = function(args) {\n if (hasAbsoluteSegment(args)) return;\n if (typeof process !== \"undefined\" && typeof process.cwd === \"function\") {\n const cwd = process.cwd();\n if (cwd && cwd.charAt(0) === \"/\") {\n args.unshift(cwd);\n }\n }\n };\n const originalResolve = result2.resolve;\n if (typeof originalResolve === \"function\" && !originalResolve._patchedForCwd) {\n const patchedResolve = function resolve2() {\n const args = Array.from(arguments);\n prependCwd(args);\n return originalResolve.apply(this, args);\n };\n patchedResolve._patchedForCwd = true;\n result2.resolve = patchedResolve;\n }\n if (result2.posix && typeof result2.posix.resolve === \"function\" && !result2.posix.resolve._patchedForCwd) {\n const originalPosixResolve = result2.posix.resolve;\n const patchedPosixResolve = function resolve2() {\n const args = Array.from(arguments);\n prependCwd(args);\n return originalPosixResolve.apply(this, args);\n };\n patchedPosixResolve._patchedForCwd = true;\n result2.posix.resolve = patchedPosixResolve;\n }\n }\n return result2;\n }\n var _deferredCoreModules = /* @__PURE__ */ new Set([\n \"tls\",\n \"readline\",\n \"perf_hooks\",\n \"async_hooks\",\n \"worker_threads\",\n \"diagnostics_channel\"\n ]);\n var _unsupportedCoreModules = /* @__PURE__ */ new Set([\n \"dgram\",\n \"cluster\",\n \"wasi\",\n \"inspector\",\n \"repl\",\n \"trace_events\",\n \"domain\"\n ]);\n function _unsupportedApiError(moduleName2, apiName) {\n return new Error(moduleName2 + \".\" + apiName + \" is not supported in sandbox\");\n }\n function _createDeferredModuleStub(moduleName2) {\n const methodCache = {};\n let stub = null;\n stub = new Proxy({}, {\n get(_target, prop) {\n if (prop === \"__esModule\") return false;\n if (prop === \"default\") return stub;\n if (prop === Symbol.toStringTag) return \"Module\";\n if (prop === \"then\") return void 0;\n if (typeof prop !== \"string\") return void 0;\n if (!methodCache[prop]) {\n methodCache[prop] = function deferredApiStub() {\n throw _unsupportedApiError(moduleName2, prop);\n };\n }\n return methodCache[prop];\n }\n });\n return stub;\n }\n var __internalModuleCache = _moduleCache;\n var __require = function require2(moduleName2) {\n return _requireFrom(moduleName2, _currentModule.dirname);\n };\n __requireExposeCustomGlobal(\"require\", __require);\n var _resolveCache = /* @__PURE__ */ Object.create(null);\n function _resolveFrom(moduleName2, fromDir2) {\n const cacheKey2 = fromDir2 + \"\\0\" + moduleName2;\n if (cacheKey2 in _resolveCache) {\n const cached = _resolveCache[cacheKey2];\n if (cached === null) {\n const err = new Error(\"Cannot find module '\" + moduleName2 + \"'\");\n err.code = \"MODULE_NOT_FOUND\";\n throw err;\n }\n return cached;\n }\n let resolved2;\n if (typeof _resolveModuleSync !== \"undefined\") {\n resolved2 = _resolveModuleSync.applySync(void 0, [moduleName2, fromDir2]);\n } else {\n resolved2 = _resolveModule.applySyncPromise(void 0, [moduleName2, fromDir2]);\n }\n _resolveCache[cacheKey2] = resolved2;\n if (resolved2 === null) {\n const err = new Error(\"Cannot find module '\" + moduleName2 + \"'\");\n err.code = \"MODULE_NOT_FOUND\";\n throw err;\n }\n return resolved2;\n }\n globalThis.require.resolve = function resolve(moduleName2) {\n return _resolveFrom(moduleName2, _currentModule.dirname);\n };\n function _debugRequire(phase, moduleName2, extra) {\n if (globalThis.__sandboxRequireDebug !== true) {\n return;\n }\n if (moduleName2 !== \"rivetkit\" && moduleName2 !== \"@rivetkit/traces\" && moduleName2 !== \"@rivetkit/on-change\" && moduleName2 !== \"async_hooks\" && !moduleName2.startsWith(\"rivetkit/\") && !moduleName2.startsWith(\"@rivetkit/\")) {\n return;\n }\n if (typeof console !== \"undefined\" && typeof console.log === \"function\") {\n console.log(\n \"[sandbox.require] \" + phase + \" \" + moduleName2 + (extra ? \" \" + extra : \"\")\n );\n }\n }\n function _requireFrom(moduleName, fromDir) {\n _debugRequire(\"start\", moduleName, fromDir);\n const name = moduleName.replace(/^node:/, \"\");\n let cacheKey = name;\n let resolved = null;\n const isRelative = name.startsWith(\"./\") || name.startsWith(\"../\");\n if (!isRelative && __internalModuleCache[name]) {\n _debugRequire(\"cache-hit\", name, name);\n return __internalModuleCache[name];\n }\n if (name === \"fs\") {\n if (__internalModuleCache[\"fs\"]) return __internalModuleCache[\"fs\"];\n const fsModule = globalThis.bridge?.fs || globalThis.bridge?.default || globalThis._fsModule || {};\n __internalModuleCache[\"fs\"] = fsModule;\n _debugRequire(\"loaded\", name, \"fs-special\");\n return fsModule;\n }\n if (name === \"fs/promises\") {\n if (__internalModuleCache[\"fs/promises\"]) return __internalModuleCache[\"fs/promises\"];\n const fsModule = _requireFrom(\"fs\", fromDir);\n __internalModuleCache[\"fs/promises\"] = fsModule.promises;\n _debugRequire(\"loaded\", name, \"fs-promises-special\");\n return fsModule.promises;\n }\n if (name === \"stream/promises\") {\n if (__internalModuleCache[\"stream/promises\"]) return __internalModuleCache[\"stream/promises\"];\n const streamModule = _requireFrom(\"stream\", fromDir);\n const promisesModule = {\n finished(stream, options) {\n return new Promise(function(resolve2, reject) {\n if (typeof streamModule.finished !== \"function\") {\n resolve2();\n return;\n }\n if (options && typeof options === \"object\" && !Array.isArray(options)) {\n streamModule.finished(stream, options, function(error) {\n if (error) {\n reject(error);\n return;\n }\n resolve2();\n });\n return;\n }\n streamModule.finished(stream, function(error) {\n if (error) {\n reject(error);\n return;\n }\n resolve2();\n });\n });\n },\n pipeline() {\n const args = Array.prototype.slice.call(arguments);\n return new Promise(function(resolve2, reject) {\n if (typeof streamModule.pipeline !== \"function\") {\n reject(new Error(\"stream.pipeline is not supported in sandbox\"));\n return;\n }\n args.push(function(error) {\n if (error) {\n reject(error);\n return;\n }\n resolve2();\n });\n streamModule.pipeline.apply(streamModule, args);\n });\n }\n };\n __internalModuleCache[\"stream/promises\"] = promisesModule;\n _debugRequire(\"loaded\", name, \"stream-promises-special\");\n return promisesModule;\n }\n if (name === \"child_process\") {\n if (__internalModuleCache[\"child_process\"]) return __internalModuleCache[\"child_process\"];\n __internalModuleCache[\"child_process\"] = _childProcessModule;\n _debugRequire(\"loaded\", name, \"child-process-special\");\n return _childProcessModule;\n }\n if (name === \"http\") {\n if (__internalModuleCache[\"http\"]) return __internalModuleCache[\"http\"];\n __internalModuleCache[\"http\"] = _httpModule;\n _debugRequire(\"loaded\", name, \"http-special\");\n return _httpModule;\n }\n if (name === \"https\") {\n if (__internalModuleCache[\"https\"]) return __internalModuleCache[\"https\"];\n __internalModuleCache[\"https\"] = _httpsModule;\n _debugRequire(\"loaded\", name, \"https-special\");\n return _httpsModule;\n }\n if (name === \"http2\") {\n if (__internalModuleCache[\"http2\"]) return __internalModuleCache[\"http2\"];\n __internalModuleCache[\"http2\"] = _http2Module;\n _debugRequire(\"loaded\", name, \"http2-special\");\n return _http2Module;\n }\n if (name === \"dns\") {\n if (__internalModuleCache[\"dns\"]) return __internalModuleCache[\"dns\"];\n __internalModuleCache[\"dns\"] = _dnsModule;\n _debugRequire(\"loaded\", name, \"dns-special\");\n return _dnsModule;\n }\n if (name === \"net\") {\n if (__internalModuleCache[\"net\"]) return __internalModuleCache[\"net\"];\n __internalModuleCache[\"net\"] = _netModule;\n _debugRequire(\"loaded\", name, \"net-special\");\n return _netModule;\n }\n if (name === \"tls\") {\n if (__internalModuleCache[\"tls\"]) return __internalModuleCache[\"tls\"];\n __internalModuleCache[\"tls\"] = _tlsModule;\n _debugRequire(\"loaded\", name, \"tls-special\");\n return _tlsModule;\n }\n if (name === \"os\") {\n if (__internalModuleCache[\"os\"]) return __internalModuleCache[\"os\"];\n __internalModuleCache[\"os\"] = _osModule;\n _debugRequire(\"loaded\", name, \"os-special\");\n return _osModule;\n }\n if (name === \"module\") {\n if (__internalModuleCache[\"module\"]) return __internalModuleCache[\"module\"];\n __internalModuleCache[\"module\"] = _moduleModule;\n _debugRequire(\"loaded\", name, \"module-special\");\n return _moduleModule;\n }\n if (name === \"process\") {\n _debugRequire(\"loaded\", name, \"process-special\");\n return globalThis.process;\n }\n if (name === \"async_hooks\") {\n if (__internalModuleCache[\"async_hooks\"]) return __internalModuleCache[\"async_hooks\"];\n class AsyncLocalStorage {\n constructor() {\n this._store = void 0;\n }\n run(store, callback) {\n const previousStore = this._store;\n this._store = store;\n try {\n const args = Array.prototype.slice.call(arguments, 2);\n return callback.apply(void 0, args);\n } finally {\n this._store = previousStore;\n }\n }\n enterWith(store) {\n this._store = store;\n }\n getStore() {\n return this._store;\n }\n disable() {\n this._store = void 0;\n }\n exit(callback) {\n const previousStore = this._store;\n this._store = void 0;\n try {\n const args = Array.prototype.slice.call(arguments, 1);\n return callback.apply(void 0, args);\n } finally {\n this._store = previousStore;\n }\n }\n }\n class AsyncResource {\n constructor(type) {\n this.type = type;\n }\n runInAsyncScope(callback, thisArg) {\n const args = Array.prototype.slice.call(arguments, 2);\n return callback.apply(thisArg, args);\n }\n emitDestroy() {\n }\n }\n const asyncHooksModule = {\n AsyncLocalStorage,\n AsyncResource,\n createHook() {\n return {\n enable() {\n return this;\n },\n disable() {\n return this;\n }\n };\n },\n executionAsyncId() {\n return 1;\n },\n triggerAsyncId() {\n return 0;\n },\n executionAsyncResource() {\n return null;\n }\n };\n __internalModuleCache[\"async_hooks\"] = asyncHooksModule;\n _debugRequire(\"loaded\", name, \"async-hooks-special\");\n return asyncHooksModule;\n }\n if (name === \"diagnostics_channel\") {\n let _createChannel2 = function() {\n return {\n hasSubscribers: false,\n publish: function() {\n },\n subscribe: function() {\n },\n unsubscribe: function() {\n }\n };\n };\n var _createChannel = _createChannel2;\n if (__internalModuleCache[name]) return __internalModuleCache[name];\n const dcModule = {\n channel: function() {\n return _createChannel2();\n },\n hasSubscribers: function() {\n return false;\n },\n tracingChannel: function() {\n return {\n start: _createChannel2(),\n end: _createChannel2(),\n asyncStart: _createChannel2(),\n asyncEnd: _createChannel2(),\n error: _createChannel2(),\n traceSync: function(fn, context, thisArg) {\n var args = Array.prototype.slice.call(arguments, 3);\n return fn.apply(thisArg, args);\n },\n tracePromise: function(fn, context, thisArg) {\n var args = Array.prototype.slice.call(arguments, 3);\n return fn.apply(thisArg, args);\n },\n traceCallback: function(fn, context, thisArg) {\n var args = Array.prototype.slice.call(arguments, 3);\n return fn.apply(thisArg, args);\n }\n };\n },\n Channel: function Channel(name2) {\n this.hasSubscribers = false;\n this.publish = function() {\n };\n this.subscribe = function() {\n };\n this.unsubscribe = function() {\n };\n }\n };\n __internalModuleCache[name] = dcModule;\n _debugRequire(\"loaded\", name, \"diagnostics-channel-special\");\n return dcModule;\n }\n if (_deferredCoreModules.has(name)) {\n if (__internalModuleCache[name]) return __internalModuleCache[name];\n const deferredStub = _createDeferredModuleStub(name);\n __internalModuleCache[name] = deferredStub;\n _debugRequire(\"loaded\", name, \"deferred-stub\");\n return deferredStub;\n }\n if (_unsupportedCoreModules.has(name)) {\n throw new Error(name + \" is not supported in sandbox\");\n }\n if (__internalModuleCache[name]) {\n _debugRequire(\"name-cache-hit\", name, name);\n return __internalModuleCache[name];\n }\n const isPath = name[0] === \".\" || name[0] === \"/\";\n const polyfillCode = isPath ? null : _loadPolyfill.applySyncPromise(void 0, [name]);\n if (polyfillCode !== null) {\n if (__internalModuleCache[name]) return __internalModuleCache[name];\n const moduleObj = { exports: {} };\n _pendingModules[name] = moduleObj;\n let result = eval(polyfillCode);\n result = _patchPolyfill(name, result);\n if (typeof result === \"object\" && result !== null) {\n Object.assign(moduleObj.exports, result);\n } else {\n moduleObj.exports = result;\n }\n __internalModuleCache[name] = moduleObj.exports;\n delete _pendingModules[name];\n _debugRequire(\"loaded\", name, \"polyfill\");\n return __internalModuleCache[name];\n }\n const resolveCacheKey = fromDir + \"\\0\" + name;\n if (resolveCacheKey in _resolveCache) {\n const cachedPath = _resolveCache[resolveCacheKey];\n if (cachedPath !== null && __internalModuleCache[cachedPath]) {\n _debugRequire(\"resolve-cache-hit\", name, cachedPath);\n return __internalModuleCache[cachedPath];\n }\n }\n resolved = _resolveFrom(name, fromDir);\n cacheKey = resolved;\n if (__internalModuleCache[cacheKey]) {\n _debugRequire(\"cache-hit\", name, cacheKey);\n return __internalModuleCache[cacheKey];\n }\n if (_pendingModules[cacheKey]) {\n _debugRequire(\"pending-hit\", name, cacheKey);\n return _pendingModules[cacheKey].exports;\n }\n let source;\n if (typeof _loadFileSync !== \"undefined\") {\n source = _loadFileSync.applySync(void 0, [resolved]);\n } else {\n source = _loadFile.applySyncPromise(void 0, [resolved]);\n }\n if (source === null) {\n const err = new Error(\"Cannot find module '\" + resolved + \"'\");\n err.code = \"MODULE_NOT_FOUND\";\n throw err;\n }\n if (resolved.endsWith(\".json\")) {\n const parsed = JSON.parse(source);\n __internalModuleCache[cacheKey] = parsed;\n return parsed;\n }\n const normalizedSource = typeof source === \"string\" ? source.replace(/import\\.meta\\.url/g, \"__filename\").replace(/fileURLToPath\\(__filename\\)/g, \"__filename\").replace(/url\\.fileURLToPath\\(__filename\\)/g, \"__filename\").replace(/fileURLToPath\\.call\\(void 0, __filename\\)/g, \"__filename\") : source;\n const module = {\n exports: {},\n filename: resolved,\n dirname: _dirname(resolved),\n id: resolved,\n loaded: false\n };\n _pendingModules[cacheKey] = module;\n const prevModule = _currentModule;\n _currentModule = module;\n try {\n let wrapper;\n try {\n wrapper = new Function(\n \"exports\",\n \"require\",\n \"module\",\n \"__filename\",\n \"__dirname\",\n \"__dynamicImport\",\n normalizedSource + \"\\n//# sourceURL=\" + resolved\n );\n } catch (error) {\n const details = error && error.stack ? error.stack : String(error);\n throw new Error(\"failed to compile module \" + resolved + \": \" + details);\n }\n const moduleRequire = function(request) {\n return _requireFrom(request, module.dirname);\n };\n moduleRequire.resolve = function(request) {\n return _resolveFrom(request, module.dirname);\n };\n const moduleDynamicImport = function(specifier) {\n if (typeof globalThis.__dynamicImport === \"function\") {\n return globalThis.__dynamicImport(specifier, module.dirname);\n }\n return Promise.reject(new Error(\"Dynamic import is not initialized\"));\n };\n wrapper(\n module.exports,\n moduleRequire,\n module,\n resolved,\n module.dirname,\n moduleDynamicImport\n );\n module.loaded = true;\n } catch (error) {\n const details = error && error.stack ? error.stack : String(error);\n throw new Error(\"failed to execute module \" + resolved + \": \" + details);\n } finally {\n _currentModule = prevModule;\n }\n __internalModuleCache[cacheKey] = module.exports;\n if (!isPath && name !== cacheKey) {\n __internalModuleCache[name] = module.exports;\n }\n delete _pendingModules[cacheKey];\n _debugRequire(\"loaded\", name, cacheKey);\n return module.exports;\n }\n __requireExposeCustomGlobal(\"_requireFrom\", _requireFrom);\n var __moduleCacheProxy = new Proxy(__internalModuleCache, {\n get(target, prop, receiver) {\n return Reflect.get(target, prop, receiver);\n },\n set(_target, prop) {\n throw new TypeError(\"Cannot set require.cache['\" + String(prop) + \"']\");\n },\n deleteProperty(_target, prop) {\n throw new TypeError(\"Cannot delete require.cache['\" + String(prop) + \"']\");\n },\n defineProperty(_target, prop) {\n throw new TypeError(\"Cannot define property '\" + String(prop) + \"' on require.cache\");\n },\n has(target, prop) {\n return Reflect.has(target, prop);\n },\n ownKeys(target) {\n return Reflect.ownKeys(target);\n },\n getOwnPropertyDescriptor(target, prop) {\n return Reflect.getOwnPropertyDescriptor(target, prop);\n }\n });\n globalThis.require.cache = __moduleCacheProxy;\n Object.defineProperty(globalThis, \"_moduleCache\", {\n value: __moduleCacheProxy,\n writable: false,\n configurable: true,\n enumerable: false\n });\n if (typeof _moduleModule !== \"undefined\") {\n if (_moduleModule.Module) {\n _moduleModule.Module._cache = __moduleCacheProxy;\n }\n _moduleModule._cache = __moduleCacheProxy;\n }\n})();\n", "setCommonjsFileGlobals": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-exposure.ts\n function defineRuntimeGlobalBinding(name, value, mutable) {\n Object.defineProperty(globalThis, name, {\n value,\n writable: mutable,\n configurable: mutable,\n enumerable: true\n });\n }\n function createRuntimeGlobalExposer(mutable) {\n return (name, value) => {\n defineRuntimeGlobalBinding(name, value, mutable);\n };\n }\n function getRuntimeExposeMutableGlobal() {\n if (typeof globalThis.__runtimeExposeMutableGlobal === \"function\") {\n return globalThis.__runtimeExposeMutableGlobal;\n }\n return createRuntimeGlobalExposer(true);\n }\n\n // isolate-runtime/src/inject/set-commonjs-file-globals.ts\n var __runtimeExposeMutableGlobal = getRuntimeExposeMutableGlobal();\n var __commonJsFileConfig = globalThis.__runtimeCommonJsFileConfig ?? {};\n var __filePath = typeof __commonJsFileConfig.filePath === \"string\" ? __commonJsFileConfig.filePath : \"/.js\";\n var __dirname = typeof __commonJsFileConfig.dirname === \"string\" ? __commonJsFileConfig.dirname : \"/\";\n __runtimeExposeMutableGlobal(\"__filename\", __filePath);\n __runtimeExposeMutableGlobal(\"__dirname\", __dirname);\n var __currentModule = globalThis._currentModule;\n if (__currentModule) {\n __currentModule.dirname = __dirname;\n __currentModule.filename = __filePath;\n }\n})();\n", "setStdinData": "\"use strict\";\n(() => {\n // isolate-runtime/src/inject/set-stdin-data.ts\n if (typeof globalThis._stdinData !== \"undefined\") {\n globalThis._stdinData = globalThis.__runtimeStdinData;\n globalThis._stdinPosition = 0;\n globalThis._stdinEnded = false;\n globalThis._stdinFlowMode = false;\n }\n})();\n", - "setupDynamicImport": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-access.ts\n function isObjectLike(value) {\n return value !== null && (typeof value === \"object\" || typeof value === \"function\");\n }\n\n // isolate-runtime/src/common/global-exposure.ts\n function defineRuntimeGlobalBinding(name, value, mutable) {\n Object.defineProperty(globalThis, name, {\n value,\n writable: mutable,\n configurable: mutable,\n enumerable: true\n });\n }\n function createRuntimeGlobalExposer(mutable) {\n return (name, value) => {\n defineRuntimeGlobalBinding(name, value, mutable);\n };\n }\n function getRuntimeExposeCustomGlobal() {\n if (typeof globalThis.__runtimeExposeCustomGlobal === \"function\") {\n return globalThis.__runtimeExposeCustomGlobal;\n }\n return createRuntimeGlobalExposer(false);\n }\n\n // isolate-runtime/src/inject/setup-dynamic-import.ts\n var __runtimeExposeCustomGlobal = getRuntimeExposeCustomGlobal();\n var __dynamicImportConfig = globalThis.__runtimeDynamicImportConfig ?? {};\n var __fallbackReferrer = typeof __dynamicImportConfig.referrerPath === \"string\" && __dynamicImportConfig.referrerPath.length > 0 ? __dynamicImportConfig.referrerPath : \"/\";\n var __dynamicImportHandler = async function(specifier, fromPath) {\n const request = String(specifier);\n const referrer = typeof fromPath === \"string\" && fromPath.length > 0 ? fromPath : __fallbackReferrer;\n const allowRequireFallback = request.endsWith(\".cjs\") || request.endsWith(\".json\");\n const namespace = await globalThis._dynamicImport(request, referrer);\n if (namespace !== null) {\n return namespace;\n }\n if (!allowRequireFallback) {\n throw new Error(\"Cannot find module '\" + request + \"'\");\n }\n const runtimeRequire = globalThis.require;\n if (typeof runtimeRequire !== \"function\") {\n throw new Error(\"Cannot find module '\" + request + \"'\");\n }\n const mod = runtimeRequire(request);\n const namespaceFallback = { default: mod };\n if (isObjectLike(mod)) {\n for (const key of Object.keys(mod)) {\n if (!(key in namespaceFallback)) {\n namespaceFallback[key] = mod[key];\n }\n }\n }\n return namespaceFallback;\n };\n __runtimeExposeCustomGlobal(\"__dynamicImport\", __dynamicImportHandler);\n})();\n", - "setupFsFacade": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-exposure.ts\n function defineRuntimeGlobalBinding(name, value, mutable) {\n Object.defineProperty(globalThis, name, {\n value,\n writable: mutable,\n configurable: mutable,\n enumerable: true\n });\n }\n function createRuntimeGlobalExposer(mutable) {\n return (name, value) => {\n defineRuntimeGlobalBinding(name, value, mutable);\n };\n }\n function getRuntimeExposeCustomGlobal() {\n if (typeof globalThis.__runtimeExposeCustomGlobal === \"function\") {\n return globalThis.__runtimeExposeCustomGlobal;\n }\n return createRuntimeGlobalExposer(false);\n }\n\n // isolate-runtime/src/inject/setup-fs-facade.ts\n var __runtimeExposeCustomGlobal = getRuntimeExposeCustomGlobal();\n var __fsFacade = {};\n Object.defineProperties(__fsFacade, {\n readFile: { get() {\n return globalThis._fsReadFile;\n }, enumerable: true },\n writeFile: { get() {\n return globalThis._fsWriteFile;\n }, enumerable: true },\n readFileBinary: { get() {\n return globalThis._fsReadFileBinary;\n }, enumerable: true },\n writeFileBinary: { get() {\n return globalThis._fsWriteFileBinary;\n }, enumerable: true },\n readDir: { get() {\n return globalThis._fsReadDir;\n }, enumerable: true },\n mkdir: { get() {\n return globalThis._fsMkdir;\n }, enumerable: true },\n rmdir: { get() {\n return globalThis._fsRmdir;\n }, enumerable: true },\n exists: { get() {\n return globalThis._fsExists;\n }, enumerable: true },\n stat: { get() {\n return globalThis._fsStat;\n }, enumerable: true },\n unlink: { get() {\n return globalThis._fsUnlink;\n }, enumerable: true },\n rename: { get() {\n return globalThis._fsRename;\n }, enumerable: true },\n chmod: { get() {\n return globalThis._fsChmod;\n }, enumerable: true },\n chown: { get() {\n return globalThis._fsChown;\n }, enumerable: true },\n link: { get() {\n return globalThis._fsLink;\n }, enumerable: true },\n symlink: { get() {\n return globalThis._fsSymlink;\n }, enumerable: true },\n readlink: { get() {\n return globalThis._fsReadlink;\n }, enumerable: true },\n lstat: { get() {\n return globalThis._fsLstat;\n }, enumerable: true },\n truncate: { get() {\n return globalThis._fsTruncate;\n }, enumerable: true },\n utimes: { get() {\n return globalThis._fsUtimes;\n }, enumerable: true }\n });\n __runtimeExposeCustomGlobal(\"_fs\", __fsFacade);\n})();\n", + "setupDynamicImport": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-access.ts\n function isObjectLike(value) {\n return value !== null && (typeof value === \"object\" || typeof value === \"function\");\n }\n\n // isolate-runtime/src/common/global-exposure.ts\n function defineRuntimeGlobalBinding(name, value, mutable) {\n Object.defineProperty(globalThis, name, {\n value,\n writable: mutable,\n configurable: mutable,\n enumerable: true\n });\n }\n function createRuntimeGlobalExposer(mutable) {\n return (name, value) => {\n defineRuntimeGlobalBinding(name, value, mutable);\n };\n }\n function getRuntimeExposeCustomGlobal() {\n if (typeof globalThis.__runtimeExposeCustomGlobal === \"function\") {\n return globalThis.__runtimeExposeCustomGlobal;\n }\n return createRuntimeGlobalExposer(false);\n }\n\n // isolate-runtime/src/inject/setup-dynamic-import.ts\n var __runtimeExposeCustomGlobal = getRuntimeExposeCustomGlobal();\n var __dynamicImportConfig = globalThis.__runtimeDynamicImportConfig ?? {};\n var __fallbackReferrer = typeof __dynamicImportConfig.referrerPath === \"string\" && __dynamicImportConfig.referrerPath.length > 0 ? __dynamicImportConfig.referrerPath : \"/\";\n var __dynamicImportHandler = async function(specifier, fromPath) {\n const request = String(specifier);\n const referrer = typeof fromPath === \"string\" && fromPath.length > 0 ? fromPath : __fallbackReferrer;\n const allowRequireFallback = request.endsWith(\".cjs\") || request.endsWith(\".json\");\n const namespace = await globalThis._dynamicImport.apply(\n void 0,\n [request, referrer],\n { result: { promise: true } }\n );\n if (namespace !== null) {\n return namespace;\n }\n if (!allowRequireFallback) {\n throw new Error(\"Cannot find module '\" + request + \"'\");\n }\n const runtimeRequire = globalThis.require;\n if (typeof runtimeRequire !== \"function\") {\n throw new Error(\"Cannot find module '\" + request + \"'\");\n }\n const mod = runtimeRequire(request);\n const namespaceFallback = { default: mod };\n if (isObjectLike(mod)) {\n for (const key of Object.keys(mod)) {\n if (!(key in namespaceFallback)) {\n namespaceFallback[key] = mod[key];\n }\n }\n }\n return namespaceFallback;\n };\n __runtimeExposeCustomGlobal(\"__dynamicImport\", __dynamicImportHandler);\n})();\n", + "setupFsFacade": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-exposure.ts\n function defineRuntimeGlobalBinding(name, value, mutable) {\n Object.defineProperty(globalThis, name, {\n value,\n writable: mutable,\n configurable: mutable,\n enumerable: true\n });\n }\n function createRuntimeGlobalExposer(mutable) {\n return (name, value) => {\n defineRuntimeGlobalBinding(name, value, mutable);\n };\n }\n function getRuntimeExposeCustomGlobal() {\n if (typeof globalThis.__runtimeExposeCustomGlobal === \"function\") {\n return globalThis.__runtimeExposeCustomGlobal;\n }\n return createRuntimeGlobalExposer(false);\n }\n\n // isolate-runtime/src/inject/setup-fs-facade.ts\n var __runtimeExposeCustomGlobal = getRuntimeExposeCustomGlobal();\n var __fsFacade = {\n readFile: globalThis._fsReadFile,\n writeFile: globalThis._fsWriteFile,\n readFileBinary: globalThis._fsReadFileBinary,\n writeFileBinary: globalThis._fsWriteFileBinary,\n readDir: globalThis._fsReadDir,\n mkdir: globalThis._fsMkdir,\n rmdir: globalThis._fsRmdir,\n exists: globalThis._fsExists,\n stat: globalThis._fsStat,\n unlink: globalThis._fsUnlink,\n rename: globalThis._fsRename,\n chmod: globalThis._fsChmod,\n chown: globalThis._fsChown,\n link: globalThis._fsLink,\n symlink: globalThis._fsSymlink,\n readlink: globalThis._fsReadlink,\n lstat: globalThis._fsLstat,\n truncate: globalThis._fsTruncate,\n utimes: globalThis._fsUtimes\n };\n __runtimeExposeCustomGlobal(\"_fs\", __fsFacade);\n})();\n", } as const; export type IsolateRuntimeSourceId = keyof typeof ISOLATE_RUNTIME_SOURCES; diff --git a/packages/secure-exec-core/src/index.ts b/packages/secure-exec-core/src/index.ts index 06b397cb..6353c885 100644 --- a/packages/secure-exec-core/src/index.ts +++ b/packages/secure-exec-core/src/index.ts @@ -107,6 +107,9 @@ export { // Bridge contract. export type { + BridgeApplyRef, + BridgeApplySyncPromiseRef, + BridgeApplySyncRef, BridgeGlobalKey, ChildProcessKillBridgeRef, ChildProcessSpawnStartBridgeRef, diff --git a/packages/secure-exec-core/src/shared/bridge-contract.ts b/packages/secure-exec-core/src/shared/bridge-contract.ts index 9bf27400..f17d4dd0 100644 --- a/packages/secure-exec-core/src/shared/bridge-contract.ts +++ b/packages/secure-exec-core/src/shared/bridge-contract.ts @@ -3,11 +3,10 @@ * host (Node.js) and the isolate (sandbox V8 context). * * Two categories: - * - Host bridge globals: set by the host before bridge code runs (fs fns, timers, etc.) + * - Host bridge globals: set by the host before bridge code runs (fs refs, timers, etc.) * - Runtime bridge globals: installed by the bridge bundle itself (active handles, modules, etc.) * - * Each type alias is a plain function signature. The Rust V8 runtime registers - * these as real JS functions on the global; bridge code calls them directly. + * The typed `Ref` aliases describe the isolated-vm calling convention for each global. */ export type ValueOf = T[keyof T]; @@ -122,44 +121,104 @@ export const BRIDGE_GLOBAL_KEY_LIST = [ ...RUNTIME_BRIDGE_GLOBAL_KEY_LIST, ] as const; +/** An isolated-vm Reference that resolves async via `{ result: { promise: true } }`. */ +export interface BridgeApplyRef { + apply( + ctx: undefined, + args: TArgs, + options: { result: { promise: true } }, + ): Promise; +} + +/** An isolated-vm Reference called synchronously (blocks the isolate). */ +export interface BridgeApplySyncRef { + applySync(ctx: undefined, args: TArgs): TResult; +} + +/** + * An isolated-vm Reference that blocks the isolate while the host resolves + * a Promise. Used for sync-looking APIs (require, readFileSync) that need + * async host operations. + */ +export interface BridgeApplySyncPromiseRef { + applySyncPromise(ctx: undefined, args: TArgs): TResult; +} + // Module loading boundary contracts. -export type DynamicImportBridgeRef = (specifier: string, fromPath: string) => Promise | null>; -export type LoadPolyfillBridgeRef = (moduleName: string) => string | null; -export type ResolveModuleBridgeRef = (request: string, fromDir: string) => string | null; -export type LoadFileBridgeRef = (path: string) => string | null; +export type DynamicImportBridgeRef = BridgeApplyRef< + [string, string], + Record | null +>; +export type LoadPolyfillBridgeRef = BridgeApplyRef<[string], string | null>; +export type ResolveModuleBridgeRef = BridgeApplySyncPromiseRef< + [string, string], + string | null +>; +export type LoadFileBridgeRef = BridgeApplySyncPromiseRef<[string], string | null>; export type RequireFromBridgeFn = (request: string, dirname: string) => unknown; export type ModuleCacheBridgeRecord = Record; // Process/console/entropy boundary contracts. -export type ProcessLogBridgeRef = (msg: string) => void; -export type ProcessErrorBridgeRef = (msg: string) => void; -export type ScheduleTimerBridgeRef = (delayMs: number) => Promise; -export type CryptoRandomFillBridgeRef = (byteLength: number) => Uint8Array; -export type CryptoRandomUuidBridgeRef = () => string; +export type ProcessLogBridgeRef = BridgeApplySyncRef<[string], void>; +export type ProcessErrorBridgeRef = BridgeApplySyncRef<[string], void>; +export type ScheduleTimerBridgeRef = BridgeApplyRef<[number], void>; +export type CryptoRandomFillBridgeRef = BridgeApplySyncRef<[number], string>; +export type CryptoRandomUuidBridgeRef = BridgeApplySyncRef<[], string>; +export type CryptoHashDigestBridgeRef = BridgeApplySyncRef<[string, string], string>; +export type CryptoHmacDigestBridgeRef = BridgeApplySyncRef<[string, string, string], string>; +export type CryptoPbkdf2BridgeRef = BridgeApplySyncRef< + [string, string, number, number, string], + string +>; +export type CryptoScryptBridgeRef = BridgeApplySyncRef< + [string, string, number, string], + string +>; +export type CryptoCipherivBridgeRef = BridgeApplySyncRef< + [string, string, string, string], + string +>; +export type CryptoDecipherivBridgeRef = BridgeApplySyncRef< + [string, string, string, string, string], + string +>; +export type CryptoSignBridgeRef = BridgeApplySyncRef< + [string, string, string], + string +>; +export type CryptoVerifyBridgeRef = BridgeApplySyncRef< + [string, string, string, string], + boolean +>; +export type CryptoGenerateKeyPairSyncBridgeRef = BridgeApplySyncRef< + [string, string], + string +>; +export type CryptoSubtleBridgeRef = BridgeApplySyncRef<[string], string>; // Filesystem boundary contracts. -export type FsReadFileBridgeRef = (path: string) => string; -export type FsWriteFileBridgeRef = (path: string, content: string) => void; -export type FsReadFileBinaryBridgeRef = (path: string) => Uint8Array; -export type FsWriteFileBinaryBridgeRef = (path: string, content: Uint8Array) => void; -export type FsReadDirEntry = { name: string; isDirectory: boolean }; -export type FsReadDirBridgeRef = (path: string) => FsReadDirEntry[]; -export type FsMkdirBridgeRef = (path: string, recursive: boolean) => void; -export type FsRmdirBridgeRef = (path: string) => void; -export type FsExistsBridgeRef = (path: string) => boolean; -export type FsStatResult = { mode: number; size: number; isDirectory: boolean; atimeMs: number; mtimeMs: number; ctimeMs: number; birthtimeMs: number }; -export type FsStatBridgeRef = (path: string) => FsStatResult; -export type FsUnlinkBridgeRef = (path: string) => void; -export type FsRenameBridgeRef = (oldPath: string, newPath: string) => void; -export type FsChmodBridgeRef = (path: string, mode: number) => void; -export type FsChownBridgeRef = (path: string, uid: number, gid: number) => void; -export type FsLinkBridgeRef = (existingPath: string, newPath: string) => void; -export type FsSymlinkBridgeRef = (target: string, path: string) => void; -export type FsReadlinkBridgeRef = (path: string) => string; -export type FsLstatResult = FsStatResult & { isSymbolicLink: boolean }; -export type FsLstatBridgeRef = (path: string) => FsLstatResult; -export type FsTruncateBridgeRef = (path: string, length: number) => void; -export type FsUtimesBridgeRef = (path: string, atime: number, mtime: number) => void; +export type FsReadFileBridgeRef = BridgeApplySyncPromiseRef<[string], string>; +export type FsWriteFileBridgeRef = BridgeApplySyncPromiseRef<[string, string], void>; +export type FsReadFileBinaryBridgeRef = BridgeApplySyncPromiseRef<[string], string>; +export type FsWriteFileBinaryBridgeRef = BridgeApplySyncPromiseRef< + [string, string], + void +>; +export type FsReadDirBridgeRef = BridgeApplySyncPromiseRef<[string], string>; +export type FsMkdirBridgeRef = BridgeApplySyncPromiseRef<[string, boolean], void>; +export type FsRmdirBridgeRef = BridgeApplySyncPromiseRef<[string], void>; +export type FsExistsBridgeRef = BridgeApplySyncPromiseRef<[string], boolean>; +export type FsStatBridgeRef = BridgeApplySyncPromiseRef<[string], string>; +export type FsUnlinkBridgeRef = BridgeApplySyncPromiseRef<[string], void>; +export type FsRenameBridgeRef = BridgeApplySyncPromiseRef<[string, string], void>; +export type FsChmodBridgeRef = BridgeApplySyncPromiseRef<[string, number], void>; +export type FsChownBridgeRef = BridgeApplySyncPromiseRef<[string, number, number], void>; +export type FsLinkBridgeRef = BridgeApplySyncPromiseRef<[string, string], void>; +export type FsSymlinkBridgeRef = BridgeApplySyncPromiseRef<[string, string], void>; +export type FsReadlinkBridgeRef = BridgeApplySyncPromiseRef<[string], string>; +export type FsLstatBridgeRef = BridgeApplySyncPromiseRef<[string], string>; +export type FsTruncateBridgeRef = BridgeApplySyncPromiseRef<[string, number], void>; +export type FsUtimesBridgeRef = BridgeApplySyncPromiseRef<[string, number, number], void>; /** Combined filesystem bridge facade installed as `globalThis._fs` in the isolate. */ export interface FsFacadeBridge { @@ -185,26 +244,42 @@ export interface FsFacadeBridge { } // Child process boundary contracts. -export type ChildProcessSpawnStartBridgeRef = (command: string, argsJson: string, optionsJson: string) => number; -export type ChildProcessStdinWriteBridgeRef = (sessionId: number, data: Uint8Array) => void; -export type ChildProcessStdinCloseBridgeRef = (sessionId: number) => void; -export type ChildProcessKillBridgeRef = (sessionId: number, signal: number) => void; -export type SpawnSyncBridgeResult = { stdout: string; stderr: string; code: number; maxBufferExceeded?: boolean }; -export type ChildProcessSpawnSyncBridgeRef = (command: string, argsJson: string, optionsJson: string) => SpawnSyncBridgeResult; +export type ChildProcessSpawnStartBridgeRef = BridgeApplySyncRef< + [string, string, string], + number +>; +export type ChildProcessStdinWriteBridgeRef = BridgeApplySyncRef< + [number, Uint8Array], + void +>; +export type ChildProcessStdinCloseBridgeRef = BridgeApplySyncRef<[number], void>; +export type ChildProcessKillBridgeRef = BridgeApplySyncRef<[number, number], void>; +export type ChildProcessSpawnSyncBridgeRef = BridgeApplySyncPromiseRef< + [string, string, string], + string +>; // Network boundary contracts. -export type NetworkFetchResult = { ok: boolean; status: number; statusText: string; headers?: Record; url?: string; redirected?: boolean; body?: string }; -export type NetworkFetchRawBridgeRef = (url: string, optionsJson: string) => Promise; -export type NetworkDnsLookupResult = { error?: string; code?: string; address?: string; family?: number }; -export type NetworkDnsLookupRawBridgeRef = (hostname: string) => Promise; -export type NetworkHttpRequestResult = { headers?: Record; url?: string; status?: number; statusText?: string; body?: string; trailers?: Record }; -export type NetworkHttpRequestRawBridgeRef = (url: string, optionsJson: string) => Promise; -export type NetworkHttpServerListenResult = { address: { address: string; family: string; port: number } | null }; -export type NetworkHttpServerListenRawBridgeRef = (optionsJson: string) => Promise; -export type NetworkHttpServerCloseRawBridgeRef = (serverId: number) => Promise; +export type NetworkFetchRawBridgeRef = BridgeApplyRef<[string, string], string>; +export type NetworkDnsLookupRawBridgeRef = BridgeApplyRef<[string], string>; +export type NetworkHttpRequestRawBridgeRef = BridgeApplyRef<[string, string], string>; +export type NetworkHttpServerListenRawBridgeRef = BridgeApplyRef<[string], string>; +export type NetworkHttpServerCloseRawBridgeRef = BridgeApplyRef<[number], void>; +export type UpgradeSocketWriteRawBridgeRef = BridgeApplySyncRef<[number, string], void>; +export type UpgradeSocketEndRawBridgeRef = BridgeApplySyncRef<[number], void>; +export type UpgradeSocketDestroyRawBridgeRef = BridgeApplySyncRef<[number], void>; + +// TCP socket (net module) boundary contracts. +export type NetSocketConnectRawBridgeRef = BridgeApplySyncRef<[string, number], number>; +export type NetSocketWriteRawBridgeRef = BridgeApplySyncRef<[number, string], void>; +export type NetSocketEndRawBridgeRef = BridgeApplySyncRef<[number], void>; +export type NetSocketDestroyRawBridgeRef = BridgeApplySyncRef<[number], void>; + +// TLS socket upgrade boundary contract. +export type NetSocketUpgradeTlsRawBridgeRef = BridgeApplySyncRef<[number, string], void>; // PTY boundary contracts. -export type PtySetRawModeBridgeRef = (mode: boolean) => void; +export type PtySetRawModeBridgeRef = BridgeApplySyncRef<[boolean], void>; // Active-handle lifecycle globals exposed by the bridge. export type RegisterHandleBridgeFn = (id: string, description: string) => void; diff --git a/packages/secure-exec-core/src/shared/console-formatter.ts b/packages/secure-exec-core/src/shared/console-formatter.ts index 5ec27aa7..d2d1f418 100644 --- a/packages/secure-exec-core/src/shared/console-formatter.ts +++ b/packages/secure-exec-core/src/shared/console-formatter.ts @@ -188,10 +188,10 @@ export function getConsoleSetupCode( const formatConsoleArgs = ${formatConsoleArgs.toString()}; globalThis.console = { - log: (...args) => _log(formatConsoleArgs(args, __consoleBudget)), - error: (...args) => _error(formatConsoleArgs(args, __consoleBudget)), - warn: (...args) => _error(formatConsoleArgs(args, __consoleBudget)), - info: (...args) => _log(formatConsoleArgs(args, __consoleBudget)), + log: (...args) => _log.applySync(undefined, [formatConsoleArgs(args, __consoleBudget)]), + error: (...args) => _error.applySync(undefined, [formatConsoleArgs(args, __consoleBudget)]), + warn: (...args) => _error.applySync(undefined, [formatConsoleArgs(args, __consoleBudget)]), + info: (...args) => _log.applySync(undefined, [formatConsoleArgs(args, __consoleBudget)]), }; `; } diff --git a/packages/secure-exec-node/package.json b/packages/secure-exec-node/package.json index 7d85cf05..7b558029 100644 --- a/packages/secure-exec-node/package.json +++ b/packages/secure-exec-node/package.json @@ -1,6 +1,6 @@ { "name": "@secure-exec/node", - "version": "0.1.1-rc.2", + "version": "0.1.0", "type": "module", "license": "Apache-2.0", "main": "./dist/index.js", @@ -85,6 +85,7 @@ "@secure-exec/core": "workspace:*", "@secure-exec/v8": "workspace:*", "esbuild": "^0.27.1", + "isolated-vm": "^6.0.0", "node-stdlib-browser": "^1.3.1" }, "devDependencies": { diff --git a/packages/secure-exec-node/src/bridge-setup.ts b/packages/secure-exec-node/src/bridge-setup.ts index 2dabe8bb..ef4464de 100644 --- a/packages/secure-exec-node/src/bridge-setup.ts +++ b/packages/secure-exec-node/src/bridge-setup.ts @@ -1,4 +1,20 @@ -import { randomFillSync, randomUUID } from "node:crypto"; +import ivm from "isolated-vm"; +import { + randomFillSync, + randomUUID, + createHash, + createHmac, + pbkdf2Sync, + scryptSync, + createCipheriv, + createDecipheriv, + sign, + verify, + generateKeyPairSync, + createPublicKey, + createPrivateKey, + timingSafeEqual, +} from "node:crypto"; import { getInitialBridgeGlobalsSetupCode, getIsolateRuntimeSource, @@ -36,6 +52,7 @@ import { checkBridgeBudget, assertPayloadByteLength, assertTextPayloadSize, + getBase64EncodedByteLength, parseJsonWithLimit, polyfillCodeCache, PAYLOAD_LIMIT_ERROR_CODE, @@ -43,14 +60,6 @@ import { } from "./isolate-bootstrap.js"; import type { DriverDeps } from "./isolate-bootstrap.js"; -// Legacy ivm-compatible context/reference types for backward compatibility. -// These functions are no longer used by the V8-based execution driver but -// are kept to avoid breaking re-export signatures. -/* eslint-disable @typescript-eslint/no-explicit-any */ -type LegacyContext = any; -type LegacyReference<_T = unknown> = any; -/* eslint-enable @typescript-eslint/no-explicit-any */ - // Env vars that could hijack child processes (library injection, node flags) const DANGEROUS_ENV_KEYS = new Set([ "LD_PRELOAD", @@ -111,33 +120,32 @@ export function emitConsoleEvent( /** * Set up console with optional streaming log hook. - * - * @deprecated Legacy function for isolated-vm contexts. Use bridge-handlers.ts for V8 runtime. */ export async function setupConsole( deps: BridgeDeps, - context: LegacyContext, - jail: LegacyReference, + context: ivm.Context, + jail: ivm.Reference>, onStdio?: StdioHook, ): Promise { - const logRef = { applySync: (_ctx: unknown, args: unknown[]) => { - const str = String(args[0]); + const logRef = new ivm.Reference((msg: string) => { + const str = String(msg); + // Enforce output byte budget — reject messages that would exceed the limit if (deps.maxOutputBytes !== undefined) { const bytes = Buffer.byteLength(str, "utf8"); if (deps.budgetState.outputBytes + bytes > deps.maxOutputBytes) return; deps.budgetState.outputBytes += bytes; } emitConsoleEvent(onStdio, { channel: "stdout", message: str }); - }}; - const errorRef = { applySync: (_ctx: unknown, args: unknown[]) => { - const str = String(args[0]); + }); + const errorRef = new ivm.Reference((msg: string) => { + const str = String(msg); if (deps.maxOutputBytes !== undefined) { const bytes = Buffer.byteLength(str, "utf8"); if (deps.budgetState.outputBytes + bytes > deps.maxOutputBytes) return; deps.budgetState.outputBytes += bytes; } emitConsoleEvent(onStdio, { channel: "stderr", message: str }); - }}; + }); await jail.set(HOST_BRIDGE_GLOBAL_KEYS.log, logRef); await jail.set(HOST_BRIDGE_GLOBAL_KEYS.error, errorRef); @@ -147,25 +155,54 @@ export async function setupConsole( /** * Set up the require() system in a context. - * - * @deprecated Legacy function for isolated-vm contexts. Use bridge-handlers.ts for V8 runtime. */ export async function setupRequire( deps: BridgeDeps, - context: LegacyContext, - jail: LegacyReference, + context: ivm.Context, + jail: ivm.Reference>, timingMitigation: TimingMitigation, frozenTimeMs: number, ): Promise { - // Create stubs matching the legacy isolated-vm Reference interface - const loadPolyfillRef = { - applySyncPromise: async (_ctx: unknown, args: unknown[]) => { - const moduleName = args[0] as string; + // Create a reference that can load polyfills on demand + const loadPolyfillRef = new ivm.Reference( + async (moduleName: string): Promise => { const name = moduleName.replace(/^node:/, ""); - if (name === "fs" || name === "child_process") return null; - if (name === "http" || name === "https" || name === "http2" || name === "dns") return null; - if (name === "os" || name === "module") return null; - if (!hasPolyfill(name)) return null; + + // fs is handled specially + if (name === "fs") { + return null; + } + + // child_process is handled specially + if (name === "child_process") { + return null; + } + + // Network modules are handled specially + if ( + name === "http" || + name === "https" || + name === "http2" || + name === "dns" || + name === "net" + ) { + return null; + } + + // os module is handled specially with our own polyfill + if (name === "os") { + return null; + } + + // module is handled specially with our own polyfill + if (name === "module") { + return null; + } + + if (!hasPolyfill(name)) { + return null; + } + // Check cache first let code = polyfillCodeCache.get(name); if (!code) { code = await bundlePolyfill(name); @@ -173,26 +210,51 @@ export async function setupRequire( } return code; }, - }; + ); - const resolveModuleRef = { - applySyncPromise: async (_ctx: unknown, args: unknown[]) => { - const request = args[0] as string; - const fromDir = args[1] as string; + // Create a reference for resolving module paths + const resolveModuleRef = new ivm.Reference( + async (request: string, fromDir: string): Promise => { const builtinSpecifier = normalizeBuiltinSpecifier(request); - if (builtinSpecifier) return builtinSpecifier; + if (builtinSpecifier) { + return builtinSpecifier; + } return resolveModule(request, fromDir, deps.filesystem, "require", deps.resolutionCache); }, - }; + ); - const loadFileRef = { - applySyncPromise: async (_ctx: unknown, args: unknown[]) => { - const path = args[0] as string; + // Synchronous module resolution using Node.js require.resolve(). + // Used as fallback inside applySync contexts where applySyncPromise can't + // pump the event loop (e.g. require() inside net socket data callbacks). + const { createRequire } = await import("node:module"); + const resolveModuleSyncRef = new ivm.Reference( + (request: string, fromDir: string): string | null => { + const builtinSpecifier = normalizeBuiltinSpecifier(request); + if (builtinSpecifier) { + return builtinSpecifier; + } + try { + const hostRequire = createRequire(fromDir + "/noop.js"); + const result = hostRequire.resolve(request); + return result; + } catch { + return null; + } + }, + ); + + // Create a reference for loading file content + // Also transforms dynamic import() calls to __dynamicImport() + const loadFileRef = new ivm.Reference( + async (path: string): Promise => { const source = await loadFile(path, deps.filesystem); - if (source === null) return null; + if (source === null) { + return null; + } + // Transform dynamic import() to __dynamicImport() for V8 compatibility return transformDynamicImport(source); }, - }; + ); // Synchronous file loading for use inside applySync contexts. const { readFileSync } = await import("node:fs"); @@ -213,52 +275,1425 @@ export async function setupRequire( await jail.set(HOST_BRIDGE_GLOBAL_KEYS.loadFile, loadFileRef); await jail.set(HOST_BRIDGE_GLOBAL_KEYS.loadFileSync, loadFileSyncRef); - const scheduleTimerRef = { - applySyncPromise: (_ctx: unknown, args: unknown[]) => { - checkBridgeBudget(deps); - const delayMs = args[0] as number; - return new Promise((resolve) => { - const id = globalThis.setTimeout(() => { - deps.activeHostTimers.delete(id); - resolve(); - }, delayMs); - deps.activeHostTimers.add(id); - }); - }, - }; + // Set up timer Reference for actual delays (not just microtasks) + const scheduleTimerRef = new ivm.Reference((delayMs: number) => { + checkBridgeBudget(deps); + return new Promise((resolve) => { + const id = globalThis.setTimeout(() => { + deps.activeHostTimers.delete(id); + resolve(); + }, delayMs); + deps.activeHostTimers.add(id); + }); + }); await jail.set(HOST_BRIDGE_GLOBAL_KEYS.scheduleTimer, scheduleTimerRef); + // Inject maxTimers limit for bridge-side enforcement (synchronous check) if (deps.maxTimers !== undefined) { await jail.set("_maxTimers", deps.maxTimers, { copy: true }); } + + // Inject maxHandles limit for bridge-side active handle cap if (deps.maxHandles !== undefined) { await jail.set("_maxHandles", deps.maxHandles, { copy: true }); } - const cryptoRandomFillRef = { - applySync: (_ctx: unknown, args: unknown[]) => { - const byteLength = args[0] as number; - if (byteLength > 65536) { - throw new RangeError( - `The ArrayBufferView's byte length (${byteLength}) exceeds the number of bytes of entropy available via this API (65536)`, - ); - } - const buffer = Buffer.allocUnsafe(byteLength); - randomFillSync(buffer); - return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); - }, - }; - const cryptoRandomUuidRef = { applySync: () => randomUUID() }; + // Set up host crypto references for secure randomness. + // Cap matches Web Crypto API spec (65536 bytes) to prevent host OOM. + const cryptoRandomFillRef = new ivm.Reference((byteLength: number) => { + if (byteLength > 65536) { + throw new RangeError( + `The ArrayBufferView's byte length (${byteLength}) exceeds the number of bytes of entropy available via this API (65536)`, + ); + } + const buffer = Buffer.allocUnsafe(byteLength); + randomFillSync(buffer); + return buffer.toString("base64"); + }); + const cryptoRandomUuidRef = new ivm.Reference(() => { + return randomUUID(); + }); await jail.set(HOST_BRIDGE_GLOBAL_KEYS.cryptoRandomFill, cryptoRandomFillRef); await jail.set(HOST_BRIDGE_GLOBAL_KEYS.cryptoRandomUuid, cryptoRandomUuidRef); - // Fs, child_process, network, PTY stubs omitted — legacy code path is unused. - // The V8-based driver uses bridge-handlers.ts instead. + // Set up host crypto references for createHash/createHmac. + // Guest accumulates update() data, sends base64 to host for digest. + const cryptoHashDigestRef = new ivm.Reference( + (algorithm: string, dataBase64: string) => { + const data = Buffer.from(dataBase64, "base64"); + const hash = createHash(algorithm); + hash.update(data); + return hash.digest("base64"); + }, + ); + const cryptoHmacDigestRef = new ivm.Reference( + (algorithm: string, keyBase64: string, dataBase64: string) => { + const key = Buffer.from(keyBase64, "base64"); + const data = Buffer.from(dataBase64, "base64"); + const hmac = createHmac(algorithm, key); + hmac.update(data); + return hmac.digest("base64"); + }, + ); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.cryptoHashDigest, cryptoHashDigestRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.cryptoHmacDigest, cryptoHmacDigestRef); + + // Set up host crypto references for pbkdf2/scrypt key derivation. + const cryptoPbkdf2Ref = new ivm.Reference( + ( + passwordBase64: string, + saltBase64: string, + iterations: number, + keylen: number, + digest: string, + ) => { + const password = Buffer.from(passwordBase64, "base64"); + const salt = Buffer.from(saltBase64, "base64"); + return pbkdf2Sync(password, salt, iterations, keylen, digest).toString( + "base64", + ); + }, + ); + const cryptoScryptRef = new ivm.Reference( + ( + passwordBase64: string, + saltBase64: string, + keylen: number, + optionsJson: string, + ) => { + const password = Buffer.from(passwordBase64, "base64"); + const salt = Buffer.from(saltBase64, "base64"); + const options = JSON.parse(optionsJson); + return scryptSync(password, salt, keylen, options).toString("base64"); + }, + ); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.cryptoPbkdf2, cryptoPbkdf2Ref); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.cryptoScrypt, cryptoScryptRef); + + // Set up host crypto references for createCipheriv/createDecipheriv. + // Guest accumulates update() data, sends base64 to host for encrypt/decrypt. + // Returns JSON for GCM (includes authTag), plain base64 for other modes. + const cryptoCipherivRef = new ivm.Reference( + ( + algorithm: string, + keyBase64: string, + ivBase64: string, + dataBase64: string, + ) => { + const key = Buffer.from(keyBase64, "base64"); + const iv = Buffer.from(ivBase64, "base64"); + const data = Buffer.from(dataBase64, "base64"); + const cipher = createCipheriv(algorithm, key, iv) as any; + const encrypted = Buffer.concat([cipher.update(data), cipher.final()]); + const isGcm = algorithm.includes("-gcm"); + if (isGcm) { + return JSON.stringify({ + data: encrypted.toString("base64"), + authTag: cipher.getAuthTag().toString("base64"), + }); + } + return JSON.stringify({ data: encrypted.toString("base64") }); + }, + ); + const cryptoDecipherivRef = new ivm.Reference( + ( + algorithm: string, + keyBase64: string, + ivBase64: string, + dataBase64: string, + optionsJson: string, + ) => { + const key = Buffer.from(keyBase64, "base64"); + const iv = Buffer.from(ivBase64, "base64"); + const data = Buffer.from(dataBase64, "base64"); + const options = JSON.parse(optionsJson); + const decipher = createDecipheriv(algorithm, key, iv) as any; + const isGcm = algorithm.includes("-gcm"); + if (isGcm && options.authTag) { + decipher.setAuthTag(Buffer.from(options.authTag, "base64")); + } + return Buffer.concat([decipher.update(data), decipher.final()]).toString( + "base64", + ); + }, + ); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.cryptoCipheriv, cryptoCipherivRef); + await jail.set( + HOST_BRIDGE_GLOBAL_KEYS.cryptoDecipheriv, + cryptoDecipherivRef, + ); + + // Stateful cipher/decipher for streaming crypto (ssh2 AES-GCM, etc.) + const cipherSessions = new Map< + number, + { instance: any; isGcm: boolean; mode: "cipher" | "decipher" } + >(); + let nextCipherSessionId = 1; + + const cryptoCipherivCreateRef = new ivm.Reference( + ( + mode: string, + algorithm: string, + keyBase64: string, + ivBase64: string, + ): number => { + const key = Buffer.from(keyBase64, "base64"); + const iv = Buffer.from(ivBase64, "base64"); + const sessionId = nextCipherSessionId++; + const isCipher = mode === "cipher"; + const instance = isCipher + ? createCipheriv(algorithm, key, iv) + : createDecipheriv(algorithm, key, iv); + cipherSessions.set(sessionId, { + instance, + isGcm: algorithm.includes("-gcm"), + mode: isCipher ? "cipher" : "decipher", + }); + return sessionId; + }, + ); + + const cryptoCipherivUpdateRef = new ivm.Reference( + (sessionId: number, dataBase64: string, optionsJson?: string): string => { + const session = cipherSessions.get(sessionId); + if (!session) throw new Error("Invalid cipher session"); + if (optionsJson) { + const opts = JSON.parse(optionsJson); + if (opts.setAAD) { + (session.instance as any).setAAD( + Buffer.from(opts.setAAD, "base64"), + ); + } + if (opts.setAuthTag) { + (session.instance as any).setAuthTag( + Buffer.from(opts.setAuthTag, "base64"), + ); + } + if (opts.setAutoPadding !== undefined) { + (session.instance as any).setAutoPadding(opts.setAutoPadding); + } + // Options-only call (no data to process) + if (!dataBase64) return ""; + } + const data = Buffer.from(dataBase64, "base64"); + const result = session.instance.update(data); + return result.toString("base64"); + }, + ); + + const cryptoCipherivFinalRef = new ivm.Reference( + (sessionId: number): string => { + const session = cipherSessions.get(sessionId); + if (!session) throw new Error("Invalid cipher session"); + const result = session.instance.final(); + const response: Record = { + data: result.toString("base64"), + }; + if (session.isGcm && session.mode === "cipher") { + response.authTag = (session.instance as any) + .getAuthTag() + .toString("base64"); + } + cipherSessions.delete(sessionId); + return JSON.stringify(response); + }, + ); + + await jail.set( + HOST_BRIDGE_GLOBAL_KEYS.cryptoCipherivCreate, + cryptoCipherivCreateRef, + ); + await jail.set( + HOST_BRIDGE_GLOBAL_KEYS.cryptoCipherivUpdate, + cryptoCipherivUpdateRef, + ); + await jail.set( + HOST_BRIDGE_GLOBAL_KEYS.cryptoCipherivFinal, + cryptoCipherivFinalRef, + ); + + // Set up host crypto references for sign/verify and key generation. + // sign: (algorithm, dataBase64, keyPem) → signatureBase64 + const cryptoSignRef = new ivm.Reference( + (algorithm: string, dataBase64: string, keyPem: string) => { + const data = Buffer.from(dataBase64, "base64"); + const key = createPrivateKey(keyPem); + const signature = sign(algorithm, data, key); + return signature.toString("base64"); + }, + ); + // verify: (algorithm, dataBase64, keyPem, signatureBase64) → boolean + const cryptoVerifyRef = new ivm.Reference( + ( + algorithm: string, + dataBase64: string, + keyPem: string, + signatureBase64: string, + ) => { + const data = Buffer.from(dataBase64, "base64"); + const key = createPublicKey(keyPem); + const signature = Buffer.from(signatureBase64, "base64"); + return verify(algorithm, data, key, signature); + }, + ); + // generateKeyPairSync: (type, optionsJson) → JSON { publicKey, privateKey } + const cryptoGenerateKeyPairSyncRef = new ivm.Reference( + (type: string, optionsJson: string) => { + const options = JSON.parse(optionsJson); + // Always produce PEM output for cross-boundary transfer + const genOptions = { + ...options, + publicKeyEncoding: { type: "spki" as const, format: "pem" as const }, + privateKeyEncoding: { + type: "pkcs8" as const, + format: "pem" as const, + }, + }; + const { publicKey, privateKey } = generateKeyPairSync( + type as any, + genOptions as any, + ); + return JSON.stringify({ publicKey, privateKey }); + }, + ); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.cryptoSign, cryptoSignRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.cryptoVerify, cryptoVerifyRef); + await jail.set( + HOST_BRIDGE_GLOBAL_KEYS.cryptoGenerateKeyPairSync, + cryptoGenerateKeyPairSyncRef, + ); + + // Set up host crypto.subtle dispatcher for Web Crypto API. + // Single dispatcher handles all subtle operations via JSON-encoded requests. + const cryptoSubtleRef = new ivm.Reference((opJson: string): string => { + const req = JSON.parse(opJson); + const normalizeHash = (h: string | { name: string }): string => { + const n = typeof h === "string" ? h : h.name; + return n.toLowerCase().replace("-", ""); + }; + switch (req.op) { + case "digest": { + const algo = normalizeHash(req.algorithm); + const data = Buffer.from(req.data, "base64"); + return JSON.stringify({ + data: createHash(algo).update(data).digest("base64"), + }); + } + case "generateKey": { + const algoName = req.algorithm.name; + if ( + algoName === "AES-GCM" || + algoName === "AES-CBC" || + algoName === "AES-CTR" + ) { + const keyBytes = Buffer.allocUnsafe(req.algorithm.length / 8); + randomFillSync(keyBytes); + return JSON.stringify({ + key: { + type: "secret", + algorithm: req.algorithm, + extractable: req.extractable, + usages: req.usages, + _raw: keyBytes.toString("base64"), + }, + }); + } + if (algoName === "HMAC") { + const hashName = + typeof req.algorithm.hash === "string" + ? req.algorithm.hash + : req.algorithm.hash.name; + const hashLens: Record = { + "SHA-1": 20, + "SHA-256": 32, + "SHA-384": 48, + "SHA-512": 64, + }; + const len = req.algorithm.length + ? req.algorithm.length / 8 + : hashLens[hashName] || 32; + const keyBytes = Buffer.allocUnsafe(len); + randomFillSync(keyBytes); + return JSON.stringify({ + key: { + type: "secret", + algorithm: req.algorithm, + extractable: req.extractable, + usages: req.usages, + _raw: keyBytes.toString("base64"), + }, + }); + } + if ( + algoName === "RSASSA-PKCS1-v1_5" || + algoName === "RSA-OAEP" || + algoName === "RSA-PSS" + ) { + let publicExponent = 65537; + if (req.algorithm.publicExponent) { + const expBytes = Buffer.from( + req.algorithm.publicExponent, + "base64", + ); + publicExponent = 0; + for (const b of expBytes) { + publicExponent = (publicExponent << 8) | b; + } + } + const { publicKey, privateKey } = generateKeyPairSync("rsa", { + modulusLength: req.algorithm.modulusLength || 2048, + publicExponent, + publicKeyEncoding: { + type: "spki" as const, + format: "pem" as const, + }, + privateKeyEncoding: { + type: "pkcs8" as const, + format: "pem" as const, + }, + }); + return JSON.stringify({ + publicKey: { + type: "public", + algorithm: req.algorithm, + extractable: req.extractable, + usages: req.usages.filter((u: string) => + ["verify", "encrypt", "wrapKey"].includes(u), + ), + _pem: publicKey, + }, + privateKey: { + type: "private", + algorithm: req.algorithm, + extractable: req.extractable, + usages: req.usages.filter((u: string) => + ["sign", "decrypt", "unwrapKey"].includes(u), + ), + _pem: privateKey, + }, + }); + } + throw new Error(`Unsupported key algorithm: ${algoName}`); + } + case "importKey": { + const { format, keyData, algorithm, extractable, usages } = req; + if (format === "raw") { + return JSON.stringify({ + key: { + type: "secret", + algorithm, + extractable, + usages, + _raw: keyData, + }, + }); + } + if (format === "jwk") { + const jwk = + typeof keyData === "string" ? JSON.parse(keyData) : keyData; + if (jwk.kty === "oct") { + const raw = Buffer.from(jwk.k, "base64url"); + return JSON.stringify({ + key: { + type: "secret", + algorithm, + extractable, + usages, + _raw: raw.toString("base64"), + }, + }); + } + if (jwk.d) { + const keyObj = createPrivateKey({ key: jwk, format: "jwk" }); + const pem = keyObj.export({ + type: "pkcs8", + format: "pem", + }) as string; + return JSON.stringify({ + key: { type: "private", algorithm, extractable, usages, _pem: pem }, + }); + } + const keyObj = createPublicKey({ key: jwk, format: "jwk" }); + const pem = keyObj.export({ type: "spki", format: "pem" }) as string; + return JSON.stringify({ + key: { type: "public", algorithm, extractable, usages, _pem: pem }, + }); + } + if (format === "pkcs8") { + const keyBuf = Buffer.from(keyData, "base64"); + const keyObj = createPrivateKey({ + key: keyBuf, + format: "der", + type: "pkcs8", + }); + const pem = keyObj.export({ + type: "pkcs8", + format: "pem", + }) as string; + return JSON.stringify({ + key: { type: "private", algorithm, extractable, usages, _pem: pem }, + }); + } + if (format === "spki") { + const keyBuf = Buffer.from(keyData, "base64"); + const keyObj = createPublicKey({ + key: keyBuf, + format: "der", + type: "spki", + }); + const pem = keyObj.export({ type: "spki", format: "pem" }) as string; + return JSON.stringify({ + key: { type: "public", algorithm, extractable, usages, _pem: pem }, + }); + } + throw new Error(`Unsupported import format: ${format}`); + } + case "exportKey": { + const { format, key } = req; + if (format === "raw") { + if (!key._raw) + throw new Error("Cannot export asymmetric key as raw"); + return JSON.stringify({ + data: key._raw, + }); + } + if (format === "jwk") { + if (key._raw) { + const raw = Buffer.from(key._raw, "base64"); + return JSON.stringify({ + jwk: { + kty: "oct", + k: raw.toString("base64url"), + ext: key.extractable, + key_ops: key.usages, + }, + }); + } + const keyObj = + key.type === "private" + ? createPrivateKey(key._pem) + : createPublicKey(key._pem); + return JSON.stringify({ + jwk: keyObj.export({ format: "jwk" }), + }); + } + if (format === "pkcs8") { + if (key.type !== "private") + throw new Error("Cannot export non-private key as pkcs8"); + const keyObj = createPrivateKey(key._pem); + const der = keyObj.export({ + type: "pkcs8", + format: "der", + }) as Buffer; + return JSON.stringify({ data: der.toString("base64") }); + } + if (format === "spki") { + const keyObj = + key.type === "private" + ? createPublicKey(createPrivateKey(key._pem)) + : createPublicKey(key._pem); + const der = keyObj.export({ + type: "spki", + format: "der", + }) as Buffer; + return JSON.stringify({ data: der.toString("base64") }); + } + throw new Error(`Unsupported export format: ${format}`); + } + case "encrypt": { + const { algorithm, key, data } = req; + const rawKey = Buffer.from(key._raw, "base64"); + const plaintext = Buffer.from(data, "base64"); + const algoName = algorithm.name; + if (algoName === "AES-GCM") { + const iv = Buffer.from(algorithm.iv, "base64"); + const tagLength = (algorithm.tagLength || 128) / 8; + const cipher = createCipheriv( + `aes-${rawKey.length * 8}-gcm` as any, + rawKey, + iv, + { authTagLength: tagLength } as any, + ) as any; + if (algorithm.additionalData) { + cipher.setAAD(Buffer.from(algorithm.additionalData, "base64")); + } + const encrypted = Buffer.concat([ + cipher.update(plaintext), + cipher.final(), + ]); + const authTag = cipher.getAuthTag(); + return JSON.stringify({ + data: Buffer.concat([encrypted, authTag]).toString("base64"), + }); + } + if (algoName === "AES-CBC") { + const iv = Buffer.from(algorithm.iv, "base64"); + const cipher = createCipheriv( + `aes-${rawKey.length * 8}-cbc` as any, + rawKey, + iv, + ); + const encrypted = Buffer.concat([ + cipher.update(plaintext), + cipher.final(), + ]); + return JSON.stringify({ data: encrypted.toString("base64") }); + } + throw new Error(`Unsupported encrypt algorithm: ${algoName}`); + } + case "decrypt": { + const { algorithm, key, data } = req; + const rawKey = Buffer.from(key._raw, "base64"); + const ciphertext = Buffer.from(data, "base64"); + const algoName = algorithm.name; + if (algoName === "AES-GCM") { + const iv = Buffer.from(algorithm.iv, "base64"); + const tagLength = (algorithm.tagLength || 128) / 8; + const encData = ciphertext.subarray( + 0, + ciphertext.length - tagLength, + ); + const authTag = ciphertext.subarray( + ciphertext.length - tagLength, + ); + const decipher = createDecipheriv( + `aes-${rawKey.length * 8}-gcm` as any, + rawKey, + iv, + { authTagLength: tagLength } as any, + ) as any; + decipher.setAuthTag(authTag); + if (algorithm.additionalData) { + decipher.setAAD( + Buffer.from(algorithm.additionalData, "base64"), + ); + } + const decrypted = Buffer.concat([ + decipher.update(encData), + decipher.final(), + ]); + return JSON.stringify({ data: decrypted.toString("base64") }); + } + if (algoName === "AES-CBC") { + const iv = Buffer.from(algorithm.iv, "base64"); + const decipher = createDecipheriv( + `aes-${rawKey.length * 8}-cbc` as any, + rawKey, + iv, + ); + const decrypted = Buffer.concat([ + decipher.update(ciphertext), + decipher.final(), + ]); + return JSON.stringify({ data: decrypted.toString("base64") }); + } + throw new Error(`Unsupported decrypt algorithm: ${algoName}`); + } + case "deriveBits": { + const { algorithm, key, length } = req; + const algoName = algorithm.name; + if (algoName === "PBKDF2") { + const password = Buffer.from(key._raw, "base64"); + const salt = Buffer.from(algorithm.salt, "base64"); + const iterations = algorithm.iterations; + const hashAlgo = normalizeHash(algorithm.hash); + const keylen = length / 8; + return JSON.stringify({ + data: pbkdf2Sync( + password, + salt, + iterations, + keylen, + hashAlgo, + ).toString("base64"), + }); + } + throw new Error(`Unsupported deriveBits algorithm: ${algoName}`); + } + case "sign": { + const { key, data } = req; + const dataBytes = Buffer.from(data, "base64"); + const algoName = key.algorithm.name; + if (algoName === "HMAC") { + const rawKey = Buffer.from(key._raw, "base64"); + const hashAlgo = normalizeHash(key.algorithm.hash); + return JSON.stringify({ + data: createHmac(hashAlgo, rawKey) + .update(dataBytes) + .digest("base64"), + }); + } + if (algoName === "RSASSA-PKCS1-v1_5") { + const hashAlgo = normalizeHash(key.algorithm.hash); + const pkey = createPrivateKey(key._pem); + return JSON.stringify({ + data: sign(hashAlgo, dataBytes, pkey).toString("base64"), + }); + } + throw new Error(`Unsupported sign algorithm: ${algoName}`); + } + case "verify": { + const { key, signature, data } = req; + const dataBytes = Buffer.from(data, "base64"); + const sigBytes = Buffer.from(signature, "base64"); + const algoName = key.algorithm.name; + if (algoName === "HMAC") { + const rawKey = Buffer.from(key._raw, "base64"); + const hashAlgo = normalizeHash(key.algorithm.hash); + const expected = createHmac(hashAlgo, rawKey) + .update(dataBytes) + .digest(); + if (expected.length !== sigBytes.length) + return JSON.stringify({ result: false }); + return JSON.stringify({ + result: timingSafeEqual(expected, sigBytes), + }); + } + if (algoName === "RSASSA-PKCS1-v1_5") { + const hashAlgo = normalizeHash(key.algorithm.hash); + const pkey = createPublicKey(key._pem); + return JSON.stringify({ + result: verify(hashAlgo, dataBytes, pkey, sigBytes), + }); + } + throw new Error(`Unsupported verify algorithm: ${algoName}`); + } + default: + throw new Error(`Unsupported subtle operation: ${req.op}`); + } + }); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.cryptoSubtle, cryptoSubtleRef); + + // Set up fs References (stubbed if filesystem is disabled) + { + const fs = deps.filesystem; + const base64Limit = deps.bridgeBase64TransferLimitBytes; + const fsJsonPayloadLimit = deps.isolateJsonPayloadLimitBytes; + + // Create individual References for each fs operation + const readFileRef = new ivm.Reference(async (path: string) => { + checkBridgeBudget(deps); + const text = await fs.readTextFile(path); + assertTextPayloadSize( + `fs.readFile ${path}`, + text, + fsJsonPayloadLimit, + ); + return text; + }); + const writeFileRef = new ivm.Reference( + async (path: string, content: string) => { + checkBridgeBudget(deps); + await fs.writeFile(path, content); + }, + ); + // Binary file operations using base64 encoding + const readFileBinaryRef = new ivm.Reference(async (path: string) => { + checkBridgeBudget(deps); + const data = await fs.readFile(path); + assertPayloadByteLength( + `fs.readFileBinary ${path}`, + getBase64EncodedByteLength(data.byteLength), + base64Limit, + ); + // Convert to base64 for transfer across isolate boundary + return Buffer.from(data).toString("base64"); + }); + const writeFileBinaryRef = new ivm.Reference( + async (path: string, base64Content: string) => { + checkBridgeBudget(deps); + assertTextPayloadSize( + `fs.writeFileBinary ${path}`, + base64Content, + base64Limit, + ); + // Decode base64 and write as binary + const data = Buffer.from(base64Content, "base64"); + await fs.writeFile(path, data); + }, + ); + const readDirRef = new ivm.Reference(async (path: string) => { + checkBridgeBudget(deps); + const entries = await fs.readDirWithTypes(path); + // Validate payload size before transfer + const json = JSON.stringify(entries); + assertTextPayloadSize(`fs.readDir ${path}`, json, fsJsonPayloadLimit); + return json; + }); + const mkdirRef = new ivm.Reference(async (path: string) => { + checkBridgeBudget(deps); + await mkdir(fs, path); + }); + const rmdirRef = new ivm.Reference(async (path: string) => { + checkBridgeBudget(deps); + await fs.removeDir(path); + }); + const existsRef = new ivm.Reference(async (path: string) => { + checkBridgeBudget(deps); + return fs.exists(path); + }); + const statRef = new ivm.Reference(async (path: string) => { + checkBridgeBudget(deps); + const statInfo = await fs.stat(path); + // Return as JSON string for transfer + return JSON.stringify({ + mode: statInfo.mode, + size: statInfo.size, + isDirectory: statInfo.isDirectory, + atimeMs: statInfo.atimeMs, + mtimeMs: statInfo.mtimeMs, + ctimeMs: statInfo.ctimeMs, + birthtimeMs: statInfo.birthtimeMs, + }); + }); + const unlinkRef = new ivm.Reference(async (path: string) => { + checkBridgeBudget(deps); + await fs.removeFile(path); + }); + const renameRef = new ivm.Reference( + async (oldPath: string, newPath: string) => { + checkBridgeBudget(deps); + await fs.rename(oldPath, newPath); + }, + ); + const chmodRef = new ivm.Reference( + async (path: string, mode: number) => { + checkBridgeBudget(deps); + await fs.chmod(path, mode); + }, + ); + const chownRef = new ivm.Reference( + async (path: string, uid: number, gid: number) => { + checkBridgeBudget(deps); + await fs.chown(path, uid, gid); + }, + ); + const linkRef = new ivm.Reference( + async (oldPath: string, newPath: string) => { + checkBridgeBudget(deps); + await fs.link(oldPath, newPath); + }, + ); + const symlinkRef = new ivm.Reference( + async (target: string, linkPath: string) => { + checkBridgeBudget(deps); + await fs.symlink(target, linkPath); + }, + ); + const readlinkRef = new ivm.Reference(async (path: string) => { + checkBridgeBudget(deps); + return fs.readlink(path); + }); + const lstatRef = new ivm.Reference(async (path: string) => { + checkBridgeBudget(deps); + const statInfo = await fs.lstat(path); + return JSON.stringify({ + mode: statInfo.mode, + size: statInfo.size, + isDirectory: statInfo.isDirectory, + isSymbolicLink: statInfo.isSymbolicLink, + atimeMs: statInfo.atimeMs, + mtimeMs: statInfo.mtimeMs, + ctimeMs: statInfo.ctimeMs, + birthtimeMs: statInfo.birthtimeMs, + }); + }); + const truncateRef = new ivm.Reference( + async (path: string, length: number) => { + checkBridgeBudget(deps); + await fs.truncate(path, length); + }, + ); + const utimesRef = new ivm.Reference( + async (path: string, atime: number, mtime: number) => { + checkBridgeBudget(deps); + await fs.utimes(path, atime, mtime); + }, + ); + + // Set up each fs Reference individually in the isolate + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsReadFile, readFileRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsWriteFile, writeFileRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsReadFileBinary, readFileBinaryRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsWriteFileBinary, writeFileBinaryRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsReadDir, readDirRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsMkdir, mkdirRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsRmdir, rmdirRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsExists, existsRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsStat, statRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsUnlink, unlinkRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsRename, renameRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsChmod, chmodRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsChown, chownRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsLink, linkRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsSymlink, symlinkRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsReadlink, readlinkRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsLstat, lstatRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsTruncate, truncateRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.fsUtimes, utimesRef); + + // Create the _fs object inside the isolate. + await context.eval(getIsolateRuntimeSource("setupFsFacade")); + } + + // Set up child_process References (stubbed when disabled) + { + const executor = deps.commandExecutor ?? createCommandExecutorStub(); + let nextSessionId = 1; + const sessions = deps.activeChildProcesses; + const jsonPayloadLimit = deps.isolateJsonPayloadLimitBytes; + + // Lazy-initialized dispatcher reference from isolate + // We can't get this upfront because _childProcessDispatch is set by bridge code + // which loads AFTER these references are set up + let dispatchRef: ivm.Reference< + ( + sessionId: number, + type: "stdout" | "stderr" | "exit", + data: Uint8Array | number, + ) => void + > | null = null; + + const getDispatchRef = () => { + if (!dispatchRef) { + dispatchRef = context.global.getSync( + RUNTIME_BRIDGE_GLOBAL_KEYS.childProcessDispatch, + { + reference: true, + }, + ) as ivm.Reference< + ( + sessionId: number, + type: "stdout" | "stderr" | "exit", + data: Uint8Array | number, + ) => void + >; + } + return dispatchRef!; + }; + + // Start a spawn - returns session ID + const spawnStartRef = new ivm.Reference( + (command: string, argsJson: string, optionsJson: string): number => { + checkBridgeBudget(deps); + if (deps.maxChildProcesses !== undefined && deps.budgetState.childProcesses >= deps.maxChildProcesses) { + throw new Error(`${RESOURCE_BUDGET_ERROR_CODE}: maximum child processes exceeded`); + } + deps.budgetState.childProcesses++; + const args = parseJsonWithLimit( + "child_process.spawn args", + argsJson, + jsonPayloadLimit, + ); + const options = parseJsonWithLimit<{ + cwd?: string; + env?: Record; + }>("child_process.spawn options", optionsJson, jsonPayloadLimit); + const sessionId = nextSessionId++; + + // Use init-time filtered env when no explicit env — sandbox + // process.env mutations must not propagate to children + const childEnv = stripDangerousEnv(options.env ?? deps.processConfig.env); + + const proc = executor.spawn(command, args, { + cwd: options.cwd, + env: childEnv, + onStdout: (data) => { + getDispatchRef().applySync( + undefined, + [sessionId, "stdout", data], + { arguments: { copy: true } }, + ); + }, + onStderr: (data) => { + getDispatchRef().applySync( + undefined, + [sessionId, "stderr", data], + { arguments: { copy: true } }, + ); + }, + }); + + proc.wait().then((code) => { + getDispatchRef().applySync(undefined, [sessionId, "exit", code]); + sessions.delete(sessionId); + }); + + sessions.set(sessionId, proc); + return sessionId; + }, + ); + + // Stdin write + const stdinWriteRef = new ivm.Reference( + (sessionId: number, data: Uint8Array): void => { + sessions.get(sessionId)?.writeStdin(data); + }, + ); + + // Stdin close + const stdinCloseRef = new ivm.Reference((sessionId: number): void => { + sessions.get(sessionId)?.closeStdin(); + }); + + // Kill + const killRef = new ivm.Reference( + (sessionId: number, signal: number): void => { + sessions.get(sessionId)?.kill(signal); + }, + ); + + // Synchronous spawn - blocks until process exits, returns all output + // Used by execSync/spawnSync which need to wait for completion + const spawnSyncRef = new ivm.Reference( + async ( + command: string, + argsJson: string, + optionsJson: string, + ): Promise => { + checkBridgeBudget(deps); + if (deps.maxChildProcesses !== undefined && deps.budgetState.childProcesses >= deps.maxChildProcesses) { + throw new Error(`${RESOURCE_BUDGET_ERROR_CODE}: maximum child processes exceeded`); + } + deps.budgetState.childProcesses++; + const args = parseJsonWithLimit( + "child_process.spawnSync args", + argsJson, + jsonPayloadLimit, + ); + const options = parseJsonWithLimit<{ + cwd?: string; + env?: Record; + maxBuffer?: number; + }>("child_process.spawnSync options", optionsJson, jsonPayloadLimit); + + // Collect stdout/stderr with maxBuffer enforcement (default 1MB) + const maxBuffer = options.maxBuffer ?? 1024 * 1024; + const stdoutChunks: Uint8Array[] = []; + const stderrChunks: Uint8Array[] = []; + let stdoutBytes = 0; + let stderrBytes = 0; + let maxBufferExceeded = false; + + // Use init-time filtered env when no explicit env — sandbox + // process.env mutations must not propagate to children + const childEnv = stripDangerousEnv(options.env ?? deps.processConfig.env); + + const proc = executor.spawn(command, args, { + cwd: options.cwd, + env: childEnv, + onStdout: (data) => { + if (maxBufferExceeded) return; + stdoutBytes += data.length; + if (maxBuffer !== undefined && stdoutBytes > maxBuffer) { + maxBufferExceeded = true; + proc.kill(15); + return; + } + stdoutChunks.push(data); + }, + onStderr: (data) => { + if (maxBufferExceeded) return; + stderrBytes += data.length; + if (maxBuffer !== undefined && stderrBytes > maxBuffer) { + maxBufferExceeded = true; + proc.kill(15); + return; + } + stderrChunks.push(data); + }, + }); + + // Wait for process to exit + const exitCode = await proc.wait(); + + // Combine chunks into strings + const decoder = new TextDecoder(); + const stdout = stdoutChunks.map((c) => decoder.decode(c)).join(""); + const stderr = stderrChunks.map((c) => decoder.decode(c)).join(""); + + return JSON.stringify({ stdout, stderr, code: exitCode, maxBufferExceeded }); + }, + ); + + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.childProcessSpawnStart, spawnStartRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.childProcessStdinWrite, stdinWriteRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.childProcessStdinClose, stdinCloseRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.childProcessKill, killRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.childProcessSpawnSync, spawnSyncRef); + } + + // Set up network References (stubbed when disabled) + { + const adapter = deps.networkAdapter ?? createNetworkStub(); + const jsonPayloadLimit = deps.isolateJsonPayloadLimitBytes; + + // Reference for fetch - returns JSON string for transfer + const networkFetchRef = new ivm.Reference( + (url: string, optionsJson: string): Promise => { + checkBridgeBudget(deps); + const options = parseJsonWithLimit<{ + method?: string; + headers?: Record; + body?: string | null; + }>("network.fetch options", optionsJson, jsonPayloadLimit); + return adapter + .fetch(url, options) + .then((result) => { + const json = JSON.stringify(result); + assertTextPayloadSize("network.fetch response", json, jsonPayloadLimit); + return json; + }); + }, + ); + + // Reference for DNS lookup - returns JSON string for transfer + const networkDnsLookupRef = new ivm.Reference( + async (hostname: string): Promise => { + checkBridgeBudget(deps); + const result = await adapter.dnsLookup(hostname); + return JSON.stringify(result); + }, + ); + + // Reference for HTTP request - returns JSON string for transfer + const networkHttpRequestRef = new ivm.Reference( + (url: string, optionsJson: string): Promise => { + checkBridgeBudget(deps); + const options = parseJsonWithLimit<{ + method?: string; + headers?: Record; + body?: string | null; + rejectUnauthorized?: boolean; + }>("network.httpRequest options", optionsJson, jsonPayloadLimit); + return adapter + .httpRequest(url, options) + .then((result) => { + const json = JSON.stringify(result); + assertTextPayloadSize("network.httpRequest response", json, jsonPayloadLimit); + return json; + }); + }, + ); + + // Track server IDs created in this context for ownership validation + const ownedHttpServers = new Set(); + + // Lazy dispatcher reference for in-sandbox HTTP server callbacks + let httpServerDispatchRef: ivm.Reference< + (serverId: number, requestJson: string) => Promise + > | null = null; + + const getHttpServerDispatchRef = () => { + if (!httpServerDispatchRef) { + httpServerDispatchRef = context.global.getSync( + RUNTIME_BRIDGE_GLOBAL_KEYS.httpServerDispatch, + { + reference: true, + }, + ) as ivm.Reference< + (serverId: number, requestJson: string) => Promise + >; + } + return httpServerDispatchRef!; + }; + + // Lazy dispatcher reference for upgrade events + let httpServerUpgradeDispatchRef: ivm.Reference< + (serverId: number, requestJson: string, headBase64: string, socketId: number) => void + > | null = null; + + const getUpgradeDispatchRef = () => { + if (!httpServerUpgradeDispatchRef) { + httpServerUpgradeDispatchRef = context.global.getSync( + RUNTIME_BRIDGE_GLOBAL_KEYS.httpServerUpgradeDispatch, + { reference: true }, + ) as ivm.Reference< + (serverId: number, requestJson: string, headBase64: string, socketId: number) => void + >; + } + return httpServerUpgradeDispatchRef!; + }; + + // Lazy dispatcher references for upgrade socket data push + let upgradeSocketDataRef: ivm.Reference< + (socketId: number, dataBase64: string) => void + > | null = null; + + const getUpgradeSocketDataRef = () => { + if (!upgradeSocketDataRef) { + upgradeSocketDataRef = context.global.getSync( + RUNTIME_BRIDGE_GLOBAL_KEYS.upgradeSocketData, + { reference: true }, + ) as ivm.Reference< + (socketId: number, dataBase64: string) => void + >; + } + return upgradeSocketDataRef!; + }; + + let upgradeSocketEndDispatchRef: ivm.Reference< + (socketId: number) => void + > | null = null; + + const getUpgradeSocketEndRef = () => { + if (!upgradeSocketEndDispatchRef) { + upgradeSocketEndDispatchRef = context.global.getSync( + RUNTIME_BRIDGE_GLOBAL_KEYS.upgradeSocketEnd, + { reference: true }, + ) as ivm.Reference<(socketId: number) => void>; + } + return upgradeSocketEndDispatchRef!; + }; + + // Reference for starting an in-sandbox HTTP server + const networkHttpServerListenRef = new ivm.Reference( + (optionsJson: string): Promise => { + if (!adapter.httpServerListen) { + throw new Error( + "http.createServer requires NetworkAdapter.httpServerListen support", + ); + } + + const options = parseJsonWithLimit<{ + serverId: number; + port?: number; + hostname?: string; + }>("network.httpServer.listen options", optionsJson, jsonPayloadLimit); + + return (async () => { + const result = await adapter.httpServerListen!({ + serverId: options.serverId, + port: options.port, + hostname: options.hostname, + onRequest: async (request) => { + const requestJson = JSON.stringify(request); + + const responseJson = await getHttpServerDispatchRef().apply( + undefined, + [options.serverId, requestJson], + { result: { promise: true } }, + ); + return parseJsonWithLimit<{ + status: number; + headers?: Array<[string, string]>; + body?: string; + bodyEncoding?: "utf8" | "base64"; + }>("network.httpServer response", String(responseJson), jsonPayloadLimit); + }, + onUpgrade: (request, head, socketId) => { + const requestJson = JSON.stringify(request); + getUpgradeDispatchRef().applySync( + undefined, + [options.serverId, requestJson, head, socketId], + ); + }, + onUpgradeSocketData: (socketId, dataBase64) => { + getUpgradeSocketDataRef().applySync( + undefined, + [socketId, dataBase64], + ); + }, + onUpgradeSocketEnd: (socketId) => { + getUpgradeSocketEndRef().applySync( + undefined, + [socketId], + ); + }, + }); + ownedHttpServers.add(options.serverId); + deps.activeHttpServerIds.add(options.serverId); + return JSON.stringify(result); + })(); + }, + ); + + // Reference for closing an in-sandbox HTTP server + const networkHttpServerCloseRef = new ivm.Reference( + (serverId: number): Promise => { + if (!adapter.httpServerClose) { + throw new Error( + "http.createServer close requires NetworkAdapter.httpServerClose support", + ); + } + // Ownership check: only allow closing servers created in this context + if (!ownedHttpServers.has(serverId)) { + throw new Error( + `Cannot close server ${serverId}: not owned by this execution context`, + ); + } + return adapter.httpServerClose(serverId).then(() => { + ownedHttpServers.delete(serverId); + deps.activeHttpServerIds.delete(serverId); + }); + }, + ); + + // References for upgrade socket write/end/destroy (sandbox → host) + const upgradeSocketWriteRef = new ivm.Reference( + (socketId: number, dataBase64: string): void => { + adapter.upgradeSocketWrite?.(socketId, dataBase64); + }, + ); + + const upgradeSocketEndRef = new ivm.Reference( + (socketId: number): void => { + adapter.upgradeSocketEnd?.(socketId); + }, + ); + + const upgradeSocketDestroyRef = new ivm.Reference( + (socketId: number): void => { + adapter.upgradeSocketDestroy?.(socketId); + }, + ); + + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.networkFetchRaw, networkFetchRef); + await jail.set( + HOST_BRIDGE_GLOBAL_KEYS.networkDnsLookupRaw, + networkDnsLookupRef, + ); + await jail.set( + HOST_BRIDGE_GLOBAL_KEYS.networkHttpRequestRaw, + networkHttpRequestRef, + ); + await jail.set( + HOST_BRIDGE_GLOBAL_KEYS.networkHttpServerListenRaw, + networkHttpServerListenRef, + ); + await jail.set( + HOST_BRIDGE_GLOBAL_KEYS.networkHttpServerCloseRaw, + networkHttpServerCloseRef, + ); + await jail.set( + HOST_BRIDGE_GLOBAL_KEYS.upgradeSocketWriteRaw, + upgradeSocketWriteRef, + ); + await jail.set( + HOST_BRIDGE_GLOBAL_KEYS.upgradeSocketEndRaw, + upgradeSocketEndRef, + ); + await jail.set( + HOST_BRIDGE_GLOBAL_KEYS.upgradeSocketDestroyRaw, + upgradeSocketDestroyRef, + ); + + // Register client-side upgrade socket callbacks so httpRequest can push data + adapter.setUpgradeSocketCallbacks?.({ + onData: (socketId, dataBase64) => { + getUpgradeSocketDataRef().applySync( + undefined, + [socketId, dataBase64], + ); + }, + onEnd: (socketId) => { + getUpgradeSocketEndRef().applySync( + undefined, + [socketId], + ); + }, + }); + + // TCP socket bridge refs (net module) + let netSocketDispatchRef: ivm.Reference< + (socketId: number, type: string, data: string) => void + > | null = null; + + const getNetSocketDispatchRef = () => { + if (!netSocketDispatchRef) { + netSocketDispatchRef = context.global.getSync( + RUNTIME_BRIDGE_GLOBAL_KEYS.netSocketDispatch, + { reference: true }, + ) as ivm.Reference< + (socketId: number, type: string, data: string) => void + >; + } + return netSocketDispatchRef!; + }; + + const dispatchNetEvent = (socketId: number, type: string, data: string) => { + try { + getNetSocketDispatchRef().applySync( + undefined, + [socketId, type, data], + ); + } catch { + // Isolate may have been disposed; silently drop the event + } + }; + + const netSocketConnectRef = new ivm.Reference( + (host: string, port: number): number => { + checkBridgeBudget(deps); + // Use adapter-returned socketId for all dispatch/write/end/destroy + let socketId = -1; + socketId = adapter.netSocketConnect?.(host, port, { + onConnect: () => dispatchNetEvent(socketId, "connect", ""), + onData: (dataBase64) => dispatchNetEvent(socketId, "data", dataBase64), + onEnd: () => dispatchNetEvent(socketId, "end", ""), + onError: (message) => dispatchNetEvent(socketId, "error", message), + onClose: (hadError) => dispatchNetEvent(socketId, "close", hadError ? "1" : "0"), + }) ?? -1; + return socketId; + }, + ); + + const netSocketWriteRef = new ivm.Reference( + (socketId: number, dataBase64: string): void => { + adapter.netSocketWrite?.(socketId, dataBase64); + }, + ); + + const netSocketEndRef = new ivm.Reference( + (socketId: number): void => { + adapter.netSocketEnd?.(socketId); + }, + ); + + const netSocketDestroyRef = new ivm.Reference( + (socketId: number): void => { + adapter.netSocketDestroy?.(socketId); + }, + ); + + const netSocketUpgradeTlsRef = new ivm.Reference( + (socketId: number, optionsJson: string): void => { + checkBridgeBudget(deps); + adapter.netSocketUpgradeTls?.(socketId, optionsJson, { + onData: (dataBase64) => dispatchNetEvent(socketId, "data", dataBase64), + onEnd: () => dispatchNetEvent(socketId, "end", ""), + onError: (message) => dispatchNetEvent(socketId, "error", message), + onClose: (hadError) => dispatchNetEvent(socketId, "close", hadError ? "1" : "0"), + onSecureConnect: () => dispatchNetEvent(socketId, "secureConnect", ""), + }); + }, + ); + + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.netSocketConnectRaw, netSocketConnectRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.netSocketWriteRaw, netSocketWriteRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.netSocketEndRaw, netSocketEndRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.netSocketDestroyRaw, netSocketDestroyRef); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.netSocketUpgradeTlsRaw, netSocketUpgradeTlsRef); + } + + // Set up PTY setRawMode bridge ref when stdin is a TTY + if (deps.processConfig.stdinIsTTY) { + const onSetRawMode = deps.onPtySetRawMode; + const ptySetRawModeRef = new ivm.Reference((mode: boolean): void => { + if (onSetRawMode) onSetRawMode(mode); + }); + await jail.set(HOST_BRIDGE_GLOBAL_KEYS.ptySetRawMode, ptySetRawModeRef); + } + + // Install isolate-global descriptor helpers before runtime bootstrap scripts. + await context.eval(getIsolateRuntimeSource("globalExposureHelpers")); + // Set up globals needed by the bridge BEFORE loading it. + const initialCwd = deps.processConfig.cwd ?? "/"; await jail.set( "__runtimeBridgeSetupConfig", { - initialCwd: deps.processConfig.cwd ?? "/", + initialCwd, jsonPayloadLimitBytes: deps.isolateJsonPayloadLimitBytes, payloadLimitErrorCode: PAYLOAD_LIMIT_ERROR_CODE, }, @@ -266,6 +1701,7 @@ export async function setupRequire( ); await context.eval(getInitialBridgeGlobalsSetupCode()); + // Load the bridge bundle which sets up all polyfill modules. await jail.set( HOST_BRIDGE_GLOBAL_KEYS.processConfig, createProcessConfigForExecution(deps.processConfig, timingMitigation, frozenTimeMs), @@ -278,18 +1714,18 @@ export async function setupRequire( await context.eval(getBridgeAttachCode()); await applyTimingMitigation(context, timingMitigation, frozenTimeMs); + // Set up the require system with dynamic CommonJS resolution await context.eval(getRequireSetupCode()); + // module and process are already initialized by the bridge } /** * Set up ESM-compatible globals (process, Buffer, etc.) - * - * @deprecated Legacy function for isolated-vm contexts. Use bridge-handlers.ts for V8 runtime. */ export async function setupESMGlobals( deps: BridgeDeps, - context: LegacyContext, - jail: LegacyReference, + context: ivm.Context, + jail: ivm.Reference>, timingMitigation: TimingMitigation, frozenTimeMs: number, ): Promise { @@ -309,7 +1745,7 @@ export function createProcessConfigForExecution( } async function applyTimingMitigation( - context: LegacyContext, + context: ivm.Context, timingMitigation: TimingMitigation, frozenTimeMs: number, ): Promise { diff --git a/packages/secure-exec-node/src/driver.ts b/packages/secure-exec-node/src/driver.ts index f6e563de..ef124d80 100644 --- a/packages/secure-exec-node/src/driver.ts +++ b/packages/secure-exec-node/src/driver.ts @@ -40,9 +40,6 @@ export interface NodeDriverOptions { export interface NodeRuntimeDriverFactoryOptions { createIsolate?(memoryLimit: number): unknown; - /** V8 runtime process to use for sessions. - * If omitted, uses the global shared process (current behavior). */ - v8Runtime?: import("@secure-exec/v8").V8Runtime; } /** Thin VFS adapter that delegates directly to `node:fs/promises`. */ @@ -780,7 +777,6 @@ export function createNodeRuntimeDriverFactory( new NodeExecutionDriver({ ...runtimeOptions, createIsolate: options.createIsolate, - v8Runtime: options.v8Runtime, }), }; } diff --git a/packages/secure-exec-node/src/esm-compiler.ts b/packages/secure-exec-node/src/esm-compiler.ts index 80fc7207..4b367329 100644 --- a/packages/secure-exec-node/src/esm-compiler.ts +++ b/packages/secure-exec-node/src/esm-compiler.ts @@ -1,3 +1,4 @@ +import ivm from "isolated-vm"; import { createBuiltinESMWrapper, getStaticBuiltinWrapperSource, @@ -27,13 +28,6 @@ import { import type { DriverDeps } from "./isolate-bootstrap.js"; import { getModuleFormat, resolveESMPath } from "./module-resolver.js"; -// Legacy types — isolated-vm has been removed. -/* eslint-disable @typescript-eslint/no-explicit-any */ -type LegacyContext = any; -type LegacyModule = any; -type LegacyReference<_T = unknown> = any; -/* eslint-enable @typescript-eslint/no-explicit-any */ - type CompilerDeps = Pick< DriverDeps, | "isolate" @@ -50,14 +44,12 @@ type CompilerDeps = Pick< /** * Load and compile an ESM module, handling both ESM and CJS sources. - * - * @deprecated Legacy function for isolated-vm. V8-based driver handles ESM natively. */ export async function compileESMModule( deps: CompilerDeps, filePath: string, - _context: LegacyContext, -): Promise { + _context: ivm.Context, +): Promise { // Check cache first const cached = deps.esmModuleCache.get(filePath); if (cached) { @@ -81,11 +73,13 @@ export async function compileESMModule( if (staticWrapperCode !== null) { code = staticWrapperCode; } else if (hostBuiltinNamedExports.length > 0) { + // Prefer the runtime builtin bridge when host exports are known. code = createBuiltinESMWrapper( runtimeBuiltinBinding, mergedBuiltinNamedExports, ); } else if (hasPolyfill(moduleName)) { + // Get polyfill code and wrap for ESM. let polyfillCode = polyfillCodeCache.get(moduleName); if (!polyfillCode) { polyfillCode = await bundlePolyfill(moduleName); @@ -108,21 +102,26 @@ export async function compileESMModule( ), ); } else { + // Fall back to the runtime require bridge for built-ins without + // dedicated polyfills so ESM named imports can still bind. code = createBuiltinESMWrapper( runtimeBuiltinBinding, mergedBuiltinNamedExports, ); } } else { + // Load from filesystem const source = await loadFile(filePath, deps.filesystem); if (source === null) { throw new Error(`Cannot load module: ${filePath}`); } + // Classify source module format using extension + package metadata. const moduleFormat = await getModuleFormat(deps, filePath, source); if (moduleFormat === "json") { code = "export default " + source + ";"; } else if (moduleFormat === "cjs") { + // Transform CommonJS modules into ESM default exports. code = wrapCJSForESMWithModulePath(source, filePath); } else { code = source; @@ -143,16 +142,16 @@ export async function compileESMModule( /** * Create the ESM resolver callback for module.instantiate(). - * - * @deprecated Legacy function for isolated-vm. V8-based driver handles ESM natively. */ export function createESMResolver( deps: CompilerDeps, - context: LegacyContext, -): (specifier: string, referrer: LegacyModule) => Promise { - return async (specifier: string, referrer: LegacyModule) => { + context: ivm.Context, +): (specifier: string, referrer: ivm.Module) => Promise { + return async (specifier: string, referrer: ivm.Module) => { + // O(1) reverse lookup via dedicated reverse cache const referrerPath = deps.esmModuleReverseCache.get(referrer) ?? "/"; + // Resolve the specifier const resolved = await resolveESMPath(deps, specifier, referrerPath); if (!resolved) { throw new Error( @@ -160,30 +159,32 @@ export function createESMResolver( ); } + // Compile and return the module return compileESMModule(deps, resolved, context); }; } /** * Run ESM code. - * - * @deprecated Legacy function for isolated-vm. V8-based driver handles ESM natively. */ export async function runESM( deps: CompilerDeps, code: string, - context: LegacyContext, + context: ivm.Context, filePath: string = "/.mjs", executionDeadlineMs?: number, ): Promise { + // Compile the entry module const entryModule = await deps.isolate.compileModule(code, { filename: filePath, }); deps.esmModuleCache.set(filePath, entryModule); deps.esmModuleReverseCache.set(entryModule, filePath); + // Instantiate with resolver (this resolves all dependencies) await entryModule.instantiate(context, createESMResolver(deps, context)); + // Evaluate before reading exports so namespace bindings are initialized. await runWithExecutionDeadline( entryModule.evaluate({ promise: true, @@ -192,16 +193,19 @@ export async function runESM( executionDeadlineMs, ); + // Set namespace on the isolate global so we can serialize a plain object. const jail = context.global; const namespaceGlobalKey = "__entryNamespace__"; await jail.set(namespaceGlobalKey, entryModule.namespace.derefInto()); try { + // Get namespace exports for run() to mirror module.exports semantics. return context.eval("Object.fromEntries(Object.entries(globalThis.__entryNamespace__))", { copy: true, ...getExecutionRunOptions(executionDeadlineMs), }); } finally { + // Clean up temporary namespace binding after copying exports. await jail.delete(namespaceGlobalKey); } } @@ -220,32 +224,34 @@ export function isAlreadyInstantiatedModuleError(error: unknown): boolean { /** * Get a cached namespace or evaluate the module on first dynamic import. - * - * @deprecated Legacy function for isolated-vm. V8-based driver handles dynamic imports natively. */ export async function resolveDynamicImportNamespace( deps: CompilerDeps, specifier: string, - context: LegacyContext, + context: ivm.Context, referrerPath: string, executionDeadlineMs?: number, -): Promise { +): Promise | null> { + // Get directly cached namespaces first. const cached = deps.dynamicImportCache.get(specifier); if (cached) { return cached; } + // Resolve before compile/evaluate. const resolved = await resolveESMPath(deps, specifier, referrerPath); if (!resolved) { return null; } + // Get resolved-path cache entry. const resolvedCached = deps.dynamicImportCache.get(resolved); if (resolvedCached) { deps.dynamicImportCache.set(specifier, resolvedCached); return resolvedCached; } + // Wait for an existing evaluation in progress. const pending = deps.dynamicImportPending.get(resolved); if (pending) { const namespace = await pending; @@ -253,7 +259,8 @@ export async function resolveDynamicImportNamespace( return namespace; } - const evaluateModule = (async (): Promise => { + // Evaluate once, then cache by both resolved path and original specifier. + const evaluateModule = (async (): Promise> => { const module = await compileESMModule(deps, resolved, context); try { await module.instantiate(context, createESMResolver(deps, context)); @@ -286,23 +293,24 @@ export async function resolveDynamicImportNamespace( /** * Pre-compile all static dynamic import specifiers found in the code. - * - * @deprecated Legacy function for isolated-vm. V8-based driver handles this natively. + * This must be called BEFORE running the code to avoid deadlocks. */ export async function precompileDynamicImports( deps: CompilerDeps, transformedCode: string, - context: LegacyContext, + context: ivm.Context, referrerPath: string = "/", ): Promise { const specifiers = extractDynamicImportSpecifiers(transformedCode); for (const specifier of specifiers) { + // Resolve the module path const resolved = await resolveESMPath(deps, specifier, referrerPath); if (!resolved) { - continue; + continue; // Skip unresolvable modules, error will be thrown at runtime } + // Compile only to warm module cache without triggering side effects. try { await compileESMModule(deps, resolved, context); } catch { @@ -313,20 +321,19 @@ export async function precompileDynamicImports( /** * Set up dynamic import() function for ESM. - * - * @deprecated Legacy function for isolated-vm. V8-based driver handles dynamic imports natively. + * Note: precompileDynamicImports must be called BEFORE running user code. + * Falls back to require() for CommonJS modules when not pre-compiled. */ export async function setupDynamicImport( deps: CompilerDeps, - context: LegacyContext, - jail: LegacyReference, + context: ivm.Context, + jail: ivm.Reference>, referrerPath: string = "/", executionDeadlineMs?: number, ): Promise { - const dynamicImportRef = { - apply: async (_ctx: unknown, args: unknown[]) => { - const specifier = args[0] as string; - const fromPath = args[1] as string | undefined; + // Set up async module resolution/evaluation for first dynamic import. + const dynamicImportRef = new ivm.Reference( + async (specifier: string, fromPath?: string) => { const effectiveReferrer = typeof fromPath === "string" && fromPath.length > 0 ? fromPath @@ -343,7 +350,7 @@ export async function setupDynamicImport( } return namespace.derefInto(); }, - }; + ); await jail.set(HOST_BRIDGE_GLOBAL_KEYS.dynamicImport, dynamicImportRef); await jail.set( @@ -351,5 +358,6 @@ export async function setupDynamicImport( { referrerPath }, { copy: true }, ); + // Resolve in ESM mode first and only use require() fallback for explicit CJS/JSON. await context.eval(getIsolateRuntimeSource("setupDynamicImport")); } diff --git a/packages/secure-exec-node/src/execution-driver.ts b/packages/secure-exec-node/src/execution-driver.ts index ec4ae8d1..8abaec6e 100644 --- a/packages/secure-exec-node/src/execution-driver.ts +++ b/packages/secure-exec-node/src/execution-driver.ts @@ -1,163 +1,34 @@ -import { createV8Runtime } from "@secure-exec/v8"; -import type { V8Runtime, V8Session, V8ExecutionResult } from "@secure-exec/v8"; - -// Shared V8 runtime — spawns one Rust child process, reused across all drivers. -// Sessions are isolated (separate V8 isolates in separate threads on the Rust side). -let sharedV8Runtime: V8Runtime | null = null; -let sharedV8RuntimePromise: Promise | null = null; - -async function getSharedV8Runtime(): Promise { - if (sharedV8Runtime) return sharedV8Runtime; - if (!sharedV8RuntimePromise) { - sharedV8RuntimePromise = createV8Runtime({ - warmupBridgeCode: composeBridgeCodeForWarmup(), - }).then((r) => { - sharedV8Runtime = r; - return r; - }); - } - return sharedV8RuntimePromise; -} -import { createResolutionCache, getIsolateRuntimeSource, TIMEOUT_ERROR_MESSAGE, TIMEOUT_EXIT_CODE } from "@secure-exec/core"; -import { getInitialBridgeGlobalsSetupCode } from "@secure-exec/core"; -import { getConsoleSetupCode } from "@secure-exec/core/internal/shared/console-formatter"; -import { getRequireSetupCode } from "@secure-exec/core/internal/shared/require-setup"; +import ivm from "isolated-vm"; +import { DEFAULT_TIMING_MITIGATION, TIMEOUT_ERROR_MESSAGE, TIMEOUT_EXIT_CODE, createIsolate as createDefaultIsolate, getExecutionDeadlineMs, getExecutionRunOptions, isExecutionTimeoutError, runWithExecutionDeadline } from "./isolate.js"; +import { getPathDir, createResolutionCache } from "@secure-exec/core"; import { createCommandExecutorStub, createFsStub, createNetworkStub, filterEnv, wrapCommandExecutor, wrapFileSystem, wrapNetworkAdapter } from "@secure-exec/core/internal/shared/permissions"; -import { transformDynamicImport } from "@secure-exec/core/internal/shared/esm-utils"; -import { HARDENED_NODE_CUSTOM_GLOBALS, MUTABLE_NODE_CUSTOM_GLOBALS } from "@secure-exec/core/internal/shared/global-exposure"; +import { executeWithRuntime } from "./execution.js"; import type { NetworkAdapter, RuntimeDriver } from "@secure-exec/core"; import type { StdioHook, ExecOptions, ExecResult, RunResult, TimingMitigation } from "@secure-exec/core/internal/shared/api-types"; -import { type DriverDeps, type NodeExecutionDriverOptions, createBudgetState, clearActiveHostTimers, killActiveChildProcesses, normalizePayloadLimit, getExecutionTimeoutMs, getTimingMitigation, DEFAULT_BRIDGE_BASE64_TRANSFER_BYTES, DEFAULT_ISOLATE_JSON_PAYLOAD_BYTES, DEFAULT_MAX_TIMERS, DEFAULT_MAX_HANDLES, DEFAULT_SANDBOX_CWD, DEFAULT_SANDBOX_HOME, DEFAULT_SANDBOX_TMPDIR, PAYLOAD_LIMIT_ERROR_CODE } from "./isolate-bootstrap.js"; -import { DEFAULT_TIMING_MITIGATION } from "./isolate.js"; -import { buildBridgeHandlers } from "./bridge-handlers.js"; -import { getIvmCompatShimSource } from "./ivm-compat.js"; -import { getRawBridgeCode, getBridgeAttachCode } from "./bridge-loader.js"; -import { createProcessConfigForExecution } from "./bridge-setup.js"; +import { type DriverDeps, type NodeExecutionDriverOptions, createBudgetState, clearActiveHostTimers, killActiveChildProcesses, normalizePayloadLimit, getExecutionTimeoutMs, getTimingMitigation, DEFAULT_BRIDGE_BASE64_TRANSFER_BYTES, DEFAULT_ISOLATE_JSON_PAYLOAD_BYTES, DEFAULT_MAX_TIMERS, DEFAULT_MAX_HANDLES, DEFAULT_SANDBOX_CWD, DEFAULT_SANDBOX_HOME, DEFAULT_SANDBOX_TMPDIR } from "./isolate-bootstrap.js"; +import { shouldRunAsESM } from "./module-resolver.js"; +import { precompileDynamicImports, runESM, setupDynamicImport } from "./esm-compiler.js"; +import { setupConsole, setupRequire, setupESMGlobals } from "./bridge-setup.js"; +import { applyExecutionOverrides, initCommonJsModuleGlobals, setCommonJsFileGlobals, applyCustomGlobalExposurePolicy, awaitScriptResult } from "./execution-lifecycle.js"; export { NodeExecutionDriverOptions }; -// Module-level cache for the static bridge IIFE (identical across all sessions) -let staticBridgeCodeCache: string | null = null; - -/** - * Compose the config-independent bridge IIFE. Output is byte-for-byte - * identical regardless of session options — uses DEFAULT values for all - * config that gets overridden by the post-restore script. - * Used for snapshot creation and as the base of every session's bridge code. - */ -export function composeStaticBridgeCode(): string { - if (staticBridgeCodeCache) return staticBridgeCodeCache; - - const parts: string[] = []; - - parts.push(getIvmCompatShimSource()); - - // Default budget values — overridden per-session by post-restore script - parts.push(`globalThis._maxTimers = ${DEFAULT_MAX_TIMERS};`); - parts.push(`globalThis._maxHandles = ${DEFAULT_MAX_HANDLES};`); - parts.push(`globalThis.__runtimeBridgeSetupConfig = ${JSON.stringify({ - initialCwd: DEFAULT_SANDBOX_CWD, - jsonPayloadLimitBytes: DEFAULT_ISOLATE_JSON_PAYLOAD_BYTES, - payloadLimitErrorCode: PAYLOAD_LIMIT_ERROR_CODE, - })};`); - - parts.push(getIsolateRuntimeSource("globalExposureHelpers")); - parts.push(getInitialBridgeGlobalsSetupCode()); - parts.push(getConsoleSetupCode()); - parts.push(getIsolateRuntimeSource("setupFsFacade")); - parts.push(getRawBridgeCode()); - parts.push(getBridgeAttachCode()); - - // Default: no timing mitigation (freeze applied via post-restore script) - parts.push(getIsolateRuntimeSource("applyTimingMitigationOff")); - - parts.push(getRequireSetupCode()); - parts.push(getIsolateRuntimeSource("initCommonjsModuleGlobals")); - - parts.push(`globalThis.__runtimeCustomGlobalPolicy = ${JSON.stringify({ - hardenedGlobals: HARDENED_NODE_CUSTOM_GLOBALS, - mutableGlobals: MUTABLE_NODE_CUSTOM_GLOBALS, - })};`); - parts.push(getIsolateRuntimeSource("applyCustomGlobalPolicy")); - - staticBridgeCodeCache = parts.join("\n"); - return staticBridgeCodeCache; -} - -/** - * Compose the per-session post-restore script. Overrides default config - * values from the static IIFE with session-specific values, applies timing - * mitigation, and handles polyfill loading. - */ -export function composePostRestoreScript(config: { - timingMitigation: TimingMitigation; - frozenTimeMs: number; - maxTimers?: number; - maxHandles?: number; - initialCwd?: string; - payloadLimitBytes?: number; - payloadLimitErrorCode?: string; -}): string { - const parts: string[] = []; - - // Override per-session budget values if they differ from defaults - if (config.maxTimers !== undefined) { - parts.push(`globalThis._maxTimers = ${config.maxTimers};`); - } - if (config.maxHandles !== undefined) { - parts.push(`globalThis._maxHandles = ${config.maxHandles};`); - } - - // Override initial cwd for module resolution - if (config.initialCwd && config.initialCwd !== DEFAULT_SANDBOX_CWD) { - parts.push(`if (globalThis._currentModule) globalThis._currentModule.dirname = ${JSON.stringify(config.initialCwd)};`); - } - - // Apply config (timing mitigation, payload limits) via __runtimeApplyConfig - parts.push(`globalThis.__runtimeApplyConfig(${JSON.stringify({ - timingMitigation: config.timingMitigation, - frozenTimeMs: config.timingMitigation === "freeze" ? config.frozenTimeMs : undefined, - payloadLimitBytes: config.payloadLimitBytes, - payloadLimitErrorCode: config.payloadLimitErrorCode, - })});`); - - // Reset mutable state from snapshot (no-op on fresh context, resets stale - // values on snapshot-restored context) - parts.push(`if (typeof globalThis.__runtimeResetProcessState === "function") globalThis.__runtimeResetProcessState();`); - - return parts.join("\n"); -} - -/** - * Compose the bridge code for snapshot warm-up. - * Returns only the static IIFE — the post-restore script is sent - * separately per-execution so the snapshot is config-independent. - */ -export function composeBridgeCodeForWarmup(): string { - return composeStaticBridgeCode(); -} - -const MAX_ERROR_MESSAGE_CHARS = 8192; - -function boundErrorMessage(message: string): string { - if (message.length <= MAX_ERROR_MESSAGE_CHARS) return message; - return `${message.slice(0, MAX_ERROR_MESSAGE_CHARS)}...[Truncated]`; -} - export class NodeExecutionDriver implements RuntimeDriver { private deps: DriverDeps; private memoryLimit: number; private disposed: boolean = false; - - // V8 session state (lazy-initialized; runtime is shared across all drivers) - private v8Session: V8Session | null = null; - private v8InitPromise: Promise | null = null; - private v8RuntimeOverride: V8Runtime | null; + private runtimeCreateIsolate: (memoryLimit: number) => ivm.Isolate; constructor(options: NodeExecutionDriverOptions) { - this.v8RuntimeOverride = options.v8Runtime ?? null; this.memoryLimit = options.memoryLimit ?? 128; const system = options.system; + this.runtimeCreateIsolate = + (options.createIsolate as + | ((memoryLimit: number) => ivm.Isolate) + | undefined) ?? + ((memoryLimit) => createDefaultIsolate(memoryLimit)); + + const isolate = this.runtimeCreateIsolate(this.memoryLimit); const permissions = system.permissions; const filesystem = system.filesystem ? wrapFileSystem(system.filesystem, permissions) @@ -191,6 +62,7 @@ export class NodeExecutionDriver implements RuntimeDriver { const budgets = options.resourceBudgets; this.deps = { + isolate, filesystem, commandExecutor, networkAdapter, @@ -211,15 +83,13 @@ export class NodeExecutionDriver implements RuntimeDriver { activeHttpServerIds: new Set(), activeChildProcesses: new Map(), activeHostTimers: new Set(), - resolutionCache: createResolutionCache(), - // Legacy fields — unused by V8-based driver, provided for DriverDeps compatibility - isolate: null, esmModuleCache: new Map(), esmModuleReverseCache: new Map(), moduleFormatCache: new Map(), packageTypeCache: new Map(), dynamicImportCache: new Map(), dynamicImportPending: new Map(), + resolutionCache: createResolutionCache(), }; } @@ -232,6 +102,46 @@ export class NodeExecutionDriver implements RuntimeDriver { }; } + get unsafeIsolate(): unknown { return this.__unsafeIsoalte; } + get __unsafeIsoalte(): ivm.Isolate { + if (this.disposed) throw new Error("NodeRuntime has been disposed"); + return this.deps.isolate; + } + + async createUnsafeContext(options: { env?: Record; cwd?: string; filePath?: string } = {}): Promise { + return this.__unsafeCreateContext(options); + } + + async __unsafeCreateContext(options: { env?: Record; cwd?: string; filePath?: string } = {}): Promise { + if (this.disposed) throw new Error("NodeRuntime has been disposed"); + this.deps.budgetState = createBudgetState(); + // Clear module caches to prevent cache poisoning across contexts + this.deps.esmModuleCache.clear(); + this.deps.esmModuleReverseCache.clear(); + this.deps.dynamicImportCache.clear(); + this.deps.dynamicImportPending.clear(); + this.deps.resolutionCache.resolveResults.clear(); + this.deps.resolutionCache.packageJsonResults.clear(); + this.deps.resolutionCache.existsResults.clear(); + this.deps.resolutionCache.statResults.clear(); + this.deps.moduleFormatCache.clear(); + this.deps.packageTypeCache.clear(); + const context = await this.deps.isolate.createContext(); + const jail = context.global; + await jail.set("global", jail.derefInto()); + const tm = getTimingMitigation(undefined, this.deps.timingMitigation); + const frozenTimeMs = Date.now(); + await setupConsole(this.deps, context, jail, this.deps.onStdio); + await setupRequire(this.deps, context, jail, tm, frozenTimeMs); + const referrer = options.filePath ? getPathDir(options.filePath) : (options.cwd ?? this.deps.processConfig.cwd ?? "/"); + await setupDynamicImport(this.deps, context, jail, referrer, undefined); + await initCommonJsModuleGlobals(context); + await applyExecutionOverrides(context, this.deps.permissions, options.env, options.cwd, undefined); + if (options.filePath) await setCommonJsFileGlobals(context, options.filePath); + await applyCustomGlobalExposurePolicy(context); + return context; + } + async run(code: string, filePath?: string): Promise> { return this.executeInternal({ mode: "run", code, filePath }); } @@ -251,49 +161,6 @@ export class NodeExecutionDriver implements RuntimeDriver { return { code: result.code, errorMessage: result.errorMessage }; } - /** Ensure V8 session is initialized (runtime is shared). */ - private async ensureV8(): Promise { - if (this.v8Session) return this.v8Session; - if (!this.v8InitPromise) { - this.v8InitPromise = this.initV8(); - } - await this.v8InitPromise; - return this.v8Session!; - } - - private async getV8Runtime(): Promise { - return this.v8RuntimeOverride ?? getSharedV8Runtime(); - } - - private async initV8(): Promise { - const runtime = await this.getV8Runtime(); - this.v8Session = await runtime.createSession({ - heapLimitMb: this.memoryLimit, - cpuTimeLimitMs: this.deps.cpuTimeLimitMs, - }); - } - - /** Compose the static bridge IIFE (no per-session config). */ - private composeBridgeCode(): string { - return composeStaticBridgeCode(); - } - - /** Compose the per-execution post-restore script. */ - private composePostRestore( - timingMitigation: TimingMitigation, - frozenTimeMs: number, - ): string { - return composePostRestoreScript({ - timingMitigation, - frozenTimeMs, - maxTimers: this.deps.maxTimers, - maxHandles: this.deps.maxHandles, - initialCwd: this.deps.processConfig.cwd ?? DEFAULT_SANDBOX_CWD, - payloadLimitBytes: this.deps.isolateJsonPayloadLimitBytes, - payloadLimitErrorCode: PAYLOAD_LIMIT_ERROR_CODE, - }); - } - private async executeInternal(options: { mode: "run" | "exec"; code: string; @@ -305,168 +172,82 @@ export class NodeExecutionDriver implements RuntimeDriver { timingMitigation?: TimingMitigation; onStdio?: StdioHook; }): Promise> { - // Reset budget state for this execution this.deps.budgetState = createBudgetState(); - - // Clear resolution caches between executions - this.deps.resolutionCache.resolveResults.clear(); - this.deps.resolutionCache.packageJsonResults.clear(); - this.deps.resolutionCache.existsResults.clear(); - this.deps.resolutionCache.statResults.clear(); - - const session = await this.ensureV8(); - - // Determine timing and build configs - const timingMitigation = getTimingMitigation(options.timingMitigation, this.deps.timingMitigation); - const frozenTimeMs = Date.now(); - - // Build bridge handlers - const bridgeHandlers = buildBridgeHandlers({ - deps: this.deps, - onStdio: options.onStdio ?? this.deps.onStdio, - sendStreamEvent: (eventType, payload) => { - session.sendStreamEvent(eventType, payload); + const d = this.deps; + return executeWithRuntime( + { + isolate: d.isolate, + esmModuleCache: d.esmModuleCache, + esmModuleReverseCache: d.esmModuleReverseCache, + dynamicImportCache: d.dynamicImportCache, + dynamicImportPending: d.dynamicImportPending, + resolutionCache: d.resolutionCache, + moduleFormatCache: d.moduleFormatCache, + packageTypeCache: d.packageTypeCache, + getTimingMitigation: (mode) => getTimingMitigation(mode, d.timingMitigation), + getExecutionTimeoutMs: (override) => getExecutionTimeoutMs(override, d.cpuTimeLimitMs), + getExecutionDeadlineMs: (timeoutMs) => getExecutionDeadlineMs(timeoutMs), + setupConsole: (context, jail, onStdio) => + setupConsole(d, context, jail, onStdio ?? d.onStdio), + shouldRunAsESM: (code, filePath) => shouldRunAsESM(d, code, filePath), + setupESMGlobals: (context, jail, tm, frozenTimeMs) => + setupESMGlobals(d, context, jail, tm, frozenTimeMs), + applyExecutionOverrides: (context, env, cwd, stdin) => + applyExecutionOverrides(context, d.permissions, env, cwd, stdin), + precompileDynamicImports: (transformedCode, context, referrerPath) => + precompileDynamicImports(d, transformedCode, context, referrerPath), + setupDynamicImport: (context, jail, referrerPath, executionDeadlineMs) => + setupDynamicImport(d, context, jail, referrerPath, executionDeadlineMs), + runESM: (code, context, filePath, executionDeadlineMs) => + runESM(d, code, context, filePath, executionDeadlineMs), + setupRequire: (context, jail, tm, frozenTimeMs) => + setupRequire(d, context, jail, tm, frozenTimeMs), + initCommonJsModuleGlobals: (context) => initCommonJsModuleGlobals(context), + applyCustomGlobalExposurePolicy: (context) => + applyCustomGlobalExposurePolicy(context), + setCommonJsFileGlobals: (context, filePath) => + setCommonJsFileGlobals(context, filePath), + awaitScriptResult: (context, executionDeadlineMs) => + awaitScriptResult(context, executionDeadlineMs), + getExecutionRunOptions: (executionDeadlineMs) => + getExecutionRunOptions(executionDeadlineMs), + runWithExecutionDeadline: (operation, executionDeadlineMs) => + runWithExecutionDeadline(operation, executionDeadlineMs), + isExecutionTimeoutError: (error) => isExecutionTimeoutError(error), + recycleIsolate: () => this.recycleIsolate(), + timeoutErrorMessage: TIMEOUT_ERROR_MESSAGE, + timeoutExitCode: TIMEOUT_EXIT_CODE, }, - }); - - // Compose bridge code and post-restore script (sent separately over IPC) - const bridgeCode = this.composeBridgeCode(); - const postRestoreScript = this.composePostRestore(timingMitigation, frozenTimeMs); - - // Transform user code (dynamic import → __dynamicImport) - const userCode = transformDynamicImport(options.code); - - // Build per-execution preamble for stdin, env/cwd overrides, and CJS file globals - const execPreamble: string[] = []; - if (options.filePath) { - const dirname = options.filePath.includes("/") - ? options.filePath.substring(0, options.filePath.lastIndexOf("/")) || "/" - : "/"; - execPreamble.push(`globalThis.__runtimeCommonJsFileConfig = ${JSON.stringify({ filePath: options.filePath, dirname })};`); - execPreamble.push(getIsolateRuntimeSource("setCommonjsFileGlobals")); - } - if (options.stdin !== undefined) { - execPreamble.push(`globalThis.__runtimeStdinData = ${JSON.stringify(options.stdin)};`); - execPreamble.push(getIsolateRuntimeSource("setStdinData")); - } - - // Build process/OS config for this execution - const processConfig = createProcessConfigForExecution( - this.deps.processConfig, - timingMitigation, - frozenTimeMs, + options, ); - // Apply per-execution env/cwd overrides - if (options.env) { - processConfig.env = { ...processConfig.env, ...filterEnv(options.env, this.deps.permissions) }; - } - if (options.cwd) { - processConfig.cwd = options.cwd; - } - - const osConfig = this.deps.osConfig; - - // Prepend per-execution preamble to user code - const fullUserCode = execPreamble.length > 0 - ? execPreamble.join("\n") + "\n" + userCode - : userCode; - - try { - // Execute via V8 session - const result: V8ExecutionResult = await session.execute({ - bridgeCode, - postRestoreScript, - userCode: fullUserCode, - mode: options.mode, - filePath: options.filePath, - processConfig: { - cwd: processConfig.cwd ?? "/", - env: processConfig.env ?? {}, - timing_mitigation: String(processConfig.timingMitigation ?? timingMitigation), - frozen_time_ms: processConfig.frozenTimeMs ?? null, - }, - osConfig: { - homedir: osConfig.homedir ?? DEFAULT_SANDBOX_HOME, - tmpdir: osConfig.tmpdir ?? DEFAULT_SANDBOX_TMPDIR, - platform: osConfig.platform ?? process.platform, - arch: osConfig.arch ?? process.arch, - }, - bridgeHandlers, - onStreamCallback: (_callbackType, _payload) => { - // Handle stream callbacks from V8 (e.g., HTTP server responses) - }, - }); - - // Map V8ExecutionResult to RunResult - if (result.error) { - // Check for timeout - if (result.error.message && /timed out|time limit exceeded/i.test(result.error.message)) { - return { - code: TIMEOUT_EXIT_CODE, - errorMessage: TIMEOUT_ERROR_MESSAGE, - exports: undefined as T, - }; - } - - // Check for process.exit() - const exitMatch = result.error.message?.match(/process\.exit\((\d+)\)/); - if (exitMatch) { - return { - code: parseInt(exitMatch[1], 10), - exports: undefined as T, - }; - } - - // Check for ProcessExitError (sentinel-based detection) - if (result.error.type === "ProcessExitError" && result.error.code) { - return { - code: parseInt(result.error.code, 10) || 1, - exports: undefined as T, - }; - } - - return { - code: result.code || 1, - errorMessage: boundErrorMessage(result.error.message || result.error.type), - exports: undefined as T, - }; - } + } - // Deserialize module exports from V8 serialized binary - let exports: T | undefined; - if (result.exports && result.exports.byteLength > 0) { - const nodeV8 = await import("node:v8"); - exports = nodeV8.deserialize(Buffer.from(result.exports)) as T; - } - return { - code: result.code, - exports, - }; - } catch (err) { - const errMessage = err instanceof Error ? err.message : String(err); - return { - code: 1, - errorMessage: boundErrorMessage(errMessage), - exports: undefined as T, - }; + private recycleIsolate(): void { + if (this.disposed) { + return; } + killActiveChildProcesses(this.deps); + this.closeActiveHttpServers(); + clearActiveHostTimers(this.deps); + this.deps.isolate.dispose(); + this.deps.isolate = this.runtimeCreateIsolate(this.memoryLimit); } dispose(): void { - if (this.disposed) return; + if (this.disposed) { + return; + } this.disposed = true; killActiveChildProcesses(this.deps); this.closeActiveHttpServers(); clearActiveHostTimers(this.deps); - // Destroy this driver's V8 session (shared runtime stays alive) - if (this.v8Session) { - void this.v8Session.destroy(); - this.v8Session = null; - } + this.deps.isolate.dispose(); } async terminate(): Promise { - if (this.disposed) return; + if (this.disposed) { + return; + } killActiveChildProcesses(this.deps); const adapter = this.deps.networkAdapter; if (adapter?.httpServerClose) { @@ -476,12 +257,10 @@ export class NodeExecutionDriver implements RuntimeDriver { this.deps.activeHttpServerIds.clear(); clearActiveHostTimers(this.deps); this.disposed = true; - if (this.v8Session) { - await this.v8Session.destroy(); - this.v8Session = null; - } + this.deps.isolate.dispose(); } + /** Close all tracked HTTP servers without awaiting. */ private closeActiveHttpServers(): void { const adapter = this.deps.networkAdapter; if (adapter?.httpServerClose) { diff --git a/packages/secure-exec-node/src/execution-lifecycle.ts b/packages/secure-exec-node/src/execution-lifecycle.ts index b3ac7366..be5031ff 100644 --- a/packages/secure-exec-node/src/execution-lifecycle.ts +++ b/packages/secure-exec-node/src/execution-lifecycle.ts @@ -1,3 +1,4 @@ +import ivm from "isolated-vm"; import { getIsolateRuntimeSource } from "@secure-exec/core"; import { HARDENED_NODE_CUSTOM_GLOBALS, @@ -11,18 +12,11 @@ import { import type { Permissions } from "@secure-exec/core"; import type { TimingMitigation } from "@secure-exec/core/internal/shared/api-types"; -// Legacy context type — isolated-vm has been removed. -/* eslint-disable @typescript-eslint/no-explicit-any */ -type LegacyContext = any; -/* eslint-enable @typescript-eslint/no-explicit-any */ - /** * Apply runtime overrides used by script-style execution. - * - * @deprecated Legacy function for isolated-vm contexts. V8-based driver handles this. */ export async function applyExecutionOverrides( - context: LegacyContext, + context: ivm.Context, permissions: Permissions | undefined, env?: Record, cwd?: string, @@ -38,20 +32,16 @@ export async function applyExecutionOverrides( /** * Initialize mutable CommonJS globals before script execution. - * - * @deprecated Legacy function for isolated-vm contexts. */ -export async function initCommonJsModuleGlobals(context: LegacyContext): Promise { +export async function initCommonJsModuleGlobals(context: ivm.Context): Promise { await context.eval(getIsolateRuntimeSource("initCommonjsModuleGlobals")); } /** * Set CommonJS file globals for accurate relative require() behavior. - * - * @deprecated Legacy function for isolated-vm contexts. */ export async function setCommonJsFileGlobals( - context: LegacyContext, + context: ivm.Context, filePath: string, ): Promise { const dirname = filePath.includes("/") @@ -67,10 +57,8 @@ export async function setCommonJsFileGlobals( /** * Apply descriptor policy to custom globals before user code executes. - * - * @deprecated Legacy function for isolated-vm contexts. */ -export async function applyCustomGlobalExposurePolicy(context: LegacyContext): Promise { +export async function applyCustomGlobalExposurePolicy(context: ivm.Context): Promise { await context.global.set( "__runtimeCustomGlobalPolicy", { @@ -84,11 +72,9 @@ export async function applyCustomGlobalExposurePolicy(context: LegacyContext): P /** * Await script result when eval() returns a Promise. - * - * @deprecated Legacy function for isolated-vm contexts. */ export async function awaitScriptResult( - context: LegacyContext, + context: ivm.Context, executionDeadlineMs?: number, ): Promise { const hasPromise = await context.eval( @@ -111,23 +97,23 @@ export async function awaitScriptResult( /** * Override process.env and process.cwd for a specific execution context. - * - * @deprecated Legacy function for isolated-vm contexts. */ export async function overrideProcessConfig( - context: LegacyContext, + context: ivm.Context, permissions: Permissions | undefined, env?: Record, cwd?: string, ): Promise { if (env) { const filtered = filterEnv(env, permissions); + // Merge provided env with existing env. await context.global.set("__runtimeProcessEnvOverride", filtered, { copy: true, }); await context.eval(getIsolateRuntimeSource("overrideProcessEnv")); } if (cwd) { + // Override cwd. await context.global.set("__runtimeProcessCwdOverride", cwd, { copy: true, }); @@ -137,13 +123,14 @@ export async function overrideProcessConfig( /** * Set stdin data for a specific execution context. - * - * @deprecated Legacy function for isolated-vm contexts. + * This injects stdin data that will be emitted when process.stdin listeners are added. */ export async function setStdinData( - context: LegacyContext, + context: ivm.Context, stdin: string, ): Promise { + // The bridge exposes these variables for stdin management. + // We need to set them before the script runs so readline can access them. await context.global.set("__runtimeStdinData", stdin, { copy: true }); await context.eval(getIsolateRuntimeSource("setStdinData")); } diff --git a/packages/secure-exec-node/src/execution.ts b/packages/secure-exec-node/src/execution.ts index 95d89acf..e2565d65 100644 --- a/packages/secure-exec-node/src/execution.ts +++ b/packages/secure-exec-node/src/execution.ts @@ -1,3 +1,4 @@ +import ivm from "isolated-vm"; import { getIsolateRuntimeSource } from "@secure-exec/core"; import type { ResolutionCache } from "@secure-exec/core/internal/package-bundler"; import { transformDynamicImport } from "@secure-exec/core/internal/shared/esm-utils"; @@ -35,26 +36,18 @@ type ExecuteOptions = { onStdio?: StdioHook; }; -// Legacy context/reference types — isolated-vm has been removed. -/* eslint-disable @typescript-eslint/no-explicit-any */ -type LegacyIsolate = any; -type LegacyContext = any; -type LegacyReference<_T = unknown> = any; -type LegacyModule = any; -/* eslint-enable @typescript-eslint/no-explicit-any */ - /** * Abstraction over the runtime environment that `executeWithRuntime` depends on. - * - * @deprecated This interface used isolated-vm types. The V8-based driver in - * execution-driver.ts replaces this execution loop entirely. + * The `NodeRuntime` class implements this interface, providing all the + * isolate setup, module loading, and bridge wiring that the generic + * execution loop delegates to. */ type ExecutionRuntime = { - isolate: LegacyIsolate; - esmModuleCache: Map; - esmModuleReverseCache: Map; - dynamicImportCache: Map; - dynamicImportPending: Map>; + isolate: ivm.Isolate; + esmModuleCache: Map; + esmModuleReverseCache: Map; + dynamicImportCache: Map>; + dynamicImportPending: Map | null>>; resolutionCache: ResolutionCache; moduleFormatCache: Map; packageTypeCache: Map; @@ -62,56 +55,56 @@ type ExecutionRuntime = { getExecutionTimeoutMs(override?: number): number | undefined; getExecutionDeadlineMs(timeoutMs?: number): number | undefined; setupConsole( - context: LegacyContext, - jail: LegacyReference, + context: ivm.Context, + jail: ivm.Reference>, onStdio?: StdioHook, ): Promise; shouldRunAsESM(code: string, filePath?: string): Promise; setupESMGlobals( - context: LegacyContext, - jail: LegacyReference, + context: ivm.Context, + jail: ivm.Reference>, timingMitigation: TimingMitigation, frozenTimeMs: number, ): Promise; applyExecutionOverrides( - context: LegacyContext, + context: ivm.Context, env?: Record, cwd?: string, stdin?: string, ): Promise; precompileDynamicImports( transformedCode: string, - context: LegacyContext, + context: ivm.Context, referrerPath?: string, ): Promise; setupDynamicImport( - context: LegacyContext, - jail: LegacyReference, + context: ivm.Context, + jail: ivm.Reference>, referrerPath?: string, executionDeadlineMs?: number, ): Promise; runESM( code: string, - context: LegacyContext, + context: ivm.Context, filePath?: string, executionDeadlineMs?: number, ): Promise; setupRequire( - context: LegacyContext, - jail: LegacyReference, + context: ivm.Context, + jail: ivm.Reference>, timingMitigation: TimingMitigation, frozenTimeMs: number, ): Promise; - initCommonJsModuleGlobals(context: LegacyContext): Promise; - applyCustomGlobalExposurePolicy(context: LegacyContext): Promise; - setCommonJsFileGlobals(context: LegacyContext, filePath: string): Promise; + initCommonJsModuleGlobals(context: ivm.Context): Promise; + applyCustomGlobalExposurePolicy(context: ivm.Context): Promise; + setCommonJsFileGlobals(context: ivm.Context, filePath: string): Promise; awaitScriptResult( - context: LegacyContext, + context: ivm.Context, executionDeadlineMs?: number, ): Promise; getExecutionRunOptions( executionDeadlineMs?: number, - ): { timeout?: number }; + ): Pick; runWithExecutionDeadline( operation: Promise, executionDeadlineMs?: number, @@ -125,8 +118,11 @@ type ExecutionRuntime = { /** * Core execution loop shared between `run()` and `exec()` modes. * - * @deprecated This function used isolated-vm internals. The V8-based driver - * in execution-driver.ts replaces this execution loop entirely. + * Creates a fresh V8 context, installs console/bridge/module globals, detects + * ESM vs CJS format, compiles and runs the code, waits for active handles + * (child processes, HTTP servers) to drain, and returns the exit code. + * + * On timeout the isolate is recycled to free any stuck V8 state. */ export async function executeWithRuntime( runtime: ExecutionRuntime, diff --git a/packages/secure-exec-node/src/index.ts b/packages/secure-exec-node/src/index.ts index 4152ab59..4840f383 100644 --- a/packages/secure-exec-node/src/index.ts +++ b/packages/secure-exec-node/src/index.ts @@ -26,7 +26,7 @@ export { } from "./polyfills.js"; // Node execution driver -export { NodeExecutionDriver, composeStaticBridgeCode, composePostRestoreScript, composeBridgeCodeForWarmup } from "./execution-driver.js"; +export { NodeExecutionDriver } from "./execution-driver.js"; export type { NodeExecutionDriverOptions } from "./isolate-bootstrap.js"; // Node system driver diff --git a/packages/secure-exec-node/src/isolate-bootstrap.ts b/packages/secure-exec-node/src/isolate-bootstrap.ts index 62d493ed..96f8e0a5 100644 --- a/packages/secure-exec-node/src/isolate-bootstrap.ts +++ b/packages/secure-exec-node/src/isolate-bootstrap.ts @@ -1,3 +1,4 @@ +import ivm from "isolated-vm"; import { createRequire } from "node:module"; import type { CommandExecutor, @@ -17,8 +18,6 @@ import type { ResolutionCache } from "@secure-exec/core"; export interface NodeExecutionDriverOptions extends RuntimeDriverOptions { createIsolate?(memoryLimit: number): unknown; - /** V8 runtime process override. If omitted, uses the global shared process. */ - v8Runtime?: import("@secure-exec/v8").V8Runtime; } export interface BudgetState { @@ -30,6 +29,7 @@ export interface BudgetState { /** Shared mutable state owned by NodeExecutionDriver, passed to extracted modules. */ export interface DriverDeps { + isolate: ivm.Isolate; filesystem: VirtualFileSystem; commandExecutor: CommandExecutor; networkAdapter: NetworkAdapter; @@ -50,20 +50,15 @@ export interface DriverDeps { activeHttpServerIds: Set; activeChildProcesses: Map; activeHostTimers: Set>; + esmModuleCache: Map; + esmModuleReverseCache: Map; + moduleFormatCache: Map; + packageTypeCache: Map; + dynamicImportCache: Map>; + dynamicImportPending: Map>>; resolutionCache: ResolutionCache; /** Optional callback for PTY setRawMode — wired by kernel when PTY is attached. */ onPtySetRawMode?: (mode: boolean) => void; - - // Legacy fields for backward compatibility with esm-compiler.ts and module-resolver.ts. - /* eslint-disable @typescript-eslint/no-explicit-any */ - isolate: any; - esmModuleCache: Map; - esmModuleReverseCache: Map; - moduleFormatCache: Map; - packageTypeCache: Map; - dynamicImportCache: Map; - dynamicImportPending: Map>; - /* eslint-enable @typescript-eslint/no-explicit-any */ } // Constants diff --git a/packages/secure-exec-node/src/isolate.ts b/packages/secure-exec-node/src/isolate.ts index 5d3116ee..f1a18648 100644 --- a/packages/secure-exec-node/src/isolate.ts +++ b/packages/secure-exec-node/src/isolate.ts @@ -1,3 +1,4 @@ +import ivm from "isolated-vm"; import type { TimingMitigation } from "@secure-exec/core/internal/shared/api-types"; import { TIMEOUT_ERROR_MESSAGE as _TIMEOUT_ERROR_MESSAGE, @@ -17,14 +18,9 @@ export class ExecutionTimeoutError extends Error { } } -/** - * Create a new V8 isolate with the given heap memory limit (in MB). - * - * @deprecated This function required isolated-vm which has been removed. - * Use @secure-exec/v8 createV8Runtime() instead. - */ -export function createIsolate(_memoryLimit: number): unknown { - throw new Error("createIsolate() is no longer available. Use @secure-exec/v8 createV8Runtime() instead."); +/** Create a new V8 isolate with the given heap memory limit (in MB). */ +export function createIsolate(memoryLimit: number): ivm.Isolate { + return new ivm.Isolate({ memoryLimit }); } /** Convert a relative timeout duration into an absolute wall-clock deadline. */ @@ -36,12 +32,12 @@ export function getExecutionDeadlineMs(timeoutMs?: number): number | undefined { } /** - * Build execution run options with a timeout derived from the remaining + * Build isolated-vm `ScriptRunOptions` with a timeout derived from the remaining * wall-clock budget. Throws immediately if the deadline has already passed. */ export function getExecutionRunOptions( executionDeadlineMs?: number, -): { timeout?: number } { +): Pick { if (executionDeadlineMs === undefined) { return {}; } @@ -85,7 +81,7 @@ export async function runWithExecutionDeadline( /** * Detect timeout errors from both our own `ExecutionTimeoutError` and - * V8 runtime timeout messages. + * isolated-vm's native timeout messages. */ export function isExecutionTimeoutError(error: unknown): boolean { if (error instanceof ExecutionTimeoutError) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc4e51c5..8206232f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -486,6 +486,9 @@ importers: esbuild: specifier: ^0.27.1 version: 0.27.4 + isolated-vm: + specifier: ^6.0.0 + version: 6.1.2 node-stdlib-browser: specifier: ^1.3.1 version: 1.3.1 @@ -530,6 +533,22 @@ importers: version: 2.1.9(@types/node@22.19.3)(@vitest/browser@2.1.9) packages/secure-exec-v8: + optionalDependencies: + '@secure-exec/v8-darwin-arm64': + specifier: 0.1.1-rc.2 + version: 0.1.1-rc.2 + '@secure-exec/v8-darwin-x64': + specifier: 0.1.1-rc.2 + version: 0.1.1-rc.2 + '@secure-exec/v8-linux-arm64-gnu': + specifier: 0.1.1-rc.2 + version: 0.1.1-rc.2 + '@secure-exec/v8-linux-x64-gnu': + specifier: 0.1.1-rc.2 + version: 0.1.1-rc.2 + '@secure-exec/v8-win32-x64': + specifier: 0.1.1-rc.2 + version: 0.1.1-rc.2 devDependencies: '@types/node': specifier: ^22.10.2 @@ -3681,6 +3700,46 @@ packages: requiresBuild: true optional: true + /@secure-exec/v8-darwin-arm64@0.1.1-rc.2: + resolution: {integrity: sha512-spRS8+YrHs8H6+dQqXY990uHhZRogoO+z1gklinm8QPFj10iffZKpa10ud24Y/EcTEqf0Gb/6qZQ2ZCc9bwHVw==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@secure-exec/v8-darwin-x64@0.1.1-rc.2: + resolution: {integrity: sha512-DF+j9t4RA1sE+N4cVTCR2rBjplN7/XVqo/8C7dVoQ/J3pzdsVVvoCM3eh0yaH8+ZvFVOvymIv/Gsye6fBIuVnA==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@secure-exec/v8-linux-arm64-gnu@0.1.1-rc.2: + resolution: {integrity: sha512-dFHqp7oZ48lRgWPwbfH6UhmcRytSTj0sAvEB5lLGT5nPAihWGOXYGpqAmF6s3WUmKeVR0U7gGuLhtjFin4kyqg==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@secure-exec/v8-linux-x64-gnu@0.1.1-rc.2: + resolution: {integrity: sha512-e8g9woHwKaXzbdQT1SXzwCQf5LcLHWKI33FHoHQOuJ6QkRmkYZ4T+M/IM8EfmzvGVNqqhAF846TJFycvlTQfvA==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@secure-exec/v8-win32-x64@0.1.1-rc.2: + resolution: {integrity: sha512-uV4YzPRT+j9KZ0AqG5juqsAsMIofi5vADvbGKxgzarvOwmm5e/OjHRNgK1wFkAkO+/8ZlcrI4l8GEw1MvPqR+g==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@shikijs/core@3.23.0: resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} dependencies: @@ -6563,6 +6622,14 @@ packages: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} dev: false + /isolated-vm@6.1.2: + resolution: {integrity: sha512-GGfsHqtlZiiurZaxB/3kY7LLAXR3sgzDul0fom4cSyBjx6ZbjpTrFWiH3z/nUfLJGJ8PIq9LQmQFiAxu24+I7A==} + engines: {node: '>=22.0.0'} + requiresBuild: true + dependencies: + node-gyp-build: 4.8.4 + dev: false + /isomorphic-timers-promises@1.0.1: resolution: {integrity: sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==} engines: {node: '>=10'} @@ -7362,7 +7429,6 @@ packages: hasBin: true requiresBuild: true dev: false - optional: true /node-liblzma@2.2.0: resolution: {integrity: sha512-s0KzNOWwOJJgPG6wxg6cKohnAl9Wk/oW1KrQaVzJBjQwVcUGPQCzpR46Ximygjqj/3KhOrtJXnYMp/xYAXp75g==} From 73fab16c45c8e900097ce7a676e33bd109caaea8 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 20 Mar 2026 01:31:38 -0700 Subject: [PATCH 2/3] fix: update pkg-pr-new to include v8 package and Docker build --- .github/workflows/pkg-pr-new.yaml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pkg-pr-new.yaml b/.github/workflows/pkg-pr-new.yaml index 04b837fc..46288910 100644 --- a/.github/workflows/pkg-pr-new.yaml +++ b/.github/workflows/pkg-pr-new.yaml @@ -11,6 +11,7 @@ on: push: branches: - main + - nathan/v8-process-isolation-pt1 paths: - ".github/workflows/pkg-pr-new.yaml" - "package.json" @@ -43,10 +44,15 @@ jobs: cache-dependency-path: pnpm-lock.yaml - name: Install dependencies - run: pnpm install --frozen-lockfile + run: pnpm install --no-frozen-lockfile - name: Build packages - run: pnpm turbo run build --filter='@secure-exec/typescript...' + run: pnpm turbo run build + + - name: Build linux-x64 binary via Docker + run: | + cd crates/v8-runtime + docker build -f docker/Dockerfile.linux-x64-gnu -o type=local,dest=npm/linux-x64-gnu . - name: Publish to pkg.pr.new run: | @@ -57,4 +63,5 @@ jobs: "./packages/secure-exec-browser" \ "./packages/secure-exec-python" \ "./packages/secure-exec-typescript" \ + "./packages/secure-exec-v8" \ --packageManager pnpm From 32e80b0f55370cda357c13ff9c1f65bf1780d9f0 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 20 Mar 2026 01:33:26 -0700 Subject: [PATCH 3/3] fix: remove branch whitelist from pkg-pr-new --- .github/workflows/pkg-pr-new.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pkg-pr-new.yaml b/.github/workflows/pkg-pr-new.yaml index 46288910..e11a9066 100644 --- a/.github/workflows/pkg-pr-new.yaml +++ b/.github/workflows/pkg-pr-new.yaml @@ -11,7 +11,6 @@ on: push: branches: - main - - nathan/v8-process-isolation-pt1 paths: - ".github/workflows/pkg-pr-new.yaml" - "package.json"