sandboxedjs 0.2.0 → 0.2.3

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/README.md CHANGED
@@ -43,6 +43,39 @@ npx sandboxedjs-build-wheels 4180
43
43
  See `docs/python/build-on-miss.md`, and `docs/python/compatibility.md` for what
44
44
  has been built and tested.
45
45
 
46
+ ## Outbound requests from a browser
47
+
48
+ A page may read a response only from a host that sends CORS headers back, and
49
+ most APIs send none — an `Authorization` header alone forces a preflight that
50
+ plenty of them answer with 405. A container whose guest calls a real API
51
+ therefore works under Node and fails in a browser, for a reason that belongs to
52
+ the page rather than to anything in the container.
53
+
54
+ Give it somewhere to send those requests instead:
55
+
56
+ ```bash
57
+ npx sandboxedjs-egress 4181 --allow api.openai.com,ollama.com
58
+ ```
59
+
60
+ ```ts
61
+ const box = await createContainer({
62
+ network: { allowOutbound: true, proxy: "http://localhost:4181" },
63
+ });
64
+ ```
65
+
66
+ Every outbound request then leaves through that endpoint, which makes it where
67
+ CORS does not apply and hands the whole response back. Loopback stays inside
68
+ the container, and the container's own outbound policy still applies before
69
+ anything is handed over — a proxy widens what a *page* can reach, not what the
70
+ container may.
71
+
72
+ In production the proxy is a route on your own origin. It answers `POST` with
73
+ `{ url, method, headers, body }` and replies `{ status, statusText, headers,
74
+ body }`, bodies base64; `bin/sandboxedjs-egress.mjs` is a complete
75
+ implementation in about a hundred lines. Anything that can reach it can make
76
+ requests through it carrying whatever credentials the guest holds, so keep it
77
+ on loopback in development and behind your own auth in production.
78
+
46
79
  ## Why
47
80
 
48
81
  Sometimes you need to run untrusted or generated code, give an AI agent a shell, build a
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The outbound half of a container that runs in a browser.
4
+ *
5
+ * A page may read a response only from a host that sends CORS headers back,
6
+ * and most APIs send none — an `Authorization` header alone forces a preflight
7
+ * that plenty of them answer with 405. So a container whose guest talks to a
8
+ * real API works under Node and fails in a browser, for a reason that has
9
+ * nothing to do with the container. This is the machine that makes those
10
+ * requests instead, where CORS does not apply.
11
+ *
12
+ * sandboxedjs-egress [port=4181] [--allow host,host]
13
+ *
14
+ * Point a container at it:
15
+ *
16
+ * createContainer({ network: { allowOutbound: true, proxy: "http://localhost:4181" } });
17
+ *
18
+ * It binds loopback only, and `--allow` narrows what it will fetch. Anything
19
+ * that can reach this port can make requests through it, carrying whatever
20
+ * credentials the guest holds — so it stays on this machine.
21
+ */
22
+
23
+ import { createServer } from "node:http";
24
+
25
+ const args = process.argv.slice(2);
26
+ const at = args.indexOf("--allow");
27
+ const allowed = at === -1 ? null : (args[at + 1] ?? "").split(",").map((h) => h.trim()).filter(Boolean);
28
+ const port = Number(args.find((a) => /^\d+$/.test(a)) ?? 4181);
29
+
30
+ /* Hop-by-hop headers describe the connection they arrived on, not the message.
31
+ * Forwarding them describes this hop's framing to a server that is not on it. */
32
+ const HOP = new Set([
33
+ "connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
34
+ "te", "trailer", "transfer-encoding", "upgrade", "host", "content-length",
35
+ ]);
36
+
37
+ const cors = {
38
+ "access-control-allow-origin": "*",
39
+ "access-control-allow-methods": "POST, OPTIONS",
40
+ "access-control-allow-headers": "content-type",
41
+ "access-control-max-age": "86400",
42
+ };
43
+
44
+ const send = (res, status, body, type = "text/plain; charset=utf-8") =>
45
+ res.writeHead(status, { ...cors, "content-type": type }).end(body);
46
+
47
+ createServer(async (req, res) => {
48
+ if (req.method === "OPTIONS") return res.writeHead(204, cors).end();
49
+ if (req.method !== "POST") return send(res, 405, "POST a JSON request here. See sandboxedjs-egress --help.");
50
+
51
+ let payload;
52
+ try {
53
+ const chunks = [];
54
+ for await (const chunk of req) chunks.push(chunk);
55
+ payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
56
+ } catch (error) {
57
+ return send(res, 400, `could not read the request: ${error.message}`);
58
+ }
59
+
60
+ let target;
61
+ try {
62
+ target = new URL(payload.url);
63
+ } catch {
64
+ return send(res, 400, `not a URL: ${payload.url}`);
65
+ }
66
+ if (target.protocol !== "http:" && target.protocol !== "https:") {
67
+ return send(res, 400, `${target.protocol} is not a protocol this proxy speaks`);
68
+ }
69
+ if (allowed && !allowed.some((host) => target.hostname === host || target.hostname.endsWith(`.${host}`))) {
70
+ return send(res, 403, `${target.hostname} is not in this proxy's --allow list`);
71
+ }
72
+
73
+ const headers = Object.fromEntries(
74
+ Object.entries(payload.headers ?? {}).filter(([name]) => !HOP.has(name.toLowerCase())),
75
+ );
76
+ try {
77
+ const answer = await fetch(target, {
78
+ method: payload.method ?? "GET",
79
+ headers,
80
+ ...(payload.body ? { body: Buffer.from(payload.body, "base64") } : {}),
81
+ redirect: "follow",
82
+ });
83
+ const body = Buffer.from(await answer.arrayBuffer());
84
+ const back = {};
85
+ answer.headers.forEach((value, name) => {
86
+ if (!HOP.has(name.toLowerCase())) back[name] = value;
87
+ });
88
+ console.log(`${payload.method ?? "GET"} ${target.href} -> ${answer.status} (${body.length} bytes)`);
89
+ send(res, 200, JSON.stringify({
90
+ status: answer.status,
91
+ statusText: answer.statusText,
92
+ headers: back,
93
+ body: body.toString("base64"),
94
+ }), "application/json");
95
+ } catch (error) {
96
+ console.error(`${payload.method ?? "GET"} ${target.href} -> ${error.message}`);
97
+ send(res, 502, `the request to ${target.hostname} failed: ${error.message}`);
98
+ }
99
+ }).listen(port, "127.0.0.1", () => {
100
+ console.log(`sandboxedjs-egress on http://localhost:${port}${allowed ? ` (allowing ${allowed.join(", ")})` : ""}`);
101
+ console.log(` createContainer({ network: { allowOutbound: true, proxy: "http://localhost:${port}" } })`);
102
+ });
package/dist/agent.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as Container } from './container-CPwyHoUy.cjs';
1
+ import { C as Container } from './container-C9Y8dqos.cjs';
2
2
  import './contracts-B6SHFjma.cjs';
