sandboxedjs 0.1.30 → 0.1.32

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.
package/dist/index.cjs CHANGED
@@ -4795,7 +4795,15 @@ var Pipe = class _Pipe {
4795
4795
  * whole line on every keystroke — so the terminal must stop echoing, or
4796
4796
  * every character appears twice.
4797
4797
  */
4798
- rawMode = false;
4798
+ raw = false;
4799
+ onRawMode;
4800
+ get rawMode() {
4801
+ return this.raw;
4802
+ }
4803
+ set rawMode(enabled) {
4804
+ this.raw = enabled;
4805
+ this.onRawMode?.(enabled);
4806
+ }
4799
4807
  columns;
4800
4808
  rows;
4801
4809
  get closed() {
@@ -5923,13 +5931,19 @@ var Kernel = class {
5923
5931
  stderr: opts.stderr ?? new NullOutput()
5924
5932
  }
5925
5933
  });
5934
+ const cancelWithParent = () => {
5935
+ if (parent?.termSignal) proc.deliver(parent.termSignal);
5936
+ };
5937
+ parent?.signal.addEventListener("abort", cancelWithParent, { once: true });
5938
+ if (parent?.signal.aborted) cancelWithParent();
5939
+ void proc.wait().then(() => parent?.signal.removeEventListener("abort", cancelWithParent));
5926
5940
  let timer;
5927
5941
  if (opts.timeoutMs !== void 0) {
5928
5942
  timer = setTimeout(() => {
5929
5943
  proc.deliver("SIGKILL");
5930
5944
  }, opts.timeoutMs);
5931
5945
  }
