sandboxedjs 0.1.29 → 0.1.30

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/LICENSE CHANGED
@@ -18,10 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
22
-
23
- ---
24
-
25
- This package depends on @scelar/nodepod, which is licensed MIT with the
26
- Commons Clause. Nodepod is not redistributed here; it is installed as a normal
27
- npm dependency.
21
+ SOFTWARE.
package/README.md CHANGED
@@ -768,6 +768,58 @@ Two caveats worth knowing:
768
768
  itself reports status correctly and can be relied on in `&&` chains.
769
769
  - **No hardware acceleration and no native codecs** beyond what the WebAssembly build ships.
770
770
 
771
+ ## Reaching a container's servers from the page
772
+
773
+ A server inside the container is not on the network, so there is no `http://localhost:5173`
774
+ your page can navigate to. There are three ways to reach one, and the right choice depends on
775
+ whether you need a whole site or a single response — and on whether you trust the code.
776
+
777
+ **1. One response, into a variable.** `request()` speaks to a server directly and hands back the
778
+ bytes. Nothing is executed, so there is nothing to be careful about.
779
+
780
+ ```ts
781
+ const res = await box.request(5173, { path: "/api/items" });
782
+ element.textContent = res.body; // text
783
+ const items = res.json<Item[]>(); // parsed
784
+ const bytes = res.bytes; // exact bytes, for images and source maps
785
+ ```
786
+
787
+ **2. One response, rendered — safely.** `renderInto` puts the response in an iframe with
788
+ `sandbox="allow-scripts"` and deliberately *without* `allow-same-origin`. The document lands in an
789
+ opaque origin: its scripts run, and they can reach neither your DOM nor your cookies and storage.
790
+
791
+ ```ts
792
+ import { renderInto } from "sandboxedjs";
793
+ await renderInto(box, document.querySelector("#preview")!, { port: 5173 });
794
+ ```
795
+
796
+ This is the option to reach for when the code came from somewhere you do not control. Its limit is
797
+ that only that one response exists — a page asking for `/main.js` gets nothing, because there is no
798
+ origin to serve it from. Good for generated HTML, a chart, a rendered document.
799
+
800
+ **3. A whole site, with real URLs.** `createPreview` registers a service worker that gives the
801
+ container's ports working URLs, so an iframe can load a dev server with all its subresources.
802
+
803
+ ```ts
804
+ import { createPreview } from "sandboxedjs";
805
+ const preview = await createPreview(box); // null where service workers are unavailable
806
+ iframe.src = preview!.urlFor(5173);
807
+ ```
808
+
809
+ Requests are routed by *which client is asking* rather than by path, so a dev server's absolute
810
+ URLs — `/src/main.js`, `/@vite/client` — resolve without rewriting anything.
811
+
812
+ > **This one runs guest code on your origin.** A service worker can only serve URLs under the
813
+ > origin that registered it, so scripts in the preview can reach `window.parent`, your cookies and
814
+ > your `localStorage`. The sandbox contains a program's *filesystem and process table*, not the
815
+ > page it serves. Use it for code you trust; for anything else, serve the preview from a separate
816
+ > origin (a subdomain pointed at the same app) or stay with option 2.
817
+
818
+ Service workers need a secure context, and some embedded browsers disable them entirely — hence
819
+ the `null` return rather than a throw. Responses are served with both
820
+ `Cross-Origin-Resource-Policy` and `Cross-Origin-Embedder-Policy`, so a preview still frames
821
+ correctly inside the cross-origin isolated page that Rolldown requires.
822
+
771
823
  ## Isolation
772
824
 
773
825
  By default each guest program runs on its own thread, in a Worker. The volume, the kernel, the
