Emit CID service[] with lws:OpenIdProvider in WebID profiles (#320)#321
Conversation
Adds a single CID 1.0 service entry to generated WebID profiles, mirroring the existing `solid:oidcIssuer` predicate. LWS-aware verifiers can now establish trust against JSS-issued profiles by looking up `service[type=lws:OpenIdProvider].serviceEndpoint` per the LWS 1.0 OpenID Connect FPWD. Additive only — all existing WebID predicates remain unchanged, so Mashlib, solid-client-authn, rdflib.js, etc. see the same profile they see today. Storage/Inbox/verificationMethod are deferred until LWS normatively defines those types.
There was a problem hiding this comment.
Pull request overview
Adds a CID 1.0 service[] entry (typed lws:OpenIdProvider) to emitted WebID profile JSON-LD so LWS-aware verifiers can discover the issuer via serviceEndpoint, while keeping existing Solid predicates for backwards compatibility.
Changes:
- Extend WebID profile
@contextwithcid/lwsnamespaces andservice/serviceEndpointterms. - Emit a single
service[]entry pointing to the OIDC issuer, using a fragment ID on the profile document URL. - Add tests asserting presence and correctness of the new
service[]entry.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/webid/profile.js | Adds CID/LWS context terms and emits a CID service[] entry mirroring oidcIssuer. |
| test/webid.test.js | Adds coverage for the new CID service[] entry and its serviceEndpoint/id values. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 'id': `${docUrl}#oidc`, | ||
| 'type': 'lws:OpenIdProvider', |
| it('lws:OpenIdProvider service.serviceEndpoint mirrors oidcIssuer', async () => { | ||
| const res = await request(profilePath); | ||
| const jsonLd = await res.json(); | ||
| const oidc = jsonLd.service.find((s) => s.type === 'lws:OpenIdProvider'); | ||
| assert.strictEqual( | ||
| oidc.serviceEndpoint, | ||
| jsonLd.oidcIssuer, | ||
| 'serviceEndpoint must equal the existing oidcIssuer value' | ||
| ); |
| it('lws:OpenIdProvider service.id is a fragment on the profile document', async () => { | ||
| const res = await request(profilePath); | ||
| const jsonLd = await res.json(); | ||
| const oidc = jsonLd.service.find((s) => s.type === 'lws:OpenIdProvider'); | ||
| const docUrl = jsonLd['@id'].split('#')[0]; | ||
| assert.strictEqual(oidc.id, `${docUrl}#oidc`, | ||
| 'service entry id should be `<profile-doc>#oidc`'); |
- Alias `id`/`type` to @id/@type in @context so nested service[] entries are interpreted as real JSON-LD node identifiers/types rather than plain properties. CID 1.0 and DID contexts do the same; LWS verifiers that expand JSON-LD would otherwise fail to read these entries. - Tighten the two service-entry tests with explicit assertions on `jsonLd.service` being an array and `oidc` being truthy, so missing data produces clean assertion failures instead of TypeError.
There was a problem hiding this comment.
Pull request overview
This PR updates generated WebID profile JSON-LD to additionally emit a CID 1.0 service[] entry of type lws:OpenIdProvider, mirroring the existing solid:oidcIssuer, to align JSS profiles with the LWS 1.0 OpenID Connect discovery expectations.
Changes:
- Extend the profile JSON-LD
@contextwithcid/lwsnamespaces and CID service terms. - Emit a
servicearray containing anlws:OpenIdProviderservice object whoseserviceEndpointmirrorsoidcIssuer. - Add tests asserting the
service[]entry exists and is shaped as expected.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/webid/profile.js |
Adds CID/LWS context terms and emits the new service[] entry in generated profiles. |
test/webid.test.js |
Adds assertions for presence/shape of the new CID service[] entry. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 'cid': CID, | ||
| 'lws': LWS, | ||
| // Alias `id` → @id and `type` → @type so nested nodes (like each | ||
| // `service[]` entry) are interpreted correctly by JSON-LD processors. | ||
| // CID 1.0 and DID contexts do the same. | ||
| 'id': '@id', | ||
| 'type': '@type', | ||
| 'inbox': { '@id': 'ldp:inbox', '@type': '@id' }, | ||
| 'storage': { '@id': 'pim:storage', '@type': '@id' }, | ||
| 'oidcIssuer': { '@id': 'solid:oidcIssuer', '@type': '@id' }, | ||
| 'preferencesFile': { '@id': 'pim:preferencesFile', '@type': '@id' }, | ||
| 'publicTypeIndex': { '@id': 'solid:publicTypeIndex', '@type': '@id' }, | ||
| 'privateTypeIndex': { '@id': 'solid:privateTypeIndex', '@type': '@id' }, | ||
| 'isPrimaryTopicOf': { '@id': 'foaf:isPrimaryTopicOf', '@type': '@id' }, | ||
| 'mainEntityOfPage': { '@id': 'schema:mainEntityOfPage', '@type': '@id' } | ||
| 'mainEntityOfPage': { '@id': 'schema:mainEntityOfPage', '@type': '@id' }, | ||
| 'service': { '@id': 'cid:service', '@container': '@set' }, | ||
| 'serviceEndpoint': { '@id': 'cid:serviceEndpoint', '@type': '@id' } |
| it('should emit a CID service[] with an lws:OpenIdProvider entry', async () => { | ||
| const res = await request(profilePath); | ||
| const jsonLd = await res.json(); | ||
| assert.ok(Array.isArray(jsonLd.service), 'profile should have a service array'); | ||
| const oidc = jsonLd.service.find((s) => s.type === 'lws:OpenIdProvider'); | ||
| assert.ok(oidc, 'service[] must include an lws:OpenIdProvider entry'); | ||
| }); | ||
|
|
||
| it('lws:OpenIdProvider service.serviceEndpoint mirrors oidcIssuer', async () => { | ||
| const res = await request(profilePath); | ||
| const jsonLd = await res.json(); | ||
| assert.ok(Array.isArray(jsonLd.service), 'profile should have a service array'); | ||
| const oidc = jsonLd.service.find((s) => s.type === 'lws:OpenIdProvider'); | ||
| assert.ok(oidc, 'service[] must include an lws:OpenIdProvider entry'); | ||
| assert.strictEqual( | ||
| oidc.serviceEndpoint, | ||
| jsonLd.oidcIssuer, | ||
| 'serviceEndpoint must equal the existing oidcIssuer value' | ||
| ); | ||
| }); | ||
|
|
||
| it('lws:OpenIdProvider service.id is a fragment on the profile document', async () => { | ||
| const res = await request(profilePath); | ||
| const jsonLd = await res.json(); | ||
| assert.ok(Array.isArray(jsonLd.service), 'profile should have a service array'); | ||
| const oidc = jsonLd.service.find((s) => s.type === 'lws:OpenIdProvider'); | ||
| assert.ok(oidc, 'service[] must include an lws:OpenIdProvider entry'); | ||
| const docUrl = jsonLd['@id'].split('#')[0]; | ||
| assert.strictEqual(oidc.id, `${docUrl}#oidc`, | ||
| 'service entry id should be `<profile-doc>#oidc`'); | ||
| }); |
| 'service': [ | ||
| { | ||
| 'id': `${docUrl}#oidc`, | ||
| 'type': 'lws:OpenIdProvider', | ||
| 'serviceEndpoint': issuer | ||
| } | ||
| ] |
Copilot spotted that the JSON-LD → Turtle converter dropped nested node objects silently: property values that were objects with their own @id/@type/properties (like a CID service[] entry) were collapsed to a bare URI reference, losing the entry's type and serviceEndpoint. The Turtle variant of a JSS profile therefore had no usable CID service data — LWS verifiers asking for Turtle would see only dangling refs. Three fixes: - src/rdf/turtle.js — the main conversion loop now does a BFS over nodes, discovering nested node objects and emitting their triples as their own subjects. - src/rdf/turtle.js — expandUri recursively expands context values that are themselves CURIEs (e.g. `cid:service` in a context term's @id), so prefixed identifiers resolve to full URIs instead of leaking as `<cid:service>` in the output. - src/webid/profile.js — service entries use @id/@type directly and the id/type aliases added in the previous commit are removed. Direct JSON-LD keywords interoperate with the converter cleanly and match the custom-context style used elsewhere in this file. Test plan: - New Turtle-conneg regression test asserts the service predicate, the lws:OpenIdProvider type, the cid:serviceEndpoint predicate, and that the service entry appears as a subject (first-class node) in Turtle. - Existing webid tests updated to check s['@type'] / s['@id'] on the nested service objects rather than the aliased keys. - Full suite green (444/444).
There was a problem hiding this comment.
Pull request overview
Adds LWS/CID-compatible service[] metadata to generated WebID profiles and updates JSON-LD→Turtle conversion so nested service nodes are preserved when conneg is enabled.
Changes:
- Emit a CID
service[]entry of typelws:OpenIdProviderin generated WebID profile JSON-LD (mirroringsolid:oidcIssuer). - Extend JSON-LD→Turtle conversion to traverse and emit nested node objects (e.g., the
service[]entry) and improve context term expansion. - Add JSON-LD and Turtle conneg tests asserting the CID/LWS service triples are present.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/webid/profile.js |
Adds CID/LWS prefixes and emits a CID service[] entry for OIDC trust discovery. |
src/rdf/turtle.js |
Updates JSON-LD→quads conversion to include nested nodes and expands context CURIE mappings. |
test/webid.test.js |
Adds assertions for CID service[] in JSON-LD and ensures Turtle conneg output retains nested service triples. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| while (queue.length) { | ||
| const node = queue.shift(); |
| const subjectUri = resolveUri(node['@id'], baseUri); | ||
| if (visited.has(subjectUri)) continue; | ||
| visited.add(subjectUri); |
| // Check if it's a term in context. A context value can itself be a | ||
| // CURIE (`cid:service`) that still needs prefix expansion, so recurse. | ||
| if (context[uri]) { | ||
| const expansion = context[uri]; | ||
| if (typeof expansion === 'string') { | ||
| return expansion; | ||
| return expandUri(expansion, context); | ||
| } | ||
| if (expansion['@id']) { | ||
| return expansion['@id']; | ||
| return expandUri(expansion['@id'], context); | ||
| } |
Three fixes to the JSON-LD → Turtle converter, all prompted by Copilot review on #321: - Replace queue.shift() (O(n) per step) with index-based iteration so the BFS is linear in the number of nodes, not quadratic. - Drop the per-subject visited-set that was silently suppressing a second top-level document with the same @id. Duplicate RDF triples are harmless; losing data is not. Cycle protection now happens at the enqueue step via a WeakSet on object identity, which prevents the same nested object from being enqueued twice but still lets distinct top-level docs for the same subject both emit. - Add cycle protection to expandUri. Without it a user-supplied context carrying a term-level loop (a → b → a) could cause unbounded recursion and a stack overflow on the server — a remote DoS once conneg is enabled on a pod. New test/turtle.test.js covers: - Cyclic context (a → b → a) does not hang - Self-loop context (a → a) does not hang - Duplicate top-level @id does not drop data - Cyclical nested node graph does not hang Full suite: 448/448.
There was a problem hiding this comment.
Pull request overview
Adds LWS/CID-aligned service[] metadata to generated WebID profiles and ensures Turtle conneg preserves nested service node triples, supporting LWS-aware issuer trust checks.
Changes:
- Emit CID/LWS namespaces and a CID
service[]entry (lws:OpenIdProvider) in generated WebID profiles. - Update JSON-LD → Turtle conversion to BFS-emit nested node objects (and add cycle-safety in URI expansion).
- Add integration + unit tests covering the new profile shape and Turtle conversion regressions.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/webid/profile.js | Adds CID/LWS context terms and emits the new service[] entry in generated profiles. |
| src/rdf/turtle.js | Ensures nested JSON-LD nodes (like service[] entries) are emitted as first-class Turtle subjects; adds cycle guard in context expansion. |
| test/webid.test.js | Adds assertions for the new service[] entry and verifies it survives Turtle content negotiation. |
| test/turtle.test.js | Adds unit tests for converter regressions (cycle safety, duplicate @id handling, cyclical nested references). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 'service': [ | ||
| { | ||
| '@id': `${docUrl}#oidc`, | ||
| '@type': 'lws:OpenIdProvider', | ||
| 'serviceEndpoint': issuer | ||
| } |
There was a problem hiding this comment.
Good catch — my issue-body example was inconsistent with the code. I've updated #320's Before/After to use @id/@type (matching what this PR actually emits) and added a short rationale:
Nested service entries use the JSON-LD keywords
@idand@typedirectly. CID 1.0's own context URL (https://www.w3.org/ns/cid/v1) providesid/typealiases for authors who prefer the shorter form, but since this profile uses inline term definitions rather than a context URL, we stick with the keywords — they're JSON-LD-native and don't depend on an out-of-band context fetch. A verifier that runs a JSON-LD processor (the expected behaviour for CID-conformant tooling) expands both forms identically.
On the naive-plain-JSON-lookup concern specifically: a verifier that reads this doc as plain JSON would also see "@type": "lws:OpenIdProvider" — which is a CURIE that only expands to https://www.w3.org/ns/lws#OpenIdProvider via the context. So any verifier that doesn't run JSON-LD expansion is already broken regardless of id vs @id. Running a JSON-LD processor is the minimum bar for reading CID-conformant documents, and once that's in play both key shapes are equivalent.
Could you re-review now that the PR description / issue examples match the code?
There was a problem hiding this comment.
Pull request overview
Adds LWS/CID-aligned service[] data to generated WebID profiles and ensures the JSON-LD→Turtle conversion preserves nested service node triples (needed for LWS verifiers requesting Turtle).
Changes:
- Emit a CID
service[]entry withlws:OpenIdProviderin generated WebID profile JSON-LD. - Update JSON-LD→Turtle conversion to BFS-traverse nested node objects and guard against cyclic context expansion.
- Add regression tests for the new WebID shape and Turtle conversion behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/webid/profile.js | Adds CID/LWS context terms and emits the service[] entry referencing the issuer. |
| src/rdf/turtle.js | Preserves nested nodes during conversion and adds cycle-safety for context expansion. |
| test/webid.test.js | Adds assertions for service[] and verifies Turtle conneg retains nested service triples. |
| test/turtle.test.js | Adds direct unit tests for converter regressions (cycles, duplicate @id, cyclical nested refs). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (uri.includes(':')) { | ||
| const [prefix, local] = uri.split(':', 2); | ||
| const ns = context[prefix] || COMMON_PREFIXES[prefix]; | ||
| if (ns) { | ||
| return ns + local; | ||
| } |
There was a problem hiding this comment.
Pull request overview
Adds LWS/CID-compatible OpenID Provider service discovery data to generated WebID profiles, and ensures RDF conneg (JSON-LD → Turtle) preserves nested CID service nodes needed by LWS verifiers.
Changes:
- Emit a CID
service[]entry (typedlws:OpenIdProvider) in generated WebID profile JSON-LD, mirroringsolid:oidcIssuer. - Update the JSON-LD → Turtle converter to BFS-traverse and emit nested node objects (and harden
expandUriagainst cyclic contexts). - Add regression/unit tests for the new profile shape and for Turtle conversion edge cases.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| test/webid.test.js | Adds assertions for emitted CID service[] and verifies Turtle conneg retains nested service-node triples. |
| test/turtle.test.js | Adds unit tests covering Turtle conversion regressions (nested nodes, cycles, duplicate @id, context pitfalls). |
| src/webid/profile.js | Adds CID/LWS context terms and emits a single service[] entry for LWS OpenID Provider discovery. |
| src/rdf/turtle.js | Ensures nested node objects are emitted as subjects in Turtle output and hardens URI expansion against cyclic contexts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…iptSolidServer#325) Two related conneg bugs filed in JavaScriptSolidServer#325: 1. Q-weights ignored in the dispatch. src/handlers/resource.js used naive `acceptHeader.includes('text/turtle')` substring matching to decide between JSON-LD and Turtle. An Accept header of `application/ld+json, text/turtle;q=0.1` returned Turtle even though JSON-LD was the higher-q preference. Replaced five substring sites (handleGet container-with-index, container listing, resource HTML data island; handleHead container) with the existing q-aware `selectContentType` from src/rdf/conneg.js. 2. HEAD/GET divergence on containers without index.html. handleHead hard-coded `application/ld+json` for that case regardless of Accept, so HEAD reported a content-type that didn't match what GET would emit. The mismatch confused tooling that read HEAD content-type and then GET'd the body. handleHead now runs the same q-aware Accept logic so HEAD content-type tracks GET. The auth-vs-anonymous mismatch in JavaScriptSolidServer#325 doesn't reproduce on the current tip — likely fixed by the Turtle-converter work in JavaScriptSolidServer#321. Both anon and auth paths now return identical content-type for the same Accept. Tests: new conneg.test.js block exercises q-weight respect, HEAD/GET parity across four Accept shapes, and auth-vs-anon parity. 470/470.
Summary
Adds a single CID 1.0
service[]entry to generated WebID profiles, mirroring the existingsolid:oidcIssuerpredicate. This lets LWS-aware verifiers establish trust against JSS-issued profiles per the LWS 1.0 OpenID Connect FPWD, which prescribes looking upservice[type=lws:OpenIdProvider].serviceEndpointto confirm the token'sissclaim.What changes
src/webid/profile.js— four small additions togenerateProfileJsonLd:cid→https://www.w3.org/ns/cid/v1#,lws→https://www.w3.org/ns/lws#.service→cid:service,serviceEndpoint→cid:serviceEndpoint.service[]entry in the emitted profile:{ id: '<doc>#oidc', type: 'lws:OpenIdProvider', serviceEndpoint: issuer }.#mefragment.Properties
foaf:name,solid:oidcIssuer,pim:storage,ldp:inbox, etc.) remain unchanged — Mashlib, solid-client-authn, rdflib.js see the same profile as before.typethat is normatively defined in the four LWS10 FPWDs (lws:OpenIdProvider). Storage, Inbox, andverificationMethod[]are explicitly out of scope — tracked for later once LWS defines those types or a concrete deployment asks.Test plan
test/webid.test.js:service[]array exists with anlws:OpenIdProviderentryservice.serviceEndpointmirrors the existingoidcIssuervalueservice.idis a fragment on the profile document URLPart of #319 (LWS 1.0 Auth Suite alignment). Fixes #320.