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.
@@ -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.17)
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 for browsing RDF resources
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 data browser | false |
143
- | `--mashlib-version <ver>` | Mashlib version | 2.0.0 |
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
- ## Pod Structure
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
- /alice/
265
- ├── index.html # WebID profile (HTML with JSON-LD)
266
- ├── .acl # Root ACL (owner + public read)
267
- ├── inbox/ # Notifications (public append)
268
- │ └── .acl
269
- ├── public/ # Public files
270
- ├── private/ # Private files (owner only)
271
- │ └── .acl
272
- └── settings/ # User preferences (owner only)
273
- ├── .acl
274
- ├── prefs
275
- ├── publicTypeIndex
276
- └── privateTypeIndex
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
- ## Configuration
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
- ### WebSocket Notifications
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
- Enable real-time notifications for resource changes:
462
+ ## Performance
436
463
 
437
- ```javascript
438
- const server = createServer({ notifications: true });
439
- ```
464
+ This server is designed for speed. Benchmark results on a typical development machine:
440
465
 
441
- Clients discover the WebSocket URL via the `Updates-Via` header:
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
- curl -I http://localhost:3000/alice/public/
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 for RDF resources')
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 to use (default: 2.0.0)')
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.mashlib) console.log(` Mashlib: v${config.mashlibVersion} (data browser enabled)`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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.mashlib ? `v${config.mashlibVersion}` : 'disabled'}`);
206
+ console.log(` Mashlib: ${config.mashlibCdn ? `CDN v${config.mashlibVersion}` : config.mashlib ? 'local' : 'disabled'}`);
205
207
  console.log('─'.repeat(40));
206
208
  }
@@ -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
- const html = generateDatabrowserHtml(resourceUrl, request.mashlibVersion);
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
 
@@ -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
- * @param {string} resourceUrl - The URL of the resource being viewed
14
- * @param {string} version - Mashlib version (default: '2.0.0')
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, version = '2.0.0') {
18
- const cdnUrl = `${CDN_BASE}@${version}/dist`;
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
- return `<!doctype html>
21
- <html>
22
- <head>
23
- <meta charset="utf-8"/>
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
- const accept = request.headers.accept || '';
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);
@@ -1,2 +0,0 @@
1
- subjects: file:/config/test-subjects.ttl
2
- target: jss
@@ -1,6 +0,0 @@
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/> .
@@ -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,3 +0,0 @@
1
- {
2
- "credtest@example.com": "ba3591b1-4653-4c64-9661-57dc355e5acc"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "credtest": "ba3591b1-4653-4c64-9661-57dc355e5acc"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "http://localhost:3101/credtest/#me": "ba3591b1-4653-4c64-9661-57dc355e5acc"
3
- }
@@ -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);