sandboxedjs 0.1.4 → 0.1.6

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
@@ -459,6 +459,7 @@ and the usual control keys.
459
459
  ## Command line
460
460
 
461
461
  ```bash
462
+ npx sandboxedjs --repl # explore: what runs, what doesn't
462
463
  npx sandboxedjs # interactive shell
463
464
  npx sandboxedjs -c 'ls -la /etc' # one command
464
465
  npx sandboxedjs script.sh # run a script
@@ -468,6 +469,24 @@ npx sandboxedjs --network -p 3000 # allow outbound, publish a port
468
469
 
469
470
  Run `sandboxedjs --help` for the full list.
470
471
 
472
+ ### Trying it out
473
+
474
+ `--repl` drops you into a shell with a summary of what this container can actually do. Every
475
+ line is measured on the spot rather than claimed, so a runtime that is missing says so:
476
+
477
+ ```
478
+ sandboxedjs — a Linux-like container inside Node.js
479
+
480
+ node v22.12.0 npm, require, http servers
481
+ python3 MicroPython no C extensions (numpy, pandas)
482
+ ffmpeg 5.1.4 video and audio
483
+ network on npm install reaches the real registry
484
+ ```
485
+
486
+ It needs no project and no files — it is meant for finding the edges. Because installing
487
+ packages is most of what people want to test, `--repl` allows outbound access; pass
488
+ `--no-network` to take it away and watch what breaks.
489
+
471
490
  ## Security
472
491
 
473
492
  The container has no access to your filesystem, environment, or network unless you grant it:
@@ -633,6 +652,25 @@ Honest list of what does not work:
633
652
  This affects older toolchains — Vite 4 and its contemporaries. Current Vite ships its Rust
634
653
  pipeline as WebAssembly and runs fine, dev server included, as do Express, Koa, Fastify-style
635
654
  apps and plain `http` servers.
655
+ - **HTTP request bodies into the container are text, not bytes.** Nodepod decodes a request body
656
+ as UTF-8 on its way to an in-container server, so any byte above `0x7f` is replaced: five bytes
657
+ containing `0x89` and `0xff` arrive as nine. Uploading a JPEG or an MP4 by POSTing it to a
658
+ server running inside the sandbox produces a file of roughly the right size that will not open,
659
+ and reports no error. This affects `box.request()`, `box.expose()` and the request proxy alike.
660
+
661
+ Write the file instead — `box.fs.writeFile(path, bytes)` takes a `Uint8Array` and is exact,
662
+ which the binary round-trip tests assert byte for byte. To accept an upload from a browser,
663
+ receive it in *your* server and write the bytes into the container:
664
+
665
+ ```js
666
+ // Express on the host, sandboxedjs holding the workspace.
667
+ app.post("/upload", async (req, res) => {
668
+ await box.fs.writeFile(`/workspace/uploads/${req.file.originalname}`, req.file.buffer);
669
+ res.json({ ok: true });
670
+ });
671
+ ```
672
+
673
+ Text bodies — JSON APIs, form fields, GraphQL — are unaffected.
636
674
  - **Python is MicroPython**, so C extensions (`numpy`, `pandas`, `cryptography`) are unavailable.
637
675
  - **No real sockets.** HTTP servers work through the request proxy; raw TCP/UDP does not.
638
676
  - **No real processes.** Processes are cooperative async tasks: `kill -9` cannot interrupt a
@@ -19,10 +19,13 @@ const USAGE = `sandboxedjs — a Linux-like container inside Node.js
19
19
 
20
20
  Usage:
21
21
  sandboxedjs [options] start an interactive shell
22
+ sandboxedjs --repl explore the sandbox interactively
22
23
  sandboxedjs [options] -c <command> run a command and exit
23
24
  sandboxedjs [options] <script.sh> run a shell script and exit
24
25
 
25
26
  Options:
27
+ --repl interactive shell with a summary of what is
28
+ available; enables --network (see --no-network)
26
29
  -c, --command <cmd> run <cmd> and exit
27
30
  -v, --volume <src:dst> copy a host directory into the container (repeatable)
28
31
  -o, --out <src:dst> copy a container path back out on exit (repeatable)
@@ -32,6 +35,7 @@ Options:
32
35
  -p, --publish <port> publish a container port on the host (repeatable)
33
36
  -H, --hostname <name> container hostname (default sandbox)
34
37
  --network allow outbound network access (off by default)
38
+ --no-network deny outbound access, even under --repl
35
39
  --allow <host> allow one outbound host (implies --network, repeatable)
36
40
  --memory <bytes> RAM reported inside the container
37
41
  --cpus <n> CPU count reported inside the container
