javascript-solid-server 0.0.13 → 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.
Files changed (46) hide show
  1. package/.claude/settings.local.json +26 -1
  2. package/CTH.md +222 -0
  3. package/README.md +32 -1
  4. package/bin/jss.js +5 -1
  5. package/cth-config/application.yaml +2 -0
  6. package/cth-config/jss.ttl +6 -0
  7. package/cth-config/test-subjects.ttl +14 -0
  8. package/cth.env +19 -0
  9. package/package.json +1 -1
  10. package/scripts/test-cth-compat.js +3 -2
  11. package/src/auth/middleware.js +6 -2
  12. package/src/auth/token.js +44 -1
  13. package/src/handlers/container.js +8 -3
  14. package/src/handlers/resource.js +65 -4
  15. package/src/idp/accounts.js +11 -2
  16. package/src/idp/credentials.js +38 -38
  17. package/src/idp/index.js +112 -21
  18. package/src/idp/interactions.js +123 -11
  19. package/src/idp/provider.js +68 -2
  20. package/src/rdf/turtle.js +15 -2
  21. package/src/wac/parser.js +43 -1
  22. package/test/idp.test.js +17 -14
  23. package/test/ldp.test.js +10 -5
  24. package/test-data-idp-accounts/.idp/accounts/292738d6-3363-4f40-9a6b-884bfd17830a.json +9 -0
  25. package/test-data-idp-accounts/.idp/accounts/_email_index.json +3 -0
  26. package/test-data-idp-accounts/.idp/accounts/_webid_index.json +3 -0
  27. package/test-data-idp-accounts/.idp/keys/jwks.json +22 -0
  28. package/test-dpop-flow.js +148 -0
  29. package/test-subjects.ttl +21 -0
  30. package/data/alice/.acl +0 -50
  31. package/data/alice/inbox/.acl +0 -50
  32. package/data/alice/index.html +0 -80
  33. package/data/alice/private/.acl +0 -32
  34. package/data/alice/public/test.json +0 -1
  35. package/data/alice/settings/.acl +0 -32
  36. package/data/alice/settings/prefs +0 -17
  37. package/data/alice/settings/privateTypeIndex +0 -7
  38. package/data/alice/settings/publicTypeIndex +0 -7
  39. package/data/bob/.acl +0 -50
  40. package/data/bob/inbox/.acl +0 -50
  41. package/data/bob/index.html +0 -80
  42. package/data/bob/private/.acl +0 -32
  43. package/data/bob/settings/.acl +0 -32
  44. package/data/bob/settings/prefs +0 -17
  45. package/data/bob/settings/privateTypeIndex +0 -7
  46. package/data/bob/settings/publicTypeIndex +0 -7
@@ -26,7 +26,32 @@
26
26
  "Bash(npm view:*)",
27
27
  "Bash(npm ls:*)",
28
28
  "Bash(timeout 10 node:*)",
