Skip to content

feat: per-artifact origin isolation — capability grant, auto-subdomains, back-button shim - #434

Open
an1va wants to merge 2 commits into
mainfrom
feat/origin-isolation
Open

an1va wants to merge 2 commits into
mainfrom
feat/origin-isolation

Conversation

@an1va

@an1va an1va commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Split out of #430 (2 of 3). Rebased clean of the reflow change: serving keeps today's auto-reflow default everywhere.

SPA bundles (client routing, storage, service workers) are broken on the shared opaque sandbox today. This gives each one its own isolated origin:

  • headersFor(isolated): artifact-bound subdomain hosts (one artifact at root) get allow-same-origin added to the sandbox CSP; /raw/* and workspace domains stay opaque. No top-navigation or popup-escape granted.
  • SPA bundles auto-receive an unguessable subdomain on first publish (d<20 hex> = 80-bit salted hash, deterministic so racing publishes are idempotent; best-effort, self-heals on next publish).
  • HISTORY_SHIM injected only on isolated origins: an embedded artifact downgrades pushState to replaceState so an iframe can't grow the embedder's tab history.
  • Deployment-invariant guard: startup warning when DERIVE_SUBDOMAIN_BASE shares a registrable domain with the app host (the Domain-cookie injection footgun).

Known, accepted residual until PSL registration lands: sibling isolated artifacts share a registrable domain, so Domain=<base> cookies are readable across them (artifact-to-artifact only; the app lives on a separate registrable domain). The PSL plan is tracked separately.

8 e2e/unit tests: grant matrix, registrable-domain guard matrix, subdomain-vs-/raw serving, auto-assignment gating + idempotence + vanity-name respect.

🤖 Generated with Claude Code

an1va and others added 2 commits July 13, 2026 19:31
…to-subdomains, back-button shim

[Q1] is a yes: app-like artifact capability is in scope, so the isolation
track lands. Three pieces, all on infrastructure that already ships (the
origin split + vanity subdomains were live) — the missing middle that
makes an SPA export actually WORK, not just render:

- Host-aware CSP (headersFor/ISOLATED_RAW_HEADERS in http.ts): an
  artifact-bound host (a subdomain serving ONE artifact at its root) now
  gets `allow-same-origin`, so localStorage/IndexedDB persist, History/
  pushState works, and service workers can register. Granted ONLY there —
  the shared /raw sandbox and workspace domains (`<host>/<ref>`, many
  artifacts per host) keep the opaque wall, so one artifact still can't
  read another's storage. Wired in app.ts domain mode (artifact-bound =
  isolated:true, workspace domain = false) through a new serveContent
  `isolated` param; /raw stays default-false.

- Auto-assigned isolated subdomains (after-publish.ts): the first time an
  SPA bundle goes live, it gets its own unguessable subdomain
  automatically — no more owner-typed vanity name required for the case
  that's actually broken without isolation (client routing). The label is
  a salted hash of the artifact id: deterministic (a concurrent publish
  derives the SAME host, so the second write is an idempotent no-op),
  unguessable (80 bits, server-salted, never sequential). Best-effort in
  the background, gated to SPA bundles with no existing artifact-bound
  host; skipped entirely when domain mode or the salt is unconfigured.
  Threaded through all three afterPublish call sites (route, MCP, restore).

- Back-button/History shim (history-shim.ts): with real History access an
  EMBEDDED artifact's in-iframe router would otherwise push onto the
  reader's parent-tab history. The shim downgrades pushState→replaceState
  inside a frame (no-op top-level), so routing still works but doesn't
  hijack the embedder's Back button. Injected only on isolated-origin
  HTML serves, same delivery as MARKS_SCRIPT.

Tests: origin-isolation.test.ts (6) — headersFor grant matrix; end-to-end
that an artifact-bound subdomain serves allow-same-origin + the shim while
the same artifact on /raw serves neither; auto-subdomain logic (SPA gets
one, plain page/static bundle get none, idempotent, respects an existing
vanity name, off without config); label determinism + DNS-label shape.
Full suites green: core 357, db 203, api 954, mcp 27; typecheck all tiers;
d1-schema + openapi + web api-types already regenerated in the prior commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ment

Follow-ups from the branch's adversarial security review (verdict: safe to
merge, no code blockers) — the boundary itself traced correct, these close
the two things it flagged:

- The one real misconfiguration risk: because per-artifact subdomain hosts
  now get the `allow-same-origin` grant, DERIVE_SUBDOMAIN_BASE sharing a
  registrable domain with the app/auth-cookie host would let an artifact
  inject a Domain-scoped cookie the app trusts. Added sharesRegistrableDomain
  (a pure, tested label-suffix predicate) + a loud startup warning in node.ts
  (mirrors the existing DERIVE_SANDBOX_URL warning; never crashes a running
  instance). The intended prod config (derived.app vs derive.to) passes clean.
- Corrected the ISOLATED_RAW_HEADERS comment: SOP — not the PSL — is what
  isolates each artifact's localStorage/IndexedDB (distinct subdomains are
  distinct origins, true today pre-registration). What the PSL actually adds
  is closing the Domain=<base> cross-artifact COOKIE channel; register before
  relying on subdomain isolation at scale.

Tests: sharesRegistrableDomain matrix (same-host / sub-of / parent-of / stray
dots flagged; separate registrable domain + non-suffix shared label + null
pass), plus an assertion that the isolated CSP grants neither top-navigation
nor popup-escape. api 956 green, typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant