@secure-exec/nodejs 0.2.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +7 -0
  3. package/dist/bindings.d.ts +31 -0
  4. package/dist/bindings.js +67 -0
  5. package/dist/bridge/active-handles.d.ts +22 -0
  6. package/dist/bridge/active-handles.js +112 -0
  7. package/dist/bridge/child-process.d.ts +99 -0
  8. package/dist/bridge/child-process.js +672 -0
  9. package/dist/bridge/dispatch.d.ts +2 -0
  10. package/dist/bridge/dispatch.js +40 -0
  11. package/dist/bridge/fs.d.ts +502 -0
  12. package/dist/bridge/fs.js +3307 -0
  13. package/dist/bridge/index.d.ts +10 -0
  14. package/dist/bridge/index.js +41 -0
  15. package/dist/bridge/module.d.ts +75 -0
  16. package/dist/bridge/module.js +325 -0
  17. package/dist/bridge/network.d.ts +1093 -0
  18. package/dist/bridge/network.js +8651 -0
  19. package/dist/bridge/os.d.ts +13 -0
  20. package/dist/bridge/os.js +256 -0
  21. package/dist/bridge/polyfills.d.ts +9 -0
  22. package/dist/bridge/polyfills.js +67 -0
  23. package/dist/bridge/process.d.ts +121 -0
  24. package/dist/bridge/process.js +1382 -0
  25. package/dist/bridge/whatwg-url.d.ts +67 -0
  26. package/dist/bridge/whatwg-url.js +712 -0
  27. package/dist/bridge-contract.d.ts +774 -0
  28. package/dist/bridge-contract.js +172 -0
  29. package/dist/bridge-handlers.d.ts +199 -0
  30. package/dist/bridge-handlers.js +4263 -0
  31. package/dist/bridge-loader.d.ts +9 -0
  32. package/dist/bridge-loader.js +87 -0
  33. package/dist/bridge-setup.d.ts +1 -0
  34. package/dist/bridge-setup.js +3 -0
  35. package/dist/bridge.js +21652 -0
  36. package/dist/builtin-modules.d.ts +25 -0
  37. package/dist/builtin-modules.js +312 -0
  38. package/dist/default-network-adapter.d.ts +13 -0
  39. package/dist/default-network-adapter.js +351 -0
  40. package/dist/driver.d.ts +87 -0
  41. package/dist/driver.js +191 -0
  42. package/dist/esm-compiler.d.ts +14 -0
  43. package/dist/esm-compiler.js +68 -0
  44. package/dist/execution-driver.d.ts +37 -0
  45. package/dist/execution-driver.js +977 -0
  46. package/dist/host-network-adapter.d.ts +7 -0
  47. package/dist/host-network-adapter.js +279 -0
  48. package/dist/index.d.ts +20 -0
  49. package/dist/index.js +23 -0
  50. package/dist/isolate-bootstrap.d.ts +86 -0
  51. package/dist/isolate-bootstrap.js +125 -0
  52. package/dist/ivm-compat.d.ts +7 -0
  53. package/dist/ivm-compat.js +31 -0
  54. package/dist/kernel-runtime.d.ts +58 -0
  55. package/dist/kernel-runtime.js +535 -0
  56. package/dist/module-access.d.ts +75 -0
  57. package/dist/module-access.js +606 -0
  58. package/dist/module-resolver.d.ts +8 -0
  59. package/dist/module-resolver.js +150 -0
  60. package/dist/os-filesystem.d.ts +42 -0
  61. package/dist/os-filesystem.js +161 -0
  62. package/dist/package-bundler.d.ts +36 -0
  63. package/dist/package-bundler.js +497 -0
  64. package/dist/polyfills.d.ts +17 -0
  65. package/dist/polyfills.js +97 -0
  66. package/dist/worker-adapter.d.ts +21 -0
  67. package/dist/worker-adapter.js +34 -0
  68. package/package.json +123 -0