29
- "Bash(npm run test:cth:*)"
29
+ "Bash(npm run test:cth:*)",
30
+ "Bash(__NEW_LINE__ curl -s -X POST http://localhost:4000/.pods )",
31
+ "Bash(lsof:*)",
32
+ "Bash(xargs kill -9)",
33
+ "Bash(docker:*)",
34
+ "Bash(solidproject/conformance-test-harness )",
35
+ "Bash(timeout 30 node:*)",
36
+ "Bash(timeout 20 node:*)",
37
+ "Bash(timeout 25 node:*)",
38
+ "Bash(JSS_PORT=4000 JSS_CONNEG=true timeout 5 node:*)",
39
+ "Bash(pgrep:*)",
40
+ "Bash(python3:*)",
41
+ "Bash(ls:*)",
42
+ "Bash(timeout 15 node:*)",
43
+ "Bash(echo 'No .idp folder' echo find /home/melvin/remote/github.com/JavaScriptSolidServer/JavaScriptSolidServer/data/.idp/ -name *.json)",
44
+ "Bash(echo '=== Interactions ===' ls -la /home/melvin/remote/github.com/JavaScriptSolidServer/JavaScriptSolidServer/data/.idp/interaction/ echo echo '=== Latest interaction ===' cat /home/melvin/remote/github.com/JavaScriptSolidServer/JavaScriptSolidServer/data/.idp/interaction/*.json)",
45
+ "Bash(1 echo \"\" echo \"=== Server errors ===\" grep -E \"(error|Error)\" /tmp/jss.log)",
46
+ "Bash(echo 'Server not ready' curl -s -X POST http://localhost:4000/.pods -H 'Content-Type: application/json' -d {\"\"name\"\":\"\"alice\"\",\"\"email\"\":\"\"alice@example.com\"\",\"\"password\"\":\"\"alicepassword123\"\"})",
47
+ "Bash(head -1 curl -s -X POST http://localhost:4000/.pods -H \"Content-Type: application/json\" -d '{\"\"\"\"name\"\"\"\":\"\"\"\"bob\"\"\"\",\"\"\"\"email\"\"\"\":\"\"\"\"bob@example.com\"\"\"\",\"\"\"\"password\"\"\"\":\"\"\"\"bobpassword123\"\"\"\"}')",
48
+ "Bash(xargs:*)",
49
+ "Bash(fuser:*)",
50
+ "Bash(kill:*)",
51
+ "Bash(ACCESS_TOKEN=\"eyJhbGciOiJFUzI1NiIsImtpZCI6IjQwY2U0YzIzLWY2OWQtNDU4NS05ODg2LTE4MDQzZWIyZjU2ZCJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAvIiwic3ViIjoiYjhlZjY5YWUtODc0ZS00MDg5LThiMDktOGQwY2QyM2VlZWY3IiwiYXVkIjoic29saWQiLCJ3ZWJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q0MDAwL2FsaWNlLyNtZSIsImlhdCI6MTc2NjgyOTQ5MiwiZXhwIjoxNzY2ODMzMDkyLCJqdGkiOiIwMWY4ODVlZS05ZjY2LTQ3M2MtYmZkNC05MWM4ZGU3NGJhZjYiLCJjbGllbnRfaWQiOiJjcmVkZW50aWFsc19jbGllbnQiLCJzY29wZSI6Im9wZW5pZCB3ZWJpZCJ9.DYTlSRkORyDN28XtXk-zbR7xNLViD97KkPqUKb6chV860BaIgwa1suif4TxHQDnK_ejvbvmZ46_n5WwwRnf_Zw\" curl -sI -X PUT http://localhost:4000/alice/cth-test/ -H \"Content-Type: text/turtle\" -H \"Authorization: Bearer $ACCESS_TOKEN\")",
52
+ "Bash(timeout 60 docker run:*)",
53
+ "Bash(rm:*)",
54
+ "Bash(mkdir:*)"
30
55
  ]
31
56
  }
32
57
  }
