javascript-solid-server 0.0.156 → 0.0.157

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.156",
3
+ "version": "0.0.157",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -385,8 +385,8 @@ function getErrorPage(statusCode, isAuthenticated, request) {
385
385
  </div>
386
386
 
387
387
  <p class="footer">
388
- Powered by <a href="https://sandy-mount.com">Sandymount</a> •
389
- <a href="https://solidproject.org">Learn about Solid</a>
388
+ Powered by <a href="https://jss.live/">JSS</a> •
389
+ <a href="https://jss.live/docs/">Docs</a>
390
390
  </p>
391
391
  </div>
392
392
  </body>
@@ -42,11 +42,16 @@ export async function handlePost(request, reply) {
42
42
 
43
43
  // Check if we can accept this input type
44
44
  if (!canAcceptInput(contentType, connegEnabled)) {
45
+ const acceptValue = connegEnabled
46
+ ? 'application/ld+json, application/json, text/turtle, text/n3'
47
+ : 'application/ld+json, application/json';
48
+ reply.header('Accept', acceptValue);
49
+ reply.header('Accept-Post', acceptValue);
45
50
  return reply.code(415).send({
46
51
  error: 'Unsupported Media Type',
47
52
  message: connegEnabled
48
- ? 'Supported types: application/ld+json, text/turtle, text/n3'
49
- : 'Supported type: application/ld+json (enable conneg for Turtle support)'
53
+ ? 'Supported types: application/ld+json, application/json, text/turtle, text/n3'
54
+ : 'Supported types: application/ld+json, application/json (enable conneg for Turtle/N3 support)'
50
55
  });
51
56
  }
52
57
 
@@ -614,13 +614,35 @@ export async function handlePut(request, reply) {
614
614
 
615
615
  const contentType = request.headers['content-type'] || '';
616
616
 
617
+ // ACL resources require a JSON-LD payload (application/ld+json or
618
+ // application/json). Round-trip serialization between JSON-LD and
619
+ // Turtle representations has limitations that can cause data loss
620
+ // when a client PUTs Turtle and later requests Turtle.
621
+ // Other RDF resources are unaffected. The guard fires regardless
622
+ // of conneg setting and also when Content-Type is missing.
623
+ const ctMain = contentType.split(';')[0].trim().toLowerCase();
624
+ const isJsonLd = ctMain === 'application/ld+json' || ctMain === 'application/json';
625
+ if (urlPath.endsWith('.acl') && !isJsonLd) {
626
+ reply.header('Accept', 'application/ld+json, application/json');
627
+ reply.header('Accept-Put', 'application/ld+json, application/json');
628
+ return reply.code(415).send({
629
+ error: 'Unsupported Media Type',
630
+ message: 'ACL resources must be sent as application/ld+json or application/json.'
631
+ });
632
+ }
633
+
617
634
  // Check if we can accept this input type
618
635
  if (!canAcceptInput(contentType, connegEnabled)) {
636
+ const acceptValue = connegEnabled
637
+ ? 'application/ld+json, application/json, text/turtle, text/n3'
638
+ : 'application/ld+json, application/json';
639
+ reply.header('Accept', acceptValue);
640
+ reply.header('Accept-Put', acceptValue);
619
641
  return reply.code(415).send({
620
642
  error: 'Unsupported Media Type',
621
643
  message: connegEnabled
622
- ? 'Supported types: application/ld+json, text/turtle, text/n3'
623
- : 'Supported type: application/ld+json (enable conneg for Turtle support)'
644
+ ? 'Supported types: application/ld+json, application/json, text/turtle, text/n3'
645
+ : 'Supported types: application/ld+json, application/json (enable conneg for Turtle/N3 support)'
624
646
  });
625
647
  }
626
648
 
package/src/rdf/conneg.js CHANGED
@@ -205,20 +205,33 @@ export function getVaryHeader(connegEnabled, mashlibEnabled = false) {
205
205
  }
206
206
 
207
207
  /**
208
- * Get Accept-* headers for responses
208
+ * Get Accept-* headers for responses.
209
+ *
210
+ * The explicitly listed RDF types are aligned with the formats this
211
+ * module accepts so clients can discover support consistently:
212
+ * - JSON-LD (application/ld+json) and JSON (application/json alias)
213
+ * are advertised in all conneg modes.
214
+ * - Turtle (text/turtle) and N3 (text/n3) are advertised only when
215
+ * conneg is enabled (SUPPORTED_INPUT in this file).
216
+ *
217
+ * Note: a wildcard (asterisk-slash-asterisk) is included as a broad
218
+ * interoperability hint for generic clients and proxies. It is not a
219
+ * strict contract that every media type matching the wildcard will be
220
+ * accepted by canAcceptInput() (e.g., application/n-triples and
221
+ * application/rdf+xml are not accepted).
209
222
  */
210
223
  export function getAcceptHeaders(connegEnabled, isContainer = false) {
211
224
  const headers = {};
212
225
 
213
226
  if (isContainer) {
214
227
  headers['Accept-Post'] = connegEnabled
215
- ? `${RDF_TYPES.JSON_LD}, ${RDF_TYPES.TURTLE}, */*`
216
- : `${RDF_TYPES.JSON_LD}, */*`;
228
+ ? `${RDF_TYPES.JSON_LD}, application/json, ${RDF_TYPES.TURTLE}, ${RDF_TYPES.N3}, */*`
229
+ : `${RDF_TYPES.JSON_LD}, application/json, */*`;
217
230
  }
218
231
 
219
232
  headers['Accept-Put'] = connegEnabled
220
- ? `${RDF_TYPES.JSON_LD}, ${RDF_TYPES.TURTLE}, */*`
221
- : `${RDF_TYPES.JSON_LD}, */*`;
233
+ ? `${RDF_TYPES.JSON_LD}, application/json, ${RDF_TYPES.TURTLE}, ${RDF_TYPES.N3}, */*`
234
+ : `${RDF_TYPES.JSON_LD}, application/json, */*`;
222
235
 
223
236
  headers['Accept-Patch'] = 'text/n3, application/sparql-update';
224
237
 
@@ -193,6 +193,34 @@ describe('Content Negotiation (conneg enabled)', () => {
193
193
  assert.ok(acceptPost && acceptPost.includes('text/turtle'),
194
194
  'Accept-Post should include text/turtle');
195
195
  });
196
+
197
+ it('should advertise N3 support in Accept-Put when conneg enabled', async () => {
198
+ const res = await request('/connegtest/public/alice.json');
199
+ const acceptPut = res.headers.get('Accept-Put');
200
+ assert.ok(acceptPut && acceptPut.includes('text/n3'),
201
+ 'Accept-Put should include text/n3 (canAcceptInput accepts it under conneg)');
202
+ });
203
+
204
+ it('should advertise N3 support in Accept-Post for containers when conneg enabled', async () => {
205
+ const res = await request('/connegtest/public/');
206
+ const acceptPost = res.headers.get('Accept-Post');
207
+ assert.ok(acceptPost && acceptPost.includes('text/n3'),
208
+ 'Accept-Post should include text/n3 (canAcceptInput accepts it under conneg)');
209
+ });
210
+
211
+ it('should advertise application/json in Accept-Put', async () => {
212
+ const res = await request('/connegtest/public/alice.json');
213
+ const acceptPut = res.headers.get('Accept-Put');
214
+ assert.ok(acceptPut && acceptPut.includes('application/json'),
215
+ 'Accept-Put should include application/json (canAcceptInput treats it as a JSON-LD alias)');
216
+ });
217
+
218
+ it('should advertise application/json in Accept-Post for containers', async () => {
219
+ const res = await request('/connegtest/public/');
220
+ const acceptPost = res.headers.get('Accept-Post');
221
+ assert.ok(acceptPost && acceptPost.includes('application/json'),
222
+ 'Accept-Post should include application/json (canAcceptInput treats it as a JSON-LD alias)');
223
+ });
196
224
  });
197
225
 
198
226
  // Regression coverage for #294 — Solid convention dotfiles (.acl, .meta)
@@ -261,6 +289,107 @@ describe('Content Negotiation (conneg enabled)', () => {
261
289
  'round-tripped JSON-LD should have at least one @-keyword');
262
290
  });
263
291
  });
