Surfaced during #388 (Phase A of #386).
Symptom
JSON-LD documents with @context in array form (e.g. [ "https://www.w3.org/ns/cid/v1", { ...inline prefixes... } ]) lose all term expansion when converted to Turtle via the conneg layer. Predicates appear as bare relative IRIs (<inbox>, <controller>, etc.) instead of expanding to their full namespace URIs.
Reproduction (during #388 development)
Switching src/webid/profile.js to emit @context as an array with https://www.w3.org/ns/cid/v1 first caused the existing #320 Turtle conneg test to fail. Output excerpt:
<http://...#me> a foaf:Person, schema:Person;
foaf:name "webidturtletest";
<inbox> "http://...inbox/";
<storage> "http://...";
<oidcIssuer> "http://...";
...
<service> <http://...#oidc>.
<http://...#oidc> a <lws:OpenIdProvider>;
<serviceEndpoint> "http://...".
Every term is a bare relative IRI. Should be ldp:inbox, pim:storage, solid:oidcIssuer, etc.
Root cause
src/rdf/turtle.js:182:
mergedContext = { ...mergedContext, ...doc['@context'] };
A shallow object spread. When doc['@context'] is an array, this copies array indices 0, 1, … as keys, breaking lookups. The function silently produces a context object where only the inline-object form survives — and only if it's a single object, not an array element.
Design constraint: don't fetch remote contexts
JSS deliberately doesn't dereference external context URLs at conversion time. Doing so would:
- SSRF risk: any URL in any user-supplied JSON-LD becomes a fetch trigger from the server
- Reliability: depend on third-party context hosts at request time
- Latency: every Turtle conneg becomes an outbound HTTPS round-trip
- Cache complexity: external contexts can change; do we cache, validate, refresh?
So the fix should not be "fetch the URL and merge it." Two reasonable fixes that preserve the no-remote-fetch invariant:
Option A — handle array form, ignore string entries
Iterate @context if it's an array; merge each entry that is an object, ignore each entry that is a string URL. Documents that need the URL-imported context's terms must declare those terms inline too (the pattern #388 ended up using).
function mergeContext(target, ctx) {
if (!ctx) return target;
if (Array.isArray(ctx)) {
for (const entry of ctx) mergeContext(target, entry);
return target;
}
if (typeof ctx === 'object') return Object.assign(target, ctx);
// String URLs ignored — JSS doesn't fetch external contexts
return target;
}
This makes JSS tolerate the array form (which is increasingly common — CID v1 docs, VC docs, DID docs all use it) without changing the no-fetch policy.
Option B — bundle well-known contexts as static files
Ship a small in-tree map of URL → static-context-object for the contexts JSS itself emits or expects (CID v1, AS2, DID core, schema.org subset). Reads cost zero — the file is just import ed at startup.
Pros: documents using @context: ["https://www.w3.org/ns/cid/v1", {…}] get the imported terms expanded correctly without any network. Solves both #388 and any future LWS / VC / DID work.
Cons: a small set of contexts that need maintenance when upstream changes. Mitigation: snapshot via test, alert on diff during periodic CI run.
Combined
Probably the right answer is A first, B opportunistically. A makes the array form non-crashing; B adds real support for canonical W3C contexts as we need them.
Acceptance
Refs
Surfaced during #388 (Phase A of #386).
Symptom
JSON-LD documents with
@contextin array form (e.g.[ "https://www.w3.org/ns/cid/v1", { ...inline prefixes... } ]) lose all term expansion when converted to Turtle via the conneg layer. Predicates appear as bare relative IRIs (<inbox>,<controller>, etc.) instead of expanding to their full namespace URIs.Reproduction (during #388 development)
Switching
src/webid/profile.jsto emit@contextas an array withhttps://www.w3.org/ns/cid/v1first caused the existing#320Turtle conneg test to fail. Output excerpt:Every term is a bare relative IRI. Should be
ldp:inbox,pim:storage,solid:oidcIssuer, etc.Root cause
src/rdf/turtle.js:182:A shallow object spread. When
doc['@context']is an array, this copies array indices0,1, … as keys, breaking lookups. The function silently produces a context object where only the inline-object form survives — and only if it's a single object, not an array element.Design constraint: don't fetch remote contexts
JSS deliberately doesn't dereference external context URLs at conversion time. Doing so would:
So the fix should not be "fetch the URL and merge it." Two reasonable fixes that preserve the no-remote-fetch invariant:
Option A — handle array form, ignore string entries
Iterate
@contextif it's an array; merge each entry that is an object, ignore each entry that is a string URL. Documents that need the URL-imported context's terms must declare those terms inline too (the pattern #388 ended up using).This makes JSS tolerate the array form (which is increasingly common — CID v1 docs, VC docs, DID docs all use it) without changing the no-fetch policy.
Option B — bundle well-known contexts as static files
Ship a small in-tree map of
URL → static-context-objectfor the contexts JSS itself emits or expects (CID v1, AS2, DID core, schema.org subset). Reads cost zero — the file is justimported at startup.Pros: documents using
@context: ["https://www.w3.org/ns/cid/v1", {…}]get the imported terms expanded correctly without any network. Solves both #388 and any future LWS / VC / DID work.Cons: a small set of contexts that need maintenance when upstream changes. Mitigation: snapshot via test, alert on diff during periodic CI run.
Combined
Probably the right answer is A first, B opportunistically. A makes the array form non-crashing; B adds real support for canonical W3C contexts as we need them.
Acceptance
src/rdf/turtle.jsaccepts array@contextwithout losing term expansion from inline objects in the same array@contextarrays are ignored gracefully (no fetch attempted)@context: ["https://www.w3.org/ns/cid/v1", { foaf, … }]round-trips to Turtle withfoaf:nameetc. preservedRefs
src/rdf/turtle.js:170-190— the merging logic