package/CTH.md ADDED
@@ -0,0 +1,222 @@
1
+ # Running Solid Conformance Test Harness (CTH)
2
+
3
+ Step-by-step instructions for running the Solid Conformance Test Harness against this server.
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js 18+
8
+ - Docker
9
+ - Port 4000 available
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # 1. Kill any existing server on port 4000
15
+ fuser -k 4000/tcp 2>/dev/null || true
16
+
17
+ # 2. Clean data directory
18
+ rm -rf data && mkdir data
19
+
20
+ # 3. Start server with IdP and content negotiation
21
+ JSS_PORT=4000 JSS_CONNEG=true JSS_IDP=true node bin/jss.js start &
22
+
23
+ # 4. Wait for server to be ready
24
+ sleep 3
25
+ curl -s http://localhost:4000/ > /dev/null && echo "Server ready"
26
+
27
+ # 5. Create test users
28
+ curl -s -X POST http://localhost:4000/.pods \
29
+ -H "Content-Type: application/json" \
30
+ -d '{"name": "alice", "email": "alice@example.com", "password": "alicepassword123"}'
31
+
32
+ curl -s -X POST http://localhost:4000/.pods \
33
+ -H "Content-Type: application/json" \
34
+ -d '{"name": "bob", "email": "bob@example.com", "password": "bobpassword123"}'
35
+
36
+ # 6. Create test container (required by CTH)
37
+ mkdir -p data/alice/cth-test
38
+
39
+ # 7. Run authentication tests (assumes test-subjects.ttl and cth.env exist - see Configuration Files below)
40
+ docker run --rm --network=host \
41
+ -v $(pwd)/test-subjects.ttl:/app/test-subjects.ttl \
42
+ --env-file cth.env \
43
+ -e SUBJECTS=/app/test-subjects.ttl \
44
+ solidproject/conformance-test-harness:latest \
45
+ --target="https://github.com/solid/conformance-test-harness/jss" \
46
+ --filter="authentication"
47
+ ```
48
+
49
+ ## Configuration Files
50
+
51
+ ### Test Subjects File (test-subjects.ttl)
52
+
53
+ Create `test-subjects.ttl`:
54
+
55
+ ```turtle
56
+ @base <https://github.com/solid/conformance-test-harness/> .
57
+ @prefix solid-test: <https://github.com/solid/conformance-test-harness/vocab#> .
58
+ @prefix doap: <http://usefulinc.com/ns/doap#> .
59
+ @prefix earl: <http://www.w3.org/ns/earl#> .
60
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
61
+
62
+ <jss>
63
+ a earl:Software, earl:TestSubject ;
64
+ doap:name "JavaScript Solid Server"@en ;
65
+ doap:release <jss#test-subject-release> ;
66
+ doap:developer <https://github.com/JavaScriptSolidServer> ;
67
+ doap:homepage <https://github.com/JavaScriptSolidServer/JavaScriptSolidServer> ;
68
+ doap:description "A minimal, fast, JSON-LD native Solid server."@en ;
69
+ doap:programming-language "JavaScript"@en ;
70
+ solid-test:skip "acp", "wac", "wac-allow-public" .
71
+
72
+ <jss#test-subject-release>
73
+ doap:revision "0.0.14"@en ;
74
+ doap:created "2025-12-27"^^xsd:date .
75
+ ```
76
+
77
+ ### Environment File (cth.env)
78
+
79
+ Create `cth.env`:
80
+
81
+ ```bash
82
+ SERVER_ROOT=http://localhost:4000
83
+ TEST_CONTAINER=/alice/cth-test/
84
+ RESOURCE_SERVER_ROOT=http://localhost:4000
85
+ LOGIN_ENDPOINT=http://localhost:4000/idp/credentials
86
+ SOLID_IDENTITY_PROVIDER=http://localhost:4000/
87
+ USERS_ALICE_IDP=http://localhost:4000/
88
+ USERS_BOB_IDP=http://localhost:4000/
89
+ USERS_ALICE_WEBID=http://localhost:4000/alice/#me
90
+ USERS_BOB_WEBID=http://localhost:4000/bob/#me
91
+ USERS_ALICE_USERNAME=alice@example.com
92
+ USERS_ALICE_PASSWORD=alicepassword123
93
+ USERS_BOB_USERNAME=bob@example.com
94
+ USERS_BOB_PASSWORD=bobpassword123
95
+ ```
96
+
97
+ ### Environment Variables Reference
98
+
99
+ | Variable | Description | Example |
100
+ |----------|-------------|---------|
101
+ | `SERVER_ROOT` | Server base URL | `http://localhost:4000` |
102
+ | `TEST_CONTAINER` | Path to test container | `/alice/cth-test/` |
103
+ | `SOLID_IDENTITY_PROVIDER` | IdP issuer URL (with trailing slash) | `http://localhost:4000/` |
104
+ | `USERS_ALICE_IDP` | Alice's IdP | `http://localhost:4000/` |
105
+ | `USERS_ALICE_WEBID` | Alice's WebID | `http://localhost:4000/alice/#me` |
106
+ | `USERS_ALICE_USERNAME` | Alice's email | `alice@example.com` |
107
+ | `USERS_ALICE_PASSWORD` | Alice's password | `alicepassword123` |
108
+ | `USERS_BOB_IDP` | Bob's IdP | `http://localhost:4000/` |
109
+ | `USERS_BOB_WEBID` | Bob's WebID | `http://localhost:4000/bob/#me` |
110
+ | `USERS_BOB_USERNAME` | Bob's email | `bob@example.com` |
111
+ | `USERS_BOB_PASSWORD` | Bob's password | `bobpassword123` |
112
+ | `SUBJECTS` | Path to test-subjects.ttl inside container | `/app/test-subjects.ttl` |
113
+
114
+ ## Running Specific Test Suites
115
+
116
+ ### Authentication Tests (6 scenarios)
117
+
118
+ ```bash
119
+ docker run --rm --network=host \
120
+ --env-file cth.env \
121
+ -v $(pwd)/test-subjects.ttl:/app/test-subjects.ttl \
122
+ -e SUBJECTS=/app/test-subjects.ttl \
123
+ solidproject/conformance-test-harness:latest \
124
+ --target="https://github.com/solid/conformance-test-harness/jss" \
125
+ --filter="authentication"
126
+ ```
127
+
128
+ **Expected result:** 6/6 scenarios passing
129
+
130
+ ### All Protocol Tests
131
+
132
+ ```bash
133
+ docker run --rm --network=host \
134
+ --env-file cth.env \
135
+ -v $(pwd)/test-subjects.ttl:/app/test-subjects.ttl \
136
+ -e SUBJECTS=/app/test-subjects.ttl \
137
+ solidproject/conformance-test-harness:latest \
138
+ --target="https://github.com/solid/conformance-test-harness/jss"
139
+ ```
140
+
141
+ ## Interpreting Results
142
+
143
+ ### Success Output
144
+
145
+ ```
146
+ scenarios: 6 | passed: 6 | failed: 0 | time: 0.6349
147
+ MustFeatures passed: 1, failed: 0
148
+ MustScenarios passed: 6, failed: 0
149
+ ```
150
+
151
+ ### Failure Output
152
+
153
+ ```
154
+ scenarios: 6 | passed: 4 | failed: 2 | time: 0.7952
155
+ Then status 401
156
+ status code was: 200, expected: 401, response time in milliseconds: 15
157
+ ```
158
+
159
+ ## Troubleshooting
160
+
161
+ ### "Cannot get ACL url for root test container"
162
+
163
+ The test container doesn't exist. Create it:
164
+
165
+ ```bash
166
+ mkdir -p data/alice/cth-test
167
+ ```
168
+
169
+ ### "Failed to read WebID Document" (401)
170
+
171
+ The WebID profile is not publicly readable. Check that the pod's ACL allows public read on the container itself (but not necessarily on children).
172
+
173
+ ### "NullPointerException" during authentication
174
+
175
+ Usually means the IdP isn't returning proper responses. Check:
176
+ 1. Server is running with `--idp` flag
177
+ 2. Issuer URL has trailing slash
178
+ 3. Users were created with email and password
179
+
180
+ ### "DPoP htu mismatch"
181
+
182
+ URL mismatch in DPoP proof validation. Check that issuer URL doesn't have double slashes.
183
+
184
+ ### Token format errors
185
+
186
+ Ensure the server returns JWT access tokens with:
187
+ - `aud: "solid"` claim
188
+ - 3-part JWT format (header.payload.signature)
189
+ - `webid` claim
190
+
191
+ ## Server Requirements for CTH
192
+
193
+ The server must support:
194
+
195
+ 1. **Solid-OIDC Identity Provider**
196
+ - OIDC discovery at `/.well-known/openid-configuration`
197
+ - JWKS at `/.well-known/jwks.json`
198
+ - Dynamic client registration at `/idp/reg`
199
+ - Credentials endpoint at `/idp/credentials` (for programmatic login)
200
+
201
+ 2. **DPoP Token Binding**
202
+ - RS256 and ES256 algorithms
203
+ - Proper `cnf.jkt` claim in tokens
204
+
205
+ 3. **WWW-Authenticate Header**
206
+ - 401 responses must include `WWW-Authenticate: DPoP realm="..."`
207
+
208
+ 4. **Container Creation via PUT**
209
+ - PUT to path ending with `/` creates container
210
+
211
+ 5. **ACL Inheritance**
212
+ - Children should NOT inherit public read by default
213
+ - Only owner permissions should have `acl:default`
214
+
215
+ ## Current CTH Status (v0.0.14)
216
+
217
+ | Test Suite | Status |
218
+ |------------|--------|
219
+ | Authentication | 6/6 passing |
220
+ | Protocol (other) | Not yet tested |
221
+ | WAC | Skipped |
222
+ | ACP | Skipped |
package/README.md CHANGED
@@ -54,7 +54,7 @@ npm run benchmark
54
54
 
