javascript-solid-server 0.0.23 → 0.0.25

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/README.md CHANGED
@@ -2,56 +2,6 @@
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
7
  ### Implemented (v0.0.23)
@@ -261,22 +211,103 @@ curl -X PUT http://localhost:3000/alice/public/new-resource.json \
261
211
  -d '{"@id": "#new"}'
262
212
  ```
263
213
 
264
- ## Pod Structure
214
+ ## Philosophy: JSON-LD First
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:
265
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 });
266
245
  ```
267
- /alice/
268
- ├── index.html # WebID profile (HTML with JSON-LD)
269
- ├── .acl # Root ACL (owner + public read)
270
- ├── inbox/ # Notifications (public append)
271
- │ └── .acl
272
- ├── public/ # Public files
273
- ├── private/ # Private files (owner only)
274
- │ └── .acl
275
- └── settings/ # User preferences (owner only)
276
- ├── .acl
277
- ├── prefs
278
- ├── publicTypeIndex
279
- └── 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)
280
311
  ```
281
312
 
282
313
  ## Authentication
@@ -350,6 +381,24 @@ curl -H "Authorization: DPoP ACCESS_TOKEN" \
350
381
  http://localhost:3000/alice/private/
351
382
  ```
352
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
+
353
402
  ## Subdomain Mode (XSS Protection)
354
403
 
355
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.
@@ -401,70 +450,30 @@ curl -X POST https://example.com/.pods \
401
450
  -d '{"name": "alice"}'
402
451
  ```
403
452
 
404
- ## Configuration
453
+ ## Comparison
405
454
 
406
- ```javascript
407
- createServer({
408
- logger: true, // Enable Fastify logging (default: true)
409
- conneg: false, // Enable content negotiation (default: false)
410
- notifications: false, // Enable WebSocket notifications (default: false)
411
- subdomains: false, // Enable subdomain-based pods (default: false)
412
- baseDomain: null, // Base domain for subdomains (e.g., "example.com")
413
- mashlib: false, // Enable Mashlib data browser - local mode (default: false)
414
- mashlibCdn: false, // Enable Mashlib data browser - CDN mode (default: false)
415
- mashlibVersion: '2.0.0', // Mashlib version for CDN mode
416
- });
417
- ```
418
-
419
- ### Mashlib Data Browser
420
-
421
- Enable the [SolidOS Mashlib](https://github.com/SolidOS/mashlib) data browser for RDF resources. Two modes are available:
422
-
423
- **CDN Mode** (recommended for getting started):
424
- ```bash
425
- jss start --mashlib-cdn --conneg
426
- ```
427
- Loads mashlib from unpkg.com CDN. Zero footprint - no local files needed.
428
-
429
- **Local Mode** (for production/offline):
430
- ```bash
431
- jss start --mashlib --conneg
432
- ```
433
- Serves mashlib from `src/mashlib-local/dist/`. Requires building mashlib locally:
434
- ```bash
435
- cd src/mashlib-local
436
- npm install && npm run build
437
- ```
438
-
439
- **How it works:**
440
- 1. Browser requests `/alice/public/data.ttl` with `Accept: text/html`
441
- 2. Server returns Mashlib HTML wrapper
442
- 3. Mashlib fetches the actual data via content negotiation
443
- 4. Mashlib renders an interactive, editable view
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 |
444
461
 
445
- **Note:** Mashlib works best with `--conneg` enabled for Turtle support. Pod profiles (`/alice/`) continue to serve our JSON-LD-in-HTML format.
446
-
447
- ### WebSocket Notifications
448
-
449
- Enable real-time notifications for resource changes:
462
+ ## Performance
450
463
 
451
- ```javascript
452
- const server = createServer({ notifications: true });
453
- ```
464
+ This server is designed for speed. Benchmark results on a typical development machine:
454
465
 
455
- 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 |
456
473
 
474
+ Run benchmarks yourself:
457
475
  ```bash
458
- curl -I http://localhost:3000/alice/public/
459
- # Updates-Via: ws://localhost:3000/.notifications
460
- ```
461
-
462
- Protocol (solid-0.1, compatible with SolidOS):
463
- ```
464
- Server: protocol solid-0.1
465
- Client: sub http://localhost:3000/alice/public/data.json
466
- Server: ack http://localhost:3000/alice/public/data.json
467
- Server: pub http://localhost:3000/alice/public/data.json (on change)
476
+ npm run benchmark
468
477
  ```
469
478
 
470
479
  ## Running Tests
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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,
@@ -276,6 +276,17 @@ export async function createProvider(issuer) {
276
276
  // Clock tolerance for token validation
277
277
  clockTolerance: 60, // 60 seconds
278
278
 
279
+ // Allow CORS for public clients from any origin
280
+ // This is needed for web apps like Mashlib loaded from CDN
281
+ clientBasedCORS: (ctx, origin, client) => {
282
+ // Allow all origins for public clients (no client_secret)
283
+ if (client.tokenEndpointAuthMethod === 'none') {
284
+ return true;
285
+ }
286
+ // For confidential clients, check registered origins
287
+ return false;
288
+ },
289
+
279
290
  // Render errors
280
291
  renderError: async (ctx, out, error) => {
281
292
  ctx.type = 'html';
@@ -55,16 +55,31 @@ export function shouldServeMashlib(request, mashlibEnabled, contentType) {
55
55
  return false;
56
56
  }
57
57
 
58
- // Don't serve mashlib for iframe/embed requests (prevents recursive loop)
59
- if (secFetchDest === 'iframe' || secFetchDest === 'embed' || secFetchDest === 'object') {
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') {
60
62
  return false;
61
63
  }
62
64
 
63
- // 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,..."
64
68
  if (!accept.includes('text/html')) {
65
69
  return false;
66
70
  }
67
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
+
68
83
  // Only serve mashlib for RDF content types
69
84
  const rdfTypes = [
70
85
  'text/turtle',
package/src/server.js CHANGED
@@ -157,25 +157,40 @@ export function createServer(options = {}) {
157
157
 
158
158
  // Mashlib static files (served from root like NSS does)
159
159
  if (mashlibEnabled) {
160
- const mashlibDir = join(__dirname, 'mashlib-local', 'dist');
161
- const mashlibFiles = {
162
- '/mashlib.min.js': { file: 'mashlib.min.js', type: 'application/javascript' },
163
- '/mashlib.min.js.map': { file: 'mashlib.min.js.map', type: 'application/json' },
164
- '/mash.css': { file: 'mash.css', type: 'text/css' },
165
- '/mash.css.map': { file: 'mash.css.map', type: 'application/json' },
166
- '/841.mashlib.min.js': { file: '841.mashlib.min.js', type: 'application/javascript' },
167
- '/841.mashlib.min.js.map': { file: '841.mashlib.min.js.map', type: 'application/json' }
168
- };
169
-
170
- for (const [path, config] of Object.entries(mashlibFiles)) {
171
- fastify.get(path, async (request, reply) => {
172
- try {
173
- const content = await readFile(join(mashlibDir, config.file));
174
- return reply.type(config.type).send(content);
175
- } catch {
176
- return reply.code(404).send({ error: 'Not Found' });
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}`);
177
170
  }
178
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
+ }
179
194
  }
180
195
  }
181
196