sandboxedjs 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,7 +51,8 @@ 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:
54
+ Give it somewhere to send those requests instead. In development that is a
55
+ process:
55
56
 
56
57
  ```bash
57
58
  npx sandboxedjs-egress 4181 --allow api.openai.com,ollama.com
@@ -63,18 +64,48 @@ const box = await createContainer({
63
64
  });
64
65
  ```
65
66
 
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.
67
+ In an app that already has a server, mount it there instead — the container
68
+ then calls its own origin, so there is no second port and no CORS on the proxy
69
+ itself:
70
+
71
+ ```ts
72
+ import { egressNodeHandler } from "sandboxedjs/egress";
73
+ app.post("/egress", egressNodeHandler({ allow: ["ollama.com"] }));
74
+ // createContainer({ network: { allowOutbound: true, proxy: "/egress" } })
75
+ ```
76
+
77
+ A static site has no server, which is what the `Request` form is for — a
78
+ Cloudflare Pages Function, a Worker, a route handler:
79
+
80
+ ```ts
81
+ // functions/egress.ts
82
+ import { handleEgressRequest } from "sandboxedjs/egress";
83
+ export const onRequest = ({ request }) => handleEgressRequest(request, { allow: ["ollama.com"] });
84
+ ```
85
+
86
+ Every exit honours it — `curl`, `wget`, a guest's own `fetch`, and Python's
87
+ sockets — so it means the same thing whatever the project is written in.
88
+ Loopback stays inside the container, and the container's outbound policy still
89
+ applies before anything is handed over: a proxy widens what a *page* can reach,
90
+ not what the container may.
91
+
92
+ Anything that can reach the proxy can make requests through it carrying
93
+ whatever credentials the guest holds, so keep `--allow` set, keep it on
94
+ loopback in development, and put it behind your own authentication in
95
+ production.
96
+
97
+ ## Servers inside the container
98
+
99
+ A project split into a frontend and a backend calls `http://localhost:8000/api`
100
+ from the frontend. That address is true inside the container and is what the
101
+ project uses everywhere else, so it is left exactly as written: a script in
102
+ each previewed page rewrites loopback addresses to the preview's own origin,
103
+ and the service worker routes them by the port in the path.
104
+
105
+ That works between pages of the preview, and from another tab of the same
106
+ browser while the page holding the container is open. It does not work from
107
+ outside that browser — Postman, curl, another machine — because the container
108
+ is the tab. There is no server anywhere to reach.
78
109
 
79
110
  ## Why
80
111
 
@@ -21,82 +21,21 @@
21
21
  */
22
22
 
23
23
  import { createServer } from "node:http";
24
+ import { egressNodeHandler } from "../dist/egress.js";
24
25
 
25
26
  const args = process.argv.slice(2);
26
27
  const at = args.indexOf("--allow");
27
- const allowed = at === -1 ? null : (args[at + 1] ?? "").split(",").map((h) => h.trim()).filter(Boolean);
28
+ const allow = at === -1 ? undefined : (args[at + 1] ?? "").split(",").map((h) => h.trim()).filter(Boolean);
28
29
  const port = Number(args.find((a) => /^\d+$/.test(a)) ?? 4181);
29
30
 
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);
31
+ const handle = egressNodeHandler({ ...(allow ? { allow } : {}) });
46
32
 
47
33
  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
- }
34
+ const started = Date.now();
35
+ await handle(req, res);
36
+ console.log(`${req.method} -> ${res.statusCode} (${Date.now() - started}ms)`);
99
37
  }).listen(port, "127.0.0.1", () => {
100
- console.log(`sandboxedjs-egress on http://localhost:${port}${allowed ? ` (allowing ${allowed.join(", ")})` : ""}`);
38
+ console.log(`sandboxedjs-egress on http://localhost:${port}${allow ? ` (allowing ${allow.join(", ")})` : ""}`);
101
39
  console.log(` createContainer({ network: { allowOutbound: true, proxy: "http://localhost:${port}" } })`);
40
+ console.log(" or mount it in your own app: import { egressNodeHandler } from \"sandboxedjs/egress\"");
102
41
  });
