sandboxedjs 0.2.3 → 0.2.5

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
@@ -8374,6 +8374,60 @@ function createSysProvider(kernel) {
8374
8374
  };
8375
8375
  }
8376
8376
 
8377
+ // src/net/proxy-fetch.ts
8378
+ function toBase64(bytes2) {
8379
+ let text2 = "";
8380
+ for (let at = 0; at < bytes2.length; at += 32768) {
8381
+ text2 += String.fromCharCode(...bytes2.subarray(at, at + 32768));
8382
+ }
8383
+ return btoa(text2);
8384
+ }
8385
+ function fromBase64(text2) {
8386
+ const binary = atob(text2);
8387
+ const bytes2 = new Uint8Array(binary.length);
8388
+ for (let at = 0; at < binary.length; at += 1) bytes2[at] = binary.charCodeAt(at);
8389
+ return bytes2;
8390
+ }
8391
+ async function proxyExchange(proxy, request, fetchImpl = fetch) {
8392
+ let answer;
8393
+ try {
8394
+ answer = await fetchImpl(proxy, {
8395
+ method: "POST",
8396
+ headers: { "content-type": "application/json" },
8397
+ body: JSON.stringify({
8398
+ url: request.url,
8399
+ method: request.method,
8400
+ headers: request.headers,
8401
+ ...request.body?.length ? { body: toBase64(request.body) } : {}
8402
+ })
8403
+ });
8404
+ } catch (error) {
8405
+ throw new Error(
8406
+ `the egress proxy at ${proxy} could not be reached (${error instanceof Error ? error.message : String(error)}). It performs this container's outbound requests, so nothing leaves until it answers.`
8407
+ );
8408
+ }
8409
+ if (!answer.ok) {
8410
+ throw new Error(
8411
+ `the egress proxy at ${proxy} refused ${request.method} ${request.url} with ${answer.status} ${answer.statusText}: ${(await answer.text()).trim().slice(0, 300)}`
8412
+ );
8413
+ }
8414
+ const payload = await answer.json();
8415
+ return {
8416
+ status: payload.status ?? 200,
8417
+ statusText: payload.statusText ?? "",
8418
+ headers: payload.headers ?? {},
8419
+ body: payload.body ? fromBase64(payload.body) : new Uint8Array(0)
8420
+ };
8421
+ }
8422
+ function isBrowser() {
8423
+ return typeof process === "undefined" || process.versions?.node == null;
8424
+ }
8425
+ function browserBlocked(hostname, message) {
8426
+ return new Error(
8427
+ `${message} \u2014 the request to ${hostname} was blocked by the browser. A page can only read a response from a host that sends CORS headers, and most APIs send none. Give the container a proxy that makes the request for it \u2014 network: { proxy }, and \`npx sandboxedjs-egress\` is one \u2014 or run the container on a server.`
8428
+ );
8429
+ }
8430
+
8377
8431
  // src/net/policy.ts
8378
8432
  function isLoopbackHostname(hostname) {
8379
8433
  const host2 = hostname.replace(/^\[|\]$/g, "").toLowerCase();
@@ -8416,7 +8470,40 @@ function policedFetch(policy, fetchImpl) {
8416
8470
  if (!outboundAllowed(policy, url)) {
8417
8471
  throw Object.assign(new TypeError("fetch failed"), { cause: outboundBlockedError(url) });
8418
8472
  }
8419
- return fetchImpl(input, init);
8473
+ if (policy.proxy) return await fetchThroughProxy(policy.proxy, fetchImpl, input, init);
8474
+ try {
8475
+ return await fetchImpl(input, init);
8476
+ } catch (error) {
8477
+ if (!isBrowser()) throw error;
8478
+ throw Object.assign(new TypeError("fetch failed"), {
8479
+ cause: browserBlocked(new URL(url).hostname, error instanceof Error ? error.message : String(error))
8480
+ });
8481
+ }
8482
+ });
8483
+ }
8484
+ async function fetchThroughProxy(proxy, fetchImpl, input, init) {
8485
+ const request = new Request(input, init);
8486
+ const headers = {};
8487
+ request.headers.forEach((value, name) => {
8488
+ headers[name] = value;
8489
+ });
8490
+ const bodyless = request.method === "GET" || request.method === "HEAD";
8491
+ const result = await proxyExchange(
8492
+ proxy,
8493
+ {
8494
+ url: request.url,
8495
+ method: request.method,
8496
+ headers,
8497
+ ...bodyless ? {} : { body: new Uint8Array(await request.arrayBuffer()) }
8498
+ },
8499
+ fetchImpl
8500
+ );
8501
+ const nullBody = result.status === 204 || result.status === 205 || result.status === 304 || request.method === "HEAD";
8502
+ const body = nullBody ? null : result.body.slice().buffer;
8503
+ return new Response(body, {
8504
+ status: result.status,
8505
+ statusText: result.statusText,
8506
+ headers: result.headers
8420
8507
  });
8421
8508
  }
8422
8509
  function policedWebSocket(Native, policy) {
@@ -17474,9 +17561,6 @@ function parseUrl(raw) {
17474
17561
  return null;
17475
17562
  }
17476
17563
  }
17477
- function isBrowser() {
17478
- return typeof process === "undefined" || process.versions?.node == null;
17479
- }
17480
17564
  async function performRequest(ctx, url, init) {
17481
17565
  const isLocal = ctx.kernel.net.isLocal(url.hostname);
17482
17566
  if (isLocal) {
@@ -17504,7 +17588,16 @@ async function performRequest(ctx, url, init) {
17504
17588
  );
17505
17589
  }
17506
17590
  const proxy = ctx.kernel.net.proxy;
17507
- if (proxy) return await proxyRequest(ctx, proxy, url, init);
17591
+ if (proxy) {
17592
+ const result = await proxyExchange(proxy, {
17593
+ url: url.toString(),
17594
+ method: init.method,
17595
+ headers: init.headers,
17596
+ ...init.body?.length ? { body: init.body } : {}
17597
+ });
17598
+ ctx.kernel.net.countRx(result.body.length);
17599
+ return { ...result, url: url.toString() };
17600
+ }
17508
17601
  let response;
17509
17602
  try {
17510
17603
  response = await fetch(url.toString(), {
@@ -17513,9 +17606,7 @@ async function performRequest(ctx, url, init) {
17513
17606
  ...init.body ? { body: init.body.slice().buffer } : {}
17514
17607
  });
17515
17608
  } catch (error) {
17516
- throw isBrowser() ? new Error(
17517
- `${error instanceof Error ? error.message : String(error)} \u2014 the request to ${url.hostname} was blocked by the browser. A page can only read a response from a host that sends CORS headers, and most APIs send none. Give the container a proxy that makes the request for it \u2014 network: { proxy }, and \`npx sandboxedjs-egress\` is one \u2014 or run the container on a server.`
17518
- ) : error;
17609
+ throw isBrowser() ? browserBlocked(url.hostname, error instanceof Error ? error.message : String(error)) : error;
17519
17610
  }
17520
17611
  const body = new Uint8Array(await response.arrayBuffer());
17521
17612
  ctx.kernel.net.countRx(body.length);
@@ -17525,53 +17616,6 @@ async function performRequest(ctx, url, init) {
17525
17616
  });
17526
17617
  return { status: response.status, statusText: response.statusText, headers, body, url: response.url || url.toString() };
17527
17618
  }
