sandboxedjs 0.2.9 → 0.2.11

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
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- <img src="https://raw.githubusercontent.com/Sharjeelbaig/sandboxedjs/main/assets/logo.png" alt="SandboxedJS" width="140" />
3
+ <img src="assets/logo.png" alt="SandboxedJS" width="140" />
4
4
 
5
5
  # SandboxedJS
6
6
 
@@ -29,7 +29,7 @@ const box = await createContainer({
29
29
  });
30
30
 
31
31
  await box.exec("ls -la /app");
32
- await box.exec("node /app/hello.js"); // → hi from linux
32
+ await box.exec("node /app/hello.js"); // → hi from linux
33
33
  await box.exec("python3 -c 'print(2**64)'"); // → 18446744073709551616
34
34
 
35
35
  box.dispose();
@@ -54,16 +54,16 @@ Node 18.17+. Pure JavaScript and WebAssembly — no build step, no native module
54
54
 
55
55
  ## What's inside
56
56
 
57
- | | |
58
- |---|---|
59
- | **Filesystem** | A full FHS tree in memory (`/etc`, `/usr`, `/var`, `/home`), permissions, ownership, symlinks, hard links, `umask` |
60
- | **Shell** | POSIX `sh` — pipes, redirection, here-docs, globbing, functions, loops, job control, traps, `[[ ]]`, `(( ))` |
61
- | **Commands** | 160 programs: `ls cat grep sed awk find sort tar gzip curl wget diff sha256sum ps top` … |
62
- | **Node.js** | Its own runtime reporting v22.12.0 — CommonJS *and* ESM, `http`, `fs`, streams, `child_process`, npm and npx |
63
- | **Python** | Source-built CPython 3.13 (WASM) on the same filesystem, with `pip` |
64
- | **WebAssembly** | A WASI preview1 host: any `wasm32-wasi` binary runs as an ordinary process |
65
- | **FFmpeg** | `ffmpeg` / `ffprobe` 5.1 over container files — [optional install](#video-and-audio) |
66
- | **Network** | In-container HTTP servers, loopback, live previews, and a proxy for real outbound calls |
57
+ | | |
58
+ | --------------- | ------------------------------------------------------------------------------------------------------------------ |
59
+ | **Filesystem** | A full FHS tree in memory (`/etc`, `/usr`, `/var`, `/home`), permissions, ownership, symlinks, hard links, `umask` |
60
+ | **Shell** | POSIX `sh` — pipes, redirection, here-docs, globbing, functions, loops, job control, traps, `[[ ]]`, `(( ))` |
61
+ | **Commands** | 160 programs: `ls cat grep sed awk find sort tar gzip curl wget diff sha256sum ps top` … |
62
+ | **Node.js** | Its own runtime reporting v22.12.0 — CommonJS _and_ ESM, `http`, `fs`, streams, `child_process`, npm and npx |
63
+ | **Python** | Source-built CPython 3.13 (WASM) on the same filesystem, with `pip` |
64
+ | **WebAssembly** | A WASI preview1 host: any `wasm32-wasi` binary runs as an ordinary process |
65
+ | **FFmpeg** | `ffmpeg` / `ffprobe` 5.1 over container files — [optional install](#video-and-audio) |
66
+ | **Network** | In-container HTTP servers, loopback, live previews, and a proxy for real outbound calls |
67
67
 
68
68
  One filesystem underneath all of it: a file written by `echo` is read by `require('fs')` and by
69
69
  Python's `open()`, in any direction.
@@ -71,13 +71,16 @@ Python's `open()`, in any direction.
71
71
  ## Quick tour
72
72
 
73
73
  ```ts
74
- const box = await createContainer({ cwd: "/app", network: { allowOutbound: true } });
74
+ const box = await createContainer({
75
+ cwd: "/app",
76
+ network: { allowOutbound: true },
77
+ });
75
78
 
76
79
  await box.exec("npm install express", { cwd: "/app", timeoutMs: 300_000 });
77
80
  box.spawn("node server.js", { cwd: "/app" });
78
81
  await box.waitForPort(3000);
79
82
 
80
- const res = await box.request(3000, { path: "/api" }); // talk to it from your code
83
+ const res = await box.request(3000, { path: "/api" }); // talk to it from your code
81
84
  console.log(res.status, res.json());
82
85
  ```
83
86
 
@@ -87,17 +90,21 @@ A stateful shell, when `exec` alone is too forgetful:
87
90
  const session = box.session();
88
91
  await session.run("cd /app");
89
92
  await session.run("export TOKEN=abc");
90
- await session.run("echo $TOKEN in $(pwd)"); // → abc in /app
93
+ await session.run("echo $TOKEN in $(pwd)"); // → abc in /app
91
94
  ```
92
95
 
93
96
  Files in and out, snapshots, and a terminal you can wire to xterm.js:
94
97
 
95
98
  ```ts
96
99
  await box.fs.writeFile("/app/config.json", JSON.stringify(config));
97
- const snapshot = box.snapshot(); // serialisable; restore() later
100
+ const snapshot = box.snapshot(); // serialisable; restore() later
98
101
 
99
102
  import { Terminal } from "sandboxedjs";
100
- const terminal = new Terminal(box.session(), { write: (d) => xterm.write(d), columns: 80, rows: 24 });
103
+ const terminal = new Terminal(box.session(), {
104
+ write: (d) => xterm.write(d),
105
+ columns: 80,
106
+ rows: 24,
107
+ });
101
108
  xterm.onData((d) => terminal.input(d));
102
109
  terminal.start();
103
110
  ```
@@ -118,7 +125,7 @@ Two different problems hide in that sentence, and SandboxedJS solves them in two
118
125
  frontend (:3000) ──▶ http://localhost:8000/agent/run ──▶ FastAPI (:8000)
119
126
  ```
120
127
 
121
- Both servers are inside the container, so `localhost:8000` is *true there* — it means the
128
+ Both servers are inside the container, so `localhost:8000` is _true there_ — it means the
122
129
  container's own backend, never your machine's. Leave the URL exactly as the project writes it. In a
123
130
  live preview, an injected script rewrites loopback addresses to the preview's origin and the
124
131
  service worker routes them back in by port, so the same code works in the frame without a proxy
@@ -129,21 +136,27 @@ import { createContainer, createPreview } from "sandboxedjs";
129
136
 
130
137
  // `allowOutbound` is still what lets the project install and call out at all;
131
138
  // the proxy is how those calls leave a browser, not permission to make them.
132
- const box = await createContainer({ files: project, network: { allowOutbound: true } });
139
+ const box = await createContainer({
140
+ files: project,
141
+ network: { allowOutbound: true },
142
+ });
133
143
 
134
- await box.exec("pip install -r requirements.txt", { cwd: "/app/backend", timeoutMs: 600_000 });
144
+ await box.exec("pip install -r requirements.txt", {
145
+ cwd: "/app/backend",
146
+ timeoutMs: 600_000,
147
+ });
135
148
  await box.exec("npm install", { cwd: "/app/frontend", timeoutMs: 600_000 });
136
149
 
137
150
  box.spawn("fastapi run", { cwd: "/app/backend" });
138
151
  box.spawn("npm run dev", { cwd: "/app/frontend" });
139
152
  await box.waitForPort(3000, { timeoutMs: 60_000 });
140
153
 
141
- const preview = await createPreview(box); // null where service workers are unavailable
154
+ const preview = await createPreview(box); // null where service workers are unavailable
142
155
  iframe.src = preview!.urlFor(3000);
143
156
  ```
144
157
 
145
158
  It works between preview pages and from another tab of the same browser while the owner page is
146
- open. It does not work from Postman, curl or another machine — the container *is* the tab; there is
159
+ open. It does not work from Postman, curl or another machine — the container _is_ the tab; there is
147
160
  no server anywhere to reach.
148
161
 
149
162
  ### 2. Backend → the internet: one file, once
@@ -155,7 +168,7 @@ FastAPI ──▶ egress proxy on your own origin ──▶ https://ollama.com
155
168
 
156
169
  A page may only read a response from a host that sends CORS headers, and most APIs send none — an
157
170
  `Authorization` header alone forces a preflight plenty of them answer with 405. That is the
158
- browser's rule about *pages*, not a container limit, and the only way through it is a request made
171
+ browser's rule about _pages_, not a container limit, and the only way through it is a request made
159
172
  somewhere a page is not. So add one to the project you already deploy:
160
173
 
161
174
  ```bash
@@ -167,13 +180,13 @@ Netlify are detected, `--target` names one. Nothing else changes: a container in
167
180
  its own origin for that proxy before giving up, so **no project passes a `proxy` option and no
168
181
  project is configured twice.**
169
182
 
170
- | Where you are | What to do |
171
- |---|---|
172
- | Deployed (Pages, Vercel, Netlify) | `npx sandboxedjs-egress init --allow …`, then deploy |
173
- | Developing | `npx sandboxedjs-egress` — the probe checks its port |
174
- | You already have a server | `app.use(EGRESS_PATH, egressNodeHandler({ allow: [...] }))`, before any body parser |
175
- | Using `npx sandboxedjs-serve` | Nothing; it mounts the proxy itself |
176
- | Node, not a browser | Nothing; there is no CORS to work around |
183
+ | Where you are | What to do |
184
+ | --------------------------------- | ----------------------------------------------------------------------------------- |
185
+ | Deployed (Pages, Vercel, Netlify) | `npx sandboxedjs-egress init --allow …`, then deploy |
186
+ | Developing | `npx sandboxedjs-egress` — the probe checks its port |
187
+ | You already have a server | `app.use(EGRESS_PATH, egressNodeHandler({ allow: [...] }))`, before any body parser |
188
+ | Using `npx sandboxedjs-serve` | Nothing; it mounts the proxy itself |
189
+ | Node, not a browser | Nothing; there is no CORS to work around |
177
190
 
178
191
  ```ts
179
192
  // Your own Express/Node server:
@@ -182,12 +195,13 @@ app.use(EGRESS_PATH, egressNodeHandler({ allow: ["ollama.com"] }));
182
195
 
183
196
  // A static host — this is what `init` writes for you:
184
197
  import { handleEgressRequest } from "sandboxedjs/egress";
185
- export const onRequest = ({ request }) => handleEgressRequest(request, { allow: ["ollama.com"] });
198
+ export const onRequest = ({ request }) =>
199
+ handleEgressRequest(request, { allow: ["ollama.com"] });
186
200
  ```
187
201
 
188
202
  Every exit honours it — `curl`, `wget`, a guest's `fetch`, Python's sockets and `httpx` — so it
189
203
  means the same thing whatever the project is written in. Loopback still stays inside the container,
190
- and the container's own policy still applies first: the proxy widens what a *page* can reach, never
204
+ and the container's own policy still applies first: the proxy widens what a _page_ can reach, never
191
205
  what the container is allowed to.
192
206
 
193
207
  > ⚠️ Anything that can reach the proxy can make requests through it carrying whatever credentials
@@ -202,11 +216,13 @@ no meta tag or polyfill can add them later:
202
216
  ```ts
203
217
  // vite.config.ts
204
218
  export default {
205
- server: { headers: {
206
- "Cross-Origin-Opener-Policy": "same-origin",
207
- "Cross-Origin-Embedder-Policy": "require-corp",
208
- } },
209
- optimizeDeps: { exclude: ["@rolldown/binding-wasm32-wasi"] }, // see below
219
+ server: {
220
+ headers: {
221
+ "Cross-Origin-Opener-Policy": "same-origin",
222
+ "Cross-Origin-Embedder-Policy": "require-corp",
223
+ },
224
+ },
225
+ optimizeDeps: { exclude: ["@rolldown/binding-wasm32-wasi"] }, // see below
210
226
  };
211
227
  ```
212
228
 
@@ -238,13 +254,13 @@ filesystem and a real socket), and the CLI. They fail only if you call them.
238
254
 
239
255
  ### Showing what runs
240
256
 
241
- | Need | Use | Runs guest code on your origin? |
242
- |---|---|---|
243
- | One response as data | `box.request(port, init)` | No — nothing executes |
244
- | One response rendered safely | `renderInto(box, el, { port })` | No — opaque-origin iframe, no `allow-same-origin` |
245
- | A whole site with real URLs | `createPreview(box)` → `urlFor(port)` | **Yes** — trusted code only |
257
+ | Need | Use | Runs guest code on your origin? |
258
+ | ---------------------------- | ------------------------------------- | ------------------------------------------------- |
259
+ | One response as data | `box.request(port, init)` | No — nothing executes |
260
+ | One response rendered safely | `renderInto(box, el, { port })` | No — opaque-origin iframe, no `allow-same-origin` |
261
+ | A whole site with real URLs | `createPreview(box)` → `urlFor(port)` | **Yes** — trusted code only |
246
262
 
247
- `createPreview` is what makes a dev server work: requests route by *which client is asking*, so
263
+ `createPreview` is what makes a dev server work: requests route by _which client is asking_, so
248
264
  `/src/main.js` and `/@vite/client` resolve without rewriting anything, and a tunnelled `WebSocket`
249
265
  carries HMR. Serve it from a separate origin for code you did not write. On Node, `box.expose(port)`
250
266
  gives a real loopback URL instead. Full lifecycle notes: [Server previews](docs/server-previews.md).
@@ -274,9 +290,15 @@ not a runtime dependency, so install it alongside your model provider.
274
290
  ```ts
275
291
  import { SandboxedJsBackend, installSandboxSkills } from "sandboxedjs/agent";
276
292
 
277
- const box = await createContainer({ cwd: "/app", network: { allowOutbound: true } });
293
+ const box = await createContainer({
294
+ cwd: "/app",
295
+ network: { allowOutbound: true },
296
+ });
278
297
  await installSandboxSkills(box);
279
- const agent = createDeepAgent({ model, backend: new SandboxedJsBackend(box, { cwd: "/app" }) });
298
+ const agent = createDeepAgent({
299
+ model,
300
+ backend: new SandboxedJsBackend(box, { cwd: "/app" }),
301
+ });
280
302
  ```
281
303
 
282
304
  ## Video and audio
@@ -287,7 +309,9 @@ filesystem — inputs and outputs are ordinary container files, so pipelines wor
287
309
  report themselves missing, as a real system does.
288
310
 
289
311
  ```js
290
- await box.exec("ffmpeg -f lavfi -i testsrc=size=640x480:rate=25:duration=5 -pix_fmt yuv420p clip.mp4");
312
+ await box.exec(
313
+ "ffmpeg -f lavfi -i testsrc=size=640x480:rate=25:duration=5 -pix_fmt yuv420p clip.mp4",
314
+ );
291
315
  await box.exec("ffmpeg -i clip.mp4 -vf scale=320:-2 -frames:v 1 thumb.png");
292
316
  ```
293
317
 
@@ -305,7 +329,7 @@ What it is **not**:
305
329
  - **Not a VM.** Everything runs in your JavaScript engine. A true escape is an engine escape. This
306
330
  is isolation from mistakes and ordinary untrusted programs, not from a determined attacker.
307
331
  - **The user model does not constrain Node.** `user: "agent"` is enforced for the shell, the
308
- commands and Python — `cat /root/secret` is denied for real. It is *not* enforced for `node`,
332
+ commands and Python — `cat /root/secret` is denied for real. It is _not_ enforced for `node`,
309
333
  which reaches the volume directly. Model ordinary multi-user behaviour with it; do not treat it
310
334
  as a privilege boundary for JavaScript you do not trust.
311
335
  - **A preview shares your origin.** `createPreview` serves guest code from the page that registered
@@ -323,7 +347,7 @@ An honest list:
323
347
 
324
348
  - **No compiled native addons.** A `.node` file cannot load; a package needs a JS or WASM fallback.
325
349
  `rollup` and `esbuild` get redirected to `@rollup/wasm-node` and `esbuild-wasm` automatically.
326
- - **esbuild cannot run *inside* the sandbox**, so tools that call it directly fail. On Node it
350
+ - **esbuild cannot run _inside_ the sandbox**, so tools that call it directly fail. On Node it
327
351
  borrows the host's `esbuild-wasm`.
328
352
  - **Vite 8 / Rolldown is browser-only.** The Node binding preopens the real filesystem root and
329
353
  cannot be handed another, so it looks for your project on the actual disk. Vite 7 works on both.
@@ -333,7 +357,7 @@ An honest list:
333
357
  - **Processes are cooperative.** `kill -9` cannot interrupt a tight synchronous loop; `SIGSTOP`
334
358
  marks state. `chroot` runs the command in the target rather than isolating it.
335
359
  - **`execSync` needs the worker runtime.** Where it is unavailable, it throws naming the command —
336
- answer *No* to prompts like `npm create vite`'s "Install and start now?" and run the steps from
360
+ answer _No_ to prompts like `npm create vite`'s "Install and start now?" and run the steps from
337
361
  the shell.
338
362
  - **`node:test`** covers what test files use (`test`/`describe`/hooks/`mock.fn`, spec and tap
339
363
  reports); `run()`, coverage and mock timers are not implemented.
@@ -357,13 +381,15 @@ dispatch all find it:
357
381
  ```ts
358
382
  import { defineCommand } from "sandboxedjs";
359
383
 
360
- box.kernel.installCommand(defineCommand({
361
- name: "greet",
362
- summary: "say hello",
363
- run: (ctx) => (ctx.line(`hello ${ctx.args[0] ?? "world"}`), 0),
364
- }));
384
+ box.kernel.installCommand(
385
+ defineCommand({
386
+ name: "greet",
387
+ summary: "say hello",
388
+ run: (ctx) => (ctx.line(`hello ${ctx.args[0] ?? "world"}`), 0),
389
+ }),
390
+ );
365
391
 
366
- await box.exec("greet there | tr a-z A-Z"); // → HELLO THERE
392
+ await box.exec("greet there | tr a-z A-Z"); // → HELLO THERE
367
393
  ```
368
394
 
369
395
  `Kernel`, `Vfs`, `Shell`, `Terminal` and `NetworkStack` are exported too, if you want to embed a
package/dist/index.cjs CHANGED
@@ -21103,7 +21103,7 @@ var wheels_default = {
21103
21103
 
21104
21104
  // package.json
21105
21105
  var package_default = {
21106
- version: "0.2.9"};
21106
+ version: "0.2.11"};
21107
21107
 
21108
21108
  // src/python/extension-abi.ts
21109
21109
  var EXTENSION_ABI = {
package/dist/index.js CHANGED
@@ -21086,7 +21086,7 @@ var wheels_default = {
21086
21086
 
21087
21087
  // package.json
21088
21088
  var package_default = {
21089
- version: "0.2.9"};
21089
+ version: "0.2.11"};
21090
21090
 
21091
21091
  // src/python/extension-abi.ts
21092
21092
  var EXTENSION_ABI = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sandboxedjs",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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",