@secure-exec/nodejs 0.2.0-rc.1 → 0.2.0-rc.2

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.
@@ -3,8 +3,11 @@
3
3
  // Each handler is a plain function that performs the host-side operation.
4
4
  // Handler names match HOST_BRIDGE_GLOBAL_KEYS from the bridge contract.
5
5
  import * as http from "node:http";
6
+ import * as https from "node:https";
6
7
  import * as http2 from "node:http2";
7
8
  import * as tls from "node:tls";
9
+ import * as hostUtil from "node:util";
10
+ import * as zlib from "node:zlib";
8
11
  import { Duplex, PassThrough } from "node:stream";
9
12
  import { readFileSync, realpathSync, existsSync } from "node:fs";
10
13
  import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from "node:path";
@@ -15,9 +18,9 @@ import { HOST_BRIDGE_GLOBAL_KEYS, } from "./bridge-contract.js";
15
18
  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
19
  import { normalizeBuiltinSpecifier } from "./builtin-modules.js";
17
20
  import { resolveModule, loadFile } from "./package-bundler.js";
18
- import { transformDynamicImport, isESM } from "@secure-exec/core/internal/shared/esm-utils";
19
21
  import { bundlePolyfill, hasPolyfill } from "./polyfills.js";
20
- import { createBuiltinESMWrapper, getStaticBuiltinWrapperSource, } from "./esm-compiler.js";
22
+ import { createBuiltinESMWrapper, getBuiltinBindingExpression, getStaticBuiltinWrapperSource, } from "./esm-compiler.js";
23
+ import { transformSourceForImport, transformSourceForImportSync, transformSourceForRequire, transformSourceForRequireSync, } from "./module-source.js";
21
24
  import { checkBridgeBudget, assertPayloadByteLength, assertTextPayloadSize, getBase64EncodedByteLength, getHostBuiltinNamedExports, parseJsonWithLimit, polyfillCodeCache, RESOURCE_BUDGET_ERROR_CODE, } from "./isolate-bootstrap.js";
22
25
  const SOL_SOCKET = 1;
23
26
  const IPPROTO_TCP = 6;
@@ -1965,89 +1968,41 @@ function buildKernelSocketBridgeHandlers(dispatch, socketTable, pid) {
1965
1968
  };
1966
1969
  return { handlers, dispose };
1967
1970
  }
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, "");
1971
+ function normalizeModuleResolveContext(referrer) {
1972
+ if (!referrer || referrer.endsWith("/")) {
1973
+ return referrer || "/";
1974
+ }
1975
+ return pathDirname(referrer) !== referrer && /\.[^/]+$/.test(referrer)
1976
+ ? pathDirname(referrer)
1977
+ : referrer;
1975
1978
  }
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]);
1979
+ function selectPackageExportTarget(entry, mode) {
1980
+ if (typeof entry === "string") {
1981
+ return entry;
2022
1982
  }
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]);
1983
+ if (Array.isArray(entry)) {
1984
+ for (const candidate of entry) {
1985
+ const resolved = selectPackageExportTarget(candidate, mode);
1986
+ if (resolved) {
1987
+ return resolved;
1988
+ }
1989
+ }
1990
+ return null;
2027
1991
  }
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]);
1992
+ if (!entry || typeof entry !== "object") {
1993
+ return null;
2032
1994
  }
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");
1995
+ const conditionalEntry = entry;
1996
+ const candidates = mode === "import"
1997
+ ? [conditionalEntry.import, conditionalEntry.default, conditionalEntry.require]
1998
+ : [conditionalEntry.require, conditionalEntry.default, conditionalEntry.import];
1999
+ for (const candidate of candidates) {
2000
+ const resolved = selectPackageExportTarget(candidate, mode);
2001
+ if (resolved) {
2002
+ return resolved;
2003
+ }
2049
2004
  }
2050
- return code;
2005
+ return null;
2051
2006
  }
