@secure-exec/core 0.1.0-rc.4 → 0.1.1-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.
@@ -376,13 +376,8 @@ function execSync(command, options) {
376
376
  }
377
377
  // Default maxBuffer 1MB (Node.js convention)
378
378
  const maxBuffer = opts.maxBuffer ?? 1024 * 1024;
379
- // Use synchronous bridge call - result is JSON string
380
- const jsonResult = _childProcessSpawnSync.applySyncPromise(undefined, [
381
- "bash",
382
- JSON.stringify(["-c", command]),
383
- JSON.stringify({ cwd: opts.cwd, env: opts.env, maxBuffer }),
384
- ]);
385
- const result = JSON.parse(jsonResult);
379
+ // Use synchronous bridge call
380
+ const result = _childProcessSpawnSync("bash", JSON.stringify(["-c", command]), JSON.stringify({ cwd: opts.cwd, env: opts.env, maxBuffer }));
386
381
  if (result.maxBufferExceeded) {
387
382
  const err = new Error("stdout maxBuffer length exceeded");
388
383
  err.code = "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
@@ -423,11 +418,7 @@ function spawn(command, args, options) {
423
418
  // This ensures process.chdir() changes are reflected in child processes
424
419
  const effectiveCwd = opts.cwd ?? (typeof process !== "undefined" ? process.cwd() : "/");
425
420
  // Streaming mode - spawn immediately
426
- const sessionId = _childProcessSpawnStart.applySync(undefined, [
427
- command,
428
- JSON.stringify(argsArray),
429
- JSON.stringify({ cwd: effectiveCwd, env: opts.env }),
430
- ]);
421
+ const sessionId = _childProcessSpawnStart(command, JSON.stringify(argsArray), JSON.stringify({ cwd: effectiveCwd, env: opts.env }));
431
422
  activeChildren.set(sessionId, child);
432
423
  // Register handle to keep sandbox alive until child exits
433
424
  // See: docs-internal/node/ACTIVE_HANDLES.md
@@ -439,12 +430,12 @@ function spawn(command, args, options) {
439
430
  if (typeof _childProcessStdinWrite === "undefined")
440
431
  return false;
441
432
  const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
442
- _childProcessStdinWrite.applySync(undefined, [sessionId, bytes]);
433
+ _childProcessStdinWrite(sessionId, bytes);
443
434
  return true;
444
435
  };
445
436
  child.stdin.end = () => {
446
437
  if (typeof _childProcessStdinClose !== "undefined") {
447
- _childProcessStdinClose.applySync(undefined, [sessionId]);
438
+ _childProcessStdinClose(sessionId);
448
439
  }
449
440
  child.stdin.writable = false;
450
441
  };
@@ -457,7 +448,7 @@ function spawn(command, args, options) {
457
448
  : signal === "SIGINT" || signal === 2
458
449
  ? 2
459
450
  : 15;
460
- _childProcessKill.applySync(undefined, [sessionId, sig]);
451
+ _childProcessKill(sessionId, sig);
461
452
  child.killed = true;
462
453
  child.signalCode = (typeof signal === "string" ? signal : "SIGTERM");
463
454
  return true;
@@ -501,13 +492,8 @@ function spawnSync(command, args, options) {
501
492
  const effectiveCwd = opts.cwd ?? (typeof process !== "undefined" ? process.cwd() : "/");
502
493
  // Pass maxBuffer through to host for enforcement
503
494
  const maxBuffer = opts.maxBuffer;
504
- // Args passed as JSON string for transferability
505
- const jsonResult = _childProcessSpawnSync.applySyncPromise(undefined, [
506
- command,
507
- JSON.stringify(argsArray),
508
- JSON.stringify({ cwd: effectiveCwd, env: opts.env, maxBuffer }),
509
- ]);
510
- const result = JSON.parse(jsonResult);
495
+ // Args and options passed as JSON strings for transferability
496
+ const result = _childProcessSpawnSync(command, JSON.stringify(argsArray), JSON.stringify({ cwd: effectiveCwd, env: opts.env, maxBuffer }));
511
497
  const stdoutBuf = typeof Buffer !== "undefined" ? Buffer.from(result.stdout) : result.stdout;
512
498
  const stderrBuf = typeof Buffer !== "undefined" ? Buffer.from(result.stderr) : result.stderr;
513
499
  if (result.maxBufferExceeded) {
package/dist/bridge/fs.js CHANGED
@@ -919,13 +919,13 @@ const fs = {
919
919
  try {
920
920
  if (encoding) {
921
921
  // Text mode - use text read
922
- const content = _fs.readFile.applySyncPromise(undefined, [pathStr]);
922
+ const content = _fs.readFile(pathStr);
923
923
  return content;
924
924
  }
925
925
  else {
926
- // Binary mode - use binary read with base64 encoding
927
- const base64Content = _fs.readFileBinary.applySyncPromise(undefined, [pathStr]);
928
- return Buffer.from(base64Content, "base64");
926
+ // Binary mode - host returns raw Uint8Array via MessagePack bin
927
+ const binaryData = _fs.readFileBinary(pathStr);
928
+ return Buffer.from(binaryData);
929
929
  }
930
930
  }
931
931
  catch (err) {
@@ -952,17 +952,16 @@ const fs = {
952
952
  if (typeof data === "string") {
953
953
  // Text mode - use text write
954
954
  // Return the result so async callers (fs.promises) can await it.
955
- return _fs.writeFile.applySyncPromise(undefined, [pathStr, data]);
955
+ return _fs.writeFile(pathStr, data);
956
956
  }
957
957
  else if (ArrayBuffer.isView(data)) {
958
- // Binary mode - convert to base64 and use binary write
958
+ // Binary mode - send raw Uint8Array via MessagePack bin
959
959
  const uint8 = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
960
- const base64 = Buffer.from(uint8).toString("base64");
961
- return _fs.writeFileBinary.applySyncPromise(undefined, [pathStr, base64]);
960
+ return _fs.writeFileBinary(pathStr, uint8);
962
961
  }
963
962
  else {
964
963
  // Fallback to text mode
965
- return _fs.writeFile.applySyncPromise(undefined, [pathStr, String(data)]);
964
+ return _fs.writeFile(pathStr, String(data));
966
965
  }
967
966
  },
968
967
  appendFileSync(path, data, options) {
@@ -975,9 +974,9 @@ const fs = {
975
974
  readdirSync(path, options) {
976
975
  const rawPath = toPathString(path);
977
976
  const pathStr = rawPath;
978
- let entriesJson;
977
+ let entries;
979
978
  try {
980
- entriesJson = _fs.readDir.applySyncPromise(undefined, [pathStr]);
979
+ entries = _fs.readDir(pathStr);
981
980
  }
982
981
  catch (err) {
983
982
  // Convert "entry not found" and similar errors to proper ENOENT
@@ -987,7 +986,6 @@ const fs = {
987
986
  }
988
987
  throw err;
989
988
  }
990
- const entries = JSON.parse(entriesJson);
991
989
  if (options?.withFileTypes) {
992
990
  return entries.map((e) => new Dirent(e.name, e.isDirectory, rawPath));
993
991
  }
@@ -997,12 +995,12 @@ const fs = {
997
995
  const rawPath = toPathString(path);
998
996
  const pathStr = rawPath;
999
997
  const recursive = typeof options === "object" ? options?.recursive ?? false : false;
1000
- _fs.mkdir.applySyncPromise(undefined, [pathStr, recursive]);
998
+ _fs.mkdir(pathStr, recursive);
1001
999
  return recursive ? rawPath : undefined;
1002
1000
  },
1003
1001
  rmdirSync(path, _options) {
1004
1002
  const pathStr = toPathString(path);
1005
- _fs.rmdir.applySyncPromise(undefined, [pathStr]);
1003
+ _fs.rmdir(pathStr);
1006
1004
  },
1007
1005
  rmSync(path, options) {
1008
1006
  const pathStr = toPathString(path);
@@ -1042,14 +1040,14 @@ const fs = {
1042
1040
  },
1043
1041
  existsSync(path) {
1044
1042
  const pathStr = toPathString(path);
1045
- return _fs.exists.applySyncPromise(undefined, [pathStr]);
1043
+ return _fs.exists(pathStr);
1046
1044
  },
1047
1045
  statSync(path, _options) {
1048
1046
  const rawPath = toPathString(path);
1049
1047
  const pathStr = rawPath;
1050
- let statJson;
1048
+ let stat;
1051
1049
  try {
1052
- statJson = _fs.stat.applySyncPromise(undefined, [pathStr]);
1050
+ stat = _fs.stat(pathStr);
1053
1051
  }
1054
1052
  catch (err) {
1055
1053
  // Convert various "not found" errors to proper ENOENT
@@ -1062,23 +1060,21 @@ const fs = {
1062
1060
  }
1063
1061
  throw err;
1064
1062
  }
1065
- const stat = JSON.parse(statJson);
1066
1063
  return new Stats(stat);
1067
1064
  },
1068
1065
  lstatSync(path, _options) {
1069
1066
  const pathStr = toPathString(path);
1070
- const statJson = bridgeCall(() => _fs.lstat.applySyncPromise(undefined, [pathStr]), "lstat", pathStr);
1071
- const stat = JSON.parse(statJson);
1067
+ const stat = bridgeCall(() => _fs.lstat(pathStr), "lstat", pathStr);
1072
1068
  return new Stats(stat);
1073
1069
  },
1074
1070
  unlinkSync(path) {
1075
1071
  const pathStr = toPathString(path);
1076
- _fs.unlink.applySyncPromise(undefined, [pathStr]);
1072
+ _fs.unlink(pathStr);
1077
1073
  },
1078
1074
  renameSync(oldPath, newPath) {
1079
1075
  const oldPathStr = toPathString(oldPath);
1080
1076
  const newPathStr = toPathString(newPath);
1081
- _fs.rename.applySyncPromise(undefined, [oldPathStr, newPathStr]);
1077
+ _fs.rename(oldPathStr, newPathStr);
1082
1078
  },
1083
1079
  copyFileSync(src, dest, _mode) {
1084
1080
  // readFileSync and writeFileSync already normalize paths
@@ -1340,35 +1336,35 @@ const fs = {
1340
1336
  chmodSync(path, mode) {
1341
1337
  const pathStr = toPathString(path);
1342
1338
  const modeNum = typeof mode === "string" ? parseInt(mode, 8) : mode;
1343
- bridgeCall(() => _fs.chmod.applySyncPromise(undefined, [pathStr, modeNum]), "chmod", pathStr);
1339
+ bridgeCall(() => _fs.chmod(pathStr, modeNum), "chmod", pathStr);
1344
1340
  },
1345
1341
  chownSync(path, uid, gid) {
1346
1342
  const pathStr = toPathString(path);
1347
- bridgeCall(() => _fs.chown.applySyncPromise(undefined, [pathStr, uid, gid]), "chown", pathStr);
1343
+ bridgeCall(() => _fs.chown(pathStr, uid, gid), "chown", pathStr);
1348
1344
  },
1349
1345
  linkSync(existingPath, newPath) {
1350
1346
  const existingStr = toPathString(existingPath);
1351
1347
  const newStr = toPathString(newPath);
1352
- bridgeCall(() => _fs.link.applySyncPromise(undefined, [existingStr, newStr]), "link", newStr);
1348
+ bridgeCall(() => _fs.link(existingStr, newStr), "link", newStr);
1353
1349
  },
1354
1350
  symlinkSync(target, path, _type) {
1355
1351
  const targetStr = toPathString(target);
1356
1352
  const pathStr = toPathString(path);
1357
- bridgeCall(() => _fs.symlink.applySyncPromise(undefined, [targetStr, pathStr]), "symlink", pathStr);
1353
+ bridgeCall(() => _fs.symlink(targetStr, pathStr), "symlink", pathStr);
1358
1354
  },
1359
1355
  readlinkSync(path, _options) {
1360
1356
  const pathStr = toPathString(path);
1361
- return bridgeCall(() => _fs.readlink.applySyncPromise(undefined, [pathStr]), "readlink", pathStr);
1357
+ return bridgeCall(() => _fs.readlink(pathStr), "readlink", pathStr);
1362
1358
  },
1363
1359
  truncateSync(path, len) {
1364
1360
  const pathStr = toPathString(path);
1365
- bridgeCall(() => _fs.truncate.applySyncPromise(undefined, [pathStr, len ?? 0]), "truncate", pathStr);
1361
+ bridgeCall(() => _fs.truncate(pathStr, len ?? 0), "truncate", pathStr);
1366
1362
  },
1367
1363
  utimesSync(path, atime, mtime) {
1368
1364
  const pathStr = toPathString(path);
1369
1365
  const atimeNum = typeof atime === "number" ? atime : new Date(atime).getTime() / 1000;
1370
1366
  const mtimeNum = typeof mtime === "number" ? mtime : new Date(mtime).getTime() / 1000;
1371
- bridgeCall(() => _fs.utimes.applySyncPromise(undefined, [pathStr, atimeNum, mtimeNum]), "utimes", pathStr);
1367
+ bridgeCall(() => _fs.utimes(pathStr, atimeNum, mtimeNum), "utimes", pathStr);
1372
1368
  },
1373
1369
  // Async methods - wrap sync methods in callbacks/promises
1374
1370
  //
@@ -78,10 +78,7 @@ export function createRequire(filename) {
78
78
  };
79
79
  // Create resolve function
80
80
  const resolve = function (request, _options) {
81
- const resolved = _resolveModule.applySyncPromise(undefined, [
82
- request,
83
- dirname,
84
- ]);
81
+ const resolved = _resolveModule(request, dirname);
85
82
  if (resolved === null) {
86
83
  const err = new Error("Cannot find module '" + request + "'");
87
84
  err.code = "MODULE_NOT_FOUND";
@@ -155,10 +152,7 @@ export class Module {
155
152
  const wrapper = new Function("exports", "require", "module", "__filename", "__dirname", content);
156
153
  const moduleRequire = (request) => _requireFrom(request, this.path);
157
154
  moduleRequire.resolve = (request) => {
158
- const resolved = _resolveModule.applySyncPromise(undefined, [
159
- request,
160
- this.path,
161
- ]);
155
+ const resolved = _resolveModule(request, this.path);
162
156
  if (resolved === null) {
163
157
  const err = new Error("Cannot find module '" + request + "'");
164
158
  err.code = "MODULE_NOT_FOUND";
@@ -190,10 +184,7 @@ export class Module {
190
184
  : {};
191
185
  static _resolveFilename(request, parent, _isMain, _options) {
192
186
  const parentDir = parent && parent.path ? parent.path : "/";
193
- const resolved = _resolveModule.applySyncPromise(undefined, [
194
- request,
195
- parentDir,
196
- ]);
187
+ const resolved = _resolveModule(request, parentDir);
197
188
  if (resolved === null) {
198
189
  const err = new Error("Cannot find module '" + request + "'");
199
190
  err.code = "MODULE_NOT_FOUND";
@@ -154,6 +154,7 @@ export declare class ClientRequest {
154
154
  private _callback?;
155
155
  private _listeners;
156
156
  private _body;
157
+ private _bodyBytes;
157
158
  private _ended;
158
159
  private _agent;
159
160
  private _hostKey;
@@ -1,5 +1,7 @@
1
1
  // Network module polyfill for isolated-vm
2
2
  // Provides fetch, http, https, and dns module emulation that bridges to host
3
+ // Cap in-sandbox request/response buffering to prevent host memory exhaustion
4
+ const MAX_HTTP_BODY_BYTES = 50 * 1024 * 1024; // 50 MB
3
5
  import { exposeCustomGlobal } from "../shared/global-exposure.js";
4
6
  // Fetch polyfill
5
7
  export async function fetch(url, options = {}) {
@@ -12,10 +14,7 @@ export async function fetch(url, options = {}) {
12
14
  headers: options.headers || {},
13
15
  body: options.body || null,
14
16
  });
15
- const responseJson = await _networkFetchRaw.apply(undefined, [String(url), optionsJson], {
16
- result: { promise: true },
17
- });
18
- const response = JSON.parse(responseJson);
17
+ const response = await _networkFetchRaw(String(url), optionsJson);
19
18
  // Create Response-like object
20
19
  return {
21
20
  ok: response.ok,
@@ -162,10 +161,8 @@ export const dns = {
162
161
  if (typeof options === "function") {
163
162
  cb = options;
164
163
  }
165
- _networkDnsLookupRaw
166
- .apply(undefined, [hostname], { result: { promise: true } })
167
- .then((resultJson) => {
168
- const result = JSON.parse(resultJson);
164
+ _networkDnsLookupRaw(hostname)
165
+ .then((result) => {
169
166
  if (result.error) {
170
167
  const err = new Error(result.error);
171
168
  err.code = result.code || "ENOTFOUND";
@@ -539,6 +536,7 @@ export class ClientRequest {
539
536
  _callback;
540
537
  _listeners = {};
541
538
  _body = "";
539
+ _bodyBytes = 0;
542
540
  _ended = false;
543
541
  _agent;
544
542
  _hostKey;
@@ -580,15 +578,17 @@ export class ClientRequest {
580
578
  throw new Error('http/https request requires NetworkAdapter to be configured');
581
579
  }
582
580
  const url = this._buildUrl();
581
+ const tls = {};
582
+ if (this._options.rejectUnauthorized !== undefined) {
583
+ tls.rejectUnauthorized = this._options.rejectUnauthorized;
584
+ }
583
585
  const optionsJson = JSON.stringify({
584
586
  method: this._options.method || "GET",
585
587
  headers: this._options.headers || {},
586
588
  body: this._body || null,
589
+ ...tls,
587
590
  });
588
- const responseJson = await _networkHttpRequestRaw.apply(undefined, [url, optionsJson], {
589
- result: { promise: true },
590
- });
591
- const response = JSON.parse(responseJson);
591
+ const response = await _networkHttpRequestRaw(url, optionsJson);
592
592
  this.finished = true;
593
593
  // 101 Switching Protocols → fire 'upgrade' event
594
594
  if (response.status === 101) {
@@ -648,12 +648,17 @@ export class ClientRequest {
648
648
  }
649
649
  }
650
650
  write(data) {
651
+ const addedBytes = typeof Buffer !== "undefined" ? Buffer.byteLength(data) : data.length;
652
+ if (this._bodyBytes + addedBytes > MAX_HTTP_BODY_BYTES) {
653
+ throw new Error("ERR_HTTP_BODY_TOO_LARGE: request body exceeds " + MAX_HTTP_BODY_BYTES + " byte limit");
654
+ }
651
655
  this._body += data;
656
+ this._bodyBytes += addedBytes;
652
657
  return true;
653
658
  }
654
659
  end(data) {
655
660
  if (data)
656
- this._body += data;
661
+ this.write(data);
657
662
  this._ended = true;
658
663
  return this;
659
664
  }
@@ -919,6 +924,7 @@ class ServerResponseBridge {
919
924
  writableFinished = false;
920
925
  _headers = new Map();
921
926
  _chunks = [];
927
+ _chunksBytes = 0;
922
928
  _listeners = {};
923
929
  _closedPromise;
924
930
  _resolveClosed = null;
@@ -993,12 +999,12 @@ class ServerResponseBridge {
993
999
  if (chunk == null)
994
1000
  return true;
995
1001
  this.headersSent = true;
996
- if (typeof chunk === "string") {
997
- this._chunks.push(Buffer.from(chunk));
998
- }
999
- else {
1000
- this._chunks.push(chunk);
1002
+ const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
1003
+ if (this._chunksBytes + buf.byteLength > MAX_HTTP_BODY_BYTES) {
1004
+ throw new Error("ERR_HTTP_BODY_TOO_LARGE: response body exceeds " + MAX_HTTP_BODY_BYTES + " byte limit");
1001
1005
  }
1006
+ this._chunks.push(buf);
1007
+ this._chunksBytes += buf.byteLength;
1002
1008
  return true;
1003
1009
  }
1004
1010
  end(chunk) {
@@ -1106,8 +1112,7 @@ class Server {
1106
1112
  if (typeof _networkHttpServerListenRaw === "undefined") {
1107
1113
  throw new Error("http.createServer requires NetworkAdapter.httpServerListen support");
1108
1114
  }
1109
- const resultJson = await _networkHttpServerListenRaw.apply(undefined, [JSON.stringify({ serverId: this._serverId, port, hostname })], { result: { promise: true } });
1110
- const result = JSON.parse(resultJson);
1115
+ const result = await _networkHttpServerListenRaw(JSON.stringify({ serverId: this._serverId, port, hostname }));
1111
1116
  this._address = result.address;
1112
1117
  this.listening = true;
1113
1118
  this._handleId = `http-server:${this._serverId}`;
@@ -1144,9 +1149,7 @@ class Server {
1144
1149
  await this._listenPromise;
1145
1150
  }
1146
1151
  if (this.listening && typeof _networkHttpServerCloseRaw !== "undefined") {
1147
- await _networkHttpServerCloseRaw.apply(undefined, [this._serverId], {
1148
- result: { promise: true },
1149
- });
1152
+ await _networkHttpServerCloseRaw(this._serverId);
1150
1153
  }
1151
1154
  this.listening = false;
1152
1155
  this._address = null;
@@ -1222,12 +1225,11 @@ class Server {
1222
1225
  }
1223
1226
  }
1224
1227
  /** Route an incoming HTTP request to the server's request listener and return the serialized response. */
1225
- async function dispatchServerRequest(serverId, requestJson) {
1228
+ async function dispatchServerRequest(serverId, request) {
1226
1229
  const listener = serverRequestListeners.get(serverId);
1227
1230
  if (!listener) {
1228
1231
  throw new Error(`Unknown HTTP server: ${serverId}`);
1229
1232
  }
1230
- const request = JSON.parse(requestJson);
1231
1233
  const incoming = new ServerIncomingMessage(request);
1232
1234
  const outgoing = new ServerResponseBridge();
1233
1235
  try {
@@ -1242,13 +1244,20 @@ async function dispatchServerRequest(serverId, requestJson) {
1242
1244
  }
1243
1245
  catch (err) {
1244
1246
  outgoing.statusCode = 500;
1245
- outgoing.end(err instanceof Error ? `Error: ${err.message}` : "Error");
1247
+ try {
1248
+ outgoing.end(err instanceof Error ? `Error: ${err.message}` : "Error");
1249
+ }
1250
+ catch {
1251
+ // Body cap may prevent writing error — finalize without data
1252
+ if (!outgoing.writableFinished)
1253
+ outgoing.end();
1254
+ }
1246
1255
  }
1247
1256
  if (!outgoing.writableFinished) {
1248
1257
  outgoing.end();
1249
1258
  }
1250
1259
  await outgoing.waitForClose();
1251
- return JSON.stringify(outgoing.serialize());
1260
+ return outgoing.serialize();
1252
1261
  }
1253
1262
  // Function-based ServerResponse constructor — allows .call() inheritance
1254
1263
  // used by light-my-request (Fastify's inject), which does
@@ -1262,6 +1271,7 @@ function ServerResponseCallable() {
1262
1271
  this.writableFinished = false;
1263
1272
  this._headers = new Map();
1264
1273
  this._chunks = [];
1274
+ this._chunksBytes = 0;
1265
1275
  this._listeners = {};
1266
1276
  this._closedPromise = new Promise((resolve) => {
1267
1277
  this._resolveClosed = resolve;
@@ -1287,10 +1297,17 @@ ServerResponseCallable.prototype = Object.create(ServerResponseBridge.prototype,
1287
1297
  constructor: { value: ServerResponseCallable, writable: true, configurable: true },
1288
1298
  });
1289
1299
  // Create HTTP module
1290
- function createHttpModule(_protocol) {
1300
+ function createHttpModule(protocol) {
1301
+ const defaultProtocol = protocol === "https" ? "https:" : "http:";
1291
1302
  const moduleAgent = new Agent({ keepAlive: false });
1292
1303
  // Set module-level globalAgent so ClientRequest defaults to it
1293
1304
  _moduleGlobalAgent = moduleAgent;
1305
+ // Ensure protocol is set on request options (defaults to module protocol)
1306
+ function ensureProtocol(opts) {
1307
+ if (!opts.protocol)
1308
+ return { ...opts, protocol: defaultProtocol };
1309
+ return opts;
1310
+ }
1294
1311
  return {
1295
1312
  request(options, callback) {
1296
1313
  let opts;
@@ -1314,7 +1331,7 @@ function createHttpModule(_protocol) {
1314
1331
  else {
1315
1332
  opts = options;
1316
1333
  }
1317
- return new ClientRequest(opts, callback);
1334
+ return new ClientRequest(ensureProtocol(opts), callback);
1318
1335
  },
1319
1336
  get(options, callback) {
1320
1337
  let opts;
@@ -1340,7 +1357,7 @@ function createHttpModule(_protocol) {
1340
1357
  else {
1341
1358
  opts = { ...options, method: "GET" };
1342
1359
  }
1343
- const req = new ClientRequest(opts, callback);
1360
+ const req = new ClientRequest(ensureProtocol(opts), callback);
1344
1361
  req.end();
1345
1362
  return req;
1346
1363
  },
@@ -19,6 +19,9 @@ export interface ProcessConfig {
19
19
  stdin?: string;
20
20
  timingMitigation?: "off" | "freeze";
21
21
  frozenTimeMs?: number;
22
+ stdinIsTTY?: boolean;
23
+ stdoutIsTTY?: boolean;
24
+ stderrIsTTY?: boolean;
22
25
  }
23
26
  /**
24
27
  * Thrown by `process.exit()` to unwind the sandbox call stack. The host
@@ -40,8 +40,8 @@ function getNowMs() {
40
40
  ? performance.now()
41
41
  : Date.now();
42
42
  }
43
- // Start time for uptime calculation
44
- const _processStartTime = getNowMs();
43
+ // Start time for uptime calculation (mutable for snapshot restore)
44
+ let _processStartTime = getNowMs();
45
45
  const BUFFER_MAX_LENGTH = typeof BufferPolyfill.kMaxLength ===
46
46
  "number"
47
47
  ? BufferPolyfill.kMaxLength
@@ -71,6 +71,18 @@ if (typeof bufferPolyfillMutable.constants !== "object" ||
71
71
  // Exit code tracking
72
72
  let _exitCode = 0;
73
73
  let _exited = false;
74
+ // Expose reset function for snapshot restore — resets mutable state
75
+ // captured in this closure so each restored context starts fresh.
76
+ globalThis.__runtimeResetProcessState =
77
+ function () {
78
+ _processStartTime =
79
+ typeof performance !== "undefined" && performance.now
80
+ ? performance.now()
81
+ : Date.now();
82
+ _exitCode = 0;
83
+ _exited = false;
84
+ delete globalThis.__runtimeResetProcessState;
85
+ };
74
86
  /**
75
87
  * Thrown by `process.exit()` to unwind the sandbox call stack. The host
76
88
  * catches this to extract the exit code without killing the isolate.
@@ -122,7 +134,7 @@ function _addListener(event, listener, once = false) {
122
134
  const warning = `MaxListenersExceededWarning: Possible EventEmitter memory leak detected. ${total} ${event} listeners added to [process]. MaxListeners is ${_processMaxListeners}. Use emitter.setMaxListeners() to increase limit`;
123
135
  // Use console.error to emit warning without recursion risk
124
136
  if (typeof _error !== "undefined") {
125
- _error.applySync(undefined, [warning]);
137
+ _error(warning);
126
138
  }
127
139
  }
128
140
  }
@@ -161,11 +173,15 @@ function _emit(event, ...args) {
161
173
  }
162
174
  return handled;
163
175
  }
176
+ // Resolve isTTY flags from config
177
+ const _stdinIsTTY = (typeof _processConfig !== "undefined" && _processConfig.stdinIsTTY) || false;
178
+ const _stdoutIsTTY = (typeof _processConfig !== "undefined" && _processConfig.stdoutIsTTY) || false;
179
+ const _stderrIsTTY = (typeof _processConfig !== "undefined" && _processConfig.stderrIsTTY) || false;
164
180
  // Stdout stream
165
181
  const _stdout = {
166
182
  write(data) {
167
183
  if (typeof _log !== "undefined") {
168
- _log.applySync(undefined, [String(data).replace(/\n$/, "")]);
184
+ _log(String(data).replace(/\n$/, ""));
169
185
  }
170
186
  return true;
171
187
  },
@@ -182,7 +198,7 @@ const _stdout = {
182
198
  return false;
183
199
  },
184
200
  writable: true,
185
- isTTY: false,
201
+ isTTY: _stdoutIsTTY,
186
202
  columns: 80,
187
203
  rows: 24,
188
204
  };
@@ -190,7 +206,7 @@ const _stdout = {
190
206
  const _stderr = {
191
207
  write(data) {
192
208
  if (typeof _error !== "undefined") {
193
- _error.applySync(undefined, [String(data).replace(/\n$/, "")]);
209
+ _error(String(data).replace(/\n$/, ""));
194
210
  }
195
211
  return true;
196
212
  },
@@ -207,7 +223,7 @@ const _stderr = {
207
223
  return false;
208
224
  },
209
225
  writable: true,
210
- isTTY: false,
226
+ isTTY: _stderrIsTTY,
211
227
  columns: 80,
212
228
  rows: 24,
213
229
  };
@@ -319,7 +335,16 @@ const _stdin = {
319
335
  this.encoding = enc;
320
336
  return this;
321
337
  },
322
- isTTY: false,
338
+ setRawMode(mode) {
339
+ if (!_stdinIsTTY) {
340
+ throw new Error("setRawMode is not supported when stdin is not a TTY");
341
+ }
342
+ if (typeof _ptySetRawMode !== "undefined") {
343
+ _ptySetRawMode(mode);
344
+ }
345
+ return this;
346
+ },
347
+ isTTY: _stdinIsTTY,
323
348
  // For readline compatibility
324
349
  [Symbol.asyncIterator]: async function* () {
325
350
  const lines = getStdinData().split("\n");
@@ -419,9 +444,9 @@ const process = {
419
444
  },
420
445
  chdir(dir) {
421
446
  // Validate directory exists in VFS before setting cwd
422
- let statJson;
447
+ let stat;
423
448
  try {
424
- statJson = _fs.stat.applySyncPromise(undefined, [dir]);
449
+ stat = _fs.stat(dir);
425
450
  }
426
451
  catch {
427
452
  const err = new Error(`ENOENT: no such file or directory, chdir '${dir}'`);
@@ -431,8 +456,7 @@ const process = {
431
456
  err.path = dir;
432
457
  throw err;
433
458
  }
434
- const parsed = JSON.parse(statJson);
435
- if (!parsed.isDirectory) {
459
+ if (!stat.isDirectory) {
436
460
  const err = new Error(`ENOTDIR: not a directory, chdir '${dir}'`);
437
461
  err.code = "ENOTDIR";
438
462
  err.errno = -20;
@@ -746,11 +770,7 @@ export function setTimeout(callback, delay, ...args) {
746
770
  const actualDelay = delay ?? 0;
747
771
  // Use host timer for actual delays if available and delay > 0
748
772
  if (typeof _scheduleTimer !== "undefined" && actualDelay > 0) {
749
- // _scheduleTimer.apply() returns a Promise that resolves after the delay
750
- // Using { result: { promise: true } } tells isolated-vm to wait for the
751
- // host Promise to resolve before resolving the apply() Promise
752
- _scheduleTimer
753
- .apply(undefined, [actualDelay], { result: { promise: true } })
773
+ _scheduleTimer(actualDelay)
754
774
  .then(() => {
755
775
  if (_timers.has(id)) {
756
776
  _timers.delete(id);
@@ -798,8 +818,7 @@ export function setInterval(callback, delay, ...args) {
798
818
  return; // Interval was cleared
799
819
  if (typeof _scheduleTimer !== "undefined" && actualDelay > 0) {
800
820
  // Use host timer for actual delays
801
- _scheduleTimer
802
- .apply(undefined, [actualDelay], { result: { promise: true } })
821
+ _scheduleTimer(actualDelay)
803
822
  .then(() => {
804
823
  if (_intervals.has(id)) {
805
824
  try {
@@ -871,8 +890,7 @@ export const cryptoPolyfill = {
871
890
  }
872
891
  const bytes = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
873
892
  try {
874
- const base64 = _cryptoRandomFill.applySync(undefined, [bytes.byteLength]);
875
- const hostBytes = BufferPolyfill.from(base64, "base64");
893
+ const hostBytes = _cryptoRandomFill(bytes.byteLength);
876
894
  if (hostBytes.byteLength !== bytes.byteLength) {
877
895
  throw new Error("invalid host entropy size");
878
896
  }
@@ -888,7 +906,7 @@ export const cryptoPolyfill = {
888
906
  throwUnsupportedCryptoApi("randomUUID");
889
907
  }
890
908
  try {
891
- const uuid = _cryptoRandomUUID.applySync(undefined, []);
909
+ const uuid = _cryptoRandomUUID();
892
910
  if (typeof uuid !== "string") {
893
911
  throw new Error("invalid host uuid");
894
912
  }