From b6e3218e80b5b46a018f53de77e999eeae00d58b Mon Sep 17 00:00:00 2001 From: ylm Date: Thu, 18 Jun 2026 17:14:24 -0400 Subject: [PATCH 1/3] CS-11658: boot assembly from trusted servers via _realm-auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the "account data is the realm list" boot path with "account data is the list of trusted servers; ask each server which realms the user has." Builds on CS-11655 which introduced the new `app.boxel.realm-servers` account-data event type. - realm-server: new fetchUserRealmsFromTrustedServers() iterates the trusted-server URLs, POSTs _realm-auth on each, and returns the union of realm URLs. Preserves the single-server invariant by calling assertOwnRealmServer() — multi-realm-server federation is out of scope for v1. - matrix-service start(): reads APP_BOXEL_REALM_SERVERS_EVENT_TYPE in parallel with favorites and assembles user realms via the new helper. Hands the result to setAvailableRealmIdentifiers and initSlidingSync (replacing the direct read of app.boxel.realms). fetchCatalogRealms() is unchanged. - Transition fallback: when app.boxel.realm-servers is absent or empty, the boot falls back to reading the legacy app.boxel.realms key so existing users aren't broken before CS-11659's lazy migration has run on their account. The fallback is clearly marked for removal once that migration ships. - Tests: boot populates the realm list from the trusted-servers path; direct unit coverage of the new method (round-trip, empty input short-circuit, non-own server rejection); fallback module verifies the legacy path still works when realm-servers is empty. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/host/app/services/matrix-service.ts | 38 ++++-- packages/host/app/services/realm-server.ts | 43 +++++++ .../matrix-service-boot-assembly-test.ts | 120 ++++++++++++++++++ 3 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 packages/host/tests/integration/matrix-service-boot-assembly-test.ts diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index 0ce074eea2..b73acdd4af 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -836,17 +836,41 @@ export default class MatrixService extends Service { this.startedAtTs = 0; } if (isTesting()) - console.warn('[start-phase] getAccountData(realms,favorites)'); - let [accountDataContent, favoritesData] = await Promise.all([ + console.warn('[start-phase] getAccountData(realm-servers,favorites)'); + let [realmServersData, favoritesData] = await Promise.all([ this.client.getAccountDataFromServer( - APP_BOXEL_REALMS_EVENT_TYPE, - ) as Promise<{ realms: string[] } | null>, + APP_BOXEL_REALM_SERVERS_EVENT_TYPE, + ) as Promise<{ realmServers: string[] } | null>, this.client.getAccountDataFromServer( APP_BOXEL_WORKSPACE_FAVORITES_EVENT_TYPE, ) as Promise<{ favorites: string[] } | null>, ]); this.workspaceFavorites = favoritesData?.favorites ?? []; + // CS-11658: boot assembles the realm list from trusted servers via + // `_realm-auth`. The transition fallback below reads the legacy + // `app.boxel.realms` key when `app.boxel.realm-servers` is absent + // or empty — necessary until CS-11659's lazy migration populates + // the new key for existing users. Remove the fallback once that + // migration has run on all active accounts. + let trustedServers = realmServersData?.realmServers ?? []; + let userRealmURLs: string[]; + if (trustedServers.length > 0) { + if (isTesting()) + console.warn('[start-phase] fetchUserRealmsFromTrustedServers'); + userRealmURLs = + await this.realmServer.fetchUserRealmsFromTrustedServers( + trustedServers, + ); + } else { + if (isTesting()) + console.warn('[start-phase] getAccountData(realms-legacy)'); + let legacyRealmsData = (await this.client.getAccountDataFromServer( + APP_BOXEL_REALMS_EVENT_TYPE, + )) as { realms: string[] } | null; + userRealmURLs = legacyRealmsData?.realms ?? []; + } + let noRealmsLoggedIn = Array.from(this.realm.realms.entries()).every( ([_url, realmResource]) => !realmResource.isLoggedIn, ); @@ -857,9 +881,7 @@ export default class MatrixService extends Service { ); await Promise.all([ this.realmServer.fetchCatalogRealms(), - this.realmServer.setAvailableRealmIdentifiers( - (accountDataContent?.realms ?? []).map(ri), - ), + this.realmServer.setAvailableRealmIdentifiers(userRealmURLs.map(ri)), ]); if (isTesting()) console.warn('[start-phase] prefetchRealmInfos'); @@ -868,7 +890,7 @@ export default class MatrixService extends Service { ); if (isTesting()) console.warn('[start-phase] initSlidingSync'); - await this.initSlidingSync(accountDataContent); + await this.initSlidingSync({ realms: userRealmURLs }); if (isTesting()) console.warn('[start-phase] startClient'); await this.client.startClient({ slidingSync: this.slidingSync }); if (isTesting()) diff --git a/packages/host/app/services/realm-server.ts b/packages/host/app/services/realm-server.ts index de0ac37780..f339a4130c 100644 --- a/packages/host/app/services/realm-server.ts +++ b/packages/host/app/services/realm-server.ts @@ -259,6 +259,49 @@ export default class RealmServerService extends Service { return response.json(); } + // CS-11658: boot assembly reads `app.boxel.realm-servers` and asks each + // trusted server (via `_realm-auth`) which realms the current user has. + // Returns the union of realm URLs across all trusted servers. v1 keeps + // the single-server invariant — assertOwnRealmServer() rejects any list + // that includes a non-own server until multi-realm-server federation + // ships. + async fetchUserRealmsFromTrustedServers( + trustedServerURLs: string[], + ): Promise { + if (trustedServerURLs.length === 0) { + return []; + } + // TODO: remove once multi-realm-server federation lands. + this.assertOwnRealmServer(trustedServerURLs); + await this.login(); + let realmURLs = new Set(); + for (let serverURL of trustedServerURLs) { + let normalizedServerURL = ensureTrailingSlash(serverURL); + let response = await this.network.fetch( + `${normalizedServerURL}_realm-auth`, + { + method: 'POST', + headers: { + Accept: SupportedMimeType.JSONAPI, + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.token}`, + }, + }, + ); + if (!response.ok) { + let responseText = await response.text(); + throw new Error( + `Failed to fetch user realms from trusted server ${normalizedServerURL}: ${response.status} - ${responseText}`, + ); + } + let tokens = (await response.json()) as Record; + for (let realmURL of Object.keys(tokens)) { + realmURLs.add(realmURL); + } + } + return [...realmURLs]; + } + @cached get availableRealmIdentifiers(): RealmIdentifier[] { return this.availableRealms.map((r) => ri(r.url)); diff --git a/packages/host/tests/integration/matrix-service-boot-assembly-test.ts b/packages/host/tests/integration/matrix-service-boot-assembly-test.ts new file mode 100644 index 0000000000..9e7f129dba --- /dev/null +++ b/packages/host/tests/integration/matrix-service-boot-assembly-test.ts @@ -0,0 +1,120 @@ +import type { RenderingTestContext } from '@ember/test-helpers'; + +import { getService } from '@universal-ember/test-support'; +import { module, test } from 'qunit'; + +import { baseRealm, ensureTrailingSlash, ri } from '@cardstack/runtime-common'; + +import ENV from '@cardstack/host/config/environment'; +import type RealmServerService from '@cardstack/host/services/realm-server'; + +import { + testRealmURL, + setupIntegrationTestRealm, + setupLocalIndexing, +} from '../helpers'; + +import { setupBaseRealm } from '../helpers/base-realm'; + +import { setupMockMatrix } from '../helpers/mock-matrix'; + +import { setupRenderingTest } from '../helpers/setup'; + +const testRealmServerURL = ensureTrailingSlash(ENV.realmServerURL); + +// CS-11658: boot assembles the available-realms list from the user's +// trusted realm-servers (`app.boxel.realm-servers`) by asking each via +// `_realm-auth`, instead of reading the realm list directly out of +// `app.boxel.realms`. A transition fallback to the legacy key remains +// until CS-11659's lazy migration has run on all active accounts. +module( + 'Integration | matrix-service | boot assembly with trusted servers', + function (hooks) { + setupRenderingTest(hooks); + setupBaseRealm(hooks); + setupLocalIndexing(hooks); + + let mockMatrixUtils = setupMockMatrix(hooks, { + loggedInAs: '@testuser:localhost', + activeRealms: [baseRealm.url, testRealmURL], + activeRealmServers: [testRealmServerURL], + autostart: true, + }); + + hooks.beforeEach(async function (this: RenderingTestContext) { + await setupIntegrationTestRealm({ + mockMatrixUtils, + contents: {}, + }); + }); + + test('boot populates availableRealmIdentifiers when `app.boxel.realm-servers` is set', async function (assert) { + let realmServer = getService('realm-server') as RealmServerService; + assert.ok( + realmServer.availableRealmIdentifiers.includes(ri(testRealmURL)), + 'testRealmURL is present in availableRealmIdentifiers', + ); + }); + + test('fetchUserRealmsFromTrustedServers returns realms advertised by `_realm-auth`', async function (assert) { + let realmServer = getService('realm-server') as RealmServerService; + let realms = await realmServer.fetchUserRealmsFromTrustedServers([ + testRealmServerURL, + ]); + assert.deepEqual( + realms, + [testRealmURL], + 'returns the trusted server’s realms', + ); + }); + + test('fetchUserRealmsFromTrustedServers returns [] for an empty input', async function (assert) { + let realmServer = getService('realm-server') as RealmServerService; + let realms = await realmServer.fetchUserRealmsFromTrustedServers([]); + assert.deepEqual(realms, [], 'short-circuits without any HTTP call'); + }); + + test('fetchUserRealmsFromTrustedServers rejects non-own realm-server URLs', async function (assert) { + let realmServer = getService('realm-server') as RealmServerService; + await assert.rejects( + realmServer.fetchUserRealmsFromTrustedServers([ + 'https://other-server.example/', + ]), + /Multi-realm server support is not yet implemented/, + ); + }); + }, +); + +module( + 'Integration | matrix-service | boot assembly fallback to legacy realms', + function (hooks) { + setupRenderingTest(hooks); + setupBaseRealm(hooks); + setupLocalIndexing(hooks); + + // No activeRealmServers — the mock returns `{ realmServers: [] }`, the + // same shape the host sees for a user who hasn’t been migrated to + // `app.boxel.realm-servers` yet (CS-11659). + let mockMatrixUtils = setupMockMatrix(hooks, { + loggedInAs: '@testuser:localhost', + activeRealms: [baseRealm.url, testRealmURL], + autostart: true, + }); + + hooks.beforeEach(async function (this: RenderingTestContext) { + await setupIntegrationTestRealm({ + mockMatrixUtils, + contents: {}, + }); + }); + + test('boot still populates realms from `app.boxel.realms`', async function (assert) { + let realmServer = getService('realm-server') as RealmServerService; + assert.ok( + realmServer.availableRealmIdentifiers.includes(ri(testRealmURL)), + 'testRealmURL is present in availableRealmIdentifiers', + ); + }); + }, +); From a3f4db36a1a3ecc798d8e58545be9be1ad31ce6b Mon Sep 17 00:00:00 2001 From: ylm Date: Thu, 18 Jun 2026 17:22:49 -0400 Subject: [PATCH 2/3] Parallelize _realm-auth fetches across trusted servers Issue per-server _realm-auth requests concurrently via Promise.all, then union the returned realm URLs. Failure semantics unchanged: the first rejection still surfaces (graceful degradation is CS-11667). Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/host/app/services/realm-server.ts | 47 +++++++++++----------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/packages/host/app/services/realm-server.ts b/packages/host/app/services/realm-server.ts index f339a4130c..a1ab3714fb 100644 --- a/packages/host/app/services/realm-server.ts +++ b/packages/host/app/services/realm-server.ts @@ -274,32 +274,31 @@ export default class RealmServerService extends Service { // TODO: remove once multi-realm-server federation lands. this.assertOwnRealmServer(trustedServerURLs); await this.login(); - let realmURLs = new Set(); - for (let serverURL of trustedServerURLs) { - let normalizedServerURL = ensureTrailingSlash(serverURL); - let response = await this.network.fetch( - `${normalizedServerURL}_realm-auth`, - { - method: 'POST', - headers: { - Accept: SupportedMimeType.JSONAPI, - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.token}`, + let perServerRealmURLs = await Promise.all( + trustedServerURLs.map(async (serverURL) => { + let normalizedServerURL = ensureTrailingSlash(serverURL); + let response = await this.network.fetch( + `${normalizedServerURL}_realm-auth`, + { + method: 'POST', + headers: { + Accept: SupportedMimeType.JSONAPI, + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.token}`, + }, }, - }, - ); - if (!response.ok) { - let responseText = await response.text(); - throw new Error( - `Failed to fetch user realms from trusted server ${normalizedServerURL}: ${response.status} - ${responseText}`, ); - } - let tokens = (await response.json()) as Record; - for (let realmURL of Object.keys(tokens)) { - realmURLs.add(realmURL); - } - } - return [...realmURLs]; + if (!response.ok) { + let responseText = await response.text(); + throw new Error( + `Failed to fetch user realms from trusted server ${normalizedServerURL}: ${response.status} - ${responseText}`, + ); + } + let tokens = (await response.json()) as Record; + return Object.keys(tokens); + }), + ); + return [...new Set(perServerRealmURLs.flat())]; } @cached From 4bb2b1283ed55b91363a918ef86e5f2d4a7f9e81 Mon Sep 17 00:00:00 2001 From: ylm Date: Thu, 18 Jun 2026 17:43:33 -0400 Subject: [PATCH 3/3] Make trusted-servers authoritative against the legacy realms event Addresses the Codex review on PR #5285. The matrix sync triggered by `startClient()` re-emits the existing `app.boxel.realms` AccountData event, and the listener bound during `bindEventListeners` was overwriting the trusted-servers boot result with the legacy key's content. - matrix-service: track `trustedRealmServersAuthoritative`. Boot sets it true when `app.boxel.realm-servers` has entries; the new realm-servers listener flips it on at runtime if the key gains content. - Legacy `app.boxel.realms` listener: skip `setAvailableRealmIdentifiers` while the flag is true. Login side effects (loginToRealms, loadMoreAuthRooms) still run so authentication for new realms isn't dropped. - New `app.boxel.realm-servers` listener: re-fetch via _realm-auth, call setAvailableRealmIdentifiers, then loginToRealms / loadMoreAuthRooms post-login. Natural runtime counterpart to the new boot path. - Regression test: a setup where mock activeRealms = [] but realmPermissions advertises two realms via _realm-auth verifies both _realm-auth realms survive `startClient`'s synthetic event. Without the listener gating the test fails. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/host/app/services/matrix-service.ts | 49 +++++++++++++++++-- .../matrix-service-boot-assembly-test.ts | 48 ++++++++++++++++++ 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index b73acdd4af..1f6f9e6eb5 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -175,6 +175,13 @@ export default class MatrixService extends Service { @tracked private _client: ExtendedClient | undefined; @tracked private _isInitializingNewUser = false; @tracked private postLoginCompleted = false; + // CS-11658: when true, `app.boxel.realm-servers` is the authoritative + // source of the user's realm list and `app.boxel.realms` events are + // ignored for `setAvailableRealmIdentifiers`. Set during boot based on + // the new key's presence; flipped on by the realm-servers listener if + // the key gains content at runtime. Login-related side effects + // (`loginToRealms`, `loadMoreAuthRooms`) still run regardless. + private trustedRealmServersAuthoritative = false; @tracked private _currentRoomId: string | undefined; @tracked private timelineLoadingState: Map = new TrackedMap(); @@ -394,16 +401,43 @@ export default class MatrixService extends Service { this.matrixSDK.ClientEvent.AccountData, async (e) => { switch (e.event.type) { - case APP_BOXEL_REALMS_EVENT_TYPE: - await this.realmServer.setAvailableRealmIdentifiers( - (e.event.content.realms as string[]).map(ri), - ); + case APP_BOXEL_REALMS_EVENT_TYPE: { + let legacyRealms = e.event.content.realms as string[]; + // CS-11658: when `app.boxel.realm-servers` is the source of + // truth, ignore the realm-list payload here — otherwise the + // initial-sync re-emission of this event would overwrite the + // trusted-servers boot result. Side effects below still run + // so post-login realm authentication isn't dropped. + if (!this.trustedRealmServersAuthoritative) { + await this.realmServer.setAvailableRealmIdentifiers( + legacyRealms.map(ri), + ); + } // Only do this after we've completed our overall login if (this.postLoginCompleted) { await this.loginToRealms(); - await this.loadMoreAuthRooms(e.event.content.realms); + await this.loadMoreAuthRooms(legacyRealms); + } + break; + } + case APP_BOXEL_REALM_SERVERS_EVENT_TYPE: { + let realmServers = e.event.content.realmServers as string[]; + this.trustedRealmServersAuthoritative = realmServers.length > 0; + if (this.trustedRealmServersAuthoritative) { + let realmURLs = + await this.realmServer.fetchUserRealmsFromTrustedServers( + realmServers, + ); + await this.realmServer.setAvailableRealmIdentifiers( + realmURLs.map(ri), + ); + if (this.postLoginCompleted) { + await this.loginToRealms(); + await this.loadMoreAuthRooms(realmURLs); + } } break; + } case APP_BOXEL_SYSTEM_CARD_EVENT_TYPE: await this.setSystemCard(e.event.content.id); break; @@ -854,6 +888,11 @@ export default class MatrixService extends Service { // the new key for existing users. Remove the fallback once that // migration has run on all active accounts. let trustedServers = realmServersData?.realmServers ?? []; + // The legacy `app.boxel.realms` AccountData event is re-emitted by + // the matrix sync that runs inside `startClient()` below. Setting + // this flag here makes that re-emission a no-op for the available- + // realms list — the realm-servers path is the authoritative source. + this.trustedRealmServersAuthoritative = trustedServers.length > 0; let userRealmURLs: string[]; if (trustedServers.length > 0) { if (isTesting()) diff --git a/packages/host/tests/integration/matrix-service-boot-assembly-test.ts b/packages/host/tests/integration/matrix-service-boot-assembly-test.ts index 9e7f129dba..f06f1e2540 100644 --- a/packages/host/tests/integration/matrix-service-boot-assembly-test.ts +++ b/packages/host/tests/integration/matrix-service-boot-assembly-test.ts @@ -86,6 +86,54 @@ module( }, ); +module( + 'Integration | matrix-service | trusted-servers result survives legacy event', + function (hooks) { + setupRenderingTest(hooks); + setupBaseRealm(hooks); + setupLocalIndexing(hooks); + + // The mock matrix client's `startClient` re-emits a synthetic + // `app.boxel.realms` AccountData event with `activeRealms` content. + // With the new key authoritative, that re-emission must NOT overwrite + // the realms the trusted-servers boot path discovered. The setup + // below deliberately diverges activeRealms from realmPermissions so + // the bug (if reintroduced) shows up as a missing realm from the + // _realm-auth response. + const otherRealmURL = 'http://test-realm/test-other/'; + + let mockMatrixUtils = setupMockMatrix(hooks, { + loggedInAs: '@testuser:localhost', + activeRealms: [], // synthetic legacy event would clear availableRealms + activeRealmServers: [testRealmServerURL], + realmPermissions: { + [testRealmURL]: ['read', 'write'], + [otherRealmURL]: ['read', 'write'], + }, + autostart: true, + }); + + hooks.beforeEach(async function (this: RenderingTestContext) { + await setupIntegrationTestRealm({ + mockMatrixUtils, + contents: {}, + }); + }); + + test('legacy realms event does not overwrite the trusted-servers boot result', async function (assert) { + let realmServer = getService('realm-server') as RealmServerService; + assert.ok( + realmServer.availableRealmIdentifiers.includes(ri(testRealmURL)), + 'testRealmURL from _realm-auth survives the legacy event', + ); + assert.ok( + realmServer.availableRealmIdentifiers.includes(ri(otherRealmURL)), + 'otherRealmURL from _realm-auth survives the legacy event (regression guard)', + ); + }); + }, +); + module( 'Integration | matrix-service | boot assembly fallback to legacy realms', function (hooks) {