2052
2007
  /**
2053
2008
  * Resolve a package specifier by walking up directories and reading package.json exports.
@@ -2067,16 +2022,16 @@ function resolvePackageExport(req, startDir, mode = "require") {
2067
2022
  const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
2068
2023
  let entry;
2069
2024
  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
- }
2025
+ const exportEntry = subpath === "." &&
2026
+ typeof pkg.exports === "object" &&
2027
+ pkg.exports !== null &&
2028
+ !Array.isArray(pkg.exports) &&
2029
+ !("." in pkg.exports)
2030
+ ? pkg.exports
2031
+ : pkg.exports[subpath];
2032
+ const resolvedEntry = selectPackageExportTarget(exportEntry, mode);
2033
+ if (resolvedEntry)
2034
+ entry = resolvedEntry;
2080
2035
  }
2081
2036
  if (!entry && subpath === ".")
2082
2037
  entry = pkg.main;
@@ -2111,8 +2066,11 @@ export function buildModuleResolutionBridgeHandlers(deps) {
2111
2066
  if (builtin)
2112
2067
  return builtin;
2113
2068
  // Translate sandbox fromDir to host path for resolution context
2114
- const sandboxDir = String(fromDir);
2115
- const hostDir = deps.sandboxToHostPath(sandboxDir) ?? sandboxDir;
2069
+ const referrer = String(fromDir);
2070
+ const sandboxDir = normalizeModuleResolveContext(referrer);
2071
+ const hostDir = normalizeModuleResolveContext(deps.sandboxToHostPath(referrer) ??
2072
+ deps.sandboxToHostPath(sandboxDir) ??
2073
+ sandboxDir);
2116
2074
  const resolveFromExports = (dir) => {
2117
2075
  const resolved = resolvePackageExport(req, dir, resolveMode);
2118
2076
  return resolved ? deps.hostToSandboxPath(resolved) : null;
@@ -2156,20 +2114,12 @@ export function buildModuleResolutionBridgeHandlers(deps) {
2156
2114
  catch { /* fallback failed */ }
2157
2115
  return null;
2158
2116
  };
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.
2117
+ // Sync file read — translates sandbox path and applies parser-backed
2118
+ // CJS transforms when require() needs ESM or import() support.
2162
2119
  handlers[K.loadFileSync] = (filePath) => {
2163
2120
  const sandboxPath = String(filePath);
2164
2121
  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
- }
2122
+ return loadHostModuleSourceSync(hostPath, sandboxPath, "require");
2173
2123
  };
2174
2124
  return handlers;
2175
2125
  }
@@ -2228,6 +2178,53 @@ export function buildConsoleBridgeHandlers(deps) {
2228
2178
  };
2229
2179
  return handlers;
2230
2180
  }
2181
+ function getStaticBuiltinRequireSource(moduleName) {
2182
+ switch (moduleName) {
2183
+ case "fs":
2184
+ return "module.exports = globalThis.bridge?.fs || globalThis.bridge?.default || {};";
2185
+ case "fs/promises":
2186
+ return "module.exports = (globalThis.bridge?.fs || globalThis.bridge?.default || {}).promises || {};";
2187
+ case "module":
2188
+ return `module.exports = ${"globalThis.bridge?.module || {" +
2189
+ "createRequire: globalThis._createRequire || function(f) {" +
2190
+ "const dir = f.replace(/\\\\[^\\\\]*$/, '') || '/';" +
2191
+ "return function(m) { return globalThis._requireFrom(m, dir); };" +
2192
+ "}," +
2193
+ "Module: { builtinModules: [] }," +
2194
+ "isBuiltin: () => false," +
2195
+ "builtinModules: []" +
2196
+ "}"};`;
2197
+ case "os":
2198
+ return "module.exports = globalThis._osModule || {};";
2199
+ case "http":
2200
+ return "module.exports = globalThis._httpModule || globalThis.bridge?.network?.http || {};";
2201
+ case "https":
2202
+ return "module.exports = globalThis._httpsModule || globalThis.bridge?.network?.https || {};";
2203
+ case "http2":
2204
+ return "module.exports = globalThis._http2Module || {};";
2205
+ case "dns":
2206
+ return "module.exports = globalThis._dnsModule || globalThis.bridge?.network?.dns || {};";
2207
+ case "child_process":
2208
+ return "module.exports = globalThis._childProcessModule || globalThis.bridge?.childProcess || {};";
2209
+ case "process":
2210
+ return "module.exports = globalThis.process || {};";
2211
+ case "v8":
2212
+ return "module.exports = globalThis._moduleCache?.v8 || {};";
2213
+ default:
2214
+ return null;
2215
+ }
2216
+ }
2217
+ function loadHostModuleSourceSync(readPath, logicalPath, loadMode) {
2218
+ try {
2219
+ const source = readFileSync(readPath, "utf-8");
2220
+ return loadMode === "require"
2221
+ ? transformSourceForRequireSync(source, logicalPath)
2222
+ : transformSourceForImportSync(source, logicalPath, readPath);
2223
+ }
2224
+ catch {
2225
+ return null;
2226
+ }
2227
+ }
2231
2228
  /** Build module loading bridge handlers (loadPolyfill, resolveModule, loadFile). */
