javascript-solid-server 0.0.156 → 0.0.158
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 +1 -1
- package/src/auth/middleware.js +2 -2
- package/src/config.js +23 -0
- package/src/handlers/container.js +7 -2
- package/src/handlers/resource.js +24 -2
- package/src/rdf/conneg.js +18 -5
- package/test/config.test.js +95 -1
- package/test/conneg.test.js +232 -0
package/package.json
CHANGED
package/src/auth/middleware.js
CHANGED
|
@@ -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://
|
|
389
|
-
<a href="https://
|
|
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>
|
package/src/config.js
CHANGED
|
@@ -324,6 +324,29 @@ export async function loadConfig(cliOptions = {}, configFile = null) {
|
|
|
324
324
|
config.conneg = true;
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
// Single-user mode strongly implies the built-in IdP. Operators seeding
|
|
328
|
+
// a password for `me` on localhost almost always want the IdP enabled
|
|
329
|
+
// so clients can authenticate. Imply --idp unless the user explicitly
|
|
330
|
+
// disabled it from any config source — CLI, env, or config file (see #331).
|
|
331
|
+
if (config.singleUser && !config.idp) {
|
|
332
|
+
const idpExplicitlyDisabled =
|
|
333
|
+
cliOptions.idp === false ||
|
|
334
|
+
envConfig.idp === false ||
|
|
335
|
+
fileConfig.idp === false;
|
|
336
|
+
if (idpExplicitlyDisabled) {
|
|
337
|
+
// Respect the explicit disable. Warn only when there is no external
|
|
338
|
+
// --idp-issuer either: without the built-in IdP and without an
|
|
339
|
+
// external issuer, /.well-known/openid-configuration returns 404
|
|
340
|
+
// and clients fail with confusing OIDC discovery errors. If the
|
|
341
|
+
// operator pointed JSS at an external issuer, no footgun applies.
|
|
342
|
+
if (!config.idpIssuer) {
|
|
343
|
+
console.warn('⚠️ --single-user is enabled but --idp is disabled and no --idp-issuer is set. Clients won\'t be able to authenticate. Use --idp, or pass an external --idp-issuer.');
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
config.idp = true;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
327
350
|
// Validate SSL config
|
|
328
351
|
if ((config.sslKey && !config.sslCert) || (!config.sslKey && config.sslCert)) {
|
|
329
352
|
throw new Error('Both --ssl-key and --ssl-cert must be provided together');
|
|
@@ -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
|
|
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
|
|
package/src/handlers/resource.js
CHANGED
|
@@ -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
|
|
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
|
|
package/test/config.test.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* become a real boolean and break downstream code (bcrypt, etc.).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { describe, it, before, after } from 'node:test';
|
|
10
|
+
import { describe, it, before, after, beforeEach } from 'node:test';
|
|
11
11
|
import assert from 'node:assert';
|
|
12
12
|
import { loadConfig } from '../src/config.js';
|
|
13
13
|
|
|
@@ -64,3 +64,97 @@ describe('config — env var boolean coercion', () => {
|
|
|
64
64
|
assert.strictEqual(cfg2.multiuser, true);
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
|
+
|
|
68
|
+
// Regression coverage for #331: --single-user without --idp boots a server
|
|
69
|
+
// that returns 404 for /.well-known/openid-configuration, which is a
|
|
70
|
+
// footgun. loadConfig() now implies --idp when --single-user is set,
|
|
71
|
+
// unless the user explicitly disables IdP via --no-idp / JSS_IDP=false /
|
|
72
|
+
// idp:false in config file.
|
|
73
|
+
describe('config — --single-user implies --idp (#331)', () => {
|
|
74
|
+
// Hermetic env-var handling so the runner's environment doesn't leak.
|
|
75
|
+
// JSS_LOG_LEVEL is included because loadConfig() can emit a warning
|
|
76
|
+
// for invalid log levels and we don't want that to pollute assertions
|
|
77
|
+
// about the #331-specific warning.
|
|
78
|
+
const KEYS = ['JSS_IDP', 'JSS_SINGLE_USER', 'JSS_IDP_ISSUER', 'JSS_LOG_LEVEL'];
|
|
79
|
+
const original = {};
|
|
80
|
+
let originalWarn;
|
|
81
|
+
let warnings;
|
|
82
|
+
|
|
83
|
+
before(() => {
|
|
84
|
+
for (const k of KEYS) original[k] = process.env[k];
|
|
85
|
+
originalWarn = console.warn;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
after(() => {
|
|
89
|
+
for (const k of KEYS) {
|
|
90
|
+
if (original[k] === undefined) delete process.env[k];
|
|
91
|
+
else process.env[k] = original[k];
|
|
92
|
+
}
|
|
93
|
+
console.warn = originalWarn;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Capture warnings so we can assert on them without polluting test output.
|
|
97
|
+
// We filter to the #331-specific warning so unrelated warnings (e.g. from
|
|
98
|
+
// a noisy runner env) don't break the assertions.
|
|
99
|
+
const isIdpFootgunWarning = (msg) =>
|
|
100
|
+
msg.includes('--single-user') && msg.includes('--idp');
|
|
101
|
+
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
for (const k of KEYS) delete process.env[k];
|
|
104
|
+
warnings = [];
|
|
105
|
+
console.warn = (msg) => warnings.push(String(msg));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('implies --idp when --single-user is set and --idp is not specified', async () => {
|
|
109
|
+
const cfg = await loadConfig({ singleUser: true }, null);
|
|
110
|
+
assert.strictEqual(cfg.idp, true,
|
|
111
|
+
'--single-user should imply --idp by default');
|
|
112
|
+
assert.ok(!warnings.some(isIdpFootgunWarning),
|
|
113
|
+
'no #331 warning when implying (this is the happy path)');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('does not imply --idp when --single-user is not set', async () => {
|
|
117
|
+
const cfg = await loadConfig({}, null);
|
|
118
|
+
assert.notStrictEqual(cfg.idp, true,
|
|
119
|
+
'--idp should not be implied without --single-user');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('respects explicit --idp=true with --single-user', async () => {
|
|
123
|
+
const cfg = await loadConfig({ singleUser: true, idp: true }, null);
|
|
124
|
+
assert.strictEqual(cfg.idp, true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('respects explicit --no-idp with --single-user (warns but does not flip)', async () => {
|
|
128
|
+
const cfg = await loadConfig({ singleUser: true, idp: false }, null);
|
|
129
|
+
assert.strictEqual(cfg.idp, false,
|
|
130
|
+
'explicit --no-idp should override the implication');
|
|
131
|
+
assert.ok(warnings.some(isIdpFootgunWarning),
|
|
132
|
+
'should warn about the footgun when --single-user + --no-idp + no --idp-issuer');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('respects explicit JSS_IDP=false with --single-user (warns but does not flip)', async () => {
|
|
136
|
+
process.env.JSS_IDP = 'false';
|
|
137
|
+
const cfg = await loadConfig({ singleUser: true }, null);
|
|
138
|
+
assert.strictEqual(cfg.idp, false,
|
|
139
|
+
'explicit JSS_IDP=false should override the implication');
|
|
140
|
+
assert.ok(warnings.some(isIdpFootgunWarning),
|
|
141
|
+
'should warn when JSS_IDP=false + --single-user + no --idp-issuer');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('does not warn when --no-idp + --single-user but --idp-issuer is set', async () => {
|
|
145
|
+
const cfg = await loadConfig({
|
|
146
|
+
singleUser: true,
|
|
147
|
+
idp: false,
|
|
148
|
+
idpIssuer: 'https://external-issuer.example/'
|
|
149
|
+
}, null);
|
|
150
|
+
assert.strictEqual(cfg.idp, false);
|
|
151
|
+
assert.ok(!warnings.some(isIdpFootgunWarning),
|
|
152
|
+
'no footgun if an external --idp-issuer is configured');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('does not warn when --single-user is unset', async () => {
|
|
156
|
+
await loadConfig({ idp: false }, null);
|
|
157
|
+
assert.ok(!warnings.some(isIdpFootgunWarning),
|
|
158
|
+
'--no-idp without --single-user should not trigger the #331 warning');
|
|
159
|
+
});
|
|
160
|
+
});
|
package/test/conneg.test.js
CHANGED
|
@@ -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
|
|