55
55
  ## Features
56
56
 
57
- ### Implemented (v0.0.13)
57
+ ### Implemented (v0.0.15)
58
58
 
59
59
  - **LDP CRUD Operations** - GET, PUT, POST, DELETE, HEAD
60
60
  - **N3 Patch** - Solid's native patch format for RDF updates
@@ -379,6 +379,37 @@ npm test
379
379
 
380
380
  Currently passing: **182 tests** (including 27 conformance tests)
381
381
 
382
+ ### Conformance Test Harness (CTH)
383
+
384
+ This server passes the Solid Conformance Test Harness authentication tests:
385
+
386
+ ```bash
387
+ # Start server with IdP and content negotiation
388
+ JSS_PORT=4000 JSS_CONNEG=true JSS_IDP=true jss start
389
+
390
+ # Create test users
391
+ curl -X POST http://localhost:4000/.pods \
392
+ -H "Content-Type: application/json" \
393
+ -d '{"name": "alice", "email": "alice@example.com", "password": "alicepassword123"}'
394
+
395
+ curl -X POST http://localhost:4000/.pods \
396
+ -H "Content-Type: application/json" \
397
+ -d '{"name": "bob", "email": "bob@example.com", "password": "bobpassword123"}'
398
+
399
+ # Run CTH authentication tests
400
+ docker run --rm --network=host \
401
+ -e SOLID_IDENTITY_PROVIDER="http://localhost:4000/" \
402
+ -e USERS_ALICE_WEBID="http://localhost:4000/alice/#me" \
403
+ -e USERS_ALICE_PASSWORD="alicepassword123" \
404
+ -e USERS_BOB_WEBID="http://localhost:4000/bob/#me" \
405
+ -e USERS_BOB_PASSWORD="bobpassword123" \
406
+ solidproject/conformance-test-harness:latest \
407
+ --filter="authentication"
408
+ ```
409
+
410
+ **CTH Status (v0.0.15):**
411
+ - Authentication tests: 6/6 passing
412
+
382
413
  ## Project Structure
