javascript-solid-server 0.0.12 → 0.0.15
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/.claude/settings.local.json +27 -1
- package/CTH.md +222 -0
- package/README.md +55 -2
- package/bin/jss.js +5 -1
- package/cth-config/application.yaml +2 -0
- package/cth-config/jss.ttl +6 -0
- package/cth-config/test-subjects.ttl +14 -0
- package/cth.env +19 -0
- package/package.json +2 -1
- package/scripts/test-cth-compat.js +370 -0
- package/src/auth/middleware.js +6 -2
- package/src/auth/token.js +44 -1
- package/src/handlers/container.js +8 -3
- package/src/handlers/resource.js +65 -4
- package/src/idp/accounts.js +11 -2
- package/src/idp/credentials.js +225 -0
- package/src/idp/index.js +129 -21
- package/src/idp/interactions.js +123 -11
- package/src/idp/provider.js +68 -2
- package/src/rdf/turtle.js +15 -2
- package/src/wac/parser.js +43 -1
- package/test/idp.test.js +173 -1
- package/test/ldp.test.js +10 -5
- package/test-data-idp-accounts/.idp/accounts/292738d6-3363-4f40-9a6b-884bfd17830a.json +9 -0
- package/test-data-idp-accounts/.idp/accounts/_email_index.json +3 -0
- package/test-data-idp-accounts/.idp/accounts/_webid_index.json +3 -0
- package/test-data-idp-accounts/.idp/keys/jwks.json +22 -0
- package/test-dpop-flow.js +148 -0
- package/test-subjects.ttl +21 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CTH Compatibility Test Script
|
|
4
|
+
*
|
|
5
|
+
* Tests that JSS is configured correctly for the Solid Conformance Test Harness.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node scripts/test-cth-compat.js [--port 3000] [--run-cth]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createServer } from '../src/server.js';
|
|
12
|
+
import fs from 'fs-extra';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
// Configuration
|
|
19
|
+
const PORT = parseInt(process.argv.find(a => a.startsWith('--port='))?.split('=')[1] || '3456');
|
|
20
|
+
const RUN_CTH = process.argv.includes('--run-cth');
|
|
21
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
22
|
+
const DATA_DIR = path.join(__dirname, '../.test-cth-data');
|
|
23
|
+
|
|
24
|
+
// Test users
|
|
25
|
+
const ALICE = {
|
|
26
|
+
name: 'alice',
|
|
27
|
+
email: 'alice@test.local',
|
|
28
|
+
password: 'alicepassword123',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const BOB = {
|
|
32
|
+
name: 'bob',
|
|
33
|
+
email: 'bob@test.local',
|
|
34
|
+
password: 'bobpassword123',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Colors for output
|
|
38
|
+
const GREEN = '\x1b[32m';
|
|
39
|
+
const RED = '\x1b[31m';
|
|
40
|
+
const YELLOW = '\x1b[33m';
|
|
41
|
+
const CYAN = '\x1b[36m';
|
|
42
|
+
const RESET = '\x1b[0m';
|
|
43
|
+
|
|
44
|
+
function log(msg, color = RESET) {
|
|
45
|
+
console.log(`${color}${msg}${RESET}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pass(msg) {
|
|
49
|
+
log(` ✓ ${msg}`, GREEN);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fail(msg) {
|
|
53
|
+
log(` ✗ ${msg}`, RED);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function info(msg) {
|
|
57
|
+
log(` ℹ ${msg}`, CYAN);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
log('\n=== JSS CTH Compatibility Test ===\n', CYAN);
|
|
62
|
+
|
|
63
|
+
let server;
|
|
64
|
+
let passed = 0;
|
|
65
|
+
let failed = 0;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// Clean up and prepare
|
|
69
|
+
await fs.remove(DATA_DIR);
|
|
70
|
+
await fs.ensureDir(DATA_DIR);
|
|
71
|
+
|
|
72
|
+
// Start server with IdP
|
|
73
|
+
log('Starting server with IdP enabled...', YELLOW);
|
|
74
|
+
server = createServer({
|
|
75
|
+
logger: false,
|
|
76
|
+
root: DATA_DIR,
|
|
77
|
+
idp: true,
|
|
78
|
+
idpIssuer: BASE_URL,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await server.listen({ port: PORT, host: 'localhost' });
|
|
82
|
+
pass(`Server running at ${BASE_URL}`);
|
|
83
|
+
passed++;
|
|
84
|
+
|
|
85
|
+
// Test 1: OIDC Discovery
|
|
86
|
+
log('\n1. OIDC Discovery', YELLOW);
|
|
87
|
+
{
|
|
88
|
+
const res = await fetch(`${BASE_URL}/.well-known/openid-configuration`);
|
|
89
|
+
if (res.status === 200) {
|
|
90
|
+
const config = await res.json();
|
|
91
|
+
// Issuer has trailing slash for CTH compatibility
|
|
92
|
+
if (config.issuer === BASE_URL + '/') {
|
|
93
|
+
pass('/.well-known/openid-configuration returns valid config');
|
|
94
|
+
passed++;
|
|
95
|
+
} else {
|
|
96
|
+
fail(`Issuer mismatch: expected ${BASE_URL}/, got ${config.issuer}`);
|
|
97
|
+
failed++;
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
fail(`OIDC discovery returned ${res.status}`);
|
|
101
|
+
failed++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Test 2: JWKS
|
|
106
|
+
log('\n2. JWKS Endpoint', YELLOW);
|
|
107
|
+
{
|
|
108
|
+
const res = await fetch(`${BASE_URL}/.well-known/jwks.json`);
|
|
109
|
+
if (res.status === 200) {
|
|
110
|
+
const jwks = await res.json();
|
|
111
|
+
if (jwks.keys?.length > 0) {
|
|
112
|
+
pass('/.well-known/jwks.json returns keys');
|
|
113
|
+
passed++;
|
|
114
|
+
} else {
|
|
115
|
+
fail('JWKS has no keys');
|
|
116
|
+
failed++;
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
fail(`JWKS returned ${res.status}`);
|
|
120
|
+
failed++;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Test 3: Create Alice
|
|
125
|
+
log('\n3. Create Test User: Alice', YELLOW);
|
|
126
|
+
let aliceWebId, aliceToken;
|
|
127
|
+
{
|
|
128
|
+
const res = await fetch(`${BASE_URL}/.pods`, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
131
|
+
body: JSON.stringify(ALICE),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (res.status === 201) {
|
|
135
|
+
const data = await res.json();
|
|
136
|
+
aliceWebId = data.webId;
|
|
137
|
+
pass(`Created Alice: ${aliceWebId}`);
|
|
138
|
+
passed++;
|
|
139
|
+
} else {
|
|
140
|
+
const err = await res.text();
|
|
141
|
+
fail(`Failed to create Alice: ${res.status} - ${err}`);
|
|
142
|
+
failed++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Test 4: Create Bob
|
|
147
|
+
log('\n4. Create Test User: Bob', YELLOW);
|
|
148
|
+
let bobWebId;
|
|
149
|
+
{
|
|
150
|
+
const res = await fetch(`${BASE_URL}/.pods`, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: { 'Content-Type': 'application/json' },
|
|
153
|
+
body: JSON.stringify(BOB),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (res.status === 201) {
|
|
157
|
+
const data = await res.json();
|
|
158
|
+
bobWebId = data.webId;
|
|
159
|
+
pass(`Created Bob: ${bobWebId}`);
|
|
160
|
+
passed++;
|
|
161
|
+
} else {
|
|
162
|
+
const err = await res.text();
|
|
163
|
+
fail(`Failed to create Bob: ${res.status} - ${err}`);
|
|
164
|
+
failed++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Test 5: Credentials endpoint - JSON
|
|
169
|
+
log('\n5. Credentials Endpoint (JSON)', YELLOW);
|
|
170
|
+
{
|
|
171
|
+
const res = await fetch(`${BASE_URL}/idp/credentials`, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: { 'Content-Type': 'application/json' },
|
|
174
|
+
body: JSON.stringify({ email: ALICE.email, password: ALICE.password }),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (res.status === 200) {
|
|
178
|
+
const data = await res.json();
|
|
179
|
+
if (data.access_token && data.webid) {
|
|
180
|
+
aliceToken = data.access_token;
|
|
181
|
+
pass(`Got token for Alice (type: ${data.token_type})`);
|
|
182
|
+
passed++;
|
|
183
|
+
} else {
|
|
184
|
+
fail('Missing access_token or webid in response');
|
|
185
|
+
failed++;
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
const err = await res.text();
|
|
189
|
+
fail(`Credentials endpoint failed: ${res.status} - ${err}`);
|
|
190
|
+
failed++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Test 6: Credentials endpoint - Form encoded
|
|
195
|
+
log('\n6. Credentials Endpoint (Form-encoded)', YELLOW);
|
|
196
|
+
{
|
|
197
|
+
const res = await fetch(`${BASE_URL}/idp/credentials`, {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
200
|
+
body: `email=${encodeURIComponent(BOB.email)}&password=${encodeURIComponent(BOB.password)}`,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (res.status === 200) {
|
|
204
|
+
const data = await res.json();
|
|
205
|
+
if (data.access_token) {
|
|
206
|
+
pass('Form-encoded credentials work');
|
|
207
|
+
passed++;
|
|
208
|
+
} else {
|
|
209
|
+
fail('Missing access_token in response');
|
|
210
|
+
failed++;
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
const err = await res.text();
|
|
214
|
+
fail(`Form-encoded credentials failed: ${res.status} - ${err}`);
|
|
215
|
+
failed++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Test 7: Invalid credentials
|
|
220
|
+
log('\n7. Invalid Credentials Handling', YELLOW);
|
|
221
|
+
{
|
|
222
|
+
const res = await fetch(`${BASE_URL}/idp/credentials`, {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: { 'Content-Type': 'application/json' },
|
|
225
|
+
body: JSON.stringify({ email: ALICE.email, password: 'wrongpassword' }),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (res.status === 401) {
|
|
229
|
+
pass('Returns 401 for invalid password');
|
|
230
|
+
passed++;
|
|
231
|
+
} else {
|
|
232
|
+
fail(`Expected 401, got ${res.status}`);
|
|
233
|
+
failed++;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Test 8: Token authentication
|
|
238
|
+
log('\n8. Token Authentication', YELLOW);
|
|
239
|
+
{
|
|
240
|
+
if (aliceToken) {
|
|
241
|
+
const res = await fetch(`${BASE_URL}/alice/`, {
|
|
242
|
+
headers: { 'Authorization': `Bearer ${aliceToken}` },
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (res.status === 200) {
|
|
246
|
+
pass('Token grants access to own pod');
|
|
247
|
+
passed++;
|
|
248
|
+
} else {
|
|
249
|
+
fail(`Token auth failed: ${res.status}`);
|
|
250
|
+
failed++;
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
fail('No token available to test');
|
|
254
|
+
failed++;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Test 9: Write with token
|
|
259
|
+
log('\n9. Write with Token', YELLOW);
|
|
260
|
+
{
|
|
261
|
+
if (aliceToken) {
|
|
262
|
+
const res = await fetch(`${BASE_URL}/alice/public/test.json`, {
|
|
263
|
+
method: 'PUT',
|
|
264
|
+
headers: {
|
|
265
|
+
'Authorization': `Bearer ${aliceToken}`,
|
|
266
|
+
'Content-Type': 'application/ld+json',
|
|
267
|
+
},
|
|
268
|
+
body: JSON.stringify({ '@id': '#test', 'http://example.org/value': 42 }),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if ([200, 201, 204].includes(res.status)) {
|
|
272
|
+
pass('Can write to pod with token');
|
|
273
|
+
passed++;
|
|
274
|
+
} else {
|
|
275
|
+
fail(`Write failed: ${res.status}`);
|
|
276
|
+
failed++;
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
fail('No token available to test');
|
|
280
|
+
failed++;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Test 10: Public read
|
|
285
|
+
log('\n10. Public Read Access', YELLOW);
|
|
286
|
+
{
|
|
287
|
+
const res = await fetch(`${BASE_URL}/alice/public/test.json`);
|
|
288
|
+
|
|
289
|
+
if (res.status === 200) {
|
|
290
|
+
pass('Public resources are readable');
|
|
291
|
+
passed++;
|
|
292
|
+
} else {
|
|
293
|
+
fail(`Public read failed: ${res.status}`);
|
|
294
|
+
failed++;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Summary
|
|
299
|
+
log('\n=== Summary ===\n', CYAN);
|
|
300
|
+
log(`Passed: ${passed}`, GREEN);
|
|
301
|
+
if (failed > 0) {
|
|
302
|
+
log(`Failed: ${failed}`, RED);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Generate CTH env file
|
|
306
|
+
log('\n=== CTH Configuration ===\n', CYAN);
|
|
307
|
+
|
|
308
|
+
const envContent = `# CTH Environment for JSS
|
|
309
|
+
# Generated by test-cth-compat.js
|
|
310
|
+
|
|
311
|
+
SOLID_IDENTITY_PROVIDER=${BASE_URL}
|
|
312
|
+
RESOURCE_SERVER_ROOT=${BASE_URL}
|
|
313
|
+
TEST_CONTAINER=alice/public/
|
|
314
|
+
|
|
315
|
+
USERS_ALICE_WEBID=${aliceWebId || `${BASE_URL}/alice/#me`}
|
|
316
|
+
USERS_ALICE_USERNAME=${ALICE.email}
|
|
317
|
+
USERS_ALICE_PASSWORD=${ALICE.password}
|
|
318
|
+
|
|
319
|
+
USERS_BOB_WEBID=${bobWebId || `${BASE_URL}/bob/#me`}
|
|
320
|
+
USERS_BOB_USERNAME=${BOB.email}
|
|
321
|
+
USERS_BOB_PASSWORD=${BOB.password}
|
|
322
|
+
|
|
323
|
+
LOGIN_ENDPOINT=${BASE_URL}/idp/credentials
|
|
324
|
+
|
|
325
|
+
# For self-signed certs
|
|
326
|
+
ALLOW_SELF_SIGNED_CERTS=true
|
|
327
|
+
`;
|
|
328
|
+
|
|
329
|
+
const envPath = path.join(__dirname, '../cth.env');
|
|
330
|
+
await fs.writeFile(envPath, envContent);
|
|
331
|
+
info(`CTH env file written to: ${envPath}`);
|
|
332
|
+
|
|
333
|
+
log('\nTo run CTH:', YELLOW);
|
|
334
|
+
log(`
|
|
335
|
+
# Keep JSS running with IdP:
|
|
336
|
+
JSS_PORT=${PORT} jss start --idp
|
|
337
|
+
|
|
338
|
+
# In another terminal, run CTH:
|
|
339
|
+
docker run -i --rm \\
|
|
340
|
+
--network host \\
|
|
341
|
+
-v "$(pwd)"/reports:/reports \\
|
|
342
|
+
--env-file=cth.env \\
|
|
343
|
+
solidproject/conformance-test-harness \\
|
|
344
|
+
--output=/reports \\
|
|
345
|
+
--target=jss
|
|
346
|
+
`);
|
|
347
|
+
|
|
348
|
+
if (RUN_CTH) {
|
|
349
|
+
log('\nRunning CTH (this may take a while)...', YELLOW);
|
|
350
|
+
// Would spawn docker here, but keeping server running is complex
|
|
351
|
+
info('CTH auto-run not implemented yet. Run manually with commands above.');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
} catch (err) {
|
|
355
|
+
fail(`Error: ${err.message}`);
|
|
356
|
+
console.error(err);
|
|
357
|
+
failed++;
|
|
358
|
+
} finally {
|
|
359
|
+
// Cleanup
|
|
360
|
+
if (server) {
|
|
361
|
+
await server.close();
|
|
362
|
+
}
|
|
363
|
+
await fs.remove(DATA_DIR);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Exit with error code if any tests failed
|
|
367
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
main();
|
package/src/auth/middleware.js
CHANGED
|
@@ -79,12 +79,16 @@ function getParentPath(path) {
|
|
|
79
79
|
* @param {boolean} isAuthenticated - Whether user is authenticated
|
|
80
80
|
* @param {string} wacAllow - WAC-Allow header value
|
|
81
81
|
* @param {string|null} authError - Authentication error message (for DPoP failures)
|
|
82
|
+
* @param {string|null} issuer - IdP issuer URL for WWW-Authenticate header
|
|
82
83
|
*/
|
|
83
|
-
export function handleUnauthorized(reply, isAuthenticated, wacAllow, authError = null) {
|
|
84
|
+
export function handleUnauthorized(reply, isAuthenticated, wacAllow, authError = null, issuer = null) {
|
|
84
85
|
reply.header('WAC-Allow', wacAllow);
|
|
85
86
|
|
|
86
87
|
if (!isAuthenticated) {
|
|
87
|
-
// Not authenticated - return 401
|
|
88
|
+
// Not authenticated - return 401 with WWW-Authenticate header
|
|
89
|
+
// Solid-OIDC requires DPoP authentication
|
|
90
|
+
const realm = issuer || 'Solid';
|
|
91
|
+
reply.header('WWW-Authenticate', `DPoP realm="${realm}", Bearer realm="${realm}"`);
|
|
88
92
|
return reply.code(401).send({
|
|
89
93
|
error: 'Unauthorized',
|
|
90
94
|
message: authError || 'Authentication required'
|
package/src/auth/token.js
CHANGED
|
@@ -35,7 +35,7 @@ export function createToken(webId, expiresIn = 3600) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Verify and decode a token
|
|
38
|
+
* Verify and decode a token (simple 2-part or JWT 3-part)
|
|
39
39
|
* @param {string} token - The token to verify
|
|
40
40
|
* @returns {{webId: string, iat: number, exp: number} | null} Decoded payload or null
|
|
41
41
|
*/
|
|
@@ -45,6 +45,12 @@ export function verifyToken(token) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const parts = token.split('.');
|
|
48
|
+
|
|
49
|
+
// Handle JWT tokens (3 parts) from credentials endpoint
|
|
50
|
+
if (parts.length === 3) {
|
|
51
|
+
return verifyJwtToken(token);
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
if (parts.length !== 2) {
|
|
49
55
|
return null;
|
|
50
56
|
}
|
|
@@ -76,6 +82,43 @@ export function verifyToken(token) {
|
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Verify a JWT token from credentials endpoint
|
|
87
|
+
* JWT tokens are self-contained and signed with the IdP's private key
|
|
88
|
+
* @param {string} token - JWT token
|
|
89
|
+
* @returns {{webId: string, iat: number, exp: number} | null} Decoded payload or null
|
|
90
|
+
*/
|
|
91
|
+
function verifyJwtToken(token) {
|
|
92
|
+
try {
|
|
93
|
+
const parts = token.split('.');
|
|
94
|
+
if (parts.length !== 3) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Decode the payload (middle part)
|
|
99
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
100
|
+
|
|
101
|
+
// Check expiration
|
|
102
|
+
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// JWT from credentials endpoint uses 'webid' claim (lowercase)
|
|
107
|
+
if (payload.webid) {
|
|
108
|
+
return { webId: payload.webid, iat: payload.iat, exp: payload.exp };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Also check uppercase WebId for compatibility
|
|
112
|
+
if (payload.webId) {
|
|
113
|
+
return payload;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
79
122
|
/**
|
|
80
123
|
* Extract token from Authorization header
|
|
81
124
|
* @param {string} authHeader - Authorization header value
|
|
@@ -2,7 +2,7 @@ import * as storage from '../storage/filesystem.js';
|
|
|
2
2
|
import { getAllHeaders } from '../ldp/headers.js';
|
|
3
3
|
import { isContainer } from '../utils/url.js';
|
|
4
4
|
import { generateProfile, generatePreferences, generateTypeIndex, serialize } from '../webid/profile.js';
|
|
5
|
-
import { generateOwnerAcl, generatePrivateAcl, generateInboxAcl, serializeAcl } from '../wac/parser.js';
|
|
5
|
+
import { generateOwnerAcl, generatePrivateAcl, generateInboxAcl, generatePublicFolderAcl, serializeAcl } from '../wac/parser.js';
|
|
6
6
|
import { createToken } from '../auth/token.js';
|
|
7
7
|
import { canAcceptInput, toJsonLd, getVaryHeader, RDF_TYPES } from '../rdf/conneg.js';
|
|
8
8
|
import { emitChange } from '../notifications/events.js';
|
|
@@ -157,7 +157,8 @@ export async function handleCreatePod(request, reply) {
|
|
|
157
157
|
const baseUri = `${request.protocol}://${request.hostname}`;
|
|
158
158
|
const podUri = `${baseUri}${podPath}`;
|
|
159
159
|
const webId = `${podUri}#me`;
|
|
160
|
-
|
|
160
|
+
// Issuer needs trailing slash for CTH compatibility
|
|
161
|
+
const issuer = baseUri + '/';
|
|
161
162
|
|
|
162
163
|
try {
|
|
163
164
|
// Create pod directory structure
|
|
@@ -199,6 +200,10 @@ export async function handleCreatePod(request, reply) {
|
|
|
199
200
|
const inboxAcl = generateInboxAcl(`${podUri}inbox/`, webId);
|
|
200
201
|
await storage.write(`${podPath}inbox/.acl`, serializeAcl(inboxAcl));
|
|
201
202
|
|
|
203
|
+
// Public folder: owner full, public read (with inheritance)
|
|
204
|
+
const publicAcl = generatePublicFolderAcl(`${podUri}public/`, webId);
|
|
205
|
+
await storage.write(`${podPath}public/.acl`, serializeAcl(publicAcl));
|
|
206
|
+
|
|
202
207
|
} catch (err) {
|
|
203
208
|
console.error('Pod creation error:', err);
|
|
204
209
|
// Cleanup on failure
|
|
@@ -223,7 +228,7 @@ export async function handleCreatePod(request, reply) {
|
|
|
223
228
|
webId,
|
|
224
229
|
podUri,
|
|
225
230
|
idpIssuer: issuer,
|
|
226
|
-
loginUrl: `${
|
|
231
|
+
loginUrl: `${baseUri}/idp/auth`,
|
|
227
232
|
});
|
|
228
233
|
} catch (err) {
|
|
229
234
|
console.error('Account creation error:', err);
|
package/src/handlers/resource.js
CHANGED
|
@@ -51,6 +51,46 @@ export async function handleGet(request, reply) {
|
|
|
51
51
|
const content = await storage.read(indexPath);
|
|
52
52
|
const indexStats = await storage.stat(indexPath);
|
|
53
53
|
|
|
54
|
+
// Check if RDF format requested via content negotiation
|
|
55
|
+
const acceptHeader = request.headers.accept || '';
|
|
56
|
+
const wantsTurtle = connegEnabled && (
|
|
57
|
+
acceptHeader.includes('text/turtle') ||
|
|
58
|
+
acceptHeader.includes('text/n3') ||
|
|
59
|
+
acceptHeader.includes('application/n-triples')
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (wantsTurtle) {
|
|
63
|
+
// Extract JSON-LD from HTML and convert to Turtle
|
|
64
|
+
try {
|
|
65
|
+
const htmlStr = content.toString();
|
|
66
|
+
const jsonLdMatch = htmlStr.match(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/);
|
|
67
|
+
if (jsonLdMatch) {
|
|
68
|
+
const jsonLd = JSON.parse(jsonLdMatch[1]);
|
|
69
|
+
const { content: turtleContent } = await fromJsonLd(
|
|
70
|
+
jsonLd,
|
|
71
|
+
'text/turtle',
|
|
72
|
+
resourceUrl,
|
|
73
|
+
true
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const headers = getAllHeaders({
|
|
77
|
+
isContainer: true,
|
|
78
|
+
etag: indexStats?.etag || stats.etag,
|
|
79
|
+
contentType: 'text/turtle',
|
|
80
|
+
origin,
|
|
81
|
+
resourceUrl,
|
|
82
|
+
connegEnabled
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
86
|
+
return reply.send(turtleContent);
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
// Fall through to serve HTML if conversion fails
|
|
90
|
+
console.error('Failed to convert profile to Turtle:', err.message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
54
94
|
const headers = getAllHeaders({
|
|
55
95
|
isContainer: true,
|
|
56
96
|
etag: indexStats?.etag || stats.etag,
|
|
@@ -176,15 +216,36 @@ export async function handleHead(request, reply) {
|
|
|
176
216
|
*/
|
|
177
217
|
export async function handlePut(request, reply) {
|
|
178
218
|
const urlPath = request.url.split('?')[0];
|
|
219
|
+
const connegEnabled = request.connegEnabled || false;
|
|
220
|
+
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
|
|
179
221
|
|
|
180
|
-
//
|
|
222
|
+
// Handle container creation via PUT
|
|
181
223
|
if (isContainer(urlPath)) {
|
|
182
|
-
|
|
224
|
+
const stats = await storage.stat(urlPath);
|
|
225
|
+
if (stats?.isDirectory) {
|
|
226
|
+
// Container already exists - don't allow PUT to modify
|
|
227
|
+
return reply.code(409).send({ error: 'Cannot PUT to existing container' });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Create the container (and any intermediate containers)
|
|
231
|
+
const success = await storage.createContainer(urlPath);
|
|
232
|
+
if (!success) {
|
|
233
|
+
return reply.code(500).send({ error: 'Failed to create container' });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const origin = request.headers.origin;
|
|
237
|
+
const headers = getAllHeaders({
|
|
238
|
+
isContainer: true,
|
|
239
|
+
origin,
|
|
240
|
+
connegEnabled
|
|
241
|
+
});
|
|
242
|
+
headers['Location'] = resourceUrl;
|
|
243
|
+
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
244
|
+
emitChange(request.protocol + '://' + request.hostname, urlPath, 'created');
|
|
245
|
+
return reply.code(201).send();
|
|
183
246
|
}
|
|
184
247
|
|
|
185
|
-
const connegEnabled = request.connegEnabled || false;
|
|
186
248
|
const contentType = request.headers['content-type'] || '';
|
|
187
|
-
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
|
|
188
249
|
|
|
189
250
|
// Check if we can accept this input type
|
|
190
251
|
if (!canAcceptInput(contentType, connegEnabled)) {
|
package/src/idp/accounts.js
CHANGED
|
@@ -241,13 +241,22 @@ export async function getAccountForProvider(id) {
|
|
|
241
241
|
// Always include webid for Solid-OIDC
|
|
242
242
|
result.webid = account.webId;
|
|
243
243
|
|
|
244
|
+
// Handle scope being a string, array, Set, or object with keys
|
|
245
|
+
const hasScope = (s) => {
|
|
246
|
+
if (typeof scope === 'string') return scope.includes(s);
|
|
247
|
+
if (Array.isArray(scope)) return scope.includes(s);
|
|
248
|
+
if (scope instanceof Set) return scope.has(s);
|
|
249
|
+
if (scope && typeof scope === 'object') return s in scope || Object.keys(scope).includes(s);
|
|
250
|
+
return false;
|
|
251
|
+
};
|
|
252
|
+
|
|
244
253
|
// Profile scope
|
|
245
|
-
if (
|
|
254
|
+
if (hasScope('profile')) {
|
|
246
255
|
result.name = account.podName;
|
|
247
256
|
}
|
|
248
257
|
|
|
249
258
|
// Email scope
|
|
250
|
-
if (
|
|
259
|
+
if (hasScope('email')) {
|
|
251
260
|
result.email = account.email;
|
|
252
261
|
result.email_verified = false; // We don't have email verification yet
|
|
253
262
|
}
|