diff --git a/src/webid/profile.js b/src/webid/profile.js index f2cefbd2..2cecebe6 100644 --- a/src/webid/profile.js +++ b/src/webid/profile.js @@ -75,7 +75,19 @@ export function generateProfileJsonLd({ webId, name, podUri, issuer }) { 'authentication': { '@id': 'cid:authentication', '@type': '@id', '@container': '@set' }, 'assertionMethod': { '@id': 'cid:assertionMethod', '@type': '@id', '@container': '@set' }, 'publicKeyJwk': { '@id': 'cid:publicKeyJwk', '@type': '@json' }, - 'publicKeyMultibase': { '@id': 'cid:publicKeyMultibase' } + 'publicKeyMultibase': { '@id': 'cid:publicKeyMultibase' }, + // CID v1 verificationMethod *class* names (#417). Without these + // term mappings, an app PATCHing in a VM with `type: "Multikey"` + // (the spec-example shape) emits a bare relative-IRI `` + // when JSS conneg-converts the profile to Turtle — which then + // resolves against the document's base URL to a fictional class + // like `/profile/Multikey`. Mapping the class names here + // means the bare term `"Multikey"` in JSON-LD expands correctly + // (cid:Multikey → https://www.w3.org/ns/cid/v1#Multikey) for both + // JSON-LD processors AND our Turtle conneg layer. Naive JSON + // readers comparing `type === "Multikey"` continue to work. + 'Multikey': 'cid:Multikey', + 'JsonWebKey': 'cid:JsonWebKey' }, '@id': webId, '@type': ['foaf:Person', 'schema:Person'], diff --git a/test/webid.test.js b/test/webid.test.js index 0af09707..1fa6d434 100644 --- a/test/webid.test.js +++ b/test/webid.test.js @@ -87,6 +87,30 @@ describe('WebID Profile', () => { assert.strictEqual(ctx.publicKeyJwk['@type'], '@json'); }); + it('declares CID v1 class names (Multikey, JsonWebKey) as flat aliases (#417)', async () => { + // Without these mappings, an app PATCHing in a VM with the + // spec-example shape `{type: "Multikey", ...}` produces a bare + // relative-IRI `` in the Turtle conneg output, which + // resolves to a fictional class on the pod's own host (e.g. + // `/profile/Multikey` instead of `cid:Multikey`). + // + // The flat-alias shape (`"Multikey": "cid:Multikey"`) makes + // bare-term emission work correctly through both JSON-LD + // expansion AND our Turtle conneg layer — and matches the + // "JSON-LD with flat context aliases" pattern consumers like + // LOSOS / LION rely on. + const res = await request(profilePath); + const jsonLd = await res.json(); + const ctx = jsonLd['@context']; + for (const cls of ['Multikey', 'JsonWebKey']) { + const mapping = ctx[cls]; + assert.ok(mapping, `@context must define class alias \`${cls}\``); + const id = typeof mapping === 'string' ? mapping : mapping['@id']; + assert.match(id, new RegExp(`^(cid:${cls}|https://www\\.w3\\.org/ns/cid/v1#${cls})$`), + `${cls} must map to the CID v1 namespace`); + } + }); + it('declares self-control via controller === @id (#386 Phase A)', async () => { const res = await request(profilePath); const jsonLd = await res.json(); @@ -216,6 +240,59 @@ describe('WebID Profile — Turtle conneg (#320)', () => { await stopTestServer(); }); + it('Turtle conneg: generated profile @context expands bare Multikey/JsonWebKey terms (#417)', async () => { + // Combine the production profile generator's @context with a + // synthetic VM (the spec-example shape `{type: "Multikey", ...}`) + // and run it through the same conneg path the live profile + // would. Asserts: the bare-term type expands to the CID v1 + // namespace, not to a relative IRI that resolves to a fake + // class on the pod's host. + const { generateProfile } = await import('../src/webid/profile.js'); + const { fromJsonLd } = await import('../src/rdf/conneg.js'); + const webId = 'https://example.test/profile/card.jsonld#me'; + const profile = generateProfile({ + webId, + name: 'mk-test', + podUri: 'https://example.test/', + issuer: 'https://example.test/', + }); + // Inject a Multikey VM authored with the spec-example bare-term + // type — this is the shape the bug surfaces on. + const vmId = webId.replace('#me', '#nostr-key-1'); + profile.verificationMethod = [{ + id: vmId, + type: 'Multikey', + controller: webId, + publicKeyMultibase: 'fe70102de7ec0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab', + }]; + profile.authentication = [vmId]; + + const { content: ttl } = await fromJsonLd(profile, 'text/turtle', 'https://example.test/', true); + + // Pre-#417: would emit `a ` (relative — resolves to + // `https://example.test/profile/Multikey`). + // After #417: the @context maps `Multikey -> cid:Multikey`, so + // the converter expands the bare term to the CID v1 IRI. + assert.ok( + ttl.includes('cid:Multikey') || ttl.includes('cid/v1#Multikey'), + `Turtle must emit a CID-namespaced Multikey class, got:\n${ttl}`, + ); + assert.ok( + !/\ba\s+\s*[;.]/.test(ttl), + `Turtle must NOT emit bare (resolves to fictional class), got:\n${ttl}`, + ); + // Don't regress #416: the VM block + publicKeyMultibase must + // still survive the conversion. + assert.ok( + ttl.includes('publicKeyMultibase') || ttl.includes('cid/v1#publicKeyMultibase'), + `Turtle must include cid:publicKeyMultibase, got:\n${ttl}`, + ); + assert.ok( + ttl.includes('fe70102de7ec'), + `Turtle must include the publicKeyMultibase value, got:\n${ttl}`, + ); + }); + it('Turtle variant includes cid:service with lws:OpenIdProvider and serviceEndpoint', async () => { const res = await request('/webidturtletest/profile/card.jsonld', { headers: { Accept: 'text/turtle' }