rdf/turtle: accept JSON-LD id/type aliases on nested objects (#415)#416
Merged
Conversation
Solid profiles in the wild use the JSON-LD 1.1 standard alias
form for nested resources — `id` and `type`, not `@id`/`@type`.
The converter only checked for the explicit `@`-prefixed names,
so a nested CID v1 verificationMethod authored as:
{
"id": ".../card.jsonld#nostr-key-1",
"type": "Multikey",
"controller": ".../card.jsonld#me",
"publicKeyMultibase": "fe70102de7ec..."
}
was silently dropped from the Turtle output. The two breakage
points:
- `valueToTerm()` checks `value['@id']`; for `{id:...}` it
returns null, and the parent `cid:verificationMethod` quad
is never emitted.
- The BFS enqueue at the bottom of `jsonLdToQuads` tests
`v['@id']`, so the nested object's own triples (its type,
controller, publicKeyMultibase) are never emitted either.
Net effect on test.solid.social's profile: the JSON-LD form had
the full Multikey VM, the conneg-converted Turtle dropped both
the `cid:verificationMethod` predicate AND the entire
`#nostr-key-1` resource block. JSS's own backend is unaffected
(LWS10-CID verifier requests JSON-LD), but third-party Turtle
consumers — RDF stores, SPARQL endpoints, generic Solid clients
that default to text/turtle — saw `cid:authentication
<#nostr-key-1>` with zero triples about `#nostr-key-1`.
Fix:
- Two helper functions: `getNodeId(n)` returns `n['@id'] ??
n.id`, `getNodeType(n)` returns `n['@type'] ?? n.type`.
Used wherever the converter consults the identifier or
type of a node.
- `jsonLdToQuads` accepts `id`/`type` for top-level docs,
nested-object enqueue, and emits the rdf:type triple from
either form.
- `valueToTerm` accepts `id` as the IRI-reference key (so
the predicate-→-IRI quad gets emitted for nested objects
using the alias).
- Predicate loop now also skips `id`/`type` keys (they were
handled above as @id/@type aliases — emitting them as
predicates would produce malformed triples like `<id>` /
`<type>` since the names don't expand via context).
- Nested-claim probe in the enqueue check ignores both
`@id` and `id` when deciding whether the object has any
own claims worth emitting.
Sibling of #389 (array-context lost) and #390 (`@type: '@JSON'`
literals not emitted) — same family of CID-v1 conversion gaps.
New test: writes a profile with the exact pattern test.solid.social
ships (nested VM with `id`/`type` aliases) and asserts the
Turtle output includes `cid:verificationMethod`, the Multikey
type, the controller predicate, and the publicKeyMultibase
literal.
Test count: 770 → 771 in full suite.
There was a problem hiding this comment.
Pull request overview
This PR fixes the JSON-LD → Turtle conversion in src/rdf/turtle.js so that nested JSON-LD nodes using the JSON-LD 1.1 id/type aliases (instead of @id/@type) are no longer silently dropped from Turtle output (e.g., CID v1 verificationMethod entries).
Changes:
- Add
getNodeId()/getNodeType()helpers and use them throughoutjsonLdToQuads()(node detection, nested enqueue, rdf:type emission). - Update
valueToTerm()to treat object{ id: ... }as an IRI reference (like{ '@id': ... }). - Add a unit test covering nested
verificationMethodwithid/typealiases.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/rdf/turtle.js | Accept id/type aliases when emitting quads and when enqueuing nested nodes so inline resources are serialized into Turtle. |
| test/turtle.test.js | Adds regression test ensuring nested alias-authored nodes (CID verificationMethod) survive Turtle conversion. |
Comments suppressed due to low confidence (1)
src/rdf/turtle.js:395
- valueToTerm() now treats any non-undefined
@id/idas an IRI reference, but resolveUri() assumes the id is a string. If a stored JSON-LD doc containsid: null(or a non-string), conneg to Turtle will throw. Please requireobjIdto be a string (allowing empty string if you want it to resolve to base) before calling resolveUri().
const objId = value['@id'] !== undefined ? value['@id'] : value.id;
if (objId !== undefined) {
const uri = resolveUri(objId, baseUri);
return uri.startsWith('_:')
? blankNode(uri.slice(2))
: namedNode(uri);
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| */ | ||
| function getNodeId(n) { | ||
| if (!n || typeof n !== 'object') return undefined; | ||
| return n['@id'] !== undefined ? n['@id'] : n.id; |
| } | ||
| function getNodeType(n) { | ||
| if (!n || typeof n !== 'object') return undefined; | ||
| return n['@type'] !== undefined ? n['@type'] : n.type; |
Two findings, both real.
`getNodeId` and `getNodeType` returned whatever they found at
`@id`/`id` and `@type`/`type` — including `null`, numbers, or
nested objects from malformed user-authored profiles. Downstream
they were passed to `resolveUri` and `expandUri`, which call
`.startsWith` and `.includes` respectively — a TypeError on the
request path for any malformed profile.
`valueToTerm` had the same gap on its added `id` alias branch.
Fix:
- getNodeId returns `undefined` unless the value is a string.
Treats `id: 42` or `id: null` as "no identifier."
- getNodeType accepts string OR array. Arrays are filtered to
string entries; if no strings remain, treated as absent.
This preserves the "string entries still emit" property
when a user writes `type: ['Multikey', 42]` — the valid
Multikey emits, the 42 is dropped.
- valueToTerm's id-alias branch checks `typeof rawObjId ===
'string'` before calling resolveUri.
New test asserts:
- nested `id: 42` doesn't crash conneg (returns valid string)
- nested `type: null` doesn't crash
- mixed `type: ['Multikey', 42, null]` — string entries survive
Test count: 771 → 772 in full suite.
This was referenced May 10, 2026
Profile emits bare 'Multikey' type — resolves to <pod>/profile/Multikey instead of cid:Multikey
#417
Closed
melvincarvalho
added a commit
that referenced
this pull request
May 10, 2026
…418) The default profile generator's `@context` mapped CID v1 *predicate* terms (`verificationMethod`, `controller`, etc.) but NOT the *class* names. So a verificationMethod authored with the spec-example shape `{type: "Multikey", ...}` left the bare term unexpanded: - in JSON-LD: a JSON-LD processor with no external context fetcher couldn't resolve "Multikey" to a class IRI - in Turtle (conneg): `expandUri("Multikey")` returned the bare string, N3 wrote `<Multikey>`, RFC-3986 resolved that against the document base — yielding `<pod>/profile/Multikey`, a fictional class on the pod's own host Net: the same key on `alice.example.com` and `bob.example.com` emitted DIFFERENT class IRIs. SPARQL queries on `?s a cid:Multikey` missed every JSS-issued VM. VC validators couldn't recognize the type. Same trap for `JsonWebKey`. Fix: two flat aliases in the profile @context: "Multikey": "cid:Multikey", "JsonWebKey": "cid:JsonWebKey" Why flat aliases (not pre-prefixed CURIE in the JSON-LD payload): - Naive JSON readers comparing `type === "Multikey"` keep working — readable, unchanged shape - JSON-LD processors expand via @context — get the canonical `https://www.w3.org/ns/cid/v1#Multikey` - Our Turtle conneg layer (which DOES respect @context per #389 and now #416) expands and N3 writes `cid:Multikey` with the `cid:` prefix declared at the top of the document — any Turtle parser resolves it correctly - Matches the "JSON-LD with flat context aliases" pattern that consumers like LION/LOSOS rely on Tests: - Profile Document: `@context` declares both `Multikey` and `JsonWebKey` as flat aliases mapping to the CID v1 namespace. - Turtle conneg: combines the production profile generator's @context with a synthetic VM `{type: "Multikey", ...}` and asserts: a. Turtle contains `cid:Multikey` (or full IRI form) b. Turtle does NOT contain bare `<Multikey>` (which would resolve to `<pod>/profile/Multikey`) c. The publicKeyMultibase value still appears (#416 didn't regress) Test count: 772 → 774 in full suite.
melvincarvalho
added a commit
that referenced
this pull request
May 10, 2026
* rdf/turtle: emit a space before `;` and `.` terminators (#419) Match the de-facto Turtle style used in W3C 1.1 spec examples, Apache Jena's RIOT writer, and most hand-authored Turtle in the Solid / linked-data ecosystem: a space between the previous token and the statement terminator. Before: <s> foaf:name "Alice"; foaf:age 30. After: <s> foaf:name "Alice" ; foaf:age 30 . n3.js's writer hardcodes the no-space form (`;\n next`) and exposes no config knob. Approach: a literal-aware post-pass on the writer's output. The naïve `\S;\n` → `\S ;\n` regex is unsafe — string literals (especially triple-quoted) can contain `;\n` or `.\n` internally, and inserting a space inside a literal would silently CHANGE the literal's value. So: 1. Stash every string literal AND every <IRI> into placeholders using a multi-character non-token-boundary sentinel bracketed by NULs (n3.js escapes raw NUL inside literals and never emits one in real Turtle output, so the sentinel can't collide with real content). 2. Apply the spacing regex to the redacted output. Now `;`/`.` only appear as actual statement terminators because all literal/IRI internals are hidden. 3. Restore placeholders. Stash order matters — triple-quoted before single-quoted, else `"""` looks like an empty `""` followed by `"` to the single-quoted regex. Same for triple-vs-single apostrophe. Tests: - "emits a space before ; and . terminators": positive - "does NOT add a space inside literals containing ; or .": safety regression — the post-pass MUST NOT corrupt literal values like `"foo;bar"`, `"has.dot"`, `"a;b.c"`. - "does NOT add a space inside an IRI containing ;": safety regression for IRIs like `<https://example.test/path;with;semis>`. Test count: 770 → 777 in full suite (+7: 3 #419 tests + 4 #416/#417/#415 follow-ups since main). * Address copilot pass 1 on #420 — assert by value, not by lexical form The two safety regression tests for #419 hardcoded that N3 serializes string literals with double quotes (`"foo;bar"`) and IRIs as `<...>`. That made them sensitive to N3 writer formatting choices — single quotes, long-string `"""..."""`, IRI escaping, etc. — even when the underlying literal value would still be preserved correctly. Fix: parse the emitted Turtle back to quads and assert the literal/IRI VALUES, not their lexical form. The properties the tests actually want to enforce are: - "foo;bar" round-trips as a literal whose value is foo;bar (no inserted space) - <https://example.test/path;with;semis> round-trips as a NamedNode with that exact IRI Both are now assertion-by-value. Resilient against any future N3 upgrade that changes quote style, IRI escaping, or the long- string threshold. Test count: same 777. * Address copilot pass 2 on #420 — pin terminator presence The "emits a space before ; and ." test asserted no offending terminators (without preceding whitespace) but didn't assert the terminators were actually present. If a future N3 upgrade ever switched to the "one triple per statement, no ; continuations" output style, the negative assertions would pass vacuously and the test would stop exercising the spacing behavior at all. Fix: pin the presence of at least one ` ;` and one ` .` (with preceding whitespace) before checking for offending forms. Now the test fails loudly if either disappears. Test count: same 777. * Address copilot pass 3 on #420 — require literal space, not any whitespace The pass-2 assertions used `\s` / `[\s]` patterns, which would treat `\n;`, `\t;`, etc. as acceptable too. The #419 intent is specifically a single SPACE before the terminator (matching W3C Turtle 1.1 examples and Apache Jena's RIOT writer): `value ;` / `value .` If a future N3 upgrade ever emitted `\n;` or `\t;`, the pass-2 test would have passed despite the visually-different output. Fix: tighten the assertions to require a literal ` ;` / ` .` (a single space). Both presence-checks and the offending-form negative checks use ` ` (space) explicitly instead of `\s`. Test count: same 777.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #415. Sibling of #389 (array-context) and #390 (
@type: '@json').Symptom
A nested CID v1 verificationMethod authored with the JSON-LD 1.1
id/typealiases (instead of@id/@type) was silently dropped from the Turtle output:JSON-LD input:
Turtle output (excerpt):
Missing: the
cid:verificationMethodpredicate AND the entire#nostr-key-1resource block (noa Multikey, no controller, no publicKeyMultibase).Root cause
src/rdf/turtle.jschecked for the explicit@-prefixed names everywhere. Two breakage points:valueToTerm()returnednullfor{id: ..., ...}because it only inspectedvalue['@id']. So the parent predicate quad was never emitted.jsonLdToQuadscheckedv['@id'], so the nested object's own triples weren't enqueued either.Fix
getNodeId(n)→n['@id'] ?? n.id,getNodeType(n)→n['@type'] ?? n.type.jsonLdToQuadsuses them for top-level docs, nested-object enqueue, and rdf:type emission.valueToTermacceptsidas the IRI-reference key.id/typekeys (handled as aliases above; emitting as predicates produces malformed<id>/<type>triples).@idandidwhen deciding "has own claims."Why this matters
JSS's own backend uses JSON-LD throughout, so the LWS10-CID verifier path is unaffected. But anyone consuming the profile as Turtle (RDF stores, SPARQL endpoints, generic Solid clients defaulting to text/turtle) saw the
cid:authenticationlink with zero description of the target — couldn't find the key material. The "JSON-LD and Turtle profiles match" property #386/#388 implicitly relies on did NOT hold for any pod with a Nostr/Schnorr verificationMethod.Test plan
id/typealiases) and asserts the Turtle output contains:cid:verificationMethodpredicate#k)Multikeytypecid:controllerpredicate on the VMpublicKeyMultibaseliteralcurl -H 'Accept: text/turtle' https://test.solid.social/profile/card.jsonldshould now include the full VM block