javascript-solid-server 0.0.13 → 0.0.16

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 (49) hide show
  1. package/.claude/settings.local.json +27 -1
  2. package/CTH.md +222 -0
  3. package/README.md +92 -3
  4. package/bin/jss.js +11 -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 +17 -7
  12. package/src/auth/token.js +44 -1
  13. package/src/config.js +7 -0
  14. package/src/handlers/container.js +49 -16
  15. package/src/handlers/resource.js +99 -32
  16. package/src/idp/accounts.js +11 -2
  17. package/src/idp/credentials.js +38 -38
  18. package/src/idp/index.js +112 -21
  19. package/src/idp/interactions.js +123 -11
  20. package/src/idp/provider.js +68 -2
  21. package/src/rdf/turtle.js +15 -2
  22. package/src/server.js +24 -0
  23. package/src/utils/url.js +52 -0
  24. package/src/wac/parser.js +43 -1
  25. package/test/idp.test.js +17 -14
  26. package/test/ldp.test.js +10 -5
  27. package/test-data-idp-accounts/.idp/accounts/3c1cd503-1d7f-4ba0-a3af-ebedf519594d.json +9 -0
  28. package/test-data-idp-accounts/.idp/accounts/_email_index.json +3 -0
  29. package/test-data-idp-accounts/.idp/accounts/_webid_index.json +3 -0
  30. package/test-data-idp-accounts/.idp/keys/jwks.json +22 -0
  31. package/test-dpop-flow.js +148 -0
  32. package/test-subjects.ttl +21 -0
  33. package/data/alice/.acl +0 -50
  34. package/data/alice/inbox/.acl +0 -50
  35. package/data/alice/index.html +0 -80
  36. package/data/alice/private/.acl +0 -32
  37. package/data/alice/public/test.json +0 -1
  38. package/data/alice/settings/.acl +0 -32
  39. package/data/alice/settings/prefs +0 -17
  40. package/data/alice/settings/privateTypeIndex +0 -7
  41. package/data/alice/settings/publicTypeIndex +0 -7
  42. package/data/bob/.acl +0 -50
  43. package/data/bob/inbox/.acl +0 -50
  44. package/data/bob/index.html +0 -80
  45. package/data/bob/private/.acl +0 -32
  46. package/data/bob/settings/.acl +0 -32
  47. package/data/bob/settings/prefs +0 -17
  48. package/data/bob/settings/privateTypeIndex +0 -7
  49. package/data/bob/settings/publicTypeIndex +0 -7
@@ -26,7 +26,33 @@
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:*)",
55
+ "WebFetch(domain:communitysolidserver.github.io)"
30
56
  ]
31
57
  }
32
58
  }
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.16)
58
58
 
59
59
  - **LDP CRUD Operations** - GET, PUT, POST, DELETE, HEAD
60
60
  - **N3 Patch** - Solid's native patch format for RDF updates
@@ -64,7 +64,8 @@ npm run benchmark
64
64
  - **SSL/TLS** - HTTPS support with certificate configuration
65
65
  - **WebSocket Notifications** - Real-time updates via solid-0.1 protocol (SolidOS compatible)
66
66
  - **Container Management** - Create, list, and manage containers
67
- - **Multi-user Pods** - Create pods at `/<username>/`
67
+ - **Multi-user Pods** - Path-based (`/alice/`) or subdomain-based (`alice.example.com`)
68
+ - **Subdomain Mode** - XSS protection via origin isolation
68
69
  - **WebID Profiles** - JSON-LD structured data in HTML at pod root
69
70
  - **Web Access Control (WAC)** - `.acl` file-based authorization
70
71
  - **Solid-OIDC Identity Provider** - Built-in IdP with DPoP, dynamic registration
@@ -135,6 +136,8 @@ jss --help # Show help
135
136
  | `--notifications` | Enable WebSocket | false |
136
137
  | `--idp` | Enable built-in IdP | false |
137
138
  | `--idp-issuer <url>` | IdP issuer URL | (auto) |
139
+ | `--subdomains` | Enable subdomain-based pods | false |
140
+ | `--base-domain <domain>` | Base domain for subdomains | - |
138
141
  | `-q, --quiet` | Suppress logs | false |
