diff --git a/src/webid/profile.js b/src/webid/profile.js index 0d987e0f..f2cefbd2 100644 --- a/src/webid/profile.js +++ b/src/webid/profile.js @@ -31,6 +31,12 @@ export function generateProfileJsonLd({ webId, name, podUri, issuer }) { const docUrl = webId.split('#')[0]; return { + // CID v1 vocabulary is declared inline (rather than via an imported + // context URL) so JSS's JSON-LD → Turtle conneg layer can expand + // every term without fetching external contexts. Semantically + // equivalent to importing https://www.w3.org/ns/cid/v1: the IRIs + // each term expands to are the same. This keeps the profile a valid + // W3C Controlled Identifier document per LWS 1.0 (#386 Phase A). '@context': { 'foaf': FOAF, 'solid': SOLID, @@ -48,13 +54,39 @@ export function generateProfileJsonLd({ webId, name, podUri, issuer }) { 'isPrimaryTopicOf': { '@id': 'foaf:isPrimaryTopicOf', '@type': '@id' }, 'mainEntityOfPage': { '@id': 'schema:mainEntityOfPage', '@type': '@id' }, 'service': { '@id': 'cid:service', '@container': '@set' }, - 'serviceEndpoint': { '@id': 'cid:serviceEndpoint', '@type': '@id' } + 'serviceEndpoint': { '@id': 'cid:serviceEndpoint', '@type': '@id' }, + // CID v1 terms used by Phase A and prepped for Phase B (the + // standalone "add my keys" app). Declaring these now means the + // app can PATCH in verificationMethod entries without having to + // also rewrite the @context. + // + // verificationMethod: NO @type:@id — values are inline verification + // method *objects* (id/type/controller/publicKey…), not just IRI + // references. @container:@set so a single entry stays an array. + // authentication / assertionMethod: @type:@id — values reference a + // verificationMethod entry by its IRI. @container:@set for arrays. + // publicKeyJwk: @type:@json so the JWK object round-trips as a + // literal JSON value (rdf:JSON datatype). Note: JSS's Turtle + // conneg layer doesn't yet emit @type:@json literals (tracked as + // a Phase B blocker in the PR description); declaring here is + // forward-looking and spec-correct. + 'controller': { '@id': 'cid:controller', '@type': '@id' }, + 'verificationMethod': { '@id': 'cid:verificationMethod', '@container': '@set' }, + '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' } }, '@id': webId, '@type': ['foaf:Person', 'schema:Person'], 'foaf:name': name, 'isPrimaryTopicOf': '', 'mainEntityOfPage': '', + // CID v1 self-control: the WebID is its own controller. Phase A of + // #386 ships this triple even with no verificationMethods yet, so a + // future Phase B "add-keys" app PATCHing in verificationMethod + // entries doesn't have to also wire up controllership separately. + 'controller': webId, 'inbox': `${pod}inbox/`, 'storage': pod, 'oidcIssuer': issuer, diff --git a/test/webid.test.js b/test/webid.test.js index 4d36233e..0af09707 100644 --- a/test/webid.test.js +++ b/test/webid.test.js @@ -47,6 +47,53 @@ describe('WebID Profile', () => { assert.ok(jsonLd['@id'], 'Should have @id'); }); + // LWS-CID document conformance, Phase A of #386. The profile must be + // structurally a W3C Controlled Identifier document so a future + // PATCH-in-keys app (or server migration) can drop verificationMethod + // entries in without further plumbing. CID v1 vocabulary is declared + // inline rather than via context URL so JSS's conneg layer can + // expand every term without fetching external contexts — the IRIs + // are the same either way. + it('declares all six CID v1 terms in @context (#386 Phase A)', async () => { + const res = await request(profilePath); + const jsonLd = await res.json(); + const ctx = jsonLd['@context']; + assert.ok(ctx, '@context required'); + + // All six CID terms must be declared and expand to the CID v1 + // namespace. Accept either prefixed (cid:term) or full-URI + // (https://www.w3.org/ns/cid/v1#term) form. + const cidTerms = ['controller', 'verificationMethod', 'authentication', 'assertionMethod', 'publicKeyJwk', 'publicKeyMultibase']; + for (const term of cidTerms) { + const mapping = ctx[term]; + assert.ok(mapping, `@context must define \`${term}\``); + const id = typeof mapping === 'string' ? mapping : mapping['@id']; + assert.match(id, new RegExp(`^(cid:${term}|https://www\\.w3\\.org/ns/cid/v1#${term})$`), + `${term} must map to the CID v1 namespace`); + } + + // Container/type flags Phase B relies on: + // verificationMethod values are inline objects, NOT IRIs — must + // NOT have @type:@id (would force string-only) and SHOULD have + // @container:@set so a single entry is still an array. + assert.notStrictEqual(ctx.verificationMethod['@type'], '@id', + 'verificationMethod values are objects, not IRIs'); + assert.strictEqual(ctx.verificationMethod['@container'], '@set'); + // authentication / assertionMethod reference verificationMethod + // entries by IRI, so @type:@id is correct. + assert.strictEqual(ctx.authentication['@type'], '@id'); + assert.strictEqual(ctx.assertionMethod['@type'], '@id'); + // JWK is a literal JSON value (rdf:JSON datatype) per JSON-LD 1.1. + assert.strictEqual(ctx.publicKeyJwk['@type'], '@json'); + }); + + it('declares self-control via controller === @id (#386 Phase A)', async () => { + const res = await request(profilePath); + const jsonLd = await res.json(); + assert.strictEqual(jsonLd.controller, jsonLd['@id'], + 'profile must declare itself as its own controller per CID v1'); + }); + it('should have correct WebID URI', async () => { const res = await request(profilePath); const jsonLd = await res.json();