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 +1 -7
- package/README.md +104 -7
- package/dist/index.cjs +1014 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +321 -8
- package/dist/index.d.ts +321 -8
- package/dist/index.js +1010 -54
- package/dist/index.js.map +1 -1
- package/dist/service-worker.js +112 -0
- package/dist/service-worker.js.map +1 -0
- package/dist/worker-entry.js +30825 -0
- package/dist/worker-entry.js.map +1 -0
- package/package.json +1 -1
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.
|
|
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
|
|
769
|
-
|
|
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
|
|
774
|
-
so a child sees the same filesystem and coreutils as the
|
|
775
|
-
`
|
|
776
|
-
|
|
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
|