sandboxedjs 0.1.50 → 0.1.51

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
@@ -89,7 +89,7 @@ it alongside their model provider.
89
89
  | **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, `[[ ]]`, `(( ))` |
90
90
  | **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 |
91
91
  | **Node.js** | Its own module engine — `require` *and* `import`, live bindings, `exports` maps, top-level `await`, npm packages, `http`, `fs`, streams, `child_process` |
92
- | **Python** | CPython 3.13 (Pyodide) on the *same* filesystem, with the real standard library |
92
+ | **Python** | Source-built CPython 3.13/WASM on the *same* filesystem, with the real standard library |
93
93
  | **WebAssembly** | A WASI (`preview1`) host — run any `wasm32-wasi` binary from clang, Rust, Zig or TinyGo as an ordinary process, on the same filesystem |
94
94
  | **FFmpeg** | `ffmpeg` and `ffprobe` (FFmpeg 5.1) reading and writing container files directly — [optional install](#video-and-audio) |
95
95
  | **`/proc`** | Live and synthesised — `ps`, `top`, `free` and `uptime` all read the same source |
@@ -391,7 +391,7 @@ your disk.
391
391
 
392
392
  ### Python
393
393
 
394
- Python is CPython 3.13, via Pyodide, mounted on the container's filesystem:
394
+ Python is the bundled, source-built CPython 3.13 WebAssembly runtime mounted on the container's filesystem:
395
395
 
396
396
  ```ts
397
397
  await box.exec("python3 -c \"print(open('/etc/hostname').read())\"");
@@ -401,8 +401,10 @@ await box.exec("echo '1 2 3' | python3 -c \"import sys; print(sum(map(int, sys.s
401
401
 
402
402
  The bundled standard library includes `json`, `re`, `os`, `sys`, `math`, `random`, `hashlib`,
403
403
  `binascii`, `struct`, `time`, `collections`, `itertools`, `functools`, `asyncio` and more. It is
404
- Real CPython, so `sqlite3`, `dataclasses`, `decimal` and `typing` all work. Packages must be built for WebAssembly: `micropip` installs pure-Python wheels, and Pyodide's own distribution covers the rest
405
- (and only with outbound networking enabled).
404
+ It is real CPython, so `sqlite3`, `dataclasses`, `decimal`, `typing`, `ssl`, and
405
+ `asyncio` work. `pip` installs compatible wheels into the same filesystem and
406
+ creates their `console_scripts` in `/usr/local/bin` (with outbound networking
407
+ enabled). Native extensions still need an Emscripten-compatible build.
406
408
 
407
409
  ### WebAssembly binaries
408
410
 
@@ -701,20 +703,21 @@ Those use dynamic imports, so they only fail if you call them.
701
703
 
702
704
  ### Python in a browser
703
705
 
704
- Python works lazily in a browser without extra configuration. On first use,
705
- SandboxedJS loads the matching Pyodide runtime and assets from jsDelivr:
706
+ Python works lazily in a browser without extra configuration. The owned CPython
707
+ runtime and its pthread worker are shipped with the npm package:
706
708
 
707
709
  ```ts
708
710
  const box = await createContainer();
709
711
  await box.exec("python3 -c 'import sqlite3; print(sqlite3.sqlite_version)'");
710
712
  ```
711
713
 
712
- To self-host Pyodide or use another trusted CDN, override both URLs:
714
+ Pyodide remains available as an explicit compatibility backend:
713
715
 
714
716
  ```ts
715
717
  import { configurePython, createContainer } from "sandboxedjs";
716
718
 
717
719
  configurePython({
720
+ backend: "pyodide",
718
721
  pyodideURL: "https://cdn.jsdelivr.net/pyodide/v0.28.3/full/pyodide.mjs",
719
722
  indexURL: "https://cdn.jsdelivr.net/pyodide/v0.28.3/full/",
720
723
  });
@@ -723,8 +726,8 @@ const box = await createContainer();
723
726
  await box.exec("python3 -c 'import sqlite3; print(sqlite3.sqlite_version)'");
724
727
  ```
725
728
 
726
- Python never falls back to a host interpreter or another runtime. A custom
727
- Pyodide module and its asset directory must use the same Pyodide version.
729
+ Python never falls back to a host interpreter. When selecting the optional
730
+ Pyodide backend, its module and asset directory must use the same version.
728
731
 
729
732
  ## Uploading files
730
733
 
@@ -972,11 +975,10 @@ Honest list of what does not work:
972
975
  now?" and run `npm install && npm run dev` from the shell instead.
973
976
  - **No `net`, `tls`, `worker_threads` or `vm`.** `http` and `https` are served by a virtual stack
974
977
  that `request()` talks to directly, so servers work; raw sockets do not.
975
- - **Python is CPython via Pyodide.** The standard library is the real one. A C
976
- extension works only if it has been built for WebAssembly — Pyodide ships
977
- many, including `numpy`, but an arbitrary wheel from PyPI will not install.
978
- Starting an interpreter costs about a second and a half; one is kept per
979
- container, and each program runs in its own namespace.
978
+ - **Python is source-built CPython/WASM.** Each program gets its own interpreter
979
+ process worker. Pure-Python wheels install normally; native extensions must
980
+ be linked or published for Emscripten. Pydantic 2's `pydantic-core` is not
981
+ currently available, so use a Pydantic 1 constraint for FastAPI deployments.
980
982
  - **No real sockets.** HTTP servers work through the request proxy; raw TCP/UDP does not.
981
983
  - **No real processes.** Processes are cooperative async tasks: `kill -9` cannot interrupt a
982
984
  tight synchronous loop, and `SIGSTOP` only marks state.
package/dist/index.cjs CHANGED
@@ -19107,7 +19107,7 @@ async function startNodeWorker(url, workerData) {
19107
19107
  /* webpackIgnore: true */
19108
19108
  specifier
19109
19109
  );
19110
- const worker = new NodeWorker(url, { workerData });
19110
+ const worker = new NodeWorker(url, { workerData, execArgv: [] });
19111
19111
  return {
19112
19112
  postMessage: (message) => worker.postMessage(message),
19113
19113
  onMessage: (listener) => worker.on("message", listener),
@@ -19380,7 +19380,17 @@ async function resolvePackageCandidates(client, requirement, environment) {
19380
19380
  const index = await client.json(`https://pypi.org/pypi/${requirement.name}/json`);
19381
19381
  const candidates = Object.keys(index.releases).filter((version) => requirement.specifiers.every((s) => compareVersions(version, s.version, s.op))).sort((a, b) => compareRelease(releaseOf(a), releaseOf(b)));
19382
19382
  const stable = candidates.filter((version) => !isPreRelease(version));
19383
- const ordered = (stable.length > 0 ? stable : candidates).reverse();
19383
+ const newestFirst = (stable.length > 0 ? stable : candidates).reverse();
19384
+ const probeIndexes = [
19385
+ 0,
19386
+ Math.floor(newestFirst.length / 2),
19387
+ Math.floor(newestFirst.length / 4),
19388
+ Math.floor(newestFirst.length * 3 / 4),
19389
+ newestFirst.length - 1
19390
+ ];
19391
+ const probes = [...new Set(probeIndexes)].map((index2) => newestFirst[index2]).filter((v) => Boolean(v));
19392
+ const probeVersions = new Set(probes);
19393
+ const ordered = [...probes, ...newestFirst.filter((version) => !probeVersions.has(version))];
19384
19394
  let sawSourceOnly = false;
19385
19395
  const resolved = [];
19386
19396
  for (const version of ordered) {
@@ -19450,7 +19460,10 @@ async function installRequirements(options) {
19450
19460
  }
19451
19461
  const archives = /* @__PURE__ */ new Map();
19452
19462
  const unavailable2 = /* @__PURE__ */ new Set();
19463
+ const unsatisfiable = /* @__PURE__ */ new Map();
19453
19464
  const candidateCache = /* @__PURE__ */ new Map();
19465
+ let candidateAttempts = 0;
19466
+ const MAX_CANDIDATE_ATTEMPTS = 96;
19454
19467
  const solve = async (pending, state) => {
19455
19468
  if (pending.length === 0) return state;
19456
19469
  const [first, ...tail2] = pending;
@@ -19474,6 +19487,9 @@ async function installRequirements(options) {
19474
19487
  skipped: [...state.skipped, { name: requirement.name, marker: requirement.marker ?? "" }]
19475
19488
  });
19476
19489
  }
19490
+ const solveKey = `${requirement.name}:${requirement.specifiers.map((s) => `${s.op}${s.version}`).sort().join(",")}:${[...extras].sort().join(",")}`;
19491
+ const knownFailure = unsatisfiable.get(solveKey);
19492
+ if (knownFailure) throw knownFailure;
19477
19493
  const already = state.chosen.get(requirement.name);
19478
19494
  if (already) {
19479
19495
  const satisfied = requirement.specifiers.every(
@@ -19518,6 +19534,12 @@ async function installRequirements(options) {
19518
19534
  let failure2;
19519
19535
  for (const resolved of candidates) {
19520
19536
  try {
19537
+ candidateAttempts += 1;
19538
+ if (candidateAttempts > MAX_CANDIDATE_ATTEMPTS) {
19539
+ throw new Error(
19540
+ "dependency resolution exceeded 96 wheel candidates; add version constraints for packages whose newest generation requires unavailable native WASM extensions"
19541
+ );
19542
+ }
19521
19543
  let entries = archives.get(resolved.url);
19522
19544
  if (!entries) {
19523
19545
  options.progress.downloading(resolved.name, resolved.version);
@@ -19544,10 +19566,13 @@ async function installRequirements(options) {
19544
19566
  skipped: state.skipped
19545
19567
  });
19546
19568
  } catch (error) {
19569
+ if (error?.message?.startsWith("dependency resolution exceeded")) throw error;
19547
19570
  failure2 = error;
19548
19571
  }
19549
19572
  }
19550
- throw failure2 instanceof Error ? failure2 : new Error(`could not resolve ${requirement.name}`);
19573
+ const resolutionError = failure2 instanceof Error ? failure2 : new Error(`could not resolve ${requirement.name}`);
19574
+ unsatisfiable.set(solveKey, resolutionError);
19575
+ throw resolutionError;
19551
19576
  };
19552
19577
  const solved = await solve(queue, {
19553
19578
  chosen: /* @__PURE__ */ new Map(),