Skip to main content
The hosted sandbox is built in three layers:
Each layer is independently testable, independently substitutable. The library has no environment assumptions; the HTTP API is the only place cookies + storage live; the UI doesn’t trust the API’s “verified” claim — it re-verifies locally.

sandbox-core

The foundational TypeScript library. Edge-runtime-compatible: uses only Web APIs (TextEncoder, btoa/atob, crypto.subtle via @noble/hashes, crypto.randomUUID, crypto.getRandomValues). No node:fs, no node:crypto, no Buffer. Conformance is enforced by __tests__/conformance.test.ts — every sandbox-issued pack is fed through @enfinitos/sdk-auditor’s verifyAll. A drift in any encoder, signature, chain, metering, or settlement primitive will fail the suite. CI gates the sandbox release on it.

HTTP API

Each route under apps/web/app/api/sandbox/ is a tiny adapter between the HTTP boundary and the sandbox-core state machines. Common scaffolding lives in _lib/wrapHandler.ts:
  • Parses cookies + HMAC validation
  • Per-IP rate limiting
  • JSON body parsing
  • Persists the mutated tenant state back to the store
  • Wraps errors into the standard envelope
Adding a new state-machine API endpoint is typically 10 lines of glue plus the route file.

Storage

The tenant store is a Map<tenantId, TenantState> keyed under a global Symbol so all routes share one view per Worker isolate. The interface (apps/web/app/sandbox/_lib/store.ts) is intentionally small — get, set, delete — so a future swap to Cloudflare KV / D1 / Durable Objects requires no route-handler changes.

Why module-level memory works for sandbox

Sandbox traffic is single-visitor, short-session, low-stakes. A typical visitor finishes the six-step flow in 5–20 minutes. Cloudflare Workers keep their isolates warm for tens of minutes under continuous traffic; the eviction case is detectable and recoverable (the API returns 410 TENANT_EVICTED; the UI prompts the user to hit Reset).

Upgrading to persistent storage

When sandbox traffic is high enough that eviction is common, the upgrade is:
  1. Create a Cloudflare KV namespace bound to the Pages project.
  2. Replace memoryStore in _lib/store.ts with a KV-backed implementation matching the same interface.
  3. KV reads are eventually consistent (~60s globally) — sandbox tenants are per-visitor so consistency isn’t a concern.
  4. Set an explicit TTL (24h matches the cookie max-age).
No route handler changes. No type changes. No UI changes.

In-browser verifier

apps/web/app/sandbox/_components/SandboxConsole.tsx runs the seven-check audit roundtrip client-side using sandbox-core primitives:
  1. Envelope version is envelope.v1.
  2. canonicaliseProofPayload re-encodes each record byte-identically.
  3. sha256HexOfString(payloadCanonical) reproduces afterHash.
  4. The hash chain is intact end-to-end.
  5. Every Ed25519 signature verifies against the published key.
  6. Metering records re-project from receipts.
  7. Settlement lines reconcile per-meter to gross.
When all seven pass, the UI shows a green panel and the visual proof-chain SVG colours every node green. A single failure flips the relevant node red and the panel says exactly which invariant broke.

Cross-environment story

A sandbox-issued pack verifies under any auditor pointed at the sandbox key directory. A production-issued pack verifies under any auditor pointed at the production key directory. Cross-pointing fails as UNKNOWN_KEY_ID — there is no environment-conditional code path on the auditor side. The same library handles both. This is the property that makes the sandbox useful for compliance review: anything that verifies under sandbox will verify under production once a real tenant exists.

Trust model summary