sandboxedjs 0.1.28 → 0.1.30

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/LICENSE CHANGED
@@ -18,10 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
22
-
23
- ---
24
-
25
- This package depends on @scelar/nodepod, which is licensed MIT with the
26
- Commons Clause. Nodepod is not redistributed here; it is installed as a normal
27
- npm dependency.
21
+ SOFTWARE.
package/README.md CHANGED
@@ -617,11 +617,21 @@ export default {
617
617
  "Cross-Origin-Opener-Policy": "same-origin",
618
618
  "Cross-Origin-Embedder-Policy": "require-corp",
619
619
  } },
620
+ // Required as well — see below.
621
+ optimizeDeps: { exclude: ["@rolldown/binding-wasm32-wasi"] },
620
622
  };
621
623
  ```
622
624
 
623
625
  Production hosting must return the same two headers. Without them SandboxedJS reports a direct
624
- configuration error when Vite/Rolldown starts. The compiler is loaded only when an installed
626
+ configuration error when Vite/Rolldown starts.
627
+
628
+ `optimizeDeps.exclude` matters just as much, and fails less obviously. The binding starts its
629
+ WASI worker with `new Worker(new URL("@rolldown/binding-wasm32-wasi/wasi-worker-browser.mjs",
630
+ import.meta.url))`. A bundler that pre-bundles the package rewrites that URL to a path inside its
631
+ own dependency cache, where the worker file does not exist — so the dev server answers with
632
+ `index.html` and the browser rejects it for its MIME type, several layers away from anything that
633
+ mentions Rolldown. Excluding the binding leaves it served from `node_modules`, where that URL
634
+ resolves. The compiler is loaded only when an installed
625
635
  project actually contains Rolldown, so ordinary container boot does not pay its WASM startup cost.
626
636
 
627
637
  **Not done — these things still stand between this and a complete browser IDE.**
@@ -758,6 +768,83 @@ Two caveats worth knowing:
758
768
  itself reports status correctly and can be relied on in `&&` chains.
759
769
  - **No hardware acceleration and no native codecs** beyond what the WebAssembly build ships.
760
770
 
771
+ ## Reaching a container's servers from the page
772
+
773
+ A server inside the container is not on the network, so there is no `http://localhost:5173`
774
+ your page can navigate to. There are three ways to reach one, and the right choice depends on
775
+ whether you need a whole site or a single response — and on whether you trust the code.
776
+
777
+ **1. One response, into a variable.** `request()` speaks to a server directly and hands back the
778
+ bytes. Nothing is executed, so there is nothing to be careful about.
779
+
780
+ ```ts
781
+ const res = await box.request(5173, { path: "/api/items" });
782
+ element.textContent = res.body; // text
783
+ const items = res.json<Item[]>(); // parsed
784
+ const bytes = res.bytes; // exact bytes, for images and source maps
785
+ ```
786
+
787
+ **2. One response, rendered — safely.** `renderInto` puts the response in an iframe with
788
+ `sandbox="allow-scripts"` and deliberately *without* `allow-same-origin`. The document lands in an
789
+ opaque origin: its scripts run, and they can reach neither your DOM nor your cookies and storage.
790
+
791
+ ```ts
792
+ import { renderInto } from "sandboxedjs";
793
+ await renderInto(box, document.querySelector("#preview")!, { port: 5173 });
794
+ ```
795
+
796
+ This is the option to reach for when the code came from somewhere you do not control. Its limit is
797
+ that only that one response exists — a page asking for `/main.js` gets nothing, because there is no
798
+ origin to serve it from. Good for generated HTML, a chart, a rendered document.
799
+
800
+ **3. A whole site, with real URLs.** `createPreview` registers a service worker that gives the
801
+ container's ports working URLs, so an iframe can load a dev server with all its subresources.
802
+
803
+ ```ts
804
+ import { createPreview } from "sandboxedjs";
805
+ const preview = await createPreview(box); // null where service workers are unavailable
806
+ iframe.src = preview!.urlFor(5173);
807
+ ```
808
+
809
+ Requests are routed by *which client is asking* rather than by path, so a dev server's absolute
810
+ URLs — `/src/main.js`, `/@vite/client` — resolve without rewriting anything.
811
+
812
+ > **This one runs guest code on your origin.** A service worker can only serve URLs under the
813
+ > origin that registered it, so scripts in the preview can reach `window.parent`, your cookies and
814
+ > your `localStorage`. The sandbox contains a program's *filesystem and process table*, not the
815
+ > page it serves. Use it for code you trust; for anything else, serve the preview from a separate
816
+ > origin (a subdomain pointed at the same app) or stay with option 2.
817
+
818
+ Service workers need a secure context, and some embedded browsers disable them entirely — hence
819
+ the `null` return rather than a throw. Responses are served with both
820
+ `Cross-Origin-Resource-Policy` and `Cross-Origin-Embedder-Policy`, so a preview still frames
821
+ correctly inside the cross-origin isolated page that Rolldown requires.
822
+
823
+ ## Isolation
824
+
825
+ By default each guest program runs on its own thread, in a Worker. The volume, the kernel, the
826
+ coreutils and the process table stay on the thread that created the container, and the program
827
+ reaches them through a `SharedArrayBuffer` channel it can wait on synchronously.
828
+
829
+ Two things follow from that. Guest code no longer shares your page's realm, so it cannot reach
830
+ your application's globals. And `child_process.execSync`, `spawnSync` and `execFileSync` work —
831
+ they cannot be expressed any other way, because a synchronous call has to block its caller while
832
+ the child it is waiting for still makes progress, which is impossible when both are the same
833
+ thread.
834
+
835
+ It is chosen automatically and falls back on its own, so nothing that runs today stops running:
836
+
837
+ | Condition | What happens |
838
+ |---|---|
839
+ | `SharedArrayBuffer` unavailable, or the page is not cross-origin isolated | Falls back to the in-realm runtime |
840
+ | The guest bundle cannot be loaded (a bundler moved or rewrote it) | Falls back to the in-realm runtime |
841
+ | `modules` supplied by the host | Falls back — live JavaScript objects cannot cross a thread |
842
+ | The project has `rolldown` installed | *That process* runs in-realm; the rest still get a thread |
843
+
844
+ In a browser this needs the same COOP/COEP headers Rolldown does, which is the usual reason to
845
+ find yourself in the fallback. `createContainer({ isolation: "realm" })` opts out entirely, and
846
+ `workerUrl` points at the guest bundle when a bundler has moved it.
847
+
761
848
  ## Known limits