17528
- async function proxyRequest(ctx, proxy, url, init) {
17529
- let answer;
17530
- try {
17531
- answer = await fetch(proxy, {
17532
- method: "POST",
17533
- headers: { "content-type": "application/json" },
17534
- body: JSON.stringify({
17535
- url: url.toString(),
17536
- method: init.method,
17537
- headers: init.headers,
17538
- ...init.body?.length ? { body: toBase64(init.body) } : {}
17539
- })
17540
- });
17541
- } catch (error) {
17542
- throw new Error(
17543
- `the egress proxy at ${proxy} could not be reached (${error instanceof Error ? error.message : String(error)}). It performs this container's outbound requests, so nothing leaves until it answers.`
17544
- );
17545
- }
17546
- if (!answer.ok) {
17547
- throw new Error(
17548
- `the egress proxy at ${proxy} refused ${init.method} ${url.toString()} with ${answer.status} ${answer.statusText}: ${(await answer.text()).trim().slice(0, 300)}`
17549
- );
17550
- }
17551
- const payload = await answer.json();
17552
- const body = payload.body ? fromBase64(payload.body) : new Uint8Array(0);
17553
- ctx.kernel.net.countRx(body.length);
17554
- return {
17555
- status: payload.status ?? 200,
17556
- statusText: payload.statusText ?? "",
17557
- headers: payload.headers ?? {},
17558
- body,
17559
- url: url.toString()
17560
- };
17561
- }
17562
- function toBase64(bytes2) {
17563
- let text2 = "";
17564
- for (let at = 0; at < bytes2.length; at += 32768) {
17565
- text2 += String.fromCharCode(...bytes2.subarray(at, at + 32768));
17566
- }
17567
- return btoa(text2);
17568
- }
17569
- function fromBase64(text2) {
17570
- const binary = atob(text2);
17571
- const bytes2 = new Uint8Array(binary.length);
17572
- for (let at = 0; at < binary.length; at += 1) bytes2[at] = binary.charCodeAt(at);
17573
- return bytes2;
17574
- }
17575
17619
  var curl = defineCommand({
17576
17620
  name: "curl",
17577
17621
  path: "/usr/bin/curl",
@@ -21013,7 +21057,7 @@ var wheels_default = {
21013
21057
 
21014
21058
  // package.json
21015
21059
  var package_default = {
21016
- version: "0.2.3"};
21060
+ version: "0.2.5"};
21017
21061
 
21018
21062
  // src/python/extension-abi.ts
21019
21063
  var EXTENSION_ABI = {
@@ -22328,8 +22372,24 @@ var VirtualTcpNetwork = class {
22328
22372
  hasListener(port) {
22329
22373
  return this.listeners.has(port);
22330
22374
  }
22375
+ /** Ports the container itself listens on, which no guest asked for. */
22376
+ internal = /* @__PURE__ */ new Set();
22377
+ /**
22378
+ * Ports a guest is serving on.
22379
+ *
22380
+ * The container's own plumbing listens too — the HTTP egress takes a port
22381
+ * like any server — and those are not the guest's. Reported, they reach
22382
+ * every consumer of "what is running here": a port picker offers one, a
22383
+ * preview opens it, and the reader is looking at an internal endpoint
22384
+ * answering that it wanted an absolute URL, with nothing to say what it is
22385
+ * or why they are there.
22386
+ */
22331
22387
  ports() {
22332
- return [...this.listeners.keys()].sort((a, b) => a - b);
22388
+ return [...this.listeners.keys()].filter((port) => !this.internal.has(port)).sort((a, b) => a - b);
22389
+ }
22390
+ /** Keep `port` out of {@link ports}: it belongs to the container, not a guest. */
22391
+ markInternal(port) {
22392
+ this.internal.add(port);
22333
22393
  }
22334
22394
  closeAll() {
22335
22395
  for (const listener of [...this.listeners.values()]) this.close(listener);
@@ -23266,6 +23326,7 @@ function ensureHttpEgress(ctx) {
23266
23326
  }
23267
23327
  }
23268
23328
  if (!listener || port === 0) return 0;
23329
+ sockets.markInternal?.(port);
23269
23330
  listeners.set(sockets, port);
23270
23331
  const accepting = listener;
23271
23332
  void (async () => {
@@ -37808,7 +37869,8 @@ var Container = class _Container {
37808
37869
  if (opts.python) configurePython(opts.python);
37809
37870
  const network = {
37810
37871
  allowOutbound: opts.network?.allowOutbound ?? false,
37811
- allowedHosts: opts.network?.allowedHosts ?? null
37872
+ allowedHosts: opts.network?.allowedHosts ?? null,
37873
+ ...opts.network?.proxy ? { proxy: opts.network.proxy } : {}
37812
37874
  };
37813
37875
  const podOptions = {
37814
37876
  workdir: opts.cwd ?? "/",
package/dist/index.d.cts CHANGED
@@ -1,8 +1,8 @@
1
- import { S as Shell, a as ShellIO, b as Session, N as Node, c as Command, K as Kernel, E as ExecContext, O as OutputStream, C as Container, B as BinaryBackend, d as createContainer } from './container-C9Y8dqos.cjs';
2
- export { e as BinaryExecutionRegistry, f as BinaryInfo, g as BinaryRequest, h as BufferSink, i as CallbackSink, j as CommandRegistry, k as ContainerFs, l as ContainerOptions, m as ContextInit, n as Env, o as ExecOptions, p as ExecResult, q as ExecutionDecision, r as ExecutionTier, F as FileData, s as FileInput, t as FileOutput, G as GroupEntry, H as HttpResponse, I as InputStream, J as Job, u as KernelOptions, L as ListeningPort, M as MANIFEST_FORMAT, v as MANIFEST_SCHEMA_VERSION, w as MountEntry, x as NetInterface, y as NetworkOptions, z as NetworkStack, A as NullInput, D as NullOutput, P as PYTHON_VERSION, Q as PasswdEntry, R as Pipe, T as Preparation, U as PreparedBinary, V as Process, W as ProcessKind, X as ProcessOptions, Y as ProcessState, Z as ProcessTable, _ as PythonCapabilities, $ as PythonOptions, a0 as PythonProfile, a1 as PythonRuntimeManifest, a2 as ResolvedExecutable, a3 as RunOptions, a4 as RunResult, a5 as SessionInit, a6 as SessionResult, a7 as SessionRunOptions, a8 as ShellExit, a9 as ShellInit, aa as ShellOptions, ab as SpawnHandle, ac as Stdio, ad as TeeOutput, ae as UserDatabase, af as Variables, ag as WasmCommandArtifact, ah as WasmTranslator, ai as binaryDigest, aj as braceExpand, ak as captureStdio, al as configurePython, am as createContext, an as createTranslationBackend, ao as createWasmCompatibilityBackend, ap as defineCommand, aq as expandWord, ar as expandWords, as as inspectElf, at as installWasmCommands, au as isElfBinary, av as isPythonAvailable, aw as resetPidCounter, ax as shellQuote, ay as validateManifest } from './container-C9Y8dqos.cjs';
3
- import { V as Vfs, C as Cred, R as RuntimeVolume, a as VirtualTcpNetwork, b as RuntimeHttpResponse, O as OutboundPolicy, S as SpawnChild, c as SyncSpawn, I as IpcTransport, d as VolumeStat, e as VolumeStats, f as RuntimePod, g as RuntimePackageInstaller, h as ChildSpawnConfig, i as ChildHandle, j as RuntimeProcess, k as RuntimeSocketPeer, l as RuntimeConnection } from './contracts-B6SHFjma.cjs';
4
- export { D as DirEntry, m as ROOT_CRED, n as RuntimeProcessManager, o as RuntimeProcessResult, p as Stats, q as VirtualNode, r as VirtualProvider, W as WriteOptions, s as applyChmod, t as createChildProcessModule, u as formatMode, v as makeCred, w as octalMode, x as parseUmask } from './contracts-B6SHFjma.cjs';
5
- import { M as MemoryVolume, a as MemoryVolumeSnapshotEntry } from './memory-volume-CRhXUyCN.cjs';
1
+ import { S as Shell, a as ShellIO, b as Session, N as Node, c as Command, K as Kernel, E as ExecContext, O as OutputStream, C as Container, B as BinaryBackend, d as createContainer } from './container-BQx27_d-.cjs';
2
+ export { e as BinaryExecutionRegistry, f as BinaryInfo, g as BinaryRequest, h as BufferSink, i as CallbackSink, j as CommandRegistry, k as ContainerFs, l as ContainerOptions, m as ContextInit, n as Env, o as ExecOptions, p as ExecResult, q as ExecutionDecision, r as ExecutionTier, F as FileData, s as FileInput, t as FileOutput, G as GroupEntry, H as HttpResponse, I as InputStream, J as Job, u as KernelOptions, L as ListeningPort, M as MANIFEST_FORMAT, v as MANIFEST_SCHEMA_VERSION, w as MountEntry, x as NetInterface, y as NetworkOptions, z as NetworkStack, A as NullInput, D as NullOutput, P as PYTHON_VERSION, Q as PasswdEntry, R as Pipe, T as Preparation, U as PreparedBinary, V as Process, W as ProcessKind, X as ProcessOptions, Y as ProcessState, Z as ProcessTable, _ as PythonCapabilities, $ as PythonOptions, a0 as PythonProfile, a1 as PythonRuntimeManifest, a2 as ResolvedExecutable, a3 as RunOptions, a4 as RunResult, a5 as SessionInit, a6 as SessionResult, a7 as SessionRunOptions, a8 as ShellExit, a9 as ShellInit, aa as ShellOptions, ab as SpawnHandle, ac as Stdio, ad as TeeOutput, ae as UserDatabase, af as Variables, ag as WasmCommandArtifact, ah as WasmTranslator, ai as binaryDigest, aj as braceExpand, ak as captureStdio, al as configurePython, am as createContext, an as createTranslationBackend, ao as createWasmCompatibilityBackend, ap as defineCommand, aq as expandWord, ar as expandWords, as as inspectElf, at as installWasmCommands, au as isElfBinary, av as isPythonAvailable, aw as resetPidCounter, ax as shellQuote, ay as validateManifest } from './container-BQx27_d-.cjs';
3
+ import { V as Vfs, C as Cred, R as RuntimeVolume, a as VirtualTcpNetwork, b as RuntimeHttpResponse, O as OutboundPolicy, S as SpawnChild, c as SyncSpawn, I as IpcTransport, d as VolumeStat, e as VolumeStats, f as RuntimePod, g as RuntimePackageInstaller, h as ChildSpawnConfig, i as ChildHandle, j as RuntimeProcess, k as RuntimeSocketPeer, l as RuntimeConnection } from './contracts-BHo4LdY2.cjs';
4
+ export { D as DirEntry, m as ROOT_CRED, n as RuntimeProcessManager, o as RuntimeProcessResult, p as Stats, q as VirtualNode, r as VirtualProvider, W as WriteOptions, s as applyChmod, t as createChildProcessModule, u as formatMode, v as makeCred, w as octalMode, x as parseUmask } from './contracts-BHo4LdY2.cjs';
5
+ import { M as MemoryVolume, a as MemoryVolumeSnapshotEntry } from './memory-volume-e-uJFRnG.cjs';
6
6
  import EventEmitter from 'events/events.js';
7
7
  import streamModule from 'stream-browserify';
8
8
 
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { S as Shell, a as ShellIO, b as Session, N as Node, c as Command, K as Kernel, E as ExecContext, O as OutputStream, C as Container, B as BinaryBackend, d as createContainer } from './container-CIE93_Gh.js';
2
- export { e as BinaryExecutionRegistry, f as BinaryInfo, g as BinaryRequest, h as BufferSink, i as CallbackSink, j as CommandRegistry, k as ContainerFs, l as ContainerOptions, m as ContextInit, n as Env, o as ExecOptions, p as ExecResult, q as ExecutionDecision, r as ExecutionTier, F as FileData, s as FileInput, t as FileOutput, G as GroupEntry, H as HttpResponse, I as InputStream, J as Job, u as KernelOptions, L as ListeningPort, M as MANIFEST_FORMAT, v as MANIFEST_SCHEMA_VERSION, w as MountEntry, x as NetInterface, y as NetworkOptions, z as NetworkStack, A as NullInput, D as NullOutput, P as PYTHON_VERSION, Q as PasswdEntry, R as Pipe, T as Preparation, U as PreparedBinary, V as Process, W as ProcessKind, X as ProcessOptions, Y as ProcessState, Z as ProcessTable, _ as PythonCapabilities, $ as PythonOptions, a0 as PythonProfile, a1 as PythonRuntimeManifest, a2 as ResolvedExecutable, a3 as RunOptions, a4 as RunResult, a5 as SessionInit, a6 as SessionResult, a7 as SessionRunOptions, a8 as ShellExit, a9 as ShellInit, aa as ShellOptions, ab as SpawnHandle, ac as Stdio, ad as TeeOutput, ae as UserDatabase, af as Variables, ag as WasmCommandArtifact, ah as WasmTranslator, ai as binaryDigest, aj as braceExpand, ak as captureStdio, al as configurePython, am as createContext, an as createTranslationBackend, ao as createWasmCompatibilityBackend, ap as defineCommand, aq as expandWord, ar as expandWords, as as inspectElf, at as installWasmCommands, au as isElfBinary, av as isPythonAvailable, aw as resetPidCounter, ax as shellQuote, ay as validateManifest } from './container-CIE93_Gh.js';
3
- import { V as Vfs, C as Cred, R as RuntimeVolume, a as VirtualTcpNetwork, b as RuntimeHttpResponse, O as OutboundPolicy, S as SpawnChild, c as SyncSpawn, I as IpcTransport, d as VolumeStat, e as VolumeStats, f as RuntimePod, g as RuntimePackageInstaller, h as ChildSpawnConfig, i as ChildHandle, j as RuntimeProcess, k as RuntimeSocketPeer, l as RuntimeConnection } from './contracts-B6SHFjma.js';
4
- export { D as DirEntry, m as ROOT_CRED, n as RuntimeProcessManager, o as RuntimeProcessResult, p as Stats, q as VirtualNode, r as VirtualProvider, W as WriteOptions, s as applyChmod, t as createChildProcessModule, u as formatMode, v as makeCred, w as octalMode, x as parseUmask } from './contracts-B6SHFjma.js';
5
- import { M as MemoryVolume, a as MemoryVolumeSnapshotEntry } from './memory-volume-CwiXvRfs.js';
1
+ import { S as Shell, a as ShellIO, b as Session, N as Node, c as Command, K as Kernel, E as ExecContext, O as OutputStream, C as Container, B as BinaryBackend, d as createContainer } from './container-D-e5SMXQ.js';
2
+ export { e as BinaryExecutionRegistry, f as BinaryInfo, g as BinaryRequest, h as BufferSink, i as CallbackSink, j as CommandRegistry, k as ContainerFs, l as ContainerOptions, m as ContextInit, n as Env, o as ExecOptions, p as ExecResult, q as ExecutionDecision, r as ExecutionTier, F as FileData, s as FileInput, t as FileOutput, G as GroupEntry, H as HttpResponse, I as InputStream, J as Job, u as KernelOptions, L as ListeningPort, M as MANIFEST_FORMAT, v as MANIFEST_SCHEMA_VERSION, w as MountEntry, x as NetInterface, y as NetworkOptions, z as NetworkStack, A as NullInput, D as NullOutput, P as PYTHON_VERSION, Q as PasswdEntry, R as Pipe, T as Preparation, U as PreparedBinary, V as Process, W as ProcessKind, X as ProcessOptions, Y as ProcessState, Z as ProcessTable, _ as PythonCapabilities, $ as PythonOptions, a0 as PythonProfile, a1 as PythonRuntimeManifest, a2 as ResolvedExecutable, a3 as RunOptions, a4 as RunResult, a5 as SessionInit, a6 as SessionResult, a7 as SessionRunOptions, a8 as ShellExit, a9 as ShellInit, aa as ShellOptions, ab as SpawnHandle, ac as Stdio, ad as TeeOutput, ae as UserDatabase, af as Variables, ag as WasmCommandArtifact, ah as WasmTranslator, ai as binaryDigest, aj as braceExpand, ak as captureStdio, al as configurePython, am as createContext, an as createTranslationBackend, ao as createWasmCompatibilityBackend, ap as defineCommand, aq as expandWord, ar as expandWords, as as inspectElf, at as installWasmCommands, au as isElfBinary, av as isPythonAvailable, aw as resetPidCounter, ax as shellQuote, ay as validateManifest } from './container-D-e5SMXQ.js';
3
+ import { V as Vfs, C as Cred, R as RuntimeVolume, a as VirtualTcpNetwork, b as RuntimeHttpResponse, O as OutboundPolicy, S as SpawnChild, c as SyncSpawn, I as IpcTransport, d as VolumeStat, e as VolumeStats, f as RuntimePod, g as RuntimePackageInstaller, h as ChildSpawnConfig, i as ChildHandle, j as RuntimeProcess, k as RuntimeSocketPeer, l as RuntimeConnection } from './contracts-BHo4LdY2.js';
4
+ export { D as DirEntry, m as ROOT_CRED, n as RuntimeProcessManager, o as RuntimeProcessResult, p as Stats, q as VirtualNode, r as VirtualProvider, W as WriteOptions, s as applyChmod, t as createChildProcessModule, u as formatMode, v as makeCred, w as octalMode, x as parseUmask } from './contracts-BHo4LdY2.js';
5
+ import { M as MemoryVolume, a as MemoryVolumeSnapshotEntry } from './memory-volume-meoRW8ST.js';
6
6
  import EventEmitter from 'events/events.js';
7
7
  import streamModule from 'stream-browserify';
8
8
 
package/dist/index.js CHANGED
@@ -8357,6 +8357,60 @@ function createSysProvider(kernel) {
8357
8357
  };
8358
8358
  }
8359
8359
 
8360
+ // src/net/proxy-fetch.ts
8361
+ function toBase64(bytes2) {
8362
+ let text2 = "";
8363
+ for (let at = 0; at < bytes2.length; at += 32768) {
8364
+ text2 += String.fromCharCode(...bytes2.subarray(at, at + 32768));
8365
+ }
8366
+ return btoa(text2);
8367
+ }
8368
+ function fromBase64(text2) {
8369
+ const binary = atob(text2);
8370
+ const bytes2 = new Uint8Array(binary.length);
8371
+ for (let at = 0; at < binary.length; at += 1) bytes2[at] = binary.charCodeAt(at);
8372
+ return bytes2;
8373
+ }
8374
+ async function proxyExchange(proxy, request, fetchImpl = fetch) {
8375
+ let answer;
8376
+ try {
8377
+ answer = await fetchImpl(proxy, {
8378
+ method: "POST",
8379
+ headers: { "content-type": "application/json" },
8380
+ body: JSON.stringify({
8381
+ url: request.url,
8382
+ method: request.method,
8383
+ headers: request.headers,
8384
+ ...request.body?.length ? { body: toBase64(request.body) } : {}
8385
+ })
8386
+ });
8387
+ } catch (error) {
8388
+ throw new Error(
8389
+ `the egress proxy at ${proxy} could not be reached (${error instanceof Error ? error.message : String(error)}). It performs this container's outbound requests, so nothing leaves until it answers.`
8390
+ );
8391
+ }
8392
+ if (!answer.ok) {
8393
+ throw new Error(
8394
+ `the egress proxy at ${proxy} refused ${request.method} ${request.url} with ${answer.status} ${answer.statusText}: ${(await answer.text()).trim().slice(0, 300)}`
8395
+ );
8396
+ }
8397
+ const payload = await answer.json();
8398
+ return {
8399
+ status: payload.status ?? 200,
8400
+ statusText: payload.statusText ?? "",
8401
+ headers: payload.headers ?? {},
8402
+ body: payload.body ? fromBase64(payload.body) : new Uint8Array(0)
8403
+ };
8404
+ }
8405
+ function isBrowser() {
8406
+ return typeof process === "undefined" || process.versions?.node == null;
8407
+ }
8408
+ function browserBlocked(hostname, message) {
8409
+ return new Error(
8410
+ `${message} \u2014 the request to ${hostname} was blocked by the browser. A page can only read a response from a host that sends CORS headers, and most APIs send none. Give the container a proxy that makes the request for it \u2014 network: { proxy }, and \`npx sandboxedjs-egress\` is one \u2014 or run the container on a server.`
8411
+ );
8412
+ }
8413
+
8360
8414
  // src/net/policy.ts
8361
8415
  function isLoopbackHostname(hostname) {
8362
8416
  const host2 = hostname.replace(/^\[|\]$/g, "").toLowerCase();
@@ -8399,7 +8453,40 @@ function policedFetch(policy, fetchImpl) {
8399
8453
  if (!outboundAllowed(policy, url)) {
8400
8454
  throw Object.assign(new TypeError("fetch failed"), { cause: outboundBlockedError(url) });
8401
8455
  }
8402
- return fetchImpl(input, init);
8456
+ if (policy.proxy) return await fetchThroughProxy(policy.proxy, fetchImpl, input, init);
8457
+ try {
8458
+ return await fetchImpl(input, init);
8459
+ } catch (error) {
8460
+ if (!isBrowser()) throw error;
8461
+ throw Object.assign(new TypeError("fetch failed"), {
8462
+ cause: browserBlocked(new URL(url).hostname, error instanceof Error ? error.message : String(error))
8463
+ });
8464
+ }
8465
+ });
8466
+ }
8467
+ async function fetchThroughProxy(proxy, fetchImpl, input, init) {
8468
+ const request = new Request(input, init);
8469
+ const headers = {};
8470
+ request.headers.forEach((value, name) => {
8471
+ headers[name] = value;
8472
+ });
8473
+ const bodyless = request.method === "GET" || request.method === "HEAD";
8474
+ const result = await proxyExchange(
8475
+ proxy,
8476
+ {
8477
+ url: request.url,
8478
+ method: request.method,
8479
+ headers,
8480
+ ...bodyless ? {} : { body: new Uint8Array(await request.arrayBuffer()) }
8481
+ },
8482
+ fetchImpl
8483
+ );
8484
+ const nullBody = result.status === 204 || result.status === 205 || result.status === 304 || request.method === "HEAD";
8485
+ const body = nullBody ? null : result.body.slice().buffer;
8486
+ return new Response(body, {
8487
+ status: result.status,
8488
+ statusText: result.statusText,
8489
+ headers: result.headers
8403
8490
  });
8404
8491
  }
8405
8492
  function policedWebSocket(Native, policy) {
@@ -17457,9 +17544,6 @@ function parseUrl(raw) {
17457
17544
  return null;
17458
17545
  }
17459
17546
  }
17460
- function isBrowser() {
17461
- return typeof process === "undefined" || process.versions?.node == null;
17462
- }
17463
17547
  async function performRequest(ctx, url, init) {
17464
17548
  const isLocal = ctx.kernel.net.isLocal(url.hostname);
17465
17549
  if (isLocal) {
@@ -17487,7 +17571,16 @@ async function performRequest(ctx, url, init) {
17487
17571
  );
17488
17572
  }
17489
17573
  const proxy = ctx.kernel.net.proxy;
17490
- if (proxy) return await proxyRequest(ctx, proxy, url, init);
17574
+ if (proxy) {
17575
+ const result = await proxyExchange(proxy, {
17576
+ url: url.toString(),
17577
+ method: init.method,
17578
+ headers: init.headers,
17579
+ ...init.body?.length ? { body: init.body } : {}
17580
+ });
17581
+ ctx.kernel.net.countRx(result.body.length);
17582
+ return { ...result, url: url.toString() };
17583
+ }
17491
17584
  let response;
17492
17585
  try {
17493
17586
  response = await fetch(url.toString(), {
@@ -17496,9 +17589,7 @@ async function performRequest(ctx, url, init) {
17496
17589
  ...init.body ? { body: init.body.slice().buffer } : {}
17497
17590
  });
17498
17591
  } catch (error) {
17499
- throw isBrowser() ? new Error(
17500
- `${error instanceof Error ? error.message : String(error)} \u2014 the request to ${url.hostname} was blocked by the browser. A page can only read a response from a host that sends CORS headers, and most APIs send none. Give the container a proxy that makes the request for it \u2014 network: { proxy }, and \`npx sandboxedjs-egress\` is one \u2014 or run the container on a server.`
17501
- ) : error;
17592
+ throw isBrowser() ? browserBlocked(url.hostname, error instanceof Error ? error.message : String(error)) : error;
17502
17593
  }
17503
17594
  const body = new Uint8Array(await response.arrayBuffer());
17504
17595
  ctx.kernel.net.countRx(body.length);
@@ -17508,53 +17599,6 @@ async function performRequest(ctx, url, init) {
17508
17599
  });
17509
17600
  return { status: response.status, statusText: response.statusText, headers, body, url: response.url || url.toString() };
17510
17601
  }
17511
- async function proxyRequest(ctx, proxy, url, init) {
17512
- let answer;
17513
- try {
17514
- answer = await fetch(proxy, {
17515
- method: "POST",
17516
- headers: { "content-type": "application/json" },
17517
- body: JSON.stringify({
17518
- url: url.toString(),
17519
- method: init.method,
17520
- headers: init.headers,
17521
- ...init.body?.length ? { body: toBase64(init.body) } : {}
17522
- })
17523
- });
17524
- } catch (error) {
17525
- throw new Error(
17526
- `the egress proxy at ${proxy} could not be reached (${error instanceof Error ? error.message : String(error)}). It performs this container's outbound requests, so nothing leaves until it answers.`
17527
- );
17528
- }
17529
- if (!answer.ok) {
17530
- throw new Error(
17531
- `the egress proxy at ${proxy} refused ${init.method} ${url.toString()} with ${answer.status} ${answer.statusText}: ${(await answer.text()).trim().slice(0, 300)}`
17532
- );
17533
- }
17534
- const payload = await answer.json();
17535
- const body = payload.body ? fromBase64(payload.body) : new Uint8Array(0);
17536
- ctx.kernel.net.countRx(body.length);
17537
- return {
17538
- status: payload.status ?? 200,
17539
- statusText: payload.statusText ?? "",
17540
- headers: payload.headers ?? {},
17541
- body,
17542
- url: url.toString()
17543
- };
17544
- }
17545
- function toBase64(bytes2) {
17546
- let text2 = "";
17547
- for (let at = 0; at < bytes2.length; at += 32768) {
17548
- text2 += String.fromCharCode(...bytes2.subarray(at, at + 32768));
17549
- }
17550
- return btoa(text2);
17551
- }
17552
- function fromBase64(text2) {
17553
- const binary = atob(text2);
17554
- const bytes2 = new Uint8Array(binary.length);
17555
- for (let at = 0; at < binary.length; at += 1) bytes2[at] = binary.charCodeAt(at);
17556
- return bytes2;
17557
- }
17558
17602
  var curl = defineCommand({
17559
17603
  name: "curl",
17560
17604
  path: "/usr/bin/curl",
@@ -20996,7 +21040,7 @@ var wheels_default = {
20996
21040
 
20997
21041
  // package.json
20998
21042
  var package_default = {
20999
- version: "0.2.3"};
21043
+ version: "0.2.5"};
21000
21044
 
21001
21045
  // src/python/extension-abi.ts
21002
21046
  var EXTENSION_ABI = {
@@ -22311,8 +22355,24 @@ var VirtualTcpNetwork = class {
22311
22355
  hasListener(port) {
22312
22356
  return this.listeners.has(port);
22313
22357
  }
22358
+ /** Ports the container itself listens on, which no guest asked for. */
22359
+ internal = /* @__PURE__ */ new Set();
22360
+ /**
22361
+ * Ports a guest is serving on.
22362
+ *
22363
+ * The container's own plumbing listens too — the HTTP egress takes a port
22364
+ * like any server — and those are not the guest's. Reported, they reach
22365
+ * every consumer of "what is running here": a port picker offers one, a
22366
+ * preview opens it, and the reader is looking at an internal endpoint
22367
+ * answering that it wanted an absolute URL, with nothing to say what it is
22368
+ * or why they are there.
22369
+ */
22314
22370
  ports() {
22315
- return [...this.listeners.keys()].sort((a, b) => a - b);
22371
+ return [...this.listeners.keys()].filter((port) => !this.internal.has(port)).sort((a, b) => a - b);
22372
+ }
22373
+ /** Keep `port` out of {@link ports}: it belongs to the container, not a guest. */
22374
+ markInternal(port) {
22375
+ this.internal.add(port);
22316
22376
  }
22317
22377
  closeAll() {
22318
22378
  for (const listener of [...this.listeners.values()]) this.close(listener);
@@ -23249,6 +23309,7 @@ function ensureHttpEgress(ctx) {
23249
23309
  }
23250
23310
  }
23251
23311
  if (!listener || port === 0) return 0;
23312
+ sockets.markInternal?.(port);
23252
23313
  listeners.set(sockets, port);
23253
23314
  const accepting = listener;
23254
23315
  void (async () => {
@@ -37791,7 +37852,8 @@ var Container = class _Container {
37791
37852
  if (opts.python) configurePython(opts.python);
37792
37853
  const network = {
37793
37854
  allowOutbound: opts.network?.allowOutbound ?? false,
37794
- allowedHosts: opts.network?.allowedHosts ?? null
37855
+ allowedHosts: opts.network?.allowedHosts ?? null,
37856
+ ...opts.network?.proxy ? { proxy: opts.network.proxy } : {}
37795
37857
  };
37796
37858
  const podOptions = {
37797
37859
  workdir: opts.cwd ?? "/",
@@ -1,4 +1,4 @@
1
- import { R as RuntimeVolume, d as VolumeStat, e as VolumeStats } from './contracts-B6SHFjma.cjs';
1
+ import { R as RuntimeVolume, d as VolumeStat, e as VolumeStats } from './contracts-BHo4LdY2.cjs';
2
2
 
3
3
  type NodeKind = "file" | "directory" | "symlink";
4
4
  interface MemoryVolumeSnapshotEntry {
@@ -1,4 +1,4 @@
1
- import { R as RuntimeVolume, d as VolumeStat, e as VolumeStats } from './contracts-B6SHFjma.js';
1
+ import { R as RuntimeVolume, d as VolumeStat, e as VolumeStats } from './contracts-BHo4LdY2.js';
2
2
 
3
3
  type NodeKind = "file" | "directory" | "symlink";
4
4
  interface MemoryVolumeSnapshotEntry {
@@ -1,6 +1,6 @@
1
- import { V as Vfs, C as Cred, a as VirtualTcpNetwork } from './contracts-B6SHFjma.cjs';
2
- export { m as ROOT_CRED, v as makeCred } from './contracts-B6SHFjma.cjs';
3
- export { M as MemoryVolume } from './memory-volume-CRhXUyCN.cjs';
1
+ import { V as Vfs, C as Cred, a as VirtualTcpNetwork } from './contracts-BHo4LdY2.cjs';
2
+ export { m as ROOT_CRED, v as makeCred } from './contracts-BHo4LdY2.cjs';
3
+ export { M as MemoryVolume } from './memory-volume-e-uJFRnG.cjs';
4
4
 
5
5
  /**
6
6
  * Open-file descriptions: the thing a descriptor points *at*.
@@ -1,6 +1,6 @@
1
- import { V as Vfs, C as Cred, a as VirtualTcpNetwork } from './contracts-B6SHFjma.js';
2
- export { m as ROOT_CRED, v as makeCred } from './contracts-B6SHFjma.js';
3
- export { M as MemoryVolume } from './memory-volume-CwiXvRfs.js';
1
+ import { V as Vfs, C as Cred, a as VirtualTcpNetwork } from './contracts-BHo4LdY2.js';
2
+ export { m as ROOT_CRED, v as makeCred } from './contracts-BHo4LdY2.js';
3
+ export { M as MemoryVolume } from './memory-volume-meoRW8ST.js';
4
4
 
5
5
  /**
6
6
  * Open-file descriptions: the thing a descriptor points *at*.
@@ -1,3 +1,58 @@
1
+ // src/preview/fetch-client.ts
2
+ function installPreviewFetch(scope) {
3
+ const MARKER = "__sbx__/";
4
+ const at = scope.location.pathname.indexOf(MARKER);
5
+ if (at < 0) return;
6
+ const base = scope.location.origin + scope.location.pathname.slice(0, at + MARKER.length);
7
+ const LOOPBACK = /^(?:localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|\[?::1\]?|[^.]+\.localhost)$/i;
8
+ const rewrite = (value) => {
9
+ let url;
10
+ try {
11
+ url = new URL(value, scope.location.href);
12
+ } catch {
13
+ return value;
14
+ }
15
+ const secure = url.protocol === "https:" || url.protocol === "wss:";
16
+ if (!secure && url.protocol !== "http:" && url.protocol !== "ws:") return value;
17
+ if (!LOOPBACK.test(url.hostname)) return value;
18
+ const port = url.port || (secure ? "443" : "80");
19
+ return `${base}${port}${url.pathname}${url.search}${url.hash}`;
20
+ };
21
+ const originalFetch = scope.fetch;
22
+ if (typeof originalFetch === "function") {
23
+ scope.fetch = function patched(input, init) {
24
+ if (typeof input === "string" || input instanceof URL) {
25
+ return originalFetch.call(this, rewrite(String(input)), init);
26
+ }
27
+ const moved = input && typeof input === "object" && "url" in input ? rewrite(input.url) : null;
28
+ if (moved === null || moved === input.url) {
29
+ return originalFetch.call(this, input, init);
30
+ }
31
+ return originalFetch.call(this, new Request(moved, input), init);
32
+ };
33
+ }
34
+ const open = scope.XMLHttpRequest?.prototype?.open;
35
+ if (typeof open === "function") {
36
+ scope.XMLHttpRequest.prototype.open = function patched(method, url, ...rest) {
37
+ return open.call(this, method, rewrite(String(url)), ...rest);
38
+ };
39
+ }
40
+ const OriginalEventSource = scope.EventSource;
41
+ if (typeof OriginalEventSource === "function") {
42
+ const Patched = function(url, init) {
43
+ return new OriginalEventSource(rewrite(String(url)), init);
44
+ };
45
+ Patched.prototype = OriginalEventSource.prototype;
46
+ for (const name of ["CONNECTING", "OPEN", "CLOSED"]) {
47
+ Object.defineProperty(Patched, name, { value: OriginalEventSource[name], enumerable: true });
48
+ }
49
+ scope.EventSource = Patched;
50
+ }
51
+ }
52
+ function previewFetchSource() {
53
+ return `(function(){try{(${installPreviewFetch.toString()})(window);}catch(error){console.warn("sandboxedjs: loopback rewriting unavailable",error);}})();`;
54
+ }
55
+
1
56
  // src/preview/ws-client.ts
2
57
  function installPreviewSockets(scope) {
3
58
  const Native = scope.WebSocket;
@@ -419,6 +474,19 @@ var PreviewRouter = class {
419
474
  portFor(clientId) {
420
475
  return this.clientPorts.get(clientId);
421
476
  }
477
+ /**
478
+ * The container port one request is for, by path first and client second.
479
+ *
480
+ * A port named in the path wins, because that is how a previewed page
481
+ * reaches its *other* servers: the frontend's document is bound to 3000, and
482
+ * its call to the backend arrives as `__sbx__/8000/…`. Reading the port from
483
+ * the path also means such a request needs nothing remembered about who is
484
+ * asking — which matters, because this worker is stopped whenever it looks
485
+ * idle and every binding it held is gone when it starts again.
486
+ */
487
+ portForRequest(pathname, clientId) {
488
+ return this.claimedPort(pathname) ?? this.portFor(clientId);
489
+ }
422
490
  /** A response has come back from the page. */
423
491
  settle(id, response) {
424
492
  const resolve = this.pending.get(id);
@@ -498,7 +566,7 @@ function withSockets(body, headers) {
498
566
  const encoding = headers.get("content-encoding");
499
567
  if (encoding && encoding.toLowerCase() !== "identity") return body;
500
568
  const html = new TextDecoder().decode(body);
501
- const tag = `<script>${previewSocketSource()}<\/script>`;
569
+ const tag = `<script>${previewSocketSource()}${previewFetchSource()}<\/script>`;
502
570
  const head = /<head[^>]*>/i.exec(html);
503
571
  const at = head ? head.index + head[0].length : 0;
504
572
  const bytes = new TextEncoder().encode(html.slice(0, at) + tag + html.slice(at));
@@ -591,7 +659,7 @@ worker.addEventListener("fetch", (event) => {
591
659
  event.respondWith(router.fetch(claimed, "/", event.request, event.resultingClientId || event.clientId));
592
660
  return;
593
661
  }
594
- const port = router.portFor(event.clientId);
662
+ const port = router.portForRequest(url.pathname, event.clientId);
595
663
  if (port === void 0) return;
596
664
  event.respondWith(
597
665
  router.fetch(port, router.containerPath(url.pathname, url.search), event.request, event.clientId)