139
142
 
140
143
  ### Environment Variables
@@ -146,6 +149,8 @@ export JSS_PORT=8443
146
149
  export JSS_SSL_KEY=/path/to/key.pem
147
150
  export JSS_SSL_CERT=/path/to/cert.pem
148
151
  export JSS_CONNEG=true
152
+ export JSS_SUBDOMAINS=true
153
+ export JSS_BASE_DOMAIN=example.com
149
154
  jss start
150
155
  ```
151
156
 
@@ -338,13 +343,66 @@ curl -H "Authorization: DPoP ACCESS_TOKEN" \
338
343
  http://localhost:3000/alice/private/
339
344
  ```
340
345
 
346
+ ## Subdomain Mode (XSS Protection)
347
+
348
+ By default, JSS uses **path-based pods** (`/alice/`, `/bob/`). This is simple but has a security limitation: all pods share the same origin, making cross-site scripting (XSS) attacks possible between pods.
349
+
350
+ **Subdomain mode** provides **origin isolation** - each pod gets its own subdomain (`alice.example.com`, `bob.example.com`), preventing XSS attacks between pods.
351
+
352
+ ### Why Subdomain Mode?
353
+
354
+ | Mode | URL | Origin | XSS Risk |
355
+ |------|-----|--------|----------|
356
+ | Path-based | `example.com/alice/` | `example.com` | Shared origin - pods can XSS each other |
357
+ | Subdomain | `alice.example.com/` | `alice.example.com` | Isolated - browser's Same-Origin Policy protects |
358
+
359
+ ### Enabling Subdomain Mode
360
+
361
+ ```bash
362
+ jss start --subdomains --base-domain example.com
363
+ ```
364
+
365
+ Or via environment variables:
366
+
367
+ ```bash
368
+ export JSS_SUBDOMAINS=true
369
+ export JSS_BASE_DOMAIN=example.com
370
+ jss start
371
+ ```
372
+
373
+ ### DNS Configuration
374
+
375
+ You need a **wildcard DNS record** pointing to your server:
376
+
377
+ ```
378
+ *.example.com A <your-server-ip>
379
+ ```
380
+
381
+ ### Pod URLs in Subdomain Mode
382
+
383
+ | Path Mode | Subdomain Mode |
384
+ |-----------|----------------|
385
+ | `example.com/alice/` | `alice.example.com/` |
386
+ | `example.com/alice/public/file.txt` | `alice.example.com/public/file.txt` |
387
+ | `example.com/alice/#me` | `alice.example.com/#me` |
388
+
389
+ Pod creation still uses the main domain:
390
+
391
+ ```bash
392
+ curl -X POST https://example.com/.pods \
393
+ -H "Content-Type: application/json" \
394
+ -d '{"name": "alice"}'
395
+ ```
396
+
341
397
  ## Configuration
342
398
 
343
399
  ```javascript
344
400
  createServer({
345
401
  logger: true, // Enable Fastify logging (default: true)
346
402
  conneg: false, // Enable content negotiation (default: false)
347
- notifications: false // Enable WebSocket notifications (default: false)
403
+ notifications: false, // Enable WebSocket notifications (default: false)
404
+ subdomains: false, // Enable subdomain-based pods (default: false)
405
+ baseDomain: null, // Base domain for subdomains (e.g., "example.com")
348
406
  });
349
407
  ```
350
408
 
@@ -379,6 +437,37 @@ npm test
379
437
 
380
438
  Currently passing: **182 tests** (including 27 conformance tests)
381
439
 
