sandboxedjs 0.1.6 → 0.1.7

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
@@ -607,6 +607,30 @@ Those three use dynamic imports, so they only fail if you call them.
607
607
  removed and the seam is there, but treat browser support as "should work, unproven" rather than
608
608
  a tested claim.
609
609
 
610
+ ## Uploading files
611
+
612
+ A file picker in a browser never hands over a path — it hands over the file's bytes. So getting a
613
+ picked file into the container is an ordinary write, and there is nothing to mirror from the host
614
+ disk.
615
+
616
+ Two ways in, both byte-exact:
617
+
618
+ ```js
619
+ // 1. Directly, when your own server received the upload.
620
+ await box.fs.writeFile("/workspace/uploads/clip.mp4", bytes); // creates missing directories
621
+ ```
622
+
623
+ ```js
624
+ // 2. Through a server running inside the container — a picker in the previewed
625
+ // app posting to its own backend. Publish the port and upload normally.
626
+ const bridge = await box.expose(3000);
627
+ await fetch(`${bridge.url}/api/upload`, { method: "POST", body: file });
628
+ ```
629
+
630
+ Either way the file lands on the container's filesystem, and `ffmpeg`, Node and Python all see the
631
+ same bytes. Uploads are held in memory like the rest of the filesystem, so a very large video is
632
+ bounded by RAM.
633
+
610
634
  ## Video and audio
611
635
 
612
636
  `ffmpeg` and `ffprobe` are FFmpeg 5.1 compiled to WebAssembly, mounted on the container's own
@@ -652,25 +676,6 @@ Honest list of what does not work:
652
676
  This affects older toolchains — Vite 4 and its contemporaries. Current Vite ships its Rust
653
677
  pipeline as WebAssembly and runs fine, dev server included, as do Express, Koa, Fastify-style
654
678
  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.
674
679
  - **Python is MicroPython**, so C extensions (`numpy`, `pandas`, `cryptography`) are unavailable.
675
680
  - **No real sockets.** HTTP servers work through the request proxy; raw TCP/UDP does not.
676
681
  - **No real processes.** Processes are cooperative async tasks: `kill -9` cannot interrupt a
package/dist/index.cjs CHANGED
@@ -23981,14 +23981,17 @@ var Container = class _Container {
23981
23981
  /** Send an HTTP request to a server running inside the container. */
23982
23982
  async request(port, init = {}) {
23983
23983
  this.assertActive();
23984
- const res = await this.pod.request(port, {
23984
+ const res = await this.dispatchBinary(port, init) ?? await this.pod.request(port, {
23985
23985
  method: init.method ?? "GET",
23986
23986
  path: init.path ?? "/",
23987
23987
  headers: init.headers ?? {},
23988
23988
  body: init.body ?? null
23989
23989
  });
23990
23990
  const bodyRaw = res.body;
23991
- const bytes = bodyRaw instanceof Uint8Array ? bodyRaw : typeof bodyRaw === "string" ? new TextEncoder().encode(bodyRaw) : new Uint8Array(0);
23991
+ const bytes = bodyRaw instanceof Uint8Array ? bodyRaw : (
23992
+ // The binary dispatcher answers with an ArrayBuffer.
23993
+ bodyRaw instanceof ArrayBuffer ? new Uint8Array(bodyRaw) : typeof bodyRaw === "string" ? new TextEncoder().encode(bodyRaw) : new Uint8Array(0)
23994
+ );
23992
23995
  const body = typeof bodyRaw === "string" ? bodyRaw : new TextDecoder().decode(bytes);
23993
23996
  return {
23994
23997
  status: res.statusCode ?? 200,
@@ -24001,6 +24004,40 @@ var Container = class _Container {
24001
24004
  }
24002
24005
  };
24003
24006
  }
24007
+ /**
24008
+ * Deliver a request whose body is bytes, without letting them become text.
24009
+ *
24010
+ * Nodepod's public `request()` runs the body through `toString("utf8")` on
24011
+ * its way in, so anything above `0x7f` is replaced: a five-byte payload
24012
+ * containing `0x89` and `0xff` arrives as nine. That silently destroys every
24013
+ * upload — an image or a video reaches the server the wrong size and no
24014
+ * longer decodes, with no error raised anywhere.
24015
+ *
24016
+ * Its own dispatcher one layer down does preserve bytes, so a binary body
24017
+ * goes straight there. This reaches past the published surface deliberately,
24018
+ * so it is written to fail soft: any shape it does not recognise returns
24019
+ * `null` and the caller falls back to the ordinary path, which is exactly
24020
+ * the behaviour that existed before. Text bodies never come through here.
24021
+ */
24022
+ async dispatchBinary(port, init) {
24023
+ if (!(init.body instanceof Uint8Array)) return null;
24024
+ const manager = this.pod._processManager;
24025
+ const dispatch = manager?.dispatchHttpRequest;
24026
+ if (typeof dispatch !== "function") return null;
24027
+ try {
24028
+ const response = await dispatch.call(
24029
+ manager,
24030
+ port,
24031
+ init.method ?? "GET",
24032
+ init.path ?? "/",
24033
+ init.headers ?? {},
24034
+ init.body
24035
+ );
24036
+ return response ?? null;
24037
+ } catch {
24038
+ return null;
24039
+ }
24040
+ }
24004
24041
  /** Wait until something inside the container answers on `port`. */
24005
24042
  waitForPort(port, opts = {}) {
24006
24043
  return this.net.waitForPort(port, opts);