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 +33 -0
- package/bin/sandboxedjs-egress.mjs +102 -0
- package/dist/agent.d.cts +1 -1
- package/dist/agent.d.ts +1 -1
- package/dist/{container-CPwyHoUy.d.cts → container-C9Y8dqos.d.cts} +22 -0
- package/dist/{container-CFieZcbe.d.ts → container-CIE93_Gh.d.ts} +22 -0
- package/dist/index.cjs +57 -3
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +57 -3
- package/package.json +2 -1
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
package/dist/agent.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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.
|
|
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.
|
|
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": {
|