sandboxedjs 0.1.97 → 0.1.100

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/index.cjs CHANGED
@@ -4273,7 +4273,8 @@ function md5Hex(input) {
4273
4273
  var isNode, zlibPromise, cryptoPromise, SUBTLE_NAMES, UnsupportedAlgorithmError;
4274
4274
  var init_binary = __esm({
4275
4275
  "src/util/binary.ts"() {
4276
- isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
4276
+ isNode = typeof window === "undefined" && typeof document === "undefined" && typeof process !== "undefined" && process.versions != null && process.versions.node != null && /* A shim has no `release.name`, and Node's has said "node" since v3. */
4277
+ process.release?.name === "node";
4277
4278
  zlibPromise = null;
4278
4279
  cryptoPromise = null;
4279
4280
  SUBTLE_NAMES = {
@@ -20958,7 +20959,7 @@ var wheels_default = {
20958
20959
 
20959
20960
  // package.json
20960
20961
  var package_default = {
20961
- version: "0.1.97"};
20962
+ version: "0.1.100"};
20962
20963
 
20963
20964
  // src/python/extension-abi.ts
20964
20965
  var EXTENSION_ABI = {
@@ -38658,6 +38659,22 @@ var WS_DATA = "sandboxedjs:ws-data";
38658
38659
  var WS_CLOSE = "sandboxedjs:ws-close";
38659
38660
  var WS_ERROR = "sandboxedjs:ws-error";
38660
38661
 
38662
+ // src/preview/router.ts
38663
+ var LOOPBACK_HOST = /^(?:localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|\[?::1\]?)$/i;
38664
+ function containerTarget(value) {
38665
+ let url;
38666
+ try {
38667
+ url = new URL(value.trim());
38668
+ } catch {
38669
+ return null;
38670
+ }
38671
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
38672
+ if (!LOOPBACK_HOST.test(url.hostname)) return null;
38673
+ const port = Number(url.port || (url.protocol === "https:" ? 443 : 80));
38674
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
38675
+ return { port, path: `${url.pathname}${url.search}` };
38676
+ }
38677
+
38661
38678
  // src/preview/register.ts
38662
38679
  function serveContainerOn(port, box) {
38663
38680
  port.onmessage = async (event) => {
@@ -38692,20 +38709,6 @@ function serveContainerOn(port, box) {
38692
38709
  };
38693
38710
  port.start?.();
38694
38711
  }
38695
- var LOOPBACK_HOST = /^(?:localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|\[?::1\]?)$/i;
38696
- function containerTarget(value) {
38697
- let url;
38698
- try {
38699
- url = new URL(value.trim());
38700
- } catch {
38701
- return null;
38702
- }
38703
- if (url.protocol !== "http:" && url.protocol !== "https:") return null;
38704
- if (!LOOPBACK_HOST.test(url.hostname)) return null;
38705
- const port = Number(url.port || (url.protocol === "https:" ? 443 : 80));
38706
- if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
38707
- return { port, path: `${url.pathname}${url.search}` };
38708
- }
38709
38712
  async function createPreview(box, options = {}) {
38710
38713
  if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) return null;
38711
38714
  const scriptUrl = options.scriptUrl ?? new URL("./service-worker.js?no-inline", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
@@ -38743,7 +38746,17 @@ async function createPreview(box, options = {}) {
38743
38746
  channel = new MessageChannel();
38744
38747
  serveContainerOn(channel.port1, box);
38745
38748
  const target = registration.active ?? worker;
38746
- target.postMessage({ type: "sandboxedjs:connect", injectSockets: options.websocket !== false }, [channel.port2]);
38749
+ target.postMessage(
38750
+ {
38751
+ type: "sandboxedjs:connect",
38752
+ injectSockets: options.websocket !== false,
38753
+ /* Sent with the channel, like `injectSockets`, because the worker is
38754
+ * restarted at the browser's convenience and remembers nothing: every
38755
+ * connect has to carry the settings again. */
38756
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
38757
+ },
38758
+ [channel.port2]
38759
+ );
38747
38760
  };
38748
38761
  const onWorkerMessage = (event) => {
38749
38762
  const type = event.data?.type;
package/dist/index.d.cts CHANGED
@@ -1590,19 +1590,22 @@ declare function startRuntimeWorker(options?: {
1590
1590
  declare function syncChannelSupported(): boolean;
1591
1591
 
1592
1592
  /**
1593
- * Wiring a container's HTTP servers up to real URLs in the page.
1593
+ * The routing a preview service worker does, separated from the worker itself.
1594
1594
  *
1595
- * The service worker does the routing; this is the half that lives in the page
1596
- * and actually knows about the container.
1595
+ * A service worker is awkward to test it needs a browser, a registration and
1596
+ * a secure context — but almost none of what makes this correct is about being
1597
+ * a service worker. The interesting part is the bookkeeping: which client is
1598
+ * previewing which port, which requests belong to a preview at all, and how a
1599
+ * response is rebuilt on the way back. That lives here, where it can be
1600
+ * exercised directly.
1597
1601
  *
1598
- * **Read the origin note before using this.** A preview served this way runs on
1599
- * *your* origin, so scripts inside it can reach `window.parent`, your cookies
1600
- * and your `localStorage` the sandbox contains the program's *filesystem and
1601
- * process table*, not the page it serves. For code you did not write, either
1602
- * host the preview on a separate origin, or use {@link renderInto}, which puts
1603
- * the response in an iframe with no origin at all.
1602
+ * The rule this implements, and the reason it exists: requests are routed by
1603
+ * **who is asking**, not by what the path looks like. An iframe navigates once
1604
+ * to `/__sbx__/<port>/`; every later request from that client however
1605
+ * absolute its path is recognised by its client id. Nothing has to rewrite
1606
+ * `/src/main.js` or `/@vite/client`, which is what makes a real dev server
1607
+ * work rather than only a single page.
1604
1608
  */
1605
-
1606
1609
  /** A container-local server, named by port rather than by an address. */
1607
1610
  interface ContainerTarget {
1608
1611
  port: number;
@@ -1618,10 +1621,31 @@ interface ContainerTarget {
1618
1621
  * is listening, so a frame pointed at it fails with "refused to connect"
1619
1622
  * rather than failing visibly as a routing mistake.
1620
1623
  *
1621
- * Exported separately from {@link Preview.resolve} because it is pure, and a
1622
- * translation table is worth testing without a service worker in the way.
1624
+ * The same translation serves two callers. A host reading an address out of a
1625
+ * build log needs it to point a frame somewhere (`Preview.resolve`), and the
1626
+ * service worker needs it for the other direction: a page *inside* a preview
1627
+ * asking its own backend for `http://localhost:8000/api` means the container's
1628
+ * port 8000, not the reader's machine.
1629
+ *
1630
+ * Kept pure, and kept here rather than beside the host half, because the
1631
+ * worker bundles this module and not that one.
1623
1632
  */
1624
1633
  declare function containerTarget(value: string): ContainerTarget | null;
1634
+
1635
+ /**
1636
+ * Wiring a container's HTTP servers up to real URLs in the page.
1637
+ *
1638
+ * The service worker does the routing; this is the half that lives in the page
1639
+ * and actually knows about the container.
1640
+ *
1641
+ * **Read the origin note before using this.** A preview served this way runs on
1642
+ * *your* origin, so scripts inside it can reach `window.parent`, your cookies
1643
+ * and your `localStorage` — the sandbox contains the program's *filesystem and
1644
+ * process table*, not the page it serves. For code you did not write, either
1645
+ * host the preview on a separate origin, or use {@link renderInto}, which puts
1646
+ * the response in an iframe with no origin at all.
1647
+ */
1648
+
1625
1649
  interface PreviewOptions {
1626
1650
  /** Where the worker script lives; defaults to the copy shipped beside the bundle. */
1627
1651
  scriptUrl?: string | URL;
@@ -1654,6 +1678,16 @@ interface PreviewOptions {
1654
1678
  * re-optimizing dependencies; {@link onStale} is the fallback for that.
1655
1679
  */
1656
1680
  websocket?: boolean;
1681
+ /**
1682
+ * How long a previewed request may take before the worker reports a timeout.
1683
+ *
1684
+ * Defaults to five minutes. Responses cross this bridge whole rather than
1685
+ * streamed, so this covers the *entire* answer: a model generating tokens, a
1686
+ * server-sent-event endpoint running to its last event, a cold build. Raise
1687
+ * it for work that legitimately takes longer; lower it when a preview should
1688
+ * fail fast.
1689
+ */
1690
+ timeoutMs?: number;
1657
1691
  }
1658
1692
  interface Preview {
1659
1693
  /** The URL an iframe should be pointed at to see `port`. */
package/dist/index.d.ts CHANGED
@@ -1590,19 +1590,22 @@ declare function startRuntimeWorker(options?: {
1590
1590
  declare function syncChannelSupported(): boolean;
1591
1591
 
1592
1592
  /**
1593
- * Wiring a container's HTTP servers up to real URLs in the page.
1593
+ * The routing a preview service worker does, separated from the worker itself.
1594
1594
  *
1595
- * The service worker does the routing; this is the half that lives in the page
1596
- * and actually knows about the container.
1595
+ * A service worker is awkward to test it needs a browser, a registration and
1596
+ * a secure context — but almost none of what makes this correct is about being
1597
+ * a service worker. The interesting part is the bookkeeping: which client is
1598
+ * previewing which port, which requests belong to a preview at all, and how a
1599
+ * response is rebuilt on the way back. That lives here, where it can be
1600
+ * exercised directly.
1597
1601
  *
1598
- * **Read the origin note before using this.** A preview served this way runs on
1599
- * *your* origin, so scripts inside it can reach `window.parent`, your cookies
1600
- * and your `localStorage` the sandbox contains the program's *filesystem and
1601
- * process table*, not the page it serves. For code you did not write, either
1602
- * host the preview on a separate origin, or use {@link renderInto}, which puts
1603
- * the response in an iframe with no origin at all.
1602
+ * The rule this implements, and the reason it exists: requests are routed by
1603
+ * **who is asking**, not by what the path looks like. An iframe navigates once
1604
+ * to `/__sbx__/<port>/`; every later request from that client however
1605
+ * absolute its path is recognised by its client id. Nothing has to rewrite
1606
+ * `/src/main.js` or `/@vite/client`, which is what makes a real dev server
1607
+ * work rather than only a single page.
1604
1608
  */
1605
-
1606
1609
  /** A container-local server, named by port rather than by an address. */
1607
1610
  interface ContainerTarget {
1608
1611
  port: number;
@@ -1618,10 +1621,31 @@ interface ContainerTarget {
1618
1621
  * is listening, so a frame pointed at it fails with "refused to connect"
1619
1622
  * rather than failing visibly as a routing mistake.
1620
1623
  *
1621
- * Exported separately from {@link Preview.resolve} because it is pure, and a
1622
- * translation table is worth testing without a service worker in the way.
1624
+ * The same translation serves two callers. A host reading an address out of a
1625
+ * build log needs it to point a frame somewhere (`Preview.resolve`), and the
1626
+ * service worker needs it for the other direction: a page *inside* a preview
1627
+ * asking its own backend for `http://localhost:8000/api` means the container's
1628
+ * port 8000, not the reader's machine.
1629
+ *
1630
+ * Kept pure, and kept here rather than beside the host half, because the
1631
+ * worker bundles this module and not that one.
1623
1632
  */
1624
1633
  declare function containerTarget(value: string): ContainerTarget | null;
1634
+
1635
+ /**
1636
+ * Wiring a container's HTTP servers up to real URLs in the page.
1637
+ *
1638
+ * The service worker does the routing; this is the half that lives in the page
1639
+ * and actually knows about the container.
1640
+ *
1641
+ * **Read the origin note before using this.** A preview served this way runs on
1642
+ * *your* origin, so scripts inside it can reach `window.parent`, your cookies
1643
+ * and your `localStorage` — the sandbox contains the program's *filesystem and
1644
+ * process table*, not the page it serves. For code you did not write, either
1645
+ * host the preview on a separate origin, or use {@link renderInto}, which puts
1646
+ * the response in an iframe with no origin at all.
1647
+ */
1648
+
1625
1649
  interface PreviewOptions {
1626
1650
  /** Where the worker script lives; defaults to the copy shipped beside the bundle. */
1627
1651
  scriptUrl?: string | URL;
@@ -1654,6 +1678,16 @@ interface PreviewOptions {
1654
1678
  * re-optimizing dependencies; {@link onStale} is the fallback for that.
1655
1679
  */
1656
1680
  websocket?: boolean;
1681
+ /**
1682
+ * How long a previewed request may take before the worker reports a timeout.
1683
+ *
1684
+ * Defaults to five minutes. Responses cross this bridge whole rather than
1685
+ * streamed, so this covers the *entire* answer: a model generating tokens, a
1686
+ * server-sent-event endpoint running to its last event, a cold build. Raise
1687
+ * it for work that legitimately takes longer; lower it when a preview should
1688
+ * fail fast.
1689
+ */
1690
+ timeoutMs?: number;
1657
1691
  }
1658
1692
  interface Preview {
1659
1693
  /** The URL an iframe should be pointed at to see `port`. */
package/dist/index.js CHANGED
@@ -4256,7 +4256,8 @@ function md5Hex(input) {
4256
4256
  var isNode, zlibPromise, cryptoPromise, SUBTLE_NAMES, UnsupportedAlgorithmError;
4257
4257
  var init_binary = __esm({
4258
4258
  "src/util/binary.ts"() {
4259
- isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
4259
+ isNode = typeof window === "undefined" && typeof document === "undefined" && typeof process !== "undefined" && process.versions != null && process.versions.node != null && /* A shim has no `release.name`, and Node's has said "node" since v3. */
4260
+ process.release?.name === "node";
4260
4261
  zlibPromise = null;
4261
4262
  cryptoPromise = null;
4262
4263
  SUBTLE_NAMES = {
@@ -20941,7 +20942,7 @@ var wheels_default = {
20941
20942
 
20942
20943
  // package.json
20943
20944
  var package_default = {
20944
- version: "0.1.97"};
20945
+ version: "0.1.100"};
20945
20946
 
20946
20947
  // src/python/extension-abi.ts
20947
20948
  var EXTENSION_ABI = {
@@ -38641,6 +38642,22 @@ var WS_DATA = "sandboxedjs:ws-data";
38641
38642
  var WS_CLOSE = "sandboxedjs:ws-close";
38642
38643
  var WS_ERROR = "sandboxedjs:ws-error";
38643
38644
 
38645
+ // src/preview/router.ts
38646
+ var LOOPBACK_HOST = /^(?:localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|\[?::1\]?)$/i;
38647
+ function containerTarget(value) {
38648
+ let url;
38649
+ try {
38650
+ url = new URL(value.trim());
38651
+ } catch {
38652
+ return null;
38653
+ }
38654
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
38655
+ if (!LOOPBACK_HOST.test(url.hostname)) return null;
38656
+ const port = Number(url.port || (url.protocol === "https:" ? 443 : 80));
38657
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
38658
+ return { port, path: `${url.pathname}${url.search}` };
38659
+ }
38660
+
38644
38661
  // src/preview/register.ts
38645
38662
  function serveContainerOn(port, box) {
38646
38663
  port.onmessage = async (event) => {
@@ -38675,20 +38692,6 @@ function serveContainerOn(port, box) {
38675
38692
  };
38676
38693
  port.start?.();
38677
38694
  }
38678
- var LOOPBACK_HOST = /^(?:localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|\[?::1\]?)$/i;
38679
- function containerTarget(value) {
38680
- let url;
38681
- try {
38682
- url = new URL(value.trim());
38683
- } catch {
38684
- return null;
38685
- }
38686
- if (url.protocol !== "http:" && url.protocol !== "https:") return null;
38687
- if (!LOOPBACK_HOST.test(url.hostname)) return null;
38688
- const port = Number(url.port || (url.protocol === "https:" ? 443 : 80));
38689
- if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
38690
- return { port, path: `${url.pathname}${url.search}` };
38691
- }
38692
38695
  async function createPreview(box, options = {}) {
38693
38696
  if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) return null;
38694
38697
  const scriptUrl = options.scriptUrl ?? new URL("./service-worker.js?no-inline", import.meta.url);
@@ -38726,7 +38729,17 @@ async function createPreview(box, options = {}) {
38726
38729
  channel = new MessageChannel();
38727
38730
  serveContainerOn(channel.port1, box);
38728
38731
  const target = registration.active ?? worker;
38729
- target.postMessage({ type: "sandboxedjs:connect", injectSockets: options.websocket !== false }, [channel.port2]);
38732
+ target.postMessage(
38733
+ {
38734
+ type: "sandboxedjs:connect",
38735
+ injectSockets: options.websocket !== false,
38736
+ /* Sent with the channel, like `injectSockets`, because the worker is
38737
+ * restarted at the browser's convenience and remembers nothing: every
38738
+ * connect has to carry the settings again. */
38739
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
38740
+ },
38741
+ [channel.port2]
38742
+ );
38730
38743
  };
38731
38744
  const onWorkerMessage = (event) => {
38732
38745
  const type = event.data?.type;
@@ -772,7 +772,8 @@ function createSbxFs(FS, client, errnoCodes) {
772
772
  }
773
773
 
774
774
  // src/util/binary.ts
775
- typeof process !== "undefined" && process.versions != null && process.versions.node != null;
775
+ typeof window === "undefined" && typeof document === "undefined" && typeof process !== "undefined" && process.versions != null && process.versions.node != null && /* A shim has no `release.name`, and Node's has said "node" since v3. */
776
+ process.release?.name === "node";
776
777
  async function nodeBuiltin(name) {
777
778
  const specifier = `node:${name}`;
778
779
  return await import(
@@ -349,8 +349,23 @@ function previewSocketSource() {
349
349
 
350
350
  // src/preview/router.ts
351
351
  var CLAIM = /(?:^|\/)__sbx__\/(\d+)(?:\/|$)/;
352
+ var LOOPBACK_HOST = /^(?:localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|\[?::1\]?)$/i;
353
+ function containerTarget(value) {
354
+ let url;
355
+ try {
356
+ url = new URL(value.trim());
357
+ } catch {
358
+ return null;
359
+ }
360
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
361
+ if (!LOOPBACK_HOST.test(url.hostname)) return null;
362
+ const port = Number(url.port || (url.protocol === "https:" ? 443 : 80));
363
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
364
+ return { port, path: `${url.pathname}${url.search}` };
365
+ }
352
366
  var OUTDATED = /outdated optimize dep/i;
353
367
  var NOT_CONNECTED = 503;
368
+ var DEFAULT_TIMEOUT_MS = 5 * 6e4;
354
369
  var PreviewRouter = class {
355
370
  constructor(options2) {
356
371
  this.options = options2;
@@ -433,16 +448,15 @@ var PreviewRouter = class {
433
448
  { id, port, path, method: request.method, headers: headerRecord(request.headers), ...sent ? { body: sent } : {} },
434
449
  sent ? [sent] : []
435
450
  );
436
- const timeoutMs = this.options.timeoutMs ?? 3e4;
451
+ const timeoutMs = this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
452
+ let timer;
437
453
  const result = await Promise.race([
438
454
  answered,
439
- new Promise(
440
- (resolve) => setTimeout(
441
- () => resolve({ status: 504, statusText: "Gateway Timeout", headers: {}, body: new ArrayBuffer(0) }),
442
- timeoutMs
443
- )
444
- )
455
+ new Promise((resolve) => {
456
+ timer = setTimeout(() => resolve(timedOut(port, path, timeoutMs)), timeoutMs);
457
+ })
445
458
  ]);
459
+ clearTimeout(timer);
446
460
  this.pending.delete(id);
447
461
  if (result.status === 504 && OUTDATED.test(result.statusText)) {
448
462
  this.options.onStale?.(clientId);
@@ -454,6 +468,19 @@ var PreviewRouter = class {
454
468
  return new Response(body, { status: result.status, statusText: result.statusText, headers });
455
469
  }
456
470
  };
471
+ function timedOut(port, path, timeoutMs) {
472
+ const message = `The sandbox preview gave up waiting for ${path} on port ${port} after ${Math.round(timeoutMs / 1e3)}s.
473
+
474
+ The server inside the container has not finished this response. Preview responses are delivered whole rather than streamed, so a long one \u2014 a model's answer, a server-sent-event stream \u2014 is outstanding until it ends.
475
+
476
+ Raise createPreview(box, { timeoutMs }) if this work legitimately takes longer.`;
477
+ return {
478
+ status: 504,
479
+ statusText: "Gateway Timeout",
480
+ headers: { "content-type": "text/plain; charset=utf-8" },
481
+ body: new TextEncoder().encode(message).buffer
482
+ };
483
+ }
457
484
  function isolated(response) {
458
485
  const headers = new Headers(response.headers);
459
486
  headers.set("Content-Type", "text/plain; charset=utf-8");
@@ -539,6 +566,8 @@ worker.addEventListener("message", (event) => {
539
566
  host = event.ports[0] ?? null;
540
567
  if (!host) return;
541
568
  options.injectSockets = event.data.injectSockets !== false;
569
+ const timeoutMs = event.data.timeoutMs;
570
+ if (typeof timeoutMs === "number" && timeoutMs > 0) options.timeoutMs = timeoutMs;
542
571
  host.onmessage = (message) => {
543
572
  const reply = message.data;
544
573
  router.settle(reply.id, reply.response);
@@ -549,7 +578,13 @@ worker.addEventListener("message", (event) => {
549
578
  });
550
579
  worker.addEventListener("fetch", (event) => {
551
580
  const url = new URL(event.request.url);
552
- if (url.origin !== worker.location.origin) return;
581
+ if (url.origin !== worker.location.origin) {
582
+ const port2 = router.portFor(event.clientId);
583
+ const target = port2 === void 0 ? null : containerTarget(url.href);
584
+ if (!target) return;
585
+ event.respondWith(router.fetch(target.port, target.path, event.request, event.clientId));
586
+ return;
587
+ }
553
588
  const claimed = router.claimedPort(url.pathname);
554
589
  if (claimed !== null && event.request.mode === "navigate") {
555
590
  router.bind(event.resultingClientId || event.clientId, claimed);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sandboxedjs",
3
- "version": "0.1.97",
3
+ "version": "0.1.100",
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",