From 7df6350f8a96ad1923456df5baf9b40cba688c6c Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 16 Apr 2026 09:27:30 -0500 Subject: [PATCH 1/7] Add addRealmMapping RealmResourceIdentifier step --- .../tests/card-reference-resolver-test.ts | 43 +++++++++++++++++++ packages/runtime-common/virtual-network.ts | 18 +++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/realm-server/tests/card-reference-resolver-test.ts b/packages/realm-server/tests/card-reference-resolver-test.ts index caa6cc48f6d..b2564fbeaab 100644 --- a/packages/realm-server/tests/card-reference-resolver-test.ts +++ b/packages/realm-server/tests/card-reference-resolver-test.ts @@ -6,6 +6,7 @@ import { resolveCardReference, resolveRRI, RealmPaths, + VirtualNetwork, } from '@cardstack/runtime-common'; import type { SingleCardDocument, @@ -618,4 +619,46 @@ module(basename(__filename), function () { }); }); }); + + module('VirtualNetwork.addRealmMapping', function (hooks) { + let vn: VirtualNetwork; + let prefix = '@test/realm/'; + let target = 'http://localhost:9000/realm/'; + + hooks.beforeEach(function () { + vn = new VirtualNetwork(); + vn.addRealmMapping(prefix, target); + }); + + hooks.afterEach(function () { + unregisterCardReferencePrefix(prefix); + }); + + test('populates importMap so resolveImport works', function (assert) { + let result = vn.resolveImport('@test/realm/card-api'); + assert.strictEqual(result, 'http://localhost:9000/realm/card-api'); + }); + + test('populates global prefixMappings so resolveCardReference works', function (assert) { + let result = resolveCardReference('@test/realm/Foo', undefined); + assert.strictEqual(result, 'http://localhost:9000/realm/Foo'); + }); + + test('normalizes trailing slashes', function (assert) { + unregisterCardReferencePrefix(prefix); + let vn2 = new VirtualNetwork(); + // No trailing slashes + vn2.addRealmMapping('@test/other', 'http://localhost:9000/other'); + let result = vn2.resolveImport('@test/other/card'); + assert.strictEqual(result, 'http://localhost:9000/other/card'); + unregisterCardReferencePrefix('@test/other/'); + }); + + test('overwrites cleanly when called twice with same prefix', function (assert) { + let newTarget = 'http://localhost:8000/realm/'; + vn.addRealmMapping(prefix, newTarget); + let result = vn.resolveImport('@test/realm/card-api'); + assert.strictEqual(result, 'http://localhost:8000/realm/card-api'); + }); + }); }); diff --git a/packages/runtime-common/virtual-network.ts b/packages/runtime-common/virtual-network.ts index 6254224cdbb..b6f7e24f38d 100644 --- a/packages/runtime-common/virtual-network.ts +++ b/packages/runtime-common/virtual-network.ts @@ -1,5 +1,6 @@ -import { RealmPaths } from './paths'; +import { RealmPaths, ensureTrailingSlash } from './paths'; import { baseRealm, isNode } from './index'; +import { registerCardReferencePrefix } from './card-reference-resolver'; import type { ModuleDescriptor } from './package-shim-handler'; import { PackageShimHandler, @@ -19,6 +20,7 @@ export class VirtualNetwork { private handlers: Handler[] = []; private urlMappings: [string, string][] = []; private importMap: Map string> = new Map(); + private realmMappings = new Map(); constructor(nativeFetch = createEnvironmentAwareFetch()) { this.nativeFetch = nativeFetch; @@ -66,6 +68,20 @@ export class VirtualNetwork { this.importMap.set(prefix, handler); } + addRealmMapping(realmIdentifier: string, targetURL: string): void { + let normalizedId = ensureTrailingSlash(realmIdentifier); + let normalizedTarget = ensureTrailingSlash(targetURL); + this.realmMappings.set(normalizedId, normalizedTarget); + + // Backward compat bridge: populate both existing registration systems + // so that resolveImport and resolveCardReference continue to work + this.addImportMap( + normalizedId, + (rest) => new URL(rest, normalizedTarget).href, + ); + registerCardReferencePrefix(normalizedId, normalizedTarget); + } + private nativeFetch: typeof globalThis.fetch; private resolveURLMapping( From a27b123e6a27b9059ba24841225656d460702e9c Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 16 Apr 2026 14:54:11 -0500 Subject: [PATCH 2/7] Add RRI support to VirtualNetwork.fetch (CS-10746) VirtualNetwork.fetch now accepts scoped RRI strings (e.g. "@cardstack/base/card-api") in addition to URLs and Requests. RRIs are resolved to real URLs via realm mappings before creating the Request, since new Request('@cardstack/base/...') would throw. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../tests/card-reference-resolver-test.ts | 55 +++++++++++++++++++ packages/runtime-common/virtual-network.ts | 18 ++++++ 2 files changed, 73 insertions(+) diff --git a/packages/realm-server/tests/card-reference-resolver-test.ts b/packages/realm-server/tests/card-reference-resolver-test.ts index b2564fbeaab..57f55286dec 100644 --- a/packages/realm-server/tests/card-reference-resolver-test.ts +++ b/packages/realm-server/tests/card-reference-resolver-test.ts @@ -661,4 +661,59 @@ module(basename(__filename), function () { assert.strictEqual(result, 'http://localhost:8000/realm/card-api'); }); }); + + module('VirtualNetwork.fetch with RRI', function (hooks) { + let vn: VirtualNetwork; + let prefix = '@test/fetch-realm/'; + let target = 'http://localhost:9000/fetch-realm/'; + + hooks.beforeEach(function () { + vn = new VirtualNetwork(); + vn.addRealmMapping(prefix, target); + }); + + hooks.afterEach(function () { + unregisterCardReferencePrefix(prefix); + }); + + test('resolves scoped RRI to real URL and fetches', async function (assert) { + let interceptedUrl: string | undefined; + vn.mount(async (req: Request) => { + interceptedUrl = req.url; + return new Response('ok', { status: 200 }); + }); + + let response = await vn.fetch('@test/fetch-realm/card-api'); + assert.strictEqual(response.status, 200); + assert.strictEqual( + interceptedUrl, + 'http://localhost:9000/fetch-realm/card-api', + ); + }); + + test('passes through normal URLs unchanged', async function (assert) { + let interceptedUrl: string | undefined; + vn.mount(async (req: Request) => { + interceptedUrl = req.url; + return new Response('ok', { status: 200 }); + }); + + await vn.fetch('http://localhost:9000/fetch-realm/card-api'); + assert.strictEqual( + interceptedUrl, + 'http://localhost:9000/fetch-realm/card-api', + ); + }); + + test('passes RequestInit through when fetching with RRI', async function (assert) { + let interceptedMethod: string | undefined; + vn.mount(async (req: Request) => { + interceptedMethod = req.method; + return new Response('ok', { status: 200 }); + }); + + await vn.fetch('@test/fetch-realm/card-api', { method: 'POST' }); + assert.strictEqual(interceptedMethod, 'POST'); + }); + }); }); diff --git a/packages/runtime-common/virtual-network.ts b/packages/runtime-common/virtual-network.ts index b6f7e24f38d..d750190d3c6 100644 --- a/packages/runtime-common/virtual-network.ts +++ b/packages/runtime-common/virtual-network.ts @@ -134,6 +134,15 @@ export class VirtualNetwork { urlOrRequest: string | URL | Request, init?: RequestInit, ) => { + // Resolve RRI strings to real URLs before creating the Request, + // since new Request('@cardstack/base/...') would throw (not a valid URL). + if (typeof urlOrRequest === 'string') { + let resolved = this.resolveRRIToURL(urlOrRequest); + if (resolved) { + urlOrRequest = resolved; + } + } + let request = urlOrRequest instanceof Request ? urlOrRequest @@ -151,6 +160,15 @@ export class VirtualNetwork { return response; }; + private resolveRRIToURL(rri: string): string | undefined { + for (let [prefix, target] of this.realmMappings) { + if (rri.startsWith(prefix)) { + return new URL(rri.slice(prefix.length), target).href; + } + } + return undefined; + } + private async runFetch(request: Request, init?: RequestInit) { let handlers: FetcherMiddlewareHandler[] = this.handlers.map((h) => { return async (request, next) => { From e04d6f58bc38fcdb5dba9b438609c6bac335a7db Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 16 Apr 2026 15:28:39 -0500 Subject: [PATCH 3/7] Add knownRealms() to VirtualNetwork (CS-10747) Returns the list of registered RealmIdentifiers from realm mappings. Non-network code can use this to check realm membership without needing to resolve RRIs to URLs. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../tests/card-reference-resolver-test.ts | 17 +++++++++++++++++ packages/runtime-common/virtual-network.ts | 9 ++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/realm-server/tests/card-reference-resolver-test.ts b/packages/realm-server/tests/card-reference-resolver-test.ts index 57f55286dec..96c56586857 100644 --- a/packages/realm-server/tests/card-reference-resolver-test.ts +++ b/packages/realm-server/tests/card-reference-resolver-test.ts @@ -660,6 +660,23 @@ module(basename(__filename), function () { let result = vn.resolveImport('@test/realm/card-api'); assert.strictEqual(result, 'http://localhost:8000/realm/card-api'); }); + + test('knownRealms returns registered realm identifiers', function (assert) { + let realms = vn.knownRealms(); + assert.true( + realms.includes('@test/realm/' as any), + 'contains the registered realm', + ); + }); + + test('knownRealms reflects multiple registrations', function (assert) { + vn.addRealmMapping('@test/other/', 'http://localhost:9000/other/'); + let realms = vn.knownRealms(); + assert.strictEqual(realms.length, 2); + assert.true(realms.includes('@test/realm/' as any)); + assert.true(realms.includes('@test/other/' as any)); + unregisterCardReferencePrefix('@test/other/'); + }); }); module('VirtualNetwork.fetch with RRI', function (hooks) { diff --git a/packages/runtime-common/virtual-network.ts b/packages/runtime-common/virtual-network.ts index d750190d3c6..5ba5bfd29e0 100644 --- a/packages/runtime-common/virtual-network.ts +++ b/packages/runtime-common/virtual-network.ts @@ -1,6 +1,9 @@ import { RealmPaths, ensureTrailingSlash } from './paths'; import { baseRealm, isNode } from './index'; -import { registerCardReferencePrefix } from './card-reference-resolver'; +import { + registerCardReferencePrefix, + type RealmIdentifier, +} from './card-reference-resolver'; import type { ModuleDescriptor } from './package-shim-handler'; import { PackageShimHandler, @@ -82,6 +85,10 @@ export class VirtualNetwork { registerCardReferencePrefix(normalizedId, normalizedTarget); } + knownRealms(): RealmIdentifier[] { + return [...this.realmMappings.keys()] as RealmIdentifier[]; + } + private nativeFetch: typeof globalThis.fetch; private resolveURLMapping( From cbb7115a771236acbcd9d69d22099d7211b1bb8e Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 16 Apr 2026 15:30:50 -0500 Subject: [PATCH 4/7] Migrate host network service to addRealmMapping (CS-10748) Replace registerCardReferencePrefix + addImportMap pairs with single addRealmMapping calls for catalog, skills, and openrouter realms. Add addRealmMapping for @cardstack/base/ scoped prefix (new). Keep addURLMapping for the fake https://cardstack.com/base/ URL mapping which is still needed during the transition. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/host/app/services/network.ts | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/host/app/services/network.ts b/packages/host/app/services/network.ts index c452419d577..08fc828f7d9 100644 --- a/packages/host/app/services/network.ts +++ b/packages/host/app/services/network.ts @@ -5,7 +5,6 @@ import { VirtualNetwork, authorizationMiddleware, baseRealm, - registerCardReferencePrefix, fetcher, } from '@cardstack/runtime-common'; @@ -54,33 +53,33 @@ export default class NetworkService extends Service { let resolvedBaseRealmURL = new URL( withTrailingSlash(config.resolvedBaseRealmURL), ); + // URL mapping kept for the fake https://cardstack.com/base/ → real URL. + // addRealmMapping registers the @cardstack/base/ scoped prefix. virtualNetwork.addURLMapping(new URL(baseRealm.url), resolvedBaseRealmURL); + virtualNetwork.addRealmMapping( + '@cardstack/base/', + resolvedBaseRealmURL.href, + ); shimExternals(virtualNetwork); virtualNetwork.addImportMap('@cardstack/boxel-icons/', (rest) => { return `${config.iconsURL}/@cardstack/boxel-icons/v1/icons/${rest}.js`; }); if (config.resolvedCatalogRealmURL) { - let catalogURL = withTrailingSlash(config.resolvedCatalogRealmURL); - registerCardReferencePrefix('@cardstack/catalog/', catalogURL); - virtualNetwork.addImportMap( + virtualNetwork.addRealmMapping( '@cardstack/catalog/', - (rest) => new URL(rest, catalogURL).href, + withTrailingSlash(config.resolvedCatalogRealmURL), ); } if (config.resolvedSkillsRealmURL) { - let skillsURL = withTrailingSlash(config.resolvedSkillsRealmURL); - registerCardReferencePrefix('@cardstack/skills/', skillsURL); - virtualNetwork.addImportMap( + virtualNetwork.addRealmMapping( '@cardstack/skills/', - (rest) => new URL(rest, skillsURL).href, + withTrailingSlash(config.resolvedSkillsRealmURL), ); } if (config.resolvedOpenRouterRealmURL) { - let openRouterURL = withTrailingSlash(config.resolvedOpenRouterRealmURL); - registerCardReferencePrefix('@cardstack/openrouter/', openRouterURL); - virtualNetwork.addImportMap( + virtualNetwork.addRealmMapping( '@cardstack/openrouter/', - (rest) => new URL(rest, openRouterURL).href, + withTrailingSlash(config.resolvedOpenRouterRealmURL), ); } return virtualNetwork; From cf90f6288026f5a86317de790e0612db6e00d4a2 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 16 Apr 2026 15:32:52 -0500 Subject: [PATCH 5/7] Migrate realm server setup to addRealmMapping (CS-10749) Replace registerCardReferencePrefix + addImportMap pairs with single addRealmMapping calls in main.ts and worker.ts for non-URL prefix mappings (e.g. @cardstack/catalog/). URL-based mappings still use addURLMapping. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/realm-server/main.ts | 5 +---- packages/realm-server/worker.ts | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/realm-server/main.ts b/packages/realm-server/main.ts index f065e41a2d9..3aaadb6e581 100644 --- a/packages/realm-server/main.ts +++ b/packages/realm-server/main.ts @@ -9,7 +9,6 @@ import { CachingDefinitionLookup, DEFAULT_CARD_SIZE_LIMIT_BYTES, DEFAULT_FILE_SIZE_LIMIT_BYTES, - registerCardReferencePrefix, } from '@cardstack/runtime-common'; import { NodeAdapter } from './node-realm'; import yargs from 'yargs'; @@ -237,9 +236,7 @@ for (let i = 0; i < fromUrls.length; i++) { virtualNetwork.addURLMapping(fromURL, to); urlMappings.push([fromURL, to]); } else { - // Non-URL prefix like @cardstack/catalog/ - registerCardReferencePrefix(from, to.href); - virtualNetwork.addImportMap(from, (rest) => new URL(rest, to).href); + virtualNetwork.addRealmMapping(from, to.href); urlMappings.push([to, to]); // use toUrl for both in hrefs } } diff --git a/packages/realm-server/worker.ts b/packages/realm-server/worker.ts index 86d654b0460..41f7d7f60e4 100644 --- a/packages/realm-server/worker.ts +++ b/packages/realm-server/worker.ts @@ -6,7 +6,6 @@ import { isUrlLike, logger, IndexWriter, - registerCardReferencePrefix, type StatusArgs, type IndexingProgressEvent, } from '@cardstack/runtime-common'; @@ -107,8 +106,7 @@ for (let i = 0; i < fromUrls.length; i++) { if (isUrlLike(from)) { virtualNetwork.addURLMapping(new URL(from), to); } else { - registerCardReferencePrefix(from, to.href); - virtualNetwork.addImportMap(from, (rest) => new URL(rest, to).href); + virtualNetwork.addRealmMapping(from, to.href); } } let autoMigrate = migrateDB || undefined; From 67fa32fc809b3e3fac75a1897395c9a87c198249 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 16 Apr 2026 16:01:19 -0500 Subject: [PATCH 6/7] Add integration test for @cardstack/base/card-api through full fetch chain Verifies that addRealmMapping + VirtualNetwork.fetch resolves @cardstack/base/card-api to http://localhost:4201/base/card-api through the complete middleware chain. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../tests/card-reference-resolver-test.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/realm-server/tests/card-reference-resolver-test.ts b/packages/realm-server/tests/card-reference-resolver-test.ts index 96c56586857..c539c4dfb6a 100644 --- a/packages/realm-server/tests/card-reference-resolver-test.ts +++ b/packages/realm-server/tests/card-reference-resolver-test.ts @@ -732,5 +732,26 @@ module(basename(__filename), function () { await vn.fetch('@test/fetch-realm/card-api', { method: 'POST' }); assert.strictEqual(interceptedMethod, 'POST'); }); + + test('@cardstack/base/card-api resolves through full fetch chain', async function (assert) { + let baseVN = new VirtualNetwork(); + baseVN.addRealmMapping( + '@cardstack/base/', + 'http://localhost:4201/base/', + ); + let interceptedUrl: string | undefined; + baseVN.mount(async (req: Request) => { + interceptedUrl = req.url; + return new Response('ok', { status: 200 }); + }); + + let response = await baseVN.fetch('@cardstack/base/card-api'); + assert.strictEqual(response.status, 200); + assert.strictEqual( + interceptedUrl, + 'http://localhost:4201/base/card-api', + ); + unregisterCardReferencePrefix('@cardstack/base/'); + }); }); }); From f03b0216c451227208dd00d11dbd5203752c00c1 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 16 Apr 2026 16:13:27 -0500 Subject: [PATCH 7/7] Add formatting autofixes --- .../realm-server/tests/card-reference-resolver-test.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/realm-server/tests/card-reference-resolver-test.ts b/packages/realm-server/tests/card-reference-resolver-test.ts index c539c4dfb6a..4a7641341e9 100644 --- a/packages/realm-server/tests/card-reference-resolver-test.ts +++ b/packages/realm-server/tests/card-reference-resolver-test.ts @@ -735,10 +735,7 @@ module(basename(__filename), function () { test('@cardstack/base/card-api resolves through full fetch chain', async function (assert) { let baseVN = new VirtualNetwork(); - baseVN.addRealmMapping( - '@cardstack/base/', - 'http://localhost:4201/base/', - ); + baseVN.addRealmMapping('@cardstack/base/', 'http://localhost:4201/base/'); let interceptedUrl: string | undefined; baseVN.mount(async (req: Request) => { interceptedUrl = req.url; @@ -747,10 +744,7 @@ module(basename(__filename), function () { let response = await baseVN.fetch('@cardstack/base/card-api'); assert.strictEqual(response.status, 200); - assert.strictEqual( - interceptedUrl, - 'http://localhost:4201/base/card-api', - ); + assert.strictEqual(interceptedUrl, 'http://localhost:4201/base/card-api'); unregisterCardReferencePrefix('@cardstack/base/'); }); });