package/dist/index.cjs CHANGED
@@ -19712,6 +19712,10 @@ var KernelChildProcess = class {
19712
19712
  started = false;
19713
19713
  cancelled = false;
19714
19714
  constructor(kernel, cred, config, pid) {
19715
+ if (config.inheritStdio) {
19716
+ this.stdin.isTTY = true;
19717
+ this.stdin.interactive = true;
19718
+ }
19715
19719
  this.kernel = kernel;
19716
19720
  this.cred = cred;
19717
19721
  this.pid = pid;
@@ -22116,7 +22120,8 @@ var ChildProcess = class extends EventEmitter4__default.default {
22116
22120
  queueMicrotask(() => this.emit("close", code, null));
22117
22121
  }
22118
22122
  };
22119
- function createChildProcessModule(spawnChild, defaultCwd, syncSpawn) {
22123
+ function createChildProcessModule(spawnChild, defaultCwd, syncSpawn, defaultEnv = () => ({})) {
22124
+ const environmentFor = (options) => options.env ? { ...options.env } : defaultEnv();
22120
22125
  const throughShell = (command, options) => {
22121
22126
  const shell = typeof options.shell === "string" ? options.shell : "/bin/sh";
22122
22127
  return { file: shell, args: ["-c", command] };
@@ -22127,7 +22132,7 @@ function createChildProcessModule(spawnChild, defaultCwd, syncSpawn) {
22127
22132
  command: resolved.file,
22128
22133
  args: resolved.args,
22129
22134
  cwd: options.cwd ?? defaultCwd(),
22130
- ...options.env ? { env: options.env } : {}
22135
+ env: environmentFor(options)
22131
22136
  });
22132
22137
  return new ChildProcess(handle, resolved.file, resolved.args);
22133
22138
  };
@@ -22173,11 +22178,11 @@ ${err.join("")}`),
22173
22178
  exec,
22174
22179
  execFile,
22175
22180
  fork: (modulePath, args = [], options = {}) => spawn("node", [modulePath, ...args], options),
22176
- ...buildSyncFamily(syncSpawn, throughShell, defaultCwd),
22181
+ ...buildSyncFamily(syncSpawn, throughShell, defaultCwd, environmentFor),
22177
22182
  ChildProcess
22178
22183
  };
22179
22184
  }
22180
- function buildSyncFamily(syncSpawn, throughShell, defaultCwd) {
22185
+ function buildSyncFamily(syncSpawn, throughShell, defaultCwd, environmentFor) {
22181
22186
  if (!syncSpawn) {
22182
22187
  return {
22183
22188
  execSync: unavailable("execSync"),
@@ -22188,12 +22193,14 @@ function buildSyncFamily(syncSpawn, throughShell, defaultCwd) {
22188
22193
  const run = (file3, args, options) => {
22189
22194
  const resolved = options.shell ? throughShell([file3, ...args].join(" "), options) : { file: file3, args };
22190
22195
  const input = options.input === void 0 ? void 0 : typeof options.input === "string" ? options.input : new TextDecoder().decode(options.input);
22196
+ const inherit = options.stdio === "inherit" || Array.isArray(options.stdio) && options.stdio[0] === "inherit";
22191
22197
  return syncSpawn({
22192
22198
  command: resolved.file,
22193
22199
  args: resolved.args,
22194
22200
  cwd: options.cwd ?? defaultCwd(),
22195
- ...options.env ? { env: options.env } : {},
22196
- ...input === void 0 ? {} : { input }
22201
+ env: environmentFor(options),
22202
+ ...input === void 0 ? {} : { input },
22203
+ ...inherit ? { inheritStdio: true } : {}
22197
22204
  });
22198
22205
  };
22199
22206
  const asOutput = (text2, options) => options.encoding === "buffer" || options.encoding === void 0 ? Buffer2.from(text2) : text2;
@@ -22873,7 +22880,7 @@ function createCoreModules(options) {
22873
22880
  };
22874
22881
  const http = options.http ? createHttpModule(options.http.router, options.http.owner, httpOptions, "http:") : createUnsupportedModule("http");
22875
22882
  const https = options.http ? createHttpModule(options.http.router, options.http.owner, httpOptions, "https:") : createUnsupportedModule("https");
22876
- const childProcess = options.spawnChild ? createChildProcessModule(options.spawnChild, () => cwd, options.syncSpawn) : createUnsupportedModule("child_process");
22883
+ const childProcess = options.spawnChild ? createChildProcessModule(options.spawnChild, () => cwd, options.syncSpawn, () => ({ ...processObject.env })) : createUnsupportedModule("child_process");
22877
22884
  const readline = createReadlineModule(() => processObject.stdin, () => processObject.stdout);
22878
22885
  const dns = createDnsModule();
22879
22886
  const builtins = {
@@ -24118,8 +24125,10 @@ var MirroringVolume = class {
24118
24125
  * container pays nothing for this.
24119
24126
  */
24120
24127
  attach(mirror, root) {
24128
+ const cleanRoot = clean(root);
24129
+ if (this.mirror === mirror && this.root === cleanRoot) return;
24121
24130
  this.mirror = mirror;
24122
- this.root = clean(root);
24131
+ this.root = cleanRoot;
24123
24132
  this.seed();
24124
24133
  }
24125
24134
  detach() {
@@ -25254,9 +25263,10 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25254
25263
  ...this.workerUrl ? { url: this.workerUrl } : {},
25255
25264
  timeoutMs: 15e3
25256
25265
  });
25266
+ const streams = { target: null };
25257
25267
  const server = new SyncChannelServer(buffers, serveSyncSyscalls({
25258
25268
  volume: this.volume,
25259
- spawnChild: (request) => this.runChildToCompletion(request)
25269
+ spawnChild: (request) => this.runChildToCompletion(request, streams.target)
25260
25270
  }));
25261
25271
  const entry = { worker, server };
25262
25272
  this.live.add(entry);
@@ -25266,6 +25276,7 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25266
25276
  this.closeProxies(owner);
25267
25277
  void worker.terminate();
25268
25278
  });
25279
+ streams.target = process2;
25269
25280
  const children = /* @__PURE__ */ new Map();
25270
25281
  worker.onMessage((raw) => {
25271
25282
  const message = raw;
@@ -25332,7 +25343,7 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25332
25343
  handle.exec();
25333
25344
  }
25334
25345
  /** Run a child to completion and collect it, for the guest's `spawnSync`. */
25335
- runChildToCompletion(request) {
25346
+ runChildToCompletion(request, streamTo) {
25336
25347
  return new Promise((resolve2) => {
25337
25348
  let handle;
25338
25349
  try {
@@ -25340,7 +25351,8 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25340
25351
  command: request.command,
25341
25352
  args: request.args,
25342
25353
  cwd: request.cwd,
25343
- ...request.env ? { env: request.env } : {}
25354
+ ...request.env ? { env: request.env } : {},
25355
+ ...request.inheritStdio ? { inheritStdio: true } : {}
25344
25356
  });
25345
25357
  } catch (error) {
25346
25358
  const failure = error;
@@ -25349,18 +25361,21 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
25349
25361
  }
25350
25362
  let stdout = "";
25351
25363
  let stderr = "";
25364
+ const live = request.inheritStdio ? streamTo : null;
25352
25365
  handle.on("stdout", (text2) => {
25353
25366
  stdout += text2;
25367
+ live?.output(text2);
25354
25368
  });
25355
25369
  handle.on("stderr", (text2) => {
25356
25370
  stderr += text2;
25371
+ live?.error(text2);
25357
25372
  });
25358
25373
  handle.on("exit", (code) => resolve2({ status: code, stdout, stderr, signal: null }));
25359
25374
  handle.exec();
25360
25375
  if (request.input !== void 0) {
25361
25376
  handle.sendStdin?.(request.input);
25362
25377
  }
25363
- handle.endStdin?.();
25378
+ if (!request.inheritStdio) handle.endStdin?.();
25364
25379
  });
25365
25380
  }
25366
25381
  // ── HTTP servers living on another thread ─────────────────────────────────
@@ -26361,6 +26376,97 @@ init_variables();
26361
26376
  init_arith();
26362
26377
  init_expand();
26363
26378
  init_builtins();
26379
+
26380
+ // src/preview/register.ts
26381
+ function serveContainerOn(port, box) {
26382
+ port.onmessage = async (event) => {
26383
+ const request = event.data;
26384
+ try {
26385
+ const response = await box.request(request.port, {
26386
+ method: request.method,
26387
+ path: request.path,
26388
+ headers: request.headers,
26389
+ ...request.body ? { body: new Uint8Array(request.body) } : {}
26390
+ });
26391
+ const bytes2 = response.bytes.slice();
26392
+ port.postMessage(
26393
+ {
26394
+ id: request.id,
26395
+ response: {
26396
+ status: response.status,
26397
+ statusText: response.statusText,
26398
+ headers: response.headers,
26399
+ body: bytes2.buffer
26400
+ }
26401
+ },
26402
+ [bytes2.buffer]
26403
+ );
26404
+ } catch (error) {
26405
+ const message = new TextEncoder().encode(error instanceof Error ? error.message : String(error));
26406
+ port.postMessage({
26407
+ id: request.id,
26408
+ response: { status: 502, statusText: "Bad Gateway", headers: {}, body: message.buffer }
26409
+ });
26410
+ }
26411
+ };
26412
+ port.start?.();
26413
+ }
26414
+ async function createPreview(box, options = {}) {
26415
+ if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) return null;
26416
+ const scriptUrl = options.scriptUrl ?? new URL("./service-worker.js", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
26417
+ let registration;
26418
+ try {
26419
+ registration = await navigator.serviceWorker.register(scriptUrl, {
26420
+ type: "module",
26421
+ ...options.scope ? { scope: options.scope } : {}
26422
+ });
26423
+ } catch {
26424
+ return null;
26425
+ }
26426
+ const worker = registration.active ?? registration.waiting ?? registration.installing;
26427
+ if (!worker) return null;
26428
+ if (worker.state !== "activated") {
26429
+ const activated = await new Promise((resolve2) => {
26430
+ const check = () => {
26431
+ if (worker.state === "activated") {
26432
+ worker.removeEventListener("statechange", check);
26433
+ resolve2(true);
26434
+ } else if (worker.state === "redundant") {
26435
+ worker.removeEventListener("statechange", check);
26436
+ resolve2(false);
26437
+ }
26438
+ };
26439
+ worker.addEventListener("statechange", check);
26440
+ setTimeout(() => resolve2(worker.state === "activated"), 1e4);
26441
+ check();
26442
+ });
26443
+ if (!activated) return null;
26444
+ }
26445
+ const channel = new MessageChannel();
26446
+ serveContainerOn(channel.port1, box);
26447
+ worker.postMessage({ type: "sandboxedjs:connect" }, [channel.port2]);
26448
+ const base2 = registration.scope.replace(/\/$/, "");
26449
+ return {
26450
+ urlFor: (port) => `${base2}/__sbx__/${port}/`,
26451
+ dispose: async () => {
26452
+ channel.port1.close();
26453
+ await registration.unregister();
26454
+ }
26455
+ };
26456
+ }
26457
+ async function renderInto(box, element, options = { port: 80 }) {
26458
+ const response = await box.request(options.port, { path: options.path ?? "/" });
26459
+ const frame = document.createElement("iframe");
26460
+ frame.setAttribute("sandbox", "allow-scripts");
26461
+ frame.style.width = "100%";
26462
+ frame.style.height = "100%";
26463
+ frame.style.border = "0";
26464
+ frame.srcdoc = response.body;
26465
+ element.replaceChildren(frame);
26466
+ return frame;
26467
+ }
26468
+
26469
+ // src/index.ts
26364
26470
  var src_default = createContainer;
26365
26471
 
26366
26472
  exports.BufferSink = BufferSink;
@@ -26404,6 +26510,7 @@ exports.VirtualHttpServer = VirtualHttpServer;
26404
26510
  exports.VirtualIncomingMessage = VirtualIncomingMessage;
26405
26511
  exports.VirtualServerResponse = VirtualServerResponse;
26406
26512
  exports.WASM_ALIASES = WASM_ALIASES;
26513
+ exports.WorkerRuntimePod = WorkerRuntimePod;
26407
26514
  exports.allCommands = allCommands;
26408
26515
  exports.applyChmod = applyChmod;
26409
26516
  exports.braceExpand = braceExpand;
@@ -26416,6 +26523,7 @@ exports.createChildProcessModule = createChildProcessModule;
26416
26523
  exports.createContainer = createContainer;
26417
26524
  exports.createContext = createContext;
26418
26525
  exports.createCoreModules = createCoreModules;
26526
+ exports.createPreview = createPreview;
26419
26527
  exports.default = src_default;
26420
26528
  exports.defineCommand = defineCommand;
26421
26529
  exports.evalArith = evalArith;
@@ -26442,6 +26550,7 @@ exports.octalMode = octalMode;
26442
26550
  exports.parseShell = parse;
26443
26551
  exports.parseUmask = parseUmask;
26444
26552
  exports.posixPath = path_exports;
26553
+ exports.renderInto = renderInto;
26445
26554
  exports.resetPidCounter = resetPidCounter;
26446
26555
  exports.shellQuote = shellQuote;
26447
26556
  exports.startRuntimeWorker = startRuntimeWorker;