@@ -100,9 +104,15 @@ function parseCli(argv) {
100
104
  case "--hostname":
101
105
  opts.hostname = value();
102
106
  break;
107
+ case "--repl":
108
+ opts.repl = true;
109
+ break;
103
110
  case "--network":
104
111
  opts.network = true;
105
112
  break;
113
+ case "--no-network":
114
+ opts.noNetwork = true;
115
+ break;
106
116
  case "--allow":
107
117
  opts.network = true;
108
118
  opts.allow.push(value());
@@ -138,9 +148,78 @@ async function packageVersion() {
138
148
  }
139
149
  }
140
150
 
151
+ const DIM = "";
152
+ const BOLD = "";
153
+ const RESET = "";
154
+
155
+ /**
156
+ * Report what this container can actually do, before handing over the prompt.
157
+ *
158
+ * Every line is measured rather than asserted: each runtime is asked for its
159
+ * version, and a runtime that is missing says so. The point of `--repl` is to
160
+ * find out what runs here, and a banner that listed capabilities from a
161
+ * hard-coded string would be the one thing that could quietly lie about it.
162
+ */
163
+ async function printReplBanner(box, opts) {
164
+ const probe = async (command, extract) => {
165
+ try {
166
+ const result = await box.exec(command, { timeoutMs: 60_000 });
167
+ if (result.exitCode !== 0) return null;
168
+ return extract(`${result.stdout}${result.stderr}`)?.trim() ?? null;
169
+ } catch {
170
+ return null;
171
+ }
172
+ };
173
+
174
+ const [node, python, ffmpeg] = await Promise.all([
175
+ probe("node -v", (out) => out.match(/v[\d.]+/)?.[0]),
176
+ probe("python3 -c \"import sys; print(sys.version.split()[0])\"", (out) => out),
177
+ probe("ffmpeg -version", (out) => out.match(/ffmpeg version (\S+)/)?.[1]),
178
+ ]);
179
+
180
+ const row = (name, value, note) =>
181
+ ` ${name.padEnd(10)}${(value ?? "not installed").padEnd(14)}${DIM}${note}${RESET}`;
182
+
183
+ const lines = [
184
+ "",
185
+ `${BOLD}sandboxedjs${RESET} — a Linux-like container inside Node.js`,
186
+ "",
187
+ row("node", node, "npm, require, http servers"),
188
+ row("python3", python && `MicroPython`, "no C extensions (numpy, pandas)"),
189
+ row("ffmpeg", ffmpeg, ffmpeg ? "video and audio" : "npm install @ffmpeg/core"),
190
+ row(
191
+ "network",
192
+ opts.network ? "on" : "off",
193
+ opts.network
194
+ ? "npm install reaches the real registry"
195
+ : "outbound denied — drop --no-network to allow it",
196
+ ),
197
+ "",
198
+ `${DIM}Try:${RESET}`,
199
+ " cat /etc/os-release && ls /usr/bin | head",
200
+ ' node -e "require(\'http\').createServer((_,r)=>r.end(\'hi\')).listen(3000)" &',
201
+ " npm create vite@latest app -- --template react && cd app && npm i && npm run dev",
202
+ " ffmpeg -f lavfi -i testsrc=size=64x64:rate=5:duration=1 out.mp4 && ls -l out.mp4",
203
+ "",
204
+ `${DIM}Known limits: no native binaries, no raw sockets, and esbuild-based`,
205
+ `toolchains (Vite 4 and its contemporaries) cannot start. Current Vite works.${RESET}`,
206
+ "",
207
+ `${DIM}Ctrl-D or \`exit\` to leave.${RESET}`,
208
+ "",
209
+ ];
210
+ process.stdout.write(lines.join("\n") + "\n");
211
+ }
212
+
141
213
  async function main() {
142
214
  const opts = parseCli(process.argv.slice(2));
143
215
 
216
+ /* The REPL exists to answer "what actually runs in here?", and the honest
217
+ * answer involves installing packages — so it opens the network unless the
218
+ * caller says otherwise, and the banner states that plainly rather than
219
+ * leaving it to be discovered. */
220
+ if (opts.repl && !opts.noNetwork) opts.network = true;
221
+ if (opts.noNetwork) opts.network = false;
222
+
144
223
  if (opts.help) {
145
224
  process.stdout.write(USAGE);
146
225
  return 0;
@@ -244,10 +323,14 @@ async function main() {
244
323
  return finish(result.exitCode);
245
324
  }
246
325
 
326
+ if (opts.repl) await printReplBanner(box, opts);
327
+
247
328
  const session = box.session();
248
329
  await new Promise((resolve) => {
249
330
  const terminal = new Terminal(session, {
250
331
  write: (data) => process.stdout.write(data),
332
+ // The REPL banner already introduced the container, and says more.
333
+ ...(opts.repl ? { motd: false } : {}),
251
334
  columns: process.stdout.columns ?? 80,
252
335
  rows: process.stdout.rows ?? 24,
253
336
  onExit: (code) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sandboxedjs",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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",