Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 74 additions & 13 deletions packages/host/app/services/matrix-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, boolean> =
new TrackedMap();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -836,17 +870,46 @@ 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 ?? [];
// 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())
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,
);
Expand All @@ -857,9 +920,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');
Expand All @@ -868,7 +929,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())
Expand Down
42 changes: 42 additions & 0 deletions packages/host/app/services/realm-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,48 @@ 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<string[]> {
if (trustedServerURLs.length === 0) {
return [];
}
// TODO: remove once multi-realm-server federation lands.
this.assertOwnRealmServer(trustedServerURLs);
await this.login();
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<string, string>;
return Object.keys(tokens);
}),
);
return [...new Set(perServerRealmURLs.flat())];
}

@cached
get availableRealmIdentifiers(): RealmIdentifier[] {
return this.availableRealms.map((r) => ri(r.url));
Expand Down
168 changes: 168 additions & 0 deletions packages/host/tests/integration/matrix-service-boot-assembly-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
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 | 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) {
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',
);
});
},
);
Loading