440
+ ### Conformance Test Harness (CTH)
441
+
442
+ This server passes the Solid Conformance Test Harness authentication tests:
443
+
444
+ ```bash
445
+ # Start server with IdP and content negotiation
446
+ JSS_PORT=4000 JSS_CONNEG=true JSS_IDP=true jss start
447
+
448
+ # Create test users
449
+ curl -X POST http://localhost:4000/.pods \
450
+ -H "Content-Type: application/json" \
451
+ -d '{"name": "alice", "email": "alice@example.com", "password": "alicepassword123"}'
452
+
453
+ curl -X POST http://localhost:4000/.pods \
454
+ -H "Content-Type: application/json" \
455
+ -d '{"name": "bob", "email": "bob@example.com", "password": "bobpassword123"}'
456
+
457
+ # Run CTH authentication tests
458
+ docker run --rm --network=host \
459
+ -e SOLID_IDENTITY_PROVIDER="http://localhost:4000/" \
460
+ -e USERS_ALICE_WEBID="http://localhost:4000/alice/#me" \
461
+ -e USERS_ALICE_PASSWORD="alicepassword123" \
462
+ -e USERS_BOB_WEBID="http://localhost:4000/bob/#me" \
463
+ -e USERS_BOB_PASSWORD="bobpassword123" \
464
+ solidproject/conformance-test-harness:latest \
465
+ --filter="authentication"
466
+ ```
467
+
468
+ **CTH Status (v0.0.15):**
469
+ - Authentication tests: 6/6 passing
470
+
382
471
  ## Project Structure
383
472
 
384
473
  ```
package/bin/jss.js CHANGED
@@ -47,6 +47,9 @@ program
47
47
  .option('--idp', 'Enable built-in Identity Provider')
48
48
  .option('--no-idp', 'Disable built-in Identity Provider')
49
49
  .option('--idp-issuer <url>', 'IdP issuer URL (defaults to server URL)')
50
+ .option('--subdomains', 'Enable subdomain-based pods (XSS protection)')
51
+ .option('--no-subdomains', 'Disable subdomain-based pods')
52
+ .option('--base-domain <domain>', 'Base domain for subdomain pods (e.g., "example.com")')
50
53
  .option('-q, --quiet', 'Suppress log output')
51
54
  .option('--print-config', 'Print configuration and exit')
52
55
  .action(async (options) => {
@@ -62,7 +65,11 @@ program
62
65
  const protocol = config.ssl ? 'https' : 'http';
63
66
  const serverHost = config.host === '0.0.0.0' ? 'localhost' : config.host;
64
67
  const baseUrl = `${protocol}://${serverHost}:${config.port}`;
65
- const idpIssuer = config.idpIssuer || baseUrl;
68
+ // Ensure issuer has trailing slash for CTH compatibility
69
+ let idpIssuer = config.idpIssuer || baseUrl;
70
+ if (idpIssuer && !idpIssuer.endsWith('/')) {
71
+ idpIssuer = idpIssuer + '/';
72
+ }
66
73
 
67
74
  // Create and start server
68
75
  const server = createServer({
@@ -76,6 +83,8 @@ program
76
83
  cert: await fs.readFile(config.sslCert),
77
84
  } : null,
78
85
  root: config.root,
86
+ subdomains: config.subdomains,
87
+ baseDomain: config.baseDomain,
79
88
  });
80
89
 
81
90
  await server.listen({ port: config.port, host: config.host });
@@ -88,6 +97,7 @@ program
88
97
  if (config.conneg) console.log(' Conneg: enabled');
89
98
  if (config.notifications) console.log(' WebSocket: enabled');
90
99
  if (config.idp) console.log(` IdP: ${idpIssuer}`);
100
+ if (config.subdomains) console.log(` Subdomains: ${config.baseDomain} (XSS protection enabled)`);
91
101
  console.log('\n Press Ctrl+C to stop\n');
92
102
  }
93
103
 