package/dist/agent.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as Container } from './container-DJfFjito.cjs';
2
- import './contracts-CXZ_25-O.cjs';
1
+ import { C as Container } from './container-BQx27_d-.cjs';
2
+ import './contracts-BHo4LdY2.cjs';
3
3
 
4
4
  /**
5
5
  * Structural copies of the LangChain Deep Agents backend contract.
package/dist/agent.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as Container } from './container-C16pCOWC.js';
2
- import './contracts-CXZ_25-O.js';
1
+ import { C as Container } from './container-D-e5SMXQ.js';
2
+ import './contracts-BHo4LdY2.js';
3
3
 
4
4
  /**
5
5
  * Structural copies of the LangChain Deep Agents backend contract.
@@ -1,4 +1,4 @@
1
- import { V as Vfs, C as Cred, f as RuntimePod, O as OutboundPolicy, D as DirEntry, p as Stats } from './contracts-CXZ_25-O.js';
1
+ import { V as Vfs, C as Cred, f as RuntimePod, O as OutboundPolicy, D as DirEntry, p as Stats } from './contracts-BHo4LdY2.cjs';
2
2
 
3
3
  /**
4
4
  * Byte streams for stdin/stdout/stderr, pipelines and redirections.
@@ -1,4 +1,4 @@
1
- import { V as Vfs, C as Cred, f as RuntimePod, O as OutboundPolicy, D as DirEntry, p as Stats } from './contracts-CXZ_25-O.cjs';
1
+ import { V as Vfs, C as Cred, f as RuntimePod, O as OutboundPolicy, D as DirEntry, p as Stats } from './contracts-BHo4LdY2.js';
2
2
 
3
3
  /**
4
4
  * Byte streams for stdin/stdout/stderr, pipelines and redirections.
@@ -114,6 +114,15 @@ interface OutboundPolicy {
114
114
  allowOutbound: boolean;
115
115
  /** When outbound is on, the hosts it may reach (subdomains included). `null` means any. */
116
116
  allowedHosts: string[] | null;
117
+ /**
118
+ * A URL that performs this container's outbound requests on its behalf.
119
+ *
120
+ * Carried with the policy rather than beside it, because every exit has to
121
+ * honour it: `curl`, the Python egress and a guest's own `fetch` all leave
122
+ * the same way, and a proxy that covered only some of them would be a
123
+ * setting whose meaning depended on which language the guest was written in.
124
+ */
125
+ proxy?: string;
117
126
  }
118
127
 
119
128
  /**
@@ -114,6 +114,15 @@ interface OutboundPolicy {
114
114
  allowOutbound: boolean;
115
115
  /** When outbound is on, the hosts it may reach (subdomains included). `null` means any. */
116
116
  allowedHosts: string[] | null;
117
+ /**
118
+ * A URL that performs this container's outbound requests on its behalf.
119
+ *
120
+ * Carried with the policy rather than beside it, because every exit has to
121
+ * honour it: `curl`, the Python egress and a guest's own `fetch` all leave
122
+ * the same way, and a proxy that covered only some of them would be a
123
+ * setting whose meaning depended on which language the guest was written in.
124
+ */
125
+ proxy?: string;
117
126
  }
118
127
 
119
128
  /**
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ // src/net/proxy-fetch.ts
4
+ function toBase64(bytes) {
5
+ let text = "";
6
+ for (let at = 0; at < bytes.length; at += 32768) {
7
+ text += String.fromCharCode(...bytes.subarray(at, at + 32768));
8
+ }
9
+ return btoa(text);
10
+ }
11
+ function fromBase64(text) {
12
+ const binary = atob(text);
13
+ const bytes = new Uint8Array(binary.length);
14
+ for (let at = 0; at < binary.length; at += 1) bytes[at] = binary.charCodeAt(at);
15
+ return bytes;
16
+ }
17
+
18
+ // src/net/egress-handler.ts
19
+ var HOP = /* @__PURE__ */ new Set([
20
+ "connection",
21
+ "keep-alive",
22
+ "proxy-authenticate",
23
+ "proxy-authorization",
24
+ "te",
25
+ "trailer",
26
+ "transfer-encoding",
27
+ "upgrade",
28
+ "host",
29
+ "content-length"
30
+ ]);
31
+ var HEADERS = {
32
+ "access-control-allow-origin": "*",
33
+ "access-control-allow-methods": "POST, OPTIONS",
34
+ "access-control-allow-headers": "content-type",
35
+ "access-control-max-age": "86400",
36
+ "cross-origin-resource-policy": "cross-origin"
37
+ };
38
+ var refuse = (status, message) => new Response(message, { status, headers: { ...HEADERS, "content-type": "text/plain; charset=utf-8" } });
39
+ async function handleEgressRequest(request, options = {}) {
40
+ if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: HEADERS });
41
+ if (request.method !== "POST") return refuse(405, "POST a JSON request here.");
42
+ let payload;
43
+ try {
44
+ payload = await request.json();
45
+ } catch (error) {
46
+ return refuse(400, `could not read the request: ${error instanceof Error ? error.message : String(error)}`);
47
+ }
48
+ let target;
49
+ try {
50
+ target = new URL(String(payload.url));
51
+ } catch {
52
+ return refuse(400, `not a URL: ${payload.url}`);
53
+ }
54
+ if (target.protocol !== "http:" && target.protocol !== "https:") {
55
+ return refuse(400, `${target.protocol} is not a protocol this proxy speaks`);
56
+ }
57
+ const allow = options.allow;
58
+ if (allow && !allow.some((host) => target.hostname === host || target.hostname.endsWith(`.${host}`))) {
59
+ return refuse(403, `${target.hostname} is not in this proxy's allow list`);
60
+ }
61
+ const headers = {};
62
+ for (const [name, value] of Object.entries(payload.headers ?? {})) {
63
+ if (!HOP.has(name.toLowerCase())) headers[name] = value;
64
+ }
65
+ const perform = options.fetch ?? fetch;
66
+ let answer;
67
+ try {
68
+ answer = await perform(target.toString(), {
69
+ method: payload.method ?? "GET",
70
+ headers,
71
+ ...payload.body ? { body: fromBase64(payload.body) } : {},
72
+ redirect: "follow"
73
+ });
74
+ } catch (error) {
75
+ return refuse(502, `the request to ${target.hostname} failed: ${error instanceof Error ? error.message : String(error)}`);
76
+ }
77
+ const back = {};
78
+ answer.headers.forEach((value, name) => {
79
+ if (!HOP.has(name.toLowerCase())) back[name] = value;
80
+ });
81
+ const body = new Uint8Array(await answer.arrayBuffer());
82
+ return new Response(
83
+ JSON.stringify({ status: answer.status, statusText: answer.statusText, headers: back, body: toBase64(body) }),
84
+ { headers: { ...HEADERS, "content-type": "application/json" } }
85
+ );
86
+ }
87
+ function egressNodeHandler(options = {}) {
88
+ return async (request, response) => {
89
+ const chunks = [];
90
+ await new Promise((done, fail) => {
91
+ request.on("data", (chunk) => chunks.push(chunk));
92
+ request.on("end", () => done());
93
+ request.on("error", (error) => fail(error));
94
+ });
95
+ let total = 0;
96
+ for (const chunk of chunks) total += chunk.length;
97
+ const body = new Uint8Array(total);
98
+ let at = 0;
99
+ for (const chunk of chunks) {
100
+ body.set(chunk, at);
101
+ at += chunk.length;
102
+ }
103
+ const headers = {};
104
+ for (const [name, value] of Object.entries(request.headers)) {
105
+ if (typeof value === "string") headers[name] = value;
106
+ }
107
+ const answer = await handleEgressRequest(
108
+ new Request("http://egress.invalid/", {
109
+ method: request.method ?? "GET",
110
+ headers,
111
+ ...body.length && request.method !== "GET" && request.method !== "HEAD" ? { body } : {}
112
+ }),
113
+ options
114
+ );
115
+ const out = {};
116
+ answer.headers.forEach((value, name) => {
117
+ out[name] = value;
118
+ });
119
+ response.writeHead(answer.status, out);
120
+ response.end(Buffer.from(await answer.arrayBuffer()));
121
+ };
122
+ }
123
+
124
+ exports.egressNodeHandler = egressNodeHandler;
125
+ exports.handleEgressRequest = handleEgressRequest;
126
+ //# sourceMappingURL=egress.cjs.map
127
+ //# sourceMappingURL=egress.cjs.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The proxy, as something a host app can mount instead of run.
3
+ *
4
+ * `sandboxedjs-egress` is a whole process, which suits a terminal and suits
5
+ * nothing else. A host app usually already has a server — an Express route, a
6
+ * Cloudflare Pages Function, a Worker — and mounting the proxy there is better
7
+ * than standing another one beside it: the container then calls its own
8
+ * origin, so there is no second port to start, no CORS on the proxy itself,
9
+ * and one thing to deploy rather than two.
10
+ *
11
+ * A static site has no server at all, which is the case this exists for. Those
12
+ * hosts all take a function of a `Request`, so that is the shape here, with a
13
+ * Node adapter for hosts that predate it.
14
+ *
15
+ * // Cloudflare Pages: functions/egress.ts
16
+ * import { handleEgressRequest } from "sandboxedjs/egress";
17
+ * export const onRequest = ({ request }) =>
18
+ * handleEgressRequest(request, { allow: ["ollama.com"] });
19
+ *
20
+ * Anything that can reach this can make requests through it, carrying whatever
21
+ * credentials the guest holds. `allow` is the limit on where those go, and on
22
+ * a public deployment it should be set and the route should be behind whatever
23
+ * authentication the rest of the app uses.
24
+ */
25
+ interface EgressOptions {
26
+ /** Hosts this proxy will fetch, subdomains included. Unset means any. */
27
+ allow?: string[];
28
+ /** The `fetch` used to make the real request. */
29
+ fetch?: typeof globalThis.fetch;
30
+ }
31
+ /** Answer one proxied request. Safe to mount wherever a `Request` arrives. */
32
+ declare function handleEgressRequest(request: Request, options?: EgressOptions): Promise<Response>;
33
+ /** Minimal shapes of Node's request and response, so this needs no `@types/node`. */
34
+ interface NodeRequest {
35
+ method?: string;
36
+ url?: string;
37
+ headers: Record<string, string | string[] | undefined>;
38
+ on(event: string, listener: (chunk?: unknown) => void): unknown;
39
+ }
40
+ interface NodeResponse {
41
+ writeHead(status: number, headers: Record<string, string>): unknown;
42
+ end(body?: unknown): unknown;
43
+ }
44
+ /**
45
+ * The same handler for Express and `node:http`.
46
+ *
47
+ * app.post("/egress", egressNodeHandler({ allow: ["ollama.com"] }));
48
+ */
49
+ declare function egressNodeHandler(options?: EgressOptions): (request: NodeRequest, response: NodeResponse) => Promise<void>;
50
+
51
+ export { type EgressOptions, egressNodeHandler, handleEgressRequest };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The proxy, as something a host app can mount instead of run.
3
+ *
4
+ * `sandboxedjs-egress` is a whole process, which suits a terminal and suits
5
+ * nothing else. A host app usually already has a server — an Express route, a
6
+ * Cloudflare Pages Function, a Worker — and mounting the proxy there is better
7
+ * than standing another one beside it: the container then calls its own
8
+ * origin, so there is no second port to start, no CORS on the proxy itself,
9
+ * and one thing to deploy rather than two.
10
+ *
11
+ * A static site has no server at all, which is the case this exists for. Those
12
+ * hosts all take a function of a `Request`, so that is the shape here, with a
13
+ * Node adapter for hosts that predate it.
14
+ *
15
+ * // Cloudflare Pages: functions/egress.ts
16
+ * import { handleEgressRequest } from "sandboxedjs/egress";
17
+ * export const onRequest = ({ request }) =>
18
+ * handleEgressRequest(request, { allow: ["ollama.com"] });
19
+ *
20
+ * Anything that can reach this can make requests through it, carrying whatever
21
+ * credentials the guest holds. `allow` is the limit on where those go, and on
22
+ * a public deployment it should be set and the route should be behind whatever
23
+ * authentication the rest of the app uses.
24
+ */
25
+ interface EgressOptions {
26
+ /** Hosts this proxy will fetch, subdomains included. Unset means any. */
27
+ allow?: string[];
28
+ /** The `fetch` used to make the real request. */
29
+ fetch?: typeof globalThis.fetch;
30
+ }
31
+ /** Answer one proxied request. Safe to mount wherever a `Request` arrives. */
32
+ declare function handleEgressRequest(request: Request, options?: EgressOptions): Promise<Response>;
33
+ /** Minimal shapes of Node's request and response, so this needs no `@types/node`. */
34
+ interface NodeRequest {
35
+ method?: string;
36
+ url?: string;
37
+ headers: Record<string, string | string[] | undefined>;
38
+ on(event: string, listener: (chunk?: unknown) => void): unknown;
39
+ }
40
+ interface NodeResponse {
41
+ writeHead(status: number, headers: Record<string, string>): unknown;
42
+ end(body?: unknown): unknown;
43
+ }
44
+ /**
45
+ * The same handler for Express and `node:http`.
46
+ *
47
+ * app.post("/egress", egressNodeHandler({ allow: ["ollama.com"] }));
48
+ */
49
+ declare function egressNodeHandler(options?: EgressOptions): (request: NodeRequest, response: NodeResponse) => Promise<void>;
50
+
51
+ export { type EgressOptions, egressNodeHandler, handleEgressRequest };
package/dist/egress.js ADDED
@@ -0,0 +1,124 @@
1
+ // src/net/proxy-fetch.ts
2
+ function toBase64(bytes) {
3
+ let text = "";
4
+ for (let at = 0; at < bytes.length; at += 32768) {
5
+ text += String.fromCharCode(...bytes.subarray(at, at + 32768));
6
+ }
7
+ return btoa(text);
8
+ }
9
+ function fromBase64(text) {
10
+ const binary = atob(text);
11
+ const bytes = new Uint8Array(binary.length);
12
+ for (let at = 0; at < binary.length; at += 1) bytes[at] = binary.charCodeAt(at);
13
+ return bytes;
14
+ }
15
+
16
+ // src/net/egress-handler.ts
17
+ var HOP = /* @__PURE__ */ new Set([
18
+ "connection",
19
+ "keep-alive",
20
+ "proxy-authenticate",
21
+ "proxy-authorization",
22
+ "te",
23
+ "trailer",
24
+ "transfer-encoding",
25
+ "upgrade",
26
+ "host",
27
+ "content-length"
28
+ ]);
29
+ var HEADERS = {
30
+ "access-control-allow-origin": "*",
31
+ "access-control-allow-methods": "POST, OPTIONS",
32
+ "access-control-allow-headers": "content-type",
33
+ "access-control-max-age": "86400",
34
+ "cross-origin-resource-policy": "cross-origin"
35
+ };
36
+ var refuse = (status, message) => new Response(message, { status, headers: { ...HEADERS, "content-type": "text/plain; charset=utf-8" } });
37
+ async function handleEgressRequest(request, options = {}) {
38
+ if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: HEADERS });
39
+ if (request.method !== "POST") return refuse(405, "POST a JSON request here.");
40
+ let payload;
41
+ try {
42
+ payload = await request.json();
43
+ } catch (error) {
44
+ return refuse(400, `could not read the request: ${error instanceof Error ? error.message : String(error)}`);
45
+ }
46
+ let target;
47
+ try {
48
+ target = new URL(String(payload.url));
49
+ } catch {
50
+ return refuse(400, `not a URL: ${payload.url}`);
51
+ }
52
+ if (target.protocol !== "http:" && target.protocol !== "https:") {
53
+ return refuse(400, `${target.protocol} is not a protocol this proxy speaks`);
54
+ }
55
+ const allow = options.allow;
56
+ if (allow && !allow.some((host) => target.hostname === host || target.hostname.endsWith(`.${host}`))) {
57
+ return refuse(403, `${target.hostname} is not in this proxy's allow list`);
58
+ }
59
+ const headers = {};
60
+ for (const [name, value] of Object.entries(payload.headers ?? {})) {
61
+ if (!HOP.has(name.toLowerCase())) headers[name] = value;
62
+ }
63
+ const perform = options.fetch ?? fetch;
64
+ let answer;
65
+ try {
66
+ answer = await perform(target.toString(), {
67
+ method: payload.method ?? "GET",
68
+ headers,
69
+ ...payload.body ? { body: fromBase64(payload.body) } : {},
70
+ redirect: "follow"
71
+ });
72
+ } catch (error) {
73
+ return refuse(502, `the request to ${target.hostname} failed: ${error instanceof Error ? error.message : String(error)}`);
74
+ }
75
+ const back = {};
76
+ answer.headers.forEach((value, name) => {
77
+ if (!HOP.has(name.toLowerCase())) back[name] = value;
78
+ });
79
+ const body = new Uint8Array(await answer.arrayBuffer());
80
+ return new Response(
81
+ JSON.stringify({ status: answer.status, statusText: answer.statusText, headers: back, body: toBase64(body) }),
82
+ { headers: { ...HEADERS, "content-type": "application/json" } }
83
+ );
84
+ }
85
+ function egressNodeHandler(options = {}) {
86
+ return async (request, response) => {
87
+ const chunks = [];
88
+ await new Promise((done, fail) => {
89
+ request.on("data", (chunk) => chunks.push(chunk));
90
+ request.on("end", () => done());
91
+ request.on("error", (error) => fail(error));
92
+ });
93
+ let total = 0;
94
+ for (const chunk of chunks) total += chunk.length;
95
+ const body = new Uint8Array(total);
96
+ let at = 0;
97
+ for (const chunk of chunks) {
98
+ body.set(chunk, at);
99
+ at += chunk.length;
100
+ }
101
+ const headers = {};
102
+ for (const [name, value] of Object.entries(request.headers)) {
103
+ if (typeof value === "string") headers[name] = value;
104
+ }
105
+ const answer = await handleEgressRequest(
106
+ new Request("http://egress.invalid/", {
107
+ method: request.method ?? "GET",
108
+ headers,
109
+ ...body.length && request.method !== "GET" && request.method !== "HEAD" ? { body } : {}
110
+ }),
111
+ options
112
+ );
113
+ const out = {};
114
+ answer.headers.forEach((value, name) => {
115
+ out[name] = value;
116
+ });
117
+ response.writeHead(answer.status, out);
118
+ response.end(Buffer.from(await answer.arrayBuffer()));
119
+ };
120
+ }
121
+
122
+ export { egressNodeHandler, handleEgressRequest };
123
+ //# sourceMappingURL=egress.js.map
124
+ //# sourceMappingURL=egress.js.map