2232
2229
  export function buildModuleLoadingBridgeHandlers(deps,
2233
2230
  /** Extra handlers to dispatch through _loadPolyfill for V8 runtime compatibility. */
@@ -2325,16 +2322,25 @@ dispatchHandlers) {
2325
2322
  // V8 ESM module mode handles static imports natively via module_resolve_callback;
2326
2323
  // this handler covers the __dynamicImport() path used in exec mode.
2327
2324
  handlers[K.dynamicImport] = async () => null;
2328
- // Async file read + dynamic import transform.
2325
+ // Async file read for CommonJS and ESM loader paths.
2329
2326
  // Also serves ESM wrappers for built-in modules (fs, path, etc.) when
2330
2327
  // used from V8's ES module system which calls _loadFile after _resolveModule.
2331
- handlers[K.loadFile] = async (path, requestedMode) => {
2328
+ handlers[K.loadFile] = (path, requestedMode) => {
2332
2329
  const p = String(path);
2333
2330
  const loadMode = requestedMode === "require" || requestedMode === "import"
2334
2331
  ? requestedMode
2335
2332
  : (deps.resolveMode ?? "require");
2336
2333
  // Built-in module ESM wrappers (V8 module system resolves 'fs' then loads it)
2337
2334
  const bare = p.replace(/^node:/, "");
2335
+ if (loadMode === "require") {
2336
+ const builtinRequireSource = getStaticBuiltinRequireSource(bare);
2337
+ if (builtinRequireSource)
2338
+ return builtinRequireSource;
2339
+ }
2340
+ const builtinBindingExpression = getBuiltinBindingExpression(bare);
2341
+ if (builtinBindingExpression) {
2342
+ return createBuiltinESMWrapper(builtinBindingExpression, getHostBuiltinNamedExports(bare));
2343
+ }
2338
2344
  const builtin = getStaticBuiltinWrapperSource(bare);
2339
2345
  if (builtin)
2340
2346
  return builtin;
@@ -2342,14 +2348,21 @@ dispatchHandlers) {
2342
2348
  if (hasPolyfill(bare)) {
2343
2349
  return createBuiltinESMWrapper(`globalThis._requireFrom(${JSON.stringify(bare)}, "/")`, getHostBuiltinNamedExports(bare));
2344
2350
  }
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
+ const hostPath = deps.sandboxToHostPath?.(p) ?? p;
2352
+ const syncSource = loadHostModuleSourceSync(hostPath, p, loadMode);
2353
+ if (syncSource !== null) {
2354
+ return syncSource;
2351
2355
  }
2352
- return transformDynamicImport(source);
2356
+ // Regular files load differently for CommonJS require() vs V8's ESM loader.
2357
+ return (async () => {
2358
+ const source = await loadFile(p, deps.filesystem);
2359
+ if (source === null)
2360
+ return null;
2361
+ if (loadMode === "require") {
2362
+ return transformSourceForRequire(source, p);
2363
+ }
2364
+ return transformSourceForImport(source, p);
2365
+ })();
2353
2366
  };
2354
2367
  return handlers;
2355
2368
  }
@@ -2369,6 +2382,40 @@ export function buildTimerBridgeHandlers(deps) {
2369
2382
  };
2370
2383
  return handlers;
2371
2384
  }
