Skip to content

Commit 8f85633

Browse files
committed
docs(secure-exec): refine runtime doc comments
1 parent a7e3843 commit 8f85633

24 files changed

Lines changed: 328 additions & 12 deletions

packages/secure-exec/src/bridge-loader.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
99
// Cache the bridge code
1010
let bridgeCodeCache: string | null = null;
1111

12+
/** Locate the bridge TypeScript source for on-demand compilation (dev only). */
1213
function findBridgeSourcePath(): string | null {
1314
const candidates = [
1415
path.join(__dirname, "bridge", "index.ts"),
@@ -20,6 +21,7 @@ function findBridgeSourcePath(): string | null {
2021
return null;
2122
}
2223

24+
/** Walk a directory tree and return the newest file modification time. */
2325
function getLatestMtimeMs(dir: string): number {
2426
let latest = 0;
2527
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
@@ -33,6 +35,10 @@ function getLatestMtimeMs(dir: string): number {
3335
return latest;
3436
}
3537

38+
/**
39+
* Auto-compile the bridge IIFE bundle from TypeScript source if stale.
40+
* Skips rebuilding when the existing bundle is newer than all source files.
41+
*/
3642
function ensureBridgeBundle(bridgePath: string): void {
3743
const sourcePath = findBridgeSourcePath();
3844

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { getIsolateRuntimeSource } from "./generated/isolate-runtime.js";
22

3+
/**
4+
* Get the isolate-side script that initializes early mutable runtime globals
5+
* (`_moduleCache`, `_pendingModules`, `_currentModule`) before module loading
6+
* and require wiring run.
7+
*/
38
export function getInitialBridgeGlobalsSetupCode(): string {
49
return getIsolateRuntimeSource("bridgeInitialGlobals");
510
}

packages/secure-exec/src/bridge/child-process.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ declare const _unregisterHandle: UnregisterHandleBridgeFn;
4747
// Active children registry - maps session ID to ChildProcess
4848
const activeChildren = new Map<number, ChildProcess>();
4949

50-
// Global dispatcher - host calls this when data arrives
50+
/**
51+
* Global dispatcher invoked by the host when child process data arrives.
52+
* Routes stdout/stderr chunks and exit codes to the corresponding ChildProcess
53+
* instance by session ID, and unregisters the active handle on exit.
54+
*/
5155
const childProcessDispatch = (
5256
sessionId: number,
5357
type: "stdout" | "stderr" | "exit",
@@ -106,7 +110,11 @@ interface OutputStreamStub {
106110
pipe<T extends NodeJS.WritableStream>(dest: T): T;
107111
}
108112

109-
// ChildProcess class - simplified interface, not strictly satisfying nodeChildProcess.ChildProcess
113+
/**
114+
* Polyfill of Node.js `ChildProcess`. Provides event-emitting stdin/stdout/stderr
115+
* streams. In streaming mode, data arrives via the `_childProcessDispatch` global
116+
* that the host calls with stdout/stderr/exit events keyed by session ID.
117+
*/
110118
class ChildProcess {
111119
private _listeners: Record<string, EventListener[]> = {};
112120
private _onceListeners: Record<string, EventListener[]> = {};

packages/secure-exec/src/bridge/module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,9 @@ export function createRequire(filename: string | URL): RequireFunction {
160160
}
161161

162162
/**
163-
* Module class constructor (for compatibility with promzard and similar)
163+
* Polyfill of Node.js `Module` class for sandbox compatibility. Provides
164+
* `_compile`, `_resolveFilename`, `_load`, `_extensions`, and `_cache` statics
165+
* that npm tooling (promzard, resolve, etc.) relies on.
164166
*/
165167
export class Module {
166168
id: string;

packages/secure-exec/src/bridge/network.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,12 @@ export const dns = {
332332
// Event listener type
333333
type EventListener = (...args: unknown[]) => void;
334334

335-
// IncomingMessage class
335+
/**
336+
* Polyfill of Node.js `http.IncomingMessage` (client-side response). Buffers
337+
* the response body eagerly and emits `data`/`end` events on listener
338+
* registration (flowing mode). Supports base64 binary decoding via
339+
* `x-body-encoding` header.
340+
*/
336341
export class IncomingMessage {
337342
headers: Record<string, string>;
338343
rawHeaders: string[];
@@ -632,7 +637,11 @@ export class IncomingMessage {
632637
}
633638
}
634639

635-
// ClientRequest class
640+
/**
641+
* Polyfill of Node.js `http.ClientRequest`. Executes the request asynchronously
642+
* via the `_networkHttpRequestRaw` bridge and emits a `response` event with
643+
* an IncomingMessage.
644+
*/
636645
export class ClientRequest {
637646
private _options: nodeHttp.RequestOptions;
638647
private _callback?: (res: IncomingMessage) => void;
@@ -900,6 +909,10 @@ class ServerIncomingMessage {
900909
}
901910
}
902911

912+
/**
913+
* Sandbox-side response writer for HTTP server requests. Collects headers and
914+
* body chunks, then serializes to JSON for transfer back to the host.
915+
*/
903916
class ServerResponseBridge {
904917
statusCode = 200;
905918
statusMessage = "OK";
@@ -1043,6 +1056,12 @@ class ServerResponseBridge {
10431056
}
10441057
}
10451058

1059+
/**
1060+
* Polyfill of Node.js `http.Server`. Delegates actual listening to the host
1061+
* via the `_networkHttpServerListenRaw` bridge. Incoming requests are
1062+
* dispatched through `_httpServerDispatch` which invokes the request listener
1063+
* inside the isolate. Registers an active handle to keep the sandbox alive.
1064+
*/
10461065
class Server {
10471066
listening = false;
10481067
private _listeners: Record<string, EventListener[]> = {};
@@ -1193,6 +1212,7 @@ class Server {
11931212
}
11941213
}
11951214

1215+
/** Route an incoming HTTP request to the server's request listener and return the serialized response. */
11961216
async function dispatchServerRequest(
11971217
serverId: number,
11981218
requestJson: string

packages/secure-exec/src/bridge/process.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ import {
2525
} from "../shared/global-exposure.js";
2626

2727

28-
// Configuration interface - values are set via globals before bridge loads
28+
/**
29+
* Process configuration injected by the host before the bridge bundle loads.
30+
* Values default to sensible Linux/x64 stubs when unset.
31+
*/
2932
export interface ProcessConfig {
3033
platform?: string;
3134
arch?: string;
@@ -87,6 +90,7 @@ const config = {
8790
typeof _processConfig !== "undefined" ? _processConfig.frozenTimeMs : undefined,
8891
};
8992

93+
/** Get the current timestamp, returning a frozen value when timing mitigation is active. */
9094
function getNowMs(): number {
9195
if (
9296
config.timingMitigation === "freeze" &&
@@ -142,7 +146,10 @@ if (
142146
let _exitCode = 0;
143147
let _exited = false;
144148

145-
// ProcessExitError class for controlled exits
149+
/**
150+
* Thrown by `process.exit()` to unwind the sandbox call stack. The host
151+
* catches this to extract the exit code without killing the isolate.
152+
*/
146153
export class ProcessExitError extends Error {
147154
code: number;
148155
constructor(code: number) {
@@ -848,7 +855,11 @@ const _queueMicrotask =
848855
Promise.resolve().then(fn);
849856
};
850857

851-
// Timer handle class that mimics Node.js Timeout object
858+
/**
859+
* Timer handle that mimics Node.js Timeout (ref/unref/Symbol.toPrimitive).
860+
* Timers with delay > 0 use the host's `_scheduleTimer` bridge to sleep
861+
* without blocking the isolate's event loop.
862+
*/
852863
class TimerHandle {
853864
_id: number;
854865
_destroyed: boolean;
@@ -1011,7 +1022,11 @@ function throwUnsupportedCryptoApi(api: "getRandomValues" | "randomUUID"): never
10111022
throw new Error(`crypto.${api} is not supported in sandbox`);
10121023
}
10131024

1014-
// Crypto polyfill
1025+
/**
1026+
* Crypto polyfill that delegates to the host for entropy. `getRandomValues`
1027+
* calls the host's `_cryptoRandomFill` bridge to get cryptographically secure
1028+
* random bytes. Subtle crypto operations are unsupported.
1029+
*/
10151030
export const cryptoPolyfill = {
10161031
getRandomValues<T extends ArrayBufferView>(array: T): T {
10171032
if (typeof _cryptoRandomFill === "undefined") {
@@ -1063,7 +1078,10 @@ export const cryptoPolyfill = {
10631078
},
10641079
};
10651080

1066-
// Setup globals function - call this to install polyfills on globalThis
1081+
/**
1082+
* Install all process/timer/URL/Buffer/crypto polyfills onto `globalThis`.
1083+
* Called once during bridge initialization before user code runs.
1084+
*/
10671085
export function setupGlobals(): void {
10681086
const g = globalThis as Record<string, unknown>;
10691087

packages/secure-exec/src/browser/driver.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ async function getRootHandle(): Promise<FileSystemDirectoryHandle> {
4747
return navigator.storage.getDirectory();
4848
}
4949

50+
/**
51+
* VFS backed by the Origin Private File System (OPFS) API. Falls back to
52+
* InMemoryFileSystem when OPFS is unavailable. Rename is not supported
53+
* (throws ENOSYS) since OPFS doesn't provide atomic rename.
54+
*/
5055
export class OpfsFileSystem implements VirtualFileSystem {
5156
private rootPromise: Promise<FileSystemDirectoryHandle>;
5257

@@ -229,13 +234,15 @@ export interface BrowserDriverOptions {
229234
useDefaultNetwork?: boolean;
230235
}
231236

237+
/** Create an OPFS-backed filesystem, falling back to in-memory if OPFS is unavailable. */
232238
export async function createOpfsFileSystem(): Promise<VirtualFileSystem> {
233239
if (!("storage" in navigator) || typeof navigator.storage.getDirectory !== "function") {
234240
return createInMemoryFileSystem();
235241
}
236242
return new OpfsFileSystem();
237243
}
238244

245+
/** Network adapter that delegates to the browser's native `fetch`. DNS and http2 are unsupported. */
239246
export function createBrowserNetworkAdapter(): NetworkAdapter {
240247
return {
241248
async fetch(url, options) {
@@ -301,6 +308,7 @@ export function createBrowserNetworkAdapter(): NetworkAdapter {
301308
};
302309
}
303310

311+
/** Assemble a browser-side SandboxDriver with permission-wrapped adapters. */
304312
export async function createBrowserDriver(
305313
options: BrowserDriverOptions = {},
306314
): Promise<SandboxDriver> {

packages/secure-exec/src/browser/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ function serializePermissions(permissions?: Permissions): SerializedPermissions
5050
};
5151
}
5252

53+
/**
54+
* Browser-side sandbox that runs code in a Web Worker. Communicates with the
55+
* worker via `postMessage` using a request/response protocol keyed by
56+
* monotonic IDs. The worker initializes its own runtime (permissions, fs,
57+
* network, bridge modules) before accepting exec/run requests.
58+
*/
5359
export class BrowserSandbox {
5460
private worker: Worker;
5561
private nextId = 1;

packages/secure-exec/src/browser/worker.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ function revivePermission(source?: string): ((req: unknown) => { allow: boolean
9292
}
9393
}
9494

95+
/** Deserialize permission callbacks that were stringified for transfer across the Worker boundary. */
9596
function revivePermissions(serialized?: SerializedPermissions): Permissions | undefined {
9697
if (!serialized) return undefined;
9798
const perms: Permissions = {};
@@ -102,6 +103,10 @@ function revivePermissions(serialized?: SerializedPermissions): Permissions | un
102103
return perms;
103104
}
104105

106+
/**
107+
* Wrap a sync function in the bridge calling convention (`applySync`) so
108+
* bridge code can call it the same way it calls isolated-vm References.
109+
*/
105110
function makeApplySync<TArgs extends unknown[], TResult>(
106111
fn: (...args: TArgs) => TResult,
107112
) {
@@ -132,6 +137,10 @@ function makeApplyPromise<TArgs extends unknown[], TResult>(
132137
};
133138
}
134139

140+
/**
141+
* Initialize the worker-side runtime: set up filesystem, network, bridge
142+
* globals, and load the bridge bundle. Called once before any exec/run.
143+
*/
135144
async function initRuntime(payload: InitPayload): Promise<void> {
136145
if (initialized) return;
137146

@@ -409,6 +418,10 @@ function updateProcessConfig(options?: ExecOptions): void {
409418
}
410419
}
411420

421+
/**
422+
* Execute user code as a script (process-style). Transforms ESM/dynamic
423+
* imports, sets up module/exports globals, and waits for active handles.
424+
*/
412425
async function execScript(
413426
code: string,
414427
options?: ExecOptions,

packages/secure-exec/src/esm-compiler.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1+
/**
2+
* ESM wrapper generator for built-in modules inside the isolate.
3+
*
4+
* isolated-vm's ESM `import` can only resolve modules we explicitly provide.
5+
* For Node built-ins (fs, path, etc.) we generate thin ESM wrappers that
6+
* re-export the bridge-provided globalThis objects as proper ESM modules
7+
* with both default and named exports.
8+
*/
9+
110
import { BUILTIN_NAMED_EXPORTS } from "./module-resolver.js";
211

312
function isValidIdentifier(value: string): boolean {
413
return /^[$A-Z_][0-9A-Z_$]*$/i.test(value);
514
}
615

16+
/** Generate `export const X = _builtin.X;` lines for each known named export. */
717
function buildNamedExportLines(namedExports: string[]): string[] {
818
return Array.from(new Set(namedExports))
919
.filter(isValidIdentifier)
@@ -17,6 +27,10 @@ function buildNamedExportLines(namedExports: string[]): string[] {
1727
);
1828
}
1929

30+
/**
31+
* Build a complete ESM wrapper that reads a bridge global via `bindingExpression`
32+
* and re-exports it as `default` plus individual named exports.
33+
*/
2034
function buildWrapperSource(bindingExpression: string, namedExports: string[]): string {
2135
const lines = [
2236
"const _builtin = " + bindingExpression + ";",
@@ -72,17 +86,20 @@ const STATIC_BUILTIN_WRAPPER_SOURCES: Readonly<Record<string, string>> = {
7286
v8: buildWrapperSource("globalThis._moduleCache?.v8 || {}", []),
7387
};
7488

89+
/** Get a pre-built ESM wrapper for a bridge-backed built-in, or null if not bridge-handled. */
7590
export function getStaticBuiltinWrapperSource(moduleName: string): string | null {
7691
return STATIC_BUILTIN_WRAPPER_SOURCES[moduleName] ?? null;
7792
}
7893

94+
/** Build a custom ESM wrapper for a dynamically-resolved module (e.g. polyfills). */
7995
export function createBuiltinESMWrapper(
8096
bindingExpression: string,
8197
namedExports: string[],
8298
): string {
8399
return buildWrapperSource(bindingExpression, namedExports);
84100
}
85101

102+
/** Wrapper for unsupported built-ins: exports an empty object as default. */
86103
export function getEmptyBuiltinESMWrapper(): string {
87104
return buildWrapperSource("{}", []);
88105
}

0 commit comments

Comments
 (0)