sandboxedjs 0.2.4 → 0.2.6
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 +44 -13
- package/bin/sandboxedjs-egress.mjs +8 -69
- package/dist/agent.d.cts +2 -2
- package/dist/agent.d.ts +2 -2
- package/dist/{container-C16pCOWC.d.ts → container-BQx27_d-.d.cts} +1 -1
- package/dist/{container-DJfFjito.d.cts → container-D-e5SMXQ.d.ts} +1 -1
- package/dist/{contracts-CXZ_25-O.d.cts → contracts-BHo4LdY2.d.cts} +9 -0
- package/dist/{contracts-CXZ_25-O.d.ts → contracts-BHo4LdY2.d.ts} +9 -0
- package/dist/egress.cjs +127 -0
- package/dist/egress.d.cts +51 -0
- package/dist/egress.d.ts +51 -0
- package/dist/egress.js +124 -0
- package/dist/index.cjs +178 -58
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +178 -58
- package/dist/{memory-volume-Bp71nqvc.d.cts → memory-volume-e-uJFRnG.d.cts} +1 -1
- package/dist/{memory-volume-C4xwtIRZ.d.ts → memory-volume-meoRW8ST.d.ts} +1 -1
- package/dist/python-abi.cjs +5 -1
- package/dist/python-abi.d.cts +3 -3
- package/dist/python-abi.d.ts +3 -3
- package/dist/python-abi.js +5 -1
- package/dist/service-worker.js +70 -2
- package/dist/worker-entry.js +88 -1
- package/package.json +6 -1
package/dist/python-abi.cjs
CHANGED
|
@@ -1653,8 +1653,12 @@ function createHostAbiServer(proc) {
|
|
|
1653
1653
|
}
|
|
1654
1654
|
case Op.resolve: {
|
|
1655
1655
|
const sockets = proc.sockets;
|
|
1656
|
+
const name = r.string();
|
|
1657
|
+
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(name)) {
|
|
1658
|
+
return { status: 0, payload: new Writer().string(name).finish() };
|
|
1659
|
+
}
|
|
1656
1660
|
if (!sockets?.outbound) throw new PosixError(Errno.ENOSYS);
|
|
1657
|
-
const address = sockets.outbound.resolve(
|
|
1661
|
+
const address = sockets.outbound.resolve(name);
|
|
1658
1662
|
return { status: 0, payload: new Writer().string(address).finish() };
|
|
1659
1663
|
}
|
|
1660
1664
|
case Op.send: {
|
package/dist/python-abi.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { V as Vfs, C as Cred, a as VirtualTcpNetwork } from './contracts-
|
|
2
|
-
export { m as ROOT_CRED, v as makeCred } from './contracts-
|
|
3
|
-
export { M as MemoryVolume } from './memory-volume-
|
|
1
|
+
import { V as Vfs, C as Cred, a as VirtualTcpNetwork } from './contracts-BHo4LdY2.cjs';
|
|
2
|
+
export { m as ROOT_CRED, v as makeCred } from './contracts-BHo4LdY2.cjs';
|
|
3
|
+
export { M as MemoryVolume } from './memory-volume-e-uJFRnG.cjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Open-file descriptions: the thing a descriptor points *at*.
|
package/dist/python-abi.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { V as Vfs, C as Cred, a as VirtualTcpNetwork } from './contracts-
|
|
2
|
-
export { m as ROOT_CRED, v as makeCred } from './contracts-
|
|
3
|
-
export { M as MemoryVolume } from './memory-volume-
|
|
1
|
+
import { V as Vfs, C as Cred, a as VirtualTcpNetwork } from './contracts-BHo4LdY2.js';
|
|
2
|
+
export { m as ROOT_CRED, v as makeCred } from './contracts-BHo4LdY2.js';
|
|
3
|
+
export { M as MemoryVolume } from './memory-volume-meoRW8ST.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Open-file descriptions: the thing a descriptor points *at*.
|
package/dist/python-abi.js
CHANGED
|
@@ -1651,8 +1651,12 @@ function createHostAbiServer(proc) {
|
|
|
1651
1651
|
}
|
|
1652
1652
|
case Op.resolve: {
|
|
1653
1653
|
const sockets = proc.sockets;
|
|
1654
|
+
const name = r.string();
|
|
1655
|
+
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(name)) {
|
|
1656
|
+
return { status: 0, payload: new Writer().string(name).finish() };
|
|
1657
|
+
}
|
|
1654
1658
|
if (!sockets?.outbound) throw new PosixError(Errno.ENOSYS);
|
|
1655
|
-
const address = sockets.outbound.resolve(
|
|
1659
|
+
const address = sockets.outbound.resolve(name);
|
|
1656
1660
|
return { status: 0, payload: new Writer().string(address).finish() };
|
|
1657
1661
|
}
|
|
1658
1662
|
case Op.send: {
|
package/dist/service-worker.js
CHANGED
|
@@ -1,3 +1,58 @@
|
|
|
1
|
+
// src/preview/fetch-client.ts
|
|
2
|
+
function installPreviewFetch(scope) {
|
|
3
|
+
const MARKER = "__sbx__/";
|
|
4
|
+
const at = scope.location.pathname.indexOf(MARKER);
|
|
5
|
+
if (at < 0) return;
|
|
6
|
+
const base = scope.location.origin + scope.location.pathname.slice(0, at + MARKER.length);
|
|
7
|
+
const LOOPBACK = /^(?:localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|\[?::1\]?|[^.]+\.localhost)$/i;
|
|
8
|
+
const rewrite = (value) => {
|
|
9
|
+
let url;
|
|
10
|
+
try {
|
|
11
|
+
url = new URL(value, scope.location.href);
|
|
12
|
+
} catch {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
const secure = url.protocol === "https:" || url.protocol === "wss:";
|
|
16
|
+
if (!secure && url.protocol !== "http:" && url.protocol !== "ws:") return value;
|
|
17
|
+
if (!LOOPBACK.test(url.hostname)) return value;
|
|
18
|
+
const port = url.port || (secure ? "443" : "80");
|
|
19
|
+
return `${base}${port}${url.pathname}${url.search}${url.hash}`;
|
|
20
|
+
};
|
|
21
|
+
const originalFetch = scope.fetch;
|
|
22
|
+
if (typeof originalFetch === "function") {
|
|
23
|
+
scope.fetch = function patched(input, init) {
|
|
24
|
+
if (typeof input === "string" || input instanceof URL) {
|
|
25
|
+
return originalFetch.call(this, rewrite(String(input)), init);
|
|
26
|
+
}
|
|
27
|
+
const moved = input && typeof input === "object" && "url" in input ? rewrite(input.url) : null;
|
|
28
|
+
if (moved === null || moved === input.url) {
|
|
29
|
+
return originalFetch.call(this, input, init);
|
|
30
|
+
}
|
|
31
|
+
return originalFetch.call(this, new Request(moved, input), init);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const open = scope.XMLHttpRequest?.prototype?.open;
|
|
35
|
+
if (typeof open === "function") {
|
|
36
|
+
scope.XMLHttpRequest.prototype.open = function patched(method, url, ...rest) {
|
|
37
|
+
return open.call(this, method, rewrite(String(url)), ...rest);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const OriginalEventSource = scope.EventSource;
|
|
41
|
+
if (typeof OriginalEventSource === "function") {
|
|
42
|
+
const Patched = function(url, init) {
|
|
43
|
+
return new OriginalEventSource(rewrite(String(url)), init);
|
|
44
|
+
};
|
|
45
|
+
Patched.prototype = OriginalEventSource.prototype;
|
|
46
|
+
for (const name of ["CONNECTING", "OPEN", "CLOSED"]) {
|
|
47
|
+
Object.defineProperty(Patched, name, { value: OriginalEventSource[name], enumerable: true });
|
|
48
|
+
}
|
|
49
|
+
scope.EventSource = Patched;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function previewFetchSource() {
|
|
53
|
+
return `(function(){try{(${installPreviewFetch.toString()})(window);}catch(error){console.warn("sandboxedjs: loopback rewriting unavailable",error);}})();`;
|
|
54
|
+
}
|
|
55
|
+
|
|
1
56
|
// src/preview/ws-client.ts
|
|
2
57
|
function installPreviewSockets(scope) {
|
|
3
58
|
const Native = scope.WebSocket;
|
|
@@ -419,6 +474,19 @@ var PreviewRouter = class {
|
|
|
419
474
|
portFor(clientId) {
|
|
420
475
|
return this.clientPorts.get(clientId);
|
|
421
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* The container port one request is for, by path first and client second.
|
|
479
|
+
*
|
|
480
|
+
* A port named in the path wins, because that is how a previewed page
|
|
481
|
+
* reaches its *other* servers: the frontend's document is bound to 3000, and
|
|
482
|
+
* its call to the backend arrives as `__sbx__/8000/…`. Reading the port from
|
|
483
|
+
* the path also means such a request needs nothing remembered about who is
|
|
484
|
+
* asking — which matters, because this worker is stopped whenever it looks
|
|
485
|
+
* idle and every binding it held is gone when it starts again.
|
|
486
|
+
*/
|
|
487
|
+
portForRequest(pathname, clientId) {
|
|
488
|
+
return this.claimedPort(pathname) ?? this.portFor(clientId);
|
|
489
|
+
}
|
|
422
490
|
/** A response has come back from the page. */
|
|
423
491
|
settle(id, response) {
|
|
424
492
|
const resolve = this.pending.get(id);
|
|
@@ -498,7 +566,7 @@ function withSockets(body, headers) {
|
|
|
498
566
|
const encoding = headers.get("content-encoding");
|
|
499
567
|
if (encoding && encoding.toLowerCase() !== "identity") return body;
|
|
500
568
|
const html = new TextDecoder().decode(body);
|
|
501
|
-
const tag = `<script>${previewSocketSource()}<\/script>`;
|
|
569
|
+
const tag = `<script>${previewSocketSource()}${previewFetchSource()}<\/script>`;
|
|
502
570
|
const head = /<head[^>]*>/i.exec(html);
|
|
503
571
|
const at = head ? head.index + head[0].length : 0;
|
|
504
572
|
const bytes = new TextEncoder().encode(html.slice(0, at) + tag + html.slice(at));
|
|
@@ -591,7 +659,7 @@ worker.addEventListener("fetch", (event) => {
|
|
|
591
659
|
event.respondWith(router.fetch(claimed, "/", event.request, event.resultingClientId || event.clientId));
|
|
592
660
|
return;
|
|
593
661
|
}
|
|
594
|
-
const port = router.
|
|
662
|
+
const port = router.portForRequest(url.pathname, event.clientId);
|
|
595
663
|
if (port === void 0) return;
|
|
596
664
|
event.respondWith(
|
|
597
665
|
router.fetch(port, router.containerPath(url.pathname, url.search), event.request, event.clientId)
|
package/dist/worker-entry.js
CHANGED
|
@@ -28231,6 +28231,60 @@ var url_module_default = urlModule;
|
|
|
28231
28231
|
var import_events = __toESM(require_events());
|
|
28232
28232
|
var import_stream_browserify3 = __toESM(require_stream_browserify());
|
|
28233
28233
|
|
|
28234
|
+
// src/net/proxy-fetch.ts
|
|
28235
|
+
function toBase64(bytes2) {
|
|
28236
|
+
let text = "";
|
|
28237
|
+
for (let at2 = 0; at2 < bytes2.length; at2 += 32768) {
|
|
28238
|
+
text += String.fromCharCode(...bytes2.subarray(at2, at2 + 32768));
|
|
28239
|
+
}
|
|
28240
|
+
return btoa(text);
|
|
28241
|
+
}
|
|
28242
|
+
function fromBase64(text) {
|
|
28243
|
+
const binary = atob(text);
|
|
28244
|
+
const bytes2 = new Uint8Array(binary.length);
|
|
28245
|
+
for (let at2 = 0; at2 < binary.length; at2 += 1) bytes2[at2] = binary.charCodeAt(at2);
|
|
28246
|
+
return bytes2;
|
|
28247
|
+
}
|
|
28248
|
+
async function proxyExchange(proxy, request, fetchImpl = fetch) {
|
|
28249
|
+
let answer2;
|
|
28250
|
+
try {
|
|
28251
|
+
answer2 = await fetchImpl(proxy, {
|
|
28252
|
+
method: "POST",
|
|
28253
|
+
headers: { "content-type": "application/json" },
|
|
28254
|
+
body: JSON.stringify({
|
|
28255
|
+
url: request.url,
|
|
28256
|
+
method: request.method,
|
|
28257
|
+
headers: request.headers,
|
|
28258
|
+
...request.body?.length ? { body: toBase64(request.body) } : {}
|
|
28259
|
+
})
|
|
28260
|
+
});
|
|
28261
|
+
} catch (error) {
|
|
28262
|
+
throw new Error(
|
|
28263
|
+
`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.`
|
|
28264
|
+
);
|
|
28265
|
+
}
|
|
28266
|
+
if (!answer2.ok) {
|
|
28267
|
+
throw new Error(
|
|
28268
|
+
`the egress proxy at ${proxy} refused ${request.method} ${request.url} with ${answer2.status} ${answer2.statusText}: ${(await answer2.text()).trim().slice(0, 300)}`
|
|
28269
|
+
);
|
|
28270
|
+
}
|
|
28271
|
+
const payload = await answer2.json();
|
|
28272
|
+
return {
|
|
28273
|
+
status: payload.status ?? 200,
|
|
28274
|
+
statusText: payload.statusText ?? "",
|
|
28275
|
+
headers: payload.headers ?? {},
|
|
28276
|
+
body: payload.body ? fromBase64(payload.body) : new Uint8Array(0)
|
|
28277
|
+
};
|
|
28278
|
+
}
|
|
28279
|
+
function isBrowser() {
|
|
28280
|
+
return typeof process === "undefined" || process.versions?.node == null;
|
|
28281
|
+
}
|
|
28282
|
+
function browserBlocked(hostname, message) {
|
|
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. 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.`
|
|
28285
|
+
);
|
|
28286
|
+
}
|
|
28287
|
+
|
|
28234
28288
|
// src/net/policy.ts
|
|
28235
28289
|
function isLoopbackHostname(hostname) {
|
|
28236
28290
|
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
@@ -28273,7 +28327,40 @@ function policedFetch(policy, fetchImpl) {
|
|
|
28273
28327
|
if (!outboundAllowed(policy, url)) {
|
|
28274
28328
|
throw Object.assign(new TypeError("fetch failed"), { cause: outboundBlockedError(url) });
|
|
28275
28329
|
}
|
|
28276
|
-
return fetchImpl
|
|
28330
|
+
if (policy.proxy) return await fetchThroughProxy(policy.proxy, fetchImpl, input, init);
|
|
28331
|
+
try {
|
|
28332
|
+
return await fetchImpl(input, init);
|
|
28333
|
+
} catch (error) {
|
|
28334
|
+
if (!isBrowser()) throw error;
|
|
28335
|
+
throw Object.assign(new TypeError("fetch failed"), {
|
|
28336
|
+
cause: browserBlocked(new URL(url).hostname, error instanceof Error ? error.message : String(error))
|
|
28337
|
+
});
|
|
28338
|
+
}
|
|
28339
|
+
});
|
|
28340
|
+
}
|
|
28341
|
+
async function fetchThroughProxy(proxy, fetchImpl, input, init) {
|
|
28342
|
+
const request = new Request(input, init);
|
|
28343
|
+
const headers = {};
|
|
28344
|
+
request.headers.forEach((value, name) => {
|
|
28345
|
+
headers[name] = value;
|
|
28346
|
+
});
|
|
28347
|
+
const bodyless = request.method === "GET" || request.method === "HEAD";
|
|
28348
|
+
const result = await proxyExchange(
|
|
28349
|
+
proxy,
|
|
28350
|
+
{
|
|
28351
|
+
url: request.url,
|
|
28352
|
+
method: request.method,
|
|
28353
|
+
headers,
|
|
28354
|
+
...bodyless ? {} : { body: new Uint8Array(await request.arrayBuffer()) }
|
|
28355
|
+
},
|
|
28356
|
+
fetchImpl
|
|
28357
|
+
);
|
|
28358
|
+
const nullBody = result.status === 204 || result.status === 205 || result.status === 304 || request.method === "HEAD";
|
|
28359
|
+
const body = nullBody ? null : result.body.slice().buffer;
|
|
28360
|
+
return new Response(body, {
|
|
28361
|
+
status: result.status,
|
|
28362
|
+
statusText: result.statusText,
|
|
28363
|
+
headers: result.headers
|
|
28277
28364
|
});
|
|
28278
28365
|
}
|
|
28279
28366
|
function policedWebSocket(Native, policy) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sandboxedjs",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
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",
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
"import": "./dist/agent.js",
|
|
32
32
|
"require": "./dist/agent.cjs"
|
|
33
33
|
},
|
|
34
|
+
"./egress": {
|
|
35
|
+
"types": "./dist/egress.d.ts",
|
|
36
|
+
"import": "./dist/egress.js",
|
|
37
|
+
"require": "./dist/egress.cjs"
|
|
38
|
+
},
|
|
34
39
|
"./browser-host": {
|
|
35
40
|
"types": "./dist/browser-host.d.ts",
|
|
36
41
|
"import": "./dist/browser-host.js",
|