javascript-solid-server 0.0.23 → 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/README.md +131 -122
- package/package.json +1 -1
- package/src/handlers/resource.js +21 -0
- package/src/mashlib/index.js +18 -3
- package/src/server.js +32 -17
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
|
-
##
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
##
|
|
453
|
+
## Comparison
|
|
405
454
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
### WebSocket Notifications
|
|
448
|
-
|
|
449
|
-
Enable real-time notifications for resource changes:
|
|
462
|
+
## Performance
|
|
450
463
|
|
|
451
|
-
|
|
452
|
-
const server = createServer({ notifications: true });
|
|
453
|
-
```
|
|
464
|
+
This server is designed for speed. Benchmark results on a typical development machine:
|
|
454
465
|
|
|
455
|
-
|
|
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
|
-
|
|
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
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,
|
package/src/mashlib/index.js
CHANGED
|
@@ -55,16 +55,31 @@ export function shouldServeMashlib(request, mashlibEnabled, contentType) {
|
|
|
55
55
|
return false;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
//
|
|
59
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|