3
3
 
4
4
  /**
package/dist/agent.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as Container } from './container-CFieZcbe.js';
1
+ import { C as Container } from './container-CIE93_Gh.js';
2
2
  import './contracts-B6SHFjma.js';
3
3
 
4
4
  /**
@@ -461,6 +461,26 @@ interface NetworkOptions {
461
461
  allowOutbound?: boolean;
462
462
  /** Host allowlist applied when `allowOutbound` is on. `null` means any host. */
463
463
  allowedHosts?: string[] | null;
464
+ /**
465
+ * A URL that performs this container's outbound requests on its behalf.
466
+ *
467
+ * In a browser the container's only way out is the page's `fetch`, and a
468
+ * page may read a response only from a host that sends CORS headers back.
469
+ * Most APIs do not — an `Authorization` header alone forces a preflight that
470
+ * plenty of them answer with 405 — so a container that works under Node
471
+ * fails in a browser at the first real API call, for a reason that belongs
472
+ * to the page rather than to anything in here.
473
+ *
474
+ * A proxy is the way out of that: a server on an origin the page is allowed
475
+ * to read, which makes the request where CORS does not apply and hands the
476
+ * whole response back. `sandboxedjs-egress` is one; any endpoint speaking
477
+ * the same small JSON protocol will do.
478
+ *
479
+ * Loopback requests never reach it — those are the container's own servers —
480
+ * and the outbound policy is applied before a request is handed over, so a
481
+ * proxy widens what a page can reach, not what the container may.
482
+ */
483
+ proxy?: string;
464
484
  /** Address handed to eth0. */
465
485
  ipv4?: string;
466
486
  gateway?: string;
