Skip to content
Open
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
141 changes: 128 additions & 13 deletions packages/host/app/services/matrix-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
APP_BOXEL_REALM_EVENT_TYPE,
APP_BOXEL_REALM_SERVER_EVENT_MSGTYPE,
APP_BOXEL_REALMS_EVENT_TYPE,
APP_BOXEL_REALM_SERVERS_EVENT_TYPE,
APP_BOXEL_WORKSPACE_FAVORITES_EVENT_TYPE,
APP_BOXEL_ACTIVE_LLM,
APP_BOXEL_LLM_MODE,
Expand Down Expand Up @@ -174,6 +175,13 @@ export default class MatrixService extends Service {
@tracked private _client: ExtendedClient | undefined;
@tracked private _isInitializingNewUser = false;
@tracked private postLoginCompleted = false;
// 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 from whether that key
// has content, and 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 @@ -393,16 +401,57 @@ 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[];
// 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) {
// A server-pushed account-data event must not crash the app:
// assembly can reject (e.g. fetchUserRealmsFromTrustedServers
// refuses a list that isn't this user's own realm server) and
// an async event handler that throws surfaces as an unhandled
// rejection. The authoritative, fail-loud assembly runs at
// start(); here we log and leave the available-realms list as
// it was.
try {
let realmURLs =
await this.realmServer.fetchUserRealmsFromTrustedServers(
realmServers,
);
await this.realmServer.setAvailableRealmIdentifiers(
realmURLs.map(ri),
);
if (this.postLoginCompleted) {
await this.loginToRealms();
await this.loadMoreAuthRooms(realmURLs);
}
} catch (err) {
console.error(
'Failed to assemble realms from trusted servers in app.boxel.realm-servers account data',
err,
);
}
}
break;
}
case APP_BOXEL_SYSTEM_CARD_EVENT_TYPE:
await this.setSystemCard(e.event.content.id);
break;
Expand Down Expand Up @@ -711,6 +760,44 @@ export default class MatrixService extends Service {
await this.realmServer.setAvailableRealmIdentifiers(newRealms.map(ri));
}

public async getRealmServersFromAccountData(): Promise<string[]> {
let { realmServers = [] } =
((await this.client.getAccountDataFromServer(
APP_BOXEL_REALM_SERVERS_EVENT_TYPE,
)) as { realmServers: string[] }) ?? {};
return realmServers;
}
Comment on lines +763 to +769

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 🤖] Leaving as-is. app.boxel.realm-servers is a brand-new event type with no legacy/migrated data, and the only writers (these host helpers plus the server-side append) always persist a string[]. The absent case is already handled by ?? {}[]. These helpers also intentionally mirror the existing legacy realms helpers, which use the same un-normalized cast. Same low-risk call as the parallel Codex thread above.


public async setRealmServersInAccountData(
realmServers: string[],
): Promise<void> {
await this.client.setAccountData(APP_BOXEL_REALM_SERVERS_EVENT_TYPE, {
realmServers,
});
}

public async appendRealmServerToAccountData(
realmServerURLString: string,
): Promise<void> {
let realmServers = await this.getRealmServersFromAccountData();
if (realmServers.includes(realmServerURLString)) {
return;
}
await this.setRealmServersInAccountData([
...realmServers,
realmServerURLString,
]);
}

public async removeRealmServerFromAccountData(
realmServerURLString: string,
): Promise<void> {
let realmServers = await this.getRealmServersFromAccountData();
await this.setRealmServersInAccountData(
realmServers.filter((s) => s !== realmServerURLString),
);
}

public async getWorkspaceFavorites(): Promise<string[]> {
let { favorites = [] } =
((await this.client.getAccountDataFromServer(
Expand Down Expand Up @@ -797,17 +884,47 @@ 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 ?? [];

// 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, so users whose accounts haven't yet been migrated to
// `app.boxel.realm-servers` still boot. Remove the fallback once
// the lazy migration that populates `app.boxel.realm-servers` 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 @@ -818,9 +935,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 @@ -829,7 +944,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();
}

// 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. assertOwnRealmServer()
// keeps the single-server invariant — it rejects any list that includes a
// server other than the user's own 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
1 change: 1 addition & 0 deletions packages/host/tests/helpers/mock-matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface Config {
loggedInAs?: string;
displayName?: string;
activeRealms?: string[];
activeRealmServers?: string[];
realmPermissions?: Record<string, string[]>;
expiresInSec?: number;
autostart?: boolean;
Expand Down
8 changes: 8 additions & 0 deletions packages/host/tests/helpers/mock-matrix/_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
APP_BOXEL_COMMAND_RESULT_EVENT_TYPE,
APP_BOXEL_DEBUG_MESSAGE_EVENT_TYPE,
APP_BOXEL_REALMS_EVENT_TYPE,
APP_BOXEL_REALM_SERVERS_EVENT_TYPE,
APP_BOXEL_ROOM_SKILLS_EVENT_TYPE,
APP_BOXEL_REALM_EVENT_TYPE,
APP_BOXEL_CODE_PATCH_RESULT_EVENT_TYPE,
Expand Down Expand Up @@ -98,6 +99,10 @@ export class MockClient implements ExtendedClient {
return {
realms: this.sdkOpts.activeRealms ?? [],
} as unknown as K;
} else if (_eventType === APP_BOXEL_REALM_SERVERS_EVENT_TYPE) {
return {
realmServers: this.sdkOpts.activeRealmServers ?? [],
} as unknown as K;
} else if (_eventType === APP_BOXEL_SYSTEM_CARD_EVENT_TYPE) {
return (this.sdkOpts.systemCardAccountData ?? null) as unknown as K;
} else if (_eventType === APP_BOXEL_WORKSPACE_FAVORITES_EVENT_TYPE) {
Expand Down Expand Up @@ -230,6 +235,8 @@ export class MockClient implements ExtendedClient {
): Promise<{}> {
if (type === APP_BOXEL_REALMS_EVENT_TYPE) {
this.sdkOpts.activeRealms = (data as any).realms;
} else if (type === APP_BOXEL_REALM_SERVERS_EVENT_TYPE) {
this.sdkOpts.activeRealmServers = (data as any).realmServers;
} else if (type === 'm.direct') {
this.sdkOpts.directRooms = (data as any)[this.loggedInAs!];
} else if (type === APP_BOXEL_SYSTEM_CARD_EVENT_TYPE) {
Expand Down Expand Up @@ -655,6 +662,7 @@ export class MockClient implements ExtendedClient {
private eventHandlerType(type: string) {
switch (type) {
case APP_BOXEL_REALMS_EVENT_TYPE:
case APP_BOXEL_REALM_SERVERS_EVENT_TYPE:
case APP_BOXEL_SYSTEM_CARD_EVENT_TYPE:
case APP_BOXEL_WORKSPACE_FAVORITES_EVENT_TYPE:
case 'm.direct':
Expand Down
Loading
Loading