5932
- void this.dispatch(proc).then((code) => {
5946
+ void (proc.signal.aborted ? Promise.resolve(proc.exitCode ?? 137) : this.dispatch(proc)).then((code) => {
5933
5947
  proc.exit(code);
5934
5948
  }).catch((e) => {
5935
5949
  try {
@@ -7921,6 +7935,22 @@ var Shell = class _Shell {
7921
7935
  }
7922
7936
  return this.lastStatus;
7923
7937
  }
7938
+ /** Signal the current terminal pipeline without terminating the interactive shell. */
7939
+ interruptForeground(signal, stdin) {
7940
+ const processes = this.kernel.procs.list().filter((process2) => process2.running);
7941
+ const selected = new Set(processes.filter((process2) => process2.ppid === this.proc.pid && process2.stdin === stdin).map((process2) => process2.pid));
7942
+ for (let changed = true; changed; ) {
7943
+ changed = false;
7944
+ const outputs = processes.filter((process2) => selected.has(process2.pid)).flatMap((process2) => [process2.stdout, process2.stderr]);
7945
+ for (const process2 of processes) {
7946
+ if (!selected.has(process2.pid) && (selected.has(process2.ppid) || outputs.some((output) => output === process2.stdin))) {
7947
+ selected.add(process2.pid);
7948
+ changed = true;
7949
+ }
7950
+ }
7951
+ }
7952
+ for (const process2 of processes) if (selected.has(process2.pid)) process2.deliver(signal);
7953
+ }
7924
7954
  get isExiting() {
7925
7955
  return this.exiting;
7926
7956
  }
@@ -8570,6 +8600,7 @@ sys ${fmt2(Math.floor(ms * 0.2))}
8570
8600
  for (const assignment of assignments) {
8571
8601
  env2[assignment.name] = await expandWordToString(assignment.value, this.expandContext());
8572
8602
  }
8603
+ if (this.proc.signal.aborted) return this.proc.exitCode ?? 137;
8573
8604
  const proc = this.kernel.spawn(words, {
8574
8605
  cwd: this.cwd,
8575
8606
  env: env2,
@@ -8827,7 +8858,7 @@ async function runShell(ctxName, ctx) {
8827
8858
  const args = ctx.args.slice();
8828
8859
  let command;
8829
8860
  let scriptPath;
8830
- let readStdin2 = false;
8861
+ let readStdin = false;
8831
8862
  let interactive = false;
8832
8863
  let login = ctxName.startsWith("-");
8833
8864
  const options = {};
@@ -8840,7 +8871,7 @@ async function runShell(ctxName, ctx) {
8840
8871
  break;
8841
8872
  }
8842
8873
  if (arg === "-s") {
8843
- readStdin2 = true;
8874
+ readStdin = true;
8844
8875
  continue;
8845
8876
  }
8846
8877
  if (arg === "-i") {
@@ -8877,7 +8908,7 @@ async function runShell(ctxName, ctx) {
8877
8908
  break;
8878
8909
  }
8879
8910
  const rest = args.slice(i);
8880
- if (command === void 0 && !readStdin2 && rest.length > 0) {
8911
+ if (command === void 0 && !readStdin && rest.length > 0) {
8881
8912
  scriptPath = rest[0];
8882
8913
  }
8883
8914
  const positional = command !== void 0 ? rest.slice(1) : scriptPath !== void 0 ? rest.slice(1) : rest;
@@ -17772,6 +17803,143 @@ function mountContainerFs(FS, opts) {
17772
17803
  }
17773
17804
  }
17774
17805
 
17806
+ // src/runtime/python-syscalls.ts
17807
+ var EMPTY = new Uint8Array(0);
17808
+ function createStdinHost(ctx) {
17809
+ let pending = EMPTY;
17810
+ let eof = false;
17811
+ const take = (size) => {
17812
+ const n = Math.min(size, pending.length);
17813
+ const out = pending.subarray(0, n);
17814
+ pending = pending.subarray(n);
17815
+ return out;
17816
+ };
17817
+ return {
17818
+ /**
17819
+ * Drain a non-interactive stdin up front.
17820
+ *
17821
+ * A pipe ends, so reading it eagerly costs nothing and leaves every read
17822
+ * answerable without suspending — which is what keeps piped input working
17823
+ * on hosts with no stack-switching. A terminal never ends, so priming one
17824
+ * would hang before the program had printed its prompt.
17825
+ */
17826
+ async prime() {
17827
+ if (ctx.stdin.isTTY || ctx.stdin.interactive) return;
17828
+ try {
17829
+ pending = await ctx.stdin.readAll();
17830
+ } catch {
17831
+ pending = EMPTY;
17832
+ }
17833
+ eof = true;
17834
+ },
17835
+ buffered(size) {
17836
+ if (pending.length > 0) return take(size);
17837
+ return eof ? EMPTY : void 0;
17838
+ },
17839
+ async read(size) {
17840
+ if (pending.length > 0) return take(size);
17841
+ if (eof) return EMPTY;
17842
+ const chunk = await ctx.stdin.read(size);
17843
+ if (chunk === null || chunk.length === 0) {
17844
+ eof = true;
17845
+ return EMPTY;
17846
+ }
17847
+ pending = chunk;
17848
+ return take(size);
17849
+ },
17850
+ isatty() {
17851
+ return Boolean(ctx.stdin.isTTY);
17852
+ }
17853
+ };
17854
+ }
17855
+ var BRIDGE_PY = `
17856
+ import builtins, io, sys
17857
+ import _sbx_host as _host
17858
+ from pyodide.ffi import can_run_sync, run_sync
17859
+
17860
+
17861
+ def _to_bytes(value):
17862
+ if value is None:
17863
+ return b""
17864
+ to_bytes = getattr(value, "to_bytes", None)
17865
+ if to_bytes is not None:
17866
+ return to_bytes()
17867
+ return bytes(value.to_py())
17868
+
17869
+
17870
+ class _SbxStdin(io.RawIOBase):
17871
+ """The container's standard input, as a blocking raw stream."""
17872
+
17873
+ def readable(self):
17874
+ return True
17875
+
17876
+ def fileno(self):
17877
+ return 0
17878
+
17879
+ def isatty(self):
17880
+ return bool(_host.isatty())
17881
+
17882
+ def readinto(self, target):
17883
+ want = len(target)
17884
+ if want == 0:
17885
+ return 0
17886
+ ready = _host.buffered(want)
17887
+ if ready is None:
17888
+ if not can_run_sync():
17889
+ raise OSError(
17890
+ "this host cannot wait for interactive input: WebAssembly "
17891
+ "stack switching (JSPI) is unavailable. Pipe the input in, "
17892
+ "or use a browser or Node build that supports it."
17893
+ )
17894
+ ready = run_sync(_host.read(want))
17895
+ data = _to_bytes(ready)
17896
+ target[: len(data)] = data
17897
+ return len(data)
17898
+
17899
+
17900
+ def _install():
17901
+ stdin = io.TextIOWrapper(
17902
+ io.BufferedReader(_SbxStdin()), encoding="utf-8", errors="replace", line_buffering=True
17903
+ )
17904
+ sys.stdin = stdin
17905
+ sys.__stdin__ = stdin
17906
+
17907
+ def input(prompt=""):
17908
+ # CPython writes the prompt to stdout and strips exactly one trailing
17909
+ # newline; anything else changes what a program reads back.
17910
+ if prompt != "":
17911
+ sys.stdout.write(str(prompt))
17912
+ sys.stdout.flush()
17913
+ line = sys.stdin.readline()
17914
+ if not line:
17915
+ raise EOFError("EOF when reading a line")
17916
+ if line.endswith("\\n"):
17917
+ line = line[:-1]
17918
+ if line.endswith("\\r"):
17919
+ line = line[:-1]
17920
+ return line
17921
+
17922
+ builtins.input = input
17923
+
17924
+
17925
+ _install()
17926
+ `;
17927
+ function installSyscallBridge(py) {
17928
+ const slot = py.__sbxStdin ??= { active: null };
17929
+ py.registerJsModule("_sbx_host", {
17930
+ /* Not `?.` and `??`: `buffered` returns undefined to mean "you will have
17931
+ * to wait", and a nullish default would quietly turn that into end-of-file —
17932
+ * which is exactly the silent EOF this bridge exists to remove. */
17933
+ buffered: (size) => slot.active ? slot.active.buffered(size) : EMPTY,
17934
+ read: (size) => slot.active ? slot.active.read(size) : Promise.resolve(EMPTY),
17935
+ isatty: () => slot.active?.isatty() ?? false
17936
+ });
17937
+ py.runPython(BRIDGE_PY);
17938
+ }
17939
+ function bindStdin(py, host2) {
17940
+ py.__sbxStdin.active = host2;
17941
+ }
17942
+
17775
17943
  // src/runtime/cpython.ts
17776
17944
  var RESERVED_FOR_INTERPRETER = /* @__PURE__ */ new Set(["lib", "dev", "proc"]);
17777
17945
  var pyodideModule = null;
@@ -17844,6 +18012,7 @@ async function interpreterFor(ctx) {
17844
18012
  });
17845
18013
  mountContainerDirs(py, ctx);
17846
18014
  installRunner(py);
18015
+ installSyscallBridge(py);
17847
18016
  return py;
17848
18017
  })();
17849
18018
  interpreters.set(ctx.vfs, starting);
@@ -17938,7 +18107,7 @@ function reportError(ctx, error) {
17938
18107
  `);
17939
18108
  return 1;
17940
18109
  }
17941
- async function runCPythonProgram(ctx, source, argv, scriptDir, stdinText) {
18110
+ async function runCPythonProgram(ctx, source, argv, scriptDir = null) {
17942
18111
  let py;
17943
18112
  try {
17944
18113
  py = await interpreterFor(ctx);
@@ -17959,7 +18128,6 @@ async function runCPythonProgram(ctx, source, argv, scriptDir, stdinText) {
17959
18128
  });
17960
18129
  } catch {
17961
18130
  }
17962
- const encoder8 = new TextEncoder();
17963
18131
  const decoder8 = new TextDecoder();
17964
18132
  py.setStdout({
17965
18133
  write: (buffer) => {
@@ -17973,20 +18141,17 @@ async function runCPythonProgram(ctx, source, argv, scriptDir, stdinText) {
17973
18141
  return buffer.length;
17974
18142
  }
17975
18143
  });
17976
- if (stdinText !== null) {
17977
- const bytes2 = encoder8.encode(stdinText);
17978
- let offset = 0;
17979
- py.setStdin({
17980
- read: (buffer) => {
17981
- const take = Math.min(buffer.length, bytes2.length - offset);
17982
- buffer.set(bytes2.subarray(offset, offset + take));
17983
- offset += take;
17984
- return take;
17985
- }
17986
- });
17987
- } else {
17988
- py.setStdin({ read: () => 0 });
17989
- }
18144
+ const stdinHost = createStdinHost(ctx);
18145
+ await stdinHost.prime();
18146
+ bindStdin(py, stdinHost);
18147
+ py.setStdin({
18148
+ read: (buffer) => {
18149
+ const ready = stdinHost.buffered(buffer.length);
18150
+ if (!ready || ready.length === 0) return 0;
18151
+ buffer.set(ready);
18152
+ return ready.length;
18153
+ }
18154
+ });
17990
18155
  try {
17991
18156
  bootstrap(py, ctx, argv, scriptDir);
17992
18157
  } catch (error) {
@@ -18119,11 +18284,6 @@ function configurePython(options = {}) {
18119
18284
  configureCPython({ indexURL: options.indexURL, moduleURL: options.pyodideURL });
18120
18285
  }
18121
18286
  var isPythonAvailable = isCPythonAvailable;
18122
- async function readStdin(ctx) {
18123
- if (ctx.stdin.isTTY || ctx.stdin.interactive) return null;
18124
- const bytes2 = await ctx.stdin.readAll();
18125
- return bytes2.length ? new TextDecoder().decode(bytes2) : null;
18126
- }
18127
18287
  var python = defineCommand({
18128
18288
  name: "python3",
18129
18289
  path: "/usr/bin/python3",
@@ -18172,7 +18332,7 @@ compatible packages can be installed with pip (micropip).`,
18172
18332
  }
18173
18333
  const rest = argv.slice(i);
18174
18334
  if (command !== void 0) {
18175
- return (await runCPythonProgram(ctx, command, ["-c", ...rest], null, await readStdin(ctx))).exitCode;
18335
+ return (await runCPythonProgram(ctx, command, ["-c", ...rest], null)).exitCode;
18176
18336
  }
18177
18337
  if (moduleName !== void 0) {
18178
18338
  const program = `
@@ -18184,16 +18344,16 @@ except ImportError:
18184
18344
  print("/usr/bin/python3: No module named " + ${JSON.stringify(moduleName)}, file=sys.stderr)
18185
18345
  raise SystemExit(1)
18186
18346
  `;
18187
- return (await runCPythonProgram(ctx, program, [moduleName, ...rest], null, await readStdin(ctx))).exitCode;
18347
+ return (await runCPythonProgram(ctx, program, [moduleName, ...rest], null)).exitCode;
18188
18348
  }
18189
18349
  if (script === void 0) {
18190
18350
  if (ctx.stdin.isTTY) return runCPythonRepl(ctx);
18191
18351
  const source2 = new TextDecoder().decode(await ctx.stdin.readAll());
18192
- return source2.trim() ? (await runCPythonProgram(ctx, source2, ["", ...rest], null, null)).exitCode : 0;
18352
+ return source2.trim() ? (await runCPythonProgram(ctx, source2, ["", ...rest])).exitCode : 0;
18193
18353
  }
18194
18354
  if (script === "-") {
18195
18355
  const source2 = new TextDecoder().decode(await ctx.stdin.readAll());
18196
- return (await runCPythonProgram(ctx, source2, ["-", ...rest], null, null)).exitCode;
18356
+ return (await runCPythonProgram(ctx, source2, ["-", ...rest])).exitCode;
18197
18357
  }
18198
18358
  const abs = ctx.path(script);
18199
18359
  let source;
@@ -18204,7 +18364,7 @@ except ImportError:
18204
18364
  `);
18205
18365
  return 2;
18206
18366
  }
18207
- return (await runCPythonProgram(ctx, source, [abs, ...rest], dirname(abs), await readStdin(ctx))).exitCode;
18367
+ return (await runCPythonProgram(ctx, source, [abs, ...rest], dirname(abs))).exitCode;
18208
18368
  }
18209
18369
  });
18210
18370
  function printHelp2(ctx) {
@@ -19716,6 +19876,7 @@ var KernelChildProcess = class {
19716
19876
  this.stdin.isTTY = true;
19717
19877
  this.stdin.interactive = true;
19718
19878
  }
19879
+ this.stdin.onRawMode = (enabled) => this.emit("rawmode", enabled);
19719
19880
  this.kernel = kernel;
19720
19881
  this.cred = cred;
19721
19882
  this.pid = pid;
@@ -19832,7 +19993,7 @@ function installNodeChildProcessBridge(pod, kernel, cred) {
19832
19993
  const originalSpawn = manager.spawn;
19833
19994
  manager.spawn = (config) => {
19834
19995
  const resolved = kernel.resolveExecutable(config.command, config.cwd ?? "/", config.env ?? {}, cred);
19835
- if (resolved?.kind === "builtin" && resolved.command?.name !== "node" && resolved.command?.name !== "nodejs") {
19996
+ if (resolved?.kind === "builtin") {
19836
19997
  return new KernelChildProcess(kernel, cred, config, nextBridgePid++);
19837
19998
  }
19838
19999
  return originalSpawn.call(manager, config);
@@ -19843,6 +20004,68 @@ function installNodeChildProcessBridge(pod, kernel, cred) {
19843
20004
  };
19844
20005
  }
19845
20006
 
20007
+ // src/runtime/host-module-tracker.ts
20008
+ var HostModuleTracker = class {
20009
+ active = 0;
20010
+ disposed = false;
20011
+ dispose() {
20012
+ this.disposed = true;
20013
+ }
20014
+ wrapped = /* @__PURE__ */ new WeakMap();
20015
+ originals = /* @__PURE__ */ new WeakMap();
20016
+ pending = () => this.active;
20017
+ unwrap = (value) => value && (typeof value === "object" || typeof value === "function") ? this.originals.get(value) ?? value : value;
20018
+ wrap(value) {
20019
+ if (value === null || typeof value !== "object" && typeof value !== "function") return value;
20020
+ const object = value;
20021
+ if (Array.isArray(value) || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return value;
20022
+ if (this.wrapped.has(object)) return this.wrapped.get(object);
20023
+ const tracker = this;
20024
+ const proxy = new Proxy(object, {
20025
+ // CommonJS native loaders re-export by assigning exports back onto the
20026
+ // binding object. Never store a process-owned proxy in a shared module.
20027
+ set(target, key, member) {
20028
+ return Reflect.set(target, key, tracker.unwrap(member), target);
20029
+ },
20030
+ defineProperty(target, key, descriptor) {
20031
+ return Reflect.defineProperty(target, key, "value" in descriptor ? { ...descriptor, value: tracker.unwrap(descriptor.value) } : descriptor);
20032
+ },
20033
+ get(target, key) {
20034
+ const descriptor = Reflect.getOwnPropertyDescriptor(target, key);
20035
+ const member = Reflect.get(target, key, target);
20036
+ if (descriptor && !descriptor.configurable && "value" in descriptor && !descriptor.writable) return member;
20037
+ return typeof member === "function" ? tracker.wrap(member) : member;
20038
+ },
20039
+ apply(target, receiver, args) {
20040
+ const result = Reflect.apply(target, tracker.unwrap(receiver), args.map(tracker.unwrap));
20041
+ if (result && typeof result.then === "function") {
20042
+ tracker.active++;
20043
+ return Promise.resolve(result).then(
20044
+ (value2) => {
20045
+ tracker.active--;
20046
+ return tracker.disposed ? new Promise(() => {
20047
+ }) : tracker.wrap(value2);
20048
+ },
20049
+ (error) => {
20050
+ tracker.active--;
20051
+ if (tracker.disposed) return new Promise(() => {
20052
+ });
20053
+ throw error;
20054
+ }
20055
+ );
20056
+ }
20057
+ return tracker.wrap(result);
20058
+ },
20059
+ construct(target, args, newTarget) {
20060
+ return tracker.wrap(Reflect.construct(target, args.map(tracker.unwrap), tracker.unwrap(newTarget)));
20061
+ }
20062
+ });
20063
+ this.wrapped.set(object, proxy);
20064
+ this.originals.set(proxy, object);
20065
+ return proxy;
20066
+ }
20067
+ };
20068
+
19846
20069
  // src/runtime/commonjs-engine.ts
19847
20070
  init_path();
19848
20071
  var HELPERS = {
@@ -21580,6 +21803,7 @@ var VirtualHttpServer = class extends EventEmitter4__default.default {
21580
21803
  owner;
21581
21804
  listening = false;
21582
21805
  portValue = null;
21806
+ referenced = true;
21583
21807
  listen(...args) {
21584
21808
  const callback = typeof args.at(-1) === "function" ? args.pop() : void 0;
21585
21809
  const first = args[0];
@@ -21608,11 +21832,16 @@ var VirtualHttpServer = class extends EventEmitter4__default.default {
21608
21832
  return this.portValue === null ? null : { address: "0.0.0.0", family: "IPv4", port: this.portValue };
21609
21833
  }
21610
21834
  ref() {
21835
+ this.referenced = true;
21611
21836
  return this;
21612
21837
  }
21613
21838
  unref() {
21839
+ this.referenced = false;
21614
21840
  return this;
21615
21841
  }
21842
+ hasRef() {
21843
+ return this.referenced;
21844
+ }
21616
21845
  setTimeout(_milliseconds, callback) {
21617
21846
  if (callback) this.on("timeout", callback);
21618
21847
  return this;
@@ -21622,13 +21851,16 @@ var VirtualHttpRouter = class {
21622
21851
  servers = /* @__PURE__ */ new Map();
21623
21852
  /** Notified when a server begins listening, for `onServerReady`. */
21624
21853
  onListen;
21854
+ onClose;
21625
21855
  register(port, server, owner) {
21626
21856
  if (this.servers.has(port)) throw Object.assign(new Error(`listen EADDRINUSE: address already in use 0.0.0.0:${port}`), { code: "EADDRINUSE", port });
21627
21857
  this.servers.set(port, { server, owner });
21628
21858
  this.onListen?.(port);
21629
21859
  }
21630
21860
  unregister(port, server) {
21631
- if (this.servers.get(port)?.server === server) this.servers.delete(port);
21861
+ if (this.servers.get(port)?.server !== server) return;
21862
+ this.servers.delete(port);
21863
+ this.onClose?.(port);
21632
21864
  }
21633
21865
  /** Whether anything in this container is listening on `port`. */
21634
21866
  activePortsIncludes(port) {
@@ -21637,6 +21869,9 @@ var VirtualHttpRouter = class {
21637
21869
  activePorts(owner) {
21638
21870
  return [...this.servers].filter(([, item]) => owner === void 0 || item.owner === owner).map(([port]) => port).sort((a, b) => a - b);
21639
21871
  }
21872
+ referencedPorts(owner) {
21873
+ return [...this.servers].filter(([, item]) => item.owner === owner && item.server.hasRef()).map(([port]) => port);
21874
+ }
21640
21875
  closeOwner(owner) {
21641
21876
  for (const { server, owner: value } of [...this.servers.values()]) if (value === owner) server.close();
21642
21877
  }
@@ -22048,6 +22283,108 @@ function hashFor(algorithm) {
22048
22283
  function toBytes2(data, encoding) {
22049
22284
  return typeof data === "string" ? Buffer2.from(data, encoding) : data;
22050
22285
  }
22286
+
22287
+ // src/runtime/sync-channel.ts
22288
+ var STATE = 0;
22289
+ var LENGTH = 1;
22290
+ var MORE = 2;
22291
+ var CONTROL_WORDS = 4;
22292
+ var STATE_REQUEST = 1;
22293
+ var STATE_RESPONSE = 2;
22294
+ var STATE_CONTINUE = 3;
22295
+ var STATE_CLOSED = 4;
22296
+ function createSyncChannelBuffers(capacityBytes = 1 << 20) {
22297
+ return {
22298
+ control: new SharedArrayBuffer(CONTROL_WORDS * Int32Array.BYTES_PER_ELEMENT),
22299
+ data: new SharedArrayBuffer(capacityBytes)
22300
+ };
22301
+ }
22302
+ function syncChannelUnavailableReason() {
22303
+ if (typeof globalThis.crossOriginIsolated === "boolean" && !globalThis.crossOriginIsolated) {
22304
+ return "The browser host is not cross-origin isolated. Serve the host app over HTTPS or localhost with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp. For a built app use sandboxedjs-serve <directory>. Embedded browsers or iframe policies may also prevent isolation.";
22305
+ }
22306
+ if (typeof SharedArrayBuffer !== "function") return "SharedArrayBuffer is unavailable in this host.";
22307
+ if (typeof Atomics !== "object" || typeof Atomics.wait !== "function") return "Atomics.wait is unavailable in this host.";
22308
+ return null;
22309
+ }
22310
+ function syncChannelSupported() {
22311
+ return syncChannelUnavailableReason() === null;
22312
+ }
22313
+ var SyncChannelServer = class {
22314
+ constructor(buffers, handle) {
22315
+ this.handle = handle;
22316
+ this.control = new Int32Array(buffers.control);
22317
+ this.data = new Uint8Array(buffers.data);
22318
+ this.capacity = this.data.length;
22319
+ }
22320
+ handle;
22321
+ control;
22322
+ data;
22323
+ capacity;
22324
+ /** Request chunks gathered so far, and the response still to be sent. */
22325
+ incoming = [];
22326
+ outgoing = null;
22327
+ sent = 0;
22328
+ closed = false;
22329
+ /** Call from the wake message the client sends after each chunk. */
22330
+ async pump() {
22331
+ if (this.closed) return;
22332
+ const state = Atomics.load(this.control, STATE);
22333
+ if (state === STATE_REQUEST) {
22334
+ const size = Atomics.load(this.control, LENGTH);
22335
+ this.incoming.push(this.data.slice(0, size));
22336
+ if (Atomics.load(this.control, MORE) === 1) {
22337
+ this.publish(STATE_CONTINUE);
22338
+ return;
22339
+ }
22340
+ const request = concat3(this.incoming);
22341
+ this.incoming = [];
22342
+ let response;
22343
+ try {
22344
+ response = await this.handle(request);
22345
+ } catch {
22346
+ response = new Uint8Array();
22347
+ }
22348
+ if (this.closed) return;
22349
+ this.outgoing = response;
22350
+ this.sent = 0;
22351
+ this.sendChunk();
22352
+ return;
22353
+ }
22354
+ if (state === STATE_CONTINUE) this.sendChunk();
22355
+ }
22356
+ /** Release a blocked client, e.g. when the container is torn down. */
22357
+ close() {
22358
+ if (this.closed) return;
22359
+ this.closed = true;
22360
+ Atomics.store(this.control, STATE, STATE_CLOSED);
22361
+ Atomics.notify(this.control, STATE);
22362
+ }
22363
+ sendChunk() {
22364
+ const payload = this.outgoing ?? new Uint8Array();
22365
+ const size = Math.min(this.capacity, payload.length - this.sent);
22366
+ this.data.set(payload.subarray(this.sent, this.sent + size), 0);
22367
+ this.sent += size;
22368
+ Atomics.store(this.control, LENGTH, size);
22369
+ Atomics.store(this.control, MORE, this.sent < payload.length ? 1 : 0);
22370
+ this.publish(STATE_RESPONSE);
22371
+ }
22372
+ publish(state) {
22373
+ Atomics.store(this.control, STATE, state);
22374
+ Atomics.notify(this.control, STATE);
22375
+ }
22376
+ };
22377
+ function concat3(parts) {
22378
+ if (parts.length === 1) return parts[0];
22379
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
22380
+ const joined = new Uint8Array(total);
22381
+ let at = 0;
22382
+ for (const part of parts) {
22383
+ joined.set(part, at);
22384
+ at += part.length;
22385
+ }
22386
+ return joined;
22387
+ }
22051
22388
  var ChildProcess = class extends EventEmitter4__default.default {
22052
22389
  stdout = new streamModule4__default.default.PassThrough();
22053
22390
  stderr = new streamModule4__default.default.PassThrough();
@@ -22252,7 +22589,7 @@ function unavailable(name) {
22252
22589
  const list = Array.isArray(args[1]) ? args[1].map(String) : [];
22253
22590
  const command = file3 ? [file3, ...list].join(" ") : void 0;
22254
22591
  const error = new Error(
22255
- `child_process.${name} is not supported in the sandboxed runtime` + (command ? ` (tried to run: ${command})` : "") + `: the caller, the child and the event loop share one thread, so blocking the caller would also stop the child. Run it with the asynchronous form (spawn/exec/execFile), or run the command from the container shell.`
22592
+ `child_process.${name} is not supported in the sandboxed runtime` + (command ? ` (tried to run: ${command})` : "") + `: the caller, the child and the event loop share one thread, so blocking the caller would also stop the child. Enable the SandboxedJs worker runtime with createContainer({ isolation: "worker" }) and fix any boot prerequisite it reports. ` + (syncChannelUnavailableReason() ?? "This process is using the realm runtime, which cannot provide synchronous child processes.")
22256
22593
  );
22257
22594
  error.code = "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM";
22258
22595
  throw error;
@@ -22540,6 +22877,11 @@ function createReadlineModule(defaultInput, defaultOutput = () => void 0) {
22540
22877
  edit(sequence, key) {
22541
22878
  if (this.closed) return;
22542
22879
  const name = key.name;
22880
+ if (key.ctrl && name === "c") {
22881
+ if (this.listenerCount("SIGINT") > 0) this.emit("SIGINT");
22882
+ else this.close();
22883
+ return;
22884
+ }
22543
22885
  if (name === "return" || name === "enter") {
22544
22886
  const value = this.line;
22545
22887
  this.line = "";
@@ -22635,7 +22977,7 @@ function createReadlineModule(defaultInput, defaultOutput = () => void 0) {
22635
22977
  };
22636
22978
  const createInterface = (options, output) => {
22637
22979
  if (options && typeof options === "object" && !options.on) {
22638
- const resolvedOutput2 = options.output ?? defaultOutput();
22980
+ const resolvedOutput2 = options.output;
22639
22981
  const instance = new Interface(
22640
22982
  options.input ?? defaultInput(),
22641
22983
  resolvedOutput2,
@@ -22644,7 +22986,7 @@ function createReadlineModule(defaultInput, defaultOutput = () => void 0) {
22644
22986
  if (typeof options.prompt === "string") instance.setPrompt(options.prompt);
22645
22987
  return instance;
22646
22988
  }
22647
- const resolvedOutput = output ?? defaultOutput();
22989
+ const resolvedOutput = output;
22648
22990
  return new Interface(options ?? defaultInput(), resolvedOutput, isTerminal(void 0, resolvedOutput));
22649
22991
  };
22650
22992
  const noop = () => {
@@ -22913,7 +23255,7 @@ function createCoreModules(options) {
22913
23255
  "stream/promises": createStreamPromises(),
22914
23256
  string_decoder: stringDecoderModule__default.default,
22915
23257
  timers: { ...timersModule__default.default, ...timers.api },
22916
- "timers/promises": createTimerPromises(),
23258
+ "timers/promises": createTimerPromises(timers.api),
22917
23259
  tty: { isatty: () => false, ReadStream: streamModule4__default.default.Readable, WriteStream: streamModule4__default.default.Writable },
22918
23260
  url: url_module_default,
22919
23261
  util: util_module_default,
@@ -23658,10 +24000,10 @@ function createTrackedTimers() {
23658
24000
  cancelAll
23659
24001
  };
23660
24002
  }
23661
- function createTimerPromises() {
24003
+ function createTimerPromises(timers) {
23662
24004
  return {
23663
- setTimeout: (delay, value) => new Promise((resolve2) => setTimeout(resolve2, delay, value)),
23664
- setImmediate: (value) => new Promise((resolve2) => setTimeout(resolve2, 0, value))
24005
+ setTimeout: (delay, value) => new Promise((resolve2) => timers.setTimeout(resolve2, delay, value)),
24006
+ setImmediate: (value) => new Promise((resolve2) => timers.setImmediate(resolve2, value))
23665
24007
  };
23666
24008
  }
23667
24009
  function createAsyncHooksModule() {
@@ -24369,11 +24711,8 @@ function ensureProcessGlobal() {
24369
24711
  // src/runtime/host-rolldown.ts
24370
24712
  var BUNDLER_HINT = "If this is a bundler pre-bundling the worker away, exclude the binding from dependency optimisation \u2014 in Vite:\n\n optimizeDeps: { exclude: ['@rolldown/binding-wasm32-wasi'] }";
24371
24713
  async function loadHostRolldownBinding() {
24372
- if (typeof window !== "undefined" && globalThis.crossOriginIsolated !== true) {
24373
- throw new Error(
24374
- "Vite 8/Rolldown requires cross-origin isolation. Serve the app with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp headers."
24375
- );
24376
- }
24714
+ const reason = syncChannelUnavailableReason();
24715
+ if (reason) throw new Error(`SandboxedJs threaded WASM unavailable: ${reason}`);
24377
24716
  try {
24378
24717
  const loaded = await import('@rolldown/binding-wasm32-wasi');
24379
24718
  return "__fs" in loaded ? { ...loaded } : loaded.default ?? loaded;
@@ -24381,9 +24720,9 @@ async function loadHostRolldownBinding() {
24381
24720
  if (typeof process !== "undefined" && process.env?.SANDBOXEDJS_DEBUG) {
24382
24721
  console.error("[sandboxedjs] Rolldown WASI binding unavailable:", error);
24383
24722
  }
24384
- const reason = error instanceof Error ? error.message : String(error);
24723
+ const reason2 = error instanceof Error ? error.message : String(error);
24385
24724
  throw new Error(
24386
- `The optional Rolldown WASI binding could not be loaded: ${reason}` + (typeof window === "undefined" ? "" : `
24725
+ `The optional Rolldown WASI binding could not be loaded: ${reason2}` + (typeof window === "undefined" ? "" : `
24387
24726
 
24388
24727
  ${BUNDLER_HINT}`),
24389
24728
  { cause: error }
@@ -24494,6 +24833,8 @@ var LocalProcess = class extends EventEmitter4__default.default {
24494
24833
  }
24495
24834
  /** End the process now, for an asynchronous `process.exit`. */
24496
24835
  exitNow(code) {
24836
+ this.killed = true;
24837
+ this.cleanup?.();
24497
24838
  this.finish(code);
24498
24839
  this.resolveKilled();
24499
24840
  }
@@ -24677,6 +25018,7 @@ var LocalRuntimePod = class _LocalRuntimePod {
24677
25018
  )
24678
25019
  };
24679
25020
  disposed = false;
25021
+ running = /* @__PURE__ */ new Set();
24680
25022
  workdir;
24681
25023
  env;
24682
25024
  aliases;
@@ -24718,8 +25060,9 @@ var LocalRuntimePod = class _LocalRuntimePod {
24718
25060
  const cwd = typeof options.cwd === "string" ? options.cwd : this.workdir;
24719
25061
  const env2 = { ...this.env, ...isRecord(options.env) ? options.env : {} };
24720
25062
  const owner = `${this.instanceId}:${Math.random().toString(36).slice(2)}`;
24721
- return new LocalProcess(async (proc) => {
25063
+ const process2 = new LocalProcess(async (proc) => {
24722
25064
  await this.prepareRolldown(cwd, env2);
25065
+ if (proc.isKilled()) return 137;
24723
25066
  const untrack = trackProcess(proc);
24724
25067
  let requestedExit = 0;
24725
25068
  let engine;
@@ -24747,7 +25090,9 @@ var LocalRuntimePod = class _LocalRuntimePod {
24747
25090
  onRawMode: (enabled) => proc.emit("rawmode", enabled)
24748
25091
  });
24749
25092
  proc.acceptInput((data) => core.writeStdin(data), () => core.endStdin());
25093
+ const hostWork = new HostModuleTracker();
24750
25094
  proc.onKill(() => {
25095
+ hostWork.dispose();
24751
25096
  core.cancelTimers();
24752
25097
  this.router.closeOwner(owner);
24753
25098
  });
@@ -24756,21 +25101,22 @@ var LocalRuntimePod = class _LocalRuntimePod {
24756
25101
  builtins: core.builtins,
24757
25102
  globals: core.globals,
24758
25103
  aliases: this.aliases,
24759
- overrides: this.modules
25104
+ overrides: Object.fromEntries(Object.entries(this.modules).map(([key, value]) => [key, hostWork.wrap(value)]))
24760
25105
  });
24761
25106
  try {
24762
- await engine.run(script);
24763
- await this.settle(owner, core.pendingHandles, core.pendingUnrefed, core.readingStdin, core.pendingRequests, () => proc.isKilled());
24764
- if (this.router.activePorts(owner).length) {
24765
- await proc.waitForKill();
24766
- return 137;
24767
- }
25107
+ await Promise.race([engine.run(script), proc.waitForKill()]);
25108
+ await this.settle(owner, core.pendingHandles, core.readingStdin, () => core.pendingRequests() + hostWork.pending(), () => proc.isKilled());
24768
25109
  return requestedExit;
24769
25110
  } finally {
25111
+ hostWork.dispose();
25112
+ core.cancelTimers();
24770
25113
  untrack();
24771
25114
  this.router.closeOwner(owner);
24772
25115
  }
24773
25116
  });
25117
+ this.running.add(process2);
25118
+ void process2.completion.then(() => this.running.delete(process2));
25119
+ return process2;
24774
25120
  }
24775
25121
  /**
24776
25122
  * Rolldown's JavaScript API synchronously requires its compiled binding.
@@ -24806,19 +25152,13 @@ var LocalRuntimePod = class _LocalRuntimePod {
24806
25152
  * A plain script that has genuinely finished falls straight through both,
24807
25153
  * costing a handful of empty turns.
24808
25154
  */
24809
- async settle(owner, pendingHandles, pendingUnrefed, readingStdin, pendingRequests, killed) {
24810
- for (let turn = 0; turn < DRAIN_TURNS; turn++) {
24811
- if (this.router.activePorts(owner).length || killed()) return;
24812
- await new Promise((resolve2) => setTimeout(resolve2, 0));
24813
- }
24814
- if (pendingHandles() === 0 && pendingRequests() === 0 && pendingUnrefed() > 0 && !readingStdin()) {
24815
- const deadline = Date.now() + 1e3;
24816
- while (!this.router.activePorts(owner).length && !killed() && Date.now() < deadline) {
24817
- await new Promise((resolve2) => setTimeout(resolve2, 5));
24818
- }
24819
- }
24820
- while (!this.router.activePorts(owner).length && !killed() && (pendingHandles() > 0 || pendingRequests() > 0 || readingStdin())) {
24821
- await new Promise((resolve2) => setTimeout(resolve2, 5));
25155
+ async settle(owner, pendingHandles, readingStdin, pendingRequests, killed) {
25156
+ let idleTurns = 0;
25157
+ while (!killed()) {
25158
+ const busy = this.router.referencedPorts(owner).length > 0 || pendingHandles() > 0 || pendingRequests() > 0 || readingStdin();
25159
+ idleTurns = busy ? 0 : idleTurns + 1;
25160
+ if (idleTurns >= DRAIN_TURNS) return;
25161
+ await new Promise((resolve2) => setTimeout(resolve2, busy ? 5 : 0));
24822
25162
  }
24823
25163
  }
24824
25164
  async request(_port, _init = {}) {
@@ -24835,6 +25175,8 @@ var LocalRuntimePod = class _LocalRuntimePod {
24835
25175
  }
24836
25176
  teardown() {
24837
25177
  this.disposed = true;
25178
+ for (const process2 of this.running) process2.kill();
25179
+ this.running.clear();
24838
25180
  this.router.closeAll();
24839
25181
  this.esbuild?.stop();
24840
25182
  }
@@ -24904,103 +25246,6 @@ function attachRolldownMirror(binding, volume, root) {
24904
25246
  volume.attach(fs, root);
24905
25247
  }
24906
25248
 
24907
- // src/runtime/sync-channel.ts
24908
- var STATE = 0;
24909
- var LENGTH = 1;
24910
- var MORE = 2;
24911
- var CONTROL_WORDS = 4;
24912
- var STATE_REQUEST = 1;
24913
- var STATE_RESPONSE = 2;
24914
- var STATE_CONTINUE = 3;
24915
- var STATE_CLOSED = 4;
24916
- function createSyncChannelBuffers(capacityBytes = 1 << 20) {
24917
- return {
24918
- control: new SharedArrayBuffer(CONTROL_WORDS * Int32Array.BYTES_PER_ELEMENT),
24919
- data: new SharedArrayBuffer(capacityBytes)
24920
- };
24921
- }
24922
- function syncChannelSupported() {
24923
- if (typeof SharedArrayBuffer !== "function") return false;
24924
- if (typeof Atomics !== "object" || typeof Atomics.wait !== "function") return false;
24925
- if (typeof window !== "undefined" && globalThis.crossOriginIsolated !== true) return false;
24926
- return true;
24927
- }
24928
- var SyncChannelServer = class {
24929
- constructor(buffers, handle) {
24930
- this.handle = handle;
24931
- this.control = new Int32Array(buffers.control);
24932
- this.data = new Uint8Array(buffers.data);
24933
- this.capacity = this.data.length;
24934
- }
24935
- handle;
24936
- control;
24937
- data;
24938
- capacity;
24939
- /** Request chunks gathered so far, and the response still to be sent. */
24940
- incoming = [];
24941
- outgoing = null;
24942
- sent = 0;
24943
- closed = false;
24944
- /** Call from the wake message the client sends after each chunk. */
24945
- async pump() {
24946
- if (this.closed) return;
24947
- const state = Atomics.load(this.control, STATE);
24948
- if (state === STATE_REQUEST) {
24949
- const size = Atomics.load(this.control, LENGTH);
24950
- this.incoming.push(this.data.slice(0, size));
24951
- if (Atomics.load(this.control, MORE) === 1) {
24952
- this.publish(STATE_CONTINUE);
24953
- return;
24954
- }
24955
- const request = concat3(this.incoming);
24956
- this.incoming = [];
24957
- let response;
24958
- try {
24959
- response = await this.handle(request);
24960
- } catch {
24961
- response = new Uint8Array();
24962
- }
24963
- if (this.closed) return;
24964
- this.outgoing = response;
24965
- this.sent = 0;
24966
- this.sendChunk();
24967
- return;
24968
- }
24969
- if (state === STATE_CONTINUE) this.sendChunk();
24970
- }
24971
- /** Release a blocked client, e.g. when the container is torn down. */
24972
- close() {
24973
- if (this.closed) return;
24974
- this.closed = true;
24975
- Atomics.store(this.control, STATE, STATE_CLOSED);
24976
- Atomics.notify(this.control, STATE);
24977
- }
24978
- sendChunk() {
24979
- const payload = this.outgoing ?? new Uint8Array();
24980
- const size = Math.min(this.capacity, payload.length - this.sent);
24981
- this.data.set(payload.subarray(this.sent, this.sent + size), 0);
24982
- this.sent += size;
24983
- Atomics.store(this.control, LENGTH, size);
24984
- Atomics.store(this.control, MORE, this.sent < payload.length ? 1 : 0);
24985
- this.publish(STATE_RESPONSE);
24986
- }
24987
- publish(state) {
24988
- Atomics.store(this.control, STATE, state);
24989
- Atomics.notify(this.control, STATE);
24990
- }
24991
- };
24992
- function concat3(parts) {
24993
- if (parts.length === 1) return parts[0];
24994
- const total = parts.reduce((sum, part) => sum + part.length, 0);
24995
- const joined = new Uint8Array(total);
24996
- let at = 0;
24997
- for (const part of parts) {
24998
- joined.set(part, at);
24999
- at += part.length;
25000
- }
25001
- return joined;
25002
- }
25003
-
25004
25249
  // src/runtime/remote-volume.ts
25005
25250
  var encoder7 = new TextEncoder();
25006
25251
  var decoder7 = new TextDecoder();
@@ -25095,24 +25340,32 @@ function defaultWorkerUrl() {
25095
25340
  return new URL("./worker-entry.js", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
25096
25341
  }
25097
25342
  function hasDomWorker() {
25098
- return typeof Worker === "function" && typeof document !== "undefined";
25343
+ return typeof Worker === "function";
25099
25344
  }
25100
25345
  async function startRuntimeWorker(options = {}) {
25101
25346
  const url = options.url ?? defaultWorkerUrl();
25102
25347
  const worker = hasDomWorker() ? await startDomWorker(url) : await startNodeWorker(url, options.workerData ?? {});
25103
- await new Promise((resolve2, reject) => {
25104
- const timer = setTimeout(() => reject(new Error("the runtime worker did not start in time")), options.timeoutMs ?? 1e4);
25105
- worker.onMessage((message) => {
25106
- if (message?.type === "sandboxedjs:ready") {
25348
+ try {
25349
+ await new Promise((resolve2, reject) => {
25350
+ const timer = setTimeout(() => reject(new Error("the runtime worker did not start in time")), options.timeoutMs ?? 1e4);
25351
+ worker.onMessage((message) => {
25352
+ if (message?.type === "sandboxedjs:ready") {
25353
+ clearTimeout(timer);
25354
+ resolve2();
25355
+ }
25356
+ });
25357
+ worker.onError((error) => {
25107
25358
  clearTimeout(timer);
25108
- resolve2();
25109
- }
25110
- });
25111
- worker.onError((error) => {
25112
- clearTimeout(timer);
25113
- reject(error instanceof Error ? error : new Error(String(error)));
25359
+ reject(error instanceof Error ? error : new Error(String(error)));
25360
+ });
25114
25361
  });
25115
- });
25362
+ } catch (error) {
25363
+ try {
25364
+ await worker.terminate();
25365
+ } catch {
25366
+ }
25367
+ throw error;
25368
+ }
25116
25369
  return worker;
25117
25370
  }
25118
25371
  async function startDomWorker(url) {
@@ -25173,6 +25426,8 @@ var WorkerProcess = class extends EventEmitter4__default.default {
25173
25426
  started = false;
25174
25427
  pendingInput = [];
25175
25428
  inputEnded = false;
25429
+ inheritedInput;
25430
+ ownRawMode = false;
25176
25431
  on(event, listener) {
25177
25432
  return super.on(event, listener);
25178
25433
  }
@@ -25198,13 +25453,38 @@ var WorkerProcess = class extends EventEmitter4__default.default {
25198
25453
  * program is still loading, and dropping them loses the answer to a prompt.
25199
25454
  */
25200
25455
  write(data) {
25456
+ if (this.inheritedInput) {
25457
+ this.inheritedInput.sendStdin(data);
25458
+ return;
25459
+ }
25201
25460
  if (this.started) this.worker.postMessage({ type: "stdin", data });
25202
25461
  else this.pendingInput.push(data);
25203
25462
  }
25204
25463
  endInput() {
25205
25464
  this.inputEnded = true;
25465
+ if (this.inheritedInput) {
25466
+ this.inheritedInput.endStdin?.();
25467
+ return;
25468
+ }
25206
25469
  if (this.started) this.worker.postMessage({ type: "stdin-end" });
25207
25470
  }
25471
+ rawMode(enabled) {
25472
+ this.ownRawMode = enabled;
25473
+ if (!this.inheritedInput) this.emit("rawmode", enabled);
25474
+ }
25475
+ /** The parent is blocked in Atomics.wait, so inherited input must bypass it. */
25476
+ inheritInput(child) {
25477
+ this.inheritedInput = child;
25478
+ this.emit("rawmode", false);
25479
+ child.on("rawmode", (enabled) => {
25480
+ if (this.inheritedInput === child) this.emit("rawmode", Boolean(enabled));
25481
+ });
25482
+ return () => {
25483
+ if (this.inheritedInput !== child) return;
25484
+ this.inheritedInput = void 0;
25485
+ this.emit("rawmode", this.ownRawMode);
25486
+ };
25487
+ }
25208
25488
  kill(_signal = "SIGTERM") {
25209
25489
  this.worker.postMessage({ type: "kill" });
25210
25490
  this.finish(137);
@@ -25225,25 +25505,30 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25225
25505
  super(options);
25226
25506
  this.workerUrl = options.workerUrl;
25227
25507
  }
25228
- /**
25229
- * Boot a Worker-backed pod, or return null when this host cannot support one.
25230
- *
25231
- * Declining is a first-class outcome. `SharedArrayBuffer` needs cross-origin
25232
- * isolation, a bundler may have made the guest script unreachable, and a host
25233
- * that supplied its own module objects has handed over things no thread
25234
- * boundary can carry. In every case the caller falls back to the in-realm pod
25235
- * and keeps working.
25236
- */
25237
- static async tryBoot(options = {}) {
25238
- if (!syncChannelSupported()) return null;
25239
- if (options.modules && Object.keys(options.modules).length > 0) return null;
25508
+ /** Boot without silently dropping synchronous child-process support. */
25509
+ static async boot(options = {}) {
25510
+ const reason = syncChannelUnavailableReason();
25511
+ if (reason) throw new Error(`SandboxedJs worker runtime unavailable: ${reason}`);
25512
+ if (options.modules && Object.keys(options.modules).length > 0) {
25513
+ throw new Error("SandboxedJs worker runtime cannot clone host-supplied modules. Use isolation: 'realm' explicitly.");
25514
+ }
25240
25515
  const pod = new _WorkerRuntimePod(options);
25241
25516
  try {
25242
25517
  const probe = await startRuntimeWorker({ ...options.workerUrl ? { url: options.workerUrl } : {}, timeoutMs: 1e4 });
25243
25518
  await probe.terminate();
25244
25519
  return pod;
25245
- } catch {
25520
+ } catch (cause) {
25246
25521
  pod.teardown();
25522
+ const detail = cause instanceof Error ? cause.message : String(cause);
25523
+ throw new Error(`SandboxedJs guest worker failed to start: ${detail}. Check that worker-entry.js is served as JavaScript; set workerUrl if your bundler moved it.`, { cause });
25524
+ }
25525
+ }
25526
+ /** Compatibility mode, with an observable explanation for every fallback. */
25527
+ static async tryBoot(options = {}, onFallback = (error) => console.warn(`[sandboxedjs] Falling back to the realm runtime; synchronous child processes are unavailable. ${error.message}`)) {
25528
+ try {
25529
+ return await _WorkerRuntimePod.boot(options);
25530
+ } catch (error) {
25531
+ onFallback(error instanceof Error ? error : new Error(String(error)));
25247
25532
  return null;
25248
25533
  }
25249
25534
  }
@@ -25266,11 +25551,14 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25266
25551
  const streams = { target: null };
25267
25552
  const server = new SyncChannelServer(buffers, serveSyncSyscalls({
25268
25553
  volume: this.volume,
25269
- spawnChild: (request) => this.runChildToCompletion(request, streams.target)
25554
+ spawnChild: (request) => this.runChildToCompletion(request, streams.target, ownedChildren)
25270
25555
  }));
25271
25556
  const entry = { worker, server };
25272
25557
  this.live.add(entry);
25558
+ const ownedChildren = /* @__PURE__ */ new Set();
25273
25559
  const process2 = new WorkerProcess(worker, () => {
25560
+ for (const child of ownedChildren) child.kill("SIGTERM");
25561
+ ownedChildren.clear();
25274
25562
  this.live.delete(entry);
25275
25563
  server.close();
25276
25564
  this.closeProxies(owner);
@@ -25291,7 +25579,7 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25291
25579
  process2.error(String(message.text));
25292
25580
  return;
25293
25581
  case "rawmode":
25294
- process2.emit("rawmode", Boolean(message.enabled));
25582
+ process2.rawMode(Boolean(message.enabled));
25295
25583
  return;
25296
25584
  case "exit":
25297
25585
  process2.finish(Number(message.code));
@@ -25299,11 +25587,20 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25299
25587
  case "listen":
25300
25588
  this.proxyPort(Number(message.port), owner, worker);
25301
25589
  return;
25590
+ case "close-port": {
25591
+ const port = Number(message.port);
25592
+ const proxy = this.proxies.get(port);
25593
+ if (proxy?.owner === owner) {
25594
+ proxy.close();
25595
+ this.proxies.delete(port);
25596
+ }
25597
+ return;
25598
+ }
25302
25599
  case "http-response":
25303
25600
  this.settleProxied(Number(message.id), message.response);
25304
25601
  return;
25305
25602
  case "child-start":
25306
- this.startChild(worker, children, message);
25603
+ this.startChild(worker, children, message, ownedChildren);
25307
25604
  return;
25308
25605
  case "child-stdin":
25309
25606
  children.get(message.id)?.sendStdin?.(String(message.data));
@@ -25334,16 +25631,21 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25334
25631
  return process2;
25335
25632
  }
25336
25633
  /** Start an asynchronous child on the host's behalf and relay its events. */
25337
- startChild(worker, children, message) {
25634
+ startChild(worker, children, message, owned) {
25338
25635
  const handle = this.processManager.spawn(message.config);
25339
25636
  children.set(message.id, handle);
25637
+ owned.add(handle);
25638
+ handle.on("exit", () => {
25639
+ owned.delete(handle);
25640
+ children.delete(message.id);
25641
+ });
25340
25642
  for (const event of ["stdout", "stderr", "exit"]) {
25341
25643
  handle.on(event, (value) => worker.postMessage({ type: "child-event", id: message.id, event, value }));
25342
25644
  }
25343
25645
  handle.exec();
25344
25646
  }
25345
25647
  /** Run a child to completion and collect it, for the guest's `spawnSync`. */
25346
- runChildToCompletion(request, streamTo) {
25648
+ runChildToCompletion(request, streamTo, owned) {
25347
25649
  return new Promise((resolve2) => {
25348
25650
  let handle;
25349
25651
  try {
@@ -25359,9 +25661,13 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25359
25661
  resolve2({ status: null, stdout: "", stderr: "", signal: null, error: { ...failure.code ? { code: failure.code } : {}, message: failure.message } });
25360
25662
  return;
25361
25663
  }
25664
+ owned.add(handle);
25665
+ handle.on("exit", () => owned.delete(handle));
25362
25666
  let stdout = "";
25363
25667
  let stderr = "";
25364
25668
  const live = request.inheritStdio ? streamTo : null;
25669
+ const restoreInput = live?.inheritInput(handle);
25670
+ if (restoreInput) handle.on("exit", restoreInput);
25365
25671
  handle.on("stdout", (text2) => {
25366
25672
  stdout += text2;
25367
25673
  live?.output(text2);
@@ -25515,7 +25821,7 @@ var Container = class _Container {
25515
25821
  ...opts.onServerReady ? { onServerReady: opts.onServerReady } : {}
25516
25822
  };
25517
25823
  const workerOptions = { ...podOptions, ...opts.workerUrl ? { workerUrl: opts.workerUrl } : {} };
25518
- const pod = opts.pod ?? (opts.isolation === "realm" ? null : await WorkerRuntimePod.tryBoot(workerOptions)) ?? await LocalRuntimePod.boot(podOptions);
25824
+ const pod = opts.pod ?? (opts.isolation === "realm" ? null : opts.isolation === "worker" ? await WorkerRuntimePod.boot(workerOptions) : await WorkerRuntimePod.tryBoot(workerOptions, opts.onRuntimeFallback)) ?? await LocalRuntimePod.boot(podOptions);
25519
25825
  const kernel = new Kernel({
25520
25826
  pod,
25521
25827
  hostname: opts.hostname ?? "sandbox",
@@ -26023,7 +26329,7 @@ var Terminal = class {
26023
26329
  if (this.running) {
26024
26330
  const raw = this.currentStdin?.rawMode === true;
26025
26331
  if (ch === CTRL_C && !raw) {
26026
- this.session.proc.deliver("SIGINT");
26332
+ if (this.currentStdin) this.session.shell.interruptForeground("SIGINT", this.currentStdin);
26027
26333
  this.write("^C\r\n");
26028
26334
  return;
26029
26335
  }
@@ -26413,7 +26719,7 @@ function serveContainerOn(port, box) {
26413
26719
  }
26414
26720
  async function createPreview(box, options = {}) {
26415
26721
  if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) return null;
26416
- const scriptUrl = options.scriptUrl ?? new URL("./service-worker.js", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
26722
+ const scriptUrl = options.scriptUrl ?? new URL("./service-worker.js?no-inline", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
26417
26723
  let registration;
26418
26724
  try {
26419
26725
  registration = await navigator.serviceWorker.register(scriptUrl, {