2385
+ function serializeMimeTypeState(value) {
2386
+ return {
2387
+ value: String(value),
2388
+ essence: value.essence,
2389
+ type: value.type,
2390
+ subtype: value.subtype,
2391
+ params: Array.from(value.params.entries()),
2392
+ };
2393
+ }
2394
+ export function buildMimeBridgeHandlers() {
2395
+ return {
2396
+ mimeBridge: (operation, input, ...args) => {
2397
+ const mime = new hostUtil.MIMEType(String(input));
2398
+ switch (String(operation)) {
2399
+ case "parse":
2400
+ return serializeMimeTypeState(mime);
2401
+ case "setType":
2402
+ mime.type = String(args[0]);
2403
+ return serializeMimeTypeState(mime);
2404
+ case "setSubtype":
2405
+ mime.subtype = String(args[0]);
2406
+ return serializeMimeTypeState(mime);
2407
+ case "setParam":
2408
+ mime.params.set(String(args[0]), String(args[1]));
2409
+ return serializeMimeTypeState(mime);
2410
+ case "deleteParam":
2411
+ mime.params.delete(String(args[0]));
2412
+ return serializeMimeTypeState(mime);
2413
+ default:
2414
+ throw new Error(`Unsupported MIME bridge operation: ${String(operation)}`);
2415
+ }
2416
+ },
2417
+ };
2418
+ }
2372
2419
  export function buildKernelTimerDispatchHandlers(deps) {
2373
2420
  const handlers = {};
2374
2421
  handlers.kernelTimerCreate = (delayMs, repeat) => {
@@ -2420,6 +2467,25 @@ export function buildKernelTimerDispatchHandlers(deps) {
2420
2467
  };
2421
2468
  return handlers;
2422
2469
  }
2470
+ export function buildKernelStdinDispatchHandlers(deps) {
2471
+ const handlers = {};
2472
+ const K = HOST_BRIDGE_GLOBAL_KEYS;
2473
+ handlers[K.kernelStdinRead] = async () => {
2474
+ checkBridgeBudget(deps);
2475
+ if (!deps.liveStdinSource) {
2476
+ return { done: true };
2477
+ }
2478
+ const chunk = await deps.liveStdinSource.read();
2479
+ if (chunk === null || chunk.length === 0) {
2480
+ return { done: true };
2481
+ }
2482
+ return {
2483
+ done: false,
2484
+ dataBase64: Buffer.from(chunk).toString("base64"),
2485
+ };
2486
+ };
2487
+ return handlers;
2488
+ }
2423
2489
  export function buildKernelHandleDispatchHandlers(deps) {
2424
2490
  const handlers = {};
2425
2491
  handlers.kernelHandleRegister = (id, description) => {
@@ -2598,8 +2664,23 @@ export function buildChildProcessBridgeHandlers(deps) {
2598
2664
  // Serialize a child process event and push it into the V8 isolate
2599
2665
  const dispatchEvent = (sessionId, type, data) => {
2600
2666
  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));
2667
+ let eventType;
2668
+ let payload;
2669
+ if (type === "stdout" || type === "stderr") {
2670
+ eventType = type === "stdout" ? "child_stdout" : "child_stderr";
2671
+ payload = {
2672
+ sessionId,
2673
+ dataBase64: Buffer.from(data).toString("base64"),
2674
+ };
2675
+ }
2676
+ else {
2677
+ eventType = "child_exit";
2678
+ payload = {
2679
+ sessionId,
2680
+ code: Number(data ?? 1),
2681
+ };
2682
+ }
2683
+ deps.sendStreamEvent(eventType, Buffer.from(JSON.stringify(payload)));
2603
2684
  }
2604
2685
  catch {
2605
2686
  // Context may be disposed
@@ -2741,10 +2822,15 @@ export function buildChildProcessBridgeHandlers(deps) {
2741
2822
  function normalizeLoopbackHostname(hostname) {
2742
2823
  if (!hostname || hostname === "localhost")
2743
2824
  return "127.0.0.1";
2744
- if (hostname === "127.0.0.1" || hostname === "::1")
2825
+ // Preserve wildcard binds so kernel listener lookup and server.address()
2826
+ // reflect the caller's requested address while loopback connects still
2827
+ // resolve through SocketTable wildcard matching.
2828
+ if (hostname === "127.0.0.1" ||
2829
+ hostname === "::1" ||
2830
+ hostname === "0.0.0.0" ||
2831
+ hostname === "::") {
2745
2832
  return hostname;
2746
- if (hostname === "0.0.0.0" || hostname === "::")
2747
- return "127.0.0.1";
2833
+ }
2748
2834
  throw new Error(`Sandbox HTTP servers are restricted to loopback interfaces. Received hostname: ${hostname}`);
2749
2835
  }
2750
2836
  function debugHttpBridge(...args) {
@@ -2752,6 +2838,50 @@ function debugHttpBridge(...args) {
2752
2838
  console.error("[secure-exec http bridge]", ...args);
2753
2839
  }
2754
2840
  }
2841
+ const MAX_REDIRECTS = 20;
2842
+ function shouldUseKernelHttpClientPath(adapter, urlString) {
2843
+ const loopbackAwareAdapter = adapter;
2844
+ if (typeof loopbackAwareAdapter.__setLoopbackPortChecker !== "function") {
2845
+ return false;
2846
+ }
2847
+ try {
2848
+ const parsed = new URL(urlString);
2849
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
2850
+ }
2851
+ catch {
2852
+ return false;
2853
+ }
2854
+ }
2855
+ async function maybeDecompressHttpBody(buffer, contentEncoding) {
2856
+ const encoding = Array.isArray(contentEncoding)
2857
+ ? contentEncoding[0]
2858
+ : contentEncoding;
2859
+ if (encoding !== "gzip" && encoding !== "deflate") {
2860
+ return buffer;
2861
+ }
2862
+ try {
2863
+ return await new Promise((resolve, reject) => {
2864
+ const decompress = encoding === "gzip" ? zlib.gunzip : zlib.inflate;
2865
+ decompress(buffer, (err, result) => {
2866
+ if (err)
2867
+ reject(err);
2868
+ else
2869
+ resolve(result);
2870
+ });
2871
+ });
2872
+ }
2873
+ catch {
2874
+ // Preserve the original bytes when decompression fails.
2875
+ return buffer;
2876
+ }
2877
+ }
2878
+ function shouldEncodeHttpBodyAsBinary(urlString, headers) {
2879
+ const contentType = headers["content-type"] || "";
2880
+ const headerValue = Array.isArray(contentType) ? contentType.join(", ") : contentType;
2881
+ return (headerValue.includes("octet-stream") ||
2882
+ headerValue.includes("gzip") ||
2883
+ urlString.endsWith(".tgz"));
2884
+ }
2755
2885
  /**
2756
2886
  * Create a Duplex stream backed by a kernel socket.
2757
2887
  * Readable side reads from kernel socket readBuffer; writable side writes via send().
@@ -2885,6 +3015,7 @@ export function buildNetworkBridgeHandlers(deps) {
2885
3015
  const kernelHttp2ClientSessions = new Map();
2886
3016
  const http2Sessions = new Map();
2887
3017
  const http2Streams = new Map();
3018
+ const pendingHttp2PushStreams = new Map();
2888
3019
  const http2ServerSessionIds = new WeakMap();
2889
3020
  let nextHttp2SessionId = 1;
2890
3021
  let nextHttp2StreamId = 1;
@@ -3017,6 +3148,173 @@ export function buildNetworkBridgeHandlers(deps) {
3017
3148
  }
3018
3149
  return false;
3019
3150
  });
3151
+ const performKernelHttpRequest = async (urlString, requestOptions) => {
3152
+ const url = new URL(urlString);
3153
+ const isHttps = url.protocol === "https:";
3154
+ const host = url.hostname;
3155
+ const port = Number(url.port || (isHttps ? 443 : 80));
3156
+ const socketId = socketTable.create(host.includes(":") ? AF_INET6 : AF_INET, SOCK_STREAM, 0, pid);
3157
+ await socketTable.connect(socketId, { host, port });
3158
+ const baseTransport = createKernelSocketDuplex(socketId, socketTable, pid);
3159
+ const requestTransport = isHttps
3160
+ ? tls.connect({
3161
+ socket: baseTransport,
3162
+ servername: host,
3163
+ ...(requestOptions.rejectUnauthorized !== undefined
3164
+ ? { rejectUnauthorized: requestOptions.rejectUnauthorized }
3165
+ : {}),
3166
+ })
3167
+ : baseTransport;
3168
+ const transport = isHttps ? https : http;
3169
+ return await new Promise((resolve, reject) => {
3170
+ let settled = false;
3171
+ const settleResolve = (value) => {
3172
+ if (settled)
3173
+ return;
3174
+ settled = true;
3175
+ resolve(value);
3176
+ };
3177
+ const settleReject = (error) => {
3178
+ if (settled)
3179
+ return;
3180
+ settled = true;
3181
+ reject(error);
3182
+ };
3183
+ const req = transport.request({
3184
+ hostname: host,
3185
+ port,
3186
+ path: `${url.pathname}${url.search}`,
3187
+ method: requestOptions.method || "GET",
3188
+ headers: requestOptions.headers || {},
3189
+ agent: false,
3190
+ createConnection: () => requestTransport,
3191
+ }, (res) => {
3192
+ const chunks = [];
3193
+ res.on("data", (chunk) => {
3194
+ chunks.push(chunk);
3195
+ });
3196
+ res.on("error", (error) => {
3197
+ requestTransport.destroy();
3198
+ settleReject(error);
3199
+ });
3200
+ res.on("end", async () => {
3201
+ const decodedBuffer = await maybeDecompressHttpBody(Buffer.concat(chunks), res.headers["content-encoding"]);
3202
+ const buffer = Buffer.from(decodedBuffer);
3203
+ const headers = {};
3204
+ const rawHeaders = [...res.rawHeaders];
3205
+ Object.entries(res.headers).forEach(([key, value]) => {
3206
+ if (typeof value === "string")
3207
+ headers[key] = value;
3208
+ else if (Array.isArray(value))
3209
+ headers[key] = value.join(", ");
3210
+ });
3211
+ delete headers["content-encoding"];
3212
+ const trailers = {};
3213
+ Object.entries(res.trailers || {}).forEach(([key, value]) => {
3214
+ if (typeof value === "string")
3215
+ trailers[key] = value;
3216
+ });
3217
+ const result = {
3218
+ status: res.statusCode || 200,
3219
+ statusText: res.statusMessage || "OK",
3220
+ headers,
3221
+ rawHeaders,
3222
+ url: urlString,
3223
+ body: shouldEncodeHttpBodyAsBinary(urlString, res.headers)
3224
+ ? (() => {
3225
+ headers["x-body-encoding"] = "base64";
3226
+ return buffer.toString("base64");
3227
+ })()
3228
+ : buffer.toString("utf8"),
3229
+ };
3230
+ if (Object.keys(trailers).length > 0) {
3231
+ result.trailers = trailers;
3232
+ }
3233
+ requestTransport.destroy();
3234
+ settleResolve(result);
3235
+ });
3236
+ });
3237
+ req.on("upgrade", (res, upgradedSocket, head) => {
3238
+ const headers = {};
3239
+ const rawHeaders = [...res.rawHeaders];
3240
+ Object.entries(res.headers).forEach(([key, value]) => {
3241
+ if (typeof value === "string")
3242
+ headers[key] = value;
3243
+ else if (Array.isArray(value))
3244
+ headers[key] = value.join(", ");
3245
+ });
3246
+ settleResolve({
3247
+ status: res.statusCode || 101,
3248
+ statusText: res.statusMessage || "Switching Protocols",
3249
+ headers,
3250
+ rawHeaders,
3251
+ body: head.toString("base64"),
3252
+ url: urlString,
3253
+ upgradeSocketId: registerKernelUpgradeSocket(upgradedSocket),
3254
+ });
3255
+ });
3256
+ req.on("connect", (res, connectSocket, head) => {
3257
+ const headers = {};
3258
+ const rawHeaders = [...res.rawHeaders];
3259
+ Object.entries(res.headers).forEach(([key, value]) => {
3260
+ if (typeof value === "string")
3261
+ headers[key] = value;
3262
+ else if (Array.isArray(value))
3263
+ headers[key] = value.join(", ");
3264
+ });
3265
+ settleResolve({
3266
+ status: res.statusCode || 200,
3267
+ statusText: res.statusMessage || "Connection established",
3268
+ headers,
3269
+ rawHeaders,
3270
+ body: head.toString("base64"),
3271
+ url: urlString,
3272
+ upgradeSocketId: registerKernelUpgradeSocket(connectSocket),
3273
+ });
3274
+ });
3275
+ req.on("error", (error) => {
3276
+ requestTransport.destroy();
3277
+ settleReject(error);
3278
+ });
3279
+ if (requestOptions.body) {
3280
+ req.write(requestOptions.body);
3281
+ }
3282
+ req.end();
3283
+ });
3284
+ };
3285
+ const performKernelFetch = async (urlString, requestOptions) => {
3286
+ let currentUrl = urlString;
3287
+ let redirected = false;
3288
+ let currentOptions = { ...requestOptions };
3289
+ for (let redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount += 1) {
3290
+ const response = await performKernelHttpRequest(currentUrl, currentOptions);
3291
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
3292
+ const location = response.headers.location;
3293
+ if (location) {
3294
+ currentUrl = new URL(location, currentUrl).href;
3295
+ redirected = true;
3296
+ if (response.status === 301 || response.status === 302 || response.status === 303) {
3297
+ currentOptions = {
3298
+ ...currentOptions,
3299
+ method: "GET",
3300
+ body: null,
3301
+ };
3302
+ }
3303
+ continue;
3304
+ }
3305
+ }
3306
+ return {
3307
+ ok: response.status >= 200 && response.status < 300,
3308
+ status: response.status,
3309
+ statusText: response.statusText,
3310
+ headers: { ...response.headers },
3311
+ body: response.body,
3312
+ url: currentUrl,
3313
+ redirected,
3314
+ };
3315
+ }
3316
+ throw new Error("Too many redirects");
3317
+ };
3020
3318
  const registerKernelUpgradeSocket = (socket) => {
3021
3319
  const socketId = nextKernelUpgradeSocketId++;
3022
3320
  kernelUpgradeSockets.set(socketId, socket);
@@ -3069,10 +3367,20 @@ export function buildNetworkBridgeHandlers(deps) {
3069
3367
  handlers[K.networkFetchRaw] = async (url, optionsJson) => {
3070
3368
  checkBridgeBudget(deps);
3071
3369
  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;
3370
+ deps.activeHttpClientRequests.count += 1;
3371
+ try {
3372
+ const urlString = String(url);
3373
+ const result = shouldUseKernelHttpClientPath(adapter, urlString)
3374
+ ? await performKernelFetch(urlString, options)
3375
+ // Legacy fallback for custom adapters and explicit no-network stubs.
3376
+ : await adapter.fetch(urlString, options);
3377
+ const json = JSON.stringify(result);
3378
+ assertTextPayloadSize("network.fetch response", json, jsonLimit);
3379
+ return json;
3380
+ }
3381
+ finally {
3382
+ deps.activeHttpClientRequests.count = Math.max(0, deps.activeHttpClientRequests.count - 1);
3383
+ }
3076
3384
  };
3077
3385
  handlers[K.networkDnsLookupRaw] = async (hostname) => {
3078
3386
  checkBridgeBudget(deps);
@@ -3082,10 +3390,20 @@ export function buildNetworkBridgeHandlers(deps) {
3082
3390
  handlers[K.networkHttpRequestRaw] = async (url, optionsJson) => {
3083
3391
  checkBridgeBudget(deps);
3084
3392
  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;
3393
+ deps.activeHttpClientRequests.count += 1;
3394
+ try {
3395
+ const urlString = String(url);
3396
+ const result = shouldUseKernelHttpClientPath(adapter, urlString)
3397
+ ? await performKernelHttpRequest(urlString, options)
3398
+ // Legacy fallback for custom adapters and explicit no-network stubs.
3399
+ : await adapter.httpRequest(urlString, options);
3400
+ const json = JSON.stringify(result);
3401
+ assertTextPayloadSize("network.httpRequest response", json, jsonLimit);
3402
+ return json;
3403
+ }
3404
+ finally {
3405
+ deps.activeHttpClientRequests.count = Math.max(0, deps.activeHttpClientRequests.count - 1);
3406
+ }
3089
3407
  };
3090
3408
  handlers[K.networkHttpServerRespondRaw] = (serverId, requestId, responseJson) => {
3091
3409
  const numericServerId = Number(serverId);
@@ -3374,6 +3692,28 @@ export function buildNetworkBridgeHandlers(deps) {
3374
3692
  code: err.code,
3375
3693
  }));
3376
3694
  };
3695
+ const resolveHostHttp2FilePath = (filePath) => {
3696
+ // The sandbox defaults process.execPath to /usr/bin/node, but the host-side
3697
+ // http2 respondWithFile helper needs a real host path when serving the Node binary.
3698
+ if (filePath === "/usr/bin/node" && process.execPath) {
3699
+ return process.execPath;
3700
+ }
3701
+ return filePath;
3702
+ };
3703
+ const withHttp2ServerStream = (streamId, action, fallback) => {
3704
+ const stream = http2Streams.get(streamId);
3705
+ if (stream) {
3706
+ return action(stream);
3707
+ }
3708
+ const pending = pendingHttp2PushStreams.get(streamId);
3709
+ if (pending) {
3710
+ pending.operations.push((resolvedStream) => {
3711
+ action(resolvedStream);
3712
+ });
3713
+ return fallback();
3714
+ }
3715
+ throw new Error(`HTTP/2 stream ${String(streamId)} not found`);
3716
+ };
3377
3717
  const attachHttp2ClientStreamListeners = (streamId, stream) => {
3378
3718
  stream.on("response", (headers) => {
3379
3719
  emitHttp2Event("clientResponseHeaders", streamId, JSON.stringify(normalizeHttp2EventHeaders(headers)));
@@ -3717,70 +4057,68 @@ export function buildNetworkBridgeHandlers(deps) {
3717
4057
  return state?.closedPromise ?? Promise.resolve();
3718
4058
  };
3719
4059
  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
4060
  const headers = parseJsonWithLimit("network.http2Stream.respond headers", String(headersJson), jsonLimit);
3725
- stream.respond(headers);
4061
+ withHttp2ServerStream(Number(streamId), (stream) => {
4062
+ stream.respond(headers);
4063
+ }, () => undefined);
3726
4064
  };
3727
- handlers[K.networkHttp2StreamPushStreamRaw] = async (streamId, headersJson, optionsJson) => {
4065
+ handlers[K.networkHttp2StreamPushStreamRaw] = (streamId, headersJson, optionsJson) => {
3728
4066
  const stream = http2Streams.get(Number(streamId));
3729
4067
  if (!stream) {
3730
4068
  throw new Error(`HTTP/2 stream ${String(streamId)} not found`);
3731
4069
  }
3732
4070
  const headers = parseJsonWithLimit("network.http2Stream.pushStream headers", String(headersJson), jsonLimit);
3733
4071
  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
- });
4072
+ const pushStreamId = nextHttp2StreamId++;
4073
+ pendingHttp2PushStreams.set(pushStreamId, {
4074
+ operations: [],
4075
+ });
4076
+ stream.pushStream(headers, options, (error, pushStream, pushHeaders) => {
4077
+ const pending = pendingHttp2PushStreams.get(pushStreamId);
4078
+ if (error) {
4079
+ pendingHttp2PushStreams.delete(pushStreamId);
4080
+ emitHttp2SerializedError("serverStreamError", Number(streamId), error);
4081
+ return;
3761
4082
  }
3762
- catch (error) {
3763
- reject(error);
4083
+ if (!pushStream) {
4084
+ pendingHttp2PushStreams.delete(pushStreamId);
4085
+ return;
3764
4086
  }
4087
+ http2Streams.set(pushStreamId, pushStream);
4088
+ pushStream.on("close", () => {
4089
+ http2Streams.delete(pushStreamId);
4090
+ pendingHttp2PushStreams.delete(pushStreamId);
4091
+ });
4092
+ for (const operation of pending?.operations ?? []) {
4093
+ operation(pushStream);
4094
+ }
4095
+ pendingHttp2PushStreams.delete(pushStreamId);
4096
+ void pushHeaders;
4097
+ });
4098
+ return JSON.stringify({
4099
+ streamId: pushStreamId,
4100
+ headers: JSON.stringify(normalizeHttp2EventHeaders(headers)),
3765
4101
  });
3766
4102
  };
3767
4103
  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"));
4104
+ return withHttp2ServerStream(Number(streamId), (stream) => stream.write(Buffer.from(String(dataBase64), "base64")), () => true);
3773
4105
  };
3774
4106
  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();
4107
+ withHttp2ServerStream(Number(streamId), (stream) => {
4108
+ if (typeof dataBase64 === "string" && dataBase64.length > 0) {
4109
+ stream.end(Buffer.from(dataBase64, "base64"));
4110
+ return;
4111
+ }
4112
+ stream.end();
4113
+ }, () => undefined);
4114
+ };
4115
+ handlers[K.networkHttp2StreamCloseRaw] = (streamId, rstCode) => {
4116
+ withHttp2ServerStream(Number(streamId), (stream) => {
4117
+ if (typeof stream.close !== "function") {
4118
+ throw new Error(`HTTP/2 stream ${String(streamId)} not found`);
4119
+ }
4120
+ stream.close(typeof rstCode === "number" ? Number(rstCode) : undefined);
4121
+ }, () => undefined);
3784
4122
  };
3785
4123
  handlers[K.networkHttp2StreamPauseRaw] = (streamId) => {
3786
4124
  http2Streams.get(Number(streamId))?.pause();
@@ -3789,13 +4127,11 @@ export function buildNetworkBridgeHandlers(deps) {
3789
4127
  http2Streams.get(Number(streamId))?.resume();
3790
4128
  };
3791
4129
  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
4130
  const headers = parseJsonWithLimit("network.http2Stream.respondWithFile headers", String(headersJson), jsonLimit);
3797
4131
  const options = parseJsonWithLimit("network.http2Stream.respondWithFile options", String(optionsJson), jsonLimit);
3798
- stream.respondWithFile(String(filePath), headers, options);
4132
+ withHttp2ServerStream(Number(streamId), (stream) => {
4133
+ stream.respondWithFile(resolveHostHttp2FilePath(String(filePath)), headers, options);
4134
+ }, () => undefined);
3799
4135
  };
3800
4136
  handlers[K.networkHttp2ServerRespondRaw] = (serverId, requestId, responseJson) => {
3801
4137
  resolveHttp2CompatResponse({