sandboxedjs 0.2.7 → 0.2.9

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.
@@ -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 looks for a proxy to make the request for it and found none: mount \`handleEgressRequest\` from "sandboxedjs/egress" at /__sandboxedjs__/egress on this origin, or run \`npx sandboxedjs-egress\`, and it is used without further configuration (network: { proxy } names one somewhere else) \u2014 or run the container on a server.`
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}${EGRESS_PATH}`);
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
- for (const candidate of candidates()) {
8463
- if (await isEgress(candidate, fetchImpl)) return candidate;
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.7"};
21106
+ version: "0.2.9"};
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 looks for a proxy to make the request for it and found none: mount \`handleEgressRequest\` from "sandboxedjs/egress" at /__sandboxedjs__/egress on this origin, or run \`npx sandboxedjs-egress\`, and it is used without further configuration (network: { proxy } names one somewhere else) \u2014 or run the container on a server.`
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}${EGRESS_PATH}`);
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
- for (const candidate of candidates()) {
8446
- if (await isEgress(candidate, fetchImpl)) return candidate;
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.7"};
21089
+ version: "0.2.9"};
21085
21090
 
21086
21091
  // src/python/extension-abi.ts
21087
21092
  var EXTENSION_ABI = {
@@ -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 looks for a proxy to make the request for it and found none: mount \`handleEgressRequest\` from "sandboxedjs/egress" at /__sandboxedjs__/egress on this origin, or run \`npx sandboxedjs-egress\`, and it is used without further configuration (network: { proxy } names one somewhere else) \u2014 or run the container on a server.`
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}${EGRESS_PATH}`);
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
- for (const candidate of candidates()) {
28320
- if (await isEgress(candidate, fetchImpl)) return candidate;
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.7",
3
+ "version": "0.2.9",
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",