javascript-solid-server 0.0.22 → 0.0.24
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 +26 -1
- package/README.md +138 -115
- package/bin/jss.js +10 -4
- package/package.json +1 -1
- package/src/config.js +3 -1
- package/src/handlers/resource.js +31 -4
- package/src/mashlib/index.js +50 -42
- package/src/rdf/conneg.js +3 -2
- package/src/server.js +52 -2
- package/cth-config/application.yaml +0 -2
- package/cth-config/jss.ttl +0 -6
- package/cth-config/test-subjects.ttl +0 -14
- package/test-data-idp-accounts/.idp/accounts/_email_index.json +0 -3
- package/test-data-idp-accounts/.idp/accounts/_username_index.json +0 -3
- package/test-data-idp-accounts/.idp/accounts/_webid_index.json +0 -3
- package/test-data-idp-accounts/.idp/accounts/ba3591b1-4653-4c64-9661-57dc355e5acc.json +0 -10
- package/test-data-idp-accounts/.idp/keys/jwks.json +0 -22
- package/test-dpop-flow.js +0 -148
- package/test-nostr-acl.js +0 -144
|
@@ -72,7 +72,32 @@
|
|
|
72
72
|
"WebFetch(domain:melvincarvalho.github.io)",
|
|
73
73
|
"WebFetch(domain:dev.to)",
|
|
74
74
|
"WebFetch(domain:solidproject.org)",
|
|
75
|
-
"WebFetch(domain:www.w3.org)"
|
|
75
|
+
"WebFetch(domain:www.w3.org)",
|
|
76
|
+
"Bash(wc:*)",
|
|
77
|
+
"Bash(TOKEN=\"eyJraW5kIjoyNzIzNSwidGFncyI6W1sidSIsImh0dHA6Ly9sb2NhbGhvc3Q6NDAwMC9kZW1vL25vc3RyLXpvbmUvIl0sWyJtZXRob2QiLCJHRVQiXV0sImNyZWF0ZWRfYXQiOjE3NjY5MzQ1NjksImNvbnRlbnQiOiIiLCJwdWJrZXkiOiI4OTg5OWNmOWEyNGE5ZTdlMTNmODU3MGRkMGI1MmJiOTQyMjllNDI2OGM1MGQ1OWZhNjdhMzQ0MGQ0NmFhZTdkIiwiaWQiOiJiNTUyMDUyOTVmYmQwYzhjZDYwMzk1NTgwOWYxZGM5Y2MwMjdlY2U4N2NjYmNlNzcwNWY2MjdmNmQ0ODk1MGJkIiwic2lnIjoiOWYzN2Y0NzIyZDlkNmFmZGQ5OTNkYTM0MDg2MWQ2YzQ4MmY1NzQ1MmFmZTIwZmY2YmI5OTAxNGIwOTU3NjUwMWZiNTgyZjEzNzNlZmVhNjI4ZDI5ZjlhMzhmZTgyODU0ODlmMzAzYzlmYmJjYWE0OTQxZjUyZGZlMWYxNzVkOWMifQ==\")",
|
|
78
|
+
"WebFetch(domain:solid-lite.org)",
|
|
79
|
+
"Bash(git push:*)",
|
|
80
|
+
"WebFetch(domain:linkedwebstorage.com)",
|
|
81
|
+
"WebFetch(domain:w3c.github.io)",
|
|
82
|
+
"WebFetch(domain:socialdocs.org)",
|
|
83
|
+
"WebFetch(domain:nosdav.com)",
|
|
84
|
+
"WebFetch(domain:sandy-mount.com)",
|
|
85
|
+
"WebFetch(domain:ditto.pub)",
|
|
86
|
+
"WebFetch(domain:blocktrails.org)",
|
|
87
|
+
"WebFetch(domain:microfed.org)",
|
|
88
|
+
"WebFetch(domain:soliddocs.org)",
|
|
89
|
+
"WebFetch(domain:agenticalliance.com)",
|
|
90
|
+
"WebFetch(domain:activitypub.rocks)",
|
|
91
|
+
"WebFetch(domain:nostrgit.org)",
|
|
92
|
+
"Bash(convert:*)",
|
|
93
|
+
"WebFetch(domain:instantdomainsearch.com)",
|
|
94
|
+
"Bash(for domain in jss.dev jss.sh jss.io jss.app solidserver.dev solid-server.dev)",
|
|
95
|
+
"Bash(do echo -n '$domain: ')",
|
|
96
|
+
"Bash(whois $domain)",
|
|
97
|
+
"Bash(done)",
|
|
98
|
+
"Bash(for domain in jss.dev jss.sh jss.io jss.app solidserver.dev)",
|
|
99
|
+
"Bash(host:*)",
|
|
100
|
+
"WebFetch(domain:nostr-components.github.io)"
|
|
76
101
|
]
|
|
77
102
|
}
|
|
78
103
|
}
|
package/README.md
CHANGED
|
@@ -2,59 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
A minimal, fast, JSON-LD native Solid server.
|
|
4
4
|
|
|
5
|
-
## Philosophy: JSON-LD First
|
|
6
|
-
|
|
7
|
-
This is a **JSON-LD native implementation**. Unlike traditional Solid servers that treat Turtle as the primary format and convert to/from it, this server:
|
|
8
|
-
|
|
9
|
-
- **Stores everything as JSON-LD** - No RDF parsing overhead for standard operations
|
|
10
|
-
- **Serves JSON-LD by default** - Modern web applications can consume responses directly
|
|
11
|
-
- **Content negotiation is optional** - Enable Turtle support with `{ conneg: true }` when needed
|
|
12
|
-
- **Fast by design** - Skip the RDF parsing tax when you don't need it
|
|
13
|
-
|
|
14
|
-
### Why JSON-LD First?
|
|
15
|
-
|
|
16
|
-
1. **Performance**: JSON parsing is native to JavaScript - no external RDF libraries needed for basic operations
|
|
17
|
-
2. **Simplicity**: JSON-LD is valid JSON - works with any JSON tooling
|
|
18
|
-
3. **Web-native**: Browsers and web apps understand JSON natively
|
|
19
|
-
4. **Semantic web ready**: JSON-LD is a W3C standard RDF serialization
|
|
20
|
-
|
|
21
|
-
### When to Enable Content Negotiation
|
|
22
|
-
|
|
23
|
-
Enable `conneg: true` when:
|
|
24
|
-
- Interoperating with Turtle-based Solid apps
|
|
25
|
-
- Serving data to legacy Solid clients
|
|
26
|
-
- Running conformance tests that require Turtle support
|
|
27
|
-
|
|
28
|
-
```javascript
|
|
29
|
-
import { createServer } from './src/server.js';
|
|
30
|
-
|
|
31
|
-
// Default: JSON-LD only (fast)
|
|
32
|
-
const server = createServer();
|
|
33
|
-
|
|
34
|
-
// With Turtle support (for interoperability)
|
|
35
|
-
const serverWithConneg = createServer({ conneg: true });
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Performance
|
|
39
|
-
|
|
40
|
-
This server is designed for speed. Benchmark results on a typical development machine:
|
|
41
|
-
|
|
42
|
-
| Operation | Requests/sec | Avg Latency | p99 Latency |
|
|
43
|
-
|-----------|-------------|-------------|-------------|
|
|
44
|
-
| GET resource | 5,400+ | 1.2ms | 3ms |
|
|
45
|
-
| GET container | 4,700+ | 1.6ms | 3ms |
|
|
46
|
-
| PUT (write) | 5,700+ | 1.1ms | 2ms |
|
|
47
|
-
| POST (create) | 5,200+ | 1.3ms | 3ms |
|
|
48
|
-
| OPTIONS | 10,000+ | 0.4ms | 1ms |
|
|
49
|
-
|
|
50
|
-
Run benchmarks yourself:
|
|
51
|
-
```bash
|
|
52
|
-
npm run benchmark
|
|
53
|
-
```
|
|
54
|
-
|
|
55
5
|
## Features
|
|
56
6
|
|
|
57
|
-
### Implemented (v0.0.
|
|
7
|
+
### Implemented (v0.0.23)
|
|
58
8
|
|
|
59
9
|
- **LDP CRUD Operations** - GET, PUT, POST, DELETE, HEAD
|
|
60
10
|
- **N3 Patch** - Solid's native patch format for RDF updates
|
|
@@ -66,11 +16,13 @@ npm run benchmark
|
|
|
66
16
|
- **Container Management** - Create, list, and manage containers
|
|
67
17
|
- **Multi-user Pods** - Path-based (`/alice/`) or subdomain-based (`alice.example.com`)
|
|
68
18
|
- **Subdomain Mode** - XSS protection via origin isolation
|
|
69
|
-
- **Mashlib Data Browser** - Optional SolidOS UI
|
|
19
|
+
- **Mashlib Data Browser** - Optional SolidOS UI (CDN or local hosting)
|
|
70
20
|
- **WebID Profiles** - JSON-LD structured data in HTML at pod root
|
|
71
21
|
- **Web Access Control (WAC)** - `.acl` file-based authorization
|
|
72
22
|
- **Solid-OIDC Identity Provider** - Built-in IdP with DPoP, dynamic registration
|
|
73
23
|
- **Solid-OIDC Resource Server** - Accept DPoP-bound access tokens from external IdPs
|
|
24
|
+
- **NSS-style Registration** - Username/password auth compatible with Solid apps
|
|
25
|
+
- **Nostr Authentication** - NIP-98 HTTP Auth with Schnorr signatures
|
|
74
26
|
- **Simple Auth Tokens** - Built-in token authentication for development
|
|
75
27
|
- **Content Negotiation** - Optional Turtle <-> JSON-LD conversion
|
|
76
28
|
- **CORS Support** - Full cross-origin resource sharing
|
|
@@ -139,8 +91,9 @@ jss --help # Show help
|
|
|
139
91
|
| `--idp-issuer <url>` | IdP issuer URL | (auto) |
|
|
140
92
|
| `--subdomains` | Enable subdomain-based pods | false |
|
|
141
93
|
| `--base-domain <domain>` | Base domain for subdomains | - |
|
|
142
|
-
| `--mashlib` | Enable Mashlib
|
|
143
|
-
| `--mashlib-
|
|
94
|
+
| `--mashlib` | Enable Mashlib (local mode) | false |
|
|
95
|
+
| `--mashlib-cdn` | Enable Mashlib (CDN mode) | false |
|
|
96
|
+
| `--mashlib-version <ver>` | Mashlib CDN version | 2.0.0 |
|
|
144
97
|
| `-q, --quiet` | Suppress logs | false |
|
|
145
98
|
|
|
146
99
|
### Environment Variables
|
|
@@ -258,22 +211,103 @@ curl -X PUT http://localhost:3000/alice/public/new-resource.json \
|
|
|
258
211
|
-d '{"@id": "#new"}'
|
|
259
212
|
```
|
|
260
213
|
|
|
261
|
-
##
|
|
214
|
+
## Philosophy: JSON-LD First
|
|
262
215
|
|
|
216
|
+
This is a **JSON-LD native implementation**. Unlike traditional Solid servers that treat Turtle as the primary format and convert to/from it, this server:
|
|
217
|
+
|
|
218
|
+
- **Stores everything as JSON-LD** - No RDF parsing overhead for standard operations
|
|
219
|
+
- **Serves JSON-LD by default** - Modern web applications can consume responses directly
|
|
220
|
+
- **Content negotiation is optional** - Enable Turtle support with `{ conneg: true }` when needed
|
|
221
|
+
- **Fast by design** - Skip the RDF parsing tax when you don't need it
|
|
222
|
+
|
|
223
|
+
### Why JSON-LD First?
|
|
224
|
+
|
|
225
|
+
1. **Performance**: JSON parsing is native to JavaScript - no external RDF libraries needed for basic operations
|
|
226
|
+
2. **Simplicity**: JSON-LD is valid JSON - works with any JSON tooling
|
|
227
|
+
3. **Web-native**: Browsers and web apps understand JSON natively
|
|
228
|
+
4. **Semantic web ready**: JSON-LD is a W3C standard RDF serialization
|
|
229
|
+
|
|
230
|
+
### When to Enable Content Negotiation
|
|
231
|
+
|
|
232
|
+
Enable `conneg: true` when:
|
|
233
|
+
- Interoperating with Turtle-based Solid apps
|
|
234
|
+
- Serving data to legacy Solid clients
|
|
235
|
+
- Running conformance tests that require Turtle support
|
|
236
|
+
|
|
237
|
+
```javascript
|
|
238
|
+
import { createServer } from './src/server.js';
|
|
239
|
+
|
|
240
|
+
// Default: JSON-LD only (fast)
|
|
241
|
+
const server = createServer();
|
|
242
|
+
|
|
243
|
+
// With Turtle support (for interoperability)
|
|
244
|
+
const serverWithConneg = createServer({ conneg: true });
|
|
263
245
|
```
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
246
|
+
|
|
247
|
+
## Configuration
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
createServer({
|
|
251
|
+
logger: true, // Enable Fastify logging (default: true)
|
|
252
|
+
conneg: false, // Enable content negotiation (default: false)
|
|
253
|
+
notifications: false, // Enable WebSocket notifications (default: false)
|
|
254
|
+
subdomains: false, // Enable subdomain-based pods (default: false)
|
|
255
|
+
baseDomain: null, // Base domain for subdomains (e.g., "example.com")
|
|
256
|
+
mashlib: false, // Enable Mashlib data browser - local mode (default: false)
|
|
257
|
+
mashlibCdn: false, // Enable Mashlib data browser - CDN mode (default: false)
|
|
258
|
+
mashlibVersion: '2.0.0', // Mashlib version for CDN mode
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Mashlib Data Browser
|
|
263
|
+
|
|
264
|
+
Enable the [SolidOS Mashlib](https://github.com/SolidOS/mashlib) data browser for RDF resources. Two modes are available:
|
|
265
|
+
|
|
266
|
+
**CDN Mode** (recommended for getting started):
|
|
267
|
+
```bash
|
|
268
|
+
jss start --mashlib-cdn --conneg
|
|
269
|
+
```
|
|
270
|
+
Loads mashlib from unpkg.com CDN. Zero footprint - no local files needed.
|
|
271
|
+
|
|
272
|
+
**Local Mode** (for production/offline):
|
|
273
|
+
```bash
|
|
274
|
+
jss start --mashlib --conneg
|
|
275
|
+
```
|
|
276
|
+
Serves mashlib from `src/mashlib-local/dist/`. Requires building mashlib locally:
|
|
277
|
+
```bash
|
|
278
|
+
cd src/mashlib-local
|
|
279
|
+
npm install && npm run build
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**How it works:**
|
|
283
|
+
1. Browser requests `/alice/public/data.ttl` with `Accept: text/html`
|
|
284
|
+
2. Server returns Mashlib HTML wrapper
|
|
285
|
+
3. Mashlib fetches the actual data via content negotiation
|
|
286
|
+
4. Mashlib renders an interactive, editable view
|
|
287
|
+
|
|
288
|
+
**Note:** Mashlib works best with `--conneg` enabled for Turtle support. Pod profiles (`/alice/`) continue to serve our JSON-LD-in-HTML format.
|
|
289
|
+
|
|
290
|
+
### WebSocket Notifications
|
|
291
|
+
|
|
292
|
+
Enable real-time notifications for resource changes:
|
|
293
|
+
|
|
294
|
+
```javascript
|
|
295
|
+
const server = createServer({ notifications: true });
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Clients discover the WebSocket URL via the `Updates-Via` header:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
curl -I http://localhost:3000/alice/public/
|
|
302
|
+
# Updates-Via: ws://localhost:3000/.notifications
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Protocol (solid-0.1, compatible with SolidOS):
|
|
306
|
+
```
|
|
307
|
+
Server: protocol solid-0.1
|
|
308
|
+
Client: sub http://localhost:3000/alice/public/data.json
|
|
309
|
+
Server: ack http://localhost:3000/alice/public/data.json
|
|
310
|
+
Server: pub http://localhost:3000/alice/public/data.json (on change)
|
|
277
311
|
```
|
|
278
312
|
|
|
279
313
|
## Authentication
|
|
@@ -347,6 +381,24 @@ curl -H "Authorization: DPoP ACCESS_TOKEN" \
|
|
|
347
381
|
http://localhost:3000/alice/private/
|
|
348
382
|
```
|
|
349
383
|
|
|
384
|
+
## Pod Structure
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
/alice/
|
|
388
|
+
├── index.html # WebID profile (HTML with JSON-LD)
|
|
389
|
+
├── .acl # Root ACL (owner + public read)
|
|
390
|
+
├── inbox/ # Notifications (public append)
|
|
391
|
+
│ └── .acl
|
|
392
|
+
├── public/ # Public files
|
|
393
|
+
├── private/ # Private files (owner only)
|
|
394
|
+
│ └── .acl
|
|
395
|
+
└── settings/ # User preferences (owner only)
|
|
396
|
+
├── .acl
|
|
397
|
+
├── prefs
|
|
398
|
+
├── publicTypeIndex
|
|
399
|
+
└── privateTypeIndex
|
|
400
|
+
```
|
|
401
|
+
|
|
350
402
|
## Subdomain Mode (XSS Protection)
|
|
351
403
|
|
|
352
404
|
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.
|
|
@@ -398,59 +450,30 @@ curl -X POST https://example.com/.pods \
|
|
|
398
450
|
-d '{"name": "alice"}'
|
|
399
451
|
```
|
|
400
452
|
|
|
401
|
-
##
|
|
402
|
-
|
|
403
|
-
```javascript
|
|
404
|
-
createServer({
|
|
405
|
-
logger: true, // Enable Fastify logging (default: true)
|
|
406
|
-
conneg: false, // Enable content negotiation (default: false)
|
|
407
|
-
notifications: false, // Enable WebSocket notifications (default: false)
|
|
408
|
-
subdomains: false, // Enable subdomain-based pods (default: false)
|
|
409
|
-
baseDomain: null, // Base domain for subdomains (e.g., "example.com")
|
|
410
|
-
mashlib: false, // Enable Mashlib data browser (default: false)
|
|
411
|
-
mashlibVersion: '2.0.0', // Mashlib version to use
|
|
412
|
-
});
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
### Mashlib Data Browser
|
|
416
|
-
|
|
417
|
-
Enable the [SolidOS Mashlib](https://github.com/SolidOS/mashlib) data browser for RDF resources:
|
|
418
|
-
|
|
419
|
-
```bash
|
|
420
|
-
jss start --mashlib --conneg
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
When enabled, requesting an RDF resource with `Accept: text/html` returns an interactive data browser UI instead of raw data. Mashlib is loaded from the unpkg CDN.
|
|
424
|
-
|
|
425
|
-
**How it works:**
|
|
426
|
-
1. Browser requests `/alice/public/data.ttl` with `Accept: text/html`
|
|
427
|
-
2. Server returns Mashlib HTML wrapper (loads JS/CSS from CDN)
|
|
428
|
-
3. Mashlib fetches the actual data via content negotiation
|
|
429
|
-
4. Mashlib renders an interactive, editable view
|
|
430
|
-
|
|
431
|
-
**Note:** Mashlib works best with `--conneg` enabled for Turtle support. Pod profiles (`/alice/`) continue to serve our JSON-LD-in-HTML format.
|
|
453
|
+
## Comparison
|
|
432
454
|
|
|
433
|
-
|
|
455
|
+
| Server | Size | Deps | Notes |
|
|
456
|
+
|--------|------|------|-------|
|
|
457
|
+
| [JSS](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer) | 432 KB | 10 | Minimal, JSON-LD native |
|
|
458
|
+
| [NSS](https://github.com/nodeSolidServer/node-solid-server) | 777 KB | 58 | Original Solid server |
|
|
459
|
+
| [CSS](https://github.com/CommunitySolidServer/CommunitySolidServer) | 5.8 MB | 70 | Modular, configurable |
|
|
460
|
+
| [Pivot](https://github.com/solid-contrib/pivot) | ~6 MB | 70+ | Built on CSS |
|
|
434
461
|
|
|
435
|
-
|
|
462
|
+
## Performance
|
|
436
463
|
|
|
437
|
-
|
|
438
|
-
const server = createServer({ notifications: true });
|
|
439
|
-
```
|
|
464
|
+
This server is designed for speed. Benchmark results on a typical development machine:
|
|
440
465
|
|
|
441
|
-
|
|
466
|
+
| Operation | Requests/sec | Avg Latency | p99 Latency |
|
|
467
|
+
|-----------|-------------|-------------|-------------|
|
|
468
|
+
| GET resource | 5,400+ | 1.2ms | 3ms |
|
|
469
|
+
| GET container | 4,700+ | 1.6ms | 3ms |
|
|
470
|
+
| PUT (write) | 5,700+ | 1.1ms | 2ms |
|
|
471
|
+
| POST (create) | 5,200+ | 1.3ms | 3ms |
|
|
472
|
+
| OPTIONS | 10,000+ | 0.4ms | 1ms |
|
|
442
473
|
|
|
474
|
+
Run benchmarks yourself:
|
|
443
475
|
```bash
|
|
444
|
-
|
|
445
|
-
# Updates-Via: ws://localhost:3000/.notifications
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
Protocol (solid-0.1, compatible with SolidOS):
|
|
449
|
-
```
|
|
450
|
-
Server: protocol solid-0.1
|
|
451
|
-
Client: sub http://localhost:3000/alice/public/data.json
|
|
452
|
-
Server: ack http://localhost:3000/alice/public/data.json
|
|
453
|
-
Server: pub http://localhost:3000/alice/public/data.json (on change)
|
|
476
|
+
npm run benchmark
|
|
454
477
|
```
|
|
455
478
|
|
|
456
479
|
## Running Tests
|
package/bin/jss.js
CHANGED
|
@@ -50,9 +50,10 @@ program
|
|
|
50
50
|
.option('--subdomains', 'Enable subdomain-based pods (XSS protection)')
|
|
51
51
|
.option('--no-subdomains', 'Disable subdomain-based pods')
|
|
52
52
|
.option('--base-domain <domain>', 'Base domain for subdomain pods (e.g., "example.com")')
|
|
53
|
-
.option('--mashlib', 'Enable Mashlib data browser
|
|
53
|
+
.option('--mashlib', 'Enable Mashlib data browser (local mode, requires mashlib in node_modules)')
|
|
54
|
+
.option('--mashlib-cdn', 'Enable Mashlib data browser (CDN mode, no local files needed)')
|
|
54
55
|
.option('--no-mashlib', 'Disable Mashlib data browser')
|
|
55
|
-
.option('--mashlib-version <version>', 'Mashlib version
|
|
56
|
+
.option('--mashlib-version <version>', 'Mashlib version for CDN mode (default: 2.0.0)')
|
|
56
57
|
.option('-q, --quiet', 'Suppress log output')
|
|
57
58
|
.option('--print-config', 'Print configuration and exit')
|
|
58
59
|
.action(async (options) => {
|
|
@@ -91,7 +92,8 @@ program
|
|
|
91
92
|
root: config.root,
|
|
92
93
|
subdomains: config.subdomains,
|
|
93
94
|
baseDomain: config.baseDomain,
|
|
94
|
-
mashlib: config.mashlib,
|
|
95
|
+
mashlib: config.mashlib || config.mashlibCdn,
|
|
96
|
+
mashlibCdn: config.mashlibCdn,
|
|
95
97
|
mashlibVersion: config.mashlibVersion,
|
|
96
98
|
});
|
|
97
99
|
|
|
@@ -106,7 +108,11 @@ program
|
|
|
106
108
|
if (config.notifications) console.log(' WebSocket: enabled');
|
|
107
109
|
if (config.idp) console.log(` IdP: ${idpIssuer}`);
|
|
108
110
|
if (config.subdomains) console.log(` Subdomains: ${config.baseDomain} (XSS protection enabled)`);
|
|
109
|
-
if (config.
|
|
111
|
+
if (config.mashlibCdn) {
|
|
112
|
+
console.log(` Mashlib: v${config.mashlibVersion} (CDN mode)`);
|
|
113
|
+
} else if (config.mashlib) {
|
|
114
|
+
console.log(` Mashlib: local (data browser enabled)`);
|
|
115
|
+
}
|
|
110
116
|
console.log('\n Press Ctrl+C to stop\n');
|
|
111
117
|
}
|
|
112
118
|
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -39,6 +39,7 @@ export const defaults = {
|
|
|
39
39
|
|
|
40
40
|
// Mashlib data browser
|
|
41
41
|
mashlib: false,
|
|
42
|
+
mashlibCdn: false,
|
|
42
43
|
mashlibVersion: '2.0.0',
|
|
43
44
|
|
|
44
45
|
// Logging
|
|
@@ -68,6 +69,7 @@ const envMap = {
|
|
|
68
69
|
JSS_SUBDOMAINS: 'subdomains',
|
|
69
70
|
JSS_BASE_DOMAIN: 'baseDomain',
|
|
70
71
|
JSS_MASHLIB: 'mashlib',
|
|
72
|
+
JSS_MASHLIB_CDN: 'mashlibCdn',
|
|
71
73
|
JSS_MASHLIB_VERSION: 'mashlibVersion',
|
|
72
74
|
};
|
|
73
75
|
|
|
@@ -201,6 +203,6 @@ export function printConfig(config) {
|
|
|
201
203
|
console.log(` Notifications: ${config.notifications}`);
|
|
202
204
|
console.log(` IdP: ${config.idp ? (config.idpIssuer || 'enabled') : 'disabled'}`);
|
|
203
205
|
console.log(` Subdomains: ${config.subdomains ? (config.baseDomain || 'enabled') : 'disabled'}`);
|
|
204
|
-
console.log(` Mashlib: ${config.
|
|
206
|
+
console.log(` Mashlib: ${config.mashlibCdn ? `CDN v${config.mashlibVersion}` : config.mashlib ? 'local' : 'disabled'}`);
|
|
205
207
|
console.log('─'.repeat(40));
|
|
206
208
|
}
|
package/src/handlers/resource.js
CHANGED
|
@@ -125,6 +125,27 @@ export async function handleGet(request, reply) {
|
|
|
125
125
|
const entries = await storage.listContainer(storagePath);
|
|
126
126
|
const jsonLd = generateContainerJsonLd(resourceUrl, entries || []);
|
|
127
127
|
|
|
128
|
+
// Check if we should serve Mashlib data browser for containers
|
|
129
|
+
if (shouldServeMashlib(request, request.mashlibEnabled, 'application/ld+json')) {
|
|
130
|
+
const cdnVersion = request.mashlibCdn ? request.mashlibVersion : null;
|
|
131
|
+
const html = generateDatabrowserHtml(resourceUrl, cdnVersion);
|
|
132
|
+
const headers = getAllHeaders({
|
|
133
|
+
isContainer: true,
|
|
134
|
+
etag: stats.etag,
|
|
135
|
+
contentType: 'text/html',
|
|
136
|
+
origin,
|
|
137
|
+
resourceUrl,
|
|
138
|
+
connegEnabled
|
|
139
|
+
});
|
|
140
|
+
headers['Vary'] = 'Accept';
|
|
141
|
+
headers['X-Frame-Options'] = 'DENY';
|
|
142
|
+
headers['Content-Security-Policy'] = "frame-ancestors 'none'";
|
|
143
|
+
headers['Cache-Control'] = 'no-store';
|
|
144
|
+
|
|
145
|
+
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
146
|
+
return reply.type('text/html').send(html);
|
|
147
|
+
}
|
|
148
|
+
|
|
128
149
|
const headers = getAllHeaders({
|
|
129
150
|
isContainer: true,
|
|
130
151
|
etag: stats.etag,
|
|
@@ -145,7 +166,9 @@ export async function handleGet(request, reply) {
|
|
|
145
166
|
// Check if we should serve Mashlib data browser
|
|
146
167
|
// Only for RDF resources when Accept: text/html is requested
|
|
147
168
|
if (shouldServeMashlib(request, request.mashlibEnabled, storedContentType)) {
|
|
148
|
-
|
|
169
|
+
// Pass CDN version if using CDN mode, null for local mode
|
|
170
|
+
const cdnVersion = request.mashlibCdn ? request.mashlibVersion : null;
|
|
171
|
+
const html = generateDatabrowserHtml(resourceUrl, cdnVersion);
|
|
149
172
|
const headers = getAllHeaders({
|
|
150
173
|
isContainer: false,
|
|
151
174
|
etag: stats.etag,
|
|
@@ -155,6 +178,10 @@ export async function handleGet(request, reply) {
|
|
|
155
178
|
connegEnabled
|
|
156
179
|
});
|
|
157
180
|
headers['Vary'] = 'Accept';
|
|
181
|
+
headers['X-Frame-Options'] = 'DENY';
|
|
182
|
+
headers['Content-Security-Policy'] = "frame-ancestors 'none'";
|
|
183
|
+
// Don't cache the HTML wrapper - always negotiate fresh
|
|
184
|
+
headers['Cache-Control'] = 'no-store';
|
|
158
185
|
|
|
159
186
|
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
160
187
|
return reply.type('text/html').send(html);
|
|
@@ -191,7 +218,7 @@ export async function handleGet(request, reply) {
|
|
|
191
218
|
resourceUrl,
|
|
192
219
|
connegEnabled
|
|
193
220
|
});
|
|
194
|
-
headers['Vary'] = getVaryHeader(connegEnabled);
|
|
221
|
+
headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);
|
|
195
222
|
|
|
196
223
|
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
197
224
|
return reply.send(outputContent);
|
|
@@ -209,7 +236,7 @@ export async function handleGet(request, reply) {
|
|
|
209
236
|
resourceUrl,
|
|
210
237
|
connegEnabled
|
|
211
238
|
});
|
|
212
|
-
headers['Vary'] = getVaryHeader(connegEnabled);
|
|
239
|
+
headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);
|
|
213
240
|
|
|
214
241
|
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
215
242
|
return reply.send(content);
|
|
@@ -353,7 +380,7 @@ export async function handlePut(request, reply) {
|
|
|
353
380
|
const origin = request.headers.origin;
|
|
354
381
|
const headers = getAllHeaders({ isContainer: false, origin, resourceUrl, connegEnabled });
|
|
355
382
|
headers['Location'] = resourceUrl;
|
|
356
|
-
headers['Vary'] = getVaryHeader(connegEnabled);
|
|
383
|
+
headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);
|
|
357
384
|
|
|
358
385
|
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
359
386
|
|
package/src/mashlib/index.js
CHANGED
|
@@ -6,51 +6,38 @@
|
|
|
6
6
|
* we return this wrapper which then fetches and renders the data.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const CDN_BASE = 'https://unpkg.com/mashlib';
|
|
10
|
-
|
|
11
9
|
/**
|
|
12
10
|
* Generate Mashlib databrowser HTML
|
|
13
|
-
*
|
|
14
|
-
* @param {string}
|
|
11
|
+
*
|
|
12
|
+
* @param {string} resourceUrl - The URL of the resource being viewed (unused, kept for API compatibility)
|
|
13
|
+
* @param {string} cdnVersion - If provided, load mashlib from unpkg CDN (e.g., "2.0.0")
|
|
15
14
|
* @returns {string} HTML content
|
|
16
15
|
*/
|
|
17
|
-
export function generateDatabrowserHtml(resourceUrl,
|
|
18
|
-
|
|
16
|
+
export function generateDatabrowserHtml(resourceUrl, cdnVersion = null) {
|
|
17
|
+
if (cdnVersion) {
|
|
18
|
+
// CDN mode - use script.onload to ensure mashlib is fully loaded before init
|
|
19
|
+
// This avoids race conditions with defer + DOMContentLoaded
|
|
20
|
+
const cdnBase = `https://unpkg.com/mashlib@${cdnVersion}/dist`;
|
|
21
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>SolidOS Web App</title>
|
|
22
|
+
<link href="${cdnBase}/mash.css" rel="stylesheet"></head>
|
|
23
|
+
<body id="PageBody"><header id="PageHeader"></header>
|
|
24
|
+
<div class="TabulatorOutline" id="DummyUUID" role="main"><table id="outline"></table><div id="GlobalDashboard"></div></div>
|
|
25
|
+
<footer id="PageFooter"></footer>
|
|
26
|
+
<script>
|
|
27
|
+
(function() {
|
|
28
|
+
var s = document.createElement('script');
|
|
29
|
+
s.src = '${cdnBase}/mashlib.min.js';
|
|
30
|
+
s.onload = function() { panes.runDataBrowser(); };
|
|
31
|
+
s.onerror = function() { document.body.innerHTML = '<p>Failed to load Mashlib from CDN</p>'; };
|
|
32
|
+
document.head.appendChild(s);
|
|
33
|
+
})();
|
|
34
|
+
</script></body></html>`;
|
|
35
|
+
}
|
|
19
36
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
25
|
-
<title>SolidOS - ${escapeHtml(resourceUrl)}</title>
|
|
26
|
-
<script defer src="${cdnUrl}/mashlib.min.js"></script>
|
|
27
|
-
<link href="${cdnUrl}/mash.css" rel="stylesheet">
|
|
28
|
-
<script>
|
|
29
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
30
|
-
// runDataBrowser uses window.location to determine what to fetch
|
|
31
|
-
panes.runDataBrowser();
|
|
32
|
-
});
|
|
33
|
-
</script>
|
|
34
|
-
<style>
|
|
35
|
-
/* Loading indicator */
|
|
36
|
-
body:not(.loaded) #PageBody::before {
|
|
37
|
-
content: 'Loading SolidOS...';
|
|
38
|
-
display: block;
|
|
39
|
-
padding: 2em;
|
|
40
|
-
text-align: center;
|
|
41
|
-
color: #666;
|
|
42
|
-
}
|
|
43
|
-
</style>
|
|
44
|
-
</head>
|
|
45
|
-
<body id="PageBody">
|
|
46
|
-
<header id="PageHeader"></header>
|
|
47
|
-
<div class="TabulatorOutline" id="DummyUUID" role="main">
|
|
48
|
-
<table id="outline"></table>
|
|
49
|
-
<div id="GlobalDashboard"></div>
|
|
50
|
-
</div>
|
|
51
|
-
<footer id="PageFooter"></footer>
|
|
52
|
-
</body>
|
|
53
|
-
</html>`;
|
|
37
|
+
// Local mode - use defer (reliable when served locally)
|
|
38
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>SolidOS Web App</title><script>document.addEventListener('DOMContentLoaded', function() {
|
|
39
|
+
panes.runDataBrowser()
|
|
40
|
+
})</script><script defer="defer" src="/mashlib.min.js"></script><link href="/mash.css" rel="stylesheet"></head><body id="PageBody"><header id="PageHeader"></header><div class="TabulatorOutline" id="DummyUUID" role="main"><table id="outline"></table><div id="GlobalDashboard"></div></div><footer id="PageFooter"></footer></body></html>`;
|
|
54
41
|
}
|
|
55
42
|
|
|
56
43
|
/**
|
|
@@ -61,17 +48,38 @@ export function generateDatabrowserHtml(resourceUrl, version = '2.0.0') {
|
|
|
61
48
|
* @returns {boolean}
|
|
62
49
|
*/
|
|
63
50
|
export function shouldServeMashlib(request, mashlibEnabled, contentType) {
|
|
51
|
+
const accept = request.headers.accept || '';
|
|
52
|
+
const secFetchDest = request.headers['sec-fetch-dest'] || '';
|
|
53
|
+
|
|
64
54
|
if (!mashlibEnabled) {
|
|
65
55
|
return false;
|
|
66
56
|
}
|
|
67
57
|
|
|
68
|
-
|
|
58
|
+
// Only serve mashlib for top-level document navigation
|
|
59
|
+
// sec-fetch-dest: 'document' = browser navigation (serve mashlib)
|
|
60
|
+
// sec-fetch-dest: 'empty' = JavaScript fetch/XHR (serve RDF data)
|
|
61
|
+
if (secFetchDest && secFetchDest !== 'document') {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
69
64
|
|
|
70
|
-
// Must explicitly accept HTML
|
|
65
|
+
// Must explicitly accept HTML as a primary type (not via */*)
|
|
66
|
+
// Browser navigation: "text/html,application/xhtml+xml,..."
|
|
67
|
+
// Mashlib fetch: "application/rdf+xml;q=0.9, */*;q=0.1,..."
|
|
71
68
|
if (!accept.includes('text/html')) {
|
|
72
69
|
return false;
|
|
73
70
|
}
|
|
74
71
|
|
|
72
|
+
// Don't serve mashlib if RDF types appear BEFORE text/html in Accept header
|
|
73
|
+
// This handles cases like "application/rdf+xml, text/html" where RDF is preferred
|
|
74
|
+
const htmlPos = accept.indexOf('text/html');
|
|
75
|
+
const acceptRdfTypes = ['application/rdf+xml', 'text/turtle', 'application/ld+json', 'text/n3', 'application/n-triples'];
|
|
76
|
+
for (const rdfType of acceptRdfTypes) {
|
|
77
|
+
const rdfPos = accept.indexOf(rdfType);
|
|
78
|
+
if (rdfPos !== -1 && rdfPos < htmlPos) {
|
|
79
|
+
return false; // RDF type is preferred over HTML
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
// Only serve mashlib for RDF content types
|
|
76
84
|
const rdfTypes = [
|
|
77
85
|
'text/turtle',
|
package/src/rdf/conneg.js
CHANGED
|
@@ -188,9 +188,10 @@ export async function fromJsonLd(jsonLd, targetType, baseUri, connegEnabled = fa
|
|
|
188
188
|
|
|
189
189
|
/**
|
|
190
190
|
* Get Vary header value for content negotiation
|
|
191
|
+
* Include Accept when conneg or mashlib is enabled (response varies by Accept header)
|
|
191
192
|
*/
|
|
192
|
-
export function getVaryHeader(connegEnabled) {
|
|
193
|
-
return connegEnabled ? 'Accept, Origin' : 'Origin';
|
|
193
|
+
export function getVaryHeader(connegEnabled, mashlibEnabled = false) {
|
|
194
|
+
return (connegEnabled || mashlibEnabled) ? 'Accept, Origin' : 'Origin';
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
/**
|
package/src/server.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import Fastify from 'fastify';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
2
5
|
import { handleGet, handleHead, handlePut, handleDelete, handleOptions, handlePatch } from './handlers/resource.js';
|
|
3
6
|
import { handlePost, handleCreatePod } from './handlers/container.js';
|
|
4
7
|
import { getCorsHeaders } from './ldp/headers.js';
|
|
@@ -6,6 +9,8 @@ import { authorize, handleUnauthorized } from './auth/middleware.js';
|
|
|
6
9
|
import { notificationsPlugin } from './notifications/index.js';
|
|
7
10
|
import { idpPlugin } from './idp/index.js';
|
|
8
11
|
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
9
14
|
/**
|
|
10
15
|
* Create and configure Fastify server
|
|
11
16
|
* @param {object} options - Server options
|
|
@@ -31,7 +36,9 @@ export function createServer(options = {}) {
|
|
|
31
36
|
const subdomainsEnabled = options.subdomains ?? false;
|
|
32
37
|
const baseDomain = options.baseDomain || null;
|
|
33
38
|
// Mashlib data browser is OFF by default
|
|
39
|
+
// mashlibCdn: if true, load from CDN; if false, serve locally
|
|
34
40
|
const mashlibEnabled = options.mashlib ?? false;
|
|
41
|
+
const mashlibCdn = options.mashlibCdn ?? false;
|
|
35
42
|
const mashlibVersion = options.mashlibVersion ?? '2.0.0';
|
|
36
43
|
|
|
37
44
|
// Set data root via environment variable if provided
|
|
@@ -70,6 +77,7 @@ export function createServer(options = {}) {
|
|
|
70
77
|
fastify.decorateRequest('baseDomain', null);
|
|
71
78
|
fastify.decorateRequest('podName', null);
|
|
72
79
|
fastify.decorateRequest('mashlibEnabled', null);
|
|
80
|
+
fastify.decorateRequest('mashlibCdn', null);
|
|
73
81
|
fastify.decorateRequest('mashlibVersion', null);
|
|
74
82
|
fastify.addHook('onRequest', async (request) => {
|
|
75
83
|
request.connegEnabled = connegEnabled;
|
|
@@ -78,6 +86,7 @@ export function createServer(options = {}) {
|
|
|
78
86
|
request.subdomainsEnabled = subdomainsEnabled;
|
|
79
87
|
request.baseDomain = baseDomain;
|
|
80
88
|
request.mashlibEnabled = mashlibEnabled;
|
|
89
|
+
request.mashlibCdn = mashlibCdn;
|
|
81
90
|
request.mashlibVersion = mashlibVersion;
|
|
82
91
|
|
|
83
92
|
// Extract pod name from subdomain if enabled
|
|
@@ -122,11 +131,13 @@ export function createServer(options = {}) {
|
|
|
122
131
|
// Authorization hook - check WAC permissions
|
|
123
132
|
// Skip for pod creation endpoint (needs special handling)
|
|
124
133
|
fastify.addHook('preHandler', async (request, reply) => {
|
|
125
|
-
// Skip auth for pod creation, OPTIONS, IdP routes, and well-known endpoints
|
|
134
|
+
// Skip auth for pod creation, OPTIONS, IdP routes, mashlib, and well-known endpoints
|
|
135
|
+
const mashlibPaths = ['/mashlib.min.js', '/mash.css', '/841.mashlib.min.js'];
|
|
126
136
|
if (request.url === '/.pods' ||
|
|
127
137
|
request.method === 'OPTIONS' ||
|
|
128
138
|
request.url.startsWith('/idp/') ||
|
|
129
|
-
request.url.startsWith('/.well-known/')
|
|
139
|
+
request.url.startsWith('/.well-known/') ||
|
|
140
|
+
mashlibPaths.some(p => request.url === p || request.url.startsWith(p + '.'))) {
|
|
130
141
|
return;
|
|
131
142
|
}
|
|
132
143
|
|
|
@@ -144,6 +155,45 @@ export function createServer(options = {}) {
|
|
|
144
155
|
// Pod creation endpoint
|
|
145
156
|
fastify.post('/.pods', handleCreatePod);
|
|
146
157
|
|
|
158
|
+
// Mashlib static files (served from root like NSS does)
|
|
159
|
+
if (mashlibEnabled) {
|
|
160
|
+
if (mashlibCdn) {
|
|
161
|
+
// CDN mode: redirect chunk requests to CDN
|
|
162
|
+
// Mashlib uses code splitting, so it loads chunks like 789.mashlib.min.js
|
|
163
|
+
const cdnBase = `https://unpkg.com/mashlib@${mashlibVersion}/dist`;
|
|
164
|
+
const chunkPattern = /^\/\d+\.mashlib\.min\.js(\.map)?$/;
|
|
165
|
+
|
|
166
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
167
|
+
if (chunkPattern.test(request.url)) {
|
|
168
|
+
const filename = request.url.split('/').pop();
|
|
169
|
+
return reply.redirect(302, `${cdnBase}/${filename}`);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
} else {
|
|
173
|
+
// Local mode: serve from local files
|
|
174
|
+
const mashlibDir = join(__dirname, 'mashlib-local', 'dist');
|
|
175
|
+
const mashlibFiles = {
|
|
176
|
+
'/mashlib.min.js': { file: 'mashlib.min.js', type: 'application/javascript' },
|
|
177
|
+
'/mashlib.min.js.map': { file: 'mashlib.min.js.map', type: 'application/json' },
|
|
178
|
+
'/mash.css': { file: 'mash.css', type: 'text/css' },
|
|
179
|
+
'/mash.css.map': { file: 'mash.css.map', type: 'application/json' },
|
|
180
|
+
'/841.mashlib.min.js': { file: '841.mashlib.min.js', type: 'application/javascript' },
|
|
181
|
+
'/841.mashlib.min.js.map': { file: '841.mashlib.min.js.map', type: 'application/json' }
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
for (const [path, config] of Object.entries(mashlibFiles)) {
|
|
185
|
+
fastify.get(path, async (request, reply) => {
|
|
186
|
+
try {
|
|
187
|
+
const content = await readFile(join(mashlibDir, config.file));
|
|
188
|
+
return reply.type(config.type).send(content);
|
|
189
|
+
} catch {
|
|
190
|
+
return reply.code(404).send({ error: 'Not Found' });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
147
197
|
// LDP routes - using wildcard routing
|
|
148
198
|
fastify.get('/*', handleGet);
|
|
149
199
|
fastify.head('/*', handleHead);
|
package/cth-config/jss.ttl
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
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" .
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "ba3591b1-4653-4c64-9661-57dc355e5acc",
|
|
3
|
-
"username": "credtest",
|
|
4
|
-
"email": "credtest@example.com",
|
|
5
|
-
"passwordHash": "$2b$10$tFYM8KuMVTFRpVMqZOYR4OKNreNLgCBqzZVTNAhpdBFUmGH1MFNBu",
|
|
6
|
-
"webId": "http://localhost:3101/credtest/#me",
|
|
7
|
-
"podName": "credtest",
|
|
8
|
-
"createdAt": "2025-12-28T14:20:02.176Z",
|
|
9
|
-
"lastLogin": "2025-12-28T14:20:02.579Z"
|
|
10
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"jwks": {
|
|
3
|
-
"keys": [
|
|
4
|
-
{
|
|
5
|
-
"kty": "EC",
|
|
6
|
-
"x": "Aa7l5-YrS54RU8xPfEphUTRwNBzSm6lxm84aqKjfrSg",
|
|
7
|
-
"y": "tWi_lhjqQhd43KdK5YqDg7ZzRSUZo3L0ytbiBTdPOWs",
|
|
8
|
-
"crv": "P-256",
|
|
9
|
-
"d": "x6NqVSfA241O10u9Qp4m0dQZsTNYw-Hku3r0eu47VZE",
|
|
10
|
-
"kid": "ed46f7df-3010-43da-9032-e0acaee4d3e1",
|
|
11
|
-
"use": "sig",
|
|
12
|
-
"alg": "ES256",
|
|
13
|
-
"iat": 1766931602
|
|
14
|
-
}
|
|
15
|
-
]
|
|
16
|
-
},
|
|
17
|
-
"cookieKeys": [
|
|
18
|
-
"Vb3JNLAlJHCOu5u73eUA_rzlc9aJ0_WCQCu9RWV5WL4",
|
|
19
|
-
"5xCVtYihgadSlvy1QRD_DcU4_9mI_Ggn0DrngzPdiyM"
|
|
20
|
-
],
|
|
21
|
-
"createdAt": "2025-12-28T14:20:02.080Z"
|
|
22
|
-
}
|
package/test-dpop-flow.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import * as jose from 'jose';
|
|
2
|
-
import crypto from 'crypto';
|
|
3
|
-
|
|
4
|
-
const BASE = 'http://localhost:4000';
|
|
5
|
-
|
|
6
|
-
// Create DPoP proof
|
|
7
|
-
async function createDpopProof(privateKey, publicJwk, method, url, ath = null) {
|
|
8
|
-
const payload = {
|
|
9
|
-
jti: crypto.randomUUID(),
|
|
10
|
-
htm: method,
|
|
11
|
-
htu: url,
|
|
12
|
-
iat: Math.floor(Date.now() / 1000),
|
|
13
|
-
};
|
|
14
|
-
if (ath) payload.ath = ath;
|
|
15
|
-
|
|
16
|
-
return new jose.SignJWT(payload)
|
|
17
|
-
.setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk })
|
|
18
|
-
.sign(privateKey);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function main() {
|
|
22
|
-
console.log('=== Testing DPoP Auth Flow ===\n');
|
|
23
|
-
|
|
24
|
-
// 1. Generate key pair
|
|
25
|
-
const { privateKey, publicKey } = await jose.generateKeyPair('ES256');
|
|
26
|
-
const publicJwk = await jose.exportJWK(publicKey);
|
|
27
|
-
const jkt = await jose.calculateJwkThumbprint(publicJwk, 'sha256');
|
|
28
|
-
console.log('1. Generated DPoP key pair, thumbprint:', jkt.substring(0, 20) + '...\n');
|
|
29
|
-
|
|
30
|
-
// 2. Register client dynamically
|
|
31
|
-
console.log('2. Registering client...');
|
|
32
|
-
const regRes = await fetch(`${BASE}/idp/reg`, {
|
|
33
|
-
method: 'POST',
|
|
34
|
-
headers: { 'Content-Type': 'application/json' },
|
|
35
|
-
body: JSON.stringify({
|
|
36
|
-
redirect_uris: ['https://tester'],
|
|
37
|
-
token_endpoint_auth_method: 'none',
|
|
38
|
-
grant_types: ['authorization_code'],
|
|
39
|
-
response_types: ['code'],
|
|
40
|
-
}),
|
|
41
|
-
});
|
|
42
|
-
const client = await regRes.json();
|
|
43
|
-
console.log(' Client ID:', client.client_id, '\n');
|
|
44
|
-
|
|
45
|
-
// 3. Generate PKCE
|
|
46
|
-
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
47
|
-
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
48
|
-
console.log('3. Generated PKCE challenge\n');
|
|
49
|
-
|
|
50
|
-
// 4. Authorization request - WITH dpop_jkt parameter
|
|
51
|
-
console.log('4. Starting authorization (with dpop_jkt)...');
|
|
52
|
-
const authUrl = new URL(`${BASE}/idp/auth`);
|
|
53
|
-
authUrl.searchParams.set('client_id', client.client_id);
|
|
54
|
-
authUrl.searchParams.set('redirect_uri', 'https://tester');
|
|
55
|
-
authUrl.searchParams.set('response_type', 'code');
|
|
56
|
-
authUrl.searchParams.set('scope', 'openid');
|
|
57
|
-
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
58
|
-
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
59
|
-
authUrl.searchParams.set('dpop_jkt', jkt); // KEY: Include dpop_jkt!
|
|
60
|
-
|
|
61
|
-
const authRes = await fetch(authUrl, { redirect: 'manual' });
|
|
62
|
-
const interactionUrl = authRes.headers.get('location');
|
|
63
|
-
console.log(' Redirected to:', interactionUrl ? interactionUrl.substring(0, 50) + '...' : 'none');
|
|
64
|
-
console.log(' Status:', authRes.status, '\n');
|
|
65
|
-
|
|
66
|
-
// 5. Get interaction session cookie
|
|
67
|
-
const rawCookies = authRes.headers.get('set-cookie') || '';
|
|
68
|
-
// Extract just name=value from each Set-Cookie, ignore attributes
|
|
69
|
-
const cookieValues = rawCookies.split(/, (?=[^;]+=[^;]+)/).map(c => c.split(';')[0]).join('; ');
|
|
70
|
-
console.log('5. Got cookies:', cookieValues ? cookieValues.substring(0, 80) + '...' : 'none\n');
|
|
71
|
-
|
|
72
|
-
// 6. Login
|
|
73
|
-
console.log('6. Logging in...');
|
|
74
|
-
const uid = interactionUrl ? interactionUrl.match(/interaction\/([^/?]+)/)?.[1] : null;
|
|
75
|
-
if (!uid) {
|
|
76
|
-
console.log(' ERROR: No interaction UID found');
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
const loginRes = await fetch(`${BASE}/idp/interaction/${uid}`, {
|
|
80
|
-
method: 'POST',
|
|
81
|
-
headers: {
|
|
82
|
-
'Content-Type': 'application/json',
|
|
83
|
-
Cookie: cookieValues,
|
|
84
|
-
},
|
|
85
|
-
body: JSON.stringify({ email: 'alice@example.com', password: 'alicepassword123' }),
|
|
86
|
-
});
|
|
87
|
-
let loginBody;
|
|
88
|
-
const loginText = await loginRes.text();
|
|
89
|
-
try {
|
|
90
|
-
loginBody = JSON.parse(loginText);
|
|
91
|
-
} catch (e) {
|
|
92
|
-
console.log(' Login response (text):', loginText.substring(0, 200));
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
console.log(' Login response:', loginRes.status, loginBody.location ? loginBody.location.substring(0, 50) : '');
|
|
96
|
-
|
|
97
|
-
// 7. Follow auth resume
|
|
98
|
-
console.log('\n7. Following auth resume...');
|
|
99
|
-
const resumeUrl = loginBody.location;
|
|
100
|
-
if (!resumeUrl) {
|
|
101
|
-
console.log(' ERROR: No resume URL');
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
const fullResumeUrl = resumeUrl.startsWith('http') ? resumeUrl : `${BASE}${resumeUrl}`;
|
|
105
|
-
const resumeRes = await fetch(fullResumeUrl, {
|
|
106
|
-
redirect: 'manual',
|
|
107
|
-
headers: { Cookie: cookieValues },
|
|
108
|
-
});
|
|
109
|
-
const callbackUrl = resumeRes.headers.get('location');
|
|
110
|
-
console.log(' Resume status:', resumeRes.status);
|
|
111
|
-
console.log(' Callback URL:', callbackUrl ? callbackUrl.substring(0, 80) + '...' : 'none');
|
|
112
|
-
|
|
113
|
-
// 8. Extract code
|
|
114
|
-
const codeMatch = callbackUrl ? callbackUrl.match(/code=([^&]+)/) : null;
|
|
115
|
-
const code = codeMatch ? codeMatch[1] : null;
|
|
116
|
-
if (!code) {
|
|
117
|
-
console.log(' ERROR: No code in callback');
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
console.log(' Code:', code.substring(0, 20) + '...\n');
|
|
121
|
-
|
|
122
|
-
// 9. Token exchange with DPoP
|
|
123
|
-
console.log('8. Exchanging code for token (with DPoP)...');
|
|
124
|
-
const dpopProof = await createDpopProof(privateKey, publicJwk, 'POST', `${BASE}/idp/token`);
|
|
125
|
-
const tokenRes = await fetch(`${BASE}/idp/token`, {
|
|
126
|
-
method: 'POST',
|
|
127
|
-
headers: {
|
|
128
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
129
|
-
DPoP: dpopProof,
|
|
130
|
-
},
|
|
131
|
-
body: new URLSearchParams({
|
|
132
|
-
grant_type: 'authorization_code',
|
|
133
|
-
code: code,
|
|
134
|
-
redirect_uri: 'https://tester',
|
|
135
|
-
client_id: client.client_id,
|
|
136
|
-
code_verifier: codeVerifier,
|
|
137
|
-
}).toString(),
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
console.log(' Token response status:', tokenRes.status);
|
|
141
|
-
const tokenBody = await tokenRes.text();
|
|
142
|
-
console.log(' Token response:', tokenBody.substring(0, 300));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
main().catch(err => {
|
|
146
|
-
console.error('Error:', err.message);
|
|
147
|
-
console.error(err.stack);
|
|
148
|
-
});
|
package/test-nostr-acl.js
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test script for did:nostr in ACL files
|
|
3
|
-
*
|
|
4
|
-
* Tests:
|
|
5
|
-
* 1. Create a container with restricted access
|
|
6
|
-
* 2. Set ACL with did:nostr agent
|
|
7
|
-
* 3. Verify Nostr auth grants access
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools';
|
|
11
|
-
import { getToken } from 'nostr-tools/nip98';
|
|
12
|
-
|
|
13
|
-
const BASE_URL = process.env.TEST_URL || 'http://localhost:4000';
|
|
14
|
-
|
|
15
|
-
async function main() {
|
|
16
|
-
console.log('=== did:nostr ACL Authorization Test ===\n');
|
|
17
|
-
|
|
18
|
-
// Generate a keypair for testing
|
|
19
|
-
const sk = generateSecretKey();
|
|
20
|
-
const pk = getPublicKey(sk);
|
|
21
|
-
const didNostr = `did:nostr:${pk}`;
|
|
22
|
-
|
|
23
|
-
console.log('1. Generated keypair');
|
|
24
|
-
console.log(` Pubkey: ${pk.slice(0, 16)}...`);
|
|
25
|
-
console.log(` DID: ${didNostr.slice(0, 24)}...\n`);
|
|
26
|
-
|
|
27
|
-
// Create a unique test container
|
|
28
|
-
const testPath = `/demo/nostr-acl-test-${Date.now()}/`;
|
|
29
|
-
const containerUrl = `${BASE_URL}${testPath}`;
|
|
30
|
-
|
|
31
|
-
console.log(`2. Creating test container: ${testPath}`);
|
|
32
|
-
|
|
33
|
-
// Create container (unauthenticated - should work on public parent)
|
|
34
|
-
const createRes = await fetch(containerUrl, {
|
|
35
|
-
method: 'PUT',
|
|
36
|
-
headers: { 'Content-Type': 'text/turtle' },
|
|
37
|
-
body: ''
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
if (!createRes.ok && createRes.status !== 201) {
|
|
41
|
-
console.log(` Failed to create container: ${createRes.status}`);
|
|
42
|
-
// Try anyway
|
|
43
|
-
} else {
|
|
44
|
-
console.log(` Created: ${createRes.status}\n`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Create ACL with did:nostr agent (Turtle format)
|
|
48
|
-
const aclUrl = `${containerUrl}.acl`;
|
|
49
|
-
const aclContent = `
|
|
50
|
-
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
|
|
51
|
-
|
|
52
|
-
<#nostrAccess>
|
|
53
|
-
a acl:Authorization;
|
|
54
|
-
acl:agent <${didNostr}>;
|
|
55
|
-
acl:accessTo <${containerUrl}>;
|
|
56
|
-
acl:default <${containerUrl}>;
|
|
57
|
-
acl:mode acl:Read, acl:Write, acl:Control.
|
|
58
|
-
`;
|
|
59
|
-
|
|
60
|
-
console.log('3. Creating ACL with did:nostr agent');
|
|
61
|
-
console.log(` ACL URL: ${aclUrl}`);
|
|
62
|
-
console.log(` Agent: ${didNostr.slice(0, 40)}...`);
|
|
63
|
-
|
|
64
|
-
const aclRes = await fetch(aclUrl, {
|
|
65
|
-
method: 'PUT',
|
|
66
|
-
headers: { 'Content-Type': 'text/turtle' },
|
|
67
|
-
body: aclContent
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
console.log(` ACL created: ${aclRes.status}\n`);
|
|
71
|
-
|
|
72
|
-
// Verify ACL was saved correctly
|
|
73
|
-
console.log('4. Verifying ACL content');
|
|
74
|
-
const aclCheck = await fetch(aclUrl, {
|
|
75
|
-
headers: { 'Accept': 'text/turtle' }
|
|
76
|
-
});
|
|
77
|
-
const savedAcl = await aclCheck.text();
|
|
78
|
-
console.log(` ACL response: ${aclCheck.status}`);
|
|
79
|
-
console.log(` Contains did:nostr: ${savedAcl.includes('did:nostr:')}\n`);
|
|
80
|
-
|
|
81
|
-
// Test 1: Access WITHOUT auth (should be denied)
|
|
82
|
-
console.log('5. Testing access WITHOUT auth (should be 401/403)...');
|
|
83
|
-
const noAuthRes = await fetch(containerUrl);
|
|
84
|
-
console.log(` Status: ${noAuthRes.status} ${noAuthRes.status === 401 || noAuthRes.status === 403 ? '✓' : '✗'}\n`);
|
|
85
|
-
|
|
86
|
-
// Test 2: Access WITH correct Nostr auth
|
|
87
|
-
console.log('6. Testing access WITH correct Nostr auth...');
|
|
88
|
-
const token = await getToken(containerUrl, 'GET', (event) => finalizeEvent(event, sk));
|
|
89
|
-
|
|
90
|
-
const authRes = await fetch(containerUrl, {
|
|
91
|
-
headers: {
|
|
92
|
-
'Authorization': `Nostr ${token}`,
|
|
93
|
-
'Accept': 'text/turtle'
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
console.log(` Status: ${authRes.status}`);
|
|
98
|
-
|
|
99
|
-
if (authRes.status === 200) {
|
|
100
|
-
console.log(' ✓ ACCESS GRANTED - did:nostr ACL working!\n');
|
|
101
|
-
} else {
|
|
102
|
-
console.log(' ✗ Access denied');
|
|
103
|
-
const body = await authRes.text();
|
|
104
|
-
console.log(` Body: ${body.slice(0, 200)}\n`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Test 3: Access with DIFFERENT Nostr key (should be denied)
|
|
108
|
-
console.log('7. Testing with DIFFERENT Nostr key (should be denied)...');
|
|
109
|
-
const wrongSk = generateSecretKey();
|
|
110
|
-
const wrongToken = await getToken(containerUrl, 'GET', (event) => finalizeEvent(event, wrongSk));
|
|
111
|
-
|
|
112
|
-
const wrongAuthRes = await fetch(containerUrl, {
|
|
113
|
-
headers: {
|
|
114
|
-
'Authorization': `Nostr ${wrongToken}`,
|
|
115
|
-
'Accept': 'text/turtle'
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
console.log(` Status: ${wrongAuthRes.status} ${wrongAuthRes.status === 403 ? '✓' : '✗'}\n`);
|
|
120
|
-
|
|
121
|
-
// Clean up
|
|
122
|
-
console.log('8. Cleaning up test container...');
|
|
123
|
-
const deleteToken = await getToken(containerUrl, 'DELETE', (event) => finalizeEvent(event, sk));
|
|
124
|
-
const deleteRes = await fetch(containerUrl, {
|
|
125
|
-
method: 'DELETE',
|
|
126
|
-
headers: { 'Authorization': `Nostr ${deleteToken}` }
|
|
127
|
-
});
|
|
128
|
-
console.log(` Delete: ${deleteRes.status}\n`);
|
|
129
|
-
|
|
130
|
-
// Summary
|
|
131
|
-
console.log('=== Test Summary ===');
|
|
132
|
-
console.log(`No auth: ${noAuthRes.status === 401 || noAuthRes.status === 403 ? 'PASS' : 'FAIL'} (${noAuthRes.status})`);
|
|
133
|
-
console.log(`Correct key: ${authRes.status === 200 ? 'PASS' : 'FAIL'} (${authRes.status})`);
|
|
134
|
-
console.log(`Wrong key: ${wrongAuthRes.status === 403 ? 'PASS' : 'FAIL'} (${wrongAuthRes.status})`);
|
|
135
|
-
|
|
136
|
-
const allPassed = (noAuthRes.status === 401 || noAuthRes.status === 403) &&
|
|
137
|
-
authRes.status === 200 &&
|
|
138
|
-
wrongAuthRes.status === 403;
|
|
139
|
-
|
|
140
|
-
console.log(`\nOverall: ${allPassed ? 'ALL TESTS PASSED ✓' : 'SOME TESTS FAILED ✗'}`);
|
|
141
|
-
process.exit(allPassed ? 0 : 1);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
main().catch(console.error);
|