292
+
293
+ // ACL resources require a JSON-LD payload (application/ld+json or
294
+ // application/json) on PUT regardless of conneg setting: round-trip
295
+ // serialization between JSON-LD and Turtle has known limitations
296
+ // that can cause data loss. See #295.
297
+ describe('ACL content-type guard (#295)', () => {
298
+ const aclJsonLd = {
299
+ '@context': { acl: 'http://www.w3.org/ns/auth/acl#' },
300
+ '@graph': [
301
+ {
302
+ '@id': '#owner',
303
+ '@type': 'acl:Authorization',
304
+ 'acl:agent': { '@id': '#me' },
305
+ 'acl:accessTo': { '@id': './' },
306
+ 'acl:mode': [{ '@id': 'acl:Read' }, { '@id': 'acl:Write' }, { '@id': 'acl:Control' }]
307
+ }
308
+ ]
309
+ };
310
+
311
+ it('rejects text/turtle PUT to .acl with 415', async () => {
312
+ const turtle = `
313
+ @prefix acl: <http://www.w3.org/ns/auth/acl#>.
314
+ <#owner> a acl:Authorization;
315
+ acl:mode acl:Read.
316
+ `;
317
+ const res = await request('/connegtest/public/turtle-reject.acl', {
318
+ method: 'PUT',
319
+ headers: { 'Content-Type': 'text/turtle' },
320
+ body: turtle,
321
+ auth: 'connegtest'
322
+ });
323
+ assertStatus(res, 415);
324
+ assertHeaderContains(res, 'Accept', 'application/ld+json');
325
+ assertHeaderContains(res, 'Accept', 'application/json');
326
+ assertHeaderContains(res, 'Accept-Put', 'application/ld+json');
327
+ assertHeaderContains(res, 'Accept-Put', 'application/json');
328
+ });
329
+
330
+ it('rejects text/n3 PUT to .acl with 415', async () => {
331
+ const res = await request('/connegtest/public/n3-reject.acl', {
332
+ method: 'PUT',
333
+ headers: { 'Content-Type': 'text/n3' },
334
+ body: '@prefix acl: <http://www.w3.org/ns/auth/acl#>. <#x> a acl:Authorization.',
335
+ auth: 'connegtest'
336
+ });
337
+ assertStatus(res, 415);
338
+ assertHeaderContains(res, 'Accept', 'application/ld+json');
339
+ assertHeaderContains(res, 'Accept', 'application/json');
340
+ assertHeaderContains(res, 'Accept-Put', 'application/ld+json');
341
+ assertHeaderContains(res, 'Accept-Put', 'application/json');
342
+ });
343
+
344
+ it('rejects text/plain PUT to .acl with 415 (URL-extension protection)', async () => {
345
+ const res = await request('/connegtest/public/plain-reject.acl', {
346
+ method: 'PUT',
347
+ headers: { 'Content-Type': 'text/plain' },
348
+ body: 'arbitrary text',
349
+ auth: 'connegtest'
350
+ });
351
+ assertStatus(res, 415);
352
+ assertHeaderContains(res, 'Accept', 'application/ld+json');
353
+ assertHeaderContains(res, 'Accept', 'application/json');
354
+ assertHeaderContains(res, 'Accept-Put', 'application/ld+json');
355
+ assertHeaderContains(res, 'Accept-Put', 'application/json');
356
+ });
357
+
358
+ it('rejects PUT to .acl with no Content-Type with 415', async () => {
359
+ // Use Uint8Array body so fetch() doesn't auto-set Content-Type
360
+ // (which it does for string bodies: text/plain;charset=UTF-8).
361
+ const res = await request('/connegtest/public/no-ct-reject.acl', {
362
+ method: 'PUT',
363
+ body: new Uint8Array([1, 2, 3, 4]),
364
+ auth: 'connegtest'
365
+ });
366
+ assertStatus(res, 415);
367
+ assertHeaderContains(res, 'Accept', 'application/ld+json');
368
+ assertHeaderContains(res, 'Accept', 'application/json');
369
+ assertHeaderContains(res, 'Accept-Put', 'application/ld+json');
370
+ assertHeaderContains(res, 'Accept-Put', 'application/json');
371
+ });
372
+
373
+ it('accepts application/ld+json PUT to .acl', async () => {
374
+ const res = await request('/connegtest/public/jsonld-accept.acl', {
375
+ method: 'PUT',
376
+ headers: { 'Content-Type': 'application/ld+json' },
377
+ body: JSON.stringify(aclJsonLd),
378
+ auth: 'connegtest'
379
+ });
380
+ assert.ok(res.status < 300, `JSON-LD PUT to .acl should succeed, got ${res.status}`);
381
+ });
382
+
383
+ it('accepts application/json PUT to .acl', async () => {
384
+ const res = await request('/connegtest/public/json-accept.acl', {
385
+ method: 'PUT',
386
+ headers: { 'Content-Type': 'application/json' },
387
+ body: JSON.stringify(aclJsonLd),
388
+ auth: 'connegtest'
389
+ });
390
+ assert.ok(res.status < 300, `application/json PUT to .acl should succeed, got ${res.status}`);
391
+ });
392
+ });
264
393
  });
265
394
 
266
395
  describe('Content Negotiation (conneg disabled - default)', () => {
@@ -352,6 +481,109 @@ describe('Content Negotiation (conneg disabled - default)', () => {
352
481
  assert.ok(!acceptPut || !acceptPut.includes('text/turtle'),
353
482
  'Accept-Put should NOT include text/turtle when conneg disabled');
354
483
  });
484
+
485
+ it('should advertise application/json in Accept-Put when conneg disabled', async () => {
486
+ const res = await request('/noconneg/public/');
487
+ const acceptPut = res.headers.get('Accept-Put');
488
+ assert.ok(acceptPut && acceptPut.includes('application/json'),
489
+ 'Accept-Put should include application/json (canAcceptInput treats it as a JSON-LD alias)');
490
+ });
491
+
492
+ it('should advertise application/json in Accept-Post when conneg disabled', async () => {
493
+ const res = await request('/noconneg/public/');
494
+ const acceptPost = res.headers.get('Accept-Post');
495
+ assert.ok(acceptPost && acceptPost.includes('application/json'),
496
+ 'Accept-Post should include application/json (canAcceptInput treats it as a JSON-LD alias)');
497
+ });
498
+
499
+ it('should not advertise text/n3 in Accept-Put when conneg disabled', async () => {
500
+ const res = await request('/noconneg/public/');
501
+ const acceptPut = res.headers.get('Accept-Put');
502
+ assert.ok(!acceptPut || !acceptPut.includes('text/n3'),
503
+ 'Accept-Put should NOT include text/n3 when conneg disabled');
504
+ });
505
+ });
506
+
507
+ // The .acl content-type guard applies regardless of conneg setting (#295).
508
+ // The default deployment configuration is conneg disabled, so ensure the
509
+ // guard fires there too.
510
+ describe('ACL content-type guard (#295) — conneg disabled', () => {
511
+ const aclJsonLd = {
512
+ '@context': { acl: 'http://www.w3.org/ns/auth/acl#' },
513
+ '@graph': [
514
+ {
515
+ '@id': '#owner',
516
+ '@type': 'acl:Authorization',
517
+ 'acl:agent': { '@id': '#me' },
518
+ 'acl:accessTo': { '@id': './' },
519
+ 'acl:mode': [{ '@id': 'acl:Read' }, { '@id': 'acl:Write' }, { '@id': 'acl:Control' }]
520
+ }
521
+ ]
522
+ };
523
+
524
+ it('rejects text/turtle PUT to .acl with 415', async () => {
525
+ const turtle = `@prefix acl: <http://www.w3.org/ns/auth/acl#>. <#x> a acl:Authorization.`;
526
+ const res = await request('/noconneg/public/turtle-reject.acl', {
527
+ method: 'PUT',
528
+ headers: { 'Content-Type': 'text/turtle' },
529
+ body: turtle,
530
+ auth: 'noconneg'
531
+ });
532
+ assertStatus(res, 415);
533
+ assertHeaderContains(res, 'Accept', 'application/ld+json');
534
+ assertHeaderContains(res, 'Accept', 'application/json');
535
+ assertHeaderContains(res, 'Accept-Put', 'application/ld+json');
536
+ assertHeaderContains(res, 'Accept-Put', 'application/json');
537
+ });
538
+
539
+ it('rejects text/plain PUT to .acl with 415', async () => {
540
+ const res = await request('/noconneg/public/plain-reject.acl', {
541
+ method: 'PUT',
542
+ headers: { 'Content-Type': 'text/plain' },
543
+ body: 'arbitrary text',
544
+ auth: 'noconneg'
545
+ });
546
+ assertStatus(res, 415);
547
+ assertHeaderContains(res, 'Accept', 'application/ld+json');
548
+ assertHeaderContains(res, 'Accept', 'application/json');
549
+ assertHeaderContains(res, 'Accept-Put', 'application/ld+json');
550
+ assertHeaderContains(res, 'Accept-Put', 'application/json');
551
+ });
552
+
553
+ it('rejects PUT to .acl with no Content-Type with 415', async () => {
554
+ // Use Uint8Array body so fetch() doesn't auto-set Content-Type
555
+ // (which it does for string bodies: text/plain;charset=UTF-8).
556
+ const res = await request('/noconneg/public/no-ct-reject.acl', {
557
+ method: 'PUT',
558
+ body: new Uint8Array([1, 2, 3, 4]),
559
+ auth: 'noconneg'
560
+ });
561
+ assertStatus(res, 415);
562
+ assertHeaderContains(res, 'Accept', 'application/ld+json');
563
+ assertHeaderContains(res, 'Accept', 'application/json');
564
+ assertHeaderContains(res, 'Accept-Put', 'application/ld+json');
565
+ assertHeaderContains(res, 'Accept-Put', 'application/json');
566
+ });
567
+
568
+ it('accepts application/ld+json PUT to .acl', async () => {
569
+ const res = await request('/noconneg/public/jsonld-accept.acl', {
570
+ method: 'PUT',
571
+ headers: { 'Content-Type': 'application/ld+json' },
572
+ body: JSON.stringify(aclJsonLd),
573
+ auth: 'noconneg'
574
+ });
575
+ assert.ok(res.status < 300, `JSON-LD PUT to .acl should succeed, got ${res.status}`);
576
+ });
577
+
578
+ it('accepts application/json PUT to .acl', async () => {
579
+ const res = await request('/noconneg/public/json-accept.acl', {
580
+ method: 'PUT',
581
+ headers: { 'Content-Type': 'application/json' },
582
+ body: JSON.stringify(aclJsonLd),
583
+ auth: 'noconneg'
584
+ });
585
+ assert.ok(res.status < 300, `application/json PUT to .acl should succeed, got ${res.status}`);
586
+ });
355
587
  });
356
588
  });
357
589