383
414
 
384
415
  ```
package/bin/jss.js CHANGED
@@ -62,7 +62,11 @@ program
62
62
  const protocol = config.ssl ? 'https' : 'http';
63
63
  const serverHost = config.host === '0.0.0.0' ? 'localhost' : config.host;
64
64
  const baseUrl = `${protocol}://${serverHost}:${config.port}`;
65
- const idpIssuer = config.idpIssuer || baseUrl;
65
+ // Ensure issuer has trailing slash for CTH compatibility
66
+ let idpIssuer = config.idpIssuer || baseUrl;
67
+ if (idpIssuer && !idpIssuer.endsWith('/')) {
68
+ idpIssuer = idpIssuer + '/';
69
+ }
66
70
 
67
71
  // Create and start server
68
72
  const server = createServer({
@@ -0,0 +1,2 @@
1
+ subjects: file:/config/test-subjects.ttl
2
+ target: jss
@@ -0,0 +1,6 @@
1
+ @prefix test-harness: <https://github.com/solid-contrib/specification-tests/> .
2
+ @prefix solid-test: <https://github.com/solid-contrib/specification-tests/blob/main/vocab.ttl#> .
3
+
4
+ <jss>
5
+ a solid-test:TestSubject ;
6
+ solid-test:serverRoot <http://localhost:4000/> .
@@ -0,0 +1,14 @@
1
+ @prefix doap: <http://usefulinc.com/ns/doap#> .
2
+ @prefix earl: <http://www.w3.org/ns/earl#> .
3
+ @prefix solid-test: <https://github.com/solid-contrib/specification-tests/blob/main/vocab.ttl#> .
4
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
5
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
6
+
7
+ <jss>
8
+ a earl:Software, earl:TestSubject ;
9
+ doap:name "JavaScript Solid Server" ;
10
+ doap:description "A minimal, fast, JSON-LD native Solid server" ;
11
+ doap:programming-language "JavaScript" ;
12
+ solid-test:serverRoot <http://localhost:4000/> ;
13
+ solid-test:skip "acp" ;
14
+ rdfs:comment "Uses WAC for access control" .
package/cth.env ADDED
@@ -0,0 +1,19 @@
1
+ # CTH Environment for JSS
2
+ # Generated by test-cth-compat.js
3
+
4
+ SOLID_IDENTITY_PROVIDER=http://localhost:3456
5
+ RESOURCE_SERVER_ROOT=http://localhost:3456
6
+ TEST_CONTAINER=alice/public/
7
+
8
+ USERS_ALICE_WEBID=http://localhost:3456/alice/#me
9
+ USERS_ALICE_USERNAME=alice@test.local
10
+ USERS_ALICE_PASSWORD=alicepassword123
11
+
12
+ USERS_BOB_WEBID=http://localhost:3456/bob/#me
13
+ USERS_BOB_USERNAME=bob@test.local
14
+ USERS_BOB_PASSWORD=bobpassword123
15
+
16
+ LOGIN_ENDPOINT=http://localhost:3456/idp/credentials
17
+
18
+ # For self-signed certs
19
+ ALLOW_SELF_SIGNED_CERTS=true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -88,11 +88,12 @@ async function main() {
88
88
  const res = await fetch(`${BASE_URL}/.well-known/openid-configuration`);
89
89
  if (res.status === 200) {
90
90
  const config = await res.json();
91
- if (config.issuer === BASE_URL) {
91
+ // Issuer has trailing slash for CTH compatibility
92
+ if (config.issuer === BASE_URL + '/') {
92
93
  pass('/.well-known/openid-configuration returns valid config');
93
94
  passed++;
94
95
  } else {
95
- fail(`Issuer mismatch: expected ${BASE_URL}, got ${config.issuer}`);
96
+ fail(`Issuer mismatch: expected ${BASE_URL}/, got ${config.issuer}`);
96
97
  failed++;
97
98
  }
98
99
  } else {
@@ -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
- const issuer = baseUri;
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: `${issuer}/idp/auth`,
231
+ loginUrl: `${baseUri}/idp/auth`,
227
232
  });
228
233
  } catch (err) {
229
234
  console.error('Account creation error:', err);
@@ -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
- // Don't allow PUT to containers
222
+ // Handle container creation via PUT
181
223
  if (isContainer(urlPath)) {
182
- return reply.code(409).send({ error: 'Cannot PUT to container. Use POST instead.' });
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)) {
@@ -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 (scope.includes('profile')) {
254
+ if (hasScope('profile')) {
246
255
  result.name = account.podName;
247
256
  }
248
257
 
249
258
  // Email scope
250
- if (scope.includes('email')) {
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
  }