Skip to content

Seal archived realms: 403 (archived) for all content requests#5360

Open
lukemelia wants to merge 5 commits into
mainfrom
cs-11664-sealed-enforcement-403-archived-for-all-content-requests
Open

Seal archived realms: 403 (archived) for all content requests#5360
lukemelia wants to merge 5 commits into
mainfrom
cs-11664-sealed-enforcement-403-archived-for-all-content-requests

Conversation

@lukemelia

@lukemelia lukemelia commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

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): after checkPermission authorizes the caller, an isRealmArchived check short-circuits the request with 403 (archived) before routing.

  • Runs after authorization, not before. An unauthenticated or unauthorized caller to a private realm gets the normal 401/403 and never learns the realm exists or is archived — only a caller who could otherwise reach the content sees the sealed response. A public realm's readers are authorized by checkPermission, so they do see the seal (the realm's existence is already public).
  • Method-agnostic. Because the seal sits at the request boundary after authorization, one check covers reads and writes alike — there's no read-only path that could miss a write, so a single layer is sufficient.
  • Read fresh per request (like the existing un-memoized fetchRealmPermissions) so a peer replica's archive/unarchive takes effect without a restart.

The 403 carries an X-Boxel-Realm-Archived: true header and a JSON:API error with a stable code: "archived", so the client can render the sealed state rather than a generic forbidden response. New ArchivedRealmError type in router.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-realm are realm-server routes that never reach the realm boundary, so a realm can always be unarchived (no lockout).

Acceptance criteria

  • Read and write requests to an archived realm return 403 with an "archived" marker, for owners and non-owners alike.
  • An unauthenticated/unauthorized caller to a private archived realm gets the normal 401/403, with no archived marker — the seal doesn't disclose the realm's existence or state.
  • _archive-realm / _unarchive-realm still work while the realm is archived.
  • Active realms are unaffected.

Testing

  • ember-tsc + eslint clean (runtime-common, realm-server).
  • realm-endpoints/archived-seal-test.ts + server-endpoints/archive-realm-test.ts against 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

  • The archived-realms listing endpoint is exempt by the same mechanism — a realm-server route that never reaches the realm boundary, like _archive-realm/_unarchive-realm. Its while-archived reachability test ships alongside that endpoint.

🤖 Generated with Claude Code

lukemelia and others added 2 commits June 29, 2026 12:56
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>
Comment thread packages/runtime-common/realm.ts Outdated
@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Host Test Results

    1 files  ±0      1 suites  ±0   2h 39m 7s ⏱️ + 5m 10s
3 327 tests +8  3 312 ✅ +8  15 💤 ±0  0 ❌ ±0 
3 346 runs  +8  3 331 ✅ +8  15 💤 ±0  0 ❌ ±0 

Results for commit 2c48b45. ± Comparison against earlier commit 17c73a07.

Realm Server Test Results

    1 files  ± 0      1 suites  ±0   10m 47s ⏱️ -30s
1 683 tests +11  1 683 ✅ +11  0 💤 ±0  0 ❌ ±0 
1 762 runs  +11  1 762 ✅ +11  0 💤 ±0  0 ❌ ±0 

Results for commit 2c48b45. ± Comparison against earlier commit 17c73a07.

lukemelia and others added 2 commits June 29, 2026 14:37
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>
@lukemelia lukemelia requested review from a team and habdelra June 30, 2026 02:05
@lukemelia lukemelia marked this pull request as ready for review June 30, 2026 02:06
@habdelra habdelra requested a review from Copilot June 30, 2026 12:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ArchivedRealmError and return a 403 JSON:API error with code: "archived" and X-Boxel-Realm-Archived: true when 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.

Comment on lines +2788 to +2793
if (
!lookupRouteTable(this.#publicEndpoints, this.paths, request) &&
(await isRealmArchived(this.#dbAdapter, new URL(this.url)))
) {
throw new ArchivedRealmError(`Realm ${this.url} is archived`);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Comment on lines +2772 to +2776
// 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Comment on lines +125 to +134
// 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',
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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>
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.

3 participants