Seal archived realms: 403 (archived) for all content requests#5360
Seal archived realms: 403 (archived) for all content requests#5360lukemelia wants to merge 5 commits into
Conversation
An archived realm is sealed for everyone, owner included. At the realm request boundary, every external content request is short-circuited with 403 carrying an "archived" marker (an X-Boxel-Realm-Archived header plus a JSON:API error with code "archived") so the client can render the sealed state. The archived flag is read fresh per request so a peer replica's archive/unarchive takes effect without a restart. The operational _readiness-check endpoint stays exempt so health probes don't read an archived realm as down. The archive-management endpoints (_archive-realm / _unarchive-realm) live on the realm server router and never reach this boundary, so a realm can always be unarchived. The write authorization chokepoint independently denies mutating requests to an archived realm, so a write is refused even if the boundary check is ever bypassed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reword the archived-seal comments to name the mechanism rather than the tracker ticket, so they stay accurate independent of issue/PR numbering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Host Test Results 1 files ±0 1 suites ±0 2h 39m 7s ⏱️ + 5m 10s Results for commit 2c48b45. ± Comparison against earlier commit 17c73a07. Realm Server Test Results 1 files ± 0 1 suites ±0 10m 47s ⏱️ -30s Results for commit 2c48b45. ± Comparison against earlier commit 17c73a07. |
Run the archived check after checkPermission rather than before it, and drop the duplicate check at the top of checkPermission. An unauthenticated or unauthorized caller to a private archived realm now gets the normal 401/403 and never learns the realm exists or is archived; only callers who could otherwise reach the content receive the sealed 403 response. The seal is method-agnostic, so a single post-authorization check covers reads and writes. Public realms still surface the seal (their readers are authorized and existence is already public). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the archived marker to Access-Control-Expose-Headers so the host can read it from a cross-origin 403 response and render the sealed state. The header is only present on the archived 403, which is itself only returned to authorized callers, so exposing it discloses nothing further. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces “sealed” behavior for archived realms: after a request is authorized, all realm content requests are short-circuited with a 403 response that includes an explicit archived marker (header + JSON:API error code) so clients can reliably detect and render the archived state. It also updates response CORS exposure and adds realm-server tests to validate the seal behavior.
Changes:
- Add
ArchivedRealmErrorand return a 403 JSON:API error withcode: "archived"andX-Boxel-Realm-Archived: truewhen an archived realm is accessed. - Enforce the archive seal at the realm request boundary (
Realm.internalHandle) after authorization, with a public-endpoint exemption. - Add a new realm-server test suite for archived sealing and register it in the test index.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/runtime-common/router.ts | Adds ArchivedRealmError for archived-realm boundary enforcement. |
| packages/runtime-common/realm.ts | Implements archived sealing in internalHandle and maps ArchivedRealmError to a 403 JSON:API response + header. |
| packages/runtime-common/create-response.ts | Exposes X-Boxel-Realm-Archived to browsers via Access-Control-Expose-Headers. |
| packages/realm-server/tests/realm-endpoints/archived-seal-test.ts | Adds coverage for sealed archived realms (403 marker), readiness exemption, and unarchive behavior. |
| packages/realm-server/tests/index.ts | Includes the new archived seal test file in the test runner list. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if ( | ||
| !lookupRouteTable(this.#publicEndpoints, this.paths, request) && | ||
| (await isRealmArchived(this.#dbAdapter, new URL(this.url))) | ||
| ) { | ||
| throw new ArchivedRealmError(`Realm ${this.url} is archived`); | ||
| } |
There was a problem hiding this comment.
[Claude Code 🤖] Fixed in 2c48b45. The exemption now matches localPath against an explicit ARCHIVED_SEAL_EXEMPT_PATHS set (_readiness-check, _session) instead of lookupRouteTable, so it no longer depends on Accept/Content-Type — a header-less probe is exempt too.
Two notes on the original framing: (1) Our in-repo readiness pollers all send Accept: application/vnd.api+json (RealmInfo and JSONAPI share that wire value), so they were already matching the old check — the real gap was the header-less / */* probe. (2) The router itself gates _readiness-check dispatch on the Accept header, so a header-less probe 404s at routing on an active realm regardless; the meaningful fix here is that the seal no longer singles such a probe out with a 403 (archived) when it would otherwise be a plain 404.
Also corrected the comment that called _readiness-check the realm's only public endpoint — _session is public too, and both are now listed explicitly as exempt.
| // An archived realm is sealed for everyone, owner included: once a | ||
| // caller is authorized, every external content request is | ||
| // short-circuited with 403 (archived). The seal runs AFTER | ||
| // checkPermission so an unauthenticated or unauthorized caller to a | ||
| // private realm gets the normal 401/403 and never learns the realm |
There was a problem hiding this comment.
[Claude Code 🤖] Correct — the second enforcement layer is not required, so I've updated the PR description to match the actual single-layer model. Because the seal sits at the request boundary after checkPermission and is method-agnostic, one check covers reads and writes alike; there's no read-only path that could miss a write, so a separate write-chokepoint check in checkPermission would be redundant.
| // The operational readiness probe is exempt so health checks don't read | ||
| // an archived realm as down. | ||
| let readiness = await request | ||
| .get('/_readiness-check') | ||
| .set('Accept', 'application/vnd.api+json'); | ||
| assert.strictEqual( | ||
| readiness.status, | ||
| 200, | ||
| '_readiness-check stays reachable while archived', | ||
| ); |
There was a problem hiding this comment.
[Claude Code 🤖] Added in 2c48b45. The test now also probes _readiness-check with no Accept header and asserts it's handled identically whether the realm is archived or active (same status as the active-realm baseline) and carries no X-Boxel-Realm-Archived marker. Paired with the path-based exemption fix, this guards against the header-dependent matching regression.
The seal exemption matched public endpoints via lookupRouteTable, which resolves routes from the Accept/Content-Type header. A header-less probe (no Accept, or */*) didn't match, so a bare health probe to an archived realm's _readiness-check was sealed with a 403 (archived) marker — a different response than the same probe gets on an active realm. Match the exempt operational endpoints (_readiness-check, _session) on localPath instead, independent of request headers, and correct the comment that called _readiness-check the realm's only public endpoint. Add regression coverage asserting a header-less readiness probe is handled identically whether the realm is archived or active, and is never given the archived marker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
An archived realm is sealed for everyone, owner included. Every content request to a sealed realm returns 403 carrying an "archived" marker, while the archive-management endpoints stay reachable so a realm can always be unarchived.
How
A single enforcement point in
packages/runtime-common/realm.ts(internalHandle, external requests): aftercheckPermissionauthorizes the caller, anisRealmArchivedcheck short-circuits the request with 403 (archived) before routing.checkPermission, so they do see the seal (the realm's existence is already public).fetchRealmPermissions) so a peer replica's archive/unarchive takes effect without a restart.The 403 carries an
X-Boxel-Realm-Archived: trueheader and a JSON:API error with a stablecode: "archived", so the client can render the sealed state rather than a generic forbidden response. NewArchivedRealmErrortype inrouter.ts.Exemptions
The realm's public operational endpoints stay reachable while archived, matched on
localPath(independent of request headers, so a header-less probe is still exempt):_readiness-check— operational health probes don't read an archived realm as down._session— authentication still works._archive-realm/_unarchive-realmare realm-server routes that never reach the realm boundary, so a realm can always be unarchived (no lockout).Acceptance criteria
_archive-realm/_unarchive-realmstill work while the realm is archived.Testing
ember-tsc+ eslint clean (runtime-common, realm-server).realm-endpoints/archived-seal-test.ts+server-endpoints/archive-realm-test.tsagainst the full dev stack. The combined run is the key proof — the seal blocks all content access while the management endpoints keep working with the seal active, and active realms are unaffected. Includes regression coverage for non-disclosure to unauthorized callers and for the header-less readiness probe.Notes
_archive-realm/_unarchive-realm. Its while-archived reachability test ships alongside that endpoint.🤖 Generated with Claude Code