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.js CHANGED
@@ -4778,7 +4778,15 @@ var Pipe = class _Pipe {
4778
4778
  * whole line on every keystroke — so the terminal must stop echoing, or
4779
4779
  * every character appears twice.
4780
4780
  */
4781
- rawMode = false;
4781
+ raw = false;
4782
+ onRawMode;
4783
+ get rawMode() {
4784
+ return this.raw;
4785
+ }
4786
+ set rawMode(enabled) {
4787
+ this.raw = enabled;
4788
+ this.onRawMode?.(enabled);
4789
+ }
4782
4790
  columns;
4783
4791
  rows;
4784
4792
  get closed() {
@@ -5906,13 +5914,19 @@ var Kernel = class {
5906
5914
  stderr: opts.stderr ?? new NullOutput()
5907
5915
  }
5908
5916
  });
5917
+ const cancelWithParent = () => {
5918
+ if (parent?.termSignal) proc.deliver(parent.termSignal);
5919
+ };
5920
+ parent?.signal.addEventListener("abort", cancelWithParent, { once: true });
5921
+ if (parent?.signal.aborted) cancelWithParent();
5922
+ void proc.wait().then(() => parent?.signal.removeEventListener("abort", cancelWithParent));
5909
5923
  let timer;
5910
5924
  if (opts.timeoutMs !== void 0) {
5911
5925
  timer = setTimeout(() => {
5912
5926
  proc.deliver("SIGKILL");
5913
5927
  }, opts.timeoutMs);
5914
5928
  }
5915
- void this.dispatch(proc).then((code) => {
5929
+ void (proc.signal.aborted ? Promise.resolve(proc.exitCode ?? 137) : this.dispatch(proc)).then((code) => {
5916
5930
  proc.exit(code);
5917
5931
  }).catch((e) => {
5918
5932
  try {
@@ -7904,6 +7918,22 @@ var Shell = class _Shell {
7904
7918
  }
7905
7919
  return this.lastStatus;
7906
7920
  }
7921
+ /** Signal the current terminal pipeline without terminating the interactive shell. */
7922
+ interruptForeground(signal, stdin) {
7923
+ const processes = this.kernel.procs.list().filter((process2) => process2.running);
7924
+ const selected = new Set(processes.filter((process2) => process2.ppid === this.proc.pid && process2.stdin === stdin).map((process2) => process2.pid));
7925
+ for (let changed = true; changed; ) {
7926
+ changed = false;
7927
+ const outputs = processes.filter((process2) => selected.has(process2.pid)).flatMap((process2) => [process2.stdout, process2.stderr]);
7928
+ for (const process2 of processes) {
7929
+ if (!selected.has(process2.pid) && (selected.has(process2.ppid) || outputs.some((output) => output === process2.stdin))) {
7930
+ selected.add(process2.pid);
7931
+ changed = true;
7932
+ }
7933
+ }
7934
+ }
7935
+ for (const process2 of processes) if (selected.has(process2.pid)) process2.deliver(signal);
7936
+ }
7907
7937
  get isExiting() {
7908
7938
  return this.exiting;
7909
7939
  }
@@ -8553,6 +8583,7 @@ sys ${fmt2(Math.floor(ms * 0.2))}
8553
8583
  for (const assignment of assignments) {
8554
8584
  env2[assignment.name] = await expandWordToString(assignment.value, this.expandContext());
8555
8585
  }
8586
+ if (this.proc.signal.aborted) return this.proc.exitCode ?? 137;
8556
8587
  const proc = this.kernel.spawn(words, {
8557
8588
  cwd: this.cwd,
8558
8589
  env: env2,
@@ -8810,7 +8841,7 @@ async function runShell(ctxName, ctx) {
8810
8841
  const args = ctx.args.slice();
8811
8842
  let command;
8812
8843
  let scriptPath;
8813
- let readStdin2 = false;
8844
+ let readStdin = false;
8814
8845
  let interactive = false;
8815
8846
  let login = ctxName.startsWith("-");
8816
8847
  const options = {};
@@ -8823,7 +8854,7 @@ async function runShell(ctxName, ctx) {
8823
8854
  break;
8824
8855
  }
8825
8856
  if (arg === "-s") {
8826
- readStdin2 = true;
8857
+ readStdin = true;
8827
8858
  continue;
8828
8859
  }
8829
8860
  if (arg === "-i") {
@@ -8860,7 +8891,7 @@ async function runShell(ctxName, ctx) {
8860
8891
  break;
8861
8892
  }
8862
8893
  const rest = args.slice(i);
8863
- if (command === void 0 && !readStdin2 && rest.length > 0) {
8894
+ if (command === void 0 && !readStdin && rest.length > 0) {
8864
8895
  scriptPath = rest[0];
8865
8896
  }
8866
8897
  const positional = command !== void 0 ? rest.slice(1) : scriptPath !== void 0 ? rest.slice(1) : rest;
@@ -17755,6 +17786,143 @@ function mountContainerFs(FS, opts) {
17755
17786
  }
17756
17787
  }
17757
17788
 
17789
+ // src/runtime/python-syscalls.ts
17790
+ var EMPTY = new Uint8Array(0);
17791
+ function createStdinHost(ctx) {
17792
+ let pending = EMPTY;
17793
+ let eof = false;
17794
+ const take = (size) => {
17795
+ const n = Math.min(size, pending.length);
17796
+ const out = pending.subarray(0, n);
17797
+ pending = pending.subarray(n);
17798
+ return out;
17799
+ };
17800
+ return {
17801
+ /**
17802
+ * Drain a non-interactive stdin up front.
17803
+ *
17804
+ * A pipe ends, so reading it eagerly costs nothing and leaves every read
17805
+ * answerable without suspending — which is what keeps piped input working
17806
+ * on hosts with no stack-switching. A terminal never ends, so priming one
17807
+ * would hang before the program had printed its prompt.
17808
+ */
17809
+ async prime() {
17810
+ if (ctx.stdin.isTTY || ctx.stdin.interactive) return;
17811
+ try {
17812
+ pending = await ctx.stdin.readAll();
17813
+ } catch {
17814
+ pending = EMPTY;
17815
+ }
17816
+ eof = true;
17817
+ },
17818
+ buffered(size) {
17819
+ if (pending.length > 0) return take(size);
17820
+ return eof ? EMPTY : void 0;
17821
+ },
17822
+ async read(size) {
17823
+ if (pending.length > 0) return take(size);
17824
+ if (eof) return EMPTY;
17825
+ const chunk = await ctx.stdin.read(size);
17826
+ if (chunk === null || chunk.length === 0) {
17827
+ eof = true;
17828
+ return EMPTY;
17829
+ }
17830
+ pending = chunk;
17831
+ return take(size);
17832
+ },
17833
+ isatty() {
17834
+ return Boolean(ctx.stdin.isTTY);
17835
+ }
17836
+ };
17837
+ }
17838
+ var BRIDGE_PY = `
17839
+ import builtins, io, sys
17840
+ import _sbx_host as _host
17841
+ from pyodide.ffi import can_run_sync, run_sync
17842
+
17843
+
17844
+ def _to_bytes(value):
17845
+ if value is None:
17846
+ return b""
17847
+ to_bytes = getattr(value, "to_bytes", None)
17848
+ if to_bytes is not None:
17849
+ return to_bytes()
17850
+ return bytes(value.to_py())
17851
+
17852
+
17853
+ class _SbxStdin(io.RawIOBase):
17854
+ """The container's standard input, as a blocking raw stream."""
17855
+
17856
+ def readable(self):
17857
+ return True
17858
+
17859
+ def fileno(self):
17860
+ return 0
17861
+
17862
+ def isatty(self):
17863
+ return bool(_host.isatty())
17864
+
17865
+ def readinto(self, target):
17866
+ want = len(target)
17867
+ if want == 0:
17868
+ return 0
17869
+ ready = _host.buffered(want)
17870
+ if ready is None:
17871
+ if not can_run_sync():
17872
+ raise OSError(
17873
+ "this host cannot wait for interactive input: WebAssembly "
17874
+ "stack switching (JSPI) is unavailable. Pipe the input in, "
17875
+ "or use a browser or Node build that supports it."
17876
+ )
17877
+ ready = run_sync(_host.read(want))
17878
+ data = _to_bytes(ready)
17879
+ target[: len(data)] = data
17880
+ return len(data)
17881
+
17882
+
17883
+ def _install():
17884
+ stdin = io.TextIOWrapper(
17885
+ io.BufferedReader(_SbxStdin()), encoding="utf-8", errors="replace", line_buffering=True
17886
+ )
17887
+ sys.stdin = stdin
17888
+ sys.__stdin__ = stdin
17889
+
17890
+ def input(prompt=""):
17891
+ # CPython writes the prompt to stdout and strips exactly one trailing
17892
+ # newline; anything else changes what a program reads back.
17893
+ if prompt != "":
17894
+ sys.stdout.write(str(prompt))
17895
+ sys.stdout.flush()
17896
+ line = sys.stdin.readline()
17897
+ if not line:
17898
+ raise EOFError("EOF when reading a line")
17899
+ if line.endswith("\\n"):
17900
+ line = line[:-1]
17901
+ if line.endswith("\\r"):
17902
+ line = line[:-1]
17903
+ return line
17904
+
17905
+ builtins.input = input
17906
+
17907
+
17908
+ _install()
17909
+ `;
17910
+ function installSyscallBridge(py) {
17911
+ const slot = py.__sbxStdin ??= { active: null };
17912
+ py.registerJsModule("_sbx_host", {
17913
+ /* Not `?.` and `??`: `buffered` returns undefined to mean "you will have
17914
+ * to wait", and a nullish default would quietly turn that into end-of-file —
17915
+ * which is exactly the silent EOF this bridge exists to remove. */
17916
+ buffered: (size) => slot.active ? slot.active.buffered(size) : EMPTY,
17917
+ read: (size) => slot.active ? slot.active.read(size) : Promise.resolve(EMPTY),
17918
+ isatty: () => slot.active?.isatty() ?? false
17919
+ });
17920
+ py.runPython(BRIDGE_PY);
17921
+ }
17922
+ function bindStdin(py, host2) {
17923
+ py.__sbxStdin.active = host2;
17924
+ }
17925
+
17758
17926
  // src/runtime/cpython.ts
17759
17927
  var RESERVED_FOR_INTERPRETER = /* @__PURE__ */ new Set(["lib", "dev", "proc"]);
17760
17928
  var pyodideModule = null;
@@ -17827,6 +17995,7 @@ async function interpreterFor(ctx) {
17827
17995
  });
17828
17996
  mountContainerDirs(py, ctx);
17829
17997
  installRunner(py);
17998
+ installSyscallBridge(py);
17830
17999
  return py;
17831
18000
  })();
17832
18001
  interpreters.set(ctx.vfs, starting);
@@ -17921,7 +18090,7 @@ function reportError(ctx, error) {
17921
18090
  `);
17922
18091
  return 1;
17923
18092
  }
17924
- async function runCPythonProgram(ctx, source, argv, scriptDir, stdinText) {
18093
+ async function runCPythonProgram(ctx, source, argv, scriptDir = null) {
17925
18094
  let py;
17926
18095
  try {
17927
18096
  py = await interpreterFor(ctx);
@@ -17942,7 +18111,6 @@ async function runCPythonProgram(ctx, source, argv, scriptDir, stdinText) {
17942
18111
  });
17943
18112
  } catch {
17944
18113
  }
17945
- const encoder8 = new TextEncoder();
17946
18114
  const decoder8 = new TextDecoder();
17947
18115
  py.setStdout({
17948
18116
  write: (buffer) => {
@@ -17956,20 +18124,17 @@ async function runCPythonProgram(ctx, source, argv, scriptDir, stdinText) {
17956
18124
  return buffer.length;
17957
18125
  }
17958
18126
  });
17959
- if (stdinText !== null) {
17960
- const bytes2 = encoder8.encode(stdinText);
17961
- let offset = 0;
17962
- py.setStdin({
17963
- read: (buffer) => {
17964
- const take = Math.min(buffer.length, bytes2.length - offset);
17965
- buffer.set(bytes2.subarray(offset, offset + take));
17966
- offset += take;
17967
- return take;
17968
- }
17969
- });
17970
- } else {
17971
- py.setStdin({ read: () => 0 });
17972
- }
18127
+ const stdinHost = createStdinHost(ctx);
18128
+ await stdinHost.prime();
18129
+ bindStdin(py, stdinHost);
18130
+ py.setStdin({
18131
+ read: (buffer) => {
18132
+ const ready = stdinHost.buffered(buffer.length);
18133
+ if (!ready || ready.length === 0) return 0;
18134
+ buffer.set(ready);
18135
+ return ready.length;
18136
+ }
18137
+ });
17973
18138
  try {
17974
18139
  bootstrap(py, ctx, argv, scriptDir);
17975
18140
  } catch (error) {
@@ -18102,11 +18267,6 @@ function configurePython(options = {}) {
18102
18267
  configureCPython({ indexURL: options.indexURL, moduleURL: options.pyodideURL });
18103
18268
  }
18104
18269
  var isPythonAvailable = isCPythonAvailable;
18105
- async function readStdin(ctx) {
18106
- if (ctx.stdin.isTTY || ctx.stdin.interactive) return null;
18107
- const bytes2 = await ctx.stdin.readAll();
18108
- return bytes2.length ? new TextDecoder().decode(bytes2) : null;
18109
- }
18110
18270
  var python = defineCommand({
18111
18271
  name: "python3",
18112
18272
  path: "/usr/bin/python3",
@@ -18155,7 +18315,7 @@ compatible packages can be installed with pip (micropip).`,
18155
18315
  }
18156
18316
  const rest = argv.slice(i);
18157
18317
  if (command !== void 0) {
18158
- return (await runCPythonProgram(ctx, command, ["-c", ...rest], null, await readStdin(ctx))).exitCode;
18318
+ return (await runCPythonProgram(ctx, command, ["-c", ...rest], null)).exitCode;
18159
18319
  }
18160
18320
  if (moduleName !== void 0) {
18161
18321
  const program = `
@@ -18167,16 +18327,16 @@ except ImportError:
18167
18327
  print("/usr/bin/python3: No module named " + ${JSON.stringify(moduleName)}, file=sys.stderr)
18168
18328
  raise SystemExit(1)
18169
18329
  `;
18170
- return (await runCPythonProgram(ctx, program, [moduleName, ...rest], null, await readStdin(ctx))).exitCode;
18330
+ return (await runCPythonProgram(ctx, program, [moduleName, ...rest], null)).exitCode;
18171
18331
  }
18172
18332
  if (script === void 0) {
18173
18333
  if (ctx.stdin.isTTY) return runCPythonRepl(ctx);
18174
18334
  const source2 = new TextDecoder().decode(await ctx.stdin.readAll());
18175
- return source2.trim() ? (await runCPythonProgram(ctx, source2, ["", ...rest], null, null)).exitCode : 0;
18335
+ return source2.trim() ? (await runCPythonProgram(ctx, source2, ["", ...rest])).exitCode : 0;
18176
18336
  }
18177
18337
  if (script === "-") {
18178
18338
  const source2 = new TextDecoder().decode(await ctx.stdin.readAll());
18179
- return (await runCPythonProgram(ctx, source2, ["-", ...rest], null, null)).exitCode;
18339
+ return (await runCPythonProgram(ctx, source2, ["-", ...rest])).exitCode;
18180
18340
  }
18181
18341
  const abs = ctx.path(script);
18182
18342
  let source;
@@ -18187,7 +18347,7 @@ except ImportError:
18187
18347
  `);
18188
18348
  return 2;
18189
18349
  }
18190
- return (await runCPythonProgram(ctx, source, [abs, ...rest], dirname(abs), await readStdin(ctx))).exitCode;
18350
+ return (await runCPythonProgram(ctx, source, [abs, ...rest], dirname(abs))).exitCode;
18191
18351
  }
18192
18352
  });
18193
18353
  function printHelp2(ctx) {
@@ -19699,6 +19859,7 @@ var KernelChildProcess = class {
19699
19859
  this.stdin.isTTY = true;
19700
19860
  this.stdin.interactive = true;
19701
19861
  }
19862
+ this.stdin.onRawMode = (enabled) => this.emit("rawmode", enabled);
19702
19863
  this.kernel = kernel;
19703
19864
  this.cred = cred;
19704
19865
  this.pid = pid;
@@ -19815,7 +19976,7 @@ function installNodeChildProcessBridge(pod, kernel, cred) {
19815
19976
  const originalSpawn = manager.spawn;
19816
19977
  manager.spawn = (config) => {
19817
19978
  const resolved = kernel.resolveExecutable(config.command, config.cwd ?? "/", config.env ?? {}, cred);
19818
- if (resolved?.kind === "builtin" && resolved.command?.name !== "node" && resolved.command?.name !== "nodejs") {
19979
+ if (resolved?.kind === "builtin") {
19819
19980
  return new KernelChildProcess(kernel, cred, config, nextBridgePid++);
19820
19981
  }
19821
19982
  return originalSpawn.call(manager, config);
@@ -19826,6 +19987,68 @@ function installNodeChildProcessBridge(pod, kernel, cred) {
19826
19987
  };
19827
19988
  }
19828
19989
 
19990
+ // src/runtime/host-module-tracker.ts
19991
+ var HostModuleTracker = class {
19992
+ active = 0;
19993
+ disposed = false;
19994
+ dispose() {
19995
+ this.disposed = true;
19996
+ }
19997
+ wrapped = /* @__PURE__ */ new WeakMap();
19998
+ originals = /* @__PURE__ */ new WeakMap();
19999
+ pending = () => this.active;
20000
+ unwrap = (value) => value && (typeof value === "object" || typeof value === "function") ? this.originals.get(value) ?? value : value;
20001
+ wrap(value) {
20002
+ if (value === null || typeof value !== "object" && typeof value !== "function") return value;
20003
+ const object = value;
20004
+ if (Array.isArray(value) || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return value;
20005
+ if (this.wrapped.has(object)) return this.wrapped.get(object);
20006
+ const tracker = this;
20007
+ const proxy = new Proxy(object, {
20008
+ // CommonJS native loaders re-export by assigning exports back onto the
20009
+ // binding object. Never store a process-owned proxy in a shared module.
20010
+ set(target, key, member) {
20011
+ return Reflect.set(target, key, tracker.unwrap(member), target);
20012
+ },
20013
+ defineProperty(target, key, descriptor) {
20014
+ return Reflect.defineProperty(target, key, "value" in descriptor ? { ...descriptor, value: tracker.unwrap(descriptor.value) } : descriptor);
20015
+ },
20016
+ get(target, key) {
20017
+ const descriptor = Reflect.getOwnPropertyDescriptor(target, key);
20018
+ const member = Reflect.get(target, key, target);
20019
+ if (descriptor && !descriptor.configurable && "value" in descriptor && !descriptor.writable) return member;
20020
+ return typeof member === "function" ? tracker.wrap(member) : member;
20021
+ },
20022
+ apply(target, receiver, args) {
20023
+ const result = Reflect.apply(target, tracker.unwrap(receiver), args.map(tracker.unwrap));
20024
+ if (result && typeof result.then === "function") {
20025
+ tracker.active++;
20026
+ return Promise.resolve(result).then(
20027
+ (value2) => {
20028
+ tracker.active--;
20029
+ return tracker.disposed ? new Promise(() => {
20030
+ }) : tracker.wrap(value2);
20031
+ },
20032
+ (error) => {
20033
+ tracker.active--;
20034
+ if (tracker.disposed) return new Promise(() => {
20035
+ });
20036
+ throw error;
20037
+ }
20038
+ );
20039
+ }
20040
+ return tracker.wrap(result);
20041
+ },
20042
+ construct(target, args, newTarget) {
20043
+ return tracker.wrap(Reflect.construct(target, args.map(tracker.unwrap), tracker.unwrap(newTarget)));
20044
+ }
20045
+ });
20046
+ this.wrapped.set(object, proxy);
20047
+ this.originals.set(proxy, object);
20048
+ return proxy;
20049
+ }
20050
+ };
20051
+
19829
20052
  // src/runtime/commonjs-engine.ts
19830
20053
  init_path();
19831
20054
  var HELPERS = {
@@ -21563,6 +21786,7 @@ var VirtualHttpServer = class extends EventEmitter4 {
21563
21786
  owner;
21564
21787
  listening = false;
21565
21788
  portValue = null;
21789
+ referenced = true;
21566
21790
  listen(...args) {
21567
21791
  const callback = typeof args.at(-1) === "function" ? args.pop() : void 0;
21568
21792
  const first = args[0];
@@ -21591,11 +21815,16 @@ var VirtualHttpServer = class extends EventEmitter4 {
21591
21815
  return this.portValue === null ? null : { address: "0.0.0.0", family: "IPv4", port: this.portValue };
21592
21816
  }
21593
21817
  ref() {
21818
+ this.referenced = true;
21594
21819
  return this;
21595
21820
  }
21596
21821
  unref() {
21822
+ this.referenced = false;
21597
21823
  return this;
21598
21824
  }
21825
+ hasRef() {
21826
+ return this.referenced;
21827
+ }
21599
21828
  setTimeout(_milliseconds, callback) {
21600
21829
  if (callback) this.on("timeout", callback);
21601
21830
  return this;
@@ -21605,13 +21834,16 @@ var VirtualHttpRouter = class {
21605
21834
  servers = /* @__PURE__ */ new Map();
21606
21835
  /** Notified when a server begins listening, for `onServerReady`. */
21607
21836
  onListen;
21837
+ onClose;
21608
21838
  register(port, server, owner) {
21609
21839
  if (this.servers.has(port)) throw Object.assign(new Error(`listen EADDRINUSE: address already in use 0.0.0.0:${port}`), { code: "EADDRINUSE", port });
21610
21840
  this.servers.set(port, { server, owner });
21611
21841
  this.onListen?.(port);
21612
21842
  }
21613
21843
  unregister(port, server) {
21614
- if (this.servers.get(port)?.server === server) this.servers.delete(port);
21844
+ if (this.servers.get(port)?.server !== server) return;
21845
+ this.servers.delete(port);
21846
+ this.onClose?.(port);
21615
21847
  }
21616
21848
  /** Whether anything in this container is listening on `port`. */
21617
21849
  activePortsIncludes(port) {
@@ -21620,6 +21852,9 @@ var VirtualHttpRouter = class {
21620
21852
  activePorts(owner) {
21621
21853
  return [...this.servers].filter(([, item]) => owner === void 0 || item.owner === owner).map(([port]) => port).sort((a, b) => a - b);
21622
21854
  }
21855
+ referencedPorts(owner) {
21856
+ return [...this.servers].filter(([, item]) => item.owner === owner && item.server.hasRef()).map(([port]) => port);
21857
+ }
21623
21858
  closeOwner(owner) {
21624
21859
  for (const { server, owner: value } of [...this.servers.values()]) if (value === owner) server.close();
21625
21860
  }
@@ -22031,6 +22266,108 @@ function hashFor(algorithm) {
22031
22266
  function toBytes2(data, encoding) {
22032
22267
  return typeof data === "string" ? Buffer2.from(data, encoding) : data;
22033
22268
  }
22269
+
22270
+ // src/runtime/sync-channel.ts
22271
+ var STATE = 0;
22272
+ var LENGTH = 1;
22273
+ var MORE = 2;
22274
+ var CONTROL_WORDS = 4;
22275
+ var STATE_REQUEST = 1;
22276
+ var STATE_RESPONSE = 2;
22277
+ var STATE_CONTINUE = 3;
22278
+ var STATE_CLOSED = 4;
22279
+ function createSyncChannelBuffers(capacityBytes = 1 << 20) {
22280
+ return {
22281
+ control: new SharedArrayBuffer(CONTROL_WORDS * Int32Array.BYTES_PER_ELEMENT),
22282
+ data: new SharedArrayBuffer(capacityBytes)
22283
+ };
22284
+ }
22285
+ function syncChannelUnavailableReason() {
22286
+ if (typeof globalThis.crossOriginIsolated === "boolean" && !globalThis.crossOriginIsolated) {
22287
+ 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.";
22288
+ }
22289
+ if (typeof SharedArrayBuffer !== "function") return "SharedArrayBuffer is unavailable in this host.";
22290
+ if (typeof Atomics !== "object" || typeof Atomics.wait !== "function") return "Atomics.wait is unavailable in this host.";
22291
+ return null;
22292
+ }
22293
+ function syncChannelSupported() {
22294
+ return syncChannelUnavailableReason() === null;
22295
+ }
22296
+ var SyncChannelServer = class {
22297
+ constructor(buffers, handle) {
22298
+ this.handle = handle;
22299
+ this.control = new Int32Array(buffers.control);
22300
+ this.data = new Uint8Array(buffers.data);
22301
+ this.capacity = this.data.length;
22302
+ }
22303
+ handle;
22304
+ control;
22305
+ data;
22306
+ capacity;
22307
+ /** Request chunks gathered so far, and the response still to be sent. */
22308
+ incoming = [];
22309
+ outgoing = null;
22310
+ sent = 0;
22311
+ closed = false;
22312
+ /** Call from the wake message the client sends after each chunk. */
22313
+ async pump() {
22314
+ if (this.closed) return;
22315
+ const state = Atomics.load(this.control, STATE);
22316
+ if (state === STATE_REQUEST) {
22317
+ const size = Atomics.load(this.control, LENGTH);
22318
+ this.incoming.push(this.data.slice(0, size));
22319
+ if (Atomics.load(this.control, MORE) === 1) {
22320
+ this.publish(STATE_CONTINUE);
22321
+ return;
22322
+ }
22323
+ const request = concat3(this.incoming);
22324
+ this.incoming = [];
22325
+ let response;
22326
+ try {
22327
+ response = await this.handle(request);
22328
+ } catch {
22329
+ response = new Uint8Array();
22330
+ }
22331
+ if (this.closed) return;
22332
+ this.outgoing = response;
22333
+ this.sent = 0;
22334
+ this.sendChunk();
22335
+ return;
22336
+ }
22337
+ if (state === STATE_CONTINUE) this.sendChunk();
22338
+ }
22339
+ /** Release a blocked client, e.g. when the container is torn down. */
22340
+ close() {
22341
+ if (this.closed) return;
22342
+ this.closed = true;
22343
+ Atomics.store(this.control, STATE, STATE_CLOSED);
22344
+ Atomics.notify(this.control, STATE);
22345
+ }
22346
+ sendChunk() {
22347
+ const payload = this.outgoing ?? new Uint8Array();
22348
+ const size = Math.min(this.capacity, payload.length - this.sent);
22349
+ this.data.set(payload.subarray(this.sent, this.sent + size), 0);
22350
+ this.sent += size;
22351
+ Atomics.store(this.control, LENGTH, size);
22352
+ Atomics.store(this.control, MORE, this.sent < payload.length ? 1 : 0);
22353
+ this.publish(STATE_RESPONSE);
22354
+ }
22355
+ publish(state) {
22356
+ Atomics.store(this.control, STATE, state);
22357
+ Atomics.notify(this.control, STATE);
22358
+ }
22359
+ };
22360
+ function concat3(parts) {
22361
+ if (parts.length === 1) return parts[0];
22362
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
22363
+ const joined = new Uint8Array(total);
22364
+ let at = 0;
22365
+ for (const part of parts) {
22366
+ joined.set(part, at);
22367
+ at += part.length;
22368
+ }
22369
+ return joined;
22370
+ }
22034
22371
  var ChildProcess = class extends EventEmitter4 {
22035
22372
  stdout = new streamModule4.PassThrough();
22036
22373
  stderr = new streamModule4.PassThrough();
@@ -22235,7 +22572,7 @@ function unavailable(name) {
22235
22572
  const list = Array.isArray(args[1]) ? args[1].map(String) : [];
22236
22573
  const command = file3 ? [file3, ...list].join(" ") : void 0;
22237
22574
  const error = new Error(
22238
- `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.`
22575
+ `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.")
22239
22576
  );
22240
22577
  error.code = "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM";
22241
22578
  throw error;
@@ -22523,6 +22860,11 @@ function createReadlineModule(defaultInput, defaultOutput = () => void 0) {
22523
22860
  edit(sequence, key) {
22524
22861
  if (this.closed) return;
22525
22862
  const name = key.name;
22863
+ if (key.ctrl && name === "c") {
22864
+ if (this.listenerCount("SIGINT") > 0) this.emit("SIGINT");
22865
+ else this.close();
22866
+ return;
22867
+ }
22526
22868
  if (name === "return" || name === "enter") {
22527
22869
  const value = this.line;
22528
22870
  this.line = "";
@@ -22618,7 +22960,7 @@ function createReadlineModule(defaultInput, defaultOutput = () => void 0) {
22618
22960
  };
22619
22961
  const createInterface = (options, output) => {
22620
22962
  if (options && typeof options === "object" && !options.on) {
22621
- const resolvedOutput2 = options.output ?? defaultOutput();
22963
+ const resolvedOutput2 = options.output;
22622
22964
  const instance = new Interface(
22623
22965
  options.input ?? defaultInput(),
22624
22966
  resolvedOutput2,
@@ -22627,7 +22969,7 @@ function createReadlineModule(defaultInput, defaultOutput = () => void 0) {
22627
22969
  if (typeof options.prompt === "string") instance.setPrompt(options.prompt);
22628
22970
  return instance;
22629
22971
  }
22630
- const resolvedOutput = output ?? defaultOutput();
22972
+ const resolvedOutput = output;
22631
22973
  return new Interface(options ?? defaultInput(), resolvedOutput, isTerminal(void 0, resolvedOutput));
22632
22974
  };
22633
22975
  const noop = () => {
@@ -22896,7 +23238,7 @@ function createCoreModules(options) {
22896
23238
  "stream/promises": createStreamPromises(),
22897
23239
  string_decoder: stringDecoderModule,
22898
23240
  timers: { ...timersModule, ...timers.api },
22899
- "timers/promises": createTimerPromises(),
23241
+ "timers/promises": createTimerPromises(timers.api),
22900
23242
  tty: { isatty: () => false, ReadStream: streamModule4.Readable, WriteStream: streamModule4.Writable },
22901
23243
  url: url_module_default,
22902
23244
  util: util_module_default,
@@ -23641,10 +23983,10 @@ function createTrackedTimers() {
23641
23983
  cancelAll
23642
23984
  };
23643
23985
  }
23644
- function createTimerPromises() {
23986
+ function createTimerPromises(timers) {
23645
23987
  return {
23646
- setTimeout: (delay, value) => new Promise((resolve2) => setTimeout(resolve2, delay, value)),
23647
- setImmediate: (value) => new Promise((resolve2) => setTimeout(resolve2, 0, value))
23988
+ setTimeout: (delay, value) => new Promise((resolve2) => timers.setTimeout(resolve2, delay, value)),
23989
+ setImmediate: (value) => new Promise((resolve2) => timers.setImmediate(resolve2, value))
23648
23990
  };
23649
23991
  }
23650
23992
  function createAsyncHooksModule() {
@@ -24352,11 +24694,8 @@ function ensureProcessGlobal() {
24352
24694
  // src/runtime/host-rolldown.ts
24353
24695
  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'] }";
24354
24696
  async function loadHostRolldownBinding() {
24355
- if (typeof window !== "undefined" && globalThis.crossOriginIsolated !== true) {
24356
- throw new Error(
24357
- "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."
24358
- );
24359
- }
24697
+ const reason = syncChannelUnavailableReason();
24698
+ if (reason) throw new Error(`SandboxedJs threaded WASM unavailable: ${reason}`);
24360
24699
  try {
24361
24700
  const loaded = await import('@rolldown/binding-wasm32-wasi');
24362
24701
  return "__fs" in loaded ? { ...loaded } : loaded.default ?? loaded;
@@ -24364,9 +24703,9 @@ async function loadHostRolldownBinding() {
24364
24703
  if (typeof process !== "undefined" && process.env?.SANDBOXEDJS_DEBUG) {
24365
24704
  console.error("[sandboxedjs] Rolldown WASI binding unavailable:", error);
24366
24705
  }
24367
- const reason = error instanceof Error ? error.message : String(error);
24706
+ const reason2 = error instanceof Error ? error.message : String(error);
24368
24707
  throw new Error(
24369
- `The optional Rolldown WASI binding could not be loaded: ${reason}` + (typeof window === "undefined" ? "" : `
24708
+ `The optional Rolldown WASI binding could not be loaded: ${reason2}` + (typeof window === "undefined" ? "" : `
24370
24709
 
24371
24710
  ${BUNDLER_HINT}`),
24372
24711
  { cause: error }
@@ -24477,6 +24816,8 @@ var LocalProcess = class extends EventEmitter4 {
24477
24816
  }
24478
24817
  /** End the process now, for an asynchronous `process.exit`. */
24479
24818
  exitNow(code) {
24819
+ this.killed = true;
24820
+ this.cleanup?.();
24480
24821
  this.finish(code);
24481
24822
  this.resolveKilled();
24482
24823
  }
@@ -24660,6 +25001,7 @@ var LocalRuntimePod = class _LocalRuntimePod {
24660
25001
  )
24661
25002
  };
24662
25003
  disposed = false;
25004
+ running = /* @__PURE__ */ new Set();
24663
25005
  workdir;
24664
25006
  env;
24665
25007
  aliases;
@@ -24701,8 +25043,9 @@ var LocalRuntimePod = class _LocalRuntimePod {
24701
25043
  const cwd = typeof options.cwd === "string" ? options.cwd : this.workdir;
24702
25044
  const env2 = { ...this.env, ...isRecord(options.env) ? options.env : {} };
24703
25045
  const owner = `${this.instanceId}:${Math.random().toString(36).slice(2)}`;
24704
- return new LocalProcess(async (proc) => {
25046
+ const process2 = new LocalProcess(async (proc) => {
24705
25047
  await this.prepareRolldown(cwd, env2);
25048
+ if (proc.isKilled()) return 137;
24706
25049
  const untrack = trackProcess(proc);
24707
25050
  let requestedExit = 0;
24708
25051
  let engine;
@@ -24730,7 +25073,9 @@ var LocalRuntimePod = class _LocalRuntimePod {
24730
25073
  onRawMode: (enabled) => proc.emit("rawmode", enabled)
24731
25074
  });
24732
25075
  proc.acceptInput((data) => core.writeStdin(data), () => core.endStdin());
25076
+ const hostWork = new HostModuleTracker();
24733
25077
  proc.onKill(() => {
25078
+ hostWork.dispose();
24734
25079
  core.cancelTimers();
24735
25080
  this.router.closeOwner(owner);
24736
25081
  });
@@ -24739,21 +25084,22 @@ var LocalRuntimePod = class _LocalRuntimePod {
24739
25084
  builtins: core.builtins,
24740
25085
  globals: core.globals,
24741
25086
  aliases: this.aliases,
24742
- overrides: this.modules
25087
+ overrides: Object.fromEntries(Object.entries(this.modules).map(([key, value]) => [key, hostWork.wrap(value)]))
24743
25088
  });
24744
25089
  try {
24745
- await engine.run(script);
24746
- await this.settle(owner, core.pendingHandles, core.pendingUnrefed, core.readingStdin, core.pendingRequests, () => proc.isKilled());
24747
- if (this.router.activePorts(owner).length) {
24748
- await proc.waitForKill();
24749
- return 137;
24750
- }
25090
+ await Promise.race([engine.run(script), proc.waitForKill()]);
25091
+ await this.settle(owner, core.pendingHandles, core.readingStdin, () => core.pendingRequests() + hostWork.pending(), () => proc.isKilled());
24751
25092
  return requestedExit;
24752
25093
  } finally {
25094
+ hostWork.dispose();
25095
+ core.cancelTimers();
24753
25096
  untrack();
24754
25097
  this.router.closeOwner(owner);
24755
25098
  }
24756
25099
  });
25100
+ this.running.add(process2);
25101
+ void process2.completion.then(() => this.running.delete(process2));
25102
+ return process2;
24757
25103
  }
24758
25104
  /**
24759
25105
  * Rolldown's JavaScript API synchronously requires its compiled binding.
@@ -24789,19 +25135,13 @@ var LocalRuntimePod = class _LocalRuntimePod {
24789
25135
  * A plain script that has genuinely finished falls straight through both,
24790
25136
  * costing a handful of empty turns.
24791
25137
  */
24792
- async settle(owner, pendingHandles, pendingUnrefed, readingStdin, pendingRequests, killed) {
24793
- for (let turn = 0; turn < DRAIN_TURNS; turn++) {
24794
- if (this.router.activePorts(owner).length || killed()) return;
24795
- await new Promise((resolve2) => setTimeout(resolve2, 0));
24796
- }
24797
- if (pendingHandles() === 0 && pendingRequests() === 0 && pendingUnrefed() > 0 && !readingStdin()) {
24798
- const deadline = Date.now() + 1e3;
24799
- while (!this.router.activePorts(owner).length && !killed() && Date.now() < deadline) {
24800
- await new Promise((resolve2) => setTimeout(resolve2, 5));
24801
- }
24802
- }
24803
- while (!this.router.activePorts(owner).length && !killed() && (pendingHandles() > 0 || pendingRequests() > 0 || readingStdin())) {
24804
- await new Promise((resolve2) => setTimeout(resolve2, 5));
25138
+ async settle(owner, pendingHandles, readingStdin, pendingRequests, killed) {
25139
+ let idleTurns = 0;
25140
+ while (!killed()) {
25141
+ const busy = this.router.referencedPorts(owner).length > 0 || pendingHandles() > 0 || pendingRequests() > 0 || readingStdin();
25142
+ idleTurns = busy ? 0 : idleTurns + 1;
25143
+ if (idleTurns >= DRAIN_TURNS) return;
25144
+ await new Promise((resolve2) => setTimeout(resolve2, busy ? 5 : 0));
24805
25145
  }
24806
25146
  }
24807
25147
  async request(_port, _init = {}) {
@@ -24818,6 +25158,8 @@ var LocalRuntimePod = class _LocalRuntimePod {
24818
25158
  }
24819
25159
  teardown() {
24820
25160
  this.disposed = true;
25161
+ for (const process2 of this.running) process2.kill();
25162
+ this.running.clear();
24821
25163
  this.router.closeAll();
24822
25164
  this.esbuild?.stop();
24823
25165
  }
@@ -24887,103 +25229,6 @@ function attachRolldownMirror(binding, volume, root) {
24887
25229
  volume.attach(fs, root);
24888
25230
  }
24889
25231
 
24890
- // src/runtime/sync-channel.ts
24891
- var STATE = 0;
24892
- var LENGTH = 1;
24893
- var MORE = 2;
24894
- var CONTROL_WORDS = 4;
24895
- var STATE_REQUEST = 1;
24896
- var STATE_RESPONSE = 2;
24897
- var STATE_CONTINUE = 3;
24898
- var STATE_CLOSED = 4;
24899
- function createSyncChannelBuffers(capacityBytes = 1 << 20) {
24900
- return {
24901
- control: new SharedArrayBuffer(CONTROL_WORDS * Int32Array.BYTES_PER_ELEMENT),
24902
- data: new SharedArrayBuffer(capacityBytes)
24903
- };
24904
- }
24905
- function syncChannelSupported() {
24906
- if (typeof SharedArrayBuffer !== "function") return false;
24907
- if (typeof Atomics !== "object" || typeof Atomics.wait !== "function") return false;
24908
- if (typeof window !== "undefined" && globalThis.crossOriginIsolated !== true) return false;
24909
- return true;
24910
- }
24911
- var SyncChannelServer = class {
24912
- constructor(buffers, handle) {
24913
- this.handle = handle;
24914
- this.control = new Int32Array(buffers.control);
24915
- this.data = new Uint8Array(buffers.data);
24916
- this.capacity = this.data.length;
24917
- }
24918
- handle;
24919
- control;
24920
- data;
24921
- capacity;
24922
- /** Request chunks gathered so far, and the response still to be sent. */
24923
- incoming = [];
24924
- outgoing = null;
24925
- sent = 0;
24926
- closed = false;
24927
- /** Call from the wake message the client sends after each chunk. */
24928
- async pump() {
24929
- if (this.closed) return;
24930
- const state = Atomics.load(this.control, STATE);
24931
- if (state === STATE_REQUEST) {
24932
- const size = Atomics.load(this.control, LENGTH);
24933
- this.incoming.push(this.data.slice(0, size));
24934
- if (Atomics.load(this.control, MORE) === 1) {
24935
- this.publish(STATE_CONTINUE);
24936
- return;
24937
- }
24938
- const request = concat3(this.incoming);
24939
- this.incoming = [];
24940
- let response;
24941
- try {
24942
- response = await this.handle(request);
24943
- } catch {
24944
- response = new Uint8Array();
24945
- }
24946
- if (this.closed) return;
24947
- this.outgoing = response;
24948
- this.sent = 0;
24949
- this.sendChunk();
24950
- return;
24951
- }
24952
- if (state === STATE_CONTINUE) this.sendChunk();
24953
- }
24954
- /** Release a blocked client, e.g. when the container is torn down. */
24955
- close() {
24956
- if (this.closed) return;
24957
- this.closed = true;
24958
- Atomics.store(this.control, STATE, STATE_CLOSED);
24959
- Atomics.notify(this.control, STATE);
24960
- }
24961
- sendChunk() {
24962
- const payload = this.outgoing ?? new Uint8Array();
24963
- const size = Math.min(this.capacity, payload.length - this.sent);
24964
- this.data.set(payload.subarray(this.sent, this.sent + size), 0);
24965
- this.sent += size;
24966
- Atomics.store(this.control, LENGTH, size);
24967
- Atomics.store(this.control, MORE, this.sent < payload.length ? 1 : 0);
24968
- this.publish(STATE_RESPONSE);
24969
- }
24970
- publish(state) {
24971
- Atomics.store(this.control, STATE, state);
24972
- Atomics.notify(this.control, STATE);
24973
- }
24974
- };
24975
- function concat3(parts) {
24976
- if (parts.length === 1) return parts[0];
24977
- const total = parts.reduce((sum, part) => sum + part.length, 0);
24978
- const joined = new Uint8Array(total);
24979
- let at = 0;
24980
- for (const part of parts) {
24981
- joined.set(part, at);
24982
- at += part.length;
24983
- }
24984
- return joined;
24985
- }
24986
-
24987
25232
  // src/runtime/remote-volume.ts
24988
25233
  var encoder7 = new TextEncoder();
24989
25234
  var decoder7 = new TextDecoder();
@@ -25078,24 +25323,32 @@ function defaultWorkerUrl() {
25078
25323
  return new URL("./worker-entry.js", import.meta.url);
25079
25324
  }
25080
25325
  function hasDomWorker() {
25081
- return typeof Worker === "function" && typeof document !== "undefined";
25326
+ return typeof Worker === "function";
25082
25327
  }
25083
25328
  async function startRuntimeWorker(options = {}) {
25084
25329
  const url = options.url ?? defaultWorkerUrl();
25085
25330
  const worker = hasDomWorker() ? await startDomWorker(url) : await startNodeWorker(url, options.workerData ?? {});
25086
- await new Promise((resolve2, reject) => {
25087
- const timer = setTimeout(() => reject(new Error("the runtime worker did not start in time")), options.timeoutMs ?? 1e4);
25088
- worker.onMessage((message) => {
25089
- if (message?.type === "sandboxedjs:ready") {
25331
+ try {
25332
+ await new Promise((resolve2, reject) => {
25333
+ const timer = setTimeout(() => reject(new Error("the runtime worker did not start in time")), options.timeoutMs ?? 1e4);
25334
+ worker.onMessage((message) => {
25335
+ if (message?.type === "sandboxedjs:ready") {
25336
+ clearTimeout(timer);
25337
+ resolve2();
25338
+ }
25339
+ });
25340
+ worker.onError((error) => {
25090
25341
  clearTimeout(timer);
25091
- resolve2();
25092
- }
25093
- });
25094
- worker.onError((error) => {
25095
- clearTimeout(timer);
25096
- reject(error instanceof Error ? error : new Error(String(error)));
25342
+ reject(error instanceof Error ? error : new Error(String(error)));
25343
+ });
25097
25344
  });
25098
- });
25345
+ } catch (error) {
25346
+ try {
25347
+ await worker.terminate();
25348
+ } catch {
25349
+ }
25350
+ throw error;
25351
+ }
25099
25352
  return worker;
25100
25353
  }
25101
25354
  async function startDomWorker(url) {
@@ -25156,6 +25409,8 @@ var WorkerProcess = class extends EventEmitter4 {
25156
25409
  started = false;
25157
25410
  pendingInput = [];
25158
25411
  inputEnded = false;
25412
+ inheritedInput;
25413
+ ownRawMode = false;
25159
25414
  on(event, listener) {
25160
25415
  return super.on(event, listener);
25161
25416
  }
@@ -25181,13 +25436,38 @@ var WorkerProcess = class extends EventEmitter4 {
25181
25436
  * program is still loading, and dropping them loses the answer to a prompt.
25182
25437
  */
25183
25438
  write(data) {
25439
+ if (this.inheritedInput) {
25440
+ this.inheritedInput.sendStdin(data);
25441
+ return;
25442
+ }
25184
25443
  if (this.started) this.worker.postMessage({ type: "stdin", data });
25185
25444
  else this.pendingInput.push(data);
25186
25445
  }
25187
25446
  endInput() {
25188
25447
  this.inputEnded = true;
25448
+ if (this.inheritedInput) {
25449
+ this.inheritedInput.endStdin?.();
25450
+ return;
25451
+ }
25189
25452
  if (this.started) this.worker.postMessage({ type: "stdin-end" });
25190
25453
  }
25454
+ rawMode(enabled) {
25455
+ this.ownRawMode = enabled;
25456
+ if (!this.inheritedInput) this.emit("rawmode", enabled);
25457
+ }
25458
+ /** The parent is blocked in Atomics.wait, so inherited input must bypass it. */
25459
+ inheritInput(child) {
25460
+ this.inheritedInput = child;
25461
+ this.emit("rawmode", false);
25462
+ child.on("rawmode", (enabled) => {
25463
+ if (this.inheritedInput === child) this.emit("rawmode", Boolean(enabled));
25464
+ });
25465
+ return () => {
25466
+ if (this.inheritedInput !== child) return;
25467
+ this.inheritedInput = void 0;
25468
+ this.emit("rawmode", this.ownRawMode);
25469
+ };
25470
+ }
25191
25471
  kill(_signal = "SIGTERM") {
25192
25472
  this.worker.postMessage({ type: "kill" });
25193
25473
  this.finish(137);
@@ -25208,25 +25488,30 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25208
25488
  super(options);
25209
25489
  this.workerUrl = options.workerUrl;
25210
25490
  }
25211
- /**
25212
- * Boot a Worker-backed pod, or return null when this host cannot support one.
25213
- *
25214
- * Declining is a first-class outcome. `SharedArrayBuffer` needs cross-origin
25215
- * isolation, a bundler may have made the guest script unreachable, and a host
25216
- * that supplied its own module objects has handed over things no thread
25217
- * boundary can carry. In every case the caller falls back to the in-realm pod
25218
- * and keeps working.
25219
- */
25220
- static async tryBoot(options = {}) {
25221
- if (!syncChannelSupported()) return null;
25222
- if (options.modules && Object.keys(options.modules).length > 0) return null;
25491
+ /** Boot without silently dropping synchronous child-process support. */
25492
+ static async boot(options = {}) {
25493
+ const reason = syncChannelUnavailableReason();
25494
+ if (reason) throw new Error(`SandboxedJs worker runtime unavailable: ${reason}`);
25495
+ if (options.modules && Object.keys(options.modules).length > 0) {
25496
+ throw new Error("SandboxedJs worker runtime cannot clone host-supplied modules. Use isolation: 'realm' explicitly.");
25497
+ }
25223
25498
  const pod = new _WorkerRuntimePod(options);
25224
25499
  try {
25225
25500
  const probe = await startRuntimeWorker({ ...options.workerUrl ? { url: options.workerUrl } : {}, timeoutMs: 1e4 });
25226
25501
  await probe.terminate();
25227
25502
  return pod;
25228
- } catch {
25503
+ } catch (cause) {
25229
25504
  pod.teardown();
25505
+ const detail = cause instanceof Error ? cause.message : String(cause);
25506
+ 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 });
25507
+ }
25508
+ }
25509
+ /** Compatibility mode, with an observable explanation for every fallback. */
25510
+ static async tryBoot(options = {}, onFallback = (error) => console.warn(`[sandboxedjs] Falling back to the realm runtime; synchronous child processes are unavailable. ${error.message}`)) {
25511
+ try {
25512
+ return await _WorkerRuntimePod.boot(options);
25513
+ } catch (error) {
25514
+ onFallback(error instanceof Error ? error : new Error(String(error)));
25230
25515
  return null;
25231
25516
  }
25232
25517
  }
@@ -25249,11 +25534,14 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25249
25534
  const streams = { target: null };
25250
25535
  const server = new SyncChannelServer(buffers, serveSyncSyscalls({
25251
25536
  volume: this.volume,
25252
- spawnChild: (request) => this.runChildToCompletion(request, streams.target)
25537
+ spawnChild: (request) => this.runChildToCompletion(request, streams.target, ownedChildren)
25253
25538
  }));
25254
25539
  const entry = { worker, server };
25255
25540
  this.live.add(entry);
25541
+ const ownedChildren = /* @__PURE__ */ new Set();
25256
25542
  const process2 = new WorkerProcess(worker, () => {
25543
+ for (const child of ownedChildren) child.kill("SIGTERM");
25544
+ ownedChildren.clear();
25257
25545
  this.live.delete(entry);
25258
25546
  server.close();
25259
25547
  this.closeProxies(owner);
@@ -25274,7 +25562,7 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25274
25562
  process2.error(String(message.text));
25275
25563
  return;
25276
25564
  case "rawmode":
25277
- process2.emit("rawmode", Boolean(message.enabled));
25565
+ process2.rawMode(Boolean(message.enabled));
25278
25566
  return;
25279
25567
  case "exit":
25280
25568
  process2.finish(Number(message.code));
@@ -25282,11 +25570,20 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25282
25570
  case "listen":
25283
25571
  this.proxyPort(Number(message.port), owner, worker);
25284
25572
  return;
25573
+ case "close-port": {
25574
+ const port = Number(message.port);
25575
+ const proxy = this.proxies.get(port);
25576
+ if (proxy?.owner === owner) {
25577
+ proxy.close();
25578
+ this.proxies.delete(port);
25579
+ }
25580
+ return;
25581
+ }
25285
25582
  case "http-response":
25286
25583
  this.settleProxied(Number(message.id), message.response);
25287
25584
  return;
25288
25585
  case "child-start":
25289
- this.startChild(worker, children, message);
25586
+ this.startChild(worker, children, message, ownedChildren);
25290
25587
  return;
25291
25588
  case "child-stdin":
25292
25589
  children.get(message.id)?.sendStdin?.(String(message.data));
@@ -25317,16 +25614,21 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25317
25614
  return process2;
25318
25615
  }
25319
25616
  /** Start an asynchronous child on the host's behalf and relay its events. */
25320
- startChild(worker, children, message) {
25617
+ startChild(worker, children, message, owned) {
25321
25618
  const handle = this.processManager.spawn(message.config);
25322
25619
  children.set(message.id, handle);
25620
+ owned.add(handle);
25621
+ handle.on("exit", () => {
25622
+ owned.delete(handle);
25623
+ children.delete(message.id);
25624
+ });
25323
25625
  for (const event of ["stdout", "stderr", "exit"]) {
25324
25626
  handle.on(event, (value) => worker.postMessage({ type: "child-event", id: message.id, event, value }));
25325
25627
  }
25326
25628
  handle.exec();
25327
25629
  }
25328
25630
  /** Run a child to completion and collect it, for the guest's `spawnSync`. */
25329
- runChildToCompletion(request, streamTo) {
25631
+ runChildToCompletion(request, streamTo, owned) {
25330
25632
  return new Promise((resolve2) => {
25331
25633
  let handle;
25332
25634
  try {
@@ -25342,9 +25644,13 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25342
25644
  resolve2({ status: null, stdout: "", stderr: "", signal: null, error: { ...failure.code ? { code: failure.code } : {}, message: failure.message } });
25343
25645
  return;
25344
25646
  }
25647
+ owned.add(handle);
25648
+ handle.on("exit", () => owned.delete(handle));
25345
25649
  let stdout = "";
25346
25650
  let stderr = "";
25347
25651
  const live = request.inheritStdio ? streamTo : null;
25652
+ const restoreInput = live?.inheritInput(handle);
25653
+ if (restoreInput) handle.on("exit", restoreInput);
25348
25654
  handle.on("stdout", (text2) => {
25349
25655
  stdout += text2;
25350
25656
  live?.output(text2);
@@ -25498,7 +25804,7 @@ var Container = class _Container {
25498
25804
  ...opts.onServerReady ? { onServerReady: opts.onServerReady } : {}
25499
25805
  };
25500
25806
  const workerOptions = { ...podOptions, ...opts.workerUrl ? { workerUrl: opts.workerUrl } : {} };
25501
- const pod = opts.pod ?? (opts.isolation === "realm" ? null : await WorkerRuntimePod.tryBoot(workerOptions)) ?? await LocalRuntimePod.boot(podOptions);
25807
+ 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);
25502
25808
  const kernel = new Kernel({
25503
25809
  pod,
25504
25810
  hostname: opts.hostname ?? "sandbox",
@@ -26006,7 +26312,7 @@ var Terminal = class {
26006
26312
  if (this.running) {
26007
26313
  const raw = this.currentStdin?.rawMode === true;
26008
26314
  if (ch === CTRL_C && !raw) {
26009
- this.session.proc.deliver("SIGINT");
26315
+ if (this.currentStdin) this.session.shell.interruptForeground("SIGINT", this.currentStdin);
26010
26316
  this.write("^C\r\n");
26011
26317
  return;
26012
26318
  }
@@ -26396,7 +26702,7 @@ function serveContainerOn(port, box) {
26396
26702
  }
26397
26703
  async function createPreview(box, options = {}) {
26398
26704
  if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) return null;
26399
- const scriptUrl = options.scriptUrl ?? new URL("./service-worker.js", import.meta.url);
26705
+ const scriptUrl = options.scriptUrl ?? new URL("./service-worker.js?no-inline", import.meta.url);
26400
26706
  let registration;
26401
26707
  try {
26402
26708
  registration = await navigator.serviceWorker.register(scriptUrl, {