sandboxedjs 0.2.7 → 0.2.8
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 +23 -16
- package/bin/sandboxedjs-egress.mjs +117 -0
- package/dist/index.cjs +12 -7
- package/dist/index.js +12 -7
- package/dist/worker-entry.js +11 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,38 +51,45 @@ plenty of them answer with 405. A container whose guest calls a real API
|
|
|
51
51
|
therefore works under Node and fails in a browser, for a reason that belongs to
|
|
52
52
|
the page rather than to anything in the container.
|
|
53
53
|
|
|
54
|
-
Give it somewhere to send those requests instead.
|
|
55
|
-
|
|
54
|
+
Give it somewhere to send those requests instead. Add that somewhere to the
|
|
55
|
+
project that is already being deployed, and the container finds it by itself:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
npx sandboxedjs-egress
|
|
58
|
+
npx sandboxedjs-egress init --allow api.openai.com,ollama.com
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
That writes one file at the path the host platform serves — Cloudflare Pages,
|
|
62
|
+
Vercel and Netlify are detected, `--target` names one — and nothing else
|
|
63
|
+
changes. A container in a page probes its own origin for the proxy before it
|
|
64
|
+
gives up, so no project passes `proxy` and none of them has to be told twice.
|
|
65
|
+
|
|
66
|
+
In development the same proxy runs as a process, on the port the probe checks:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx sandboxedjs-egress
|
|
65
70
|
```
|
|
66
71
|
|
|
67
|
-
In an app that already has a server, mount
|
|
68
|
-
|
|
69
|
-
itself:
|
|
72
|
+
In an app that already has a server, mount the handler on the well-known path.
|
|
73
|
+
Before any body parser: it reads the request stream itself.
|
|
70
74
|
|
|
71
75
|
```ts
|
|
72
|
-
import { egressNodeHandler } from "sandboxedjs/egress";
|
|
73
|
-
app.
|
|
74
|
-
// createContainer({ network: { allowOutbound: true, proxy: "/egress" } })
|
|
76
|
+
import { egressNodeHandler, EGRESS_PATH } from "sandboxedjs/egress";
|
|
77
|
+
app.use(EGRESS_PATH, egressNodeHandler({ allow: ["ollama.com"] }));
|
|
75
78
|
```
|
|
76
79
|
|
|
77
|
-
A static
|
|
78
|
-
Cloudflare Pages Function, a Worker, a route handler:
|
|
80
|
+
A static host takes the `Request` form — which is what `init` writes:
|
|
79
81
|
|
|
80
82
|
```ts
|
|
81
|
-
// functions/egress.ts
|
|
83
|
+
// functions/__sandboxedjs__/egress.ts, on Cloudflare Pages
|
|
82
84
|
import { handleEgressRequest } from "sandboxedjs/egress";
|
|
83
85
|
export const onRequest = ({ request }) => handleEgressRequest(request, { allow: ["ollama.com"] });
|
|
84
86
|
```
|
|
85
87
|
|
|
88
|
+
`network: { proxy }` still names one explicitly — a proxy somewhere else, or
|
|
89
|
+
one that discovery should not be trusted to find — and it is never overridden.
|
|
90
|
+
`sandboxedjs-serve` mounts the proxy itself, so an app served by it needs none
|
|
91
|
+
of this.
|
|
92
|
+
|
|
86
93
|
Every exit honours it — `curl`, `wget`, a guest's own `fetch`, and Python's
|
|
87
94
|
sockets — so it means the same thing whatever the project is written in.
|
|
88
95
|
Loopback stays inside the container, and the container's outbound policy still
|
|
@@ -21,9 +21,126 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { createServer } from "node:http";
|
|
24
|
+
import { mkdir, writeFile, access } from "node:fs/promises";
|
|
25
|
+
import { dirname, join } from "node:path";
|
|
24
26
|
import { egressNodeHandler } from "../dist/egress.js";
|
|
25
27
|
|
|
26
28
|
const args = process.argv.slice(2);
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* `sandboxedjs-egress init` — the proxy as a file in the project that is
|
|
32
|
+
* already being deployed.
|
|
33
|
+
*
|
|
34
|
+
* Running a process beside a site works on a laptop and nowhere else, and a
|
|
35
|
+
* static host has no server to add one to. What every static host does have is
|
|
36
|
+
* a functions directory, so the proxy goes there: one file, at the path the
|
|
37
|
+
* container already looks for, and a deployment that needed a setting needs
|
|
38
|
+
* nothing.
|
|
39
|
+
*/
|
|
40
|
+
const TARGETS = {
|
|
41
|
+
cloudflare: {
|
|
42
|
+
file: "functions/__sandboxedjs__/egress.ts",
|
|
43
|
+
detect: ["wrangler.toml", "wrangler.json", "wrangler.jsonc", "functions"],
|
|
44
|
+
source: (allow) => `import { handleEgressRequest } from "sandboxedjs/egress";
|
|
45
|
+
${allow}
|
|
46
|
+
/* Cloudflare Pages serves this file at /__sandboxedjs__/egress, which is where
|
|
47
|
+
* a container in this site's pages looks for its way out. */
|
|
48
|
+
export const onRequest = ({ request }) => handleEgressRequest(request, options);
|
|
49
|
+
`,
|
|
50
|
+
},
|
|
51
|
+
vercel: {
|
|
52
|
+
file: "api/__sandboxedjs__/egress.ts",
|
|
53
|
+
detect: ["vercel.json", "api"],
|
|
54
|
+
source: (allow) => `import { handleEgressRequest } from "sandboxedjs/egress";
|
|
55
|
+
${allow}
|
|
56
|
+
export const config = { runtime: "edge" };
|
|
57
|
+
|
|
58
|
+
/* Vercel serves this file at /api/__sandboxedjs__/egress, which is one of the
|
|
59
|
+
* paths a container in this site's pages probes for its way out. */
|
|
60
|
+
export default (request) => handleEgressRequest(request, options);
|
|
61
|
+
`,
|
|
62
|
+
},
|
|
63
|
+
netlify: {
|
|
64
|
+
file: "netlify/functions/sandboxedjs-egress.ts",
|
|
65
|
+
detect: ["netlify.toml", "netlify"],
|
|
66
|
+
source: (allow) => `import { handleEgressRequest } from "sandboxedjs/egress";
|
|
67
|
+
${allow}
|
|
68
|
+
export const config = { path: "/.netlify/functions/sandboxedjs-egress" };
|
|
69
|
+
|
|
70
|
+
export default (request) => handleEgressRequest(request, options);
|
|
71
|
+
`,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const NODE_SNIPPET = ` import { egressNodeHandler, EGRESS_PATH } from "sandboxedjs/egress";
|
|
76
|
+
// Before any body parser: the handler reads the request stream itself.
|
|
77
|
+
app.use(EGRESS_PATH, egressNodeHandler({ allow: ["api.example.com"] }));`;
|
|
78
|
+
|
|
79
|
+
async function exists(path) {
|
|
80
|
+
try { await access(path); return true; } catch { return false; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function detectTarget(root) {
|
|
84
|
+
for (const [name, target] of Object.entries(TARGETS)) {
|
|
85
|
+
for (const marker of target.detect) {
|
|
86
|
+
if (await exists(join(root, marker))) return name;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function init(argv) {
|
|
93
|
+
const root = argv.find((a) => !a.startsWith("-")) ?? process.cwd();
|
|
94
|
+
const at = argv.indexOf("--target");
|
|
95
|
+
const allowAt = argv.indexOf("--allow");
|
|
96
|
+
const allowed = allowAt === -1
|
|
97
|
+
? null
|
|
98
|
+
: (argv[allowAt + 1] ?? "").split(",").map((h) => h.trim()).filter(Boolean);
|
|
99
|
+
const name = at === -1 ? await detectTarget(root) : argv[at + 1];
|
|
100
|
+
if (!name || !TARGETS[name]) {
|
|
101
|
+
console.error(
|
|
102
|
+
name
|
|
103
|
+
? `sandboxedjs-egress init: unknown target ${name}`
|
|
104
|
+
: "sandboxedjs-egress init: could not tell what this project deploys to.",
|
|
105
|
+
);
|
|
106
|
+
console.error(`Targets: ${Object.keys(TARGETS).join(", ")} (--target <name>)`);
|
|
107
|
+
console.error(`A server of your own takes the handler directly:\n\n${NODE_SNIPPET}\n`);
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const target = TARGETS[name];
|
|
112
|
+
const path = join(root, target.file);
|
|
113
|
+
if (await exists(path)) {
|
|
114
|
+
console.log(`${target.file} is already there; leaving it alone.`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
/* An open proxy carries whatever credentials the container holds to wherever
|
|
118
|
+
* it is asked, so the list is the limit and its absence is said out loud. */
|
|
119
|
+
const allow = allowed?.length
|
|
120
|
+
? `\n/* The hosts this proxy will fetch, subdomains included. */\nconst options = { allow: ${JSON.stringify(allowed)} };\n`
|
|
121
|
+
: `\n/* Anything that can reach this route can make a request through it, carrying\n * whatever credentials the container holds. Narrow it before this is public:\n * const options = { allow: ["api.example.com"] };\n */\nconst options = {};\n`;
|
|
122
|
+
await mkdir(dirname(path), { recursive: true });
|
|
123
|
+
await writeFile(path, target.source(allow));
|
|
124
|
+
console.log(`Wrote ${target.file} (${name}).`);
|
|
125
|
+
if (!allowed?.length) console.log("It will fetch any host: set `allow` in that file before deploying it publicly.");
|
|
126
|
+
console.log("Deploy it, and containers in this site's pages find it with no further configuration.");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (args[0] === "init") {
|
|
130
|
+
await init(args.slice(1));
|
|
131
|
+
process.exit(process.exitCode ?? 0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
135
|
+
console.log(`sandboxedjs-egress — outbound HTTP for a container that runs in a browser
|
|
136
|
+
|
|
137
|
+
Usage:
|
|
138
|
+
sandboxedjs-egress [port=4181] [--allow host,host] run the proxy here
|
|
139
|
+
sandboxedjs-egress init [dir] [--target cloudflare|vercel|netlify] [--allow host,host]
|
|
140
|
+
add it to a deployment
|
|
141
|
+
`);
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
27
144
|
const at = args.indexOf("--allow");
|
|
28
145
|
const allow = at === -1 ? undefined : (args[at + 1] ?? "").split(",").map((h) => h.trim()).filter(Boolean);
|
|
29
146
|
const port = Number(args.find((a) => /^\d+$/.test(a)) ?? 4181);
|
package/dist/index.cjs
CHANGED
|
@@ -8424,12 +8424,17 @@ function isBrowser() {
|
|
|
8424
8424
|
}
|
|
8425
8425
|
function browserBlocked(hostname, message) {
|
|
8426
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. The container
|
|
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. The container looked on this origin for a proxy to make the request for it and found none: run \`npx sandboxedjs-egress init\` to add one to this deployment, or \`npx sandboxedjs-egress\` while developing, and it is used with no further configuration (network: { proxy } names one somewhere else) \u2014 or run the container on a server, where CORS does not apply.`
|
|
8428
8428
|
);
|
|
8429
8429
|
}
|
|
8430
8430
|
|
|
8431
8431
|
// src/net/discover-egress.ts
|
|
8432
8432
|
var EGRESS_PATH = "/__sandboxedjs__/egress";
|
|
8433
|
+
var EGRESS_PATHS = [
|
|
8434
|
+
EGRESS_PATH,
|
|
8435
|
+
`/api${EGRESS_PATH}`,
|
|
8436
|
+
"/.netlify/functions/sandboxedjs-egress"
|
|
8437
|
+
];
|
|
8433
8438
|
var EGRESS_MARKER = { sandboxedjs: "egress", protocol: 1 };
|
|
8434
8439
|
var CLI_PORT = 4181;
|
|
8435
8440
|
var found = null;
|
|
@@ -8451,7 +8456,7 @@ async function isEgress(url, fetchImpl) {
|
|
|
8451
8456
|
function candidates() {
|
|
8452
8457
|
const origin = globalThis.location?.origin;
|
|
8453
8458
|
const list = [];
|
|
8454
|
-
if (origin && /^https?:/.test(origin)) list.push(`${origin}${
|
|
8459
|
+
if (origin && /^https?:/.test(origin)) list.push(...EGRESS_PATHS.map((path) => `${origin}${path}`));
|
|
8455
8460
|
list.push(`http://127.0.0.1:${CLI_PORT}/`);
|
|
8456
8461
|
return list;
|
|
8457
8462
|
}
|
|
@@ -8459,10 +8464,10 @@ async function discoverEgress(fetchImpl = fetch) {
|
|
|
8459
8464
|
if (!isBrowser()) return null;
|
|
8460
8465
|
if (!found) {
|
|
8461
8466
|
found = (async () => {
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
return null;
|
|
8467
|
+
const list = candidates();
|
|
8468
|
+
const answers = await Promise.all(list.map((candidate) => isEgress(candidate, fetchImpl)));
|
|
8469
|
+
const at = answers.findIndex(Boolean);
|
|
8470
|
+
return at === -1 ? null : list[at];
|
|
8466
8471
|
})();
|
|
8467
8472
|
}
|
|
8468
8473
|
return await found;
|
|
@@ -21098,7 +21103,7 @@ var wheels_default = {
|
|
|
21098
21103
|
|
|
21099
21104
|
// package.json
|
|
21100
21105
|
var package_default = {
|
|
21101
|
-
version: "0.2.
|
|
21106
|
+
version: "0.2.8"};
|
|
21102
21107
|
|
|
21103
21108
|
// src/python/extension-abi.ts
|
|
21104
21109
|
var EXTENSION_ABI = {
|
package/dist/index.js
CHANGED
|
@@ -8407,12 +8407,17 @@ function isBrowser() {
|
|
|
8407
8407
|
}
|
|
8408
8408
|
function browserBlocked(hostname, message) {
|
|
8409
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. The container
|
|
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. The container looked on this origin for a proxy to make the request for it and found none: run \`npx sandboxedjs-egress init\` to add one to this deployment, or \`npx sandboxedjs-egress\` while developing, and it is used with no further configuration (network: { proxy } names one somewhere else) \u2014 or run the container on a server, where CORS does not apply.`
|
|
8411
8411
|
);
|
|
8412
8412
|
}
|
|
8413
8413
|
|
|
8414
8414
|
// src/net/discover-egress.ts
|
|
8415
8415
|
var EGRESS_PATH = "/__sandboxedjs__/egress";
|
|
8416
|
+
var EGRESS_PATHS = [
|
|
8417
|
+
EGRESS_PATH,
|
|
8418
|
+
`/api${EGRESS_PATH}`,
|
|
8419
|
+
"/.netlify/functions/sandboxedjs-egress"
|
|
8420
|
+
];
|
|
8416
8421
|
var EGRESS_MARKER = { sandboxedjs: "egress", protocol: 1 };
|
|
8417
8422
|
var CLI_PORT = 4181;
|
|
8418
8423
|
var found = null;
|
|
@@ -8434,7 +8439,7 @@ async function isEgress(url, fetchImpl) {
|
|
|
8434
8439
|
function candidates() {
|
|
8435
8440
|
const origin = globalThis.location?.origin;
|
|
8436
8441
|
const list = [];
|
|
8437
|
-
if (origin && /^https?:/.test(origin)) list.push(`${origin}${
|
|
8442
|
+
if (origin && /^https?:/.test(origin)) list.push(...EGRESS_PATHS.map((path) => `${origin}${path}`));
|
|
8438
8443
|
list.push(`http://127.0.0.1:${CLI_PORT}/`);
|
|
8439
8444
|
return list;
|
|
8440
8445
|
}
|
|
@@ -8442,10 +8447,10 @@ async function discoverEgress(fetchImpl = fetch) {
|
|
|
8442
8447
|
if (!isBrowser()) return null;
|
|
8443
8448
|
if (!found) {
|
|
8444
8449
|
found = (async () => {
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
return null;
|
|
8450
|
+
const list = candidates();
|
|
8451
|
+
const answers = await Promise.all(list.map((candidate) => isEgress(candidate, fetchImpl)));
|
|
8452
|
+
const at = answers.findIndex(Boolean);
|
|
8453
|
+
return at === -1 ? null : list[at];
|
|
8449
8454
|
})();
|
|
8450
8455
|
}
|
|
8451
8456
|
return await found;
|
|
@@ -21081,7 +21086,7 @@ var wheels_default = {
|
|
|
21081
21086
|
|
|
21082
21087
|
// package.json
|
|
21083
21088
|
var package_default = {
|
|
21084
|
-
version: "0.2.
|
|
21089
|
+
version: "0.2.8"};
|
|
21085
21090
|
|
|
21086
21091
|
// src/python/extension-abi.ts
|
|
21087
21092
|
var EXTENSION_ABI = {
|
package/dist/worker-entry.js
CHANGED
|
@@ -28281,12 +28281,17 @@ function isBrowser() {
|
|
|
28281
28281
|
}
|
|
28282
28282
|
function browserBlocked(hostname, message) {
|
|
28283
28283
|
return new Error(
|
|
28284
|
-
`${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. The container
|
|
28284
|
+
`${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. The container looked on this origin for a proxy to make the request for it and found none: run \`npx sandboxedjs-egress init\` to add one to this deployment, or \`npx sandboxedjs-egress\` while developing, and it is used with no further configuration (network: { proxy } names one somewhere else) \u2014 or run the container on a server, where CORS does not apply.`
|
|
28285
28285
|
);
|
|
28286
28286
|
}
|
|
28287
28287
|
|
|
28288
28288
|
// src/net/discover-egress.ts
|
|
28289
28289
|
var EGRESS_PATH = "/__sandboxedjs__/egress";
|
|
28290
|
+
var EGRESS_PATHS = [
|
|
28291
|
+
EGRESS_PATH,
|
|
28292
|
+
`/api${EGRESS_PATH}`,
|
|
28293
|
+
"/.netlify/functions/sandboxedjs-egress"
|
|
28294
|
+
];
|
|
28290
28295
|
var EGRESS_MARKER = { sandboxedjs: "egress", protocol: 1 };
|
|
28291
28296
|
var CLI_PORT = 4181;
|
|
28292
28297
|
var found = null;
|
|
@@ -28308,7 +28313,7 @@ async function isEgress(url, fetchImpl) {
|
|
|
28308
28313
|
function candidates() {
|
|
28309
28314
|
const origin = globalThis.location?.origin;
|
|
28310
28315
|
const list = [];
|
|
28311
|
-
if (origin && /^https?:/.test(origin)) list.push(`${origin}${
|
|
28316
|
+
if (origin && /^https?:/.test(origin)) list.push(...EGRESS_PATHS.map((path) => `${origin}${path}`));
|
|
28312
28317
|
list.push(`http://127.0.0.1:${CLI_PORT}/`);
|
|
28313
28318
|
return list;
|
|
28314
28319
|
}
|
|
@@ -28316,10 +28321,10 @@ async function discoverEgress(fetchImpl = fetch) {
|
|
|
28316
28321
|
if (!isBrowser()) return null;
|
|
28317
28322
|
if (!found) {
|
|
28318
28323
|
found = (async () => {
|
|
28319
|
-
|
|
28320
|
-
|
|
28321
|
-
|
|
28322
|
-
return null;
|
|
28324
|
+
const list = candidates();
|
|
28325
|
+
const answers = await Promise.all(list.map((candidate) => isEgress(candidate, fetchImpl)));
|
|
28326
|
+
const at2 = answers.findIndex(Boolean);
|
|
28327
|
+
return at2 === -1 ? null : list[at2];
|
|
28323
28328
|
})();
|
|
28324
28329
|
}
|
|
28325
28330
|
return await found;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sandboxedjs",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
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",
|