javascript-solid-server 0.0.182 → 0.0.184

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -411,7 +411,9 @@
411
411
  "Bash(cp /tmp/consent-preview.html ~/consent-preview.html)",
412
412
  "Read(//home/melvin/**)",
413
413
  "Bash(/usr/bin/chromium-browser --headless --no-sandbox --disable-gpu --hide-scrollbars --window-size=520,900 --screenshot=consent.png file:///home/melvin/consent-preview.html)",
414
- "Bash(sed -i '0,/\"version\": \"0.0.181\"/{s/\"version\": \"0.0.181\"/\"version\": \"0.0.182\"/}' package-lock.json)"
414
+ "Bash(sed -i '0,/\"version\": \"0.0.181\"/{s/\"version\": \"0.0.181\"/\"version\": \"0.0.182\"/}' package-lock.json)",
415
+ "WebFetch(domain:losos.org)",
416
+ "Bash(sed -i '0,/\"version\": \"0.0.183\"/{s/\"version\": \"0.0.183\"/\"version\": \"0.0.184\"/}' package-lock.json)"
415
417
  ]
416
418
  }
417
419
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.182",
3
+ "version": "0.0.184",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/rdf/turtle.js CHANGED
@@ -164,6 +164,47 @@ function quadsToJsonLd(quads, baseUri, prefixes = {}) {
164
164
  return nodes.map((node, i) => i === 0 ? { '@context': context, ...node } : node);
165
165
  }
166
166
 
167
+ /**
168
+ * Read a JSON-LD node's identifier, accepting both the explicit
169
+ * `@id` form AND the unprefixed `id` alias that JSON-LD 1.1 treats
170
+ * as equivalent (and that Solid profiles in the wild use). Same
171
+ * fallback for `@type` / `type`.
172
+ *
173
+ * Without this aliasing, nested objects authored with `id`/`type`
174
+ * (e.g. a CID v1 verificationMethod entry) get silently dropped:
175
+ * - the predicate-→-IRI quad isn't emitted (valueToTerm sees
176
+ * no `@id` and returns null)
177
+ * - the BFS enqueue check (`v['@id']`) is false, so the nested
178
+ * object's own triples are never written either
179
+ * - net result: the entire `cid:verificationMethod` predicate
180
+ * and the `#nostr-key-1` resource block disappear from Turtle.
181
+ *
182
+ * #415.
183
+ */
184
+ function getNodeId(n) {
185
+ if (!n || typeof n !== 'object') return undefined;
186
+ const v = n['@id'] !== undefined ? n['@id'] : n.id;
187
+ // Strict string-only — downstream resolveUri/`.startsWith` would
188
+ // throw on a number, null, or object. Malformed user content
189
+ // (a profile that authored `id: 42`) shouldn't crash conneg;
190
+ // treat non-string identifiers as absent.
191
+ return typeof v === 'string' ? v : undefined;
192
+ }
193
+ function getNodeType(n) {
194
+ if (!n || typeof n !== 'object') return undefined;
195
+ const v = n['@type'] !== undefined ? n['@type'] : n.type;
196
+ // Accept string OR array — expandUri/`.includes` would throw on
197
+ // anything else. For arrays, filter to string entries downstream
198
+ // (handled by Array.isArray + the per-entry expandUri call which
199
+ // assumes string; we filter here to be safe).
200
+ if (typeof v === 'string') return v;
201
+ if (Array.isArray(v)) {
202
+ const strs = v.filter(t => typeof t === 'string');
203
+ return strs.length > 0 ? strs : undefined;
204
+ }
205
+ return undefined;
206
+ }
207
+
167
208
  /**
168
209
  * Convert JSON-LD to N3.js quads
169
210
  */
@@ -181,8 +222,8 @@ function jsonLdToQuads(jsonLd, baseUri) {
181
222
  if (doc['@context']) {
182
223
  mergedContext = { ...mergedContext, ...doc['@context'] };
183
224
  }
184
- // Each document with @id is a node (no @graph needed)
185
- if (doc['@id']) {
225
+ // Each document with @id (or `id` alias) is a node (no @graph needed)
226
+ if (getNodeId(doc) !== undefined) {
186
227
  nodes.push(doc);
187
228
  }
188
229
  }
@@ -206,16 +247,18 @@ function jsonLdToQuads(jsonLd, baseUri) {
206
247
  const queue = [...nodes];
207
248
  for (let i = 0; i < queue.length; i++) {
208
249
  const node = queue[i];
209
- if (!node['@id']) continue;
210
- const subjectUri = resolveUri(node['@id'], baseUri);
250
+ const nodeId = getNodeId(node);
251
+ if (nodeId === undefined) continue;
252
+ const subjectUri = resolveUri(nodeId, baseUri);
211
253
 
212
254
  const subject = subjectUri.startsWith('_:')
213
255
  ? blankNode(subjectUri.slice(2))
214
256
  : namedNode(subjectUri);
215
257
 
216
- // Handle @type
217
- if (node['@type']) {
218
- const types = Array.isArray(node['@type']) ? node['@type'] : [node['@type']];
258
+ // Handle @type (or `type` alias).
259
+ const nodeType = getNodeType(node);
260
+ if (nodeType !== undefined) {
261
+ const types = Array.isArray(nodeType) ? nodeType : [nodeType];
219
262
  for (const type of types) {
220
263
  const typeUri = expandUri(type, context);
221
264
  quads.push(quad(
@@ -226,9 +269,13 @@ function jsonLdToQuads(jsonLd, baseUri) {
226
269
  }
227
270
  }
228
271
 
229
- // Handle other properties
272
+ // Handle other properties. Skip `@`-prefixed keys AND the `id`/
273
+ // `type` aliases (handled above as @id/@type) — emitting them as
274
+ // predicates would produce malformed triples like `<id>` and
275
+ // `<type>` since the names don't expand to URIs via context.
230
276
  for (const [key, value] of Object.entries(node)) {
231
277
  if (key.startsWith('@')) continue;
278
+ if (key === 'id' || key === 'type') continue;
232
279
 
233
280
  const predicateUri = expandUri(key, context);
234
281
  const predicate = namedNode(predicateUri);
@@ -243,15 +290,16 @@ function jsonLdToQuads(jsonLd, baseUri) {
243
290
  if (object) {
244
291
  quads.push(quad(subject, predicate, object));
245
292
  }
246
- // If v is a nested node (object with @id and at least one non-@value
247
- // own property beyond @id), enqueue it so its triples are also
248
- // emitted. Object-identity tracking (WeakSet) prevents the same
249
- // nested object from being enqueued twice, which would otherwise
250
- // loop for graphs that reuse an object reference (cycles).
293
+ // If v is a nested node (object with @id/id and at least one
294
+ // own property beyond the identifier), enqueue it so its
295
+ // triples are also emitted. Object-identity tracking
296
+ // (WeakSet) prevents the same nested object from being
297
+ // enqueued twice, which would otherwise loop for graphs
298
+ // that reuse an object reference (cycles).
251
299
  if (v && typeof v === 'object' && !Array.isArray(v) &&
252
- v['@id'] && v['@value'] === undefined &&
300
+ getNodeId(v) !== undefined && v['@value'] === undefined &&
253
301
  !enqueuedNested.has(v)) {
254
- const hasOwnClaims = Object.keys(v).some(k => k !== '@id');
302
+ const hasOwnClaims = Object.keys(v).some(k => k !== '@id' && k !== 'id');
255
303
  if (hasOwnClaims) {
256
304
  enqueuedNested.add(v);
257
305
  queue.push(v);
@@ -348,9 +396,18 @@ function valueToTerm(value, baseUri, context, isIdType = false) {
348
396
 
349
397
  // Object values
350
398
  if (typeof value === 'object') {
351
- // @id reference
352
- if (value['@id']) {
353
- const uri = resolveUri(value['@id'], baseUri);
399
+ // @id reference (or `id` alias — same JSON-LD 1.1 convention).
400
+ // This is what makes the predicate-→-IRI quad get emitted for
401
+ // nested objects authored with `id` instead of `@id`. Without
402
+ // it, an inline verificationMethod with `id`/`type` returned
403
+ // null here and the parent predicate triple was lost.
404
+ //
405
+ // String-only — a numeric or null `@id`/`id` would crash
406
+ // resolveUri's `.startsWith`. Treat as absent and fall through
407
+ // to the @value/@language branches below.
408
+ const rawObjId = value['@id'] !== undefined ? value['@id'] : value.id;
409
+ if (typeof rawObjId === 'string') {
410
+ const uri = resolveUri(rawObjId, baseUri);
354
411
  return uri.startsWith('_:')
355
412
  ? blankNode(uri.slice(2))
356
413
  : namedNode(uri);
@@ -75,7 +75,19 @@ export function generateProfileJsonLd({ webId, name, podUri, issuer }) {
75
75
  'authentication': { '@id': 'cid:authentication', '@type': '@id', '@container': '@set' },
76
76
  'assertionMethod': { '@id': 'cid:assertionMethod', '@type': '@id', '@container': '@set' },
77
77
  'publicKeyJwk': { '@id': 'cid:publicKeyJwk', '@type': '@json' },
78
- 'publicKeyMultibase': { '@id': 'cid:publicKeyMultibase' }
78
+ 'publicKeyMultibase': { '@id': 'cid:publicKeyMultibase' },
79
+ // CID v1 verificationMethod *class* names (#417). Without these
80
+ // term mappings, an app PATCHing in a VM with `type: "Multikey"`
81
+ // (the spec-example shape) emits a bare relative-IRI `<Multikey>`
82
+ // when JSS conneg-converts the profile to Turtle — which then
83
+ // resolves against the document's base URL to a fictional class
84
+ // like `<pod>/profile/Multikey`. Mapping the class names here
85
+ // means the bare term `"Multikey"` in JSON-LD expands correctly
86
+ // (cid:Multikey → https://www.w3.org/ns/cid/v1#Multikey) for both
87
+ // JSON-LD processors AND our Turtle conneg layer. Naive JSON
88
+ // readers comparing `type === "Multikey"` continue to work.
89
+ 'Multikey': 'cid:Multikey',
90
+ 'JsonWebKey': 'cid:JsonWebKey'
79
91
  },
80
92
  '@id': webId,
81
93
  '@type': ['foaf:Person', 'schema:Person'],
@@ -85,6 +85,74 @@ describe('turtle converter — unit (#320 follow-ups)', () => {
85
85
  `Turtle output must not contain object-stringification, got:\n${content}`);
86
86
  });
87
87
 
88
+ it('nested object with `id`/`type` aliases survives the conversion (#415)', async () => {
89
+ // Solid profiles use the JSON-LD 1.1 `id`/`type` aliases for
90
+ // nested resources (no `@`). The converter must accept both
91
+ // forms — without this, a CID v1 verificationMethod object
92
+ // gets silently dropped:
93
+ // - the `cid:verificationMethod` predicate isn't emitted
94
+ // - the nested `#nostr-key-1` resource (Multikey, controller,
95
+ // publicKeyMultibase) isn't emitted either
96
+ // Net: third-party Turtle consumers see `cid:authentication
97
+ // <#nostr-key-1>` with no description of `#nostr-key-1`.
98
+ const doc = {
99
+ '@context': {
100
+ cid: 'https://www.w3.org/ns/cid/v1#',
101
+ verificationMethod: { '@id': 'cid:verificationMethod', '@container': '@set' },
102
+ authentication: { '@id': 'cid:authentication', '@type': '@id', '@container': '@set' },
103
+ controller: { '@id': 'cid:controller', '@type': '@id' },
104
+ publicKeyMultibase: { '@id': 'cid:publicKeyMultibase' },
105
+ },
106
+ '@id': 'https://example.test/profile/card.jsonld#me',
107
+ verificationMethod: [{
108
+ // Aliases — `id`/`type`, not `@id`/`@type`.
109
+ id: 'https://example.test/profile/card.jsonld#k',
110
+ type: 'Multikey',
111
+ controller: 'https://example.test/profile/card.jsonld#me',
112
+ publicKeyMultibase: 'fe70102de7ec',
113
+ }],
114
+ authentication: ['https://example.test/profile/card.jsonld#k'],
115
+ };
116
+ const { content } = await fromJsonLd(doc, 'text/turtle', 'https://example.test/', true);
117
+
118
+ // The cid:verificationMethod predicate must connect #me to the VM.
119
+ assert.match(content, /cid:verificationMethod|<https:\/\/www\.w3\.org\/ns\/cid\/v1#verificationMethod>/,
120
+ `cid:verificationMethod predicate missing from Turtle:\n${content}`);
121
+ // The VM resource must be described — its type, controller, key.
122
+ assert.ok(content.includes('https://example.test/profile/card.jsonld#k'),
123
+ `VM #k must appear in Turtle:\n${content}`);
124
+ assert.match(content, /Multikey|<https:\/\/www\.w3\.org\/ns\/cid\/v1#Multikey>/,
125
+ `Multikey type missing from Turtle:\n${content}`);
126
+ assert.ok(content.includes('fe70102de7ec'),
127
+ `publicKeyMultibase value missing from Turtle:\n${content}`);
128
+ assert.match(content, /cid:controller|<https:\/\/www\.w3\.org\/ns\/cid\/v1#controller>/,
129
+ `cid:controller predicate missing on the VM:\n${content}`);
130
+ });
131
+
132
+ it('malformed `id`/`type` values are silently dropped, not crashed on (#415 review)', async () => {
133
+ // Profiles in the wild can have malformed user-authored content
134
+ // — e.g. `id: 42` or `type: null`. The converter must NOT throw
135
+ // (downstream `resolveUri.startsWith` and `expandUri.includes`
136
+ // assume strings); it should treat the malformed value as absent
137
+ // and skip the affected resource cleanly.
138
+ const doc = {
139
+ '@context': { 'cid': 'https://www.w3.org/ns/cid/v1#' },
140
+ '@id': 'https://example.test/s',
141
+ // Nested object with a non-string `id` — must not crash.
142
+ 'cid:bad1': { id: 42, 'cid:foo': 'x' },
143
+ // Nested object with a null `type` — must not crash.
144
+ 'cid:bad2': { id: 'https://example.test/n2', type: null, 'cid:foo': 'x' },
145
+ // Array `type` with mixed string/non-string entries — string
146
+ // entries should still emit.
147
+ 'cid:mixed': { id: 'https://example.test/n3', type: ['Multikey', 42, null], 'cid:foo': 'x' },
148
+ };
149
+ const { content } = await fromJsonLd(doc, 'text/turtle', 'https://example.test/', true);
150
+ assert.ok(typeof content === 'string', 'must produce a string output, not throw');
151
+ // The valid string type entry should survive in the mixed-type case.
152
+ assert.ok(content.includes('https://example.test/n3'),
153
+ `node n3 should appear:\n${content}`);
154
+ });
155
+
88
156
  it('cyclical nested node reference does not hang', async () => {
89
157
  // Two nested nodes reference each other. BFS must not loop.
90
158
  const a = { '@id': 'https://example.test/a', 'ex:knows': null };
@@ -87,6 +87,30 @@ describe('WebID Profile', () => {
87
87
  assert.strictEqual(ctx.publicKeyJwk['@type'], '@json');
88
88
  });
89
89
 
90
+ it('declares CID v1 class names (Multikey, JsonWebKey) as flat aliases (#417)', async () => {
91
+ // Without these mappings, an app PATCHing in a VM with the
92
+ // spec-example shape `{type: "Multikey", ...}` produces a bare
93
+ // relative-IRI `<Multikey>` in the Turtle conneg output, which
94
+ // resolves to a fictional class on the pod's own host (e.g.
95
+ // `<pod>/profile/Multikey` instead of `cid:Multikey`).
96
+ //
97
+ // The flat-alias shape (`"Multikey": "cid:Multikey"`) makes
98
+ // bare-term emission work correctly through both JSON-LD
99
+ // expansion AND our Turtle conneg layer — and matches the
100
+ // "JSON-LD with flat context aliases" pattern consumers like
101
+ // LOSOS / LION rely on.
102
+ const res = await request(profilePath);
103
+ const jsonLd = await res.json();
104
+ const ctx = jsonLd['@context'];
105
+ for (const cls of ['Multikey', 'JsonWebKey']) {
106
+ const mapping = ctx[cls];
107
+ assert.ok(mapping, `@context must define class alias \`${cls}\``);
108
+ const id = typeof mapping === 'string' ? mapping : mapping['@id'];
109
+ assert.match(id, new RegExp(`^(cid:${cls}|https://www\\.w3\\.org/ns/cid/v1#${cls})$`),
110
+ `${cls} must map to the CID v1 namespace`);
111
+ }
112
+ });
113
+
90
114
  it('declares self-control via controller === @id (#386 Phase A)', async () => {
91
115
  const res = await request(profilePath);
92
116
  const jsonLd = await res.json();
@@ -216,6 +240,59 @@ describe('WebID Profile — Turtle conneg (#320)', () => {
216
240
  await stopTestServer();
217
241
  });
218
242
 
243
+ it('Turtle conneg: generated profile @context expands bare Multikey/JsonWebKey terms (#417)', async () => {
244
+ // Combine the production profile generator's @context with a
245
+ // synthetic VM (the spec-example shape `{type: "Multikey", ...}`)
246
+ // and run it through the same conneg path the live profile
247
+ // would. Asserts: the bare-term type expands to the CID v1
248
+ // namespace, not to a relative IRI that resolves to a fake
249
+ // class on the pod's host.
250
+ const { generateProfile } = await import('../src/webid/profile.js');
251
+ const { fromJsonLd } = await import('../src/rdf/conneg.js');
252
+ const webId = 'https://example.test/profile/card.jsonld#me';
253
+ const profile = generateProfile({
254
+ webId,
255
+ name: 'mk-test',
256
+ podUri: 'https://example.test/',
257
+ issuer: 'https://example.test/',
258
+ });
259
+ // Inject a Multikey VM authored with the spec-example bare-term
260
+ // type — this is the shape the bug surfaces on.
261
+ const vmId = webId.replace('#me', '#nostr-key-1');
262
+ profile.verificationMethod = [{
263
+ id: vmId,
264
+ type: 'Multikey',
265
+ controller: webId,
266
+ publicKeyMultibase: 'fe70102de7ec0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab',
267
+ }];
268
+ profile.authentication = [vmId];
269
+
270
+ const { content: ttl } = await fromJsonLd(profile, 'text/turtle', 'https://example.test/', true);
271
+
272
+ // Pre-#417: would emit `a <Multikey>` (relative — resolves to
273
+ // `https://example.test/profile/Multikey`).
274
+ // After #417: the @context maps `Multikey -> cid:Multikey`, so
275
+ // the converter expands the bare term to the CID v1 IRI.
276
+ assert.ok(
277
+ ttl.includes('cid:Multikey') || ttl.includes('cid/v1#Multikey'),
278
+ `Turtle must emit a CID-namespaced Multikey class, got:\n${ttl}`,
279
+ );
280
+ assert.ok(
281
+ !/\ba\s+<Multikey>\s*[;.]/.test(ttl),
282
+ `Turtle must NOT emit bare <Multikey> (resolves to fictional class), got:\n${ttl}`,
283
+ );
284
+ // Don't regress #416: the VM block + publicKeyMultibase must
285
+ // still survive the conversion.
286
+ assert.ok(
287
+ ttl.includes('publicKeyMultibase') || ttl.includes('cid/v1#publicKeyMultibase'),
288
+ `Turtle must include cid:publicKeyMultibase, got:\n${ttl}`,
289
+ );
290
+ assert.ok(
291
+ ttl.includes('fe70102de7ec'),
292
+ `Turtle must include the publicKeyMultibase value, got:\n${ttl}`,
293
+ );
294
+ });
295
+
219
296
  it('Turtle variant includes cid:service with lws:OpenIdProvider and serviceEndpoint', async () => {
220
297
  const res = await request('/webidturtletest/profile/card.jsonld', {
221
298
  headers: { Accept: 'text/turtle' }