@@ -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.16",
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 {
@@ -7,6 +7,7 @@
7
7
  import { getWebIdFromRequestAsync } from './token.js';
8
8
  import { checkAccess, getRequiredMode } from '../wac/checker.js';
9
9
  import * as storage from '../storage/filesystem.js';
10
+ import { getEffectiveUrlPath } from '../utils/url.js';
10
11
 
11
12
  /**
12
13
  * Check if request is authorized
@@ -27,27 +28,32 @@ export async function authorize(request, reply) {
27
28
  // Get WebID from token (supports both simple and Solid-OIDC tokens)
28
29
  const { webId, error: authError } = await getWebIdFromRequestAsync(request);
29
30
 
31
+ // Get effective storage path (includes pod name in subdomain mode)
32
+ const storagePath = getEffectiveUrlPath(request);
33
+
30
34
  // Get resource info
31
- const stats = await storage.stat(urlPath);
35
+ const stats = await storage.stat(storagePath);
32
36
  const resourceExists = stats !== null;
33
37
  const isContainer = stats?.isDirectory || urlPath.endsWith('/');
34
38
 
35
- // Build resource URL
39
+ // Build resource URL (uses actual request hostname which may be subdomain)
36
40
  const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
37
41
 
38
42
  // Get required access mode for this method
39
43
  const requiredMode = getRequiredMode(method);
40
44
 
41
45
  // For write operations on non-existent resources, check parent container
42
- let checkPath = urlPath;
46
+ let checkPath = storagePath;
43
47
  let checkUrl = resourceUrl;
44
48
  let checkIsContainer = isContainer;
45
49
 
46
50
  if (!resourceExists && (method === 'PUT' || method === 'POST' || method === 'PATCH')) {
47
51
  // Check write permission on parent container
48
- const parentPath = getParentPath(urlPath);
52
+ const parentPath = getParentPath(storagePath);
49
53
  checkPath = parentPath;
50
- checkUrl = `${request.protocol}://${request.hostname}${parentPath}`;
54
+ // For URL, also need to get parent
55
+ const parentUrlPath = getParentPath(urlPath);
56
+ checkUrl = `${request.protocol}://${request.hostname}${parentUrlPath}`;
51
57
  checkIsContainer = true;
52
58
  }
53
59
 
@@ -79,12 +85,16 @@ function getParentPath(path) {
79
85
  * @param {boolean} isAuthenticated - Whether user is authenticated
80
86
  * @param {string} wacAllow - WAC-Allow header value
81
87
  * @param {string|null} authError - Authentication error message (for DPoP failures)
88
+ * @param {string|null} issuer - IdP issuer URL for WWW-Authenticate header
82
89
  */
83
- export function handleUnauthorized(reply, isAuthenticated, wacAllow, authError = null) {
90
+ export function handleUnauthorized(reply, isAuthenticated, wacAllow, authError = null, issuer = null) {
84
91
  reply.header('WAC-Allow', wacAllow);
85
92
 
86
93
  if (!isAuthenticated) {
87
- // Not authenticated - return 401
94
+ // Not authenticated - return 401 with WWW-Authenticate header
95
+ // Solid-OIDC requires DPoP authentication
96
+ const realm = issuer || 'Solid';
97
+ reply.header('WWW-Authenticate', `DPoP realm="${realm}", Bearer realm="${realm}"`);
88
98
  return reply.code(401).send({
89
99
  error: 'Unauthorized',
90
100
  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
package/src/config.js CHANGED
@@ -33,6 +33,10 @@ export const defaults = {
33
33
  idp: false,
34
34
  idpIssuer: null,
35
35
 
36
+ // Subdomain mode (XSS protection)
37
+ subdomains: false,
38
+ baseDomain: null,
39
+
36
40
  // Logging
37
41
  logger: true,
38
42
  quiet: false,
@@ -57,6 +61,8 @@ const envMap = {
57
61
  JSS_CONFIG_PATH: 'configPath',
58
62
  JSS_IDP: 'idp',
59
63
  JSS_IDP_ISSUER: 'idpIssuer',
64
+ JSS_SUBDOMAINS: 'subdomains',
65
+ JSS_BASE_DOMAIN: 'baseDomain',
60
66
  };
61
67
 
62
68
  /**
@@ -188,5 +194,6 @@ export function printConfig(config) {
188
194
  console.log(` Conneg: ${config.conneg}`);
189
195
  console.log(` Notifications: ${config.notifications}`);
190
196
  console.log(` IdP: ${config.idp ? (config.idpIssuer || 'enabled') : 'disabled'}`);
197
+ console.log(` Subdomains: ${config.subdomains ? (config.baseDomain || 'enabled') : 'disabled'}`);
191
198
  console.log('─'.repeat(40));
192
199
  }