javascript-solid-server 0.0.22 → 0.0.23

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
@@ -54,7 +54,7 @@ npm run benchmark
54
54
 
55
55
  ## Features
56
56
 
57
- ### Implemented (v0.0.17)
57
+ ### Implemented (v0.0.23)
58
58
 
59
59
  - **LDP CRUD Operations** - GET, PUT, POST, DELETE, HEAD
60
60
  - **N3 Patch** - Solid's native patch format for RDF updates
@@ -66,11 +66,13 @@ npm run benchmark
66
66
  - **Container Management** - Create, list, and manage containers
67
67
  - **Multi-user Pods** - Path-based (`/alice/`) or subdomain-based (`alice.example.com`)
68
68
  - **Subdomain Mode** - XSS protection via origin isolation
69
- - **Mashlib Data Browser** - Optional SolidOS UI for browsing RDF resources
69
+ - **Mashlib Data Browser** - Optional SolidOS UI (CDN or local hosting)
70
70
  - **WebID Profiles** - JSON-LD structured data in HTML at pod root
71
71
  - **Web Access Control (WAC)** - `.acl` file-based authorization
72
72
  - **Solid-OIDC Identity Provider** - Built-in IdP with DPoP, dynamic registration
73
73
  - **Solid-OIDC Resource Server** - Accept DPoP-bound access tokens from external IdPs
74
+ - **NSS-style Registration** - Username/password auth compatible with Solid apps
75
+ - **Nostr Authentication** - NIP-98 HTTP Auth with Schnorr signatures
74
76
  - **Simple Auth Tokens** - Built-in token authentication for development
75
77
  - **Content Negotiation** - Optional Turtle <-> JSON-LD conversion
76
78
  - **CORS Support** - Full cross-origin resource sharing
@@ -139,8 +141,9 @@ jss --help # Show help
139
141
  | `--idp-issuer <url>` | IdP issuer URL | (auto) |
140
142
  | `--subdomains` | Enable subdomain-based pods | false |
141
143
  | `--base-domain <domain>` | Base domain for subdomains | - |
142
- | `--mashlib` | Enable Mashlib data browser | false |
143
- | `--mashlib-version <ver>` | Mashlib version | 2.0.0 |
144
+ | `--mashlib` | Enable Mashlib (local mode) | false |
145
+ | `--mashlib-cdn` | Enable Mashlib (CDN mode) | false |
146
+ | `--mashlib-version <ver>` | Mashlib CDN version | 2.0.0 |
144
147
  | `-q, --quiet` | Suppress logs | false |
145
148
 
146
149
  ### Environment Variables
@@ -407,24 +410,35 @@ createServer({
407
410
  notifications: false, // Enable WebSocket notifications (default: false)
408
411
  subdomains: false, // Enable subdomain-based pods (default: false)
409
412
  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
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
412
416
  });
413
417
  ```
414
418
 
415
419
  ### Mashlib Data Browser
416
420
 
417
- Enable the [SolidOS Mashlib](https://github.com/SolidOS/mashlib) data browser for RDF resources:
421
+ Enable the [SolidOS Mashlib](https://github.com/SolidOS/mashlib) data browser for RDF resources. Two modes are available:
418
422
 
423
+ **CDN Mode** (recommended for getting started):
419
424
  ```bash
420
- jss start --mashlib --conneg
425
+ jss start --mashlib-cdn --conneg
421
426
  ```
427
+ Loads mashlib from unpkg.com CDN. Zero footprint - no local files needed.
422
428
 
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.
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
+ ```
424
438
 
425
439
  **How it works:**
426
440
  1. Browser requests `/alice/public/data.ttl` with `Accept: text/html`
427
- 2. Server returns Mashlib HTML wrapper (loads JS/CSS from CDN)
441
+ 2. Server returns Mashlib HTML wrapper
428
442
  3. Mashlib fetches the actual data via content negotiation
429
443
  4. Mashlib renders an interactive, editable view
430
444
 
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.23",
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
  }
@@ -145,7 +145,9 @@ export async function handleGet(request, reply) {
145
145
  // Check if we should serve Mashlib data browser
146
146
  // Only for RDF resources when Accept: text/html is requested
147
147
  if (shouldServeMashlib(request, request.mashlibEnabled, storedContentType)) {
148
- const html = generateDatabrowserHtml(resourceUrl, request.mashlibVersion);
148
+ // Pass CDN version if using CDN mode, null for local mode
149
+ const cdnVersion = request.mashlibCdn ? request.mashlibVersion : null;
150
+ const html = generateDatabrowserHtml(resourceUrl, cdnVersion);
149
151
  const headers = getAllHeaders({
150
152
  isContainer: false,
151
153
  etag: stats.etag,
@@ -155,6 +157,10 @@ export async function handleGet(request, reply) {
155
157
  connegEnabled
156
158
  });
157
159
  headers['Vary'] = 'Accept';
160
+ headers['X-Frame-Options'] = 'DENY';
161
+ headers['Content-Security-Policy'] = "frame-ancestors 'none'";
162
+ // Don't cache the HTML wrapper - always negotiate fresh
163
+ headers['Cache-Control'] = 'no-store';
158
164
 
159
165
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
160
166
  return reply.type('text/html').send(html);
@@ -191,7 +197,7 @@ export async function handleGet(request, reply) {
191
197
  resourceUrl,
192
198
  connegEnabled
193
199
  });
194
- headers['Vary'] = getVaryHeader(connegEnabled);
200
+ headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);
195
201
 
196
202
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
197
203
  return reply.send(outputContent);
@@ -209,7 +215,7 @@ export async function handleGet(request, reply) {
209
215
  resourceUrl,
210
216
  connegEnabled
211
217
  });
212
- headers['Vary'] = getVaryHeader(connegEnabled);
218
+ headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);
213
219
 
214
220
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
215
221
  return reply.send(content);
@@ -353,7 +359,7 @@ export async function handlePut(request, reply) {
353
359
  const origin = request.headers.origin;
354
360
  const headers = getAllHeaders({ isContainer: false, origin, resourceUrl, connegEnabled });
355
361
  headers['Location'] = resourceUrl;
356
- headers['Vary'] = getVaryHeader(connegEnabled);
362
+ headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);
357
363
 
358
364
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
359
365
 
@@ -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,11 +48,17 @@ 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
+ // Don't serve mashlib for iframe/embed requests (prevents recursive loop)
59
+ if (secFetchDest === 'iframe' || secFetchDest === 'embed' || secFetchDest === 'object') {
60
+ return false;
61
+ }
69
62
 
70
63
  // Must explicitly accept HTML
71
64
  if (!accept.includes('text/html')) {
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,30 @@ 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
+ 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' });
177
+ }
178
+ });
179
+ }
180
+ }
181
+
147
182
  // LDP routes - using wildcard routing
148
183
  fastify.get('/*', handleGet);
149
184
  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);