sandboxedjs 0.1.30 → 0.1.31

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,
@@ -19716,6 +19747,7 @@ var KernelChildProcess = class {
19716
19747
  this.stdin.isTTY = true;
19717
19748
  this.stdin.interactive = true;
19718
19749
  }
19750
+ this.stdin.onRawMode = (enabled) => this.emit("rawmode", enabled);
19719
19751
  this.kernel = kernel;
19720
19752
  this.cred = cred;
19721
19753
  this.pid = pid;
@@ -19832,7 +19864,7 @@ function installNodeChildProcessBridge(pod, kernel, cred) {
19832
19864
  const originalSpawn = manager.spawn;
19833
19865
  manager.spawn = (config) => {
19834
19866
  const resolved = kernel.resolveExecutable(config.command, config.cwd ?? "/", config.env ?? {}, cred);
19835
- if (resolved?.kind === "builtin" && resolved.command?.name !== "node" && resolved.command?.name !== "nodejs") {
19867
+ if (resolved?.kind === "builtin") {
19836
19868
  return new KernelChildProcess(kernel, cred, config, nextBridgePid++);
19837
19869
  }
19838
19870
  return originalSpawn.call(manager, config);
@@ -19843,6 +19875,68 @@ function installNodeChildProcessBridge(pod, kernel, cred) {
19843
19875
  };
19844
19876
  }
19845
19877
 
19878
+ // src/runtime/host-module-tracker.ts
19879
+ var HostModuleTracker = class {
19880
+ active = 0;
19881
+ disposed = false;
19882
+ dispose() {
19883
+ this.disposed = true;
19884
+ }
19885
+ wrapped = /* @__PURE__ */ new WeakMap();
19886
+ originals = /* @__PURE__ */ new WeakMap();
19887
+ pending = () => this.active;
19888
+ unwrap = (value) => value && (typeof value === "object" || typeof value === "function") ? this.originals.get(value) ?? value : value;
19889
+ wrap(value) {
19890
+ if (value === null || typeof value !== "object" && typeof value !== "function") return value;
19891
+ const object = value;
19892
+ if (Array.isArray(value) || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return value;
19893
+ if (this.wrapped.has(object)) return this.wrapped.get(object);
19894
+ const tracker = this;
19895
+ const proxy = new Proxy(object, {
19896
+ // CommonJS native loaders re-export by assigning exports back onto the
19897
+ // binding object. Never store a process-owned proxy in a shared module.
19898
+ set(target, key, member) {
19899
+ return Reflect.set(target, key, tracker.unwrap(member), target);
19900
+ },
19901
+ defineProperty(target, key, descriptor) {
19902
+ return Reflect.defineProperty(target, key, "value" in descriptor ? { ...descriptor, value: tracker.unwrap(descriptor.value) } : descriptor);
19903
+ },
19904
+ get(target, key) {
19905
+ const descriptor = Reflect.getOwnPropertyDescriptor(target, key);
19906
+ const member = Reflect.get(target, key, target);
19907
+ if (descriptor && !descriptor.configurable && "value" in descriptor && !descriptor.writable) return member;
19908
+ return typeof member === "function" ? tracker.wrap(member) : member;
19909
+ },
19910
+ apply(target, receiver, args) {
19911
+ const result = Reflect.apply(target, tracker.unwrap(receiver), args.map(tracker.unwrap));
19912
+ if (result && typeof result.then === "function") {
19913
+ tracker.active++;
19914
+ return Promise.resolve(result).then(
19915
+ (value2) => {
19916
+ tracker.active--;
19917
+ return tracker.disposed ? new Promise(() => {
19918
+ }) : tracker.wrap(value2);
19919
+ },
19920
+ (error) => {
19921
+ tracker.active--;
19922
+ if (tracker.disposed) return new Promise(() => {
19923
+ });
19924
+ throw error;
19925
+ }
19926
+ );
19927
+ }
19928
+ return tracker.wrap(result);
19929
+ },
19930
+ construct(target, args, newTarget) {
19931
+ return tracker.wrap(Reflect.construct(target, args.map(tracker.unwrap), tracker.unwrap(newTarget)));
19932
+ }
19933
+ });
19934
+ this.wrapped.set(object, proxy);
19935
+ this.originals.set(proxy, object);
19936
+ return proxy;
19937
+ }
19938
+ };
19939
+
19846
19940
  // src/runtime/commonjs-engine.ts
19847
19941
  init_path();
19848
19942
  var HELPERS = {
@@ -21580,6 +21674,7 @@ var VirtualHttpServer = class extends EventEmitter4__default.default {
21580
21674
  owner;
21581
21675
  listening = false;
21582
21676
  portValue = null;
21677
+ referenced = true;
21583
21678
  listen(...args) {
21584
21679
  const callback = typeof args.at(-1) === "function" ? args.pop() : void 0;
21585
21680
  const first = args[0];
@@ -21608,11 +21703,16 @@ var VirtualHttpServer = class extends EventEmitter4__default.default {
21608
21703
  return this.portValue === null ? null : { address: "0.0.0.0", family: "IPv4", port: this.portValue };
21609
21704
  }
21610
21705
  ref() {
21706
+ this.referenced = true;
21611
21707
  return this;
21612
21708
  }
21613
21709
  unref() {
21710
+ this.referenced = false;
21614
21711
  return this;
21615
21712
  }
21713
+ hasRef() {
21714
+ return this.referenced;
21715
+ }
21616
21716
  setTimeout(_milliseconds, callback) {
21617
21717
  if (callback) this.on("timeout", callback);
21618
21718
  return this;
@@ -21622,13 +21722,16 @@ var VirtualHttpRouter = class {
21622
21722
  servers = /* @__PURE__ */ new Map();
21623
21723
  /** Notified when a server begins listening, for `onServerReady`. */
21624
21724
  onListen;
21725
+ onClose;
21625
21726
  register(port, server, owner) {
21626
21727
  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
21728
  this.servers.set(port, { server, owner });
21628
21729
  this.onListen?.(port);
21629
21730
  }
21630
21731
  unregister(port, server) {
21631
- if (this.servers.get(port)?.server === server) this.servers.delete(port);
21732
+ if (this.servers.get(port)?.server !== server) return;
21733
+ this.servers.delete(port);
21734
+ this.onClose?.(port);
21632
21735
  }
21633
21736
  /** Whether anything in this container is listening on `port`. */
21634
21737
  activePortsIncludes(port) {
@@ -21637,6 +21740,9 @@ var VirtualHttpRouter = class {
21637
21740
  activePorts(owner) {
21638
21741
  return [...this.servers].filter(([, item]) => owner === void 0 || item.owner === owner).map(([port]) => port).sort((a, b) => a - b);
21639
21742
  }
21743
+ referencedPorts(owner) {
21744
+ return [...this.servers].filter(([, item]) => item.owner === owner && item.server.hasRef()).map(([port]) => port);
21745
+ }
21640
21746
  closeOwner(owner) {
21641
21747
  for (const { server, owner: value } of [...this.servers.values()]) if (value === owner) server.close();
21642
21748
  }
@@ -22048,6 +22154,108 @@ function hashFor(algorithm) {
22048
22154
  function toBytes2(data, encoding) {
22049
22155
  return typeof data === "string" ? Buffer2.from(data, encoding) : data;
22050
22156
  }
22157
+
22158
+ // src/runtime/sync-channel.ts
22159
+ var STATE = 0;
22160
+ var LENGTH = 1;
22161
+ var MORE = 2;
22162
+ var CONTROL_WORDS = 4;
22163
+ var STATE_REQUEST = 1;
22164
+ var STATE_RESPONSE = 2;
22165
+ var STATE_CONTINUE = 3;
22166
+ var STATE_CLOSED = 4;
22167
+ function createSyncChannelBuffers(capacityBytes = 1 << 20) {
22168
+ return {
22169
+ control: new SharedArrayBuffer(CONTROL_WORDS * Int32Array.BYTES_PER_ELEMENT),
22170
+ data: new SharedArrayBuffer(capacityBytes)
22171
+ };
22172
+ }
22173
+ function syncChannelUnavailableReason() {
22174
+ if (typeof globalThis.crossOriginIsolated === "boolean" && !globalThis.crossOriginIsolated) {
22175
+ 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.";
22176
+ }
22177
+ if (typeof SharedArrayBuffer !== "function") return "SharedArrayBuffer is unavailable in this host.";
22178
+ if (typeof Atomics !== "object" || typeof Atomics.wait !== "function") return "Atomics.wait is unavailable in this host.";
22179
+ return null;
22180
+ }
22181
+ function syncChannelSupported() {
22182
+ return syncChannelUnavailableReason() === null;
22183
+ }
22184
+ var SyncChannelServer = class {
22185
+ constructor(buffers, handle) {
22186
+ this.handle = handle;
22187
+ this.control = new Int32Array(buffers.control);
22188
+ this.data = new Uint8Array(buffers.data);
22189
+ this.capacity = this.data.length;
22190
+ }
22191
+ handle;
22192
+ control;
22193
+ data;
22194
+ capacity;
22195
+ /** Request chunks gathered so far, and the response still to be sent. */
22196
+ incoming = [];
22197
+ outgoing = null;
22198
+ sent = 0;
22199
+ closed = false;
22200
+ /** Call from the wake message the client sends after each chunk. */
22201
+ async pump() {
22202
+ if (this.closed) return;
22203
+ const state = Atomics.load(this.control, STATE);
22204
+ if (state === STATE_REQUEST) {
22205
+ const size = Atomics.load(this.control, LENGTH);
22206
+ this.incoming.push(this.data.slice(0, size));
22207
+ if (Atomics.load(this.control, MORE) === 1) {
22208
+ this.publish(STATE_CONTINUE);
22209
+ return;
22210
+ }
22211
+ const request = concat3(this.incoming);
22212
+ this.incoming = [];
22213
+ let response;
22214
+ try {
22215
+ response = await this.handle(request);
22216
+ } catch {
22217
+ response = new Uint8Array();
22218
+ }
22219
+ if (this.closed) return;
22220
+ this.outgoing = response;
22221
+ this.sent = 0;
22222
+ this.sendChunk();
22223
+ return;
22224
+ }
22225
+ if (state === STATE_CONTINUE) this.sendChunk();
22226
+ }
22227
+ /** Release a blocked client, e.g. when the container is torn down. */
22228
+ close() {
22229
+ if (this.closed) return;
22230
+ this.closed = true;
22231
+ Atomics.store(this.control, STATE, STATE_CLOSED);
22232
+ Atomics.notify(this.control, STATE);
22233
+ }
22234
+ sendChunk() {
22235
+ const payload = this.outgoing ?? new Uint8Array();
22236
+ const size = Math.min(this.capacity, payload.length - this.sent);
22237
+ this.data.set(payload.subarray(this.sent, this.sent + size), 0);
22238
+ this.sent += size;
22239
+ Atomics.store(this.control, LENGTH, size);
22240
+ Atomics.store(this.control, MORE, this.sent < payload.length ? 1 : 0);
22241
+ this.publish(STATE_RESPONSE);
22242
+ }
22243
+ publish(state) {
22244
+ Atomics.store(this.control, STATE, state);
22245
+ Atomics.notify(this.control, STATE);
22246
+ }
22247
+ };
22248
+ function concat3(parts) {
22249
+ if (parts.length === 1) return parts[0];
22250
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
22251
+ const joined = new Uint8Array(total);
22252
+ let at = 0;
22253
+ for (const part of parts) {
22254
+ joined.set(part, at);
22255
+ at += part.length;
22256
+ }
22257
+ return joined;
22258
+ }
22051
22259
  var ChildProcess = class extends EventEmitter4__default.default {
22052
22260
  stdout = new streamModule4__default.default.PassThrough();
22053
22261
  stderr = new streamModule4__default.default.PassThrough();
@@ -22252,7 +22460,7 @@ function unavailable(name) {
22252
22460
  const list = Array.isArray(args[1]) ? args[1].map(String) : [];
22253
22461
  const command = file3 ? [file3, ...list].join(" ") : void 0;
22254
22462
  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.`
22463
+ `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
22464
  );
22257
22465
  error.code = "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM";
22258
22466
  throw error;
@@ -22540,6 +22748,11 @@ function createReadlineModule(defaultInput, defaultOutput = () => void 0) {
22540
22748
  edit(sequence, key) {
22541
22749
  if (this.closed) return;
22542
22750
  const name = key.name;
22751
+ if (key.ctrl && name === "c") {
22752
+ if (this.listenerCount("SIGINT") > 0) this.emit("SIGINT");
22753
+ else this.close();
22754
+ return;
22755
+ }
22543
22756
  if (name === "return" || name === "enter") {
22544
22757
  const value = this.line;
22545
22758
  this.line = "";
@@ -22635,7 +22848,7 @@ function createReadlineModule(defaultInput, defaultOutput = () => void 0) {
22635
22848
  };
22636
22849
  const createInterface = (options, output) => {
22637
22850
  if (options && typeof options === "object" && !options.on) {
22638
- const resolvedOutput2 = options.output ?? defaultOutput();
22851
+ const resolvedOutput2 = options.output;
22639
22852
  const instance = new Interface(
22640
22853
  options.input ?? defaultInput(),
22641
22854
  resolvedOutput2,
@@ -22644,7 +22857,7 @@ function createReadlineModule(defaultInput, defaultOutput = () => void 0) {
22644
22857
  if (typeof options.prompt === "string") instance.setPrompt(options.prompt);
22645
22858
  return instance;
22646
22859
  }
22647
- const resolvedOutput = output ?? defaultOutput();
22860
+ const resolvedOutput = output;
22648
22861
  return new Interface(options ?? defaultInput(), resolvedOutput, isTerminal(void 0, resolvedOutput));
22649
22862
  };
22650
22863
  const noop = () => {
@@ -22913,7 +23126,7 @@ function createCoreModules(options) {
22913
23126
  "stream/promises": createStreamPromises(),
22914
23127
  string_decoder: stringDecoderModule__default.default,
22915
23128
  timers: { ...timersModule__default.default, ...timers.api },
22916
- "timers/promises": createTimerPromises(),
23129
+ "timers/promises": createTimerPromises(timers.api),
22917
23130
  tty: { isatty: () => false, ReadStream: streamModule4__default.default.Readable, WriteStream: streamModule4__default.default.Writable },
22918
23131
  url: url_module_default,
22919
23132
  util: util_module_default,
@@ -23658,10 +23871,10 @@ function createTrackedTimers() {
23658
23871
  cancelAll
23659
23872
  };
23660
23873
  }
23661
- function createTimerPromises() {
23874
+ function createTimerPromises(timers) {
23662
23875
  return {
23663
- setTimeout: (delay, value) => new Promise((resolve2) => setTimeout(resolve2, delay, value)),
23664
- setImmediate: (value) => new Promise((resolve2) => setTimeout(resolve2, 0, value))
23876
+ setTimeout: (delay, value) => new Promise((resolve2) => timers.setTimeout(resolve2, delay, value)),
23877
+ setImmediate: (value) => new Promise((resolve2) => timers.setImmediate(resolve2, value))
23665
23878
  };
23666
23879
  }
23667
23880
  function createAsyncHooksModule() {
@@ -24369,11 +24582,8 @@ function ensureProcessGlobal() {
24369
24582
  // src/runtime/host-rolldown.ts
24370
24583
  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
24584
  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
- }
24585
+ const reason = syncChannelUnavailableReason();
24586
+ if (reason) throw new Error(`SandboxedJs threaded WASM unavailable: ${reason}`);
24377
24587
  try {
24378
24588
  const loaded = await import('@rolldown/binding-wasm32-wasi');
24379
24589
  return "__fs" in loaded ? { ...loaded } : loaded.default ?? loaded;
@@ -24381,9 +24591,9 @@ async function loadHostRolldownBinding() {
24381
24591
  if (typeof process !== "undefined" && process.env?.SANDBOXEDJS_DEBUG) {
24382
24592
  console.error("[sandboxedjs] Rolldown WASI binding unavailable:", error);
24383
24593
  }
24384
- const reason = error instanceof Error ? error.message : String(error);
24594
+ const reason2 = error instanceof Error ? error.message : String(error);
24385
24595
  throw new Error(
24386
- `The optional Rolldown WASI binding could not be loaded: ${reason}` + (typeof window === "undefined" ? "" : `
24596
+ `The optional Rolldown WASI binding could not be loaded: ${reason2}` + (typeof window === "undefined" ? "" : `
24387
24597
 
24388
24598
  ${BUNDLER_HINT}`),
24389
24599
  { cause: error }
@@ -24494,6 +24704,8 @@ var LocalProcess = class extends EventEmitter4__default.default {
24494
24704
  }
24495
24705
  /** End the process now, for an asynchronous `process.exit`. */
24496
24706
  exitNow(code) {
24707
+ this.killed = true;
24708
+ this.cleanup?.();
24497
24709
  this.finish(code);
24498
24710
  this.resolveKilled();
24499
24711
  }
@@ -24677,6 +24889,7 @@ var LocalRuntimePod = class _LocalRuntimePod {
24677
24889
  )
24678
24890
  };
24679
24891
  disposed = false;
24892
+ running = /* @__PURE__ */ new Set();
24680
24893
  workdir;
24681
24894
  env;
24682
24895
  aliases;
@@ -24718,8 +24931,9 @@ var LocalRuntimePod = class _LocalRuntimePod {
24718
24931
  const cwd = typeof options.cwd === "string" ? options.cwd : this.workdir;
24719
24932
  const env2 = { ...this.env, ...isRecord(options.env) ? options.env : {} };
24720
24933
  const owner = `${this.instanceId}:${Math.random().toString(36).slice(2)}`;
24721
- return new LocalProcess(async (proc) => {
24934
+ const process2 = new LocalProcess(async (proc) => {
24722
24935
  await this.prepareRolldown(cwd, env2);
24936
+ if (proc.isKilled()) return 137;
24723
24937
  const untrack = trackProcess(proc);
24724
24938
  let requestedExit = 0;
24725
24939
  let engine;
@@ -24747,7 +24961,9 @@ var LocalRuntimePod = class _LocalRuntimePod {
24747
24961
  onRawMode: (enabled) => proc.emit("rawmode", enabled)
24748
24962
  });
24749
24963
  proc.acceptInput((data) => core.writeStdin(data), () => core.endStdin());
24964
+ const hostWork = new HostModuleTracker();
24750
24965
  proc.onKill(() => {
24966
+ hostWork.dispose();
24751
24967
  core.cancelTimers();
24752
24968
  this.router.closeOwner(owner);
24753
24969
  });
@@ -24756,21 +24972,22 @@ var LocalRuntimePod = class _LocalRuntimePod {
24756
24972
  builtins: core.builtins,
24757
24973
  globals: core.globals,
24758
24974
  aliases: this.aliases,
24759
- overrides: this.modules
24975
+ overrides: Object.fromEntries(Object.entries(this.modules).map(([key, value]) => [key, hostWork.wrap(value)]))
24760
24976
  });
24761
24977
  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
- }
24978
+ await Promise.race([engine.run(script), proc.waitForKill()]);
24979
+ await this.settle(owner, core.pendingHandles, core.readingStdin, () => core.pendingRequests() + hostWork.pending(), () => proc.isKilled());
24768
24980
  return requestedExit;
24769
24981
  } finally {
24982
+ hostWork.dispose();
24983
+ core.cancelTimers();
24770
24984
  untrack();
24771
24985
  this.router.closeOwner(owner);
24772
24986
  }
24773
24987
  });
24988
+ this.running.add(process2);
24989
+ void process2.completion.then(() => this.running.delete(process2));
24990
+ return process2;
24774
24991
  }
24775
24992
  /**
24776
24993
  * Rolldown's JavaScript API synchronously requires its compiled binding.
@@ -24806,19 +25023,13 @@ var LocalRuntimePod = class _LocalRuntimePod {
24806
25023
  * A plain script that has genuinely finished falls straight through both,
24807
25024
  * costing a handful of empty turns.
24808
25025
  */
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));
25026
+ async settle(owner, pendingHandles, readingStdin, pendingRequests, killed) {
25027
+ let idleTurns = 0;
25028
+ while (!killed()) {
25029
+ const busy = this.router.referencedPorts(owner).length > 0 || pendingHandles() > 0 || pendingRequests() > 0 || readingStdin();
25030
+ idleTurns = busy ? 0 : idleTurns + 1;
25031
+ if (idleTurns >= DRAIN_TURNS) return;
25032
+ await new Promise((resolve2) => setTimeout(resolve2, busy ? 5 : 0));
24822
25033
  }
24823
25034
  }
24824
25035
  async request(_port, _init = {}) {
@@ -24835,6 +25046,8 @@ var LocalRuntimePod = class _LocalRuntimePod {
24835
25046
  }
24836
25047
  teardown() {
24837
25048
  this.disposed = true;
25049
+ for (const process2 of this.running) process2.kill();
25050
+ this.running.clear();
24838
25051
  this.router.closeAll();
24839
25052
  this.esbuild?.stop();
24840
25053
  }
@@ -24904,103 +25117,6 @@ function attachRolldownMirror(binding, volume, root) {
24904
25117
  volume.attach(fs, root);
24905
25118
  }
24906
25119
 
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
25120
  // src/runtime/remote-volume.ts
25005
25121
  var encoder7 = new TextEncoder();
25006
25122
  var decoder7 = new TextDecoder();
@@ -25095,24 +25211,32 @@ function defaultWorkerUrl() {
25095
25211
  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
25212
  }
25097
25213
  function hasDomWorker() {
25098
- return typeof Worker === "function" && typeof document !== "undefined";
25214
+ return typeof Worker === "function";
25099
25215
  }
25100
25216
  async function startRuntimeWorker(options = {}) {
25101
25217
  const url = options.url ?? defaultWorkerUrl();
25102
25218
  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") {
25219
+ try {
25220
+ await new Promise((resolve2, reject) => {
25221
+ const timer = setTimeout(() => reject(new Error("the runtime worker did not start in time")), options.timeoutMs ?? 1e4);
25222
+ worker.onMessage((message) => {
25223
+ if (message?.type === "sandboxedjs:ready") {
25224
+ clearTimeout(timer);
25225
+ resolve2();
25226
+ }
25227
+ });
25228
+ worker.onError((error) => {
25107
25229
  clearTimeout(timer);
25108
- resolve2();
25109
- }
25110
- });
25111
- worker.onError((error) => {
25112
- clearTimeout(timer);
25113
- reject(error instanceof Error ? error : new Error(String(error)));
25230
+ reject(error instanceof Error ? error : new Error(String(error)));
25231
+ });
25114
25232
  });
25115
- });
25233
+ } catch (error) {
25234
+ try {
25235
+ await worker.terminate();
25236
+ } catch {
25237
+ }
25238
+ throw error;
25239
+ }
25116
25240
  return worker;
25117
25241
  }
25118
25242
  async function startDomWorker(url) {
@@ -25173,6 +25297,8 @@ var WorkerProcess = class extends EventEmitter4__default.default {
25173
25297
  started = false;
25174
25298
  pendingInput = [];
25175
25299
  inputEnded = false;
25300
+ inheritedInput;
25301
+ ownRawMode = false;
25176
25302
  on(event, listener) {
25177
25303
  return super.on(event, listener);
25178
25304
  }
@@ -25198,13 +25324,38 @@ var WorkerProcess = class extends EventEmitter4__default.default {
25198
25324
  * program is still loading, and dropping them loses the answer to a prompt.
25199
25325
  */
25200
25326
  write(data) {
25327
+ if (this.inheritedInput) {
25328
+ this.inheritedInput.sendStdin(data);
25329
+ return;
25330
+ }
25201
25331
  if (this.started) this.worker.postMessage({ type: "stdin", data });
25202
25332
  else this.pendingInput.push(data);
25203
25333
  }
25204
25334
  endInput() {
25205
25335
  this.inputEnded = true;
25336
+ if (this.inheritedInput) {
25337
+ this.inheritedInput.endStdin?.();
25338
+ return;
25339
+ }
25206
25340
  if (this.started) this.worker.postMessage({ type: "stdin-end" });
25207
25341
  }
25342
+ rawMode(enabled) {
25343
+ this.ownRawMode = enabled;
25344
+ if (!this.inheritedInput) this.emit("rawmode", enabled);
25345
+ }
25346
+ /** The parent is blocked in Atomics.wait, so inherited input must bypass it. */
25347
+ inheritInput(child) {
25348
+ this.inheritedInput = child;
25349
+ this.emit("rawmode", false);
25350
+ child.on("rawmode", (enabled) => {
25351
+ if (this.inheritedInput === child) this.emit("rawmode", Boolean(enabled));
25352
+ });
25353
+ return () => {
25354
+ if (this.inheritedInput !== child) return;
25355
+ this.inheritedInput = void 0;
25356
+ this.emit("rawmode", this.ownRawMode);
25357
+ };
25358
+ }
25208
25359
  kill(_signal = "SIGTERM") {
25209
25360
  this.worker.postMessage({ type: "kill" });
25210
25361
  this.finish(137);
@@ -25225,25 +25376,30 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25225
25376
  super(options);
25226
25377
  this.workerUrl = options.workerUrl;
25227
25378
  }
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;
25379
+ /** Boot without silently dropping synchronous child-process support. */
25380
+ static async boot(options = {}) {
25381
+ const reason = syncChannelUnavailableReason();
25382
+ if (reason) throw new Error(`SandboxedJs worker runtime unavailable: ${reason}`);
25383
+ if (options.modules && Object.keys(options.modules).length > 0) {
25384
+ throw new Error("SandboxedJs worker runtime cannot clone host-supplied modules. Use isolation: 'realm' explicitly.");
25385
+ }
25240
25386
  const pod = new _WorkerRuntimePod(options);
25241
25387
  try {
25242
25388
  const probe = await startRuntimeWorker({ ...options.workerUrl ? { url: options.workerUrl } : {}, timeoutMs: 1e4 });
25243
25389
  await probe.terminate();
25244
25390
  return pod;
25245
- } catch {
25391
+ } catch (cause) {
25246
25392
  pod.teardown();
25393
+ const detail = cause instanceof Error ? cause.message : String(cause);
25394
+ 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 });
25395
+ }
25396
+ }
25397
+ /** Compatibility mode, with an observable explanation for every fallback. */
25398
+ static async tryBoot(options = {}, onFallback = (error) => console.warn(`[sandboxedjs] Falling back to the realm runtime; synchronous child processes are unavailable. ${error.message}`)) {
25399
+ try {
25400
+ return await _WorkerRuntimePod.boot(options);
25401
+ } catch (error) {
25402
+ onFallback(error instanceof Error ? error : new Error(String(error)));
25247
25403
  return null;
25248
25404
  }
25249
25405
  }
@@ -25266,11 +25422,14 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25266
25422
  const streams = { target: null };
25267
25423
  const server = new SyncChannelServer(buffers, serveSyncSyscalls({
25268
25424
  volume: this.volume,
25269
- spawnChild: (request) => this.runChildToCompletion(request, streams.target)
25425
+ spawnChild: (request) => this.runChildToCompletion(request, streams.target, ownedChildren)
25270
25426
  }));
25271
25427
  const entry = { worker, server };
25272
25428
  this.live.add(entry);
25429
+ const ownedChildren = /* @__PURE__ */ new Set();
25273
25430
  const process2 = new WorkerProcess(worker, () => {
25431
+ for (const child of ownedChildren) child.kill("SIGTERM");
25432
+ ownedChildren.clear();
25274
25433
  this.live.delete(entry);
25275
25434
  server.close();
25276
25435
  this.closeProxies(owner);
@@ -25291,7 +25450,7 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25291
25450
  process2.error(String(message.text));
25292
25451
  return;
25293
25452
  case "rawmode":
25294
- process2.emit("rawmode", Boolean(message.enabled));
25453
+ process2.rawMode(Boolean(message.enabled));
25295
25454
  return;
25296
25455
  case "exit":
25297
25456
  process2.finish(Number(message.code));
@@ -25299,11 +25458,20 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25299
25458
  case "listen":
25300
25459
  this.proxyPort(Number(message.port), owner, worker);
25301
25460
  return;
25461
+ case "close-port": {
25462
+ const port = Number(message.port);
25463
+ const proxy = this.proxies.get(port);
25464
+ if (proxy?.owner === owner) {
25465
+ proxy.close();
25466
+ this.proxies.delete(port);
25467
+ }
25468
+ return;
25469
+ }
25302
25470
  case "http-response":
25303
25471
  this.settleProxied(Number(message.id), message.response);
25304
25472
  return;
25305
25473
  case "child-start":
25306
- this.startChild(worker, children, message);
25474
+ this.startChild(worker, children, message, ownedChildren);
25307
25475
  return;
25308
25476
  case "child-stdin":
25309
25477
  children.get(message.id)?.sendStdin?.(String(message.data));
@@ -25334,16 +25502,21 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25334
25502
  return process2;
25335
25503
  }
25336
25504
  /** Start an asynchronous child on the host's behalf and relay its events. */
25337
- startChild(worker, children, message) {
25505
+ startChild(worker, children, message, owned) {
25338
25506
  const handle = this.processManager.spawn(message.config);
25339
25507
  children.set(message.id, handle);
25508
+ owned.add(handle);
25509
+ handle.on("exit", () => {
25510
+ owned.delete(handle);
25511
+ children.delete(message.id);
25512
+ });
25340
25513
  for (const event of ["stdout", "stderr", "exit"]) {
25341
25514
  handle.on(event, (value) => worker.postMessage({ type: "child-event", id: message.id, event, value }));
25342
25515
  }
25343
25516
  handle.exec();
25344
25517
  }
25345
25518
  /** Run a child to completion and collect it, for the guest's `spawnSync`. */
25346
- runChildToCompletion(request, streamTo) {
25519
+ runChildToCompletion(request, streamTo, owned) {
25347
25520
  return new Promise((resolve2) => {
25348
25521
  let handle;
25349
25522
  try {
@@ -25359,9 +25532,13 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25359
25532
  resolve2({ status: null, stdout: "", stderr: "", signal: null, error: { ...failure.code ? { code: failure.code } : {}, message: failure.message } });
25360
25533
  return;
25361
25534
  }
25535
+ owned.add(handle);
25536
+ handle.on("exit", () => owned.delete(handle));
25362
25537
  let stdout = "";
25363
25538
  let stderr = "";
25364
25539
  const live = request.inheritStdio ? streamTo : null;
25540
+ const restoreInput = live?.inheritInput(handle);
25541
+ if (restoreInput) handle.on("exit", restoreInput);
25365
25542
  handle.on("stdout", (text2) => {
25366
25543
  stdout += text2;
25367
25544
  live?.output(text2);
@@ -25515,7 +25692,7 @@ var Container = class _Container {
25515
25692
  ...opts.onServerReady ? { onServerReady: opts.onServerReady } : {}
25516
25693
  };
25517
25694
  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);
25695
+ 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
25696
  const kernel = new Kernel({
25520
25697
  pod,
25521
25698
  hostname: opts.hostname ?? "sandbox",
@@ -26023,7 +26200,7 @@ var Terminal = class {
26023
26200
  if (this.running) {
26024
26201
  const raw = this.currentStdin?.rawMode === true;
26025
26202
  if (ch === CTRL_C && !raw) {
26026
- this.session.proc.deliver("SIGINT");
26203
+ if (this.currentStdin) this.session.shell.interruptForeground("SIGINT", this.currentStdin);
26027
26204
  this.write("^C\r\n");
26028
26205
  return;
26029
26206
  }
@@ -26413,7 +26590,7 @@ function serveContainerOn(port, box) {
26413
26590
  }
26414
26591
  async function createPreview(box, options = {}) {
26415
26592
  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)));
26593
+ 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
26594
  let registration;
26418
26595
  try {
26419
26596
  registration = await navigator.serviceWorker.register(scriptUrl, {