sandboxedjs 0.2.5 → 0.2.7
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/dist/browser-host.cjs +137 -0
- package/dist/browser-host.d.cts +12 -0
- package/dist/browser-host.d.ts +12 -0
- package/dist/browser-host.js +137 -0
- package/dist/egress.cjs +12 -1
- package/dist/egress.d.cts +33 -1
- package/dist/egress.d.ts +33 -1
- package/dist/egress.js +11 -2
- package/dist/index.cjs +245 -129
- package/dist/index.js +245 -129
- package/dist/python-abi.cjs +5 -1
- package/dist/python-abi.js +5 -1
- package/dist/worker-entry.js +58 -17
- package/package.json +1 -1
package/dist/browser-host.cjs
CHANGED
|
@@ -4,6 +4,138 @@ var http = require('http');
|
|
|
4
4
|
var promises = require('fs/promises');
|
|
5
5
|
var path = require('path');
|
|
6
6
|
|
|
7
|
+
// src/hosting/browser-host.ts
|
|
8
|
+
|
|
9
|
+
// src/net/proxy-fetch.ts
|
|
10
|
+
function toBase64(bytes) {
|
|
11
|
+
let text = "";
|
|
12
|
+
for (let at = 0; at < bytes.length; at += 32768) {
|
|
13
|
+
text += String.fromCharCode(...bytes.subarray(at, at + 32768));
|
|
14
|
+
}
|
|
15
|
+
return btoa(text);
|
|
16
|
+
}
|
|
17
|
+
function fromBase64(text) {
|
|
18
|
+
const binary = atob(text);
|
|
19
|
+
const bytes = new Uint8Array(binary.length);
|
|
20
|
+
for (let at = 0; at < binary.length; at += 1) bytes[at] = binary.charCodeAt(at);
|
|
21
|
+
return bytes;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/net/discover-egress.ts
|
|
25
|
+
var EGRESS_PATH = "/__sandboxedjs__/egress";
|
|
26
|
+
var EGRESS_MARKER = { sandboxedjs: "egress", protocol: 1 };
|
|
27
|
+
|
|
28
|
+
// src/net/egress-handler.ts
|
|
29
|
+
var HOP = /* @__PURE__ */ new Set([
|
|
30
|
+
"connection",
|
|
31
|
+
"keep-alive",
|
|
32
|
+
"proxy-authenticate",
|
|
33
|
+
"proxy-authorization",
|
|
34
|
+
"te",
|
|
35
|
+
"trailer",
|
|
36
|
+
"transfer-encoding",
|
|
37
|
+
"upgrade",
|
|
38
|
+
"host",
|
|
39
|
+
"content-length"
|
|
40
|
+
]);
|
|
41
|
+
var HEADERS = {
|
|
42
|
+
"access-control-allow-origin": "*",
|
|
43
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
44
|
+
"access-control-allow-headers": "content-type",
|
|
45
|
+
"access-control-max-age": "86400",
|
|
46
|
+
"cross-origin-resource-policy": "cross-origin"
|
|
47
|
+
};
|
|
48
|
+
var refuse = (status, message) => new Response(message, { status, headers: { ...HEADERS, "content-type": "text/plain; charset=utf-8" } });
|
|
49
|
+
async function handleEgressRequest(request, options = {}) {
|
|
50
|
+
if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: HEADERS });
|
|
51
|
+
if (request.method === "GET" || request.method === "HEAD") {
|
|
52
|
+
return new Response(request.method === "HEAD" ? null : JSON.stringify(EGRESS_MARKER), {
|
|
53
|
+
headers: { ...HEADERS, "content-type": "application/json" }
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (request.method !== "POST") return refuse(405, "POST a JSON request here.");
|
|
57
|
+
let payload;
|
|
58
|
+
try {
|
|
59
|
+
payload = await request.json();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return refuse(400, `could not read the request: ${error instanceof Error ? error.message : String(error)}`);
|
|
62
|
+
}
|
|
63
|
+
let target;
|
|
64
|
+
try {
|
|
65
|
+
target = new URL(String(payload.url));
|
|
66
|
+
} catch {
|
|
67
|
+
return refuse(400, `not a URL: ${payload.url}`);
|
|
68
|
+
}
|
|
69
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") {
|
|
70
|
+
return refuse(400, `${target.protocol} is not a protocol this proxy speaks`);
|
|
71
|
+
}
|
|
72
|
+
const allow = options.allow;
|
|
73
|
+
if (allow && !allow.some((host) => target.hostname === host || target.hostname.endsWith(`.${host}`))) {
|
|
74
|
+
return refuse(403, `${target.hostname} is not in this proxy's allow list`);
|
|
75
|
+
}
|
|
76
|
+
const headers = {};
|
|
77
|
+
for (const [name, value] of Object.entries(payload.headers ?? {})) {
|
|
78
|
+
if (!HOP.has(name.toLowerCase())) headers[name] = value;
|
|
79
|
+
}
|
|
80
|
+
const perform = options.fetch ?? fetch;
|
|
81
|
+
let answer;
|
|
82
|
+
try {
|
|
83
|
+
answer = await perform(target.toString(), {
|
|
84
|
+
method: payload.method ?? "GET",
|
|
85
|
+
headers,
|
|
86
|
+
...payload.body ? { body: fromBase64(payload.body) } : {},
|
|
87
|
+
redirect: "follow"
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return refuse(502, `the request to ${target.hostname} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
91
|
+
}
|
|
92
|
+
const back = {};
|
|
93
|
+
answer.headers.forEach((value, name) => {
|
|
94
|
+
if (!HOP.has(name.toLowerCase())) back[name] = value;
|
|
95
|
+
});
|
|
96
|
+
const body = new Uint8Array(await answer.arrayBuffer());
|
|
97
|
+
return new Response(
|
|
98
|
+
JSON.stringify({ status: answer.status, statusText: answer.statusText, headers: back, body: toBase64(body) }),
|
|
99
|
+
{ headers: { ...HEADERS, "content-type": "application/json" } }
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
function egressNodeHandler(options = {}) {
|
|
103
|
+
return async (request, response) => {
|
|
104
|
+
const chunks = [];
|
|
105
|
+
await new Promise((done, fail) => {
|
|
106
|
+
request.on("data", (chunk) => chunks.push(chunk));
|
|
107
|
+
request.on("end", () => done());
|
|
108
|
+
request.on("error", (error) => fail(error));
|
|
109
|
+
});
|
|
110
|
+
let total = 0;
|
|
111
|
+
for (const chunk of chunks) total += chunk.length;
|
|
112
|
+
const body = new Uint8Array(total);
|
|
113
|
+
let at = 0;
|
|
114
|
+
for (const chunk of chunks) {
|
|
115
|
+
body.set(chunk, at);
|
|
116
|
+
at += chunk.length;
|
|
117
|
+
}
|
|
118
|
+
const headers = {};
|
|
119
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
120
|
+
if (typeof value === "string") headers[name] = value;
|
|
121
|
+
}
|
|
122
|
+
const answer = await handleEgressRequest(
|
|
123
|
+
new Request("http://egress.invalid/", {
|
|
124
|
+
method: request.method ?? "GET",
|
|
125
|
+
headers,
|
|
126
|
+
...body.length && request.method !== "GET" && request.method !== "HEAD" ? { body } : {}
|
|
127
|
+
}),
|
|
128
|
+
options
|
|
129
|
+
);
|
|
130
|
+
const out = {};
|
|
131
|
+
answer.headers.forEach((value, name) => {
|
|
132
|
+
out[name] = value;
|
|
133
|
+
});
|
|
134
|
+
response.writeHead(answer.status, out);
|
|
135
|
+
response.end(Buffer.from(await answer.arrayBuffer()));
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
7
139
|
// src/hosting/browser-host.ts
|
|
8
140
|
var browserIsolationHeaders = Object.freeze({
|
|
9
141
|
"Cross-Origin-Opener-Policy": "same-origin",
|
|
@@ -33,7 +165,12 @@ async function serveBrowserApp(options) {
|
|
|
33
165
|
const rel = path.relative(root, path$1);
|
|
34
166
|
return rel !== ".." && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel);
|
|
35
167
|
};
|
|
168
|
+
const egress = options.egress === false ? null : egressNodeHandler({ ...options.egress?.allow ? { allow: options.egress.allow } : {} });
|
|
36
169
|
const handle = async (req, res) => {
|
|
170
|
+
if (egress && (req.url ?? "").split("?")[0] === EGRESS_PATH) {
|
|
171
|
+
await egress(req, res);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
37
174
|
for (const [name, value] of Object.entries(browserIsolationHeaders)) res.setHeader(name, value);
|
|
38
175
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
39
176
|
res.setHeader("Cache-Control", "no-cache");
|
package/dist/browser-host.d.cts
CHANGED
|
@@ -16,6 +16,18 @@ declare function serveBrowserApp(options: {
|
|
|
16
16
|
directory: string;
|
|
17
17
|
port?: number;
|
|
18
18
|
hostname?: string;
|
|
19
|
+
/**
|
|
20
|
+
* The outbound proxy this host serves for the containers it hosts.
|
|
21
|
+
*
|
|
22
|
+
* On by default, because a container in a browser cannot reach an API that
|
|
23
|
+
* sends no CORS headers without one, and a host that serves the page is
|
|
24
|
+
* already the right place to put it -- same origin, nothing else to start,
|
|
25
|
+
* and the container finds it without being configured. `allow` narrows what
|
|
26
|
+
* it will fetch; `false` turns it off for a host that wants none.
|
|
27
|
+
*/
|
|
28
|
+
egress?: false | {
|
|
29
|
+
allow?: string[];
|
|
30
|
+
};
|
|
19
31
|
}): Promise<{
|
|
20
32
|
server: Server;
|
|
21
33
|
url: string;
|
package/dist/browser-host.d.ts
CHANGED
|
@@ -16,6 +16,18 @@ declare function serveBrowserApp(options: {
|
|
|
16
16
|
directory: string;
|
|
17
17
|
port?: number;
|
|
18
18
|
hostname?: string;
|
|
19
|
+
/**
|
|
20
|
+
* The outbound proxy this host serves for the containers it hosts.
|
|
21
|
+
*
|
|
22
|
+
* On by default, because a container in a browser cannot reach an API that
|
|
23
|
+
* sends no CORS headers without one, and a host that serves the page is
|
|
24
|
+
* already the right place to put it -- same origin, nothing else to start,
|
|
25
|
+
* and the container finds it without being configured. `allow` narrows what
|
|
26
|
+
* it will fetch; `false` turns it off for a host that wants none.
|
|
27
|
+
*/
|
|
28
|
+
egress?: false | {
|
|
29
|
+
allow?: string[];
|
|
30
|
+
};
|
|
19
31
|
}): Promise<{
|
|
20
32
|
server: Server;
|
|
21
33
|
url: string;
|
package/dist/browser-host.js
CHANGED
|
@@ -2,6 +2,138 @@ import { createServer } from 'http';
|
|
|
2
2
|
import { realpath, stat, readFile } from 'fs/promises';
|
|
3
3
|
import { resolve, extname, relative, sep, isAbsolute } from 'path';
|
|
4
4
|
|
|
5
|
+
// src/hosting/browser-host.ts
|
|
6
|
+
|
|
7
|
+
// src/net/proxy-fetch.ts
|
|
8
|
+
function toBase64(bytes) {
|
|
9
|
+
let text = "";
|
|
10
|
+
for (let at = 0; at < bytes.length; at += 32768) {
|
|
11
|
+
text += String.fromCharCode(...bytes.subarray(at, at + 32768));
|
|
12
|
+
}
|
|
13
|
+
return btoa(text);
|
|
14
|
+
}
|
|
15
|
+
function fromBase64(text) {
|
|
16
|
+
const binary = atob(text);
|
|
17
|
+
const bytes = new Uint8Array(binary.length);
|
|
18
|
+
for (let at = 0; at < binary.length; at += 1) bytes[at] = binary.charCodeAt(at);
|
|
19
|
+
return bytes;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/net/discover-egress.ts
|
|
23
|
+
var EGRESS_PATH = "/__sandboxedjs__/egress";
|
|
24
|
+
var EGRESS_MARKER = { sandboxedjs: "egress", protocol: 1 };
|
|
25
|
+
|
|
26
|
+
// src/net/egress-handler.ts
|
|
27
|
+
var HOP = /* @__PURE__ */ new Set([
|
|
28
|
+
"connection",
|
|
29
|
+
"keep-alive",
|
|
30
|
+
"proxy-authenticate",
|
|
31
|
+
"proxy-authorization",
|
|
32
|
+
"te",
|
|
33
|
+
"trailer",
|
|
34
|
+
"transfer-encoding",
|
|
35
|
+
"upgrade",
|
|
36
|
+
"host",
|
|
37
|
+
"content-length"
|
|
38
|
+
]);
|
|
39
|
+
var HEADERS = {
|
|
40
|
+
"access-control-allow-origin": "*",
|
|
41
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
42
|
+
"access-control-allow-headers": "content-type",
|
|
43
|
+
"access-control-max-age": "86400",
|
|
44
|
+
"cross-origin-resource-policy": "cross-origin"
|
|
45
|
+
};
|
|
46
|
+
var refuse = (status, message) => new Response(message, { status, headers: { ...HEADERS, "content-type": "text/plain; charset=utf-8" } });
|
|
47
|
+
async function handleEgressRequest(request, options = {}) {
|
|
48
|
+
if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: HEADERS });
|
|
49
|
+
if (request.method === "GET" || request.method === "HEAD") {
|
|
50
|
+
return new Response(request.method === "HEAD" ? null : JSON.stringify(EGRESS_MARKER), {
|
|
51
|
+
headers: { ...HEADERS, "content-type": "application/json" }
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (request.method !== "POST") return refuse(405, "POST a JSON request here.");
|
|
55
|
+
let payload;
|
|
56
|
+
try {
|
|
57
|
+
payload = await request.json();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return refuse(400, `could not read the request: ${error instanceof Error ? error.message : String(error)}`);
|
|
60
|
+
}
|
|
61
|
+
let target;
|
|
62
|
+
try {
|
|
63
|
+
target = new URL(String(payload.url));
|
|
64
|
+
} catch {
|
|
65
|
+
return refuse(400, `not a URL: ${payload.url}`);
|
|
66
|
+
}
|
|
67
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") {
|
|
68
|
+
return refuse(400, `${target.protocol} is not a protocol this proxy speaks`);
|
|
69
|
+
}
|
|
70
|
+
const allow = options.allow;
|
|
71
|
+
if (allow && !allow.some((host) => target.hostname === host || target.hostname.endsWith(`.${host}`))) {
|
|
72
|
+
return refuse(403, `${target.hostname} is not in this proxy's allow list`);
|
|
73
|
+
}
|
|
74
|
+
const headers = {};
|
|
75
|
+
for (const [name, value] of Object.entries(payload.headers ?? {})) {
|
|
76
|
+
if (!HOP.has(name.toLowerCase())) headers[name] = value;
|
|
77
|
+
}
|
|
78
|
+
const perform = options.fetch ?? fetch;
|
|
79
|
+
let answer;
|
|
80
|
+
try {
|
|
81
|
+
answer = await perform(target.toString(), {
|
|
82
|
+
method: payload.method ?? "GET",
|
|
83
|
+
headers,
|
|
84
|
+
...payload.body ? { body: fromBase64(payload.body) } : {},
|
|
85
|
+
redirect: "follow"
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return refuse(502, `the request to ${target.hostname} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
89
|
+
}
|
|
90
|
+
const back = {};
|
|
91
|
+
answer.headers.forEach((value, name) => {
|
|
92
|
+
if (!HOP.has(name.toLowerCase())) back[name] = value;
|
|
93
|
+
});
|
|
94
|
+
const body = new Uint8Array(await answer.arrayBuffer());
|
|
95
|
+
return new Response(
|
|
96
|
+
JSON.stringify({ status: answer.status, statusText: answer.statusText, headers: back, body: toBase64(body) }),
|
|
97
|
+
{ headers: { ...HEADERS, "content-type": "application/json" } }
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
function egressNodeHandler(options = {}) {
|
|
101
|
+
return async (request, response) => {
|
|
102
|
+
const chunks = [];
|
|
103
|
+
await new Promise((done, fail) => {
|
|
104
|
+
request.on("data", (chunk) => chunks.push(chunk));
|
|
105
|
+
request.on("end", () => done());
|
|
106
|
+
request.on("error", (error) => fail(error));
|
|
107
|
+
});
|
|
108
|
+
let total = 0;
|
|
109
|
+
for (const chunk of chunks) total += chunk.length;
|
|
110
|
+
const body = new Uint8Array(total);
|
|
111
|
+
let at = 0;
|
|
112
|
+
for (const chunk of chunks) {
|
|
113
|
+
body.set(chunk, at);
|
|
114
|
+
at += chunk.length;
|
|
115
|
+
}
|
|
116
|
+
const headers = {};
|
|
117
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
118
|
+
if (typeof value === "string") headers[name] = value;
|
|
119
|
+
}
|
|
120
|
+
const answer = await handleEgressRequest(
|
|
121
|
+
new Request("http://egress.invalid/", {
|
|
122
|
+
method: request.method ?? "GET",
|
|
123
|
+
headers,
|
|
124
|
+
...body.length && request.method !== "GET" && request.method !== "HEAD" ? { body } : {}
|
|
125
|
+
}),
|
|
126
|
+
options
|
|
127
|
+
);
|
|
128
|
+
const out = {};
|
|
129
|
+
answer.headers.forEach((value, name) => {
|
|
130
|
+
out[name] = value;
|
|
131
|
+
});
|
|
132
|
+
response.writeHead(answer.status, out);
|
|
133
|
+
response.end(Buffer.from(await answer.arrayBuffer()));
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
5
137
|
// src/hosting/browser-host.ts
|
|
6
138
|
var browserIsolationHeaders = Object.freeze({
|
|
7
139
|
"Cross-Origin-Opener-Policy": "same-origin",
|
|
@@ -31,7 +163,12 @@ async function serveBrowserApp(options) {
|
|
|
31
163
|
const rel = relative(root, path);
|
|
32
164
|
return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
33
165
|
};
|
|
166
|
+
const egress = options.egress === false ? null : egressNodeHandler({ ...options.egress?.allow ? { allow: options.egress.allow } : {} });
|
|
34
167
|
const handle = async (req, res) => {
|
|
168
|
+
if (egress && (req.url ?? "").split("?")[0] === EGRESS_PATH) {
|
|
169
|
+
await egress(req, res);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
35
172
|
for (const [name, value] of Object.entries(browserIsolationHeaders)) res.setHeader(name, value);
|
|
36
173
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
37
174
|
res.setHeader("Cache-Control", "no-cache");
|
package/dist/egress.cjs
CHANGED
|
@@ -15,6 +15,10 @@ function fromBase64(text) {
|
|
|
15
15
|
return bytes;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// src/net/discover-egress.ts
|
|
19
|
+
var EGRESS_PATH = "/__sandboxedjs__/egress";
|
|
20
|
+
var EGRESS_MARKER = { sandboxedjs: "egress", protocol: 1 };
|
|
21
|
+
|
|
18
22
|
// src/net/egress-handler.ts
|
|
19
23
|
var HOP = /* @__PURE__ */ new Set([
|
|
20
24
|
"connection",
|
|
@@ -30,7 +34,7 @@ var HOP = /* @__PURE__ */ new Set([
|
|
|
30
34
|
]);
|
|
31
35
|
var HEADERS = {
|
|
32
36
|
"access-control-allow-origin": "*",
|
|
33
|
-
"access-control-allow-methods": "POST, OPTIONS",
|
|
37
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
34
38
|
"access-control-allow-headers": "content-type",
|
|
35
39
|
"access-control-max-age": "86400",
|
|
36
40
|
"cross-origin-resource-policy": "cross-origin"
|
|
@@ -38,6 +42,11 @@ var HEADERS = {
|
|
|
38
42
|
var refuse = (status, message) => new Response(message, { status, headers: { ...HEADERS, "content-type": "text/plain; charset=utf-8" } });
|
|
39
43
|
async function handleEgressRequest(request, options = {}) {
|
|
40
44
|
if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: HEADERS });
|
|
45
|
+
if (request.method === "GET" || request.method === "HEAD") {
|
|
46
|
+
return new Response(request.method === "HEAD" ? null : JSON.stringify(EGRESS_MARKER), {
|
|
47
|
+
headers: { ...HEADERS, "content-type": "application/json" }
|
|
48
|
+
});
|
|
49
|
+
}
|
|
41
50
|
if (request.method !== "POST") return refuse(405, "POST a JSON request here.");
|
|
42
51
|
let payload;
|
|
43
52
|
try {
|
|
@@ -121,6 +130,8 @@ function egressNodeHandler(options = {}) {
|
|
|
121
130
|
};
|
|
122
131
|
}
|
|
123
132
|
|
|
133
|
+
exports.EGRESS_MARKER = EGRESS_MARKER;
|
|
134
|
+
exports.EGRESS_PATH = EGRESS_PATH;
|
|
124
135
|
exports.egressNodeHandler = egressNodeHandler;
|
|
125
136
|
exports.handleEgressRequest = handleEgressRequest;
|
|
126
137
|
//# sourceMappingURL=egress.cjs.map
|
package/dist/egress.d.cts
CHANGED
|
@@ -48,4 +48,36 @@ interface NodeResponse {
|
|
|
48
48
|
*/
|
|
49
49
|
declare function egressNodeHandler(options?: EgressOptions): (request: NodeRequest, response: NodeResponse) => Promise<void>;
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Finding the proxy instead of being told where it is.
|
|
53
|
+
*
|
|
54
|
+
* A page cannot read a response from a host that does not send CORS headers,
|
|
55
|
+
* so a container in a browser needs a proxy to make its outbound requests --
|
|
56
|
+
* and until now every project had to say where that proxy was. That is a
|
|
57
|
+
* setting whose right value is almost always the same, and getting it wrong
|
|
58
|
+
* looks like the container being broken: the guest reports "Failed to fetch"
|
|
59
|
+
* from code that works everywhere else.
|
|
60
|
+
*
|
|
61
|
+
* So a browser container with no proxy configured looks for one before it
|
|
62
|
+
* gives up. A host that mounts `handleEgressRequest` at `EGRESS_PATH` on its
|
|
63
|
+
* own origin, or runs `sandboxedjs-egress` on its default port, is found
|
|
64
|
+
* without being named.
|
|
65
|
+
*
|
|
66
|
+
* A probe is a GET, which the handler answers with a marker. That matters more
|
|
67
|
+
* than it sounds: a single-page host answers every unknown path with its own
|
|
68
|
+
* index.html and status 200, so "the request succeeded" cannot mean "the proxy
|
|
69
|
+
* is here". Only the marker does.
|
|
70
|
+
*
|
|
71
|
+
* Discovery never overrides a proxy the host named, and finding nothing is not
|
|
72
|
+
* an error -- the request is then made directly, which is right on a server
|
|
73
|
+
* and is what produces the explanatory CORS failure in a browser.
|
|
74
|
+
*/
|
|
75
|
+
/** Where a host app mounts the proxy for its own containers to find. */
|
|
76
|
+
declare const EGRESS_PATH = "/__sandboxedjs__/egress";
|
|
77
|
+
/** What a GET to the proxy answers, so a probe can tell it from a 200 page. */
|
|
78
|
+
declare const EGRESS_MARKER: {
|
|
79
|
+
readonly sandboxedjs: "egress";
|
|
80
|
+
readonly protocol: 1;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export { EGRESS_MARKER, EGRESS_PATH, type EgressOptions, egressNodeHandler, handleEgressRequest };
|
package/dist/egress.d.ts
CHANGED
|
@@ -48,4 +48,36 @@ interface NodeResponse {
|
|
|
48
48
|
*/
|
|
49
49
|
declare function egressNodeHandler(options?: EgressOptions): (request: NodeRequest, response: NodeResponse) => Promise<void>;
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Finding the proxy instead of being told where it is.
|
|
53
|
+
*
|
|
54
|
+
* A page cannot read a response from a host that does not send CORS headers,
|
|
55
|
+
* so a container in a browser needs a proxy to make its outbound requests --
|
|
56
|
+
* and until now every project had to say where that proxy was. That is a
|
|
57
|
+
* setting whose right value is almost always the same, and getting it wrong
|
|
58
|
+
* looks like the container being broken: the guest reports "Failed to fetch"
|
|
59
|
+
* from code that works everywhere else.
|
|
60
|
+
*
|
|
61
|
+
* So a browser container with no proxy configured looks for one before it
|
|
62
|
+
* gives up. A host that mounts `handleEgressRequest` at `EGRESS_PATH` on its
|
|
63
|
+
* own origin, or runs `sandboxedjs-egress` on its default port, is found
|
|
64
|
+
* without being named.
|
|
65
|
+
*
|
|
66
|
+
* A probe is a GET, which the handler answers with a marker. That matters more
|
|
67
|
+
* than it sounds: a single-page host answers every unknown path with its own
|
|
68
|
+
* index.html and status 200, so "the request succeeded" cannot mean "the proxy
|
|
69
|
+
* is here". Only the marker does.
|
|
70
|
+
*
|
|
71
|
+
* Discovery never overrides a proxy the host named, and finding nothing is not
|
|
72
|
+
* an error -- the request is then made directly, which is right on a server
|
|
73
|
+
* and is what produces the explanatory CORS failure in a browser.
|
|
74
|
+
*/
|
|
75
|
+
/** Where a host app mounts the proxy for its own containers to find. */
|
|
76
|
+
declare const EGRESS_PATH = "/__sandboxedjs__/egress";
|
|
77
|
+
/** What a GET to the proxy answers, so a probe can tell it from a 200 page. */
|
|
78
|
+
declare const EGRESS_MARKER: {
|
|
79
|
+
readonly sandboxedjs: "egress";
|
|
80
|
+
readonly protocol: 1;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export { EGRESS_MARKER, EGRESS_PATH, type EgressOptions, egressNodeHandler, handleEgressRequest };
|
package/dist/egress.js
CHANGED
|
@@ -13,6 +13,10 @@ function fromBase64(text) {
|
|
|
13
13
|
return bytes;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// src/net/discover-egress.ts
|
|
17
|
+
var EGRESS_PATH = "/__sandboxedjs__/egress";
|
|
18
|
+
var EGRESS_MARKER = { sandboxedjs: "egress", protocol: 1 };
|
|
19
|
+
|
|
16
20
|
// src/net/egress-handler.ts
|
|
17
21
|
var HOP = /* @__PURE__ */ new Set([
|
|
18
22
|
"connection",
|
|
@@ -28,7 +32,7 @@ var HOP = /* @__PURE__ */ new Set([
|
|
|
28
32
|
]);
|
|
29
33
|
var HEADERS = {
|
|
30
34
|
"access-control-allow-origin": "*",
|
|
31
|
-
"access-control-allow-methods": "POST, OPTIONS",
|
|
35
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
32
36
|
"access-control-allow-headers": "content-type",
|
|
33
37
|
"access-control-max-age": "86400",
|
|
34
38
|
"cross-origin-resource-policy": "cross-origin"
|
|
@@ -36,6 +40,11 @@ var HEADERS = {
|
|
|
36
40
|
var refuse = (status, message) => new Response(message, { status, headers: { ...HEADERS, "content-type": "text/plain; charset=utf-8" } });
|
|
37
41
|
async function handleEgressRequest(request, options = {}) {
|
|
38
42
|
if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: HEADERS });
|
|
43
|
+
if (request.method === "GET" || request.method === "HEAD") {
|
|
44
|
+
return new Response(request.method === "HEAD" ? null : JSON.stringify(EGRESS_MARKER), {
|
|
45
|
+
headers: { ...HEADERS, "content-type": "application/json" }
|
|
46
|
+
});
|
|
47
|
+
}
|
|
39
48
|
if (request.method !== "POST") return refuse(405, "POST a JSON request here.");
|
|
40
49
|
let payload;
|
|
41
50
|
try {
|
|
@@ -119,6 +128,6 @@ function egressNodeHandler(options = {}) {
|
|
|
119
128
|
};
|
|
120
129
|
}
|
|
121
130
|
|
|
122
|
-
export { egressNodeHandler, handleEgressRequest };
|
|
131
|
+
export { EGRESS_MARKER, EGRESS_PATH, egressNodeHandler, handleEgressRequest };
|
|
123
132
|
//# sourceMappingURL=egress.js.map
|
|
124
133
|
//# sourceMappingURL=egress.js.map
|