sandboxedjs 0.1.22 → 0.1.24

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 CHANGED
@@ -4,8 +4,9 @@ A Linux-like container that runs entirely inside a Node.js process. No Docker, n
4
4
  modules — a virtual filesystem, a POSIX shell, 154 Unix programs, and both Node.js and Python
5
5
  runtimes, all in-process.
6
6
 
7
- Built on [Nodepod](https://github.com/R1ck404/Nodepod), which supplies the Node.js execution
8
- engine and the in-memory volume.
7
+ The Node.js runtime is its own: a module engine that loads both CommonJS and ES modules, an
8
+ in-memory volume, an npm-registry installer and a virtual HTTP stack, with no native modules and
9
+ no dependency on the host's `node_modules`.
9
10
 
10
11
  ```ts
11
12
  import { createContainer } from "sandboxedjs";
@@ -43,7 +44,7 @@ Node 18.17+. Everything is pure JavaScript and WebAssembly — no compilation st
43
44
  | **Filesystem** | Full FHS tree (`/etc`, `/usr`, `/var`, `/home`, …), permissions, ownership, symlinks, hard links, `umask`, sticky bits |
44
45
  | **Shell** | POSIX `sh` (54 builtins) — pipelines, redirection, here-docs, globbing, brace/parameter/arithmetic/command expansion, functions, `if`/`for`/`while`/`case`/`select`, job control, traps, arrays, `[[ ]]`, `(( ))` |
45
46
  | **Coreutils** | 154 programs — `ls cat cp mv rm mkdir grep sed awk find head tail sort uniq wc cut tr tee xargs chmod chown ln du df ps tar gzip base64 sha256sum diff curl wget` and the rest — plus 54 shell builtins |
46
- | **Node.js** | Real Node semantics via Nodepod — `require`, npm packages, `http`, `fs`, streams, `worker_threads` |
47
+ | **Node.js** | Its own module engine — `require` *and* `import`, live bindings, `exports` maps, top-level `await`, npm packages, `http`, `fs`, streams, `child_process` |
47
48
  | **Python** | CPython 3.13 (Pyodide) on the *same* filesystem, with the real standard library |
48
49
  | **FFmpeg** | `ffmpeg` and `ffprobe` (FFmpeg 5.1) reading and writing container files directly — [optional install](#video-and-audio) |
49
50
  | **`/proc`** | Live and synthesised — `ps`, `top`, `free` and `uptime` all read the same source |
@@ -219,10 +220,11 @@ The iframe is cross-origin, so your IDE cannot read its DOM. You can still:
219
220
  #### In a pure browser IDE
220
221
 
221
222
  `expose()` opens a real `node:http` listener, so it needs a Node host — an Electron app, or a web
222
- IDE with a Node backend. With no backend at all, the preview goes through Nodepod's service
223
- worker instead of this package: boot Nodepod's browser build, pass it in with
224
- `createContainer({ pod })`, and use Nodepod's own preview iframe support. I have not verified
225
- that path; see [Running in a browser](#running-in-a-browser).
223
+ IDE with a Node backend. With no backend at all, an iframe needs a real URL to load, and giving
224
+ it one means a service worker that intercepts requests and routes them to `request()`. That is
225
+ not built yet; see [Running in a browser](#running-in-a-browser). Until it is, you can still
226
+ drive an in-container server programmatically through `request()` and render the result
227
+ yourself.
226
228
 
227
229
  ### npm and npx
228
230
 
@@ -517,10 +519,14 @@ so it can read and write any path in the container regardless of `user`:
517
519
  await box.exec("node -e \"require('fs').readFileSync('/root/secret')\"", { user: "agent" }); // succeeds
518
520
  ```
519
521
 
520
- Node code runs inside Nodepod's worker, which owns the volume and has no notion of container
521
- uids. Treat `user` as a way to model *ordinary* multi-user behaviour, not as a privilege
522
- boundary for JavaScript you do not trust. If untrusted JavaScript must not see something, keep
523
- it out of the container rather than relying on file modes.
522
+ Node code runs on the JavaScript runtime, which owns its own volume and has no notion of
523
+ container uids. Treat `user` as a way to model *ordinary* multi-user behaviour, not as a
524
+ privilege boundary for JavaScript you do not trust. If untrusted JavaScript must not see
525
+ something, keep it out of the container rather than relying on file modes.
526
+
527
+ The runtime also shares the host's JavaScript realm today rather than running in a worker, so a
528
+ program inside the container can reach host globals. Isolating it in a worker is planned; until
529
+ then, do not treat the container as a boundary against hostile code.
524
530
 
525
531
  ### And it is not a VM
526
532
 
@@ -579,9 +585,8 @@ resolve their implementation at call time (`node:zlib`/`node:crypto` on Node,
579
585
  `CompressionStream`/`crypto.subtle` plus a JS MD5 in a browser), so nothing pulls a `node:`
580
586
  builtin in when the module loads and a bundler will not fail on it.
581
587
 
582
- **One call, either side.** `createContainer()` picks its own host: the headless
583
- `worker_threads` engine on Node, Nodepod's browser build in a browser. You do not import Nodepod,
584
- and you do not pass a pod:
588
+ **One call, either side.** `createContainer()` boots its own runtime. There is no host to pick
589
+ and no pod to pass:
585
590
 
586
591
  ```ts
587
592
  import { createContainer } from "sandboxedjs";
@@ -592,40 +597,32 @@ await box.exec("ls -la /"); // shell + coreutils
592
597
  await box.exec("node app/index.js");
593
598
  ```
594
599
 
595
- **Serve the service worker.** In a browser, a container's preview iframe reaches an HTTP server
596
- running inside the page through a service worker, and a browser will not register one from
597
- `node_modules` — it has to come from your own origin at `/__sw__.js`. One line:
598
-
599
- ```ts
600
- // vite.config.ts
601
- import sandboxedjs from "sandboxedjs/vite";
602
- export default defineConfig({ plugins: [sandboxedjs()] });
603
- ```
604
-
605
- For anything else, `sandboxedjs/server` exports `serveSW()` (Fetch-style hosts) and
606
- `serveSWNode()` (Express, Fastify, `node:http`). Skip it with
607
- `createContainer({ browser: { serviceWorker: false } })`, which also disables preview iframes.
600
+ **Not done three things stand between this and a browser IDE.**
608
601
 
609
- You can still boot the pod yourself and pass it as `pod` when you need control over Nodepod's
610
- own options.
611
-
612
- **Verified in a browser.** Booted under Vite, the following run:
613
-
614
- ```
615
- $ uname -a Linux sandbox 5.10.0 (sandboxedjs@nodepod) x86_64 GNU/Linux
616
- $ ls -la /app the mounted files
617
- $ grep beta … beta
618
- $ node /app/hello.js node runtime says hi
619
- ```
602
+ - *A service worker.* A preview iframe needs a real URL, and giving it one means a worker on your
603
+ own origin that intercepts requests and routes them into the container. `request()` works
604
+ everywhere and is unaffected; only the iframe needs this. Earlier versions re-exported one from
605
+ the package this was built on, at `sandboxedjs/vite` and `sandboxedjs/server`. Both are gone
606
+ along with that dependency, and a replacement is not written yet.
607
+ - *Worker isolation.* The runtime executes in whichever realm you boot it from, which in a browser
608
+ is the main thread — so a long build blocks the UI, and container code can reach page globals.
609
+ `comlink` is a dependency in anticipation of this and is not used yet.
610
+ - *esbuild.* The runtime cannot execute any build of esbuild itself: one dlopens a compiled addon,
611
+ the other drives a Go program through facilities the sandbox does not have. On Node it borrows
612
+ the host's `esbuild-wasm`, which is why Vite's dev server works there. In a browser there is
613
+ nothing to borrow, so anything routed through esbuild fails until esbuild-wasm runs *inside*
614
+ the sandbox.
620
615
 
621
- Shell, coreutils, the virtual filesystem and the Node runtime all work. Boot costs about ten
622
- seconds, nearly all of it Nodepod starting.
616
+ Everything else is already browser-shaped. Nothing in the runtime imports a `node:` builtin except
617
+ through an explicitly Node-only path that returns `null` elsewhere, compression and hashing pick
618
+ their implementation at call time, and the module engine, volume, HTTP stack and npm installer are
619
+ built on `fetch`, `acorn`, `resolve.exports`, `@noble/hashes` and `pako`.
623
620
 
624
621
  **What does not work in a browser:**
625
622
 
626
623
  - `copyIn()` / `copyOut()` — they read and write the host filesystem, which does not exist.
627
- - `expose()` — it opens a real `node:http` listener. Use the service worker and a preview iframe
628
- to reach an in-container server instead of a host port.
624
+ - `expose()` — it opens a real `node:http` listener. Use `request()` to reach an in-container
625
+ server instead of a host port.
629
626
  - `python3` — see below.
630
627
  - The `sandboxedjs` CLI, obviously.
631
628
 
@@ -736,11 +733,18 @@ Two caveats worth knowing:
736
733
 
737
734
  Honest list of what does not work:
738
735
 
739
- - **esbuild-based toolchains.** Nodepod boots esbuild by importing it from a CDN over `https:`,
740
- which the Node ESM loader refuses, so anything routed through esbuild cannot start under Node.
741
- This affects older toolchains Vite 4 and its contemporaries. Current Vite ships its Rust
742
- pipeline as WebAssembly and runs fine, dev server included, as do Express, Koa, Fastify-style
743
- apps and plain `http` servers.
736
+ - **Compiled native addons.** A `.node` file cannot be loaded, so a package that ships one has to
737
+ have a JavaScript or WebAssembly build to fall back on. `rollup` and `esbuild` do, and the
738
+ runtime redirects those two names to `@rollup/wasm-node` and `esbuild-wasm` automatically when
739
+ they are installed. **Vite 8 does not work** for this reason: it builds on Rolldown, whose only
740
+ sandbox-viable binding needs WASI and `worker_threads`. Vite 7 runs, dev server included, as do
741
+ Express, Koa, Fastify-style apps and plain `http` servers.
742
+ - **`child_process` is asynchronous only.** `spawn`, `exec` and `execFile` run through the kernel,
743
+ so a child sees the same filesystem and coreutils as the shell. `execSync`, `spawnSync` and
744
+ `execFileSync` throw `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM`: blocking the JavaScript thread on
745
+ another process is not expressible here.
746
+ - **No `net`, `tls`, `worker_threads` or `vm`.** `http` and `https` are served by a virtual stack
747
+ that `request()` talks to directly, so servers work; raw sockets do not.
744
748
  - **Python is CPython via Pyodide.** The standard library is the real one. A C
745
749
  extension works only if it has been built for WebAssembly — Pyodide ships
746
750
  many, including `numpy`, but an arbitrary wheel from PyPI will not install.
@@ -772,6 +776,9 @@ Honest list of what does not work:
772
776
  | `timezone` | `string` | `"UTC"` | |
773
777
  | `timeoutMs` | `number` | none | Default limit for `exec` |
774
778
  | `onStdout` / `onStderr` | `(chunk: string) => void` | — | Container-wide output taps |
779
+ | `onServerReady` | `(port, url) => void` | — | Fires when something inside starts listening |
780
+ | `pod` | `RuntimePod` | booted for you | Share or substitute the JavaScript runtime |
781
+ | `python` | `PythonOptions` | jsDelivr | Where to load Pyodide from |
775
782
 
776
783
  ### `Container`
777
784
 
@@ -825,5 +832,4 @@ agent sandbox, and a browser terminal.
825
832
 
826
833
  ## License
827
834
 
828
- MIT. Nodepod itself is MIT with the Commons Clause; this package depends on it rather than
829
- redistributing it.
835
+ MIT, with no dependency carrying a stricter licence.