@@ -494,6 +514,8 @@ declare class NetworkStack {
494
514
  /** The policy every way out of the container applies, not only the shell's. */
495
515
  get policy(): OutboundPolicy;
496
516
  outboundAllowed(url: string): boolean;
517
+ /** The endpoint outbound requests are handed to, when the host named one. */
518
+ get proxy(): string | null;
497
519
  procNetDev(): string;
498
520
  procNetRoute(): string;
499
521
  countTx(bytes: number, iface?: string): void;
@@ -461,6 +461,26 @@ interface NetworkOptions {
461
461
  allowOutbound?: boolean;
462
462
  /** Host allowlist applied when `allowOutbound` is on. `null` means any host. */
463
463
  allowedHosts?: string[] | null;
464
+ /**
465
+ * A URL that performs this container's outbound requests on its behalf.
466
+ *
467
+ * In a browser the container's only way out is the page's `fetch`, and a
468
+ * page may read a response only from a host that sends CORS headers back.
469
+ * Most APIs do not — an `Authorization` header alone forces a preflight that
470
+ * plenty of them answer with 405 — so a container that works under Node
471
+ * fails in a browser at the first real API call, for a reason that belongs
472
+ * to the page rather than to anything in here.
473
+ *
474
+ * A proxy is the way out of that: a server on an origin the page is allowed
475
+ * to read, which makes the request where CORS does not apply and hands the
476
+ * whole response back. `sandboxedjs-egress` is one; any endpoint speaking
477
+ * the same small JSON protocol will do.
478
+ *
479
+ * Loopback requests never reach it — those are the container's own servers —
480
+ * and the outbound policy is applied before a request is handed over, so a
481
+ * proxy widens what a page can reach, not what the container may.
482
+ */
483
+ proxy?: string;
464
484
  /** Address handed to eth0. */
465
485
  ipv4?: string;
466
486
  gateway?: string;
@@ -494,6 +514,8 @@ declare class NetworkStack {
494
514
  /** The policy every way out of the container applies, not only the shell's. */
495
515
  get policy(): OutboundPolicy;
496
516
  outboundAllowed(url: string): boolean;
517
+ /** The endpoint outbound requests are handed to, when the host named one. */
518
+ get proxy(): string | null;
497
519
  procNetDev(): string;
498
520
  procNetRoute(): string;
499
521
  countTx(bytes: number, iface?: string): void;
package/dist/index.cjs CHANGED
@@ -8453,6 +8453,7 @@ var NetworkStack = class {
8453
8453
  this.options = {
8454
8454
  allowOutbound: options.allowOutbound ?? false,
8455
8455
  allowedHosts: options.allowedHosts ?? null,
8456
+ ...options.proxy ? { proxy: options.proxy } : {},
8456
8457
  ipv4: options.ipv4 ?? "172.17.0.2",
8457
8458
  gateway: options.gateway ?? "172.17.0.1"
8458
8459
  };
@@ -8590,6 +8591,10 @@ var NetworkStack = class {
8590
8591
  outboundAllowed(url) {
8591
8592
  return outboundAllowed(this.policy, url);
8592
8593
  }
8594
+ /** The endpoint outbound requests are handed to, when the host named one. */
8595
+ get proxy() {
8596
+ return this.options.proxy ?? null;
8597
+ }
8593
8598
  // ── /proc plumbing ───────────────────────────────────────────────────────
8594
8599
  procNetDev() {
8595
8600
  let out = "Inter-| Receive | Transmit\n";
@@ -17498,6 +17503,8 @@ async function performRequest(ctx, url, init) {
17498
17503
  `outbound network access to ${url.hostname} is disabled for this container (enable with network: { allowOutbound: true })`
17499
17504
  );
17500
17505
  }
17506
+ const proxy = ctx.kernel.net.proxy;
17507
+ if (proxy) return await proxyRequest(ctx, proxy, url, init);
17501
17508
  let response;
17502
17509
  try {
17503
17510
  response = await fetch(url.toString(), {
@@ -17507,7 +17514,7 @@ async function performRequest(ctx, url, init) {
17507
17514
  });
17508
17515
  } catch (error) {
17509
17516
  throw isBrowser() ? new Error(
17510
- `${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; route it through a proxy on your own origin, or run the container on a server.`
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.`
17511
17518
  ) : error;
17512
17519
  }
17513
17520
  const body = new Uint8Array(await response.arrayBuffer());
@@ -17518,6 +17525,53 @@ async function performRequest(ctx, url, init) {
17518
17525
  });
17519
17526
  return { status: response.status, statusText: response.statusText, headers, body, url: response.url || url.toString() };
17520
17527
  }
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
+ }
17521
17575
  var curl = defineCommand({
17522
17576
  name: "curl",
17523
17577
  path: "/usr/bin/curl",
@@ -20959,7 +21013,7 @@ var wheels_default = {
20959
21013
 
20960
21014
  // package.json
20961
21015
  var package_default = {
20962
- version: "0.2.0"};
21016
+ version: "0.2.3"};
20963
21017
 
20964
21018
  // src/python/extension-abi.ts
20965
21019
  var EXTENSION_ABI = {
@@ -24320,7 +24374,7 @@ async function runOwnedPython(ctx, argv) {
24320
24374
  }
24321
24375
  } catch {
24322
24376
  }
24323
- if (ctx.kernel.pod.sockets?.outbound) env2.SBX_OUTBOUND_SOCKETS = "1";
24377
+ if (ctx.kernel.pod.sockets?.outbound && !ctx.kernel.net.proxy) env2.SBX_OUTBOUND_SOCKETS = "1";
24324
24378
  env2.SBX_SERIAL_HOST_CALLS = "1";
24325
24379
  const inherit = claimInheritance(env2);
24326
24380
  const process2 = await startPythonProcess({
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
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-CPwyHoUy.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-CPwyHoUy.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-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
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
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
5
  import { M as MemoryVolume, a as MemoryVolumeSnapshotEntry } from './memory-volume-CRhXUyCN.cjs';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
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-CFieZcbe.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-CFieZcbe.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-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
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
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
5
  import { M as MemoryVolume, a as MemoryVolumeSnapshotEntry } from './memory-volume-CwiXvRfs.js';
package/dist/index.js CHANGED
@@ -8436,6 +8436,7 @@ var NetworkStack = class {
8436
8436
  this.options = {
8437
8437
  allowOutbound: options.allowOutbound ?? false,
8438
8438
  allowedHosts: options.allowedHosts ?? null,
8439
+ ...options.proxy ? { proxy: options.proxy } : {},
8439
8440
  ipv4: options.ipv4 ?? "172.17.0.2",
8440
8441
  gateway: options.gateway ?? "172.17.0.1"
8441
8442
  };
@@ -8573,6 +8574,10 @@ var NetworkStack = class {
8573
8574
  outboundAllowed(url) {
8574
8575
  return outboundAllowed(this.policy, url);
8575
8576
  }
8577
+ /** The endpoint outbound requests are handed to, when the host named one. */
8578
+ get proxy() {
8579
+ return this.options.proxy ?? null;
8580
+ }
8576
8581
  // ── /proc plumbing ───────────────────────────────────────────────────────
8577
8582
  procNetDev() {
8578
8583
  let out = "Inter-| Receive | Transmit\n";
@@ -17481,6 +17486,8 @@ async function performRequest(ctx, url, init) {
17481
17486
  `outbound network access to ${url.hostname} is disabled for this container (enable with network: { allowOutbound: true })`
17482
17487
  );
17483
17488
  }
17489
+ const proxy = ctx.kernel.net.proxy;
17490
+ if (proxy) return await proxyRequest(ctx, proxy, url, init);
17484
17491
  let response;
17485
17492
  try {
17486
17493
  response = await fetch(url.toString(), {
@@ -17490,7 +17497,7 @@ async function performRequest(ctx, url, init) {
17490
17497
  });
17491
17498
  } catch (error) {
17492
17499
  throw isBrowser() ? new Error(
17493
- `${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; route it through a proxy on your own origin, or run the container on a server.`
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.`
17494
17501
  ) : error;
17495
17502
  }
17496
17503
  const body = new Uint8Array(await response.arrayBuffer());
@@ -17501,6 +17508,53 @@ async function performRequest(ctx, url, init) {
17501
17508
  });
17502
17509
  return { status: response.status, statusText: response.statusText, headers, body, url: response.url || url.toString() };
17503
17510
  }
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
+ }
17504
17558
  var curl = defineCommand({
17505
17559
  name: "curl",
17506
17560
  path: "/usr/bin/curl",
@@ -20942,7 +20996,7 @@ var wheels_default = {
20942
20996
 
20943
20997
  // package.json
20944
20998
  var package_default = {
20945
- version: "0.2.0"};
20999
+ version: "0.2.3"};
20946
21000
 
20947
21001
  // src/python/extension-abi.ts
20948
21002
  var EXTENSION_ABI = {
@@ -24303,7 +24357,7 @@ async function runOwnedPython(ctx, argv) {
24303
24357
  }
24304
24358
  } catch {
24305
24359
  }
24306
- if (ctx.kernel.pod.sockets?.outbound) env2.SBX_OUTBOUND_SOCKETS = "1";
24360
+ if (ctx.kernel.pod.sockets?.outbound && !ctx.kernel.net.proxy) env2.SBX_OUTBOUND_SOCKETS = "1";
24307
24361
  env2.SBX_SERIAL_HOST_CALLS = "1";
24308
24362
  const inherit = claimInheritance(env2);
24309
24363
  const process2 = await startPythonProcess({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sandboxedjs",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
4
4
  "description": "A Linux-like container that runs entirely inside Node.js — POSIX shell, ~140 coreutils, Node.js and Python runtimes, virtual filesystem and networking. No Docker, no VM, no native modules.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -16,6 +16,7 @@
16
16
  "sbx": "bin/sandboxedjs.mjs",
17
17
  "sandboxedjs-serve": "bin/sandboxedjs-serve.mjs",
18
18
  "sandboxedjs-build-wheels": "bin/sandboxedjs-build-wheels.mjs",
19
+ "sandboxedjs-egress": "bin/sandboxedjs-egress.mjs",
19
20
  "sandboxedjs-pack": "bin/sandboxedjs-pack.mjs"
20
21
  },
21
22
  "exports": {