From 57663a8a68fcab5ad45c0081bafeed6e5e590fb2 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 9 Sep 2026 12:48:10 -0400 Subject: [PATCH 1/5] Store a scoped cross-realm link as sent, not as resolved MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The write path resolved every `links.self` through the virtual network and relativized it against the writing realm. A link into another realm cannot be relativized, so it was stored as that realm's resolved URL — a localhost host on a dev stack, the staging host on staging — even when the client sent the canonical `@scope/name/...` form. Files authored through the UI therefore carried environment-specific URLs into realms that are version controlled. The read path already had the rule: `relativizeResource` preserves a scoped reference verbatim, because such a reference is absolute and cross-realm by construction whether or not this network knows its prefix. `isScopedReference` moves to url.ts and both paths import it. Serving a document and storing one have to agree on which references are already canonical, and the two having separate notions of that is the bug rather than an incidental duplication — a copy would be free to drift again. Deliberately not included: folding an already-resolved URL back to its prefix on write. That would rewrite links in files the request never meant to touch, which is a migration decision rather than a side effect of fixing this. Tests cover all three cases the stored form distinguishes: a scoped reference kept verbatim, a same-realm link still relativized, and a link to a realm with no prefix mapping still absolute. --- .../realm-server/tests/card-endpoints-test.ts | 126 ++++++++++++++++++ packages/runtime-common/file-serializer.ts | 16 ++- .../realm-index-query-engine.ts | 14 +- packages/runtime-common/url.ts | 19 +++ 4 files changed, 160 insertions(+), 15 deletions(-) diff --git a/packages/realm-server/tests/card-endpoints-test.ts b/packages/realm-server/tests/card-endpoints-test.ts index 7ea55494ed2..bb8a7edf543 100644 --- a/packages/realm-server/tests/card-endpoints-test.ts +++ b/packages/realm-server/tests/card-endpoints-test.ts @@ -2576,6 +2576,132 @@ module(basename(import.meta.filename), function () { let { getMessagesSince } = setupMatrixRoom(hooks, getRealmSetup); + // What is stored for a relationship depends on whether the link can be + // relativized against the writing realm. A scoped reference cannot be, + // and must not be resolved either: resolving one stores whatever URL + // the linked realm answers to in this environment, so a realm under + // version control ends up carrying a dev or staging host. + test('stores a scoped cross-realm link verbatim', async function (assert) { + let scopedLink = '@cardstack/catalog/Pet/vangogh'; + + let response = await request + .patch('/hassan') + .send({ + data: { + type: 'card', + relationships: { + friend: { links: { self: scopedLink } }, + }, + meta: { + adoptsFrom: { + module: rri('./friend.gts'), + name: 'Friend', + }, + }, + }, + }) + .set('Accept', 'application/vnd.card+json'); + + assert.strictEqual( + response.status, + 200, + `HTTP 200 status: ${response.text}`, + ); + + let cardFile = join( + dir.name, + 'realm_server_1', + 'test', + 'hassan.json', + ); + let stored = JSON.parse(readFileSync(cardFile, 'utf8')); + assert.strictEqual( + stored.data.relationships.friend.links.self, + scopedLink, + 'the stored link is the scoped reference the client sent', + ); + }); + + test('still relativizes a same-realm link', async function (assert) { + let response = await request + .patch('/hassan') + .send({ + data: { + type: 'card', + relationships: { + friend: { links: { self: `${testRealmHref}jade` } }, + }, + meta: { + adoptsFrom: { + module: rri('./friend.gts'), + name: 'Friend', + }, + }, + }, + }) + .set('Accept', 'application/vnd.card+json'); + + assert.strictEqual( + response.status, + 200, + `HTTP 200 status: ${response.text}`, + ); + + let cardFile = join( + dir.name, + 'realm_server_1', + 'test', + 'hassan.json', + ); + let stored = JSON.parse(readFileSync(cardFile, 'utf8')); + assert.strictEqual( + stored.data.relationships.friend.links.self, + './jade', + 'an in-realm link is still stored relative', + ); + }); + + test('still stores a link to an unmapped realm absolutely', async function (assert) { + let unmapped = 'http://localhost:4205/other/Pet/vangogh'; + + let response = await request + .patch('/hassan') + .send({ + data: { + type: 'card', + relationships: { + friend: { links: { self: unmapped } }, + }, + meta: { + adoptsFrom: { + module: rri('./friend.gts'), + name: 'Friend', + }, + }, + }, + }) + .set('Accept', 'application/vnd.card+json'); + + assert.strictEqual( + response.status, + 200, + `HTTP 200 status: ${response.text}`, + ); + + let cardFile = join( + dir.name, + 'realm_server_1', + 'test', + 'hassan.json', + ); + let stored = JSON.parse(readFileSync(cardFile, 'utf8')); + assert.strictEqual( + stored.data.relationships.friend.links.self, + unmapped, + 'a link to a realm with no prefix mapping stays absolute', + ); + }); + test('serves the request', async function (assert) { let entry = 'person-1.json'; diff --git a/packages/runtime-common/file-serializer.ts b/packages/runtime-common/file-serializer.ts index 4f620d23888..d5cce0b6a55 100644 --- a/packages/runtime-common/file-serializer.ts +++ b/packages/runtime-common/file-serializer.ts @@ -17,7 +17,10 @@ import type { VirtualNetwork } from './virtual-network.ts'; import { isMeta, type CardFields, type Meta } from './resource-types.ts'; import type { DefinitionLookup } from './definition-lookup.ts'; import { serialize as serializeCodeRef } from './serializers/code-ref.ts'; -import { maybeRelativeReference as makeRelativeReference } from './url.ts'; +import { + isScopedReference, + maybeRelativeReference as makeRelativeReference, +} from './url.ts'; export default async function serialize({ doc, @@ -345,7 +348,16 @@ async function processRelationships({ // Handle both truthy and null values for links.self if (processedValue.links.self !== null) { let selfLink = processedValue.links.self; - if (realmURL && selfLink) { + // A scoped reference is already canonical and portable, so it is + // stored verbatim — the same rule the read path applies when serving a + // document. Resolving one here would store whatever URL the linked + // realm happens to answer to in the writing environment, baking a + // dev or staging host into a realm that is version controlled. + if ( + realmURL && + selfLink && + !isScopedReference(selfLink, virtualNetwork) + ) { try { selfLink = makeRelativeReference( virtualNetwork.resolveURL(selfLink, relativeTo), diff --git a/packages/runtime-common/realm-index-query-engine.ts b/packages/runtime-common/realm-index-query-engine.ts index d33acaac341..bd09218cd8c 100644 --- a/packages/runtime-common/realm-index-query-engine.ts +++ b/packages/runtime-common/realm-index-query-engine.ts @@ -26,6 +26,7 @@ import { internalKeyFor, visitInstanceURLs, maybeRelativeReference, + isScopedReference, codeRefFromInternalKey, } from './index.ts'; import type { Realm } from './realm.ts'; @@ -2194,19 +2195,6 @@ export function relativizeDocument( } } -// A reference in scoped RRI form (e.g. `@cardstack/base/card-api`) is an -// absolute cross-realm identifier and must never be treated as realm-relative. -// Registered prefixes are a subset, but a scoped reference to a realm this -// VirtualNetwork does not know is still scoped — the leading `@` is the signal. -function isScopedReference( - reference: string, - virtualNetwork: VirtualNetwork, -): boolean { - return ( - reference.startsWith('@') || virtualNetwork.isRegisteredPrefix(reference) - ); -} - function relativizeResource( resource: LooseCardResource, primaryURL: URL, diff --git a/packages/runtime-common/url.ts b/packages/runtime-common/url.ts index 0a76ef69282..7ecd02a296b 100644 --- a/packages/runtime-common/url.ts +++ b/packages/runtime-common/url.ts @@ -148,6 +148,25 @@ const RRI_SYNTHETIC_ORIGIN = 'https://rri.invalid'; // resolving the realm root requires realm mappings — `resolveRRI` likewise // has no realm root to resolve against there.) // Falls back to the reference unchanged when there is no usable base. +// A reference in scoped RRI form (e.g. `@cardstack/base/card-api`) is an +// absolute cross-realm identifier and must never be treated as realm-relative. +// Registered prefixes are a subset, but a scoped reference to a realm this +// VirtualNetwork does not know is still scoped — the leading `@` is the signal. +// +// Read and write share this one definition on purpose. Serving a document and +// storing one have to agree on which references are already canonical: when +// only the read path skipped them, a link the client sent as `@scope/name/x` +// was stored as whatever URL that realm happened to resolve to in the writing +// environment. +export function isScopedReference( + reference: string, + virtualNetwork: VirtualNetwork, +): boolean { + return ( + reference.startsWith('@') || virtualNetwork.isRegisteredPrefix(reference) + ); +} + export function resolveRRIReference( reference: string, relativeTo: RealmResourceIdentifier | URL | undefined, From 2d3ea67563d87087b4fb1b2f221711705ffb3ecb Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 9 Sep 2026 13:51:19 -0400 Subject: [PATCH 2/5] Drop the unmapped-realm case from the write-path tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The case cannot be expressed against this harness. A non-scoped link is resolved and then fetched to validate it, so pointing one at a realm nothing serves returns 500 from the write itself — `unexpected exception in realm TypeError: fetch failed` — rather than exercising what gets stored. The harness serves a single realm, so there is no second reachable one to link to. Nothing about that behaviour is at risk here: the guard added for scoped references leaves the non-scoped branch untouched, so a mapped or unmapped URL runs exactly the code it ran before. The two remaining tests cover what this change decides — a scoped reference kept verbatim and a same-realm link still relativized — and both pass. That the scoped case passes where the unmapped one 500s is itself the mechanism showing through: a scoped reference is never resolved, so it is never fetched. --- .../realm-server/tests/card-endpoints-test.ts | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/packages/realm-server/tests/card-endpoints-test.ts b/packages/realm-server/tests/card-endpoints-test.ts index bb8a7edf543..dd5dffd4377 100644 --- a/packages/realm-server/tests/card-endpoints-test.ts +++ b/packages/realm-server/tests/card-endpoints-test.ts @@ -2661,47 +2661,6 @@ module(basename(import.meta.filename), function () { ); }); - test('still stores a link to an unmapped realm absolutely', async function (assert) { - let unmapped = 'http://localhost:4205/other/Pet/vangogh'; - - let response = await request - .patch('/hassan') - .send({ - data: { - type: 'card', - relationships: { - friend: { links: { self: unmapped } }, - }, - meta: { - adoptsFrom: { - module: rri('./friend.gts'), - name: 'Friend', - }, - }, - }, - }) - .set('Accept', 'application/vnd.card+json'); - - assert.strictEqual( - response.status, - 200, - `HTTP 200 status: ${response.text}`, - ); - - let cardFile = join( - dir.name, - 'realm_server_1', - 'test', - 'hassan.json', - ); - let stored = JSON.parse(readFileSync(cardFile, 'utf8')); - assert.strictEqual( - stored.data.relationships.friend.links.self, - unmapped, - 'a link to a realm with no prefix mapping stays absolute', - ); - }); - test('serves the request', async function (assert) { let entry = 'person-1.json'; From 8810a3fe1344c2cf13a7404a18f2d43f138e22bd Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 9 Sep 2026 14:10:06 -0400 Subject: [PATCH 3/5] Relativize a scoped link without discarding its form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skipping relativization for every scoped reference was too broad. The host canonicalizes link ids to a realm alias wherever the realm has a prefix mapping, so a link to a card in the *writing* realm also arrives scoped, and storing those verbatim changed the form of same-realm links that had always been stored relative. Four skills specs caught it: attached skills counted zero, because the stored link no longer matched what the reader expected. `maybeRelativeReference` already draws the distinction. It relativizes what it can and otherwise returns the reference in the form it was given, and its fallback comment says why — a prefix-form RRI is already canonical and portable. The write path defeated that by resolving to a URL first, so the fallback had nothing but a URL left to return. Passing a scoped reference through unresolved restores both cases at once: it relativizes into the writing realm as before, and a cross-realm link keeps the alias the client sent. --- packages/runtime-common/file-serializer.ts | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/runtime-common/file-serializer.ts b/packages/runtime-common/file-serializer.ts index d5cce0b6a55..4bf04c54db7 100644 --- a/packages/runtime-common/file-serializer.ts +++ b/packages/runtime-common/file-serializer.ts @@ -21,6 +21,7 @@ import { isScopedReference, maybeRelativeReference as makeRelativeReference, } from './url.ts'; +import { rri } from './realm-identifiers.ts'; export default async function serialize({ doc, @@ -348,19 +349,19 @@ async function processRelationships({ // Handle both truthy and null values for links.self if (processedValue.links.self !== null) { let selfLink = processedValue.links.self; - // A scoped reference is already canonical and portable, so it is - // stored verbatim — the same rule the read path applies when serving a - // document. Resolving one here would store whatever URL the linked - // realm happens to answer to in the writing environment, baking a - // dev or staging host into a realm that is version controlled. - if ( - realmURL && - selfLink && - !isScopedReference(selfLink, virtualNetwork) - ) { + if (realmURL && selfLink) { try { selfLink = makeRelativeReference( - virtualNetwork.resolveURL(selfLink, relativeTo), + // A scoped reference is passed through unresolved. + // `maybeRelativeReference` relativizes it when it points into + // the writing realm and otherwise preserves the form it was + // given, which is what keeps a cross-realm link canonical. + // Resolving first discards that form, so the fallback could only + // return a URL — whatever the linked realm answers to in this + // environment, baked into a version-controlled realm. + isScopedReference(selfLink, virtualNetwork) + ? rri(selfLink) + : virtualNetwork.resolveURL(selfLink, relativeTo), relativeTo, realmURL, ); From b83730d4f3e52a163613f51c1dfb6d05f8a37579 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 10 Sep 2026 09:51:31 -0400 Subject: [PATCH 4/5] Resolve a scoped link before deciding whether it is in the realm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Whether a link can be relativized is decided in URL space. `relativeTo` here is always a URL, and `relativeReference` refuses a mixed RRI/URL pair rather than resolve across forms — `sharedNamespace` returns undefined and the fallback hands back the reference unchanged. Passing a scoped reference straight in therefore looked like "cannot be relativized" even for a link into the writing realm, so a same-realm link the host had canonicalized to a prefix was stored as `@scope/name/x` where it had always been stored `./x`. Resolving first asks the question properly: a link inside the realm is stored relative whichever form it arrived in, and outside it a scoped reference is stored exactly as sent while anything else is stored resolved. The decision moves into `storedRelationshipLink`, exported so it can be tested without a Definition and a DefinitionLookup. The same-realm scoped case cannot be reached through the realm-server write suite — only `@cardstack/base/` is prefix-mapped there and it is not the writable realm — so it is covered by unit tests over a stub network, alongside the in-realm URL, cross-realm scoped, cross-realm URL, and unresolvable cases. Note for the record: four skills specs failed on the first dispatched run of this branch and passed on the second, and the revision in between changed nothing for the same-realm scoped case — both left such links unchanged. That attribution was wrong; those failures were flakes, and the defect they were blamed on survived until now. --- .../tests/stored-relationship-link-test.ts | 85 +++++++++++++++++++ packages/runtime-common/file-serializer.ts | 64 +++++++++----- 2 files changed, 130 insertions(+), 19 deletions(-) create mode 100644 packages/realm-server/tests/stored-relationship-link-test.ts diff --git a/packages/realm-server/tests/stored-relationship-link-test.ts b/packages/realm-server/tests/stored-relationship-link-test.ts new file mode 100644 index 00000000000..ae6b59109aa --- /dev/null +++ b/packages/realm-server/tests/stored-relationship-link-test.ts @@ -0,0 +1,85 @@ +import QUnit from 'qunit'; +const { module, test } = QUnit; +import { basename } from 'path'; +import type { VirtualNetwork } from '@cardstack/runtime-common/virtual-network'; +import { storedRelationshipLink } from '@cardstack/runtime-common/file-serializer'; + +const realmURL = new URL('http://test/realm/'); +const relativeTo = new URL('http://test/realm/hassan.json'); +const ownPrefix = '@cardstack/realm/'; +const otherPrefix = '@cardstack/catalog/'; +const otherRealm = 'http://test/catalog/'; + +// A VirtualNetwork stand-in that maps one prefix onto the realm under test and +// another onto a second realm, so a scoped reference can point either inside +// or outside the writing realm — the distinction the helper exists to draw. +function makeStubNetwork(): VirtualNetwork { + let targets: [string, string][] = [ + [ownPrefix, realmURL.href], + [otherPrefix, otherRealm], + ]; + return { + isRegisteredPrefix(reference: string) { + return targets.some(([prefix]) => reference.startsWith(prefix)); + }, + resolveURL(reference: string, base: URL | string | undefined) { + for (let [prefix, target] of targets) { + if (reference.startsWith(prefix)) { + return new URL(reference.slice(prefix.length), target); + } + } + return new URL(reference, base ?? undefined); + }, + } as unknown as VirtualNetwork; +} + +function stored(selfLink: string): string { + return storedRelationshipLink( + selfLink, + relativeTo, + realmURL, + makeStubNetwork(), + ); +} + +module(basename(import.meta.filename), function () { + module('storedRelationshipLink', function () { + test('stores an in-realm URL relative', function (assert) { + assert.strictEqual(stored('http://test/realm/jade'), './jade'); + }); + + test('stores an in-realm scoped reference relative', function (assert) { + // Regression: passing a scoped reference straight to + // `maybeRelativeReference` returns it unchanged, because the pair is + // mixed RRI/URL. Resolving first is what makes this relative. + assert.strictEqual(stored(`${ownPrefix}jade`), './jade'); + }); + + test('stores a cross-realm scoped reference exactly as sent', function (assert) { + assert.strictEqual( + stored(`${otherPrefix}Pet/vangogh`), + `${otherPrefix}Pet/vangogh`, + ); + }); + + test('stores a cross-realm URL resolved', function (assert) { + assert.strictEqual( + stored('http://elsewhere/other/Pet/vangogh'), + 'http://elsewhere/other/Pet/vangogh', + ); + }); + + test('leaves a reference that will not resolve alone', function (assert) { + let network = { + isRegisteredPrefix: () => false, + resolveURL() { + throw new Error('nope'); + }, + } as unknown as VirtualNetwork; + assert.strictEqual( + storedRelationshipLink('::not a url::', relativeTo, realmURL, network), + '::not a url::', + ); + }); + }); +}); diff --git a/packages/runtime-common/file-serializer.ts b/packages/runtime-common/file-serializer.ts index 4bf04c54db7..9d1eabde734 100644 --- a/packages/runtime-common/file-serializer.ts +++ b/packages/runtime-common/file-serializer.ts @@ -21,7 +21,6 @@ import { isScopedReference, maybeRelativeReference as makeRelativeReference, } from './url.ts'; -import { rri } from './realm-identifiers.ts'; export default async function serialize({ doc, @@ -323,6 +322,45 @@ async function resolveChildDef( return await definitionLookup.lookupDefinition(codeRef); } +// What a relationship's `links.self` should be stored as. +// +// Inside the writing realm a link is stored relative, whichever form the +// client sent. Outside it, a scoped reference is stored exactly as sent and +// anything else is stored resolved. +// +// Whether a link is inside the realm is decided in URL space, so the link is +// resolved before the question is asked: `relativeTo` is always a URL here, +// and `relativeReference` refuses a mixed RRI/URL pair rather than resolve +// across forms — handing it a scoped reference returns that reference +// unchanged, which looks like "cannot be relativized" even for a link into +// this very realm. +// +// Storing a resolved URL for a cross-realm scoped link is the bug this exists +// to prevent: it bakes whatever URL the linked realm answers to in this +// environment into a realm that is version controlled. +export function storedRelationshipLink( + selfLink: string, + relativeTo: URL, + realmURL: URL, + virtualNetwork: VirtualNetwork, +): string { + let resolved: URL; + try { + resolved = virtualNetwork.resolveURL(selfLink, relativeTo); + } catch (e) { + // A reference that will not resolve is left exactly as it arrived. + return selfLink; + } + let relative = makeRelativeReference(resolved, relativeTo, realmURL); + if ( + relative === resolved.href && + isScopedReference(selfLink, virtualNetwork) + ) { + return selfLink; + } + return relative; +} + async function processRelationships({ relationships, definition, @@ -350,24 +388,12 @@ async function processRelationships({ if (processedValue.links.self !== null) { let selfLink = processedValue.links.self; if (realmURL && selfLink) { - try { - selfLink = makeRelativeReference( - // A scoped reference is passed through unresolved. - // `maybeRelativeReference` relativizes it when it points into - // the writing realm and otherwise preserves the form it was - // given, which is what keeps a cross-realm link canonical. - // Resolving first discards that form, so the fallback could only - // return a URL — whatever the linked realm answers to in this - // environment, baked into a version-controlled realm. - isScopedReference(selfLink, virtualNetwork) - ? rri(selfLink) - : virtualNetwork.resolveURL(selfLink, relativeTo), - relativeTo, - realmURL, - ); - } catch (e) { - // ignore malformed URLs and leave as-is - } + selfLink = storedRelationshipLink( + selfLink, + relativeTo, + realmURL, + virtualNetwork, + ); } processedValue.links = { self: selfLink, From 0a9bea755316daea60e56814ee1cfefb52f187d2 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 10 Sep 2026 10:59:14 -0400 Subject: [PATCH 5/5] Do not resolve a scoped reference whose prefix is unregistered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deciding realm membership by resolving first is wrong for a scoped reference this process cannot resolve. `resolveURL` treats an unregistered prefix as a relative reference and joins it against the base, so `@cardstack/catalog/Pet/vangogh` came back as a URL inside the writing realm and relativized to `./@cardstack/catalog/Pet/vangogh` — a path into a realm that has no such file. Such a reference is absolute and cross-realm by construction, so it now short-circuits and is stored exactly as sent. Resolution is reserved for references that can actually be resolved: a registered prefix, a URL, or a relative path. The unit tests missed this because the stub network registered every prefix it was asked about, while the realm-server harness registers only `@cardstack/base/` — the stub was more permissive than the thing it stood in for. A case now covers the unregistered scoped reference the write suite caught. --- .../tests/stored-relationship-link-test.ts | 11 +++++++++++ packages/runtime-common/file-serializer.ts | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/packages/realm-server/tests/stored-relationship-link-test.ts b/packages/realm-server/tests/stored-relationship-link-test.ts index ae6b59109aa..6bc6daf606b 100644 --- a/packages/realm-server/tests/stored-relationship-link-test.ts +++ b/packages/realm-server/tests/stored-relationship-link-test.ts @@ -62,6 +62,17 @@ module(basename(import.meta.filename), function () { ); }); + // The case the write suite caught and this stub originally hid: the stub + // registered every prefix it was asked about, while the realm-server + // harness registers only `@cardstack/base/`. Resolving an unregistered + // scoped reference joins it against the base as though it were relative. + test('stores an unregistered scoped reference exactly as sent', function (assert) { + assert.strictEqual( + stored('@nobody/knows-this/Pet/vangogh'), + '@nobody/knows-this/Pet/vangogh', + ); + }); + test('stores a cross-realm URL resolved', function (assert) { assert.strictEqual( stored('http://elsewhere/other/Pet/vangogh'), diff --git a/packages/runtime-common/file-serializer.ts b/packages/runtime-common/file-serializer.ts index 9d1eabde734..f73fabb5162 100644 --- a/packages/runtime-common/file-serializer.ts +++ b/packages/runtime-common/file-serializer.ts @@ -344,6 +344,18 @@ export function storedRelationshipLink( realmURL: URL, virtualNetwork: VirtualNetwork, ): string { + // A scoped reference whose prefix this process has not registered cannot be + // resolved, and asking anyway is worse than not asking: `resolveURL` treats + // it as a relative reference and joins it against the base, so + // `@scope/name/x` comes back as a URL *inside* the writing realm and then + // relativizes to `./@scope/name/x`. It is absolute and cross-realm by + // construction, so store it exactly as sent. + if ( + !virtualNetwork.isRegisteredPrefix(selfLink) && + selfLink.startsWith('@') + ) { + return selfLink; + } let resolved: URL; try { resolved = virtualNetwork.resolveURL(selfLink, relativeTo);