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.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,
@@ -19699,6 +19730,7 @@ var KernelChildProcess = class {
19699
19730
  this.stdin.isTTY = true;
19700
19731
  this.stdin.interactive = true;
19701
19732
  }
19733
+ this.stdin.onRawMode = (enabled) => this.emit("rawmode", enabled);
19702
19734
  this.kernel = kernel;
19703
19735
  this.cred = cred;
19704
19736
  this.pid = pid;
@@ -19815,7 +19847,7 @@ function installNodeChildProcessBridge(pod, kernel, cred) {
19815
19847
  const originalSpawn = manager.spawn;
19816
19848
  manager.spawn = (config) => {
19817
19849
  const resolved = kernel.resolveExecutable(config.command, config.cwd ?? "/", config.env ?? {}, cred);
19818
- if (resolved?.kind === "builtin" && resolved.command?.name !== "node" && resolved.command?.name !== "nodejs") {
19850
+ if (resolved?.kind === "builtin") {
19819
19851
  return new KernelChildProcess(kernel, cred, config, nextBridgePid++);
19820
19852
  }
19821
19853
  return originalSpawn.call(manager, config);
@@ -19826,6 +19858,68 @@ function installNodeChildProcessBridge(pod, kernel, cred) {
19826
19858
  };
19827
19859
  }
19828
19860
 
19861
+ // src/runtime/host-module-tracker.ts
19862
+ var HostModuleTracker = class {
19863
+ active = 0;
19864
+ disposed = false;
19865
+ dispose() {
19866
+ this.disposed = true;
19867
+ }
19868
+ wrapped = /* @__PURE__ */ new WeakMap();
19869
+ originals = /* @__PURE__ */ new WeakMap();
19870
+ pending = () => this.active;
19871
+ unwrap = (value) => value && (typeof value === "object" || typeof value === "function") ? this.originals.get(value) ?? value : value;
19872
+ wrap(value) {
19873
+ if (value === null || typeof value !== "object" && typeof value !== "function") return value;
19874
+ const object = value;
19875
+ if (Array.isArray(value) || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return value;
19876
+ if (this.wrapped.has(object)) return this.wrapped.get(object);
19877
+ const tracker = this;
19878
+ const proxy = new Proxy(object, {
19879
+ // CommonJS native loaders re-export by assigning exports back onto the
19880
+ // binding object. Never store a process-owned proxy in a shared module.
19881
+ set(target, key, member) {
19882
+ return Reflect.set(target, key, tracker.unwrap(member), target);
19883
+ },
19884
+ defineProperty(target, key, descriptor) {
19885
+ return Reflect.defineProperty(target, key, "value" in descriptor ? { ...descriptor, value: tracker.unwrap(descriptor.value) } : descriptor);
19886
+ },
19887
+ get(target, key) {
19888
+ const descriptor = Reflect.getOwnPropertyDescriptor(target, key);
19889
+ const member = Reflect.get(target, key, target);
19890
+ if (descriptor && !descriptor.configurable && "value" in descriptor && !descriptor.writable) return member;
19891
+ return typeof member === "function" ? tracker.wrap(member) : member;
19892
+ },
19893
+ apply(target, receiver, args) {
19894
+ const result = Reflect.apply(target, tracker.unwrap(receiver), args.map(tracker.unwrap));
19895
+ if (result && typeof result.then === "function") {
19896
+ tracker.active++;
19897
+ return Promise.resolve(result).then(
19898
+ (value2) => {
19899
+ tracker.active--;
19900
+ return tracker.disposed ? new Promise(() => {
19901
+ }) : tracker.wrap(value2);
19902
+ },
19903
+ (error) => {
19904
+ tracker.active--;
19905
+ if (tracker.disposed) return new Promise(() => {
19906
+ });
19907
+ throw error;
19908
+ }
19909
+ );
19910
+ }
19911
+ return tracker.wrap(result);
19912
+ },
19913
+ construct(target, args, newTarget) {
19914
+ return tracker.wrap(Reflect.construct(target, args.map(tracker.unwrap), tracker.unwrap(newTarget)));
19915
+ }
19916
+ });
19917
+ this.wrapped.set(object, proxy);
19918
+ this.originals.set(proxy, object);
19919
+ return proxy;
19920
+ }
19921
+ };
19922
+
19829
19923
  // src/runtime/commonjs-engine.ts
19830
19924
  init_path();
19831
19925
  var HELPERS = {
@@ -21563,6 +21657,7 @@ var VirtualHttpServer = class extends EventEmitter4 {
21563
21657
  owner;
21564
21658
  listening = false;
21565
21659
  portValue = null;
21660
+ referenced = true;
21566
21661
  listen(...args) {
21567
21662
  const callback = typeof args.at(-1) === "function" ? args.pop() : void 0;
21568
21663
  const first = args[0];
@@ -21591,11 +21686,16 @@ var VirtualHttpServer = class extends EventEmitter4 {
21591
21686
  return this.portValue === null ? null : { address: "0.0.0.0", family: "IPv4", port: this.portValue };
21592
21687
  }
21593
21688
  ref() {
21689
+ this.referenced = true;
21594
21690
  return this;
21595
21691
  }
21596
21692
  unref() {
21693
+ this.referenced = false;
21597
21694
  return this;
21598
21695
  }
21696
+ hasRef() {
21697
+ return this.referenced;
21698
+ }
21599
21699
  setTimeout(_milliseconds, callback) {
21600
21700
  if (callback) this.on("timeout", callback);
21601
21701
  return this;
@@ -21605,13 +21705,16 @@ var VirtualHttpRouter = class {
21605
21705
  servers = /* @__PURE__ */ new Map();
21606
21706
  /** Notified when a server begins listening, for `onServerReady`. */
21607
21707
  onListen;
21708
+ onClose;
21608
21709
  register(port, server, owner) {
21609
21710
  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
21711
  this.servers.set(port, { server, owner });
21611
21712
  this.onListen?.(port);
21612
21713
  }
21613
21714
  unregister(port, server) {
21614
- if (this.servers.get(port)?.server === server) this.servers.delete(port);
21715
+ if (this.servers.get(port)?.server !== server) return;
21716
+ this.servers.delete(port);
21717
+ this.onClose?.(port);
21615
21718
  }
21616
21719
  /** Whether anything in this container is listening on `port`. */
21617
21720
  activePortsIncludes(port) {
@@ -21620,6 +21723,9 @@ var VirtualHttpRouter = class {
21620
21723
  activePorts(owner) {
21621
21724
  return [...this.servers].filter(([, item]) => owner === void 0 || item.owner === owner).map(([port]) => port).sort((a, b) => a - b);
21622
21725
  }
21726
+ referencedPorts(owner) {
21727
+ return [...this.servers].filter(([, item]) => item.owner === owner && item.server.hasRef()).map(([port]) => port);
21728
+ }
21623
21729
  closeOwner(owner) {
21624
21730
  for (const { server, owner: value } of [...this.servers.values()]) if (value === owner) server.close();
21625
21731
  }
@@ -22031,6 +22137,108 @@ function hashFor(algorithm) {
22031
22137
  function toBytes2(data, encoding) {
22032
22138
  return typeof data === "string" ? Buffer2.from(data, encoding) : data;
22033
22139
  }
22140
+
22141
+ // src/runtime/sync-channel.ts
22142
+ var STATE = 0;
22143
+ var LENGTH = 1;
22144
+ var MORE = 2;
22145
+ var CONTROL_WORDS = 4;
22146
+ var STATE_REQUEST = 1;
22147
+ var STATE_RESPONSE = 2;
22148
+ var STATE_CONTINUE = 3;
22149
+ var STATE_CLOSED = 4;
22150
+ function createSyncChannelBuffers(capacityBytes = 1 << 20) {
22151
+ return {
22152
+ control: new SharedArrayBuffer(CONTROL_WORDS * Int32Array.BYTES_PER_ELEMENT),
22153
+ data: new SharedArrayBuffer(capacityBytes)
22154
+ };
22155
+ }
22156
+ function syncChannelUnavailableReason() {
22157
+ if (typeof globalThis.crossOriginIsolated === "boolean" && !globalThis.crossOriginIsolated) {
22158
+ 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.";
22159
+ }
22160
+ if (typeof SharedArrayBuffer !== "function") return "SharedArrayBuffer is unavailable in this host.";
22161
+ if (typeof Atomics !== "object" || typeof Atomics.wait !== "function") return "Atomics.wait is unavailable in this host.";
22162
+ return null;
22163
+ }
22164
+ function syncChannelSupported() {
22165
+ return syncChannelUnavailableReason() === null;
22166
+ }
22167
+ var SyncChannelServer = class {
22168
+ constructor(buffers, handle) {
22169
+ this.handle = handle;
22170
+ this.control = new Int32Array(buffers.control);
22171
+ this.data = new Uint8Array(buffers.data);
22172
+ this.capacity = this.data.length;
22173
+ }
22174
+ handle;
22175
+ control;
22176
+ data;
22177
+ capacity;
22178
+ /** Request chunks gathered so far, and the response still to be sent. */
22179
+ incoming = [];
22180
+ outgoing = null;
22181
+ sent = 0;
22182
+ closed = false;
22183
+ /** Call from the wake message the client sends after each chunk. */
22184
+ async pump() {
22185
+ if (this.closed) return;
22186
+ const state = Atomics.load(this.control, STATE);
22187
+ if (state === STATE_REQUEST) {
22188
+ const size = Atomics.load(this.control, LENGTH);
22189
+ this.incoming.push(this.data.slice(0, size));
22190
+ if (Atomics.load(this.control, MORE) === 1) {
22191
+ this.publish(STATE_CONTINUE);
22192
+ return;
22193
+ }
22194
+ const request = concat3(this.incoming);
22195
+ this.incoming = [];
22196
+ let response;
22197
+ try {
22198
+ response = await this.handle(request);
22199
+ } catch {
22200
+ response = new Uint8Array();
22201
+ }
22202
+ if (this.closed) return;
22203
+ this.outgoing = response;
22204
+ this.sent = 0;
22205
+ this.sendChunk();
22206
+ return;
22207
+ }
22208
+ if (state === STATE_CONTINUE) this.sendChunk();
22209
+ }
22210
+ /** Release a blocked client, e.g. when the container is torn down. */
22211
+ close() {
22212
+ if (this.closed) return;
22213
+ this.closed = true;
22214
+ Atomics.store(this.control, STATE, STATE_CLOSED);
22215
+ Atomics.notify(this.control, STATE);
22216
+ }
22217
+ sendChunk() {
22218
+ const payload = this.outgoing ?? new Uint8Array();
22219
+ const size = Math.min(this.capacity, payload.length - this.sent);
22220
+ this.data.set(payload.subarray(this.sent, this.sent + size), 0);
22221
+ this.sent += size;
22222
+ Atomics.store(this.control, LENGTH, size);
22223
+ Atomics.store(this.control, MORE, this.sent < payload.length ? 1 : 0);
22224
+ this.publish(STATE_RESPONSE);
22225
+ }
22226
+ publish(state) {
22227
+ Atomics.store(this.control, STATE, state);
22228
+ Atomics.notify(this.control, STATE);
22229
+ }
22230
+ };
22231
+ function concat3(parts) {
22232
+ if (parts.length === 1) return parts[0];
22233
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
22234
+ const joined = new Uint8Array(total);
22235
+ let at = 0;
22236
+ for (const part of parts) {
22237
+ joined.set(part, at);
22238
+ at += part.length;
22239
+ }
22240
+ return joined;
22241
+ }
22034
22242
  var ChildProcess = class extends EventEmitter4 {
22035
22243
  stdout = new streamModule4.PassThrough();
22036
22244
  stderr = new streamModule4.PassThrough();
@@ -22235,7 +22443,7 @@ function unavailable(name) {
22235
22443
  const list = Array.isArray(args[1]) ? args[1].map(String) : [];
22236
22444
  const command = file3 ? [file3, ...list].join(" ") : void 0;
22237
22445
  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.`
22446
+ `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
22447
  );
22240
22448
  error.code = "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM";
22241
22449
  throw error;
@@ -22523,6 +22731,11 @@ function createReadlineModule(defaultInput, defaultOutput = () => void 0) {
22523
22731
  edit(sequence, key) {
22524
22732
  if (this.closed) return;
22525
22733
  const name = key.name;
22734
+ if (key.ctrl && name === "c") {
22735
+ if (this.listenerCount("SIGINT") > 0) this.emit("SIGINT");
22736
+ else this.close();
22737
+ return;
22738
+ }
22526
22739
  if (name === "return" || name === "enter") {
22527
22740
  const value = this.line;
22528
22741
  this.line = "";
@@ -22618,7 +22831,7 @@ function createReadlineModule(defaultInput, defaultOutput = () => void 0) {
22618
22831
  };
22619
22832
  const createInterface = (options, output) => {
22620
22833
  if (options && typeof options === "object" && !options.on) {
22621
- const resolvedOutput2 = options.output ?? defaultOutput();
22834
+ const resolvedOutput2 = options.output;
22622
22835
  const instance = new Interface(
22623
22836
  options.input ?? defaultInput(),
22624
22837
  resolvedOutput2,
@@ -22627,7 +22840,7 @@ function createReadlineModule(defaultInput, defaultOutput = () => void 0) {
22627
22840
  if (typeof options.prompt === "string") instance.setPrompt(options.prompt);
22628
22841
  return instance;
22629
22842
  }
22630
- const resolvedOutput = output ?? defaultOutput();
22843
+ const resolvedOutput = output;
22631
22844
  return new Interface(options ?? defaultInput(), resolvedOutput, isTerminal(void 0, resolvedOutput));
22632
22845
  };
22633
22846
  const noop = () => {
@@ -22896,7 +23109,7 @@ function createCoreModules(options) {
22896
23109
  "stream/promises": createStreamPromises(),
22897
23110
  string_decoder: stringDecoderModule,
22898
23111
  timers: { ...timersModule, ...timers.api },
22899
- "timers/promises": createTimerPromises(),
23112
+ "timers/promises": createTimerPromises(timers.api),
22900
23113
  tty: { isatty: () => false, ReadStream: streamModule4.Readable, WriteStream: streamModule4.Writable },
22901
23114
  url: url_module_default,
22902
23115
  util: util_module_default,
@@ -23641,10 +23854,10 @@ function createTrackedTimers() {
23641
23854
  cancelAll
23642
23855
  };
23643
23856
  }
23644
- function createTimerPromises() {
23857
+ function createTimerPromises(timers) {
23645
23858
  return {
23646
- setTimeout: (delay, value) => new Promise((resolve2) => setTimeout(resolve2, delay, value)),
23647
- setImmediate: (value) => new Promise((resolve2) => setTimeout(resolve2, 0, value))
23859
+ setTimeout: (delay, value) => new Promise((resolve2) => timers.setTimeout(resolve2, delay, value)),
23860
+ setImmediate: (value) => new Promise((resolve2) => timers.setImmediate(resolve2, value))
23648
23861
  };
23649
23862
  }
23650
23863
  function createAsyncHooksModule() {
@@ -24352,11 +24565,8 @@ function ensureProcessGlobal() {
24352
24565
  // src/runtime/host-rolldown.ts
24353
24566
  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
24567
  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
- }
24568
+ const reason = syncChannelUnavailableReason();
24569
+ if (reason) throw new Error(`SandboxedJs threaded WASM unavailable: ${reason}`);
24360
24570
  try {
24361
24571
  const loaded = await import('@rolldown/binding-wasm32-wasi');
24362
24572
  return "__fs" in loaded ? { ...loaded } : loaded.default ?? loaded;
@@ -24364,9 +24574,9 @@ async function loadHostRolldownBinding() {
24364
24574
  if (typeof process !== "undefined" && process.env?.SANDBOXEDJS_DEBUG) {
24365
24575
  console.error("[sandboxedjs] Rolldown WASI binding unavailable:", error);
24366
24576
  }
24367
- const reason = error instanceof Error ? error.message : String(error);
24577
+ const reason2 = error instanceof Error ? error.message : String(error);
24368
24578
  throw new Error(
24369
- `The optional Rolldown WASI binding could not be loaded: ${reason}` + (typeof window === "undefined" ? "" : `
24579
+ `The optional Rolldown WASI binding could not be loaded: ${reason2}` + (typeof window === "undefined" ? "" : `
24370
24580
 
24371
24581
  ${BUNDLER_HINT}`),
24372
24582
  { cause: error }
@@ -24477,6 +24687,8 @@ var LocalProcess = class extends EventEmitter4 {
24477
24687
  }
24478
24688
  /** End the process now, for an asynchronous `process.exit`. */
24479
24689
  exitNow(code) {
24690
+ this.killed = true;
24691
+ this.cleanup?.();
24480
24692
  this.finish(code);
24481
24693
  this.resolveKilled();
24482
24694
  }
@@ -24660,6 +24872,7 @@ var LocalRuntimePod = class _LocalRuntimePod {
24660
24872
  )
24661
24873
  };
24662
24874
  disposed = false;
24875
+ running = /* @__PURE__ */ new Set();
24663
24876
  workdir;
24664
24877
  env;
24665
24878
  aliases;
@@ -24701,8 +24914,9 @@ var LocalRuntimePod = class _LocalRuntimePod {
24701
24914
  const cwd = typeof options.cwd === "string" ? options.cwd : this.workdir;
24702
24915
  const env2 = { ...this.env, ...isRecord(options.env) ? options.env : {} };
24703
24916
  const owner = `${this.instanceId}:${Math.random().toString(36).slice(2)}`;
24704
- return new LocalProcess(async (proc) => {
24917
+ const process2 = new LocalProcess(async (proc) => {
24705
24918
  await this.prepareRolldown(cwd, env2);
24919
+ if (proc.isKilled()) return 137;
24706
24920
  const untrack = trackProcess(proc);
24707
24921
  let requestedExit = 0;
24708
24922
  let engine;
@@ -24730,7 +24944,9 @@ var LocalRuntimePod = class _LocalRuntimePod {
24730
24944
  onRawMode: (enabled) => proc.emit("rawmode", enabled)
24731
24945
  });
24732
24946
  proc.acceptInput((data) => core.writeStdin(data), () => core.endStdin());
24947
+ const hostWork = new HostModuleTracker();
24733
24948
  proc.onKill(() => {
24949
+ hostWork.dispose();
24734
24950
  core.cancelTimers();
24735
24951
  this.router.closeOwner(owner);
24736
24952
  });
@@ -24739,21 +24955,22 @@ var LocalRuntimePod = class _LocalRuntimePod {
24739
24955
  builtins: core.builtins,
24740
24956
  globals: core.globals,
24741
24957
  aliases: this.aliases,
24742
- overrides: this.modules
24958
+ overrides: Object.fromEntries(Object.entries(this.modules).map(([key, value]) => [key, hostWork.wrap(value)]))
24743
24959
  });
24744
24960
  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
- }
24961
+ await Promise.race([engine.run(script), proc.waitForKill()]);
24962
+ await this.settle(owner, core.pendingHandles, core.readingStdin, () => core.pendingRequests() + hostWork.pending(), () => proc.isKilled());
24751
24963
  return requestedExit;
24752
24964
  } finally {
24965
+ hostWork.dispose();
24966
+ core.cancelTimers();
24753
24967
  untrack();
24754
24968
  this.router.closeOwner(owner);
24755
24969
  }
24756
24970
  });
24971
+ this.running.add(process2);
24972
+ void process2.completion.then(() => this.running.delete(process2));
24973
+ return process2;
24757
24974
  }
24758
24975
  /**
24759
24976
  * Rolldown's JavaScript API synchronously requires its compiled binding.
@@ -24789,19 +25006,13 @@ var LocalRuntimePod = class _LocalRuntimePod {
24789
25006
  * A plain script that has genuinely finished falls straight through both,
24790
25007
  * costing a handful of empty turns.
24791
25008
  */
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));
25009
+ async settle(owner, pendingHandles, readingStdin, pendingRequests, killed) {
25010
+ let idleTurns = 0;
25011
+ while (!killed()) {
25012
+ const busy = this.router.referencedPorts(owner).length > 0 || pendingHandles() > 0 || pendingRequests() > 0 || readingStdin();
25013
+ idleTurns = busy ? 0 : idleTurns + 1;
25014
+ if (idleTurns >= DRAIN_TURNS) return;
25015
+ await new Promise((resolve2) => setTimeout(resolve2, busy ? 5 : 0));
24805
25016
  }
24806
25017
  }
24807
25018
  async request(_port, _init = {}) {
@@ -24818,6 +25029,8 @@ var LocalRuntimePod = class _LocalRuntimePod {
24818
25029
  }
24819
25030
  teardown() {
24820
25031
  this.disposed = true;
25032
+ for (const process2 of this.running) process2.kill();
25033
+ this.running.clear();
24821
25034
  this.router.closeAll();
24822
25035
  this.esbuild?.stop();
24823
25036
  }
@@ -24887,103 +25100,6 @@ function attachRolldownMirror(binding, volume, root) {
24887
25100
  volume.attach(fs, root);
24888
25101
  }
24889
25102
 
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
25103
  // src/runtime/remote-volume.ts
24988
25104
  var encoder7 = new TextEncoder();
24989
25105
  var decoder7 = new TextDecoder();
@@ -25078,24 +25194,32 @@ function defaultWorkerUrl() {
25078
25194
  return new URL("./worker-entry.js", import.meta.url);
25079
25195
  }
25080
25196
  function hasDomWorker() {
25081
- return typeof Worker === "function" && typeof document !== "undefined";
25197
+ return typeof Worker === "function";
25082
25198
  }
25083
25199
  async function startRuntimeWorker(options = {}) {
25084
25200
  const url = options.url ?? defaultWorkerUrl();
25085
25201
  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") {
25202
+ try {
25203
+ await new Promise((resolve2, reject) => {
25204
+ const timer = setTimeout(() => reject(new Error("the runtime worker did not start in time")), options.timeoutMs ?? 1e4);
25205
+ worker.onMessage((message) => {
25206
+ if (message?.type === "sandboxedjs:ready") {
25207
+ clearTimeout(timer);
25208
+ resolve2();
25209
+ }
25210
+ });
25211
+ worker.onError((error) => {
25090
25212
  clearTimeout(timer);
25091
- resolve2();
25092
- }
25093
- });
25094
- worker.onError((error) => {
25095
- clearTimeout(timer);
25096
- reject(error instanceof Error ? error : new Error(String(error)));
25213
+ reject(error instanceof Error ? error : new Error(String(error)));
25214
+ });
25097
25215
  });
25098
- });
25216
+ } catch (error) {
25217
+ try {
25218
+ await worker.terminate();
25219
+ } catch {
25220
+ }
25221
+ throw error;
25222
+ }
25099
25223
  return worker;
25100
25224
  }
25101
25225
  async function startDomWorker(url) {
@@ -25156,6 +25280,8 @@ var WorkerProcess = class extends EventEmitter4 {
25156
25280
  started = false;
25157
25281
  pendingInput = [];
25158
25282
  inputEnded = false;
25283
+ inheritedInput;
25284
+ ownRawMode = false;
25159
25285
  on(event, listener) {
25160
25286
  return super.on(event, listener);
25161
25287
  }
@@ -25181,13 +25307,38 @@ var WorkerProcess = class extends EventEmitter4 {
25181
25307
  * program is still loading, and dropping them loses the answer to a prompt.
25182
25308
  */
25183
25309
  write(data) {
25310
+ if (this.inheritedInput) {
25311
+ this.inheritedInput.sendStdin(data);
25312
+ return;
25313
+ }
25184
25314
  if (this.started) this.worker.postMessage({ type: "stdin", data });
25185
25315
  else this.pendingInput.push(data);
25186
25316
  }
25187
25317
  endInput() {
25188
25318
  this.inputEnded = true;
25319
+ if (this.inheritedInput) {
25320
+ this.inheritedInput.endStdin?.();
25321
+ return;
25322
+ }
25189
25323
  if (this.started) this.worker.postMessage({ type: "stdin-end" });
25190
25324
  }
25325
+ rawMode(enabled) {
25326
+ this.ownRawMode = enabled;
25327
+ if (!this.inheritedInput) this.emit("rawmode", enabled);
25328
+ }
25329
+ /** The parent is blocked in Atomics.wait, so inherited input must bypass it. */
25330
+ inheritInput(child) {
25331
+ this.inheritedInput = child;
25332
+ this.emit("rawmode", false);
25333
+ child.on("rawmode", (enabled) => {
25334
+ if (this.inheritedInput === child) this.emit("rawmode", Boolean(enabled));
25335
+ });
25336
+ return () => {
25337
+ if (this.inheritedInput !== child) return;
25338
+ this.inheritedInput = void 0;
25339
+ this.emit("rawmode", this.ownRawMode);
25340
+ };
25341
+ }
25191
25342
  kill(_signal = "SIGTERM") {
25192
25343
  this.worker.postMessage({ type: "kill" });
25193
25344
  this.finish(137);
@@ -25208,25 +25359,30 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25208
25359
  super(options);
25209
25360
  this.workerUrl = options.workerUrl;
25210
25361
  }
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;
25362
+ /** Boot without silently dropping synchronous child-process support. */
25363
+ static async boot(options = {}) {
25364
+ const reason = syncChannelUnavailableReason();
25365
+ if (reason) throw new Error(`SandboxedJs worker runtime unavailable: ${reason}`);
25366
+ if (options.modules && Object.keys(options.modules).length > 0) {
25367
+ throw new Error("SandboxedJs worker runtime cannot clone host-supplied modules. Use isolation: 'realm' explicitly.");
25368
+ }
25223
25369
  const pod = new _WorkerRuntimePod(options);
25224
25370
  try {
25225
25371
  const probe = await startRuntimeWorker({ ...options.workerUrl ? { url: options.workerUrl } : {}, timeoutMs: 1e4 });
25226
25372
  await probe.terminate();
25227
25373
  return pod;
25228
- } catch {
25374
+ } catch (cause) {
25229
25375
  pod.teardown();
25376
+ const detail = cause instanceof Error ? cause.message : String(cause);
25377
+ 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 });
25378
+ }
25379
+ }
25380
+ /** Compatibility mode, with an observable explanation for every fallback. */
25381
+ static async tryBoot(options = {}, onFallback = (error) => console.warn(`[sandboxedjs] Falling back to the realm runtime; synchronous child processes are unavailable. ${error.message}`)) {
25382
+ try {
25383
+ return await _WorkerRuntimePod.boot(options);
25384
+ } catch (error) {
25385
+ onFallback(error instanceof Error ? error : new Error(String(error)));
25230
25386
  return null;
25231
25387
  }
25232
25388
  }
@@ -25249,11 +25405,14 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25249
25405
  const streams = { target: null };
25250
25406
  const server = new SyncChannelServer(buffers, serveSyncSyscalls({
25251
25407
  volume: this.volume,
25252
- spawnChild: (request) => this.runChildToCompletion(request, streams.target)
25408
+ spawnChild: (request) => this.runChildToCompletion(request, streams.target, ownedChildren)
25253
25409
  }));
25254
25410
  const entry = { worker, server };
25255
25411
  this.live.add(entry);
25412
+ const ownedChildren = /* @__PURE__ */ new Set();
25256
25413
  const process2 = new WorkerProcess(worker, () => {
25414
+ for (const child of ownedChildren) child.kill("SIGTERM");
25415
+ ownedChildren.clear();
25257
25416
  this.live.delete(entry);
25258
25417
  server.close();
25259
25418
  this.closeProxies(owner);
@@ -25274,7 +25433,7 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25274
25433
  process2.error(String(message.text));
25275
25434
  return;
25276
25435
  case "rawmode":
25277
- process2.emit("rawmode", Boolean(message.enabled));
25436
+ process2.rawMode(Boolean(message.enabled));
25278
25437
  return;
25279
25438
  case "exit":
25280
25439
  process2.finish(Number(message.code));
@@ -25282,11 +25441,20 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25282
25441
  case "listen":
25283
25442
  this.proxyPort(Number(message.port), owner, worker);
25284
25443
  return;
25444
+ case "close-port": {
25445
+ const port = Number(message.port);
25446
+ const proxy = this.proxies.get(port);
25447
+ if (proxy?.owner === owner) {
25448
+ proxy.close();
25449
+ this.proxies.delete(port);
25450
+ }
25451
+ return;
25452
+ }
25285
25453
  case "http-response":
25286
25454
  this.settleProxied(Number(message.id), message.response);
25287
25455
  return;
25288
25456
  case "child-start":
25289
- this.startChild(worker, children, message);
25457
+ this.startChild(worker, children, message, ownedChildren);
25290
25458
  return;
25291
25459
  case "child-stdin":
25292
25460
  children.get(message.id)?.sendStdin?.(String(message.data));
@@ -25317,16 +25485,21 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25317
25485
  return process2;
25318
25486
  }
25319
25487
  /** Start an asynchronous child on the host's behalf and relay its events. */
25320
- startChild(worker, children, message) {
25488
+ startChild(worker, children, message, owned) {
25321
25489
  const handle = this.processManager.spawn(message.config);
25322
25490
  children.set(message.id, handle);
25491
+ owned.add(handle);
25492
+ handle.on("exit", () => {
25493
+ owned.delete(handle);
25494
+ children.delete(message.id);
25495
+ });
25323
25496
  for (const event of ["stdout", "stderr", "exit"]) {
25324
25497
  handle.on(event, (value) => worker.postMessage({ type: "child-event", id: message.id, event, value }));
25325
25498
  }
25326
25499
  handle.exec();
25327
25500
  }
25328
25501
  /** Run a child to completion and collect it, for the guest's `spawnSync`. */
25329
- runChildToCompletion(request, streamTo) {
25502
+ runChildToCompletion(request, streamTo, owned) {
25330
25503
  return new Promise((resolve2) => {
25331
25504
  let handle;
25332
25505
  try {
@@ -25342,9 +25515,13 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25342
25515
  resolve2({ status: null, stdout: "", stderr: "", signal: null, error: { ...failure.code ? { code: failure.code } : {}, message: failure.message } });
25343
25516
  return;
25344
25517
  }
25518
+ owned.add(handle);
25519
+ handle.on("exit", () => owned.delete(handle));
25345
25520
  let stdout = "";
25346
25521
  let stderr = "";
25347
25522
  const live = request.inheritStdio ? streamTo : null;
25523
+ const restoreInput = live?.inheritInput(handle);
25524
+ if (restoreInput) handle.on("exit", restoreInput);
25348
25525
  handle.on("stdout", (text2) => {
25349
25526
  stdout += text2;
25350
25527
  live?.output(text2);
@@ -25498,7 +25675,7 @@ var Container = class _Container {
25498
25675
  ...opts.onServerReady ? { onServerReady: opts.onServerReady } : {}
25499
25676
  };
25500
25677
  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);
25678
+ 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
25679
  const kernel = new Kernel({
25503
25680
  pod,
25504
25681
  hostname: opts.hostname ?? "sandbox",
@@ -26006,7 +26183,7 @@ var Terminal = class {
26006
26183
  if (this.running) {
26007
26184
  const raw = this.currentStdin?.rawMode === true;
26008
26185
  if (ch === CTRL_C && !raw) {
26009
- this.session.proc.deliver("SIGINT");
26186
+ if (this.currentStdin) this.session.shell.interruptForeground("SIGINT", this.currentStdin);
26010
26187
  this.write("^C\r\n");
26011
26188
  return;
26012
26189
  }
@@ -26396,7 +26573,7 @@ function serveContainerOn(port, box) {
26396
26573
  }
26397
26574
  async function createPreview(box, options = {}) {
26398
26575
  if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) return null;
26399
- const scriptUrl = options.scriptUrl ?? new URL("./service-worker.js", import.meta.url);
26576
+ const scriptUrl = options.scriptUrl ?? new URL("./service-worker.js?no-inline", import.meta.url);
26400
26577
  let registration;
26401
26578
  try {
26402
26579
  registration = await navigator.serviceWorker.register(scriptUrl, {