From 55475ba28fbeaad442ec6b349fcb3404faffbd9a Mon Sep 17 00:00:00 2001 From: Melvin Carvalho Date: Sat, 9 May 2026 06:12:25 +0200 Subject: [PATCH 1/2] profile: emit CID v1 controller + prep verificationMethod context (#387, #386 Phase A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Smallest delta toward LWS-CID conformance for new pod profiles. New pods now declare themselves as W3C Controlled Identifier documents so a future Phase B "add-keys" app (a standalone tool that PATCHes the profile after authentication) can drop verificationMethod entries in without rewriting the @context. Two changes in src/webid/profile.js: 1. Add `controller: ` triple to every new profile. Per CID v1 self-control: the WebID is its own controller. The triple itself is meaningful even with no keys yet. 2. Extend the @context with the six CID v1 terms a key-emitting profile will need (controller, verificationMethod, authentication, assertionMethod, publicKeyJwk, publicKeyMultibase). Declared inline (rather than via the cid/v1 imported-context URL) because JSS's JSON-LD → Turtle conneg layer can't resolve external context URLs; the IRIs each term expands to are identical either way. Explicitly out of scope for Phase A: - No verificationMethod data (the standalone Phase B app populates these per-key) - No migration of existing pods (they keep their current profiles until a user opts in via Phase B) - No verifier-side changes (#386 Phase 3, separate) - No new dependencies Tests: two new assertions in test/webid.test.js — `controller` is defined in @context and expands to the CID v1 namespace, and controller === @id at the data layer. Existing profile / Turtle conneg / OIDC discovery / Solid client tests continue to pass. 619/619 tests pass. --- src/webid/profile.js | 23 ++++++++++++++++++++++- test/webid.test.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/webid/profile.js b/src/webid/profile.js index 0d987e0f..bbfe2644 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,28 @@ 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. + 'controller': { '@id': 'cid:controller', '@type': '@id' }, + 'verificationMethod': { '@id': 'cid:verificationMethod', '@type': '@id' }, + 'authentication': { '@id': 'cid:authentication', '@type': '@id' }, + 'assertionMethod': { '@id': 'cid:assertionMethod', '@type': '@id' }, + 'publicKeyJwk': { '@id': 'cid:publicKeyJwk' }, + '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..b2f432d9 100644 --- a/test/webid.test.js +++ b/test/webid.test.js @@ -47,6 +47,37 @@ 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 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'); + // Both 'controller' and 'verificationMethod' must expand to the + // CID v1 namespace. Inline form: '@id': 'cid:controller' or + // '@id': 'https://www.w3.org/ns/cid/v1#controller'. + const controllerMapping = ctx.controller; + assert.ok(controllerMapping, '@context must define `controller`'); + const id = typeof controllerMapping === 'string' ? controllerMapping : controllerMapping['@id']; + assert.match(id, /^(cid:controller|https:\/\/www\.w3\.org\/ns\/cid\/v1#controller)$/, + 'controller must map to the CID v1 namespace'); + assert.ok(ctx.verificationMethod, '@context must define `verificationMethod` for Phase B'); + assert.ok(ctx.authentication, '@context must define `authentication` for Phase B'); + }); + + 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(); From 5c9ba9342f8ba0bb1b7a7a75069b70738cf1e214 Mon Sep 17 00:00:00 2001 From: Melvin Carvalho Date: Sat, 9 May 2026 06:19:55 +0200 Subject: [PATCH 2/2] =?UTF-8?q?profile:=20address=20Copilot=20review=20on?= =?UTF-8?q?=20#388=20=E2=80=94=20fix=20CID=20context=20typing=20for=20Phas?= =?UTF-8?q?e=20B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes per the review: 1. verificationMethod no longer marked @type:@id. Its values are inline verification method objects (id/type/controller/public-key…), not IRI references, so @type:@id was semantically wrong. Added @container:@set so a single-entry list is still an array. 2. authentication / assertionMethod KEEP @type:@id (they reference verificationMethod entries by IRI) but get @container:@set added too for proper array semantics. 3. publicKeyJwk now declares @type:@json. JWK is a literal JSON object (rdf:JSON datatype per JSON-LD 1.1). JSS's Turtle conneg layer doesn't yet emit @type:@json literals — filed #390 as a separate Phase B blocker. Declaring it here is the spec-correct shape. Test strengthened to cover all six CID terms mapping to the CID v1 namespace, plus the container/type flags Phase B will rely on: - verificationMethod NOT @type:@id, has @container:@set - authentication / assertionMethod have @type:@id - publicKeyJwk has @type:@json 17/17 webid tests pass (control: full Turtle conneg #320 still green). --- src/webid/profile.js | 19 +++++++++++++++---- test/webid.test.js | 38 +++++++++++++++++++++++++++----------- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/webid/profile.js b/src/webid/profile.js index bbfe2644..f2cefbd2 100644 --- a/src/webid/profile.js +++ b/src/webid/profile.js @@ -59,11 +59,22 @@ export function generateProfileJsonLd({ webId, name, podUri, issuer }) { // 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', '@type': '@id' }, - 'authentication': { '@id': 'cid:authentication', '@type': '@id' }, - 'assertionMethod': { '@id': 'cid:assertionMethod', '@type': '@id' }, - 'publicKeyJwk': { '@id': 'cid:publicKeyJwk' }, + '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, diff --git a/test/webid.test.js b/test/webid.test.js index b2f432d9..0af09707 100644 --- a/test/webid.test.js +++ b/test/webid.test.js @@ -54,21 +54,37 @@ describe('WebID Profile', () => { // 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 CID v1 terms in @context (#386 Phase A)', async () => { + 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'); - // Both 'controller' and 'verificationMethod' must expand to the - // CID v1 namespace. Inline form: '@id': 'cid:controller' or - // '@id': 'https://www.w3.org/ns/cid/v1#controller'. - const controllerMapping = ctx.controller; - assert.ok(controllerMapping, '@context must define `controller`'); - const id = typeof controllerMapping === 'string' ? controllerMapping : controllerMapping['@id']; - assert.match(id, /^(cid:controller|https:\/\/www\.w3\.org\/ns\/cid\/v1#controller)$/, - 'controller must map to the CID v1 namespace'); - assert.ok(ctx.verificationMethod, '@context must define `verificationMethod` for Phase B'); - assert.ok(ctx.authentication, '@context must define `authentication` for Phase B'); + + // 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 () => {