From dbeaac0816cda368176e30a084b4df87406b4291 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Mon, 29 Jun 2026 15:17:52 -0400 Subject: [PATCH 1/2] Render the sealed state for an archived realm in card view When a realm content request comes back 403 with the X-Boxel-Realm-Archived marker, stack-item renders a dedicated archived state (a locked "Workspace Archived" header plus a restore-from-the-chooser message) instead of card chrome or a generic card error. Detection reads the marker from the card error's response headers. The three write-destination pickers (search RealmPicker, linksTo target picker, create-file destination picker) need no change: all source their realm list from the _realm-auth enumeration, which omits archived realms, so archived realms never appear as write targets. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../operator-mode/archived-realm-state.gts | 80 ++++++++++++++ .../components/operator-mode/stack-item.gts | 103 +++++++++++++----- .../components/operator-mode-test.gts | 56 ++++++++++ 3 files changed, 209 insertions(+), 30 deletions(-) create mode 100644 packages/host/app/components/operator-mode/archived-realm-state.gts diff --git a/packages/host/app/components/operator-mode/archived-realm-state.gts b/packages/host/app/components/operator-mode/archived-realm-state.gts new file mode 100644 index 00000000000..95ebd92da51 --- /dev/null +++ b/packages/host/app/components/operator-mode/archived-realm-state.gts @@ -0,0 +1,80 @@ +import type { TemplateOnlyComponent } from '@ember/component/template-only'; + +import { CardHeader } from '@cardstack/boxel-ui/components'; +import type { MenuItem } from '@cardstack/boxel-ui/helpers'; +import { Lock } from '@cardstack/boxel-ui/icons'; + +import type { CardErrorJSONAPI } from '@cardstack/host/services/store'; + +interface Signature { + Args: { + error: CardErrorJSONAPI; + headerOptions?: { + isTopCard?: boolean; + moreOptionsMenuItems?: MenuItem[]; + onClose?: () => void; + }; + }; + Element: HTMLElement; +} + +// Shown in place of card chrome when a realm responds 403 (archived). An +// archived realm is sealed for everyone, so there is no read-only browsing — +// the only way back in is to restore it. +const ArchivedRealmState: TemplateOnlyComponent = ; + +export default ArchivedRealmState; diff --git a/packages/host/app/components/operator-mode/stack-item.gts b/packages/host/app/components/operator-mode/stack-item.gts index 004279269b7..2b858727b9b 100644 --- a/packages/host/app/components/operator-mode/stack-item.gts +++ b/packages/host/app/components/operator-mode/stack-item.gts @@ -82,6 +82,7 @@ import ElementTracker, { } from '../../resources/element-tracker'; import CardRenderer from '../card-renderer'; +import ArchivedRealmState from './archived-realm-state'; import CardError from './card-error'; import DeleteModal from './delete-modal'; @@ -715,6 +716,18 @@ export default class OperatorModeStackItem extends Component { return this.cardResource?.cardError; } + // A realm that responds 403 (archived) carries the X-Boxel-Realm-Archived + // marker (exposed to cross-origin clients via Access-Control-Expose-Headers). + // The realm only returns this to callers it has already authorized, so its + // presence is an unambiguous signal to render the sealed state rather than a + // generic card error. + private get isArchivedRealmError() { + return ( + this.cardError?.meta?.responseHeaders?.['x-boxel-realm-archived'] === + 'true' + ); + } + private get isWideFormat() { if (!this.card) { return false; @@ -986,36 +999,66 @@ export default class OperatorModeStackItem extends Component { {{else if this.showError}} {{! this is for types--this.cardError is always true in this case !}} {{#if this.cardError}} - + {{#if this.isArchivedRealmError}} + + {{else}} + + {{/if}} {{/if}} {{else if this.card}} {{this.setWindowTitle}} diff --git a/packages/host/tests/integration/components/operator-mode-test.gts b/packages/host/tests/integration/components/operator-mode-test.gts index 05d1e363c52..e07850cd1db 100644 --- a/packages/host/tests/integration/components/operator-mode-test.gts +++ b/packages/host/tests/integration/components/operator-mode-test.gts @@ -95,6 +95,62 @@ module('Integration | operator-mode | basics', function (hooks) { ); }); + test('navigating to an archived realm shows the sealed state, not card chrome or a generic error', async function (assert) { + // A realm sealed by the archive flag answers content requests with 403 and + // the X-Boxel-Realm-Archived marker (CS-11664). Intercept the card fetch to + // reproduce that response and assert the host renders the sealed state. + let networkService = getService('network'); + networkService.virtualNetwork.mount( + async (req: Request) => { + if (req.method === 'GET' && req.url.includes('Person/fadhlan')) { + return new Response( + JSON.stringify({ + errors: [ + { + status: '403', + code: 'archived', + title: 'Realm Archived', + detail: `Realm ${testRealmURL} is archived`, + }, + ], + }), + { + status: 403, + headers: { + 'Content-Type': 'application/vnd.api+json', + 'X-Boxel-Realm-Archived': 'true', + 'X-Boxel-Realm-Url': testRealmURL, + }, + }, + ); + } + return null; + }, + { prepend: true }, + ); + + ctx.setCardInOperatorModeState(`${testRealmURL}Person/fadhlan`); + await renderComponent( + class TestDriver extends GlimmerComponent { + + }, + ); + + await waitFor('[data-test-archived-realm-state]'); + assert + .dom('[data-test-archived-realm-state]') + .containsText( + 'This workspace is archived', + 'the sealed/archived state is shown', + ); + assert + .dom('[data-test-boxel-card-header-title]') + .containsText('Workspace Archived', 'the archived header is shown'); + assert + .dom('[data-test-card-error]') + .doesNotExist('a generic card error is not shown for an archived realm'); + }); + module( 'card with an error that has a last known good state', function (hooks) { From 2a23d774101b234db4e6b54c670fdf4d0bb37751 Mon Sep 17 00:00:00 2001 From: ylm Date: Tue, 30 Jun 2026 12:48:55 -0400 Subject: [PATCH 2/2] Address review: drop ticket ref + name both seal-exempt public endpoints - operator-mode-test: drop the private tracker reference from the new test's comment; the marker behavior is self-evident from the request body the test mounts. - realm.ts: the archived-realm seal exempts everything in #publicEndpoints (`POST /_session` and `GET /_readiness-check`), not readiness only. Reword the comment to match the actual lookup. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../tests/integration/components/operator-mode-test.gts | 4 ++-- packages/runtime-common/realm.ts | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/host/tests/integration/components/operator-mode-test.gts b/packages/host/tests/integration/components/operator-mode-test.gts index e07850cd1db..c0fcc525565 100644 --- a/packages/host/tests/integration/components/operator-mode-test.gts +++ b/packages/host/tests/integration/components/operator-mode-test.gts @@ -97,8 +97,8 @@ module('Integration | operator-mode | basics', function (hooks) { test('navigating to an archived realm shows the sealed state, not card chrome or a generic error', async function (assert) { // A realm sealed by the archive flag answers content requests with 403 and - // the X-Boxel-Realm-Archived marker (CS-11664). Intercept the card fetch to - // reproduce that response and assert the host renders the sealed state. + // the X-Boxel-Realm-Archived marker. Intercept the card fetch to reproduce + // that response and assert the host renders the sealed state. let networkService = getService('network'); networkService.virtualNetwork.mount( async (req: Request) => { diff --git a/packages/runtime-common/realm.ts b/packages/runtime-common/realm.ts index d4ae3574bb7..e970164f729 100644 --- a/packages/runtime-common/realm.ts +++ b/packages/runtime-common/realm.ts @@ -2778,9 +2778,10 @@ export class Realm { // content see 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). The seal is method-agnostic, so reads - // and writes are blocked by this one check. The operational - // `_readiness-check` (the realm's only public endpoint) is exempt so - // health probes don't read an archived realm as down. The + // and writes are blocked by this one check. The realm's public + // endpoints (`POST /_session`, `GET /_readiness-check`) are exempt so + // the auth handshake and health probes still respond; an archived + // realm's content requests still hit the seal. The // archive-management endpoints live on the realm SERVER router and // never reach this boundary, so they stay reachable. Read fresh (no // memoization) for the same reason createRequestContext does: a peer