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.
@@ -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(r.string());
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: {
@@ -1,6 +1,6 @@
1
- import { V as Vfs, C as Cred, a as VirtualTcpNetwork } from './contracts-CXZ_25-O.cjs';
2
- export { m as ROOT_CRED, v as makeCred } from './contracts-CXZ_25-O.cjs';
3
- export { M as MemoryVolume } from './memory-volume-Bp71nqvc.cjs';
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*.
@@ -1,6 +1,6 @@
1
- import { V as Vfs, C as Cred, a as VirtualTcpNetwork } from './contracts-CXZ_25-O.js';
2
- export { m as ROOT_CRED, v as makeCred } from './contracts-CXZ_25-O.js';
3
- export { M as MemoryVolume } from './memory-volume-C4xwtIRZ.js';
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*.
@@ -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(r.string());
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: {
@@ -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.portFor(event.clientId);
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)
@@ -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(input, init);
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.4",
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",