762
849
 
763
850
  Honest list of what does not work:
@@ -765,15 +852,25 @@ Honest list of what does not work:
765
852
  - **Compiled native addons.** A `.node` file cannot be loaded, so a package that ships one has to
766
853
  have a JavaScript or WebAssembly build to fall back on. `rollup` and `esbuild` do, and the
767
854
  runtime redirects those two names to `@rollup/wasm-node` and `esbuild-wasm` automatically when
768
- they are installed. Vite 8 is supported through Rolldown's official WASI build (with the browser
769
- isolation headers above). Express, Koa, Fastify-style apps and plain `http` servers also run.
855
+ they are installed. Vite 8 is supported through Rolldown's official WASI build **in a browser
856
+ host** (with the two configuration steps above). Express, Koa, Fastify-style apps and plain
857
+ `http` servers also run.
858
+ - **Vite 8 / Rolldown does not work when the host is Node.** The binding has two builds. The
859
+ browser one owns a `memfs` volume, which SandboxedJS mirrors the project into; the Node one
860
+ builds a `node:wasi` instance that preopens the *real* filesystem root, and there is no way to
861
+ hand it a different one. Rolldown therefore looks for `/app/my-app/index.html` on your actual
862
+ disk, does not find it, and the dev server answers every request with its fallback page. Vite 7
863
+ works on both hosts and is what the Node acceptance test pins.
770
864
  - **Concurrent browser Rolldown projects need distinct absolute working directories.** The
771
865
  official binding owns one WASI memfs per page; SandboxedJS mirrors each project into it before
772
866
  startup. Two live projects using the same path such as `/workspace` can overwrite that mirror.
773
- - **`child_process` is asynchronous only.** `spawn`, `exec` and `execFile` run through the kernel,
774
- so a child sees the same filesystem and coreutils as the shell. `execSync`, `spawnSync` and
775
- `execFileSync` throw `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM`: blocking the JavaScript thread on
776
- another process is not expressible here.
867
+ - **`child_process` is synchronous only under the Worker pod.** `spawn`, `exec` and `execFile`
868
+ always work and run through the kernel, so a child sees the same filesystem and coreutils as the
869
+ shell. `execSync`, `spawnSync` and `execFileSync` need the guest program to be on its own thread
870
+ see *Isolation* above. Where it is, they work; where it is not, they throw
871
+ `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM` naming the command they were asked to run, and the way
872
+ through is to answer **No** to a prompt like `npm create vite`'s "Install with npm and start
873
+ now?" and run `npm install && npm run dev` from the shell instead.
777
874
  - **No `net`, `tls`, `worker_threads` or `vm`.** `http` and `https` are served by a virtual stack
778
875
  that `request()` talks to directly, so servers work; raw sockets do not.
779
876
  - **Python is CPython via Pyodide.** The standard library is the real one. A C