Monorepo for the nibling.run platform
README.md

@nibling/sandbox #

The page every sandbox origin serves (VISION D3, D27). The app on nibling.dev frames it from <id>.nibling.run in a hidden iframe. The page boots a kernel on its own origin and serves it to the app over a MessagePort. The kernel, its Service Worker, its storage, and the guest pages it serves stay on the sandbox's origin, so guest code cannot reach the app's OAuth session.

@nibling/sandbox/client is the app's side. connectSandbox({ origin }) creates the iframe, handshakes, and resolves with a SandboxKernel, the part of @nibling/kernel's Kernel the app drives. A local Kernel satisfies the same type.

Handshake #

  1. The coordinator creates the iframe with allow="cross-origin-isolated".
  2. On the iframe's load, it posts { t: "connect" } to the iframe's window with a transferred MessagePort, targeting the sandbox's origin.
  3. The page accepts the first such message whose event.origin is one of its app origins and whose event.source is its parent. It ignores every other message.
  4. The page answers accepted on the port at once, then ready once the kernel boots, or bootFailed with the error.

After ready, the port carries the messages in src/protocol.ts. Requests correlate by id, processes by a proc handle the coordinator picks, and subscriptions by a sub number per listener. Stdio crosses as chunk messages whose buffers are transferred. A process started with pty takes keys as ptyWrite chunks and sizes as ptyResize, and its output arrives as stdout. Each HostFs method is a request named by op. Its error keeps the Node code, and readFile bytes cross transferred.

Anything running on the sandbox's origin can reach the page's port, so the client treats what arrives as untrusted. It drops a listening URL that is not on the sandbox's origin, and a guest write that is malformed or names a path outside the prefixes its listener asked for. putFile rejects an answer that is not a generation, and each fs call rejects an answer of the wrong shape for its method. A process drops output that is not bytes, a second end or exit, and an exit whose code is not a whole number or whose signal is not a signal name. An error keeps its code only when it looks like a Node code.

Configuration #

NIB_APP_ORIGINS names the coordinators, comma-separated, at build time and when the dev server starts. They are the only parents the page accepts, and the page's Content-Security-Policy: frame-ancestors. The default is http://127.0.0.1:5173, the app's dev server.

Development #

pnpm --filter @nibling/sandbox dev

This serves the page on port 5174. Chromium and Firefox resolve every *.localhost name to loopback, so http://<id>.localhost:5174 is a separate origin per sandbox. The app's dev script starts this server too (see packages/app/README.md).

Deployment #

pnpm run deploy deploys the nibling-sandbox Worker with the built page as static assets. The Worker redirects the apex nibling.run to https://nibling.dev and serves the page on every subdomain. Build with NIB_APP_ORIGINS=https://nibling.dev.

Cloudflare custom domains take no wildcard, so the Worker serves through the routes *.nibling.run/* and nibling.run/*. Wrangler does not create DNS records for routes. The nibling.run zone needs two proxied records:

Type Name Content Proxy
AAAA * 100:: Proxied
AAAA nibling.run 100:: Proxied

100:: is a discard address. Requests never reach it, because the routes answer first.