@@ -0,0 +1,4263 @@
1
+ // Build a BridgeHandlers map for V8 runtime.
2
+ //
3
+ // Each handler is a plain function that performs the host-side operation.
4
+ // Handler names match HOST_BRIDGE_GLOBAL_KEYS from the bridge contract.
5
+ import * as http from "node:http";
6
+ import * as http2 from "node:http2";
7
+ import * as tls from "node:tls";
8
+ import { Duplex, PassThrough } from "node:stream";
9
+ import { readFileSync, realpathSync, existsSync } from "node:fs";
10
+ import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from "node:path";
11
+ import { createRequire } from "node:module";
12
+ import { serialize } from "node:v8";
13
+ import { randomFillSync, randomUUID, createHash, createHmac, pbkdf2Sync, scryptSync, hkdfSync, createCipheriv, createDecipheriv, sign, verify, generateKeyPairSync, createPrivateKey, createPublicKey, createSecretKey, createDiffieHellman, getDiffieHellman, createECDH, diffieHellman, generateKeySync, generatePrimeSync, publicEncrypt, privateDecrypt, privateEncrypt, publicDecrypt, timingSafeEqual, constants as cryptoConstants, } from "node:crypto";
14
+ import { HOST_BRIDGE_GLOBAL_KEYS, } from "./bridge-contract.js";
15
+ import { AF_INET, AF_INET6, AF_UNIX, SOCK_DGRAM, SOCK_STREAM, mkdir, FDTableManager, O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC, O_APPEND, FILETYPE_REGULAR_FILE, } from "@secure-exec/core";
16
+ import { normalizeBuiltinSpecifier } from "./builtin-modules.js";
17
+ import { resolveModule, loadFile } from "./package-bundler.js";
18
+ import { transformDynamicImport, isESM } from "@secure-exec/core/internal/shared/esm-utils";
19
+ import { bundlePolyfill, hasPolyfill } from "./polyfills.js";
20
+ import { createBuiltinESMWrapper, getStaticBuiltinWrapperSource, } from "./esm-compiler.js";
21
+ import { checkBridgeBudget, assertPayloadByteLength, assertTextPayloadSize, getBase64EncodedByteLength, getHostBuiltinNamedExports, parseJsonWithLimit, polyfillCodeCache, RESOURCE_BUDGET_ERROR_CODE, } from "./isolate-bootstrap.js";
22
+ const SOL_SOCKET = 1;
23
+ const IPPROTO_TCP = 6;
24
+ const SO_KEEPALIVE = 9;
25
+ const SO_RCVBUF = 8;
26
+ const SO_SNDBUF = 7;
27
+ const TCP_NODELAY = 1;
28
+ function serializeKeyDetails(details) {
29
+ if (!details || typeof details !== "object") {
30
+ return undefined;
31
+ }
32
+ return Object.fromEntries(Object.entries(details).map(([key, value]) => [
33
+ key,
34
+ typeof value === "bigint"
35
+ ? { __type: "bigint", value: value.toString() }
36
+ : value,
37
+ ]));
38
+ }
39
+ function serializeKeyValue(value) {
40
+ if (Buffer.isBuffer(value)) {
41
+ return {
42
+ kind: "buffer",
43
+ value: value.toString("base64"),
44
+ };
45
+ }
46
+ if (typeof value === "string") {
47
+ return {
48
+ kind: "string",
49
+ value,
50
+ };
51
+ }
52
+ if (value &&
53
+ typeof value === "object" &&
54
+ "type" in value &&
55
+ (value.type === "public" ||
56
+ value.type === "private") &&
57
+ typeof value.export === "function") {
58
+ return {
59
+ kind: "keyObject",
60
+ value: serializeSandboxKeyObject(value),
61
+ };
62
+ }
63
+ return {
64
+ kind: "object",
65
+ value: value,
66
+ };
67
+ }
68
+ function exportAsPem(keyObject) {
69
+ return keyObject.type === "private"
70
+ ? keyObject.export({ type: "pkcs8", format: "pem" })
71
+ : keyObject.export({ type: "spki", format: "pem" });
72
+ }
73
+ function serializeSandboxKeyObject(keyObject) {
74
+ let jwk;
75
+ try {
76
+ jwk = keyObject.export({ format: "jwk" });
77
+ }
78
+ catch {
79
+ jwk = undefined;
80
+ }
81
+ return {
82
+ type: keyObject.type,
83
+ pem: exportAsPem(keyObject),
84
+ asymmetricKeyType: keyObject.asymmetricKeyType ?? undefined,
85
+ asymmetricKeyDetails: serializeKeyDetails(keyObject.asymmetricKeyDetails),
86
+ jwk,
87
+ };
88
+ }
89
+ function serializeAnyKeyObject(keyObject) {
90
+ if (keyObject.type === "secret") {
91
+ return {
92
+ type: "secret",
93
+ raw: Buffer.from(keyObject.export()).toString("base64"),
94
+ };
95
+ }
96
+ return serializeSandboxKeyObject(keyObject);
97
+ }
98
+ function serializeBridgeValue(value) {
99
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
100
+ return value;
101
+ }
102
+ if (typeof value === "bigint") {
103
+ return {
104
+ __type: "bigint",
105
+ value: value.toString(),
106
+ };
107
+ }
108
+ if (Buffer.isBuffer(value)) {
109
+ return {
110
+ __type: "buffer",
111
+ value: value.toString("base64"),
112
+ };
113
+ }
114
+ if (value instanceof ArrayBuffer) {
115
+ return {
116
+ __type: "buffer",
117
+ value: Buffer.from(value).toString("base64"),
118
+ };
119
+ }
120
+ if (ArrayBuffer.isView(value)) {
121
+ return {
122
+ __type: "buffer",
123
+ value: Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("base64"),
124
+ };
125
+ }
126
+ if (Array.isArray(value)) {
127
+ return value.map((entry) => serializeBridgeValue(entry));
128
+ }
129
+ if (value &&
130
+ typeof value === "object" &&
131
+ "type" in value &&
132
+ ((value.type === "public" ||
133
+ value.type === "private" ||
134
+ value.type === "secret")) &&
135
+ typeof value.export === "function") {
136
+ return {
137
+ __type: "keyObject",
138
+ value: serializeAnyKeyObject(value),
139
+ };
140
+ }
141
+ if (value && typeof value === "object") {
142
+ return Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => entry === undefined ? [] : [[key, serializeBridgeValue(entry)]]));
143
+ }
144
+ return String(value);
145
+ }
146
+ function deserializeSandboxKeyObject(serialized) {
147
+ if (serialized.type === "secret") {
148
+ return createSecretKey(Buffer.from(serialized.raw || "", "base64"));
149
+ }
150
+ if (serialized.type === "private") {
151
+ return createPrivateKey(String(serialized.pem || ""));
152
+ }
153
+ return createPublicKey(String(serialized.pem || ""));
154
+ }
155
+ function deserializeBridgeValue(value) {
156
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
157
+ return value;
158
+ }
159
+ if (Array.isArray(value)) {
160
+ return value.map((entry) => deserializeBridgeValue(entry));
161
+ }
162
+ if ("__type" in value) {
163
+ if (value.__type === "buffer") {
164
+ return Buffer.from(value.value, "base64");
165
+ }
166
+ if (value.__type === "bigint") {
167
+ return BigInt(value.value);
168
+ }
169
+ if (value.__type === "keyObject") {
170
+ return deserializeSandboxKeyObject(value.value);
171
+ }
172
+ }
173
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, deserializeBridgeValue(entry)]));
174
+ }
175
+ function parseSerializedOptions(optionsJson) {
176
+ const parsed = JSON.parse(String(optionsJson));
177
+ if (!parsed || parsed.hasOptions !== true) {
178
+ return undefined;
179
+ }
180
+ return deserializeBridgeValue(parsed.options ?? null);
181
+ }
182
+ function serializeDispatchError(error) {
183
+ if (error instanceof Error) {
184
+ const withCode = error;
185
+ return {
186
+ message: error.message,
187
+ name: error.name,
188
+ code: typeof withCode.code === "string" ? withCode.code : undefined,
189
+ stack: error.stack,
190
+ };
191
+ }
192
+ return {
193
+ message: String(error),
194
+ name: "Error",
195
+ };
196
+ }
197
+ function restoreDispatchArgument(value) {
198
+ if (!value || typeof value !== "object") {
199
+ return value;
200
+ }
201
+ if (value.__secureExecDispatchType ===
202
+ "undefined") {
203
+ return undefined;
204
+ }
205
+ if (Array.isArray(value)) {
206
+ return value.map((entry) => restoreDispatchArgument(entry));
207
+ }
208
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, restoreDispatchArgument(entry)]));
209
+ }
210
+ function normalizeBridgeAlgorithm(algorithm) {
211
+ if (algorithm === null || algorithm === undefined || algorithm === "") {
212
+ return null;
213
+ }
214
+ return String(algorithm);
215
+ }
216
+ function decodeBridgeBuffer(data) {
217
+ return Buffer.from(String(data), "base64");
218
+ }
219
+ function sanitizeJsonValue(value) {
220
+ if (typeof value === "bigint") {
221
+ return Number(value);
222
+ }
223
+ if (Array.isArray(value)) {
224
+ return value.map((entry) => sanitizeJsonValue(entry));
225
+ }
226
+ if (!value || typeof value !== "object") {
227
+ return value;
228
+ }
229
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
230
+ key,
231
+ sanitizeJsonValue(entry),
232
+ ]));
233
+ }
234
+ function serializeCryptoKeyDataFromKeyObject(keyObject, type, algorithm, extractable, usages) {
235
+ if (type === "secret") {
236
+ return {
237
+ type,
238
+ algorithm,
239
+ extractable,
240
+ usages,
241
+ _raw: keyObject.export().toString("base64"),
242
+ _sourceKeyObjectData: {
243
+ type: "secret",
244
+ raw: keyObject.export().toString("base64"),
245
+ },
246
+ };
247
+ }
248
+ return {
249
+ type,
250
+ algorithm,
251
+ extractable,
252
+ usages,
253
+ _pem: type === "private"
254
+ ? keyObject.export({ type: "pkcs8", format: "pem" })
255
+ : keyObject.export({ type: "spki", format: "pem" }),
256
+ _sourceKeyObjectData: {
257
+ type,
258
+ pem: type === "private"
259
+ ? keyObject.export({ type: "pkcs8", format: "pem" })
260
+ : keyObject.export({ type: "spki", format: "pem" }),
261
+ asymmetricKeyType: keyObject.asymmetricKeyType,
262
+ asymmetricKeyDetails: sanitizeJsonValue(keyObject.asymmetricKeyDetails),
263
+ },
264
+ };
265
+ }
266
+ function deserializeCryptoKeyObject(key) {
267
+ if (key.type === "secret") {
268
+ return createSecretKey(decodeBridgeBuffer(key._raw));
269
+ }
270
+ return key.type === "private"
271
+ ? createPrivateKey(key._pem ?? "")
272
+ : createPublicKey(key._pem ?? "");
273
+ }
274
+ function normalizeHmacLength(hashName, explicitLength) {
275
+ if (typeof explicitLength === "number") {
276
+ return explicitLength;
277
+ }
278
+ switch (hashName) {
279
+ case "SHA-1":
280
+ case "SHA-256":
281
+ return 512;
282
+ case "SHA-384":
283
+ case "SHA-512":
284
+ return 1024;
285
+ default:
286
+ return 512;
287
+ }
288
+ }
289
+ function sliceDerivedBits(secret, length) {
290
+ if (length === undefined || length === null) {
291
+ return Buffer.from(secret);
292
+ }
293
+ const requestedBits = Number(length);
294
+ const maxBits = secret.byteLength * 8;
295
+ if (requestedBits > maxBits) {
296
+ throw new Error("derived bit length is too small");
297
+ }
298
+ const requestedBytes = Math.ceil(requestedBits / 8);
299
+ const derived = Buffer.from(secret.subarray(0, requestedBytes));
300
+ const remainder = requestedBits % 8;
301
+ if (remainder !== 0 && derived.length > 0) {
302
+ derived[derived.length - 1] &= 0xff << (8 - remainder);
303
+ }
304
+ return derived;
305
+ }
306
+ function deriveSecretKeyData(derivedKeyAlgorithm, extractable, usages, secret) {
307
+ const normalizedAlgorithm = typeof derivedKeyAlgorithm === "string"
308
+ ? { name: derivedKeyAlgorithm }
309
+ : derivedKeyAlgorithm;
310
+ const algorithmName = String(normalizedAlgorithm.name ?? "");
311
+ if (algorithmName === "HMAC") {
312
+ const hashName = typeof normalizedAlgorithm.hash === "string"
313
+ ? normalizedAlgorithm.hash
314
+ : String(normalizedAlgorithm.hash?.name ?? "");
315
+ const lengthBits = normalizeHmacLength(hashName, normalizedAlgorithm.length);
316
+ const keyBytes = Buffer.from(secret.subarray(0, Math.ceil(lengthBits / 8)));
317
+ return serializeCryptoKeyDataFromKeyObject(createSecretKey(keyBytes), "secret", {
318
+ name: "HMAC",
319
+ hash: { name: hashName },
320
+ length: lengthBits,
321
+ }, extractable, usages);
322
+ }
323
+ const lengthBits = Number(normalizedAlgorithm.length ?? secret.byteLength * 8);
324
+ const keyBytes = Buffer.from(secret.subarray(0, Math.ceil(lengthBits / 8)));
325
+ return serializeCryptoKeyDataFromKeyObject(createSecretKey(keyBytes), "secret", {
326
+ ...normalizedAlgorithm,
327
+ length: lengthBits,
328
+ }, extractable, usages);
329
+ }
330
+ function resolveDerivedKeyLengthBits(derivedKeyAlgorithm, fallbackBits) {
331
+ const normalizedAlgorithm = typeof derivedKeyAlgorithm === "string"
332
+ ? { name: derivedKeyAlgorithm }
333
+ : derivedKeyAlgorithm;
334
+ if (typeof normalizedAlgorithm.length === "number") {
335
+ return normalizedAlgorithm.length;
336
+ }
337
+ if (normalizedAlgorithm.name === "HMAC") {
338
+ const hashName = typeof normalizedAlgorithm.hash === "string"
339
+ ? normalizedAlgorithm.hash
340
+ : String(normalizedAlgorithm.hash?.name ?? "");
341
+ return normalizeHmacLength(hashName);
342
+ }
343
+ return fallbackBits;
344
+ }
345
+ /**
346
+ * Build crypto bridge handlers.
347
+ *
348
+ * All handler functions are plain functions (no ivm.Reference wrapping).
349
+ * The V8 runtime registers these by name on the V8 global.
350
+ * Call dispose() when the execution ends to clear stateful cipher sessions.
351
+ */
352
+ export function buildCryptoBridgeHandlers() {
353
+ const handlers = {};
354
+ const K = HOST_BRIDGE_GLOBAL_KEYS;
355
+ // Stateful cipher sessions — tracks cipher/decipher instances between
356
+ // create/update/final bridge calls (needed for ssh2 streaming AES-GCM).
357
+ const cipherSessions = new Map();
358
+ let nextCipherSessionId = 1;
359
+ const diffieHellmanSessions = new Map();
360
+ let nextDiffieHellmanSessionId = 1;
361
+ // Secure randomness — cap matches Web Crypto API spec (65536 bytes).
362
+ handlers[K.cryptoRandomFill] = (byteLength) => {
363
+ const len = Number(byteLength);
364
+ if (len > 65536) {
365
+ throw new RangeError(`The ArrayBufferView's byte length (${len}) exceeds the number of bytes of entropy available via this API (65536)`);
366
+ }
367
+ const buffer = Buffer.allocUnsafe(len);
368
+ randomFillSync(buffer);
369
+ return buffer.toString("base64");
370
+ };
371
+ handlers[K.cryptoRandomUuid] = () => randomUUID();
372
+ // createHash — guest accumulates update() data, sends base64 to host for digest.
373
+ handlers[K.cryptoHashDigest] = (algorithm, dataBase64) => {
374
+ const data = Buffer.from(String(dataBase64), "base64");
375
+ const hash = createHash(String(algorithm));
376
+ hash.update(data);
377
+ return hash.digest("base64");
378
+ };
379
+ // createHmac — guest accumulates update() data, sends base64 to host for HMAC digest.
380
+ handlers[K.cryptoHmacDigest] = (algorithm, keyBase64, dataBase64) => {
381
+ const key = Buffer.from(String(keyBase64), "base64");
382
+ const data = Buffer.from(String(dataBase64), "base64");
383
+ const hmac = createHmac(String(algorithm), key);
384
+ hmac.update(data);
385
+ return hmac.digest("base64");
386
+ };
387
+ // pbkdf2Sync — derive key from password + salt.
388
+ handlers[K.cryptoPbkdf2] = (passwordBase64, saltBase64, iterations, keylen, digest) => {
389
+ const password = Buffer.from(String(passwordBase64), "base64");
390
+ const salt = Buffer.from(String(saltBase64), "base64");
391
+ return pbkdf2Sync(password, salt, Number(iterations), Number(keylen), String(digest)).toString("base64");
392
+ };
393
+ // scryptSync — derive key from password + salt with tunable cost params.
394
+ handlers[K.cryptoScrypt] = (passwordBase64, saltBase64, keylen, optionsJson) => {
395
+ const password = Buffer.from(String(passwordBase64), "base64");
396
+ const salt = Buffer.from(String(saltBase64), "base64");
397
+ const options = JSON.parse(String(optionsJson));
398
+ return scryptSync(password, salt, Number(keylen), options).toString("base64");
399
+ };
400
+ // createCipheriv — guest accumulates update() data, sends base64 to host for encryption.
401
+ // Returns JSON with data (and authTag for GCM modes).
402
+ handlers[K.cryptoCipheriv] = (algorithm, keyBase64, ivBase64, dataBase64, optionsJson) => {
403
+ const key = Buffer.from(String(keyBase64), "base64");
404
+ const iv = ivBase64 === null ? null : Buffer.from(String(ivBase64), "base64");
405
+ const data = Buffer.from(String(dataBase64), "base64");
406
+ const options = optionsJson ? JSON.parse(String(optionsJson)) : {};
407
+ const cipher = createCipheriv(String(algorithm), key, iv, (options.authTagLength !== undefined
408
+ ? { authTagLength: options.authTagLength }
409
+ : undefined));
410
+ if (options.validateOnly) {
411
+ return JSON.stringify({ data: "" });
412
+ }
413
+ if (options.aad) {
414
+ cipher.setAAD(Buffer.from(String(options.aad), "base64"), options.aadOptions);
415
+ }
416
+ if (options.autoPadding !== undefined) {
417
+ cipher.setAutoPadding(Boolean(options.autoPadding));
418
+ }
419
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
420
+ const isAead = /-(gcm|ccm)$/i.test(String(algorithm));
421
+ if (isAead) {
422
+ return JSON.stringify({
423
+ data: encrypted.toString("base64"),
424
+ authTag: cipher.getAuthTag().toString("base64"),
425
+ });
426
+ }
427
+ return JSON.stringify({ data: encrypted.toString("base64") });
428
+ };
429
+ // createDecipheriv — guest accumulates update() data, sends base64 to host for decryption.
430
+ // Accepts optionsJson with authTag for GCM modes.
431
+ handlers[K.cryptoDecipheriv] = (algorithm, keyBase64, ivBase64, dataBase64, optionsJson) => {
432
+ const key = Buffer.from(String(keyBase64), "base64");
433
+ const iv = ivBase64 === null ? null : Buffer.from(String(ivBase64), "base64");
434
+ const data = Buffer.from(String(dataBase64), "base64");
435
+ const options = JSON.parse(String(optionsJson));
436
+ const decipher = createDecipheriv(String(algorithm), key, iv, (options.authTagLength !== undefined
437
+ ? { authTagLength: options.authTagLength }
438
+ : undefined));
439
+ if (options.validateOnly) {
440
+ return "";
441
+ }
442
+ const isAead = /-(gcm|ccm)$/i.test(String(algorithm));
443
+ if (isAead && options.authTag) {
444
+ decipher.setAuthTag(Buffer.from(options.authTag, "base64"));
445
+ }
446
+ if (options.aad) {
447
+ decipher.setAAD(Buffer.from(String(options.aad), "base64"), options.aadOptions);
448
+ }
449
+ if (options.autoPadding !== undefined) {
450
+ decipher.setAutoPadding(Boolean(options.autoPadding));
451
+ }
452
+ return Buffer.concat([decipher.update(data), decipher.final()]).toString("base64");
453
+ };
454
+ // Stateful cipheriv create — opens a cipher or decipher session on the host.
455
+ // mode: "cipher" | "decipher"; returns sessionId.
456
+ handlers[K.cryptoCipherivCreate] = (mode, algorithm, keyBase64, ivBase64, optionsJson) => {
457
+ const algo = String(algorithm);
458
+ const key = Buffer.from(String(keyBase64), "base64");
459
+ const iv = ivBase64 === null ? null : Buffer.from(String(ivBase64), "base64");
460
+ const options = optionsJson ? JSON.parse(String(optionsJson)) : {};
461
+ const isAead = /-(gcm|ccm)$/i.test(algo);
462
+ let instance;
463
+ if (String(mode) === "decipher") {
464
+ const d = createDecipheriv(algo, key, iv, (options.authTagLength !== undefined
465
+ ? { authTagLength: options.authTagLength }
466
+ : undefined));
467
+ if (isAead && options.authTag) {
468
+ d.setAuthTag(Buffer.from(options.authTag, "base64"));
469
+ }
470
+ instance = d;
471
+ }
472
+ else {
473
+ instance = createCipheriv(algo, key, iv, (options.authTagLength !== undefined
474
+ ? { authTagLength: options.authTagLength }
475
+ : undefined));
476
+ }
477
+ const sessionId = nextCipherSessionId++;
478
+ cipherSessions.set(sessionId, { cipher: instance, algorithm: algo });
479
+ return sessionId;
480
+ };
481
+ // Stateful cipheriv update — feeds data into an open session, returns partial result.
482
+ handlers[K.cryptoCipherivUpdate] = (sessionId, dataBase64) => {
483
+ const id = Number(sessionId);
484
+ const session = cipherSessions.get(id);
485
+ if (!session)
486
+ throw new Error(`Cipher session ${id} not found`);
487
+ const data = Buffer.from(String(dataBase64), "base64");
488
+ const result = session.cipher.update(data);
489
+ return result.toString("base64");
490
+ };
491
+ // Stateful cipheriv final — finalizes session, returns last block + authTag for GCM.
492
+ // Removes session from map.
493
+ handlers[K.cryptoCipherivFinal] = (sessionId) => {
494
+ const id = Number(sessionId);
495
+ const session = cipherSessions.get(id);
496
+ if (!session)
497
+ throw new Error(`Cipher session ${id} not found`);
498
+ cipherSessions.delete(id);
499
+ const final = session.cipher.final();
500
+ const isAead = /-(gcm|ccm)$/i.test(session.algorithm);
501
+ if (isAead) {
502
+ const authTag = session.cipher.getAuthTag?.();
503
+ return JSON.stringify({
504
+ data: final.toString("base64"),
505
+ authTag: authTag ? authTag.toString("base64") : undefined,
506
+ });
507
+ }
508
+ return JSON.stringify({ data: final.toString("base64") });
509
+ };
510
+ // sign — host signs data with a PEM private key.
511
+ handlers[K.cryptoSign] = (algorithm, dataBase64, keyJson) => {
512
+ const data = Buffer.from(String(dataBase64), "base64");
513
+ const key = deserializeBridgeValue(JSON.parse(String(keyJson)));
514
+ const signature = sign(normalizeBridgeAlgorithm(algorithm), data, key);
515
+ return signature.toString("base64");
516
+ };
517
+ // verify — host verifies signature with a PEM public key.
518
+ handlers[K.cryptoVerify] = (algorithm, dataBase64, keyJson, signatureBase64) => {
519
+ const data = Buffer.from(String(dataBase64), "base64");
520
+ const key = deserializeBridgeValue(JSON.parse(String(keyJson)));
521
+ const signature = Buffer.from(String(signatureBase64), "base64");
522
+ return verify(normalizeBridgeAlgorithm(algorithm), data, key, signature);
523
+ };
524
+ // Asymmetric encrypt/decrypt — use real Node crypto so DER inputs, encrypted
525
+ // PEM options bags, and sandbox KeyObject handles all follow host semantics.
526
+ handlers[K.cryptoAsymmetricOp] = (operation, keyJson, dataBase64) => {
527
+ const key = deserializeBridgeValue(JSON.parse(String(keyJson)));
528
+ const data = Buffer.from(String(dataBase64), "base64");
529
+ switch (String(operation)) {
530
+ case "publicEncrypt":
531
+ return publicEncrypt(key, data).toString("base64");
532
+ case "privateDecrypt":
533
+ return privateDecrypt(key, data).toString("base64");
534
+ case "privateEncrypt":
535
+ return privateEncrypt(key, data).toString("base64");
536
+ case "publicDecrypt":
537
+ return publicDecrypt(key, data).toString("base64");
538
+ default:
539
+ throw new Error(`Unsupported asymmetric crypto operation: ${String(operation)}`);
540
+ }
541
+ };
542
+ // createPublicKey/createPrivateKey — import through host crypto so metadata
543
+ // like asymmetricKeyType/asymmetricKeyDetails survives reconstruction.
544
+ handlers[K.cryptoCreateKeyObject] = (operation, keyJson) => {
545
+ const key = deserializeBridgeValue(JSON.parse(String(keyJson)));
546
+ switch (String(operation)) {
547
+ case "createPrivateKey":
548
+ return JSON.stringify(serializeAnyKeyObject(createPrivateKey(key)));
549
+ case "createPublicKey":
550
+ return JSON.stringify(serializeAnyKeyObject(createPublicKey(key)));
551
+ default:
552
+ throw new Error(`Unsupported key creation operation: ${String(operation)}`);
553
+ }
554
+ };
555
+ // generateKeyPairSync — host generates key pair, preserving requested encodings.
556
+ // For KeyObject output, serialize PEM + metadata so the isolate can recreate a
557
+ // Node-compatible KeyObject surface.
558
+ handlers[K.cryptoGenerateKeyPairSync] = (type, optionsJson) => {
559
+ const options = parseSerializedOptions(optionsJson);
560
+ const encodingOptions = options;
561
+ const hasExplicitEncoding = encodingOptions &&
562
+ (encodingOptions.publicKeyEncoding || encodingOptions.privateKeyEncoding);
563
+ const { publicKey, privateKey } = generateKeyPairSync(type, options);
564
+ if (hasExplicitEncoding) {
565
+ return JSON.stringify({
566
+ publicKey: serializeKeyValue(publicKey),
567
+ privateKey: serializeKeyValue(privateKey),
568
+ });
569
+ }
570
+ return JSON.stringify({
571
+ publicKey: serializeSandboxKeyObject(publicKey),
572
+ privateKey: serializeSandboxKeyObject(privateKey),
573
+ });
574
+ };
575
+ // generateKeySync — host generates symmetric KeyObject values with native
576
+ // validation so length/error semantics match Node.
577
+ handlers[K.cryptoGenerateKeySync] = (type, optionsJson) => {
578
+ const options = parseSerializedOptions(optionsJson);
579
+ return JSON.stringify(serializeAnyKeyObject(generateKeySync(type, options)));
580
+ };
581
+ // generatePrimeSync — host generates prime material so bigint/add/rem options
582
+ // follow Node semantics instead of polyfill approximations.
583
+ handlers[K.cryptoGeneratePrimeSync] = (size, optionsJson) => {
584
+ const options = parseSerializedOptions(optionsJson);
585
+ const prime = options === undefined
586
+ ? generatePrimeSync(size)
587
+ : generatePrimeSync(size, options);
588
+ return JSON.stringify(serializeBridgeValue(prime));
589
+ };
590
+ // Diffie-Hellman/ECDH — keep native host objects alive by session id so
591
+ // sandbox calls preserve Node's return values, validation, and stateful key material.
592
+ handlers[K.cryptoDiffieHellman] = (optionsJson) => {
593
+ const options = deserializeBridgeValue(JSON.parse(String(optionsJson)));
594
+ return JSON.stringify(serializeBridgeValue(diffieHellman(options)));
595
+ };
596
+ handlers[K.cryptoDiffieHellmanGroup] = (name) => {
597
+ const group = getDiffieHellman(String(name));
598
+ return JSON.stringify({
599
+ prime: serializeBridgeValue(group.getPrime()),
600
+ generator: serializeBridgeValue(group.getGenerator()),
601
+ });
602
+ };
603
+ handlers[K.cryptoDiffieHellmanSessionCreate] = (requestJson) => {
604
+ const request = JSON.parse(String(requestJson));
605
+ const args = (request.args ?? []).map((value) => deserializeBridgeValue(value));
606
+ let session;
607
+ switch (request.type) {
608
+ case "dh":
609
+ session = createDiffieHellman(...args);
610
+ break;
611
+ case "group":
612
+ session = getDiffieHellman(String(request.name));
613
+ break;
614
+ case "ecdh":
615
+ session = createECDH(String(request.name));
616
+ break;
617
+ default:
618
+ throw new Error(`Unsupported Diffie-Hellman session type: ${String(request.type)}`);
619
+ }
620
+ const sessionId = nextDiffieHellmanSessionId++;
621
+ diffieHellmanSessions.set(sessionId, session);
622
+ return sessionId;
623
+ };
624
+ handlers[K.cryptoDiffieHellmanSessionCall] = (sessionId, requestJson) => {
625
+ const session = diffieHellmanSessions.get(Number(sessionId));
626
+ if (!session) {
627
+ throw new Error(`Diffie-Hellman session ${String(sessionId)} not found`);
628
+ }
629
+ const request = JSON.parse(String(requestJson));
630
+ const args = (request.args ?? []).map((value) => deserializeBridgeValue(value));
631
+ const sessionRecord = session;
632
+ if (request.method === "verifyError") {
633
+ return JSON.stringify({
634
+ result: typeof sessionRecord.verifyError === "number" ? sessionRecord.verifyError : undefined,
635
+ hasResult: typeof sessionRecord.verifyError === "number",
636
+ });
637
+ }
638
+ const method = sessionRecord[request.method];
639
+ if (typeof method !== "function") {
640
+ throw new Error(`Unsupported Diffie-Hellman method: ${request.method}`);
641
+ }
642
+ const result = method.apply(session, args);
643
+ return JSON.stringify({
644
+ result: result === undefined ? null : serializeBridgeValue(result),
645
+ hasResult: result !== undefined,
646
+ });
647
+ };
648
+ // crypto.subtle — single dispatcher for all Web Crypto API operations.
649
+ // Guest-side SandboxSubtle serializes each call as JSON { op, ... }.
650
+ handlers[K.cryptoSubtle] = (opJson) => {
651
+ const req = JSON.parse(String(opJson));
652
+ const normalizeHash = (h) => {
653
+ const n = typeof h === "string" ? h : h.name;
654
+ return n.toLowerCase().replace("-", "");
655
+ };
656
+ switch (req.op) {
657
+ case "digest": {
658
+ const algo = normalizeHash(req.algorithm);
659
+ const data = Buffer.from(req.data, "base64");
660
+ return JSON.stringify({
661
+ data: createHash(algo).update(data).digest("base64"),
662
+ });
663
+ }
664
+ case "generateKey": {
665
+ const algoName = req.algorithm.name;
666
+ if (algoName === "AES-GCM" ||
667
+ algoName === "AES-CBC" ||
668
+ algoName === "AES-CTR" ||
669
+ algoName === "AES-KW") {
670
+ const keyBytes = Buffer.allocUnsafe(req.algorithm.length / 8);
671
+ randomFillSync(keyBytes);
672
+ return JSON.stringify({
673
+ key: serializeCryptoKeyDataFromKeyObject(createSecretKey(keyBytes), "secret", req.algorithm, req.extractable, req.usages),
674
+ });
675
+ }
676
+ if (algoName === "HMAC") {
677
+ const hashName = typeof req.algorithm.hash === "string"
678
+ ? req.algorithm.hash
679
+ : req.algorithm.hash.name;
680
+ const len = normalizeHmacLength(hashName, req.algorithm.length) / 8;
681
+ const keyBytes = Buffer.allocUnsafe(len);
682
+ randomFillSync(keyBytes);
683
+ return JSON.stringify({
684
+ key: serializeCryptoKeyDataFromKeyObject(createSecretKey(keyBytes), "secret", {
685
+ ...req.algorithm,
686
+ hash: { name: hashName },
687
+ length: len * 8,
688
+ }, req.extractable, req.usages),
689
+ });
690
+ }
691
+ if (algoName === "RSASSA-PKCS1-v1_5" ||
692
+ algoName === "RSA-OAEP" ||
693
+ algoName === "RSA-PSS") {
694
+ let publicExponent = 65537;
695
+ if (req.algorithm.publicExponent) {
696
+ const expBytes = Buffer.from(req.algorithm.publicExponent, "base64");
697
+ publicExponent = 0;
698
+ for (const b of expBytes) {
699
+ publicExponent = (publicExponent << 8) | b;
700
+ }
701
+ }
702
+ const { publicKey, privateKey } = generateKeyPairSync("rsa", {
703
+ modulusLength: req.algorithm.modulusLength || 2048,
704
+ publicExponent,
705
+ publicKeyEncoding: {
706
+ type: "spki",
707
+ format: "pem",
708
+ },
709
+ privateKeyEncoding: {
710
+ type: "pkcs8",
711
+ format: "pem",
712
+ },
713
+ });
714
+ const publicKeyObject = createPublicKey(publicKey);
715
+ const privateKeyObject = createPrivateKey(privateKey);
716
+ return JSON.stringify({
717
+ publicKey: serializeCryptoKeyDataFromKeyObject(publicKeyObject, "public", req.algorithm, req.extractable, req.usages.filter((u) => ["verify", "encrypt", "wrapKey"].includes(u))),
718
+ privateKey: serializeCryptoKeyDataFromKeyObject(privateKeyObject, "private", req.algorithm, req.extractable, req.usages.filter((u) => ["sign", "decrypt", "unwrapKey"].includes(u))),
719
+ });
720
+ }
721
+ if (algoName === "ECDSA" || algoName === "ECDH") {
722
+ const { publicKey, privateKey } = generateKeyPairSync("ec", {
723
+ namedCurve: String(req.algorithm.namedCurve),
724
+ publicKeyEncoding: { type: "spki", format: "pem" },
725
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
726
+ });
727
+ return JSON.stringify({
728
+ publicKey: serializeCryptoKeyDataFromKeyObject(createPublicKey(publicKey), "public", { ...req.algorithm, name: algoName }, req.extractable, req.usages.filter((u) => algoName === "ECDSA"
729
+ ? ["verify"].includes(u)
730
+ : ["deriveBits", "deriveKey"].includes(u))),
731
+ privateKey: serializeCryptoKeyDataFromKeyObject(createPrivateKey(privateKey), "private", { ...req.algorithm, name: algoName }, req.extractable, req.usages.filter((u) => algoName === "ECDSA"
732
+ ? ["sign"].includes(u)
733
+ : ["deriveBits", "deriveKey"].includes(u))),
734
+ });
735
+ }
736
+ if (["Ed25519", "Ed448", "X25519", "X448"].includes(algoName)) {
737
+ const keyPair = algoName === "Ed25519"
738
+ ? generateKeyPairSync("ed25519")
739
+ : algoName === "Ed448"
740
+ ? generateKeyPairSync("ed448")
741
+ : algoName === "X25519"
742
+ ? generateKeyPairSync("x25519")
743
+ : generateKeyPairSync("x448");
744
+ const { publicKey, privateKey } = keyPair;
745
+ return JSON.stringify({
746
+ publicKey: serializeCryptoKeyDataFromKeyObject(publicKey, "public", { name: algoName }, req.extractable, req.usages.filter((u) => algoName.startsWith("Ed")
747
+ ? ["verify"].includes(u)
748
+ : ["deriveBits", "deriveKey"].includes(u))),
749
+ privateKey: serializeCryptoKeyDataFromKeyObject(privateKey, "private", { name: algoName }, req.extractable, req.usages.filter((u) => algoName.startsWith("Ed")
750
+ ? ["sign"].includes(u)
751
+ : ["deriveBits", "deriveKey"].includes(u))),
752
+ });
753
+ }
754
+ throw new Error(`Unsupported key algorithm: ${algoName}`);
755
+ }
756
+ case "importKey": {
757
+ const { format, keyData, algorithm, extractable, usages } = req;
758
+ if (format === "raw") {
759
+ return JSON.stringify({
760
+ key: serializeCryptoKeyDataFromKeyObject(createSecretKey(Buffer.from(keyData, "base64")), "secret", algorithm.name === "HMAC" && !algorithm.length
761
+ ? {
762
+ ...algorithm,
763
+ hash: typeof algorithm.hash === "string"
764
+ ? { name: algorithm.hash }
765
+ : algorithm.hash,
766
+ length: Buffer.from(keyData, "base64").byteLength * 8,
767
+ }
768
+ : algorithm, extractable, usages),
769
+ });
770
+ }
771
+ if (format === "jwk") {
772
+ const jwk = typeof keyData === "string" ? JSON.parse(keyData) : keyData;
773
+ if (jwk.kty === "oct") {
774
+ const raw = Buffer.from(jwk.k, "base64url");
775
+ return JSON.stringify({
776
+ key: serializeCryptoKeyDataFromKeyObject(createSecretKey(raw), "secret", algorithm, extractable, usages),
777
+ });
778
+ }
779
+ if (jwk.d) {
780
+ const keyObj = createPrivateKey({ key: jwk, format: "jwk" });
781
+ const pem = keyObj.export({
782
+ type: "pkcs8",
783
+ format: "pem",
784
+ });
785
+ return JSON.stringify({
786
+ key: serializeCryptoKeyDataFromKeyObject(createPrivateKey(pem), "private", algorithm, extractable, usages),
787
+ });
788
+ }
789
+ const keyObj = createPublicKey({ key: jwk, format: "jwk" });
790
+ const pem = keyObj.export({ type: "spki", format: "pem" });
791
+ return JSON.stringify({
792
+ key: serializeCryptoKeyDataFromKeyObject(createPublicKey(pem), "public", algorithm, extractable, usages),
793
+ });
794
+ }
795
+ if (format === "pkcs8") {
796
+ const keyBuf = Buffer.from(keyData, "base64");
797
+ const keyObj = createPrivateKey({
798
+ key: keyBuf,
799
+ format: "der",
800
+ type: "pkcs8",
801
+ });
802
+ const pem = keyObj.export({
803
+ type: "pkcs8",
804
+ format: "pem",
805
+ });
806
+ return JSON.stringify({
807
+ key: serializeCryptoKeyDataFromKeyObject(createPrivateKey(pem), "private", algorithm, extractable, usages),
808
+ });
809
+ }
810
+ if (format === "spki") {
811
+ const keyBuf = Buffer.from(keyData, "base64");
812
+ const keyObj = createPublicKey({
813
+ key: keyBuf,
814
+ format: "der",
815
+ type: "spki",
816
+ });
817
+ const pem = keyObj.export({ type: "spki", format: "pem" });
818
+ return JSON.stringify({
819
+ key: serializeCryptoKeyDataFromKeyObject(createPublicKey(pem), "public", algorithm, extractable, usages),
820
+ });
821
+ }
822
+ throw new Error(`Unsupported import format: ${format}`);
823
+ }
824
+ case "exportKey": {
825
+ const { format, key } = req;
826
+ if (format === "raw") {
827
+ if (!key._raw)
828
+ throw new Error("Cannot export asymmetric key as raw");
829
+ return JSON.stringify({
830
+ data: key._raw,
831
+ });
832
+ }
833
+ if (format === "jwk") {
834
+ if (key._raw) {
835
+ const raw = Buffer.from(key._raw, "base64");
836
+ return JSON.stringify({
837
+ jwk: {
838
+ kty: "oct",
839
+ k: raw.toString("base64url"),
840
+ ext: key.extractable,
841
+ key_ops: key.usages,
842
+ },
843
+ });
844
+ }
845
+ const keyObj = key.type === "private"
846
+ ? createPrivateKey(key._pem)
847
+ : createPublicKey(key._pem);
848
+ return JSON.stringify({
849
+ jwk: keyObj.export({ format: "jwk" }),
850
+ });
851
+ }
852
+ if (format === "pkcs8") {
853
+ if (key.type !== "private")
854
+ throw new Error("Cannot export non-private key as pkcs8");
855
+ const keyObj = createPrivateKey(key._pem);
856
+ const der = keyObj.export({
857
+ type: "pkcs8",
858
+ format: "der",
859
+ });
860
+ return JSON.stringify({ data: der.toString("base64") });
861
+ }
862
+ if (format === "spki") {
863
+ const keyObj = key.type === "private"
864
+ ? createPublicKey(createPrivateKey(key._pem))
865
+ : createPublicKey(key._pem);
866
+ const der = keyObj.export({
867
+ type: "spki",
868
+ format: "der",
869
+ });
870
+ return JSON.stringify({ data: der.toString("base64") });
871
+ }
872
+ throw new Error(`Unsupported export format: ${format}`);
873
+ }
874
+ case "encrypt": {
875
+ const { algorithm, key, data } = req;
876
+ const rawKey = Buffer.from(key._raw, "base64");
877
+ const plaintext = Buffer.from(data, "base64");
878
+ const algoName = algorithm.name;
879
+ if (algoName === "AES-GCM") {
880
+ const iv = Buffer.from(algorithm.iv, "base64");
881
+ const tagLength = (algorithm.tagLength || 128) / 8;
882
+ const cipher = createCipheriv(`aes-${rawKey.length * 8}-gcm`, rawKey, iv, { authTagLength: tagLength });
883
+ if (algorithm.additionalData) {
884
+ cipher.setAAD(Buffer.from(algorithm.additionalData, "base64"));
885
+ }
886
+ const encrypted = Buffer.concat([
887
+ cipher.update(plaintext),
888
+ cipher.final(),
889
+ ]);
890
+ const authTag = cipher.getAuthTag();
891
+ return JSON.stringify({
892
+ data: Buffer.concat([encrypted, authTag]).toString("base64"),
893
+ });
894
+ }
895
+ if (algoName === "AES-CBC") {
896
+ const iv = Buffer.from(algorithm.iv, "base64");
897
+ const cipher = createCipheriv(`aes-${rawKey.length * 8}-cbc`, rawKey, iv);
898
+ const encrypted = Buffer.concat([
899
+ cipher.update(plaintext),
900
+ cipher.final(),
901
+ ]);
902
+ return JSON.stringify({ data: encrypted.toString("base64") });
903
+ }
904
+ throw new Error(`Unsupported encrypt algorithm: ${algoName}`);
905
+ }
906
+ case "decrypt": {
907
+ const { algorithm, key, data } = req;
908
+ const rawKey = Buffer.from(key._raw, "base64");
909
+ const ciphertext = Buffer.from(data, "base64");
910
+ const algoName = algorithm.name;
911
+ if (algoName === "AES-GCM") {
912
+ const iv = Buffer.from(algorithm.iv, "base64");
913
+ const tagLength = (algorithm.tagLength || 128) / 8;
914
+ const encData = ciphertext.subarray(0, ciphertext.length - tagLength);
915
+ const authTag = ciphertext.subarray(ciphertext.length - tagLength);
916
+ const decipher = createDecipheriv(`aes-${rawKey.length * 8}-gcm`, rawKey, iv, { authTagLength: tagLength });
917
+ decipher.setAuthTag(authTag);
918
+ if (algorithm.additionalData) {
919
+ decipher.setAAD(Buffer.from(algorithm.additionalData, "base64"));
920
+ }
921
+ const decrypted = Buffer.concat([
922
+ decipher.update(encData),
923
+ decipher.final(),
924
+ ]);
925
+ return JSON.stringify({ data: decrypted.toString("base64") });
926
+ }
927
+ if (algoName === "AES-CBC") {
928
+ const iv = Buffer.from(algorithm.iv, "base64");
929
+ const decipher = createDecipheriv(`aes-${rawKey.length * 8}-cbc`, rawKey, iv);
930
+ const decrypted = Buffer.concat([
931
+ decipher.update(ciphertext),
932
+ decipher.final(),
933
+ ]);
934
+ return JSON.stringify({ data: decrypted.toString("base64") });
935
+ }
936
+ throw new Error(`Unsupported decrypt algorithm: ${algoName}`);
937
+ }
938
+ case "sign": {
939
+ const { key, data, algorithm } = req;
940
+ const dataBytes = Buffer.from(data, "base64");
941
+ const algoName = key.algorithm.name;
942
+ if (algoName === "HMAC") {
943
+ const rawKey = Buffer.from(key._raw, "base64");
944
+ const hashAlgo = normalizeHash(algorithm.hash ?? key.algorithm.hash);
945
+ return JSON.stringify({
946
+ data: createHmac(hashAlgo, rawKey)
947
+ .update(dataBytes)
948
+ .digest("base64"),
949
+ });
950
+ }
951
+ if (algoName === "RSASSA-PKCS1-v1_5") {
952
+ const hashAlgo = normalizeHash(key.algorithm.hash);
953
+ const pkey = createPrivateKey(key._pem);
954
+ return JSON.stringify({
955
+ data: sign(hashAlgo, dataBytes, pkey).toString("base64"),
956
+ });
957
+ }
958
+ if (algoName === "RSA-PSS") {
959
+ const hashAlgo = normalizeHash(key.algorithm.hash);
960
+ return JSON.stringify({
961
+ data: sign(hashAlgo, dataBytes, {
962
+ key: createPrivateKey(key._pem),
963
+ padding: cryptoConstants.RSA_PKCS1_PSS_PADDING,
964
+ saltLength: algorithm.saltLength,
965
+ }).toString("base64"),
966
+ });
967
+ }
968
+ if (algoName === "ECDSA") {
969
+ const hashAlgo = normalizeHash(algorithm.hash ?? key.algorithm.hash);
970
+ return JSON.stringify({
971
+ data: sign(hashAlgo, dataBytes, createPrivateKey(key._pem)).toString("base64"),
972
+ });
973
+ }
974
+ if (algoName === "Ed25519" || algoName === "Ed448") {
975
+ if (algoName === "Ed448" &&
976
+ algorithm.context &&
977
+ Buffer.from(algorithm.context, "base64").byteLength > 0) {
978
+ throw new Error("Non zero-length context is not yet supported");
979
+ }
980
+ return JSON.stringify({
981
+ data: sign(null, dataBytes, createPrivateKey(key._pem)).toString("base64"),
982
+ });
983
+ }
984
+ throw new Error(`Unsupported sign algorithm: ${algoName}`);
985
+ }
986
+ case "verify": {
987
+ const { key, signature, data, algorithm } = req;
988
+ const dataBytes = Buffer.from(data, "base64");
989
+ const sigBytes = Buffer.from(signature, "base64");
990
+ const algoName = key.algorithm.name;
991
+ if (algoName === "HMAC") {
992
+ const rawKey = Buffer.from(key._raw, "base64");
993
+ const hashAlgo = normalizeHash(algorithm.hash ?? key.algorithm.hash);
994
+ const expected = createHmac(hashAlgo, rawKey)
995
+ .update(dataBytes)
996
+ .digest();
997
+ if (expected.length !== sigBytes.length)
998
+ return JSON.stringify({ result: false });
999
+ return JSON.stringify({
1000
+ result: timingSafeEqual(expected, sigBytes),
1001
+ });
1002
+ }
1003
+ if (algoName === "RSASSA-PKCS1-v1_5") {
1004
+ const hashAlgo = normalizeHash(key.algorithm.hash);
1005
+ const pkey = createPublicKey(key._pem);
1006
+ return JSON.stringify({
1007
+ result: verify(hashAlgo, dataBytes, pkey, sigBytes),
1008
+ });
1009
+ }
1010
+ if (algoName === "RSA-PSS") {
1011
+ const hashAlgo = normalizeHash(key.algorithm.hash);
1012
+ return JSON.stringify({
1013
+ result: verify(hashAlgo, dataBytes, {
1014
+ key: createPublicKey(key._pem),
1015
+ padding: cryptoConstants.RSA_PKCS1_PSS_PADDING,
1016
+ saltLength: algorithm.saltLength,
1017
+ }, sigBytes),
1018
+ });
1019
+ }
1020
+ if (algoName === "ECDSA") {
1021
+ const hashAlgo = normalizeHash(algorithm.hash ?? key.algorithm.hash);
1022
+ return JSON.stringify({
1023
+ result: verify(hashAlgo, dataBytes, createPublicKey(key._pem), sigBytes),
1024
+ });
1025
+ }
1026
+ if (algoName === "Ed25519" || algoName === "Ed448") {
1027
+ if (algoName === "Ed448" &&
1028
+ algorithm.context &&
1029
+ Buffer.from(algorithm.context, "base64").byteLength > 0) {
1030
+ throw new Error("Non zero-length context is not yet supported");
1031
+ }
1032
+ return JSON.stringify({
1033
+ result: verify(null, dataBytes, createPublicKey(key._pem), sigBytes),
1034
+ });
1035
+ }
1036
+ throw new Error(`Unsupported verify algorithm: ${algoName}`);
1037
+ }
1038
+ case "deriveBits": {
1039
+ const { algorithm, baseKey, length } = req;
1040
+ const algoName = algorithm.name;
1041
+ if (algoName === "PBKDF2") {
1042
+ const bitLength = Number(length);
1043
+ const byteLength = bitLength / 8;
1044
+ const password = Buffer.from(baseKey._raw, "base64");
1045
+ const salt = Buffer.from(algorithm.salt, "base64");
1046
+ const hash = normalizeHash(algorithm.hash);
1047
+ const derived = pbkdf2Sync(password, salt, algorithm.iterations, byteLength, hash);
1048
+ return JSON.stringify({ data: derived.toString("base64") });
1049
+ }
1050
+ if (algoName === "HKDF") {
1051
+ const bitLength = Number(length);
1052
+ const byteLength = bitLength / 8;
1053
+ const ikm = Buffer.from(baseKey._raw, "base64");
1054
+ const salt = Buffer.from(algorithm.salt, "base64");
1055
+ const info = Buffer.from(algorithm.info, "base64");
1056
+ const hash = normalizeHash(algorithm.hash);
1057
+ const derived = Buffer.from(hkdfSync(hash, ikm, salt, info, byteLength));
1058
+ return JSON.stringify({ data: derived.toString("base64") });
1059
+ }
1060
+ if (algoName === "ECDH" || algoName === "X25519" || algoName === "X448") {
1061
+ const secret = diffieHellman({
1062
+ privateKey: deserializeCryptoKeyObject(baseKey),
1063
+ publicKey: deserializeCryptoKeyObject(algorithm.public),
1064
+ });
1065
+ return JSON.stringify({
1066
+ data: sliceDerivedBits(secret, length).toString("base64"),
1067
+ });
1068
+ }
1069
+ throw new Error(`Unsupported deriveBits algorithm: ${algoName}`);
1070
+ }
1071
+ case "deriveKey": {
1072
+ const { algorithm, baseKey, derivedKeyAlgorithm, extractable, usages } = req;
1073
+ const algoName = algorithm.name;
1074
+ if (algoName === "PBKDF2") {
1075
+ const keyLengthBits = resolveDerivedKeyLengthBits(derivedKeyAlgorithm, Buffer.from(baseKey._raw, "base64").byteLength * 8);
1076
+ const byteLength = keyLengthBits / 8;
1077
+ const password = Buffer.from(baseKey._raw, "base64");
1078
+ const salt = Buffer.from(algorithm.salt, "base64");
1079
+ const hash = normalizeHash(algorithm.hash);
1080
+ const derived = pbkdf2Sync(password, salt, algorithm.iterations, byteLength, hash);
1081
+ return JSON.stringify({ key: deriveSecretKeyData(derivedKeyAlgorithm, extractable, usages, derived) });
1082
+ }
1083
+ if (algoName === "HKDF") {
1084
+ const keyLengthBits = resolveDerivedKeyLengthBits(derivedKeyAlgorithm, Buffer.from(baseKey._raw, "base64").byteLength * 8);
1085
+ const byteLength = keyLengthBits / 8;
1086
+ const ikm = Buffer.from(baseKey._raw, "base64");
1087
+ const salt = Buffer.from(algorithm.salt, "base64");
1088
+ const info = Buffer.from(algorithm.info, "base64");
1089
+ const hash = normalizeHash(algorithm.hash);
1090
+ const derived = Buffer.from(hkdfSync(hash, ikm, salt, info, byteLength));
1091
+ return JSON.stringify({ key: deriveSecretKeyData(derivedKeyAlgorithm, extractable, usages, derived) });
1092
+ }
1093
+ if (algoName === "ECDH" || algoName === "X25519" || algoName === "X448") {
1094
+ const secret = diffieHellman({
1095
+ privateKey: deserializeCryptoKeyObject(baseKey),
1096
+ publicKey: deserializeCryptoKeyObject(algorithm.public),
1097
+ });
1098
+ return JSON.stringify({
1099
+ key: deriveSecretKeyData(derivedKeyAlgorithm, extractable, usages, secret),
1100
+ });
1101
+ }
1102
+ throw new Error(`Unsupported deriveKey algorithm: ${algoName}`);
1103
+ }
1104
+ case "wrapKey": {
1105
+ const { format, key, wrappingKey, wrapAlgorithm } = req;
1106
+ const exported = JSON.parse(handlers[K.cryptoSubtle](JSON.stringify({
1107
+ op: "exportKey",
1108
+ format,
1109
+ key,
1110
+ })));
1111
+ const keyData = format === "jwk"
1112
+ ? Buffer.from(JSON.stringify(exported.jwk), "utf8")
1113
+ : decodeBridgeBuffer(exported.data);
1114
+ if (wrapAlgorithm.name === "AES-KW") {
1115
+ const wrappingBytes = decodeBridgeBuffer(wrappingKey._raw);
1116
+ const cipherName = `id-aes${wrappingBytes.byteLength * 8}-wrap`;
1117
+ const cipher = createCipheriv(cipherName, wrappingBytes, Buffer.alloc(8, 0xa6));
1118
+ return JSON.stringify({
1119
+ data: Buffer.concat([cipher.update(keyData), cipher.final()]).toString("base64"),
1120
+ });
1121
+ }
1122
+ if (wrapAlgorithm.name === "RSA-OAEP") {
1123
+ return JSON.stringify({
1124
+ data: publicEncrypt({
1125
+ key: createPublicKey(wrappingKey._pem),
1126
+ oaepHash: normalizeHash(wrappingKey.algorithm.hash),
1127
+ oaepLabel: wrapAlgorithm.label
1128
+ ? decodeBridgeBuffer(wrapAlgorithm.label)
1129
+ : undefined,
1130
+ }, keyData).toString("base64"),
1131
+ });
1132
+ }
1133
+ if (wrapAlgorithm.name === "AES-CTR" ||
1134
+ wrapAlgorithm.name === "AES-CBC" ||
1135
+ wrapAlgorithm.name === "AES-GCM") {
1136
+ const wrappingBytes = decodeBridgeBuffer(wrappingKey._raw);
1137
+ const algorithmName = wrapAlgorithm.name === "AES-CTR"
1138
+ ? `aes-${wrappingBytes.byteLength * 8}-ctr`
1139
+ : wrapAlgorithm.name === "AES-CBC"
1140
+ ? `aes-${wrappingBytes.byteLength * 8}-cbc`
1141
+ : `aes-${wrappingBytes.byteLength * 8}-gcm`;
1142
+ const iv = wrapAlgorithm.name === "AES-CTR"
1143
+ ? decodeBridgeBuffer(wrapAlgorithm.counter)
1144
+ : decodeBridgeBuffer(wrapAlgorithm.iv);
1145
+ const cipher = createCipheriv(algorithmName, wrappingBytes, iv, wrapAlgorithm.name === "AES-GCM"
1146
+ ? { authTagLength: (wrapAlgorithm.tagLength || 128) / 8 }
1147
+ : undefined);
1148
+ if (wrapAlgorithm.name === "AES-GCM" && wrapAlgorithm.additionalData) {
1149
+ cipher.setAAD?.(decodeBridgeBuffer(wrapAlgorithm.additionalData));
1150
+ }
1151
+ const encrypted = Buffer.concat([cipher.update(keyData), cipher.final()]);
1152
+ const payload = wrapAlgorithm.name === "AES-GCM"
1153
+ ? Buffer.concat([encrypted, cipher.getAuthTag?.() ?? Buffer.alloc(0)])
1154
+ : encrypted;
1155
+ return JSON.stringify({ data: payload.toString("base64") });
1156
+ }
1157
+ throw new Error(`Unsupported wrap algorithm: ${wrapAlgorithm.name}`);
1158
+ }
1159
+ case "unwrapKey": {
1160
+ const { format, wrappedKey, unwrappingKey, unwrapAlgorithm, unwrappedKeyAlgorithm, extractable, usages, } = req;
1161
+ let unwrapped;
1162
+ if (unwrapAlgorithm.name === "AES-KW") {
1163
+ const unwrappingBytes = decodeBridgeBuffer(unwrappingKey._raw);
1164
+ const cipherName = `id-aes${unwrappingBytes.byteLength * 8}-wrap`;
1165
+ const decipher = createDecipheriv(cipherName, unwrappingBytes, Buffer.alloc(8, 0xa6));
1166
+ unwrapped = Buffer.concat([
1167
+ decipher.update(decodeBridgeBuffer(wrappedKey)),
1168
+ decipher.final(),
1169
+ ]);
1170
+ }
1171
+ else if (unwrapAlgorithm.name === "RSA-OAEP") {
1172
+ unwrapped = privateDecrypt({
1173
+ key: createPrivateKey(unwrappingKey._pem),
1174
+ oaepHash: normalizeHash(unwrappingKey.algorithm.hash),
1175
+ oaepLabel: unwrapAlgorithm.label
1176
+ ? decodeBridgeBuffer(unwrapAlgorithm.label)
1177
+ : undefined,
1178
+ }, decodeBridgeBuffer(wrappedKey));
1179
+ }
1180
+ else if (unwrapAlgorithm.name === "AES-CTR" ||
1181
+ unwrapAlgorithm.name === "AES-CBC" ||
1182
+ unwrapAlgorithm.name === "AES-GCM") {
1183
+ const unwrappingBytes = decodeBridgeBuffer(unwrappingKey._raw);
1184
+ const algorithmName = unwrapAlgorithm.name === "AES-CTR"
1185
+ ? `aes-${unwrappingBytes.byteLength * 8}-ctr`
1186
+ : unwrapAlgorithm.name === "AES-CBC"
1187
+ ? `aes-${unwrappingBytes.byteLength * 8}-cbc`
1188
+ : `aes-${unwrappingBytes.byteLength * 8}-gcm`;
1189
+ const iv = unwrapAlgorithm.name === "AES-CTR"
1190
+ ? decodeBridgeBuffer(unwrapAlgorithm.counter)
1191
+ : decodeBridgeBuffer(unwrapAlgorithm.iv);
1192
+ const wrappedBytes = decodeBridgeBuffer(wrappedKey);
1193
+ const decipher = createDecipheriv(algorithmName, unwrappingBytes, iv, unwrapAlgorithm.name === "AES-GCM"
1194
+ ? { authTagLength: (unwrapAlgorithm.tagLength || 128) / 8 }
1195
+ : undefined);
1196
+ let ciphertext = wrappedBytes;
1197
+ if (unwrapAlgorithm.name === "AES-GCM") {
1198
+ const tagLength = (unwrapAlgorithm.tagLength || 128) / 8;
1199
+ ciphertext = wrappedBytes.subarray(0, wrappedBytes.byteLength - tagLength);
1200
+ decipher.setAuthTag?.(wrappedBytes.subarray(wrappedBytes.byteLength - tagLength));
1201
+ if (unwrapAlgorithm.additionalData) {
1202
+ decipher.setAAD?.(decodeBridgeBuffer(unwrapAlgorithm.additionalData));
1203
+ }
1204
+ }
1205
+ unwrapped = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
1206
+ }
1207
+ else {
1208
+ throw new Error(`Unsupported unwrap algorithm: ${unwrapAlgorithm.name}`);
1209
+ }
1210
+ return handlers[K.cryptoSubtle](JSON.stringify({
1211
+ op: "importKey",
1212
+ format,
1213
+ keyData: format === "jwk"
1214
+ ? JSON.parse(unwrapped.toString("utf8"))
1215
+ : unwrapped.toString("base64"),
1216
+ algorithm: unwrappedKeyAlgorithm,
1217
+ extractable,
1218
+ usages,
1219
+ }));
1220
+ }
1221
+ default:
1222
+ throw new Error(`Unsupported subtle operation: ${req.op}`);
1223
+ }
1224
+ };
1225
+ const dispose = () => {
1226
+ cipherSessions.clear();
1227
+ diffieHellmanSessions.clear();
1228
+ };
1229
+ return { handlers, dispose };
1230
+ }
1231
+ /**
1232
+ * Build net socket bridge handlers.
1233
+ *
1234
+ * All TCP operations route through kernel sockets (loopback or external via
1235
+ * the host adapter).
1236
+ * Call dispose() when the execution ends to destroy all open sockets.
1237
+ */
1238
+ export function buildNetworkSocketBridgeHandlers(deps) {
1239
+ const { socketTable, pid } = deps;
1240
+ if (!socketTable || pid === undefined) {
1241
+ throw new Error("buildNetworkSocketBridgeHandlers requires a kernel socketTable and pid");
1242
+ }
1243
+ return buildKernelSocketBridgeHandlers(deps.dispatch, socketTable, pid);
1244
+ }
1245
+ /**
1246
+ * Build bridge handlers that route net socket operations through the
1247
+ * kernel SocketTable. Data flows through kernel send/recv, connections
1248
+ * route through loopback (paired sockets) or external (host adapter).
1249
+ */
1250
+ function buildKernelSocketBridgeHandlers(dispatch, socketTable, pid) {
1251
+ const handlers = {};
1252
+ const K = HOST_BRIDGE_GLOBAL_KEYS;
1253
+ const NET_BRIDGE_TIMEOUT_SENTINEL = "__secure_exec_net_timeout__";
1254
+ // Track active kernel socket IDs for cleanup
1255
+ const activeSocketIds = new Set();
1256
+ const activeServerIds = new Set();
1257
+ const activeDgramIds = new Set();
1258
+ // Track TLS-upgraded sockets that bypass kernel recv (host-side TLS)
1259
+ const tlsSockets = new Map();
1260
+ const loopbackTlsTransports = new Map();
1261
+ const loopbackTlsClientHello = new Map();
1262
+ const pendingConnects = new Map();
1263
+ function addressFamily(host) {
1264
+ return host?.includes(":") ? "IPv6" : "IPv4";
1265
+ }
1266
+ function decodeTlsMaterial(value) {
1267
+ if (value === undefined) {
1268
+ return undefined;
1269
+ }
1270
+ const decodeOne = (entry) => entry.kind === "buffer" ? Buffer.from(entry.data, "base64") : entry.data;
1271
+ return Array.isArray(value) ? value.map(decodeOne) : decodeOne(value);
1272
+ }
1273
+ function buildHostTlsOptions(options) {
1274
+ const hostOptions = {};
1275
+ const key = decodeTlsMaterial(options.key);
1276
+ const cert = decodeTlsMaterial(options.cert);
1277
+ const ca = decodeTlsMaterial(options.ca);
1278
+ if (key !== undefined)
1279
+ hostOptions.key = key;
1280
+ if (cert !== undefined)
1281
+ hostOptions.cert = cert;
1282
+ if (ca !== undefined)
1283
+ hostOptions.ca = ca;
1284
+ if (typeof options.passphrase === "string")
1285
+ hostOptions.passphrase = options.passphrase;
1286
+ if (typeof options.ciphers === "string")
1287
+ hostOptions.ciphers = options.ciphers;
1288
+ if (typeof options.session === "string")
1289
+ hostOptions.session = Buffer.from(options.session, "base64");
1290
+ if (Array.isArray(options.ALPNProtocols) && options.ALPNProtocols.length > 0) {
1291
+ hostOptions.ALPNProtocols = [...options.ALPNProtocols];
1292
+ }
1293
+ if (typeof options.minVersion === "string")
1294
+ hostOptions.minVersion = options.minVersion;
1295
+ if (typeof options.maxVersion === "string")
1296
+ hostOptions.maxVersion = options.maxVersion;
1297
+ if (typeof options.servername === "string")
1298
+ hostOptions.servername = options.servername;
1299
+ if (typeof options.requestCert === "boolean")
1300
+ hostOptions.requestCert = options.requestCert;
1301
+ return hostOptions;
1302
+ }
1303
+ function getLoopbackTlsKey(socketId, peerId) {
1304
+ return socketId < peerId ? `${socketId}:${peerId}` : `${peerId}:${socketId}`;
1305
+ }
1306
+ function createTlsTransportEndpoint(readable, writable) {
1307
+ const duplex = new Duplex({
1308
+ read() {
1309
+ let chunk;
1310
+ while ((chunk = readable.read()) !== null) {
1311
+ if (!this.push(chunk)) {
1312
+ return;
1313
+ }
1314
+ }
1315
+ },
1316
+ write(chunk, _encoding, callback) {
1317
+ if (!writable.write(chunk)) {
1318
+ writable.once("drain", callback);
1319
+ return;
1320
+ }
1321
+ callback();
1322
+ },
1323
+ final(callback) {
1324
+ writable.end();
1325
+ callback();
1326
+ },
1327
+ destroy(error, callback) {
1328
+ readable.destroy(error ?? undefined);
1329
+ writable.destroy(error ?? undefined);
1330
+ callback(error ?? null);
1331
+ },
1332
+ });
1333
+ readable.on("readable", () => {
1334
+ let chunk;
1335
+ while ((chunk = readable.read()) !== null) {
1336
+ if (!duplex.push(chunk)) {
1337
+ return;
1338
+ }
1339
+ }
1340
+ });
1341
+ readable.on("end", () => duplex.push(null));
1342
+ readable.on("error", (error) => duplex.destroy(error));
1343
+ return duplex;
1344
+ }
1345
+ function getLoopbackTlsTransport(socket) {
1346
+ if (socket.peerId === undefined) {
1347
+ throw new Error(`Socket ${socket.id} has no loopback peer for TLS upgrade`);
1348
+ }
1349
+ const key = getLoopbackTlsKey(socket.id, socket.peerId);
1350
+ let pair = loopbackTlsTransports.get(key);
1351
+ if (!pair) {
1352
+ const aIn = new PassThrough();
1353
+ const bIn = new PassThrough();
1354
+ pair = {
1355
+ a: createTlsTransportEndpoint(aIn, bIn),
1356
+ b: createTlsTransportEndpoint(bIn, aIn),
1357
+ };
1358
+ loopbackTlsTransports.set(key, pair);
1359
+ }
1360
+ return socket.id < socket.peerId ? pair.a : pair.b;
1361
+ }
1362
+ function cleanupLoopbackTlsTransport(socketId, peerId) {
1363
+ if (peerId === undefined) {
1364
+ return;
1365
+ }
1366
+ if (tlsSockets.has(socketId) || tlsSockets.has(peerId)) {
1367
+ return;
1368
+ }
1369
+ const key = getLoopbackTlsKey(socketId, peerId);
1370
+ const pair = loopbackTlsTransports.get(key);
1371
+ if (!pair) {
1372
+ return;
1373
+ }
1374
+ pair.a.destroy();
1375
+ pair.b.destroy();
1376
+ loopbackTlsTransports.delete(key);
1377
+ loopbackTlsClientHello.delete(key);
1378
+ }
1379
+ function serializeTlsState(tlsSocket) {
1380
+ let cipher = null;
1381
+ try {
1382
+ const details = tlsSocket.getCipher();
1383
+ if (details) {
1384
+ const standardName = details.standardName ?? details.name;
1385
+ cipher = {
1386
+ name: details.name,
1387
+ standardName,
1388
+ version: details.version,
1389
+ };
1390
+ }
1391
+ }
1392
+ catch {
1393
+ cipher = null;
1394
+ }
1395
+ return JSON.stringify({
1396
+ authorized: tlsSocket.authorized === true,
1397
+ authorizationError: typeof tlsSocket.authorizationError === "string"
1398
+ ? tlsSocket.authorizationError
1399
+ : undefined,
1400
+ alpnProtocol: tlsSocket.alpnProtocol || false,
1401
+ servername: tlsSocket.servername,
1402
+ protocol: tlsSocket.getProtocol?.() ?? null,
1403
+ sessionReused: tlsSocket.isSessionReused?.() === true,
1404
+ cipher,
1405
+ });
1406
+ }
1407
+ function serializeTlsBridgeValue(value, seen = new Map()) {
1408
+ if (value === undefined) {
1409
+ return { type: "undefined" };
1410
+ }
1411
+ if (value === null ||
1412
+ typeof value === "boolean" ||
1413
+ typeof value === "number" ||
1414
+ typeof value === "string") {
1415
+ return value;
1416
+ }
1417
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
1418
+ return {
1419
+ type: "buffer",
1420
+ data: Buffer.from(value).toString("base64"),
1421
+ };
1422
+ }
1423
+ if (Array.isArray(value)) {
1424
+ return {
1425
+ type: "array",
1426
+ value: value.map((entry) => serializeTlsBridgeValue(entry, seen)),
1427
+ };
1428
+ }
1429
+ if (typeof value === "object") {
1430
+ const existingId = seen.get(value);
1431
+ if (existingId !== undefined) {
1432
+ return { type: "ref", id: existingId };
1433
+ }
1434
+ const id = seen.size + 1;
1435
+ seen.set(value, id);
1436
+ const serialized = {};
1437
+ for (const [key, entry] of Object.entries(value)) {
1438
+ serialized[key] = serializeTlsBridgeValue(entry, seen);
1439
+ }
1440
+ return {
1441
+ type: "object",
1442
+ id,
1443
+ value: serialized,
1444
+ };
1445
+ }
1446
+ return String(value);
1447
+ }
1448
+ function serializeTlsError(error, tlsSocket) {
1449
+ const err = error instanceof Error ? error : new Error(typeof error === "string" ? error : String(error));
1450
+ const payload = {
1451
+ message: err.message,
1452
+ name: err.name,
1453
+ stack: err.stack,
1454
+ };
1455
+ const code = err.code;
1456
+ if (typeof code === "string") {
1457
+ payload.code = code;
1458
+ }
1459
+ if (tlsSocket) {
1460
+ payload.authorized = tlsSocket.authorized === true;
1461
+ if (typeof tlsSocket.authorizationError === "string") {
1462
+ payload.authorizationError = tlsSocket.authorizationError;
1463
+ }
1464
+ }
1465
+ return JSON.stringify(payload);
1466
+ }
1467
+ function serializeSocketInfo(socketId) {
1468
+ const socket = socketTable.get(socketId);
1469
+ const localAddr = socket?.localAddr;
1470
+ const remoteAddr = socket?.remoteAddr;
1471
+ return {
1472
+ localAddress: localAddr && typeof localAddr === "object" && "host" in localAddr
1473
+ ? localAddr.host
1474
+ : localAddr && typeof localAddr === "object" && "path" in localAddr
1475
+ ? localAddr.path
1476
+ : "0.0.0.0",
1477
+ localPort: localAddr && typeof localAddr === "object" && "port" in localAddr
1478
+ ? localAddr.port
1479
+ : 0,
1480
+ localFamily: localAddr && typeof localAddr === "object" && "host" in localAddr
1481
+ ? addressFamily(localAddr.host)
1482
+ : localAddr && typeof localAddr === "object" && "path" in localAddr
1483
+ ? "Unix"
1484
+ : "IPv4",
1485
+ ...(localAddr && typeof localAddr === "object" && "path" in localAddr
1486
+ ? { localPath: localAddr.path }
1487
+ : {}),
1488
+ ...(remoteAddr && typeof remoteAddr === "object" && "host" in remoteAddr
1489
+ ? {
1490
+ remoteAddress: remoteAddr.host,
1491
+ remotePort: remoteAddr.port,
1492
+ remoteFamily: addressFamily(remoteAddr.host),
1493
+ }
1494
+ : remoteAddr && typeof remoteAddr === "object" && "path" in remoteAddr
1495
+ ? {
1496
+ remoteAddress: remoteAddr.path,
1497
+ remoteFamily: "Unix",
1498
+ remotePath: remoteAddr.path,
1499
+ }
1500
+ : {}),
1501
+ };
1502
+ }
1503
+ function getBackingSocket(socketId) {
1504
+ const tlsSocket = tlsSockets.get(socketId);
1505
+ if (tlsSocket) {
1506
+ return tlsSocket;
1507
+ }
1508
+ const socket = socketTable.get(socketId);
1509
+ const hostSocket = socket?.hostSocket;
1510
+ return hostSocket?.socket;
1511
+ }
1512
+ function dispatchAsync(socketId, event, data) {
1513
+ setTimeout(() => {
1514
+ dispatch(socketId, event, data);
1515
+ }, 0);
1516
+ }
1517
+ /** Background read pump: polls kernel recv() and dispatches data/end/close. */
1518
+ function startReadPump(socketId) {
1519
+ const pump = async () => {
1520
+ try {
1521
+ while (activeSocketIds.has(socketId)) {
1522
+ // Try to read data
1523
+ let data;
1524
+ try {
1525
+ data = socketTable.recv(socketId, 65536, 0);
1526
+ }
1527
+ catch {
1528
+ // Socket closed or error — stop pump
1529
+ break;
1530
+ }
1531
+ if (data !== null) {
1532
+ dispatchAsync(socketId, "data", Buffer.from(data).toString("base64"));
1533
+ continue;
1534
+ }
1535
+ // No data — check if EOF
1536
+ const socket = socketTable.get(socketId);
1537
+ if (!socket)
1538
+ break;
1539
+ if (socket.state === "closed" || socket.state === "read-closed") {
1540
+ dispatchAsync(socketId, "end");
1541
+ break;
1542
+ }
1543
+ if (socket.peerWriteClosed || (socket.peerId === undefined && !socket.external)) {
1544
+ dispatchAsync(socketId, "end");
1545
+ break;
1546
+ }
1547
+ // For external sockets, check hostSocket EOF via readBuffer state
1548
+ if (socket.external && socket.readBuffer.length === 0 && socket.peerWriteClosed) {
1549
+ dispatchAsync(socketId, "end");
1550
+ break;
1551
+ }
1552
+ // Wait for data to arrive
1553
+ const handle = socket.readWaiters.enqueue();
1554
+ await handle.wait();
1555
+ }
1556
+ }
1557
+ catch {
1558
+ // Socket destroyed during pump — expected
1559
+ }
1560
+ // Dispatch close if socket was active
1561
+ if (activeSocketIds.delete(socketId)) {
1562
+ dispatchAsync(socketId, "close");
1563
+ }
1564
+ };
1565
+ pump();
1566
+ }
1567
+ // Connect — create kernel socket and start async connect + read pump
1568
+ handlers[K.netSocketConnectRaw] = (optionsJson) => {
1569
+ const options = parseJsonWithLimit("net.socket.connect options", String(optionsJson), 128 * 1024);
1570
+ const isUnixPath = typeof options.path === "string" && options.path.length > 0;
1571
+ const host = String(options.host ?? "127.0.0.1");
1572
+ const port = Number(options.port ?? 0);
1573
+ const socketId = socketTable.create(isUnixPath ? AF_UNIX : host.includes(":") ? AF_INET6 : AF_INET, SOCK_STREAM, 0, pid);
1574
+ activeSocketIds.add(socketId);
1575
+ // Async connect completion is polled from the isolate via waitConnectRaw.
1576
+ pendingConnects.set(socketId, socketTable.connect(socketId, isUnixPath ? { path: options.path } : { host, port }).then(() => ({ ok: true }), (error) => ({
1577
+ ok: false,
1578
+ error: error instanceof Error ? error.message : String(error),
1579
+ })));
1580
+ return socketId;
1581
+ };
1582
+ handlers[K.netSocketWaitConnectRaw] = async (socketId) => {
1583
+ const id = Number(socketId);
1584
+ const pending = pendingConnects.get(id);
1585
+ try {
1586
+ if (pending) {
1587
+ const result = await pending;
1588
+ if (!result.ok) {
1589
+ throw new Error(result.error);
1590
+ }
1591
+ }
1592
+ return JSON.stringify(serializeSocketInfo(id));
1593
+ }
1594
+ finally {
1595
+ pendingConnects.delete(id);
1596
+ }
1597
+ };
1598
+ handlers[K.netSocketReadRaw] = (socketId) => {
1599
+ const id = Number(socketId);
1600
+ if (!activeSocketIds.has(id)) {
1601
+ return null;
1602
+ }
1603
+ try {
1604
+ const chunk = socketTable.recv(id, 65536, 0);
1605
+ if (chunk !== null) {
1606
+ return Buffer.from(chunk).toString("base64");
1607
+ }
1608
+ const socket = socketTable.get(id);
1609
+ if (!socket ||
1610
+ socket.state === "closed" ||
1611
+ socket.state === "read-closed" ||
1612
+ socket.peerWriteClosed) {
1613
+ return null;
1614
+ }
1615
+ return NET_BRIDGE_TIMEOUT_SENTINEL;
1616
+ }
1617
+ catch (error) {
1618
+ if (error instanceof Error && error.message.includes("EAGAIN")) {
1619
+ return NET_BRIDGE_TIMEOUT_SENTINEL;
1620
+ }
1621
+ return null;
1622
+ }
1623
+ };
1624
+ handlers[K.netSocketSetNoDelayRaw] = (socketId, enable) => {
1625
+ const id = Number(socketId);
1626
+ socketTable.setsockopt(id, IPPROTO_TCP, TCP_NODELAY, enable ? 1 : 0);
1627
+ getBackingSocket(id)?.setNoDelay(Boolean(enable));
1628
+ };
1629
+ handlers[K.netSocketSetKeepAliveRaw] = (socketId, enable, initialDelaySeconds) => {
1630
+ const id = Number(socketId);
1631
+ const delaySeconds = Math.max(0, Number(initialDelaySeconds) || 0);
1632
+ socketTable.setsockopt(id, SOL_SOCKET, SO_KEEPALIVE, enable ? 1 : 0);
1633
+ getBackingSocket(id)?.setKeepAlive(Boolean(enable), delaySeconds * 1000);
1634
+ };
1635
+ // Write — send data through kernel socket
1636
+ handlers[K.netSocketWriteRaw] = (socketId, dataBase64) => {
1637
+ const id = Number(socketId);
1638
+ // TLS-upgraded sockets write directly to host TLS socket
1639
+ const tlsSocket = tlsSockets.get(id);
1640
+ if (tlsSocket) {
1641
+ tlsSocket.write(Buffer.from(String(dataBase64), "base64"));
1642
+ return;
1643
+ }
1644
+ const data = Buffer.from(String(dataBase64), "base64");
1645
+ socketTable.send(id, new Uint8Array(data), 0);
1646
+ };
1647
+ // End — half-close write side
1648
+ handlers[K.netSocketEndRaw] = (socketId) => {
1649
+ const id = Number(socketId);
1650
+ const tlsSocket = tlsSockets.get(id);
1651
+ if (tlsSocket) {
1652
+ tlsSocket.end();
1653
+ return;
1654
+ }
1655
+ try {
1656
+ socketTable.shutdown(id, "write");
1657
+ }
1658
+ catch {
1659
+ // Socket may already be closed
1660
+ }
1661
+ };
1662
+ // Destroy — close kernel socket
1663
+ handlers[K.netSocketDestroyRaw] = (socketId) => {
1664
+ const id = Number(socketId);
1665
+ const socket = socketTable.get(id);
1666
+ const tlsSocket = tlsSockets.get(id);
1667
+ if (tlsSocket) {
1668
+ tlsSocket.destroy();
1669
+ tlsSockets.delete(id);
1670
+ }
1671
+ cleanupLoopbackTlsTransport(id, socket?.peerId);
1672
+ socketTable.get(id)?.readWaiters.wakeAll();
1673
+ if (activeSocketIds.has(id)) {
1674
+ activeSocketIds.delete(id);
1675
+ try {
1676
+ socketTable.close(id, pid);
1677
+ }
1678
+ catch {
1679
+ // Already closed
1680
+ }
1681
+ }
1682
+ };
1683
+ // TLS upgrade — for external kernel sockets, unwrap the host socket
1684
+ // and wrap with TLS. Loopback sockets cannot be TLS-upgraded (no real TCP).
1685
+ handlers[K.netSocketUpgradeTlsRaw] = (socketId, optionsJson) => {
1686
+ const id = Number(socketId);
1687
+ const socket = socketTable.get(id);
1688
+ if (!socket)
1689
+ throw new Error(`Socket ${id} not found for TLS upgrade`);
1690
+ const options = optionsJson
1691
+ ? parseJsonWithLimit("net.socket.upgradeTls options", String(optionsJson), 256 * 1024)
1692
+ : {};
1693
+ const hostTlsOptions = buildHostTlsOptions(options);
1694
+ const peerId = socket.peerId;
1695
+ const loopbackTlsKey = peerId === undefined ? undefined : getLoopbackTlsKey(id, peerId);
1696
+ if (!options.isServer && loopbackTlsKey) {
1697
+ loopbackTlsClientHello.set(loopbackTlsKey, {
1698
+ servername: options.servername,
1699
+ ALPNProtocols: options.ALPNProtocols,
1700
+ });
1701
+ }
1702
+ let transport;
1703
+ if (socket.external && socket.hostSocket) {
1704
+ const hostSocket = socket.hostSocket;
1705
+ const realSocket = hostSocket.socket;
1706
+ if (!realSocket) {
1707
+ throw new Error(`Socket ${id} has no underlying TCP socket for TLS upgrade`);
1708
+ }
1709
+ socket.hostSocket = undefined;
1710
+ transport = realSocket;
1711
+ }
1712
+ else {
1713
+ transport = getLoopbackTlsTransport(socket);
1714
+ }
1715
+ const tlsSocket = options.isServer
1716
+ ? new tls.TLSSocket(transport, {
1717
+ isServer: true,
1718
+ secureContext: tls.createSecureContext(hostTlsOptions),
1719
+ requestCert: options.requestCert === true,
1720
+ rejectUnauthorized: options.rejectUnauthorized === true,
1721
+ })
1722
+ : tls.connect({
1723
+ socket: transport,
1724
+ ...hostTlsOptions,
1725
+ rejectUnauthorized: options.rejectUnauthorized !== false,
1726
+ });
1727
+ // Track TLS socket for write/end/destroy bypass
1728
+ tlsSockets.set(id, tlsSocket);
1729
+ tlsSocket.on("secureConnect", () => dispatchAsync(id, "secureConnect", serializeTlsState(tlsSocket)));
1730
+ tlsSocket.on("secure", () => dispatchAsync(id, "secure", serializeTlsState(tlsSocket)));
1731
+ tlsSocket.on("session", (session) => dispatchAsync(id, "session", session.toString("base64")));
1732
+ tlsSocket.on("data", (chunk) => dispatchAsync(id, "data", chunk.toString("base64")));
1733
+ tlsSocket.on("end", () => dispatchAsync(id, "end"));
1734
+ tlsSocket.on("error", (err) => dispatchAsync(id, "error", serializeTlsError(err, tlsSocket)));
1735
+ tlsSocket.on("close", () => {
1736
+ tlsSockets.delete(id);
1737
+ activeSocketIds.delete(id);
1738
+ cleanupLoopbackTlsTransport(id, peerId);
1739
+ dispatchAsync(id, "close");
1740
+ });
1741
+ };
1742
+ handlers[K.netSocketGetTlsClientHelloRaw] = (socketId) => {
1743
+ const id = Number(socketId);
1744
+ const socket = socketTable.get(id);
1745
+ if (!socket || socket.peerId === undefined) {
1746
+ return "{}";
1747
+ }
1748
+ const entry = loopbackTlsClientHello.get(getLoopbackTlsKey(id, socket.peerId));
1749
+ return JSON.stringify(entry ?? {});
1750
+ };
1751
+ handlers[K.netSocketTlsQueryRaw] = (socketId, query, detailed) => {
1752
+ const tlsSocket = tlsSockets.get(Number(socketId));
1753
+ if (!tlsSocket) {
1754
+ return JSON.stringify({ type: "undefined" });
1755
+ }
1756
+ let result;
1757
+ switch (String(query)) {
1758
+ case "getSession":
1759
+ result = tlsSocket.getSession();
1760
+ break;
1761
+ case "isSessionReused":
1762
+ result = tlsSocket.isSessionReused();
1763
+ break;
1764
+ case "getPeerCertificate":
1765
+ result = tlsSocket.getPeerCertificate(Boolean(detailed));
1766
+ break;
1767
+ case "getCertificate":
1768
+ result = tlsSocket.getCertificate();
1769
+ break;
1770
+ case "getProtocol":
1771
+ result = tlsSocket.getProtocol();
1772
+ break;
1773
+ case "getCipher":
1774
+ result = tlsSocket.getCipher();
1775
+ break;
1776
+ default:
1777
+ result = undefined;
1778
+ break;
1779
+ }
1780
+ return JSON.stringify(serializeTlsBridgeValue(result));
1781
+ };
1782
+ handlers[K.tlsGetCiphersRaw] = () => JSON.stringify(tls.getCiphers());
1783
+ handlers[K.netServerListenRaw] = async (optionsJson) => {
1784
+ const options = parseJsonWithLimit("net.server.listen options", String(optionsJson), 128 * 1024);
1785
+ const isUnixPath = typeof options.path === "string" && options.path.length > 0;
1786
+ const host = String(options.host ?? "127.0.0.1");
1787
+ const serverId = socketTable.create(isUnixPath ? AF_UNIX : host.includes(":") ? AF_INET6 : AF_INET, SOCK_STREAM, 0, pid);
1788
+ activeServerIds.add(serverId);
1789
+ const socketMode = options.readableAll || options.writableAll
1790
+ ? 0o600 |
1791
+ (options.readableAll ? 0o044 : 0) |
1792
+ (options.writableAll ? 0o022 : 0)
1793
+ : undefined;
1794
+ await socketTable.bind(serverId, isUnixPath
1795
+ ? { path: options.path }
1796
+ : {
1797
+ host,
1798
+ port: Number(options.port ?? 0),
1799
+ }, socketMode === undefined ? undefined : { mode: socketMode });
1800
+ await socketTable.listen(serverId, Number(options.backlog ?? 511));
1801
+ return JSON.stringify({
1802
+ serverId,
1803
+ address: serializeSocketInfo(serverId),
1804
+ });
1805
+ };
1806
+ handlers[K.netServerAcceptRaw] = (serverId) => {
1807
+ const id = Number(serverId);
1808
+ if (!activeServerIds.has(id)) {
1809
+ return null;
1810
+ }
1811
+ const listener = socketTable.get(id);
1812
+ if (!listener || listener.state !== "listening") {
1813
+ return null;
1814
+ }
1815
+ const acceptedId = socketTable.accept(id);
1816
+ if (acceptedId === null) {
1817
+ return NET_BRIDGE_TIMEOUT_SENTINEL;
1818
+ }
1819
+ activeSocketIds.add(acceptedId);
1820
+ return JSON.stringify({
1821
+ socketId: acceptedId,
1822
+ info: serializeSocketInfo(acceptedId),
1823
+ });
1824
+ };
1825
+ handlers[K.netServerCloseRaw] = async (serverId) => {
1826
+ const id = Number(serverId);
1827
+ activeServerIds.delete(id);
1828
+ socketTable.get(id)?.acceptWaiters.wakeAll();
1829
+ try {
1830
+ socketTable.close(id, pid);
1831
+ }
1832
+ catch {
1833
+ // Already closed
1834
+ }
1835
+ };
1836
+ handlers[K.dgramSocketCreateRaw] = (type) => {
1837
+ const socketType = String(type);
1838
+ const domain = socketType === "udp6" ? AF_INET6 : AF_INET;
1839
+ const socketId = socketTable.create(domain, SOCK_DGRAM, 0, pid);
1840
+ activeDgramIds.add(socketId);
1841
+ return socketId;
1842
+ };
1843
+ handlers[K.dgramSocketBindRaw] = async (socketId, optionsJson) => {
1844
+ const id = Number(socketId);
1845
+ const socket = socketTable.get(id);
1846
+ if (!socket) {
1847
+ throw new Error(`UDP socket ${id} not found`);
1848
+ }
1849
+ const options = parseJsonWithLimit("dgram.socket.bind options", String(optionsJson), 128 * 1024);
1850
+ const host = String(options.address ??
1851
+ (socket.domain === AF_INET6 ? "::" : "0.0.0.0"));
1852
+ await socketTable.bind(id, {
1853
+ host,
1854
+ port: Number(options.port ?? 0),
1855
+ });
1856
+ return JSON.stringify(serializeSocketInfo(id));
1857
+ };
1858
+ handlers[K.dgramSocketRecvRaw] = (socketId) => {
1859
+ const id = Number(socketId);
1860
+ if (!activeDgramIds.has(id)) {
1861
+ return null;
1862
+ }
1863
+ try {
1864
+ const socket = socketTable.get(id);
1865
+ if (!socket || socket.state === "closed") {
1866
+ return null;
1867
+ }
1868
+ const message = socketTable.recvFrom(id, 65535, 0);
1869
+ if (message === null) {
1870
+ return NET_BRIDGE_TIMEOUT_SENTINEL;
1871
+ }
1872
+ return JSON.stringify({
1873
+ data: Buffer.from(message.data).toString("base64"),
1874
+ rinfo: "path" in message.srcAddr
1875
+ ? {
1876
+ address: message.srcAddr.path,
1877
+ family: "unix",
1878
+ port: 0,
1879
+ size: message.data.length,
1880
+ }
1881
+ : {
1882
+ address: message.srcAddr.host,
1883
+ family: addressFamily(message.srcAddr.host),
1884
+ port: message.srcAddr.port,
1885
+ size: message.data.length,
1886
+ },
1887
+ });
1888
+ }
1889
+ catch (error) {
1890
+ if (error instanceof Error && error.message.includes("EAGAIN")) {
1891
+ return NET_BRIDGE_TIMEOUT_SENTINEL;
1892
+ }
1893
+ return null;
1894
+ }
1895
+ };
1896
+ handlers[K.dgramSocketSendRaw] = async (socketId, optionsJson) => {
1897
+ const id = Number(socketId);
1898
+ const options = parseJsonWithLimit("dgram.socket.send options", String(optionsJson), 256 * 1024);
1899
+ const data = Buffer.from(options.data, "base64");
1900
+ return socketTable.sendTo(id, new Uint8Array(data), 0, { host: String(options.address), port: Number(options.port) });
1901
+ };
1902
+ handlers[K.dgramSocketCloseRaw] = async (socketId) => {
1903
+ const id = Number(socketId);
1904
+ activeDgramIds.delete(id);
1905
+ socketTable.get(id)?.readWaiters.wakeAll();
1906
+ try {
1907
+ socketTable.close(id, pid);
1908
+ }
1909
+ catch {
1910
+ // Already closed
1911
+ }
1912
+ };
1913
+ handlers[K.dgramSocketAddressRaw] = (socketId) => {
1914
+ const id = Number(socketId);
1915
+ const socket = socketTable.get(id);
1916
+ if (!socket?.localAddr || "path" in socket.localAddr) {
1917
+ throw new Error("getsockname EBADF");
1918
+ }
1919
+ return JSON.stringify({
1920
+ address: socket.localAddr.host,
1921
+ family: addressFamily(socket.localAddr.host),
1922
+ port: socket.localAddr.port,
1923
+ });
1924
+ };
1925
+ handlers[K.dgramSocketSetBufferSizeRaw] = (socketId, which, size) => {
1926
+ const optname = which === "send" ? SO_SNDBUF : SO_RCVBUF;
1927
+ socketTable.setsockopt(Number(socketId), SOL_SOCKET, optname, Number(size));
1928
+ };
1929
+ handlers[K.dgramSocketGetBufferSizeRaw] = (socketId, which) => {
1930
+ const optname = which === "send" ? SO_SNDBUF : SO_RCVBUF;
1931
+ return socketTable.getsockopt(Number(socketId), SOL_SOCKET, optname) ?? 0;
1932
+ };
1933
+ const dispose = () => {
1934
+ for (const id of activeServerIds) {
1935
+ try {
1936
+ socketTable.close(id, pid);
1937
+ }
1938
+ catch { /* best effort */ }
1939
+ }
1940
+ activeServerIds.clear();
1941
+ for (const id of activeDgramIds) {
1942
+ try {
1943
+ socketTable.close(id, pid);
1944
+ }
1945
+ catch { /* best effort */ }
1946
+ }
1947
+ activeDgramIds.clear();
1948
+ for (const id of activeSocketIds) {
1949
+ try {
1950
+ socketTable.close(id, pid);
1951
+ }
1952
+ catch { /* best effort */ }
1953
+ }
1954
+ activeSocketIds.clear();
1955
+ for (const socket of tlsSockets.values()) {
1956
+ socket.destroy();
1957
+ }
1958
+ tlsSockets.clear();
1959
+ for (const pair of loopbackTlsTransports.values()) {
1960
+ pair.a.destroy();
1961
+ pair.b.destroy();
1962
+ }
1963
+ loopbackTlsTransports.clear();
1964
+ loopbackTlsClientHello.clear();
1965
+ };
1966
+ return { handlers, dispose };
1967
+ }
1968
+ /**
1969
+ * Convert ESM source to CJS-compatible code for require() loading.
1970
+ * Handles import declarations, export declarations, and re-exports.
1971
+ */
1972
+ /** Strip // and /* comments from an export/import list string. */
1973
+ function stripComments(s) {
1974
+ return s.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
1975
+ }
1976
+ function convertEsmToCjs(source, filePath) {
1977
+ if (!isESM(source, filePath))
1978
+ return source;
1979
+ let code = source;
1980
+ // Remove const __filename/dirname declarations (already provided by CJS wrapper)
1981
+ code = code.replace(/^\s*(?:const|let|var)\s+__filename\s*=\s*[^;]+;?\s*$/gm, "// __filename provided by CJS wrapper");
1982
+ code = code.replace(/^\s*(?:const|let|var)\s+__dirname\s*=\s*[^;]+;?\s*$/gm, "// __dirname provided by CJS wrapper");
1983
+ // import X from 'Y' → const X = require('Y')
1984
+ code = code.replace(/^\s*import\s+(\w+)\s+from\s+['"]([^'"]+)['"]\s*;?/gm, "const $1 = (function(m) { return m && m.__esModule ? m.default : m; })(require('$2'));");
1985
+ // import { a, b as c } from 'Y' → const { a, b: c } = require('Y')
1986
+ code = code.replace(/^\s*import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]\s*;?/gm, (_match, imports, mod) => {
1987
+ const mapped = stripComments(imports).split(",").map((s) => {
1988
+ const t = s.trim();
1989
+ if (!t)
1990
+ return null;
1991
+ const parts = t.split(/\s+as\s+/);
1992
+ return parts.length === 2 ? `${parts[0].trim()}: ${parts[1].trim()}` : t;
1993
+ }).filter(Boolean).join(", ");
1994
+ return `const { ${mapped} } = require('${mod}');`;
1995
+ });
1996
+ // import * as X from 'Y' → const X = require('Y')
1997
+ code = code.replace(/^\s*import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]\s*;?/gm, "const $1 = require('$2');");
1998
+ // Side-effect imports: import 'Y' → require('Y')
1999
+ code = code.replace(/^\s*import\s+['"]([^'"]+)['"]\s*;?/gm, "require('$1');");
2000
+ // export { a, b } from 'Y' → re-export
2001
+ code = code.replace(/^\s*export\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]\s*;?/gm, (_match, exports, mod) => {
2002
+ return stripComments(exports).split(",").map((s) => {
2003
+ const t = s.trim();
2004
+ if (!t)
2005
+ return "";
2006
+ const parts = t.split(/\s+as\s+/);
2007
+ const local = parts[0].trim();
2008
+ const exported = parts.length === 2 ? parts[1].trim() : local;
2009
+ return `Object.defineProperty(exports, '${exported}', { get: () => require('${mod}').${local}, enumerable: true });`;
2010
+ }).filter(Boolean).join("\n");
2011
+ });
2012
+ // export * from 'Y'
2013
+ code = code.replace(/^\s*export\s+\*\s+from\s+['"]([^'"]+)['"]\s*;?/gm, "Object.assign(exports, require('$1'));");
2014
+ // export default X → module.exports.default = X
2015
+ code = code.replace(/^\s*export\s+default\s+/gm, "module.exports.default = ");
2016
+ // export const/let/var X = ... → const/let/var X = ...; exports.X = X;
2017
+ code = code.replace(/^\s*export\s+(const|let|var)\s+(\w+)\s*=/gm, "$1 $2 =");
2018
+ // Capture the names separately to add exports at the end
2019
+ const exportedVars = [];
2020
+ for (const m of source.matchAll(/^\s*export\s+(?:const|let|var)\s+(\w+)\s*=/gm)) {
2021
+ exportedVars.push(m[1]);
2022
+ }
2023
+ // export function X(...) → function X(...); exports.X = X;
2024
+ code = code.replace(/^\s*export\s+function\s+(\w+)/gm, "function $1");
2025
+ for (const m of source.matchAll(/^\s*export\s+function\s+(\w+)/gm)) {
2026
+ exportedVars.push(m[1]);
2027
+ }
2028
+ // export class X → class X; exports.X = X;
2029
+ code = code.replace(/^\s*export\s+class\s+(\w+)/gm, "class $1");
2030
+ for (const m of source.matchAll(/^\s*export\s+class\s+(\w+)/gm)) {
2031
+ exportedVars.push(m[1]);
2032
+ }
2033
+ // export { a, b } (local re-export without from)
2034
+ code = code.replace(/^\s*export\s+\{([^}]+)\}\s*;?/gm, (_match, exports) => {
2035
+ return stripComments(exports).split(",").map((s) => {
2036
+ const t = s.trim();
2037
+ if (!t)
2038
+ return "";
2039
+ const parts = t.split(/\s+as\s+/);
2040
+ const local = parts[0].trim();
2041
+ const exported = parts.length === 2 ? parts[1].trim() : local;
2042
+ return `Object.defineProperty(exports, '${exported}', { get: () => ${local}, enumerable: true });`;
2043
+ }).filter(Boolean).join("\n");
2044
+ });
2045
+ // Append named exports for exported vars/functions/classes
2046
+ if (exportedVars.length > 0) {
2047
+ const lines = exportedVars.map((name) => `Object.defineProperty(exports, '${name}', { get: () => ${name}, enumerable: true });`);
2048
+ code += "\n" + lines.join("\n");
2049
+ }
2050
+ return code;
2051
+ }
2052
+ /**
2053
+ * Resolve a package specifier by walking up directories and reading package.json exports.
2054
+ * Handles both root imports ('pkg') and subpath imports ('pkg/sub').
2055
+ */
2056
+ function resolvePackageExport(req, startDir, mode = "require") {
2057
+ // Split into package name and subpath
2058
+ const parts = req.startsWith("@") ? req.split("/") : [req.split("/")[0], ...req.split("/").slice(1)];
2059
+ const pkgName = req.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
2060
+ const subpath = req.startsWith("@")
2061
+ ? (parts.length > 2 ? "./" + parts.slice(2).join("/") : ".")
2062
+ : (parts.length > 1 ? "./" + parts.slice(1).join("/") : ".");
2063
+ let cur = startDir;
2064
+ while (cur !== pathDirname(cur)) {
2065
+ const pkgJsonPath = pathJoin(cur, "node_modules", ...pkgName.split("/"), "package.json");
2066
+ if (existsSync(pkgJsonPath)) {
2067
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
2068
+ let entry;
2069
+ if (pkg.exports) {
2070
+ const exportEntry = pkg.exports[subpath];
2071
+ if (typeof exportEntry === "string")
2072
+ entry = exportEntry;
2073
+ else if (exportEntry) {
2074
+ const conditionalEntry = exportEntry;
2075
+ entry =
2076
+ mode === "import"
2077
+ ? conditionalEntry.import ?? conditionalEntry.default ?? conditionalEntry.require
2078
+ : conditionalEntry.require ?? conditionalEntry.default ?? conditionalEntry.import;
2079
+ }
2080
+ }
2081
+ if (!entry && subpath === ".")
2082
+ entry = pkg.main;
2083
+ if (entry)
2084
+ return pathResolve(pathDirname(pkgJsonPath), entry);
2085
+ }
2086
+ cur = pathDirname(cur);
2087
+ }
2088
+ return null;
2089
+ }
2090
+ const hostRequire = createRequire(import.meta.url);
2091
+ /**
2092
+ * Build sync module resolution bridge handlers.
2093
+ *
2094
+ * These use Node.js require.resolve() and readFileSync() directly,
2095
+ * avoiding the async VirtualFileSystem path. Needed because the async
2096
+ * applySyncPromise pattern can't nest inside synchronous bridge
2097
+ * callbacks (e.g. net socket data events that trigger require()).
2098
+ */
2099
+ export function buildModuleResolutionBridgeHandlers(deps) {
2100
+ const handlers = {};
2101
+ const K = HOST_BRIDGE_GLOBAL_KEYS;
2102
+ // Sync require.resolve — translates sandbox paths and uses Node.js resolution.
2103
+ // Falls back to realpath + manual package.json resolution for pnpm/ESM packages.
2104
+ handlers[K.resolveModuleSync] = (request, fromDir, requestedMode) => {
2105
+ const req = String(request);
2106
+ const resolveMode = requestedMode === "require" || requestedMode === "import"
2107
+ ? requestedMode
2108
+ : "require";
2109
+ // Builtins don't need filesystem resolution
2110
+ const builtin = normalizeBuiltinSpecifier(req);
2111
+ if (builtin)
2112
+ return builtin;
2113
+ // Translate sandbox fromDir to host path for resolution context
2114
+ const sandboxDir = String(fromDir);
2115
+ const hostDir = deps.sandboxToHostPath(sandboxDir) ?? sandboxDir;
2116
+ const resolveFromExports = (dir) => {
2117
+ const resolved = resolvePackageExport(req, dir, resolveMode);
2118
+ return resolved ? deps.hostToSandboxPath(resolved) : null;
2119
+ };
2120
+ if (resolveMode === "import") {
2121
+ const resolved = resolveFromExports(hostDir);
2122
+ if (resolved)
2123
+ return resolved;
2124
+ }
2125
+ // Try require.resolve first
2126
+ try {
2127
+ const resolved = hostRequire.resolve(req, { paths: [hostDir] });
2128
+ return deps.hostToSandboxPath(resolved);
2129
+ }
2130
+ catch { /* CJS resolution failed */ }
2131
+ // Fallback: follow symlinks and try ESM-compatible resolution
2132
+ try {
2133
+ let realDir;
2134
+ try {
2135
+ realDir = realpathSync(hostDir);
2136
+ }
2137
+ catch {
2138
+ realDir = hostDir;
2139
+ }
2140
+ if (resolveMode === "import") {
2141
+ const resolved = resolveFromExports(realDir);
2142
+ if (resolved)
2143
+ return resolved;
2144
+ }
2145
+ // Try require.resolve from real path
2146
+ try {
2147
+ const resolved = hostRequire.resolve(req, { paths: [realDir] });
2148
+ return deps.hostToSandboxPath(resolved);
2149
+ }
2150
+ catch { /* ESM-only, manual resolution */ }
2151
+ // Manual package.json resolution for ESM packages
2152
+ const resolved = resolveFromExports(realDir);
2153
+ if (resolved)
2154
+ return resolved;
2155
+ }
2156
+ catch { /* fallback failed */ }
2157
+ return null;
2158
+ };
2159
+ // Sync file read — translates sandbox path and reads via readFileSync.
2160
+ // Transforms dynamic import() to __dynamicImport() and converts ESM to CJS
2161
+ // for npm packages so require() can load ESM-only dependencies.
2162
+ handlers[K.loadFileSync] = (filePath) => {
2163
+ const sandboxPath = String(filePath);
2164
+ const hostPath = deps.sandboxToHostPath(sandboxPath) ?? sandboxPath;
2165
+ try {
2166
+ let source = readFileSync(hostPath, "utf-8");
2167
+ source = convertEsmToCjs(source, hostPath);
2168
+ return transformDynamicImport(source);
2169
+ }
2170
+ catch {
2171
+ return null;
2172
+ }
2173
+ };
2174
+ return handlers;
2175
+ }
2176
+ // Env vars that could hijack child processes (library injection, node flags)
2177
+ const DANGEROUS_ENV_KEYS = new Set([
2178
+ "LD_PRELOAD",
2179
+ "LD_LIBRARY_PATH",
2180
+ "NODE_OPTIONS",
2181
+ "DYLD_INSERT_LIBRARIES",
2182
+ ]);
2183
+ /** Strip env vars that allow library injection or node flag smuggling. */
2184
+ export function stripDangerousEnv(env) {
2185
+ if (!env)
2186
+ return env;
2187
+ const result = {};
2188
+ for (const [key, value] of Object.entries(env)) {
2189
+ if (!DANGEROUS_ENV_KEYS.has(key)) {
2190
+ result[key] = value;
2191
+ }
2192
+ }
2193
+ return result;
2194
+ }
2195
+ export function emitConsoleEvent(onStdio, event) {
2196
+ if (!onStdio)
2197
+ return;
2198
+ try {
2199
+ onStdio(event);
2200
+ }
2201
+ catch {
2202
+ // Keep runtime execution deterministic even when host hooks fail.
2203
+ }
2204
+ }
2205
+ /** Build console/logging bridge handlers. */
2206
+ export function buildConsoleBridgeHandlers(deps) {
2207
+ const handlers = {};
2208
+ const K = HOST_BRIDGE_GLOBAL_KEYS;
2209
+ handlers[K.log] = (msg) => {
2210
+ const str = String(msg);
2211
+ if (deps.maxOutputBytes !== undefined) {
2212
+ const bytes = Buffer.byteLength(str, "utf8");
2213
+ if (deps.budgetState.outputBytes + bytes > deps.maxOutputBytes)
2214
+ return;
2215
+ deps.budgetState.outputBytes += bytes;
2216
+ }
2217
+ emitConsoleEvent(deps.onStdio, { channel: "stdout", message: str });
2218
+ };
2219
+ handlers[K.error] = (msg) => {
2220
+ const str = String(msg);
2221
+ if (deps.maxOutputBytes !== undefined) {
2222
+ const bytes = Buffer.byteLength(str, "utf8");
2223
+ if (deps.budgetState.outputBytes + bytes > deps.maxOutputBytes)
2224
+ return;
2225
+ deps.budgetState.outputBytes += bytes;
2226
+ }
2227
+ emitConsoleEvent(deps.onStdio, { channel: "stderr", message: str });
2228
+ };
2229
+ return handlers;
2230
+ }
2231
+ /** Build module loading bridge handlers (loadPolyfill, resolveModule, loadFile). */
2232
+ export function buildModuleLoadingBridgeHandlers(deps,
2233
+ /** Extra handlers to dispatch through _loadPolyfill for V8 runtime compatibility. */
2234
+ dispatchHandlers) {
2235
+ const handlers = {};
2236
+ const K = HOST_BRIDGE_GLOBAL_KEYS;
2237
+ // Polyfill loading — also serves as bridge dispatch multiplexer.
2238
+ // The V8 runtime binary only registers a fixed set of bridge globals.
2239
+ // Newer handlers (crypto, net sockets, etc.) are dispatched through
2240
+ // _loadPolyfill with a "__bd:" prefix.
2241
+ handlers[K.loadPolyfill] = async (moduleName) => {
2242
+ const nameStr = String(moduleName);
2243
+ // Bridge dispatch: "__bd:methodName:base64args"
2244
+ if (nameStr.startsWith("__bd:") && dispatchHandlers) {
2245
+ const colonIdx = nameStr.indexOf(":", 5);
2246
+ const method = nameStr.substring(5, colonIdx > 0 ? colonIdx : undefined);
2247
+ const argsJson = colonIdx > 0 ? nameStr.substring(colonIdx + 1) : "[]";
2248
+ const handler = dispatchHandlers[method];
2249
+ if (!handler)
2250
+ return JSON.stringify({ __bd_error: `No handler: ${method}` });
2251
+ try {
2252
+ const args = restoreDispatchArgument(JSON.parse(argsJson));
2253
+ const result = await handler(...(Array.isArray(args) ? args : [args]));
2254
+ return JSON.stringify({ __bd_result: result });
2255
+ }
2256
+ catch (err) {
2257
+ return JSON.stringify({ __bd_error: serializeDispatchError(err) });
2258
+ }
2259
+ }
2260
+ const name = nameStr.replace(/^node:/, "");
2261
+ if (name === "fs" || name === "child_process" || name === "http" ||
2262
+ name === "https" || name === "http2" || name === "dns" ||
2263
+ name === "os" || name === "module") {
2264
+ return null;
2265
+ }
2266
+ if (!hasPolyfill(name))
2267
+ return null;
2268
+ let code = polyfillCodeCache.get(name);
2269
+ if (!code) {
2270
+ code = await bundlePolyfill(name);
2271
+ polyfillCodeCache.set(name, code);
2272
+ }
2273
+ return code;
2274
+ };
2275
+ // Async module path resolution via VFS
2276
+ // V8 ESM module resolve sends the full file path as referrer, not a directory.
2277
+ // Extract dirname when the referrer looks like a file path.
2278
+ // Falls back to Node.js require.resolve() with realpath for pnpm compatibility.
2279
+ handlers[K.resolveModule] = async (request, fromDir, requestedMode) => {
2280
+ const req = String(request);
2281
+ const resolveMode = requestedMode === "require" || requestedMode === "import"
2282
+ ? requestedMode
2283
+ : (deps.resolveMode ?? "require");
2284
+ const builtin = normalizeBuiltinSpecifier(req);
2285
+ if (builtin)
2286
+ return builtin;
2287
+ let dir = String(fromDir);
2288
+ if (/\.[cm]?[jt]sx?$/.test(dir)) {
2289
+ const lastSlash = dir.lastIndexOf("/");
2290
+ if (lastSlash > 0)
2291
+ dir = dir.slice(0, lastSlash);
2292
+ }
2293
+ const vfsResult = await resolveModule(req, dir, deps.filesystem, resolveMode, deps.resolutionCache);
2294
+ if (vfsResult)
2295
+ return vfsResult;
2296
+ // Fallback: resolve through real host paths for pnpm symlink compatibility.
2297
+ const hostDir = deps.sandboxToHostPath?.(dir) ?? dir;
2298
+ try {
2299
+ let realDir;
2300
+ try {
2301
+ realDir = realpathSync(hostDir);
2302
+ }
2303
+ catch {
2304
+ realDir = hostDir;
2305
+ }
2306
+ if (resolveMode === "import") {
2307
+ const resolvedImport = resolvePackageExport(req, realDir, "import");
2308
+ if (resolvedImport)
2309
+ return resolvedImport;
2310
+ }
2311
+ // Try require.resolve (works for CJS packages)
2312
+ try {
2313
+ return hostRequire.resolve(req, { paths: [realDir] });
2314
+ }
2315
+ catch { /* ESM-only, try manual resolution */ }
2316
+ // Manual package.json resolution for ESM packages
2317
+ const resolved = resolvePackageExport(req, realDir, resolveMode);
2318
+ if (resolved)
2319
+ return resolved;
2320
+ }
2321
+ catch { /* resolution failed */ }
2322
+ return null;
2323
+ };
2324
+ // Dynamic import bridge — returns null to fall back to require() in the sandbox.
2325
+ // V8 ESM module mode handles static imports natively via module_resolve_callback;
2326
+ // this handler covers the __dynamicImport() path used in exec mode.
2327
+ handlers[K.dynamicImport] = async () => null;
2328
+ // Async file read + dynamic import transform.
2329
+ // Also serves ESM wrappers for built-in modules (fs, path, etc.) when
2330
+ // used from V8's ES module system which calls _loadFile after _resolveModule.
2331
+ handlers[K.loadFile] = async (path, requestedMode) => {
2332
+ const p = String(path);
2333
+ const loadMode = requestedMode === "require" || requestedMode === "import"
2334
+ ? requestedMode
2335
+ : (deps.resolveMode ?? "require");
2336
+ // Built-in module ESM wrappers (V8 module system resolves 'fs' then loads it)
2337
+ const bare = p.replace(/^node:/, "");
2338
+ const builtin = getStaticBuiltinWrapperSource(bare);
2339
+ if (builtin)
2340
+ return builtin;
2341
+ // Polyfill-backed builtins (crypto, zlib, etc.)
2342
+ if (hasPolyfill(bare)) {
2343
+ return createBuiltinESMWrapper(`globalThis._requireFrom(${JSON.stringify(bare)}, "/")`, getHostBuiltinNamedExports(bare));
2344
+ }
2345
+ // Regular files load differently for CommonJS require() vs V8's ESM loader.
2346
+ let source = await loadFile(p, deps.filesystem);
2347
+ if (source === null)
2348
+ return null;
2349
+ if (loadMode === "require") {
2350
+ source = convertEsmToCjs(source, p);
2351
+ }
2352
+ return transformDynamicImport(source);
2353
+ };
2354
+ return handlers;
2355
+ }
2356
+ /** Build timer bridge handler. */
2357
+ export function buildTimerBridgeHandlers(deps) {
2358
+ const handlers = {};
2359
+ const K = HOST_BRIDGE_GLOBAL_KEYS;
2360
+ handlers[K.scheduleTimer] = (delayMs) => {
2361
+ checkBridgeBudget(deps);
2362
+ return new Promise((resolve) => {
2363
+ const id = globalThis.setTimeout(() => {
2364
+ deps.activeHostTimers.delete(id);
2365
+ resolve();
2366
+ }, Number(delayMs));
2367
+ deps.activeHostTimers.add(id);
2368
+ });
2369
+ };
2370
+ return handlers;
2371
+ }
2372
+ export function buildKernelTimerDispatchHandlers(deps) {
2373
+ const handlers = {};
2374
+ handlers.kernelTimerCreate = (delayMs, repeat) => {
2375
+ checkBridgeBudget(deps);
2376
+ const normalizedDelay = Number(delayMs);
2377
+ return deps.timerTable.createTimer(deps.pid, Number.isFinite(normalizedDelay) && normalizedDelay > 0
2378
+ ? Math.floor(normalizedDelay)
2379
+ : 0, Boolean(repeat), () => { });
2380
+ };
2381
+ handlers.kernelTimerArm = (timerId) => {
2382
+ checkBridgeBudget(deps);
2383
+ const timer = deps.timerTable.get(Number(timerId));
2384
+ if (!timer || timer.pid !== deps.pid || timer.cleared) {
2385
+ return;
2386
+ }
2387
+ const dispatchFire = () => {
2388
+ const activeTimer = deps.timerTable.get(timer.id);
2389
+ if (!activeTimer || activeTimer.pid !== deps.pid || activeTimer.cleared) {
2390
+ return;
2391
+ }
2392
+ activeTimer.hostHandle = undefined;
2393
+ if (!activeTimer.repeat) {
2394
+ deps.timerTable.clearTimer(activeTimer.id, deps.pid);
2395
+ }
2396
+ deps.sendStreamEvent("timer", Buffer.from(JSON.stringify({ timerId: activeTimer.id })));
2397
+ };
2398
+ if (timer.delayMs <= 0) {
2399
+ queueMicrotask(dispatchFire);
2400
+ return;
2401
+ }
2402
+ const hostHandle = globalThis.setTimeout(() => {
2403
+ deps.activeHostTimers.delete(hostHandle);
2404
+ dispatchFire();
2405
+ }, timer.delayMs);
2406
+ timer.hostHandle = hostHandle;
2407
+ deps.activeHostTimers.add(hostHandle);
2408
+ };
2409
+ handlers.kernelTimerClear = (timerId) => {
2410
+ checkBridgeBudget(deps);
2411
+ const timer = deps.timerTable.get(Number(timerId));
2412
+ if (!timer || timer.pid !== deps.pid)
2413
+ return;
2414
+ if (timer.hostHandle !== undefined) {
2415
+ clearTimeout(timer.hostHandle);
2416
+ deps.activeHostTimers.delete(timer.hostHandle);
2417
+ timer.hostHandle = undefined;
2418
+ }
2419
+ deps.timerTable.clearTimer(timer.id, deps.pid);
2420
+ };
2421
+ return handlers;
2422
+ }
2423
+ export function buildKernelHandleDispatchHandlers(deps) {
2424
+ const handlers = {};
2425
+ handlers.kernelHandleRegister = (id, description) => {
2426
+ checkBridgeBudget(deps);
2427
+ if (!deps.processTable)
2428
+ return;
2429
+ const handleId = String(id);
2430
+ let activeHandles;
2431
+ try {
2432
+ activeHandles = deps.processTable.getHandles(deps.pid);
2433
+ }
2434
+ catch {
2435
+ return;
2436
+ }
2437
+ if (activeHandles.has(handleId)) {
2438
+ try {
2439
+ deps.processTable.unregisterHandle(deps.pid, handleId);
2440
+ }
2441
+ catch {
2442
+ // Process exit races turn re-register into a no-op.
2443
+ }
2444
+ }
2445
+ deps.processTable.registerHandle(deps.pid, handleId, String(description));
2446
+ };
2447
+ handlers.kernelHandleUnregister = (id) => {
2448
+ checkBridgeBudget(deps);
2449
+ if (!deps.processTable)
2450
+ return 0;
2451
+ try {
2452
+ deps.processTable.unregisterHandle(deps.pid, String(id));
2453
+ }
2454
+ catch {
2455
+ // Unknown handles already behave like a no-op at the bridge layer.
2456
+ }
2457
+ try {
2458
+ return deps.processTable.getHandles(deps.pid).size;
2459
+ }
2460
+ catch {
2461
+ return 0;
2462
+ }
2463
+ };
2464
+ handlers.kernelHandleList = () => {
2465
+ checkBridgeBudget(deps);
2466
+ if (!deps.processTable)
2467
+ return [];
2468
+ try {
2469
+ return Array.from(deps.processTable.getHandles(deps.pid).entries());
2470
+ }
2471
+ catch {
2472
+ return [];
2473
+ }
2474
+ };
2475
+ return handlers;
2476
+ }
2477
+ /** Build filesystem bridge handlers (readFile, writeFile, stat, etc.). */
2478
+ export function buildFsBridgeHandlers(deps) {
2479
+ const handlers = {};
2480
+ const K = HOST_BRIDGE_GLOBAL_KEYS;
2481
+ const fs = deps.filesystem;
2482
+ const base64Limit = deps.bridgeBase64TransferLimitBytes;
2483
+ const jsonLimit = deps.isolateJsonPayloadLimitBytes;
2484
+ handlers[K.fsReadFile] = async (path) => {
2485
+ checkBridgeBudget(deps);
2486
+ const text = await readStandaloneProcAwareTextFile(fs, String(path));
2487
+ assertTextPayloadSize(`fs.readFile ${path}`, text, jsonLimit);
2488
+ return text;
2489
+ };
2490
+ handlers[K.fsWriteFile] = async (path, content) => {
2491
+ checkBridgeBudget(deps);
2492
+ await fs.writeFile(String(path), String(content));
2493
+ };
2494
+ handlers[K.fsReadFileBinary] = async (path) => {
2495
+ checkBridgeBudget(deps);
2496
+ const data = await readStandaloneProcAwareFile(fs, String(path));
2497
+ assertPayloadByteLength(`fs.readFileBinary ${path}`, getBase64EncodedByteLength(data.byteLength), base64Limit);
2498
+ return Buffer.from(data).toString("base64");
2499
+ };
2500
+ handlers[K.fsWriteFileBinary] = async (path, base64Content) => {
2501
+ checkBridgeBudget(deps);
2502
+ const b64 = String(base64Content);
2503
+ assertTextPayloadSize(`fs.writeFileBinary ${path}`, b64, base64Limit);
2504
+ await fs.writeFile(String(path), Buffer.from(b64, "base64"));
2505
+ };
2506
+ handlers[K.fsReadDir] = async (path) => {
2507
+ checkBridgeBudget(deps);
2508
+ const entries = (await fs.readDirWithTypes(String(path))).filter((entry) => entry.name !== "." && entry.name !== "..");
2509
+ const json = JSON.stringify(entries);
2510
+ assertTextPayloadSize(`fs.readDir ${path}`, json, jsonLimit);
2511
+ return json;
2512
+ };
2513
+ handlers[K.fsMkdir] = async (path) => {
2514
+ checkBridgeBudget(deps);
2515
+ await mkdir(fs, String(path));
2516
+ };
2517
+ handlers[K.fsRmdir] = async (path) => {
2518
+ checkBridgeBudget(deps);
2519
+ await fs.removeDir(String(path));
2520
+ };
2521
+ handlers[K.fsExists] = async (path) => {
2522
+ checkBridgeBudget(deps);
2523
+ return standaloneProcAwareExists(fs, String(path));
2524
+ };
2525
+ handlers[K.fsStat] = async (path) => {
2526
+ checkBridgeBudget(deps);
2527
+ const s = await standaloneProcAwareStat(fs, String(path));
2528
+ return JSON.stringify({ mode: s.mode, size: s.size, isDirectory: s.isDirectory,
2529
+ atimeMs: s.atimeMs, mtimeMs: s.mtimeMs, ctimeMs: s.ctimeMs, birthtimeMs: s.birthtimeMs });
2530
+ };
2531
+ handlers[K.fsUnlink] = async (path) => {
2532
+ checkBridgeBudget(deps);
2533
+ await fs.removeFile(String(path));
2534
+ };
2535
+ handlers[K.fsRename] = async (oldPath, newPath) => {
2536
+ checkBridgeBudget(deps);
2537
+ await fs.rename(String(oldPath), String(newPath));
2538
+ };
2539
+ handlers[K.fsChmod] = async (path, mode) => {
2540
+ checkBridgeBudget(deps);
2541
+ await fs.chmod(String(path), Number(mode));
2542
+ };
2543
+ handlers[K.fsChown] = async (path, uid, gid) => {
2544
+ checkBridgeBudget(deps);
2545
+ await fs.chown(String(path), Number(uid), Number(gid));
2546
+ };
2547
+ handlers[K.fsLink] = async (oldPath, newPath) => {
2548
+ checkBridgeBudget(deps);
2549
+ await fs.link(String(oldPath), String(newPath));
2550
+ };
2551
+ handlers[K.fsSymlink] = async (target, linkPath) => {
2552
+ checkBridgeBudget(deps);
2553
+ await fs.symlink(String(target), String(linkPath));
2554
+ };
2555
+ handlers[K.fsReadlink] = async (path) => {
2556
+ checkBridgeBudget(deps);
2557
+ return fs.readlink(String(path));
2558
+ };
2559
+ handlers[K.fsLstat] = async (path) => {
2560
+ checkBridgeBudget(deps);
2561
+ const s = await fs.lstat(String(path));
2562
+ return JSON.stringify({ mode: s.mode, size: s.size, isDirectory: s.isDirectory,
2563
+ isSymbolicLink: s.isSymbolicLink, atimeMs: s.atimeMs, mtimeMs: s.mtimeMs,
2564
+ ctimeMs: s.ctimeMs, birthtimeMs: s.birthtimeMs });
2565
+ };
2566
+ handlers[K.fsTruncate] = async (path, length) => {
2567
+ checkBridgeBudget(deps);
2568
+ await fs.truncate(String(path), Number(length));
2569
+ };
2570
+ handlers[K.fsUtimes] = async (path, atime, mtime) => {
2571
+ checkBridgeBudget(deps);
2572
+ await fs.utimes(String(path), Number(atime), Number(mtime));
2573
+ };
2574
+ return handlers;
2575
+ }
2576
+ /** Build child process bridge handlers. */
2577
+ export function buildChildProcessBridgeHandlers(deps) {
2578
+ const handlers = {};
2579
+ const K = HOST_BRIDGE_GLOBAL_KEYS;
2580
+ const jsonLimit = deps.isolateJsonPayloadLimitBytes;
2581
+ let nextSessionId = 1;
2582
+ const sessions = deps.activeChildProcesses;
2583
+ const { processTable, parentPid } = deps;
2584
+ // Map sessionId → kernel PID for kernel-registered processes
2585
+ const sessionToPid = new Map();
2586
+ /** Wrap a SpawnedProcess as a kernel DriverProcess (adds callback stubs). */
2587
+ function wrapAsDriverProcess(proc) {
2588
+ return {
2589
+ writeStdin: (data) => proc.writeStdin(data),
2590
+ closeStdin: () => proc.closeStdin(),
2591
+ kill: (signal) => proc.kill(signal),
2592
+ wait: () => proc.wait(),
2593
+ onStdout: null,
2594
+ onStderr: null,
2595
+ onExit: null,
2596
+ };
2597
+ }
2598
+ // Serialize a child process event and push it into the V8 isolate
2599
+ const dispatchEvent = (sessionId, type, data) => {
2600
+ try {
2601
+ const payload = JSON.stringify({ sessionId, type, data: data instanceof Uint8Array ? Buffer.from(data).toString("base64") : data });
2602
+ deps.sendStreamEvent("childProcess", Buffer.from(payload));
2603
+ }
2604
+ catch {
2605
+ // Context may be disposed
2606
+ }
2607
+ };
2608
+ handlers[K.childProcessSpawnStart] = (command, argsJson, optionsJson) => {
2609
+ checkBridgeBudget(deps);
2610
+ if (deps.maxChildProcesses !== undefined && deps.budgetState.childProcesses >= deps.maxChildProcesses) {
2611
+ throw new Error(`${RESOURCE_BUDGET_ERROR_CODE}: maximum child processes exceeded`);
2612
+ }
2613
+ deps.budgetState.childProcesses++;
2614
+ const args = parseJsonWithLimit("child_process.spawn args", String(argsJson), jsonLimit);
2615
+ const options = parseJsonWithLimit("child_process.spawn options", String(optionsJson), jsonLimit);
2616
+ const sessionId = nextSessionId++;
2617
+ const childEnv = stripDangerousEnv(options.env ?? deps.processConfig.env);
2618
+ const proc = deps.commandExecutor.spawn(String(command), args, {
2619
+ cwd: options.cwd,
2620
+ env: childEnv,
2621
+ onStdout: (data) => dispatchEvent(sessionId, "stdout", data),
2622
+ onStderr: (data) => dispatchEvent(sessionId, "stderr", data),
2623
+ });
2624
+ // Register with kernel process table for cross-runtime visibility
2625
+ if (processTable && parentPid !== undefined) {
2626
+ const childPid = processTable.allocatePid();
2627
+ processTable.register(childPid, "node", String(command), args, {
2628
+ pid: childPid,
2629
+ ppid: parentPid,
2630
+ env: childEnv ?? {},
2631
+ cwd: options.cwd ?? deps.processConfig.cwd ?? "/",
2632
+ fds: { stdin: 0, stdout: 1, stderr: 2 },
2633
+ }, wrapAsDriverProcess(proc));
2634
+ sessionToPid.set(sessionId, childPid);
2635
+ }
2636
+ proc.wait().then((code) => {
2637
+ // Mark exited in kernel process table
2638
+ const childPid = sessionToPid.get(sessionId);
2639
+ if (childPid !== undefined && processTable) {
2640
+ try {
2641
+ processTable.markExited(childPid, code);
2642
+ }
2643
+ catch { /* already exited */ }
2644
+ sessionToPid.delete(sessionId);
2645
+ }
2646
+ dispatchEvent(sessionId, "exit", code);
2647
+ sessions.delete(sessionId);
2648
+ });
2649
+ sessions.set(sessionId, proc);
2650
+ return sessionId;
2651
+ };
2652
+ handlers[K.childProcessStdinWrite] = (sessionId, data) => {
2653
+ const d = data instanceof Uint8Array ? data : Buffer.from(String(data), "base64");
2654
+ sessions.get(Number(sessionId))?.writeStdin(d);
2655
+ };
2656
+ handlers[K.childProcessStdinClose] = (sessionId) => {
2657
+ sessions.get(Number(sessionId))?.closeStdin();
2658
+ };
2659
+ handlers[K.childProcessKill] = (sessionId, signal) => {
2660
+ const id = Number(sessionId);
2661
+ // Route through kernel process table when available
2662
+ const childPid = sessionToPid.get(id);
2663
+ if (childPid !== undefined && processTable) {
2664
+ try {
2665
+ processTable.kill(childPid, Number(signal));
2666
+ }
2667
+ catch { /* already dead */ }
2668
+ return;
2669
+ }
2670
+ sessions.get(id)?.kill(Number(signal));
2671
+ };
2672
+ handlers[K.childProcessSpawnSync] = async (command, argsJson, optionsJson) => {
2673
+ checkBridgeBudget(deps);
2674
+ if (deps.maxChildProcesses !== undefined && deps.budgetState.childProcesses >= deps.maxChildProcesses) {
2675
+ throw new Error(`${RESOURCE_BUDGET_ERROR_CODE}: maximum child processes exceeded`);
2676
+ }
2677
+ deps.budgetState.childProcesses++;
2678
+ const args = parseJsonWithLimit("child_process.spawnSync args", String(argsJson), jsonLimit);
2679
+ const options = parseJsonWithLimit("child_process.spawnSync options", String(optionsJson), jsonLimit);
2680
+ const maxBuffer = options.maxBuffer ?? 1024 * 1024;
2681
+ const stdoutChunks = [];
2682
+ const stderrChunks = [];
2683
+ let stdoutBytes = 0;
2684
+ let stderrBytes = 0;
2685
+ let maxBufferExceeded = false;
2686
+ const childEnv = stripDangerousEnv(options.env ?? deps.processConfig.env);
2687
+ const proc = deps.commandExecutor.spawn(String(command), args, {
2688
+ cwd: options.cwd,
2689
+ env: childEnv,
2690
+ onStdout: (data) => {
2691
+ if (maxBufferExceeded)
2692
+ return;
2693
+ stdoutBytes += data.length;
2694
+ if (maxBuffer !== undefined && stdoutBytes > maxBuffer) {
2695
+ maxBufferExceeded = true;
2696
+ proc.kill(15);
2697
+ return;
2698
+ }
2699
+ stdoutChunks.push(data);
2700
+ },
2701
+ onStderr: (data) => {
2702
+ if (maxBufferExceeded)
2703
+ return;
2704
+ stderrBytes += data.length;
2705
+ if (maxBuffer !== undefined && stderrBytes > maxBuffer) {
2706
+ maxBufferExceeded = true;
2707
+ proc.kill(15);
2708
+ return;
2709
+ }
2710
+ stderrChunks.push(data);
2711
+ },
2712
+ });
2713
+ // Register sync child with kernel process table
2714
+ let syncChildPid;
2715
+ if (processTable && parentPid !== undefined) {
2716
+ syncChildPid = processTable.allocatePid();
2717
+ processTable.register(syncChildPid, "node", String(command), args, {
2718
+ pid: syncChildPid,
2719
+ ppid: parentPid,
2720
+ env: childEnv ?? {},
2721
+ cwd: options.cwd ?? deps.processConfig.cwd ?? "/",
2722
+ fds: { stdin: 0, stdout: 1, stderr: 2 },
2723
+ }, wrapAsDriverProcess(proc));
2724
+ }
2725
+ const exitCode = await proc.wait();
2726
+ // Mark exited in kernel
2727
+ if (syncChildPid !== undefined && processTable) {
2728
+ try {
2729
+ processTable.markExited(syncChildPid, exitCode);
2730
+ }
2731
+ catch { /* already exited */ }
2732
+ }
2733
+ const decoder = new TextDecoder();
2734
+ const stdout = stdoutChunks.map((c) => decoder.decode(c)).join("");
2735
+ const stderr = stderrChunks.map((c) => decoder.decode(c)).join("");
2736
+ return JSON.stringify({ stdout, stderr, code: exitCode, maxBufferExceeded });
2737
+ };
2738
+ return handlers;
2739
+ }
2740
+ /** Restrict HTTP server hostname to loopback interfaces. */
2741
+ function normalizeLoopbackHostname(hostname) {
2742
+ if (!hostname || hostname === "localhost")
2743
+ return "127.0.0.1";
2744
+ if (hostname === "127.0.0.1" || hostname === "::1")
2745
+ return hostname;
2746
+ if (hostname === "0.0.0.0" || hostname === "::")
2747
+ return "127.0.0.1";
2748
+ throw new Error(`Sandbox HTTP servers are restricted to loopback interfaces. Received hostname: ${hostname}`);
2749
+ }
2750
+ function debugHttpBridge(...args) {
2751
+ if (process.env.SECURE_EXEC_DEBUG_HTTP_BRIDGE === "1") {
2752
+ console.error("[secure-exec http bridge]", ...args);
2753
+ }
2754
+ }
2755
+ /**
2756
+ * Create a Duplex stream backed by a kernel socket.
2757
+ * Readable side reads from kernel socket readBuffer; writable side writes via send().
2758
+ */
2759
+ function createKernelSocketDuplex(socketId, socketTable, pid) {
2760
+ let readPumpStarted = false;
2761
+ const duplex = new Duplex({
2762
+ read() {
2763
+ if (readPumpStarted)
2764
+ return;
2765
+ readPumpStarted = true;
2766
+ runReadPump();
2767
+ },
2768
+ write(chunk, encoding, callback) {
2769
+ try {
2770
+ const data = typeof chunk === "string"
2771
+ ? Buffer.from(chunk, encoding)
2772
+ : Buffer.isBuffer(chunk)
2773
+ ? chunk
2774
+ : Buffer.from(chunk);
2775
+ debugHttpBridge("socket write", socketId, data.length);
2776
+ socketTable.send(socketId, new Uint8Array(data), 0);
2777
+ callback();
2778
+ }
2779
+ catch (err) {
2780
+ debugHttpBridge("socket write error", socketId, err);
2781
+ callback(err instanceof Error ? err : new Error(String(err)));
2782
+ }
2783
+ },
2784
+ final(callback) {
2785
+ try {
2786
+ socketTable.shutdown(socketId, "write");
2787
+ }
2788
+ catch { /* already closed */ }
2789
+ callback();
2790
+ },
2791
+ destroy(err, callback) {
2792
+ try {
2793
+ socketTable.close(socketId, pid);
2794
+ }
2795
+ catch { /* already closed */ }
2796
+ callback(err);
2797
+ },
2798
+ });
2799
+ // Socket-like properties for Node http module
2800
+ const socket = socketTable.get(socketId);
2801
+ const localAddr = socket?.localAddr;
2802
+ const remoteAddr = socket?.remoteAddr;
2803
+ duplex.remoteAddress =
2804
+ remoteAddr && typeof remoteAddr === "object" && "host" in remoteAddr
2805
+ ? remoteAddr.host
2806
+ : "127.0.0.1";
2807
+ duplex.remotePort =
2808
+ remoteAddr && typeof remoteAddr === "object" && "port" in remoteAddr
2809
+ ? remoteAddr.port
2810
+ : 0;
2811
+ duplex.localAddress =
2812
+ localAddr && typeof localAddr === "object" && "host" in localAddr
2813
+ ? localAddr.host
2814
+ : "127.0.0.1";
2815
+ duplex.localPort =
2816
+ localAddr && typeof localAddr === "object" && "port" in localAddr
2817
+ ? localAddr.port
2818
+ : 0;
2819
+ duplex.encrypted = false;
2820
+ duplex.setNoDelay = () => duplex;
2821
+ duplex.setKeepAlive = () => duplex;
2822
+ duplex.setTimeout = (ms, cb) => {
2823
+ if (cb)
2824
+ duplex.once("timeout", cb);
2825
+ return duplex;
2826
+ };
2827
+ duplex.ref = () => duplex;
2828
+ duplex.unref = () => duplex;
2829
+ async function runReadPump() {
2830
+ try {
2831
+ while (true) {
2832
+ let data;
2833
+ try {
2834
+ data = socketTable.recv(socketId, 65536, 0);
2835
+ }
2836
+ catch {
2837
+ break; // socket closed or error
2838
+ }
2839
+ if (data !== null) {
2840
+ debugHttpBridge("socket read", socketId, data.length);
2841
+ if (!duplex.push(Buffer.from(data))) {
2842
+ // Backpressure — wait for drain before continuing
2843
+ readPumpStarted = false;
2844
+ return;
2845
+ }
2846
+ continue;
2847
+ }
2848
+ // Check for EOF
2849
+ const sock = socketTable.get(socketId);
2850
+ if (!sock)
2851
+ break;
2852
+ if (sock.state === "closed" || sock.state === "read-closed")
2853
+ break;
2854
+ if (sock.peerWriteClosed || (sock.peerId === undefined && !sock.external))
2855
+ break;
2856
+ if (sock.external && sock.readBuffer.length === 0 && sock.peerWriteClosed)
2857
+ break;
2858
+ // Wait for data
2859
+ const handle = sock.readWaiters.enqueue();
2860
+ await handle.wait();
2861
+ }
2862
+ }
2863
+ catch {
2864
+ // Socket destroyed during pump
2865
+ }
2866
+ duplex.push(null); // EOF
2867
+ }
2868
+ return duplex;
2869
+ }
2870
+ /** Build network bridge handlers (fetch, httpRequest, dnsLookup, httpServer). */
2871
+ export function buildNetworkBridgeHandlers(deps) {
2872
+ if (!deps.socketTable || deps.pid === undefined) {
2873
+ throw new Error("buildNetworkBridgeHandlers requires a kernel socketTable and pid");
2874
+ }
2875
+ const handlers = {};
2876
+ const K = HOST_BRIDGE_GLOBAL_KEYS;
2877
+ const adapter = deps.networkAdapter;
2878
+ const jsonLimit = deps.isolateJsonPayloadLimitBytes;
2879
+ const ownedHttpServers = new Set();
2880
+ const ownedHttp2Servers = new Set();
2881
+ const { socketTable, pid } = deps;
2882
+ // Track kernel HTTP servers for cleanup
2883
+ const kernelHttpServers = new Map();
2884
+ const kernelHttp2Servers = new Map();
2885
+ const kernelHttp2ClientSessions = new Map();
2886
+ const http2Sessions = new Map();
2887
+ const http2Streams = new Map();
2888
+ const http2ServerSessionIds = new WeakMap();
2889
+ let nextHttp2SessionId = 1;
2890
+ let nextHttp2StreamId = 1;
2891
+ const kernelUpgradeSockets = new Map();
2892
+ let nextKernelUpgradeSocketId = 1;
2893
+ const loopbackAwareAdapter = adapter;
2894
+ const decodeTlsMaterial = (value) => {
2895
+ if (value === undefined) {
2896
+ return undefined;
2897
+ }
2898
+ const decodeOne = (entry) => entry.kind === "buffer" ? Buffer.from(entry.data, "base64") : entry.data;
2899
+ return Array.isArray(value) ? value.map(decodeOne) : decodeOne(value);
2900
+ };
2901
+ const buildHostTlsOptions = (options) => {
2902
+ if (!options) {
2903
+ return {};
2904
+ }
2905
+ const hostOptions = {};
2906
+ const key = decodeTlsMaterial(options.key);
2907
+ const cert = decodeTlsMaterial(options.cert);
2908
+ const ca = decodeTlsMaterial(options.ca);
2909
+ if (key !== undefined)
2910
+ hostOptions.key = key;
2911
+ if (cert !== undefined)
2912
+ hostOptions.cert = cert;
2913
+ if (ca !== undefined)
2914
+ hostOptions.ca = ca;
2915
+ if (typeof options.passphrase === "string")
2916
+ hostOptions.passphrase = options.passphrase;
2917
+ if (typeof options.ciphers === "string")
2918
+ hostOptions.ciphers = options.ciphers;
2919
+ if (typeof options.session === "string")
2920
+ hostOptions.session = Buffer.from(options.session, "base64");
2921
+ if (Array.isArray(options.ALPNProtocols) && options.ALPNProtocols.length > 0) {
2922
+ hostOptions.ALPNProtocols = [...options.ALPNProtocols];
2923
+ }
2924
+ if (typeof options.minVersion === "string")
2925
+ hostOptions.minVersion = options.minVersion;
2926
+ if (typeof options.maxVersion === "string")
2927
+ hostOptions.maxVersion = options.maxVersion;
2928
+ if (typeof options.servername === "string")
2929
+ hostOptions.servername = options.servername;
2930
+ if (typeof options.requestCert === "boolean")
2931
+ hostOptions.requestCert = options.requestCert;
2932
+ if (typeof options.rejectUnauthorized === "boolean") {
2933
+ hostOptions.rejectUnauthorized = options.rejectUnauthorized;
2934
+ }
2935
+ return hostOptions;
2936
+ };
2937
+ const debugHttp2Bridge = (...args) => {
2938
+ if (process.env.SECURE_EXEC_DEBUG_HTTP2_BRIDGE === "1") {
2939
+ console.error("[secure-exec http2 bridge]", ...args);
2940
+ }
2941
+ };
2942
+ const emitHttp2Event = (...fields) => {
2943
+ const [kind, id, data, extra, extraNumber, extraHeaders, flags] = fields;
2944
+ debugHttp2Bridge("emit", kind, id);
2945
+ deps.sendStreamEvent("http2", Buffer.from(JSON.stringify({
2946
+ kind,
2947
+ id,
2948
+ data,
2949
+ extra,
2950
+ extraNumber,
2951
+ extraHeaders,
2952
+ flags,
2953
+ })));
2954
+ };
2955
+ const serializeHttp2SocketState = (socket) => JSON.stringify({
2956
+ encrypted: socket.encrypted === true,
2957
+ allowHalfOpen: socket.allowHalfOpen === true,
2958
+ localAddress: socket.localAddress,
2959
+ localPort: socket.localPort,
2960
+ localFamily: socket.localAddress?.includes(":") ? "IPv6" : "IPv4",
2961
+ remoteAddress: socket.remoteAddress,
2962
+ remotePort: socket.remotePort,
2963
+ remoteFamily: socket.remoteAddress?.includes(":") ? "IPv6" : "IPv4",
2964
+ servername: typeof socket.servername === "string"
2965
+ ? socket.servername
2966
+ : undefined,
2967
+ alpnProtocol: socket.alpnProtocol || false,
2968
+ });
2969
+ const serializeHttp2SessionState = (session) => JSON.stringify({
2970
+ encrypted: session.encrypted === true,
2971
+ alpnProtocol: session.alpnProtocol || (session.encrypted ? "h2" : "h2c"),
2972
+ originSet: Array.isArray(session.originSet) ? [...session.originSet] : undefined,
2973
+ localSettings: session.localSettings && typeof session.localSettings === "object"
2974
+ ? session.localSettings
2975
+ : undefined,
2976
+ remoteSettings: session.remoteSettings && typeof session.remoteSettings === "object"
2977
+ ? session.remoteSettings
2978
+ : undefined,
2979
+ state: session.state && typeof session.state === "object"
2980
+ ? {
2981
+ effectiveLocalWindowSize: typeof session.state.effectiveLocalWindowSize === "number"
2982
+ ? session.state.effectiveLocalWindowSize
2983
+ : undefined,
2984
+ localWindowSize: typeof session.state.localWindowSize === "number"
2985
+ ? session.state.localWindowSize
2986
+ : undefined,
2987
+ remoteWindowSize: typeof session.state.remoteWindowSize === "number"
2988
+ ? session.state.remoteWindowSize
2989
+ : undefined,
2990
+ nextStreamID: typeof session.state.nextStreamID === "number"
2991
+ ? session.state.nextStreamID
2992
+ : undefined,
2993
+ outboundQueueSize: typeof session.state.outboundQueueSize === "number"
2994
+ ? session.state.outboundQueueSize
2995
+ : undefined,
2996
+ deflateDynamicTableSize: typeof session.state.deflateDynamicTableSize === "number"
2997
+ ? session.state.deflateDynamicTableSize
2998
+ : undefined,
2999
+ inflateDynamicTableSize: typeof session.state.inflateDynamicTableSize === "number"
3000
+ ? session.state.inflateDynamicTableSize
3001
+ : undefined,
3002
+ }
3003
+ : undefined,
3004
+ socket: session.socket ? JSON.parse(serializeHttp2SocketState(session.socket)) : undefined,
3005
+ });
3006
+ // Let host-side runtime.network.fetch/httpRequest reach only the HTTP
3007
+ // listeners owned by this execution.
3008
+ loopbackAwareAdapter.__setLoopbackPortChecker?.((_hostname, port) => {
3009
+ for (const state of kernelHttpServers.values()) {
3010
+ const socket = socketTable.get(state.listenSocketId);
3011
+ const localAddr = socket?.localAddr;
3012
+ if (localAddr && typeof localAddr === "object" && "port" in localAddr) {
3013
+ if (localAddr.port === port) {
3014
+ return true;
3015
+ }
3016
+ }
3017
+ }
3018
+ return false;
3019
+ });
3020
+ const registerKernelUpgradeSocket = (socket) => {
3021
+ const socketId = nextKernelUpgradeSocketId++;
3022
+ kernelUpgradeSockets.set(socketId, socket);
3023
+ socket.on("data", (chunk) => {
3024
+ deps.sendStreamEvent("upgradeSocketData", Buffer.from(JSON.stringify({
3025
+ socketId,
3026
+ dataBase64: Buffer.from(chunk).toString("base64"),
3027
+ })));
3028
+ });
3029
+ socket.on("end", () => {
3030
+ deps.sendStreamEvent("upgradeSocketEnd", Buffer.from(JSON.stringify({ socketId })));
3031
+ });
3032
+ socket.on("close", () => {
3033
+ kernelUpgradeSockets.delete(socketId);
3034
+ });
3035
+ return socketId;
3036
+ };
3037
+ const finalizeKernelServerClose = (serverId, state) => {
3038
+ debugHttpBridge("finalize close check", serverId, state.closeRequested, state.pendingRequests);
3039
+ if (!state.closeRequested || state.pendingRequests > 0) {
3040
+ return;
3041
+ }
3042
+ if (!state.transportClosed) {
3043
+ state.acceptLoopActive = false;
3044
+ state.transportClosed = true;
3045
+ try {
3046
+ socketTable?.close(state.listenSocketId, pid);
3047
+ }
3048
+ catch { /* already closed */ }
3049
+ try {
3050
+ state.httpServer.close();
3051
+ }
3052
+ catch { /* parser server is never bound */ }
3053
+ }
3054
+ debugHttpBridge("finalize close", serverId);
3055
+ state.resolveClosed();
3056
+ kernelHttpServers.delete(serverId);
3057
+ ownedHttpServers.delete(serverId);
3058
+ deps.activeHttpServerIds.delete(serverId);
3059
+ deps.activeHttpServerClosers.delete(serverId);
3060
+ };
3061
+ const closeKernelServer = async (serverId) => {
3062
+ const state = kernelHttpServers.get(serverId);
3063
+ if (!state)
3064
+ return;
3065
+ debugHttpBridge("close requested", serverId, state.pendingRequests);
3066
+ state.closeRequested = true;
3067
+ finalizeKernelServerClose(serverId, state);
3068
+ };
3069
+ handlers[K.networkFetchRaw] = async (url, optionsJson) => {
3070
+ checkBridgeBudget(deps);
3071
+ const options = parseJsonWithLimit("network.fetch options", String(optionsJson), jsonLimit);
3072
+ const result = await adapter.fetch(String(url), options);
3073
+ const json = JSON.stringify(result);
3074
+ assertTextPayloadSize("network.fetch response", json, jsonLimit);
3075
+ return json;
3076
+ };
3077
+ handlers[K.networkDnsLookupRaw] = async (hostname) => {
3078
+ checkBridgeBudget(deps);
3079
+ const result = await adapter.dnsLookup(String(hostname));
3080
+ return JSON.stringify(result);
3081
+ };
3082
+ handlers[K.networkHttpRequestRaw] = async (url, optionsJson) => {
3083
+ checkBridgeBudget(deps);
3084
+ const options = parseJsonWithLimit("network.httpRequest options", String(optionsJson), jsonLimit);
3085
+ const result = await adapter.httpRequest(String(url), options);
3086
+ const json = JSON.stringify(result);
3087
+ assertTextPayloadSize("network.httpRequest response", json, jsonLimit);
3088
+ return json;
3089
+ };
3090
+ handlers[K.networkHttpServerRespondRaw] = (serverId, requestId, responseJson) => {
3091
+ const numericServerId = Number(serverId);
3092
+ debugHttpBridge("respond callback", numericServerId, requestId);
3093
+ resolveHttpServerResponse({
3094
+ serverId: numericServerId,
3095
+ requestId: Number(requestId),
3096
+ responseJson: String(responseJson),
3097
+ });
3098
+ const state = kernelHttpServers.get(numericServerId);
3099
+ if (!state) {
3100
+ return;
3101
+ }
3102
+ state.pendingRequests = Math.max(0, state.pendingRequests - 1);
3103
+ finalizeKernelServerClose(numericServerId, state);
3104
+ };
3105
+ handlers[K.networkHttpServerWaitRaw] = async (serverId) => {
3106
+ const numericServerId = Number(serverId);
3107
+ debugHttpBridge("wait start", numericServerId);
3108
+ const state = kernelHttpServers.get(numericServerId);
3109
+ if (!state) {
3110
+ debugHttpBridge("wait missing", numericServerId);
3111
+ return;
3112
+ }
3113
+ await state.closedPromise;
3114
+ debugHttpBridge("wait resolved", numericServerId);
3115
+ };
3116
+ // HTTP server listen — always route through the kernel socket table
3117
+ handlers[K.networkHttpServerListenRaw] = (optionsJson) => {
3118
+ const options = parseJsonWithLimit("network.httpServer.listen options", String(optionsJson), jsonLimit);
3119
+ deps.pendingHttpServerStarts.count += 1;
3120
+ return (async () => {
3121
+ try {
3122
+ const host = normalizeLoopbackHostname(options.hostname);
3123
+ debugHttpBridge("listen start", options.serverId, host, options.port ?? 0);
3124
+ const listenSocketId = socketTable.create(AF_INET, SOCK_STREAM, 0, pid);
3125
+ await socketTable.bind(listenSocketId, { host, port: options.port ?? 0 });
3126
+ await socketTable.listen(listenSocketId, 128, { external: true });
3127
+ // Get actual bound address (may differ for ephemeral port)
3128
+ const listenSocket = socketTable.get(listenSocketId);
3129
+ const addr = listenSocket?.localAddr;
3130
+ const address = addr ? {
3131
+ address: addr.host,
3132
+ family: addr.host.includes(":") ? "IPv6" : "IPv4",
3133
+ port: addr.port,
3134
+ } : null;
3135
+ // Create local HTTP server for parsing (not bound to any port)
3136
+ const httpServer = http.createServer(async (req, res) => {
3137
+ try {
3138
+ debugHttpBridge("request start", options.serverId, req.method, req.url);
3139
+ const chunks = [];
3140
+ for await (const chunk of req) {
3141
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
3142
+ }
3143
+ const headers = {};
3144
+ Object.entries(req.headers).forEach(([key, value]) => {
3145
+ if (typeof value === "string")
3146
+ headers[key] = value;
3147
+ else if (Array.isArray(value))
3148
+ headers[key] = value[0] ?? "";
3149
+ });
3150
+ if (!headers.host && addr) {
3151
+ headers.host = `${addr.host}:${addr.port}`;
3152
+ }
3153
+ const requestJson = JSON.stringify({
3154
+ method: req.method || "GET",
3155
+ url: req.url || "/",
3156
+ headers,
3157
+ rawHeaders: req.rawHeaders || [],
3158
+ bodyBase64: chunks.length > 0
3159
+ ? Buffer.concat(chunks).toString("base64")
3160
+ : undefined,
3161
+ });
3162
+ const requestId = nextHttpRequestId++;
3163
+ // Send request to sandbox and wait for response
3164
+ const responsePromise = new Promise((resolve) => {
3165
+ registerPendingHttpResponse(options.serverId, requestId, resolve);
3166
+ });
3167
+ state.pendingRequests += 1;
3168
+ deps.sendStreamEvent("http_request", serialize({
3169
+ requestId,
3170
+ serverId: options.serverId,
3171
+ request: requestJson,
3172
+ }));
3173
+ const responseJson = await responsePromise;
3174
+ const response = parseJsonWithLimit("network.httpServer response", responseJson, jsonLimit);
3175
+ for (const informational of response.informational || []) {
3176
+ const rawHeaderLines = informational.rawHeaders && informational.rawHeaders.length > 0
3177
+ ? informational.rawHeaders
3178
+ : (informational.headers || []).flatMap(([key, value]) => [key, value]);
3179
+ const statusText = informational.statusText ||
3180
+ http.STATUS_CODES[informational.status] ||
3181
+ "";
3182
+ const rawFrame = `HTTP/1.1 ${informational.status} ${statusText}\r\n` +
3183
+ rawHeaderLines.reduce((acc, entry, index) => index % 2 === 0
3184
+ ? `${acc}${entry}: ${rawHeaderLines[index + 1] ?? ""}\r\n`
3185
+ : acc, "") +
3186
+ "\r\n";
3187
+ res._writeRaw?.(rawFrame);
3188
+ }
3189
+ res.statusCode = response.status || 200;
3190
+ for (const [key, value] of response.headers || []) {
3191
+ res.setHeader(key, value);
3192
+ }
3193
+ if (response.body !== undefined) {
3194
+ if (response.bodyEncoding === "base64") {
3195
+ debugHttpBridge("response end", options.serverId, response.status, "base64", response.body.length);
3196
+ res.end(Buffer.from(response.body, "base64"));
3197
+ }
3198
+ else {
3199
+ debugHttpBridge("response end", options.serverId, response.status, "utf8", response.body.length);
3200
+ res.end(response.body);
3201
+ }
3202
+ }
3203
+ else {
3204
+ debugHttpBridge("response end", options.serverId, response.status, "empty", 0);
3205
+ res.end();
3206
+ }
3207
+ }
3208
+ catch {
3209
+ debugHttpBridge("request error", options.serverId, req.method, req.url);
3210
+ res.statusCode = 500;
3211
+ res.end("Internal Server Error");
3212
+ }
3213
+ });
3214
+ // Handle HTTP upgrades through kernel sockets
3215
+ httpServer.on("upgrade", (req, socket, head) => {
3216
+ const upgradeHeaders = {};
3217
+ Object.entries(req.headers).forEach(([key, value]) => {
3218
+ if (typeof value === "string")
3219
+ upgradeHeaders[key] = value;
3220
+ else if (Array.isArray(value))
3221
+ upgradeHeaders[key] = value[0] ?? "";
3222
+ });
3223
+ const upgradeSocketId = registerKernelUpgradeSocket(socket);
3224
+ deps.sendStreamEvent("httpServerUpgrade", Buffer.from(JSON.stringify({
3225
+ serverId: options.serverId,
3226
+ request: JSON.stringify({
3227
+ method: req.method || "GET",
3228
+ url: req.url || "/",
3229
+ headers: upgradeHeaders,
3230
+ rawHeaders: req.rawHeaders || [],
3231
+ }),
3232
+ head: head.toString("base64"),
3233
+ socketId: upgradeSocketId,
3234
+ })));
3235
+ });
3236
+ httpServer.on("connect", (req, socket, head) => {
3237
+ const connectHeaders = {};
3238
+ Object.entries(req.headers).forEach(([key, value]) => {
3239
+ if (typeof value === "string")
3240
+ connectHeaders[key] = value;
3241
+ else if (Array.isArray(value))
3242
+ connectHeaders[key] = value[0] ?? "";
3243
+ });
3244
+ const connectSocketId = registerKernelUpgradeSocket(socket);
3245
+ deps.sendStreamEvent("httpServerConnect", Buffer.from(JSON.stringify({
3246
+ serverId: options.serverId,
3247
+ request: JSON.stringify({
3248
+ method: req.method || "CONNECT",
3249
+ url: req.url || "/",
3250
+ headers: connectHeaders,
3251
+ rawHeaders: req.rawHeaders || [],
3252
+ }),
3253
+ head: head.toString("base64"),
3254
+ socketId: connectSocketId,
3255
+ })));
3256
+ });
3257
+ let resolveClosed;
3258
+ const closedPromise = new Promise((resolve) => {
3259
+ resolveClosed = resolve;
3260
+ });
3261
+ const state = {
3262
+ listenSocketId,
3263
+ httpServer,
3264
+ acceptLoopActive: true,
3265
+ closedPromise,
3266
+ resolveClosed,
3267
+ pendingRequests: 0,
3268
+ closeRequested: false,
3269
+ transportClosed: false,
3270
+ };
3271
+ debugHttpBridge("listen ready", options.serverId, address);
3272
+ kernelHttpServers.set(options.serverId, state);
3273
+ ownedHttpServers.add(options.serverId);
3274
+ deps.activeHttpServerIds.add(options.serverId);
3275
+ deps.activeHttpServerClosers.set(options.serverId, () => closeKernelServer(options.serverId));
3276
+ // Start accept loop (fire-and-forget)
3277
+ void startKernelHttpAcceptLoop(state, socketTable, pid);
3278
+ return JSON.stringify({ address });
3279
+ }
3280
+ finally {
3281
+ deps.pendingHttpServerStarts.count -= 1;
3282
+ }
3283
+ })();
3284
+ };
3285
+ // HTTP server close — kernel-owned servers only
3286
+ handlers[K.networkHttpServerCloseRaw] = (serverId) => {
3287
+ const id = Number(serverId);
3288
+ debugHttpBridge("close bridge call", id);
3289
+ if (!ownedHttpServers.has(id)) {
3290
+ throw new Error(`Cannot close server ${id}: not owned by this execution context`);
3291
+ }
3292
+ const kernelState = kernelHttpServers.get(id);
3293
+ if (!kernelState) {
3294
+ throw new Error(`Cannot close server ${id}: kernel server state missing`);
3295
+ }
3296
+ return closeKernelServer(id);
3297
+ };
3298
+ const closeKernelHttp2Server = async (serverId) => {
3299
+ const state = kernelHttp2Servers.get(serverId);
3300
+ if (!state) {
3301
+ return;
3302
+ }
3303
+ state.acceptLoopActive = false;
3304
+ try {
3305
+ socketTable.close(state.listenSocketId, pid);
3306
+ }
3307
+ catch {
3308
+ // Listener already closed.
3309
+ }
3310
+ for (const session of [...state.sessions]) {
3311
+ try {
3312
+ session.close();
3313
+ }
3314
+ catch {
3315
+ // Ignore already-closing sessions.
3316
+ }
3317
+ }
3318
+ await new Promise((resolve) => {
3319
+ try {
3320
+ state.server.close(() => resolve());
3321
+ }
3322
+ catch {
3323
+ resolve();
3324
+ }
3325
+ });
3326
+ kernelHttp2Servers.delete(serverId);
3327
+ ownedHttp2Servers.delete(serverId);
3328
+ deps.activeHttpServerIds.delete(serverId);
3329
+ deps.activeHttpServerClosers.delete(serverId);
3330
+ state.resolveClosed();
3331
+ };
3332
+ const startKernelHttp2AcceptLoop = async (state) => {
3333
+ try {
3334
+ while (state.acceptLoopActive) {
3335
+ const listenSocket = socketTable.get(state.listenSocketId);
3336
+ if (!listenSocket || listenSocket.state !== "listening") {
3337
+ break;
3338
+ }
3339
+ const acceptedId = socketTable.accept(state.listenSocketId);
3340
+ if (acceptedId !== null) {
3341
+ const duplex = createKernelSocketDuplex(acceptedId, socketTable, pid);
3342
+ state.server.emit("connection", duplex);
3343
+ continue;
3344
+ }
3345
+ const handle = listenSocket.acceptWaiters.enqueue();
3346
+ const acceptedAfterEnqueue = socketTable.accept(state.listenSocketId);
3347
+ if (acceptedAfterEnqueue !== null) {
3348
+ handle.wake();
3349
+ const duplex = createKernelSocketDuplex(acceptedAfterEnqueue, socketTable, pid);
3350
+ state.server.emit("connection", duplex);
3351
+ continue;
3352
+ }
3353
+ await handle.wait();
3354
+ }
3355
+ }
3356
+ catch {
3357
+ // Listener closed.
3358
+ }
3359
+ };
3360
+ const normalizeHttp2EventHeaders = (headers) => {
3361
+ const normalizedHeaders = {};
3362
+ for (const [key, value] of Object.entries(headers)) {
3363
+ if (value !== undefined) {
3364
+ normalizedHeaders[key] = value;
3365
+ }
3366
+ }
3367
+ return normalizedHeaders;
3368
+ };
3369
+ const emitHttp2SerializedError = (kind, id, error) => {
3370
+ const err = error instanceof Error ? error : new Error(String(error));
3371
+ emitHttp2Event(kind, id, JSON.stringify({
3372
+ message: err.message,
3373
+ name: err.name,
3374
+ code: err.code,
3375
+ }));
3376
+ };
3377
+ const attachHttp2ClientStreamListeners = (streamId, stream) => {
3378
+ stream.on("response", (headers) => {
3379
+ emitHttp2Event("clientResponseHeaders", streamId, JSON.stringify(normalizeHttp2EventHeaders(headers)));
3380
+ });
3381
+ stream.on("push", (headers, flags) => {
3382
+ setImmediate(() => {
3383
+ emitHttp2Event("clientPushHeaders", streamId, JSON.stringify(normalizeHttp2EventHeaders(headers)), undefined, String(flags ?? 0));
3384
+ });
3385
+ });
3386
+ stream.on("data", (chunk) => {
3387
+ emitHttp2Event("clientData", streamId, (Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)).toString("base64"));
3388
+ });
3389
+ stream.on("end", () => {
3390
+ debugHttp2Bridge("client response end", streamId);
3391
+ setImmediate(() => {
3392
+ emitHttp2Event("clientEnd", streamId);
3393
+ });
3394
+ });
3395
+ stream.on("close", () => {
3396
+ setImmediate(() => {
3397
+ emitHttp2Event("clientClose", streamId, undefined, undefined, String(stream.rstCode ?? 0));
3398
+ http2Streams.delete(streamId);
3399
+ });
3400
+ });
3401
+ stream.on("error", (error) => {
3402
+ emitHttp2SerializedError("clientError", streamId, error);
3403
+ });
3404
+ stream.resume();
3405
+ };
3406
+ const attachHttp2SessionListeners = (sessionId, session, onClose) => {
3407
+ session.on("close", () => {
3408
+ debugHttp2Bridge("session close", sessionId);
3409
+ emitHttp2Event("sessionClose", sessionId);
3410
+ http2Sessions.delete(sessionId);
3411
+ onClose?.();
3412
+ });
3413
+ session.on("error", (error) => {
3414
+ debugHttp2Bridge("session error", sessionId, error instanceof Error ? error.message : String(error));
3415
+ emitHttp2SerializedError("sessionError", sessionId, error);
3416
+ });
3417
+ session.on("localSettings", (settings) => {
3418
+ emitHttp2Event("sessionLocalSettings", sessionId, JSON.stringify(settings));
3419
+ });
3420
+ session.on("remoteSettings", (settings) => {
3421
+ emitHttp2Event("sessionRemoteSettings", sessionId, JSON.stringify(settings));
3422
+ });
3423
+ session.on("goaway", (errorCode, lastStreamID, opaqueData) => {
3424
+ emitHttp2Event("sessionGoaway", sessionId, Buffer.isBuffer(opaqueData) ? opaqueData.toString("base64") : undefined, undefined, String(errorCode), undefined, String(lastStreamID));
3425
+ });
3426
+ };
3427
+ handlers[K.networkHttp2ServerListenRaw] = (optionsJson) => {
3428
+ const options = parseJsonWithLimit("network.http2Server.listen options", String(optionsJson), jsonLimit);
3429
+ return (async () => {
3430
+ debugHttp2Bridge("server listen start", options.serverId, options.secure, options.host, options.port);
3431
+ const host = normalizeLoopbackHostname(options.host);
3432
+ const listenSocketId = socketTable.create(AF_INET, SOCK_STREAM, 0, pid);
3433
+ await socketTable.bind(listenSocketId, { host, port: options.port ?? 0 });
3434
+ await socketTable.listen(listenSocketId, options.backlog ?? 128, { external: true });
3435
+ const listenSocket = socketTable.get(listenSocketId);
3436
+ const addr = listenSocket?.localAddr;
3437
+ const address = addr ? {
3438
+ address: addr.host,
3439
+ family: addr.host.includes(":") ? "IPv6" : "IPv4",
3440
+ port: addr.port,
3441
+ } : null;
3442
+ const server = options.secure
3443
+ ? http2.createSecureServer({
3444
+ allowHTTP1: options.allowHTTP1 === true,
3445
+ settings: options.settings,
3446
+ remoteCustomSettings: options.remoteCustomSettings,
3447
+ ...buildHostTlsOptions(options.tls),
3448
+ })
3449
+ : http2.createServer({
3450
+ allowHTTP1: options.allowHTTP1 === true,
3451
+ settings: options.settings,
3452
+ remoteCustomSettings: options.remoteCustomSettings,
3453
+ });
3454
+ if (typeof options.timeout === "number" && options.timeout > 0) {
3455
+ server.setTimeout(options.timeout);
3456
+ }
3457
+ server.on("timeout", () => {
3458
+ emitHttp2Event("serverTimeout", options.serverId);
3459
+ });
3460
+ server.on("connection", (socket) => {
3461
+ emitHttp2Event("serverConnection", options.serverId, serializeHttp2SocketState(socket));
3462
+ });
3463
+ if (options.secure) {
3464
+ server.on("secureConnection", (socket) => {
3465
+ emitHttp2Event("serverSecureConnection", options.serverId, serializeHttp2SocketState(socket));
3466
+ });
3467
+ }
3468
+ server.on("request", (req, res) => {
3469
+ if (req.httpVersionMajor === 2) {
3470
+ return;
3471
+ }
3472
+ void (async () => {
3473
+ const chunks = [];
3474
+ for await (const chunk of req) {
3475
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
3476
+ }
3477
+ const headers = {};
3478
+ Object.entries(req.headers).forEach(([key, value]) => {
3479
+ if (typeof value === "string")
3480
+ headers[key] = value;
3481
+ else if (Array.isArray(value))
3482
+ headers[key] = value[0] ?? "";
3483
+ });
3484
+ const requestJson = JSON.stringify({
3485
+ method: req.method || "GET",
3486
+ url: req.url || "/",
3487
+ headers,
3488
+ rawHeaders: req.rawHeaders || [],
3489
+ bodyBase64: chunks.length > 0 ? Buffer.concat(chunks).toString("base64") : undefined,
3490
+ });
3491
+ const requestId = nextHttp2CompatRequestId++;
3492
+ const responsePromise = new Promise((resolve) => {
3493
+ registerPendingHttp2CompatResponse(options.serverId, requestId, resolve);
3494
+ });
3495
+ emitHttp2Event("serverCompatRequest", options.serverId, requestJson, undefined, String(requestId));
3496
+ const responseJson = await responsePromise;
3497
+ const response = parseJsonWithLimit("network.http2Server.compat response", responseJson, jsonLimit);
3498
+ res.statusCode = response.status || 200;
3499
+ for (const [key, value] of response.headers || []) {
3500
+ res.setHeader(key, value);
3501
+ }
3502
+ if (response.bodyEncoding === "base64" && typeof response.body === "string") {
3503
+ res.end(Buffer.from(response.body, "base64"));
3504
+ }
3505
+ else if (typeof response.body === "string") {
3506
+ res.end(response.body);
3507
+ }
3508
+ else {
3509
+ res.end();
3510
+ }
3511
+ })().catch((error) => {
3512
+ try {
3513
+ res.statusCode = 500;
3514
+ res.end(error instanceof Error ? error.message : String(error));
3515
+ }
3516
+ catch {
3517
+ // Response already closed.
3518
+ }
3519
+ });
3520
+ });
3521
+ server.on("stream", (stream, headers, flags) => {
3522
+ debugHttp2Bridge("server stream", options.serverId, flags);
3523
+ const streamSession = stream.session;
3524
+ if (!streamSession) {
3525
+ return;
3526
+ }
3527
+ let sessionId = http2ServerSessionIds.get(streamSession);
3528
+ if (sessionId === undefined) {
3529
+ sessionId = nextHttp2SessionId++;
3530
+ http2ServerSessionIds.set(streamSession, sessionId);
3531
+ http2Sessions.set(sessionId, streamSession);
3532
+ attachHttp2SessionListeners(sessionId, streamSession);
3533
+ emitHttp2Event("serverSession", options.serverId, serializeHttp2SessionState(streamSession), undefined, String(sessionId));
3534
+ }
3535
+ const streamId = nextHttp2StreamId++;
3536
+ http2Streams.set(streamId, stream);
3537
+ stream.pause();
3538
+ stream.on("data", (chunk) => {
3539
+ emitHttp2Event("serverStreamData", streamId, (Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)).toString("base64"));
3540
+ });
3541
+ stream.on("end", () => {
3542
+ emitHttp2Event("serverStreamEnd", streamId);
3543
+ });
3544
+ stream.on("drain", () => {
3545
+ emitHttp2Event("serverStreamDrain", streamId);
3546
+ });
3547
+ stream.on("error", (error) => {
3548
+ emitHttp2SerializedError("serverStreamError", streamId, error);
3549
+ });
3550
+ stream.on("close", () => {
3551
+ emitHttp2Event("serverStreamClose", streamId, undefined, undefined, String(stream.rstCode ?? 0));
3552
+ http2Streams.delete(streamId);
3553
+ });
3554
+ emitHttp2Event("serverStream", options.serverId, String(streamId), serializeHttp2SessionState(streamSession), String(sessionId), JSON.stringify(normalizeHttp2EventHeaders(headers)), String(flags ?? 0));
3555
+ });
3556
+ server.on("close", () => {
3557
+ debugHttp2Bridge("server close", options.serverId);
3558
+ emitHttp2Event("serverClose", options.serverId);
3559
+ });
3560
+ let resolveClosed;
3561
+ const closedPromise = new Promise((resolve) => {
3562
+ resolveClosed = resolve;
3563
+ });
3564
+ const state = {
3565
+ listenSocketId,
3566
+ server,
3567
+ sessions: new Set(),
3568
+ acceptLoopActive: true,
3569
+ closedPromise,
3570
+ resolveClosed,
3571
+ };
3572
+ server.on("session", (session) => {
3573
+ state.sessions.add(session);
3574
+ session.once("close", () => {
3575
+ state.sessions.delete(session);
3576
+ });
3577
+ });
3578
+ kernelHttp2Servers.set(options.serverId, state);
3579
+ ownedHttp2Servers.add(options.serverId);
3580
+ deps.activeHttpServerIds.add(options.serverId);
3581
+ deps.activeHttpServerClosers.set(options.serverId, () => closeKernelHttp2Server(options.serverId));
3582
+ void startKernelHttp2AcceptLoop(state);
3583
+ return JSON.stringify({ address });
3584
+ })();
3585
+ };
3586
+ handlers[K.networkHttp2ServerCloseRaw] = (serverId) => {
3587
+ const id = Number(serverId);
3588
+ if (!ownedHttp2Servers.has(id)) {
3589
+ throw new Error(`Cannot close HTTP/2 server ${id}: not owned by this execution context`);
3590
+ }
3591
+ return closeKernelHttp2Server(id);
3592
+ };
3593
+ handlers[K.networkHttp2ServerWaitRaw] = (serverId) => {
3594
+ const state = kernelHttp2Servers.get(Number(serverId));
3595
+ return state?.closedPromise ?? Promise.resolve();
3596
+ };
3597
+ handlers[K.networkHttp2SessionConnectRaw] = (optionsJson) => {
3598
+ const options = parseJsonWithLimit("network.http2Session.connect options", String(optionsJson), jsonLimit);
3599
+ return (async () => {
3600
+ const authority = String(options.authority);
3601
+ debugHttp2Bridge("session connect start", authority, options.socketId ?? null);
3602
+ const sessionId = nextHttp2SessionId++;
3603
+ let transport;
3604
+ if (typeof options.socketId === "number") {
3605
+ transport = createKernelSocketDuplex(options.socketId, socketTable, pid);
3606
+ }
3607
+ else {
3608
+ const host = String(options.host ?? "127.0.0.1");
3609
+ const port = Number(options.port ?? 0);
3610
+ const socketId = socketTable.create(host.includes(":") ? AF_INET6 : AF_INET, SOCK_STREAM, 0, pid);
3611
+ if (typeof options.localAddress === "string" && options.localAddress.length > 0) {
3612
+ await socketTable.bind(socketId, {
3613
+ host: options.localAddress,
3614
+ port: 0,
3615
+ });
3616
+ }
3617
+ await socketTable.connect(socketId, { host, port });
3618
+ transport = createKernelSocketDuplex(socketId, socketTable, pid);
3619
+ }
3620
+ const session = http2.connect(authority, {
3621
+ settings: options.settings,
3622
+ remoteCustomSettings: options.remoteCustomSettings,
3623
+ createConnection: () => {
3624
+ debugHttp2Bridge("createConnection", authority, options.protocol);
3625
+ if (options.protocol === "https:") {
3626
+ return tls.connect({
3627
+ socket: transport,
3628
+ ALPNProtocols: ["h2"],
3629
+ servername: typeof options.tls?.servername === "string" && options.tls.servername.length > 0
3630
+ ? options.tls.servername
3631
+ : undefined,
3632
+ ...buildHostTlsOptions(options.tls),
3633
+ });
3634
+ }
3635
+ return transport;
3636
+ },
3637
+ });
3638
+ let resolveClosed;
3639
+ const closedPromise = new Promise((resolve) => {
3640
+ resolveClosed = resolve;
3641
+ });
3642
+ http2Sessions.set(sessionId, session);
3643
+ kernelHttp2ClientSessions.set(sessionId, {
3644
+ session,
3645
+ closedPromise,
3646
+ resolveClosed,
3647
+ });
3648
+ session.on("connect", () => {
3649
+ debugHttp2Bridge("session connect", sessionId, authority);
3650
+ emitHttp2Event("sessionConnect", sessionId, serializeHttp2SessionState(session));
3651
+ });
3652
+ attachHttp2SessionListeners(sessionId, session, () => {
3653
+ kernelHttp2ClientSessions.get(sessionId)?.resolveClosed();
3654
+ kernelHttp2ClientSessions.delete(sessionId);
3655
+ });
3656
+ session.on("stream", (stream, headers, flags) => {
3657
+ const streamId = nextHttp2StreamId++;
3658
+ http2Streams.set(streamId, stream);
3659
+ attachHttp2ClientStreamListeners(streamId, stream);
3660
+ emitHttp2Event("clientPushStream", sessionId, String(streamId), undefined, undefined, JSON.stringify(normalizeHttp2EventHeaders(headers)), String(flags ?? 0));
3661
+ });
3662
+ return JSON.stringify({
3663
+ sessionId,
3664
+ state: serializeHttp2SessionState(session),
3665
+ });
3666
+ })();
3667
+ };
3668
+ handlers[K.networkHttp2SessionRequestRaw] = (sessionId, headersJson, optionsJson) => {
3669
+ const session = http2Sessions.get(Number(sessionId));
3670
+ if (!session) {
3671
+ throw new Error(`HTTP/2 session ${String(sessionId)} not found`);
3672
+ }
3673
+ const headers = parseJsonWithLimit("network.http2Session.request headers", String(headersJson), jsonLimit);
3674
+ const requestOptions = parseJsonWithLimit("network.http2Session.request options", String(optionsJson), jsonLimit);
3675
+ const stream = session.request(headers, requestOptions);
3676
+ debugHttp2Bridge("session request", sessionId, stream.id);
3677
+ const streamId = nextHttp2StreamId++;
3678
+ http2Streams.set(streamId, stream);
3679
+ attachHttp2ClientStreamListeners(streamId, stream);
3680
+ return streamId;
3681
+ };
3682
+ handlers[K.networkHttp2SessionCloseRaw] = (sessionId) => {
3683
+ http2Sessions.get(Number(sessionId))?.close();
3684
+ };
3685
+ handlers[K.networkHttp2SessionSettingsRaw] = (sessionId, settingsJson) => {
3686
+ const session = http2Sessions.get(Number(sessionId));
3687
+ if (!session) {
3688
+ throw new Error(`HTTP/2 session ${String(sessionId)} not found`);
3689
+ }
3690
+ const settings = parseJsonWithLimit("network.http2Session.settings settings", String(settingsJson), jsonLimit);
3691
+ session.settings(settings, () => {
3692
+ emitHttp2Event("sessionSettingsAck", Number(sessionId));
3693
+ });
3694
+ };
3695
+ handlers[K.networkHttp2SessionSetLocalWindowSizeRaw] = (sessionId, windowSize) => {
3696
+ const session = http2Sessions.get(Number(sessionId));
3697
+ if (!session) {
3698
+ throw new Error(`HTTP/2 session ${String(sessionId)} not found`);
3699
+ }
3700
+ session.setLocalWindowSize(Number(windowSize));
3701
+ return serializeHttp2SessionState(session);
3702
+ };
3703
+ handlers[K.networkHttp2SessionGoawayRaw] = (sessionId, errorCode, lastStreamID, opaqueDataBase64) => {
3704
+ const session = http2Sessions.get(Number(sessionId));
3705
+ if (!session) {
3706
+ throw new Error(`HTTP/2 session ${String(sessionId)} not found`);
3707
+ }
3708
+ session.goaway(Number(errorCode), Number(lastStreamID), typeof opaqueDataBase64 === "string" && opaqueDataBase64.length > 0
3709
+ ? Buffer.from(opaqueDataBase64, "base64")
3710
+ : undefined);
3711
+ };
3712
+ handlers[K.networkHttp2SessionDestroyRaw] = (sessionId) => {
3713
+ http2Sessions.get(Number(sessionId))?.destroy();
3714
+ };
3715
+ handlers[K.networkHttp2SessionWaitRaw] = (sessionId) => {
3716
+ const state = kernelHttp2ClientSessions.get(Number(sessionId));
3717
+ return state?.closedPromise ?? Promise.resolve();
3718
+ };
3719
+ handlers[K.networkHttp2StreamRespondRaw] = (streamId, headersJson) => {
3720
+ const stream = http2Streams.get(Number(streamId));
3721
+ if (!stream) {
3722
+ throw new Error(`HTTP/2 stream ${String(streamId)} not found`);
3723
+ }
3724
+ const headers = parseJsonWithLimit("network.http2Stream.respond headers", String(headersJson), jsonLimit);
3725
+ stream.respond(headers);
3726
+ };
3727
+ handlers[K.networkHttp2StreamPushStreamRaw] = async (streamId, headersJson, optionsJson) => {
3728
+ const stream = http2Streams.get(Number(streamId));
3729
+ if (!stream) {
3730
+ throw new Error(`HTTP/2 stream ${String(streamId)} not found`);
3731
+ }
3732
+ const headers = parseJsonWithLimit("network.http2Stream.pushStream headers", String(headersJson), jsonLimit);
3733
+ const options = parseJsonWithLimit("network.http2Stream.pushStream options", String(optionsJson), jsonLimit);
3734
+ return await new Promise((resolve, reject) => {
3735
+ try {
3736
+ stream.pushStream(headers, options, (error, pushStream, pushHeaders) => {
3737
+ if (error) {
3738
+ resolve(JSON.stringify({
3739
+ error: JSON.stringify({
3740
+ message: error.message,
3741
+ name: error.name,
3742
+ code: error.code,
3743
+ }),
3744
+ }));
3745
+ return;
3746
+ }
3747
+ if (!pushStream) {
3748
+ reject(new Error("HTTP/2 push stream callback returned no stream"));
3749
+ return;
3750
+ }
3751
+ const pushStreamId = nextHttp2StreamId++;
3752
+ http2Streams.set(pushStreamId, pushStream);
3753
+ pushStream.on("close", () => {
3754
+ http2Streams.delete(pushStreamId);
3755
+ });
3756
+ resolve(JSON.stringify({
3757
+ streamId: pushStreamId,
3758
+ headers: JSON.stringify(normalizeHttp2EventHeaders(pushHeaders ?? {})),
3759
+ }));
3760
+ });
3761
+ }
3762
+ catch (error) {
3763
+ reject(error);
3764
+ }
3765
+ });
3766
+ };
3767
+ handlers[K.networkHttp2StreamWriteRaw] = (streamId, dataBase64) => {
3768
+ const stream = http2Streams.get(Number(streamId));
3769
+ if (!stream) {
3770
+ throw new Error(`HTTP/2 stream ${String(streamId)} not found`);
3771
+ }
3772
+ return stream.write(Buffer.from(String(dataBase64), "base64"));
3773
+ };
3774
+ handlers[K.networkHttp2StreamEndRaw] = (streamId, dataBase64) => {
3775
+ const stream = http2Streams.get(Number(streamId));
3776
+ if (!stream) {
3777
+ throw new Error(`HTTP/2 stream ${String(streamId)} not found`);
3778
+ }
3779
+ if (typeof dataBase64 === "string" && dataBase64.length > 0) {
3780
+ stream.end(Buffer.from(dataBase64, "base64"));
3781
+ return;
3782
+ }
3783
+ stream.end();
3784
+ };
3785
+ handlers[K.networkHttp2StreamPauseRaw] = (streamId) => {
3786
+ http2Streams.get(Number(streamId))?.pause();
3787
+ };
3788
+ handlers[K.networkHttp2StreamResumeRaw] = (streamId) => {
3789
+ http2Streams.get(Number(streamId))?.resume();
3790
+ };
3791
+ handlers[K.networkHttp2StreamRespondWithFileRaw] = (streamId, filePath, headersJson, optionsJson) => {
3792
+ const stream = http2Streams.get(Number(streamId));
3793
+ if (!stream) {
3794
+ throw new Error(`HTTP/2 stream ${String(streamId)} not found`);
3795
+ }
3796
+ const headers = parseJsonWithLimit("network.http2Stream.respondWithFile headers", String(headersJson), jsonLimit);
3797
+ const options = parseJsonWithLimit("network.http2Stream.respondWithFile options", String(optionsJson), jsonLimit);
3798
+ stream.respondWithFile(String(filePath), headers, options);
3799
+ };
3800
+ handlers[K.networkHttp2ServerRespondRaw] = (serverId, requestId, responseJson) => {
3801
+ resolveHttp2CompatResponse({
3802
+ serverId: Number(serverId),
3803
+ requestId: Number(requestId),
3804
+ responseJson: String(responseJson),
3805
+ });
3806
+ };
3807
+ handlers[K.upgradeSocketWriteRaw] = (socketId, dataBase64) => {
3808
+ const id = Number(socketId);
3809
+ const socket = kernelUpgradeSockets.get(id);
3810
+ if (socket) {
3811
+ socket.write(Buffer.from(String(dataBase64), "base64"));
3812
+ return;
3813
+ }
3814
+ adapter.upgradeSocketWrite?.(id, String(dataBase64));
3815
+ };
3816
+ handlers[K.upgradeSocketEndRaw] = (socketId) => {
3817
+ const id = Number(socketId);
3818
+ const socket = kernelUpgradeSockets.get(id);
3819
+ if (socket) {
3820
+ socket.end();
3821
+ return;
3822
+ }
3823
+ adapter.upgradeSocketEnd?.(id);
3824
+ };
3825
+ handlers[K.upgradeSocketDestroyRaw] = (socketId) => {
3826
+ const id = Number(socketId);
3827
+ const socket = kernelUpgradeSockets.get(id);
3828
+ if (socket) {
3829
+ kernelUpgradeSockets.delete(id);
3830
+ socket.destroy();
3831
+ return;
3832
+ }
3833
+ adapter.upgradeSocketDestroy?.(id);
3834
+ };
3835
+ // Register upgrade socket callbacks for httpRequest client-side upgrades
3836
+ adapter.setUpgradeSocketCallbacks?.({
3837
+ onData: (socketId, dataBase64) => {
3838
+ deps.sendStreamEvent("upgradeSocketData", Buffer.from(JSON.stringify({ socketId, dataBase64 })));
3839
+ },
3840
+ onEnd: (socketId) => {
3841
+ deps.sendStreamEvent("upgradeSocketEnd", Buffer.from(JSON.stringify({ socketId })));
3842
+ },
3843
+ });
3844
+ // Dispose: close all kernel HTTP servers
3845
+ const dispose = async () => {
3846
+ for (const serverId of Array.from(kernelHttpServers.keys())) {
3847
+ await closeKernelServer(serverId);
3848
+ }
3849
+ for (const serverId of Array.from(kernelHttp2Servers.keys())) {
3850
+ await closeKernelHttp2Server(serverId);
3851
+ }
3852
+ for (const session of http2Sessions.values()) {
3853
+ try {
3854
+ session.destroy();
3855
+ }
3856
+ catch {
3857
+ // Session already closed.
3858
+ }
3859
+ }
3860
+ kernelHttp2ClientSessions.clear();
3861
+ http2Sessions.clear();
3862
+ http2Streams.clear();
3863
+ for (const socket of kernelUpgradeSockets.values()) {
3864
+ socket.destroy();
3865
+ }
3866
+ kernelUpgradeSockets.clear();
3867
+ };
3868
+ return { handlers, dispose };
3869
+ }
3870
+ /** Accept loop: dequeue connections from kernel listener and feed to http.Server. */
3871
+ async function startKernelHttpAcceptLoop(state, socketTable, pid) {
3872
+ try {
3873
+ while (state.acceptLoopActive) {
3874
+ const listenSocket = socketTable.get(state.listenSocketId);
3875
+ if (!listenSocket || listenSocket.state !== "listening")
3876
+ break;
3877
+ const acceptedId = socketTable.accept(state.listenSocketId);
3878
+ if (acceptedId !== null) {
3879
+ debugHttpBridge("accept backlog", state.listenSocketId, acceptedId);
3880
+ // Wrap kernel socket in Duplex and hand off to http.Server
3881
+ const duplex = createKernelSocketDuplex(acceptedId, socketTable, pid);
3882
+ state.httpServer.emit("connection", duplex);
3883
+ continue;
3884
+ }
3885
+ // Avoid a lost wake-up if a connection arrives between accept() and enqueue().
3886
+ const handle = listenSocket.acceptWaiters.enqueue();
3887
+ const acceptedAfterEnqueue = socketTable.accept(state.listenSocketId);
3888
+ if (acceptedAfterEnqueue !== null) {
3889
+ handle.wake();
3890
+ debugHttpBridge("accept after enqueue", state.listenSocketId, acceptedAfterEnqueue);
3891
+ const duplex = createKernelSocketDuplex(acceptedAfterEnqueue, socketTable, pid);
3892
+ state.httpServer.emit("connection", duplex);
3893
+ continue;
3894
+ }
3895
+ // No pending connections — wait for accept waker
3896
+ await handle.wait();
3897
+ }
3898
+ }
3899
+ catch {
3900
+ // Listener closed — expected
3901
+ }
3902
+ }
3903
+ // Track request IDs directly, but also keep per-server FIFO queues so older
3904
+ // callbacks that only report serverId still resolve the correct pending waiters.
3905
+ const pendingHttpResponses = new Map();
3906
+ const pendingHttpResponsesByServer = new Map();
3907
+ let nextHttpRequestId = 1;
3908
+ const pendingHttp2CompatResponses = new Map();
3909
+ const pendingHttp2CompatResponsesByServer = new Map();
3910
+ let nextHttp2CompatRequestId = 1;
3911
+ function registerPendingHttpResponse(serverId, requestId, resolve) {
3912
+ pendingHttpResponses.set(requestId, { serverId, resolve });
3913
+ const queue = pendingHttpResponsesByServer.get(serverId);
3914
+ if (queue) {
3915
+ queue.push(requestId);
3916
+ }
3917
+ else {
3918
+ pendingHttpResponsesByServer.set(serverId, [requestId]);
3919
+ }
3920
+ }
3921
+ function removePendingHttpResponse(serverId, requestId) {
3922
+ const pending = pendingHttpResponses.get(requestId);
3923
+ if (!pending)
3924
+ return undefined;
3925
+ pendingHttpResponses.delete(requestId);
3926
+ const queue = pendingHttpResponsesByServer.get(serverId);
3927
+ if (queue) {
3928
+ const index = queue.indexOf(requestId);
3929
+ if (index !== -1)
3930
+ queue.splice(index, 1);
3931
+ if (queue.length === 0)
3932
+ pendingHttpResponsesByServer.delete(serverId);
3933
+ }
3934
+ return pending;
3935
+ }
3936
+ function takePendingHttpResponseByServer(serverId) {
3937
+ const queue = pendingHttpResponsesByServer.get(serverId);
3938
+ if (!queue || queue.length === 0)
3939
+ return undefined;
3940
+ const requestId = queue.shift();
3941
+ if (queue.length === 0)
3942
+ pendingHttpResponsesByServer.delete(serverId);
3943
+ const pending = pendingHttpResponses.get(requestId);
3944
+ if (pending) {
3945
+ pendingHttpResponses.delete(requestId);
3946
+ }
3947
+ return pending;
3948
+ }
3949
+ function registerPendingHttp2CompatResponse(serverId, requestId, resolve) {
3950
+ pendingHttp2CompatResponses.set(requestId, { serverId, resolve });
3951
+ const queue = pendingHttp2CompatResponsesByServer.get(serverId);
3952
+ if (queue) {
3953
+ queue.push(requestId);
3954
+ }
3955
+ else {
3956
+ pendingHttp2CompatResponsesByServer.set(serverId, [requestId]);
3957
+ }
3958
+ }
3959
+ function removePendingHttp2CompatResponse(serverId, requestId) {
3960
+ const pending = pendingHttp2CompatResponses.get(requestId);
3961
+ if (!pending)
3962
+ return undefined;
3963
+ pendingHttp2CompatResponses.delete(requestId);
3964
+ const queue = pendingHttp2CompatResponsesByServer.get(serverId);
3965
+ if (queue) {
3966
+ const index = queue.indexOf(requestId);
3967
+ if (index !== -1)
3968
+ queue.splice(index, 1);
3969
+ if (queue.length === 0)
3970
+ pendingHttp2CompatResponsesByServer.delete(serverId);
3971
+ }
3972
+ return pending;
3973
+ }
3974
+ function takePendingHttp2CompatResponseByServer(serverId) {
3975
+ const queue = pendingHttp2CompatResponsesByServer.get(serverId);
3976
+ if (!queue || queue.length === 0)
3977
+ return undefined;
3978
+ const requestId = queue.shift();
3979
+ if (queue.length === 0)
3980
+ pendingHttp2CompatResponsesByServer.delete(serverId);
3981
+ const pending = pendingHttp2CompatResponses.get(requestId);
3982
+ if (pending) {
3983
+ pendingHttp2CompatResponses.delete(requestId);
3984
+ }
3985
+ return pending;
3986
+ }
3987
+ /** Resolve a pending HTTP server response (called from stream callback handler). */
3988
+ export function resolveHttpServerResponse(options) {
3989
+ const pending = options.requestId !== undefined
3990
+ ? removePendingHttpResponse(options.serverId ?? pendingHttpResponses.get(options.requestId)?.serverId ?? -1, options.requestId)
3991
+ : options.serverId !== undefined
3992
+ ? takePendingHttpResponseByServer(options.serverId)
3993
+ : undefined;
3994
+ pending?.resolve(options.responseJson);
3995
+ }
3996
+ export function resolveHttp2CompatResponse(options) {
3997
+ const pending = options.requestId !== undefined
3998
+ ? removePendingHttp2CompatResponse(options.serverId ?? pendingHttp2CompatResponses.get(options.requestId)?.serverId ?? -1, options.requestId)
3999
+ : options.serverId !== undefined
4000
+ ? takePendingHttp2CompatResponseByServer(options.serverId)
4001
+ : undefined;
4002
+ pending?.resolve(options.responseJson);
4003
+ }
4004
+ /** Build PTY bridge handlers. */
4005
+ export function buildPtyBridgeHandlers(deps) {
4006
+ const handlers = {};
4007
+ const K = HOST_BRIDGE_GLOBAL_KEYS;
4008
+ if (deps.stdinIsTTY && deps.onPtySetRawMode) {
4009
+ handlers[K.ptySetRawMode] = (mode) => {
4010
+ deps.onPtySetRawMode(Boolean(mode));
4011
+ };
4012
+ }
4013
+ return handlers;
4014
+ }
4015
+ const O_ACCMODE = 3;
4016
+ function canRead(flags) {
4017
+ const access = flags & O_ACCMODE;
4018
+ return access === O_RDONLY || access === O_RDWR;
4019
+ }
4020
+ function canWrite(flags) {
4021
+ const access = flags & O_ACCMODE;
4022
+ return access === O_WRONLY || access === O_RDWR;
4023
+ }
4024
+ const PROC_SYS_KERNEL_HOSTNAME_PATH = "/proc/sys/kernel/hostname";
4025
+ function getStandaloneProcFileContent(path) {
4026
+ if (path === PROC_SYS_KERNEL_HOSTNAME_PATH) {
4027
+ return Buffer.from("sandbox\n", "utf8");
4028
+ }
4029
+ return null;
4030
+ }
4031
+ function getStandaloneProcFileStat(path) {
4032
+ const content = getStandaloneProcFileContent(path);
4033
+ if (!content)
4034
+ return null;
4035
+ const now = Date.now();
4036
+ return {
4037
+ mode: 0o100444,
4038
+ size: content.length,
4039
+ isDirectory: false,
4040
+ isSymbolicLink: false,
4041
+ atimeMs: now,
4042
+ mtimeMs: now,
4043
+ ctimeMs: now,
4044
+ birthtimeMs: now,
4045
+ ino: 0xfffe0001,
4046
+ nlink: 1,
4047
+ uid: 0,
4048
+ gid: 0,
4049
+ };
4050
+ }
4051
+ async function readStandaloneProcAwareFile(vfs, path) {
4052
+ return getStandaloneProcFileContent(path) ?? vfs.readFile(path);
4053
+ }
4054
+ async function readStandaloneProcAwareTextFile(vfs, path) {
4055
+ const content = getStandaloneProcFileContent(path);
4056
+ if (content)
4057
+ return new TextDecoder().decode(content);
4058
+ return vfs.readTextFile(path);
4059
+ }
4060
+ async function standaloneProcAwareExists(vfs, path) {
4061
+ if (getStandaloneProcFileContent(path))
4062
+ return true;
4063
+ return vfs.exists(path);
4064
+ }
4065
+ async function standaloneProcAwareStat(vfs, path) {
4066
+ return getStandaloneProcFileStat(path) ?? vfs.stat(path);
4067
+ }
4068
+ async function standaloneProcAwarePread(vfs, path, offset, length) {
4069
+ const content = getStandaloneProcFileContent(path);
4070
+ if (content) {
4071
+ if (offset >= content.length)
4072
+ return new Uint8Array(0);
4073
+ return content.slice(offset, offset + length);
4074
+ }
4075
+ return vfs.pread(path, offset, length);
4076
+ }
4077
+ /**
4078
+ * Build kernel FD table bridge handlers.
4079
+ *
4080
+ * Creates a ProcessFDTable per execution and routes all FD operations
4081
+ * (open, close, read, write, fstat, ftruncate, fsync) through it.
4082
+ * The FD table tracks file descriptors, cursor positions, and flags.
4083
+ * Actual file I/O is delegated to the VirtualFileSystem.
4084
+ */
4085
+ export function buildKernelFdBridgeHandlers(deps) {
4086
+ const handlers = {};
4087
+ const K = HOST_BRIDGE_GLOBAL_KEYS;
4088
+ const vfs = deps.filesystem;
4089
+ // Create a per-execution FD table via the kernel FDTableManager
4090
+ const fdManager = new FDTableManager();
4091
+ const pid = 1;
4092
+ const fdTable = fdManager.create(pid);
4093
+ // fdOpen(path, flags, mode?) → fd number
4094
+ handlers[K.fdOpen] = async (path, flags, mode) => {
4095
+ checkBridgeBudget(deps);
4096
+ const pathStr = String(path);
4097
+ const numFlags = Number(flags);
4098
+ const numMode = mode !== undefined && mode !== null ? Number(mode) : undefined;
4099
+ const exists = await standaloneProcAwareExists(vfs, pathStr);
4100
+ // O_CREAT: create if doesn't exist
4101
+ if ((numFlags & O_CREAT) && !exists) {
4102
+ await vfs.writeFile(pathStr, "");
4103
+ }
4104
+ else if (!exists && !(numFlags & O_CREAT)) {
4105
+ throw new Error(`ENOENT: no such file or directory, open '${pathStr}'`);
4106
+ }
4107
+ // O_TRUNC: truncate existing file
4108
+ if ((numFlags & O_TRUNC) && exists) {
4109
+ await vfs.writeFile(pathStr, "");
4110
+ }
4111
+ const fd = fdTable.open(pathStr, numFlags, FILETYPE_REGULAR_FILE);
4112
+ // Store creation mode for umask application
4113
+ if (numMode !== undefined && (numFlags & O_CREAT)) {
4114
+ const entry = fdTable.get(fd);
4115
+ if (entry)
4116
+ entry.description.creationMode = numMode;
4117
+ }
4118
+ return fd;
4119
+ };
4120
+ // fdClose(fd)
4121
+ handlers[K.fdClose] = (fd) => {
4122
+ const fdNum = Number(fd);
4123
+ const ok = fdTable.close(fdNum);
4124
+ if (!ok)
4125
+ throw new Error("EBADF: bad file descriptor, close");
4126
+ };
4127
+ // fdRead(fd, length, position?) → base64 data
4128
+ handlers[K.fdRead] = async (fd, length, position) => {
4129
+ checkBridgeBudget(deps);
4130
+ const fdNum = Number(fd);
4131
+ const len = Number(length);
4132
+ const entry = fdTable.get(fdNum);
4133
+ if (!entry)
4134
+ throw new Error("EBADF: bad file descriptor, read");
4135
+ if (!canRead(entry.description.flags))
4136
+ throw new Error("EBADF: bad file descriptor, read");
4137
+ const pos = (position !== null && position !== undefined)
4138
+ ? Number(position)
4139
+ : Number(entry.description.cursor);
4140
+ const data = await standaloneProcAwarePread(vfs, entry.description.path, pos, len);
4141
+ // Update cursor only when no explicit position
4142
+ if (position === null || position === undefined) {
4143
+ entry.description.cursor += BigInt(data.length);
4144
+ }
4145
+ return Buffer.from(data).toString("base64");
4146
+ };
4147
+ // fdWrite(fd, base64data, position?) → bytes written
4148
+ handlers[K.fdWrite] = async (fd, base64data, position) => {
4149
+ checkBridgeBudget(deps);
4150
+ const fdNum = Number(fd);
4151
+ const entry = fdTable.get(fdNum);
4152
+ if (!entry)
4153
+ throw new Error("EBADF: bad file descriptor, write");
4154
+ if (!canWrite(entry.description.flags))
4155
+ throw new Error("EBADF: bad file descriptor, write");
4156
+ const data = Buffer.from(String(base64data), "base64");
4157
+ // Read existing content
4158
+ let content;
4159
+ try {
4160
+ content = await readStandaloneProcAwareFile(vfs, entry.description.path);
4161
+ }
4162
+ catch {
4163
+ content = new Uint8Array(0);
4164
+ }
4165
+ // Determine write position
4166
+ let writePos;
4167
+ if (entry.description.flags & O_APPEND) {
4168
+ writePos = content.length;
4169
+ }
4170
+ else if (position !== null && position !== undefined) {
4171
+ writePos = Number(position);
4172
+ }
4173
+ else {
4174
+ writePos = Number(entry.description.cursor);
4175
+ }
4176
+ // Splice data into content
4177
+ const endPos = writePos + data.length;
4178
+ const newContent = new Uint8Array(Math.max(content.length, endPos));
4179
+ newContent.set(content);
4180
+ newContent.set(data, writePos);
4181
+ await vfs.writeFile(entry.description.path, newContent);
4182
+ // Update cursor only when no explicit position
4183
+ if (position === null || position === undefined) {
4184
+ entry.description.cursor = BigInt(endPos);
4185
+ }
4186
+ return data.length;
4187
+ };
4188
+ // fdFstat(fd) → JSON stat string
4189
+ handlers[K.fdFstat] = async (fd) => {
4190
+ checkBridgeBudget(deps);
4191
+ const fdNum = Number(fd);
4192
+ const entry = fdTable.get(fdNum);
4193
+ if (!entry)
4194
+ throw new Error("EBADF: bad file descriptor, fstat");
4195
+ const stat = await standaloneProcAwareStat(vfs, entry.description.path);
4196
+ return JSON.stringify({
4197
+ dev: 0,
4198
+ ino: stat.ino ?? 0,
4199
+ mode: stat.mode,
4200
+ nlink: stat.nlink ?? 1,
4201
+ uid: stat.uid ?? 0,
4202
+ gid: stat.gid ?? 0,
4203
+ rdev: 0,
4204
+ size: stat.size,
4205
+ blksize: 4096,
4206
+ blocks: Math.ceil(stat.size / 512),
4207
+ atimeMs: stat.atimeMs ?? Date.now(),
4208
+ mtimeMs: stat.mtimeMs ?? Date.now(),
4209
+ ctimeMs: stat.ctimeMs ?? Date.now(),
4210
+ birthtimeMs: stat.birthtimeMs ?? Date.now(),
4211
+ });
4212
+ };
4213
+ // fdFtruncate(fd, len?)
4214
+ handlers[K.fdFtruncate] = async (fd, len) => {
4215
+ checkBridgeBudget(deps);
4216
+ const fdNum = Number(fd);
4217
+ const entry = fdTable.get(fdNum);
4218
+ if (!entry)
4219
+ throw new Error("EBADF: bad file descriptor, ftruncate");
4220
+ const newLen = (len !== undefined && len !== null) ? Number(len) : 0;
4221
+ let content;
4222
+ try {
4223
+ content = await readStandaloneProcAwareFile(vfs, entry.description.path);
4224
+ }
4225
+ catch {
4226
+ content = new Uint8Array(0);
4227
+ }
4228
+ if (content.length > newLen) {
4229
+ await vfs.writeFile(entry.description.path, content.slice(0, newLen));
4230
+ }
4231
+ else if (content.length < newLen) {
4232
+ const padded = new Uint8Array(newLen);
4233
+ padded.set(content);
4234
+ await vfs.writeFile(entry.description.path, padded);
4235
+ }
4236
+ };
4237
+ // fdFsync(fd) — no-op for in-memory VFS, validates FD exists
4238
+ handlers[K.fdFsync] = (fd) => {
4239
+ const fdNum = Number(fd);
4240
+ const entry = fdTable.get(fdNum);
4241
+ if (!entry)
4242
+ throw new Error("EBADF: bad file descriptor, fsync");
4243
+ };
4244
+ // fdGetPath(fd) → path string or null
4245
+ handlers[K.fdGetPath] = (fd) => {
4246
+ const fdNum = Number(fd);
4247
+ const entry = fdTable.get(fdNum);
4248
+ return entry ? entry.description.path : null;
4249
+ };
4250
+ return {
4251
+ handlers,
4252
+ dispose: () => {
4253
+ fdTable.closeAll();
4254
+ },
4255
+ };
4256
+ }
4257
+ export function createProcessConfigForExecution(processConfig, timingMitigation, frozenTimeMs) {
4258
+ return {
4259
+ ...processConfig,
4260
+ timingMitigation: timingMitigation,
4261
+ frozenTimeMs: timingMitigation === "freeze" ? frozenTimeMs : undefined,
4262
+ };
4263
+ }