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
16 changes: 5 additions & 11 deletions packages/host/app/components/operator-mode/interact-submode.gts
Original file line number Diff line number Diff line change
Expand Up @@ -752,21 +752,15 @@ export default class InteractSubmode extends Component {
}

let items: { name: string; icon: Icon; ref: ResolvedCodeRef }[] = [];
// A realm index card id and a recent card's id can be in different forms
// (e.g. the base realm's alias `https://cardstack.com/base/index` vs an
// instance's registered-prefix form `@cardstack/base/index`). Unresolve
// both sides to the same form so index cards are excluded regardless.
let { virtualNetwork } = this.network;
// Both sides are canonical: the realm list holds identifiers in canonical
// form and a card's `id` is canonical in memory, so index cards compare
// directly.
const excludedCardIds = new Set(
this.realmServer.availableRealmIndexCardIds.map((id) =>
virtualNetwork.unresolveURL(id),
),
this.realmServer.availableRealmIndexCardIds,
);

recentCards
.filter(
(card) => !excludedCardIds.has(virtualNetwork.unresolveURL(card.id)),
) // filter out realm index cards
.filter((card) => !excludedCardIds.has(card.id)) // filter out realm index cards
.map((card) => {
let ref = identifyCard(card.constructor);
let name = cardTypeDisplayName(card);
Expand Down
14 changes: 5 additions & 9 deletions packages/host/app/components/realm-picker/index.gts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Picker, type PickerOption } from '@cardstack/boxel-ui/components';
import type RealmService from '@cardstack/host/services/realm';
import type RealmServerService from '@cardstack/host/services/realm-server';

import { realmIdentifierSegments } from '../../lib/realm-utils';
import WithKnownRealmsLoaded from '../with-known-realms-loaded';

export interface RealmFilter {
Expand Down Expand Up @@ -86,16 +87,11 @@ export default class RealmPicker extends Component<Signature> {
};

private realmDisplayNameFromURL(realmURL: string): string {
try {
const pathname = new URL(realmURL).pathname;
const segments = pathname.split('/').filter(Boolean);
if (segments.length === 0) {
return 'Base';
}
return segments[segments.length - 1] ?? 'Base';
} catch {
return 'Unknown Workspace';
const segments = realmIdentifierSegments(realmURL);
if (segments.length === 0) {
return 'Base';
}
return segments[segments.length - 1] ?? 'Base';
}

<template>
Expand Down
27 changes: 27 additions & 0 deletions packages/host/app/lib/realm-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
isUrlLike,
RealmPaths,
ri,
rri,
Expand Down Expand Up @@ -52,3 +53,29 @@ export function resolveCardRealmUrl(
}
return new RealmPaths(virtualNetwork.toURL(cardId)).url;
}

/**
* The meaningful segments of a realm identifier, in whichever form it is
* expressed.
*
* A realm identifier is either a URL (`https://host/foo/bar/`) or a registered
* prefix (`@scope/name/`). Only the first has a pathname to take apart, so
* anything deriving segments by parsing loses the prefix form — silently, if
* the parse sits in a `try`. Both forms name the realm by the same trailing
* segments, which is what callers here are after.
*
* @example
* realmIdentifierSegments('https://cardstack.com/base/') // ['base']
* realmIdentifierSegments('@cardstack/base/') // ['@cardstack', 'base']
* realmIdentifierSegments('https://example.com/') // []
*/
export function realmIdentifierSegments(realmIdentifier: string): string[] {
if (!isUrlLike(realmIdentifier)) {
return realmIdentifier.split('/').filter(Boolean);
}
try {
return new URL(realmIdentifier).pathname.split('/').filter(Boolean);
} catch {
return [];
}
}
14 changes: 5 additions & 9 deletions packages/host/app/routes/index.gts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import type RealmService from '@cardstack/host/services/realm';
import type RealmServerService from '@cardstack/host/services/realm-server';
import type StoreService from '@cardstack/host/services/store';

import { realmIdentifierSegments } from '../lib/realm-utils';

const { hostsOwnAssets } = ENV;

export type ErrorModel = {
Expand Down Expand Up @@ -294,15 +296,9 @@ export default class Card extends Route {
// availableRealmIdentifiers is set in matrixService.start(), so we can use it here
let realmUrl = this.realmServer.availableRealmIdentifiers.find(
(realmUrl) => {
// `availableRealmIdentifiers` is seeded from `baseRealm.url` and from
// realm-server responses, so entries are URL-form and this does not
// throw. If prefix forms reach here, this needs to compare namespaces
// rather than pathnames — there is no path to take apart in
// `@scope/name/`.
// eslint-disable-next-line @cardstack/boxel/no-url-from-realm-identifier
let realmPathParts = new URL(realmUrl).pathname
.split('/')
.filter((part) => part !== '');
// A realm identifier may be a URL or a registered prefix, and only
// the first has a pathname; take the segments of whichever it is.
let realmPathParts = realmIdentifierSegments(realmUrl);
let cardPathParts = cardPath!
.split('/')
.filter((part) => part !== '');
Expand Down
32 changes: 23 additions & 9 deletions packages/host/app/services/realm-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import window from 'ember-window-mock';
import { TrackedArray } from 'tracked-built-ins';

import {
baseRealm,
baseRealmRRI,
ensureTrailingSlash,
publishRealm as publishRealmOperation,
SupportedMimeType,
Expand Down Expand Up @@ -110,8 +110,15 @@ export default class RealmServerService extends Service {
@service declare private realmServer: RealmServerService;
private auth: AuthStatus = { type: 'anonymous' };
private client: ExtendedClient | undefined;
// Realm identifiers in canonical form. The base realm is the only entry with
// a registered prefix, so it is the only one whose form could disagree with
// an instance's `id`; user and catalog entries arrive from `_realm-auth` and
// `_catalog-realms` as real URLs with no prefix to fold to, and are already
// canonical. Seeding base with its alias URL instead is what forced callers
// comparing this list against a card id to reconcile the two through the
// VirtualNetwork.
private availableRealms = new TrackedArray<AvailableRealm>([
{ type: 'base', url: baseRealm.url },
{ type: 'base', url: baseRealmRRI },
]);
private archivedRealmsList = new TrackedArray<ArchivedRealmInfo>([]);
private archivedRealmsFetched = false;
Expand Down Expand Up @@ -144,7 +151,7 @@ export default class RealmServerService extends Service {
);
this.logout();
this.availableRealms = new TrackedArray([
{ type: 'base', url: baseRealm.url },
{ type: 'base', url: baseRealmRRI },
...catalogRealms,
]);
// Clear in place rather than reassigning: the `archivedRealms` @cached
Expand Down Expand Up @@ -354,7 +361,7 @@ export default class RealmServerService extends Service {
this.auth = { type: 'anonymous' };
this.availableRealms.splice(0, this.availableRealms.length, {
type: 'base',
url: baseRealm.url,
url: baseRealmRRI,
});
this.unreachableRealmServersList.splice(
0,
Expand Down Expand Up @@ -517,11 +524,18 @@ export default class RealmServerService extends Service {

for (let realmURL of realms) {
let normalizedRealmURL = ensureTrailingSlash(realmURL);
if (
testRealmOrigin &&
new URL(normalizedRealmURL).origin === testRealmOrigin
) {
continue;
// A realm identifier may be a registered prefix, which has no origin to
// compare — resolve it before asking. Anything that resolves to the test
// realm's origin is served by the test realm server and skipped.
if (testRealmOrigin) {
let resolved = this.network.virtualNetwork.isRegisteredPrefix(
normalizedRealmURL,
)
? this.network.virtualNetwork.toURL(normalizedRealmURL).href
: normalizedRealmURL;
if (new URL(resolved).origin === testRealmOrigin) {
continue;
}
}
let token = sessionTokens[normalizedRealmURL] ?? sessionTokens[realmURL];
if (!token) {
Expand Down
6 changes: 6 additions & 0 deletions packages/host/app/services/realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1652,6 +1652,12 @@ export default class RealmService extends Service {
realmURL: string,
token: string | undefined = undefined,
): RealmResource {
// Realm resources are keyed by URL and log in with one, so a caller
// holding the canonical prefix form resolves here — the same
// normalization `info()` performs before its own lookup.
if (this.network.virtualNetwork.isRegisteredPrefix(realmURL)) {
realmURL = this.network.virtualNetwork.toURL(realmURL).href;
}
// this should be the only place we do the untracked read. It needs to be
// untracked so our `this._realms.set` below will not be an assertion.
let resource = this.knownRealm(realmURL, { tracked: false });
Expand Down
Loading