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.
- package/.claude/settings.local.json +26 -1
- package/README.md +24 -10
- package/bin/jss.js +10 -4
- package/package.json +1 -1
- package/src/config.js +3 -1
- package/src/handlers/resource.js +10 -4
- package/src/mashlib/index.js +34 -41
- package/src/rdf/conneg.js +3 -2
- package/src/server.js +37 -2
- package/cth-config/application.yaml +0 -2
- package/cth-config/jss.ttl +0 -6
- package/cth-config/test-subjects.ttl +0 -14
- package/test-data-idp-accounts/.idp/accounts/_email_index.json +0 -3
- package/test-data-idp-accounts/.idp/accounts/_username_index.json +0 -3
- package/test-data-idp-accounts/.idp/accounts/_webid_index.json +0 -3
- package/test-data-idp-accounts/.idp/accounts/ba3591b1-4653-4c64-9661-57dc355e5acc.json +0 -10
- package/test-data-idp-accounts/.idp/keys/jwks.json +0 -22
- package/test-dpop-flow.js +0 -148
- package/test-nostr-acl.js +0 -144
|
@@ -72,7 +72,32 @@
|
|
|
72
72
|
"WebFetch(domain:melvincarvalho.github.io)",
|
|
73
73
|
"WebFetch(domain:dev.to)",
|
|
74
74
|
"WebFetch(domain:solidproject.org)",
|
|
75
|
-
"WebFetch(domain:www.w3.org)"
|
|
75
|
+
"WebFetch(domain:www.w3.org)",
|
|
76
|
+
"Bash(wc:*)",
|
|
77
|
+
"Bash(TOKEN=\"eyJraW5kIjoyNzIzNSwidGFncyI6W1sidSIsImh0dHA6Ly9sb2NhbGhvc3Q6NDAwMC9kZW1vL25vc3RyLXpvbmUvIl0sWyJtZXRob2QiLCJHRVQiXV0sImNyZWF0ZWRfYXQiOjE3NjY5MzQ1NjksImNvbnRlbnQiOiIiLCJwdWJrZXkiOiI4OTg5OWNmOWEyNGE5ZTdlMTNmODU3MGRkMGI1MmJiOTQyMjllNDI2OGM1MGQ1OWZhNjdhMzQ0MGQ0NmFhZTdkIiwiaWQiOiJiNTUyMDUyOTVmYmQwYzhjZDYwMzk1NTgwOWYxZGM5Y2MwMjdlY2U4N2NjYmNlNzcwNWY2MjdmNmQ0ODk1MGJkIiwic2lnIjoiOWYzN2Y0NzIyZDlkNmFmZGQ5OTNkYTM0MDg2MWQ2YzQ4MmY1NzQ1MmFmZTIwZmY2YmI5OTAxNGIwOTU3NjUwMWZiNTgyZjEzNzNlZmVhNjI4ZDI5ZjlhMzhmZTgyODU0ODlmMzAzYzlmYmJjYWE0OTQxZjUyZGZlMWYxNzVkOWMifQ==\")",
|
|
78
|
+
"WebFetch(domain:solid-lite.org)",
|
|
79
|
+
"Bash(git push:*)",
|
|
80
|
+
"WebFetch(domain:linkedwebstorage.com)",
|
|
81
|
+
"WebFetch(domain:w3c.github.io)",
|
|
82
|
+
"WebFetch(domain:socialdocs.org)",
|
|
83
|
+
"WebFetch(domain:nosdav.com)",
|
|
84
|
+
"WebFetch(domain:sandy-mount.com)",
|
|
85
|
+
"WebFetch(domain:ditto.pub)",
|
|
86
|
+
"WebFetch(domain:blocktrails.org)",
|
|
87
|
+
"WebFetch(domain:microfed.org)",
|
|
88
|
+
"WebFetch(domain:soliddocs.org)",
|
|
89
|
+
"WebFetch(domain:agenticalliance.com)",
|
|
90
|
+
"WebFetch(domain:activitypub.rocks)",
|
|
91
|
+
"WebFetch(domain:nostrgit.org)",
|
|
92
|
+
"Bash(convert:*)",
|
|
93
|
+
"WebFetch(domain:instantdomainsearch.com)",
|
|
94
|
+
"Bash(for domain in jss.dev jss.sh jss.io jss.app solidserver.dev solid-server.dev)",
|
|
95
|
+
"Bash(do echo -n '$domain: ')",
|
|
96
|
+
"Bash(whois $domain)",
|
|
97
|
+
"Bash(done)",
|
|
98
|
+
"Bash(for domain in jss.dev jss.sh jss.io jss.app solidserver.dev)",
|
|
99
|
+
"Bash(host:*)",
|
|
100
|
+
"WebFetch(domain:nostr-components.github.io)"
|
|
76
101
|
]
|
|
77
102
|
}
|
|
78
103
|
}
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ npm run benchmark
|
|
|
54
54
|
|
|
55
55
|
## Features
|
|
56
56
|
|
|
57
|
-
### Implemented (v0.0.
|
|
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
|
|
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
|
|
143
|
-
| `--mashlib-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
53
|
+
.option('--mashlib', 'Enable Mashlib data browser (local mode, requires mashlib in node_modules)')
|
|
54
|
+
.option('--mashlib-cdn', 'Enable Mashlib data browser (CDN mode, no local files needed)')
|
|
54
55
|
.option('--no-mashlib', 'Disable Mashlib data browser')
|
|
55
|
-
.option('--mashlib-version <version>', 'Mashlib version
|
|
56
|
+
.option('--mashlib-version <version>', 'Mashlib version for CDN mode (default: 2.0.0)')
|
|
56
57
|
.option('-q, --quiet', 'Suppress log output')
|
|
57
58
|
.option('--print-config', 'Print configuration and exit')
|
|
58
59
|
.action(async (options) => {
|
|
@@ -91,7 +92,8 @@ program
|
|
|
91
92
|
root: config.root,
|
|
92
93
|
subdomains: config.subdomains,
|
|
93
94
|
baseDomain: config.baseDomain,
|
|
94
|
-
mashlib: config.mashlib,
|
|
95
|
+
mashlib: config.mashlib || config.mashlibCdn,
|
|
96
|
+
mashlibCdn: config.mashlibCdn,
|
|
95
97
|
mashlibVersion: config.mashlibVersion,
|
|
96
98
|
});
|
|
97
99
|
|
|
@@ -106,7 +108,11 @@ program
|
|
|
106
108
|
if (config.notifications) console.log(' WebSocket: enabled');
|
|
107
109
|
if (config.idp) console.log(` IdP: ${idpIssuer}`);
|
|
108
110
|
if (config.subdomains) console.log(` Subdomains: ${config.baseDomain} (XSS protection enabled)`);
|
|
109
|
-
if (config.
|
|
111
|
+
if (config.mashlibCdn) {
|
|
112
|
+
console.log(` Mashlib: v${config.mashlibVersion} (CDN mode)`);
|
|
113
|
+
} else if (config.mashlib) {
|
|
114
|
+
console.log(` Mashlib: local (data browser enabled)`);
|
|
115
|
+
}
|
|
110
116
|
console.log('\n Press Ctrl+C to stop\n');
|
|
111
117
|
}
|
|
112
118
|
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -39,6 +39,7 @@ export const defaults = {
|
|
|
39
39
|
|
|
40
40
|
// Mashlib data browser
|
|
41
41
|
mashlib: false,
|
|
42
|
+
mashlibCdn: false,
|
|
42
43
|
mashlibVersion: '2.0.0',
|
|
43
44
|
|
|
44
45
|
// Logging
|
|
@@ -68,6 +69,7 @@ const envMap = {
|
|
|
68
69
|
JSS_SUBDOMAINS: 'subdomains',
|
|
69
70
|
JSS_BASE_DOMAIN: 'baseDomain',
|
|
70
71
|
JSS_MASHLIB: 'mashlib',
|
|
72
|
+
JSS_MASHLIB_CDN: 'mashlibCdn',
|
|
71
73
|
JSS_MASHLIB_VERSION: 'mashlibVersion',
|
|
72
74
|
};
|
|
73
75
|
|
|
@@ -201,6 +203,6 @@ export function printConfig(config) {
|
|
|
201
203
|
console.log(` Notifications: ${config.notifications}`);
|
|
202
204
|
console.log(` IdP: ${config.idp ? (config.idpIssuer || 'enabled') : 'disabled'}`);
|
|
203
205
|
console.log(` Subdomains: ${config.subdomains ? (config.baseDomain || 'enabled') : 'disabled'}`);
|
|
204
|
-
console.log(` Mashlib: ${config.
|
|
206
|
+
console.log(` Mashlib: ${config.mashlibCdn ? `CDN v${config.mashlibVersion}` : config.mashlib ? 'local' : 'disabled'}`);
|
|
205
207
|
console.log('─'.repeat(40));
|
|
206
208
|
}
|
package/src/handlers/resource.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/mashlib/index.js
CHANGED
|
@@ -6,51 +6,38 @@
|
|
|
6
6
|
* we return this wrapper which then fetches and renders the data.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const CDN_BASE = 'https://unpkg.com/mashlib';
|
|
10
|
-
|
|
11
9
|
/**
|
|
12
10
|
* Generate Mashlib databrowser HTML
|
|
13
|
-
*
|
|
14
|
-
* @param {string}
|
|
11
|
+
*
|
|
12
|
+
* @param {string} resourceUrl - The URL of the resource being viewed (unused, kept for API compatibility)
|
|
13
|
+
* @param {string} cdnVersion - If provided, load mashlib from unpkg CDN (e.g., "2.0.0")
|
|
15
14
|
* @returns {string} HTML content
|
|
16
15
|
*/
|
|
17
|
-
export function generateDatabrowserHtml(resourceUrl,
|
|
18
|
-
|
|
16
|
+
export function generateDatabrowserHtml(resourceUrl, cdnVersion = null) {
|
|
17
|
+
if (cdnVersion) {
|
|
18
|
+
// CDN mode - use script.onload to ensure mashlib is fully loaded before init
|
|
19
|
+
// This avoids race conditions with defer + DOMContentLoaded
|
|
20
|
+
const cdnBase = `https://unpkg.com/mashlib@${cdnVersion}/dist`;
|
|
21
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>SolidOS Web App</title>
|
|
22
|
+
<link href="${cdnBase}/mash.css" rel="stylesheet"></head>
|
|
23
|
+
<body id="PageBody"><header id="PageHeader"></header>
|
|
24
|
+
<div class="TabulatorOutline" id="DummyUUID" role="main"><table id="outline"></table><div id="GlobalDashboard"></div></div>
|
|
25
|
+
<footer id="PageFooter"></footer>
|
|
26
|
+
<script>
|
|
27
|
+
(function() {
|
|
28
|
+
var s = document.createElement('script');
|
|
29
|
+
s.src = '${cdnBase}/mashlib.min.js';
|
|
30
|
+
s.onload = function() { panes.runDataBrowser(); };
|
|
31
|
+
s.onerror = function() { document.body.innerHTML = '<p>Failed to load Mashlib from CDN</p>'; };
|
|
32
|
+
document.head.appendChild(s);
|
|
33
|
+
})();
|
|
34
|
+
</script></body></html>`;
|
|
35
|
+
}
|
|
19
36
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
25
|
-
<title>SolidOS - ${escapeHtml(resourceUrl)}</title>
|
|
26
|
-
<script defer src="${cdnUrl}/mashlib.min.js"></script>
|
|
27
|
-
<link href="${cdnUrl}/mash.css" rel="stylesheet">
|
|
28
|
-
<script>
|
|
29
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
30
|
-
// runDataBrowser uses window.location to determine what to fetch
|
|
31
|
-
panes.runDataBrowser();
|
|
32
|
-
});
|
|
33
|
-
</script>
|
|
34
|
-
<style>
|
|
35
|
-
/* Loading indicator */
|
|
36
|
-
body:not(.loaded) #PageBody::before {
|
|
37
|
-
content: 'Loading SolidOS...';
|
|
38
|
-
display: block;
|
|
39
|
-
padding: 2em;
|
|
40
|
-
text-align: center;
|
|
41
|
-
color: #666;
|
|
42
|
-
}
|
|
43
|
-
</style>
|
|
44
|
-
</head>
|
|
45
|
-
<body id="PageBody">
|
|
46
|
-
<header id="PageHeader"></header>
|
|
47
|
-
<div class="TabulatorOutline" id="DummyUUID" role="main">
|
|
48
|
-
<table id="outline"></table>
|
|
49
|
-
<div id="GlobalDashboard"></div>
|
|
50
|
-
</div>
|
|
51
|
-
<footer id="PageFooter"></footer>
|
|
52
|
-
</body>
|
|
53
|
-
</html>`;
|
|
37
|
+
// Local mode - use defer (reliable when served locally)
|
|
38
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>SolidOS Web App</title><script>document.addEventListener('DOMContentLoaded', function() {
|
|
39
|
+
panes.runDataBrowser()
|
|
40
|
+
})</script><script defer="defer" src="/mashlib.min.js"></script><link href="/mash.css" rel="stylesheet"></head><body id="PageBody"><header id="PageHeader"></header><div class="TabulatorOutline" id="DummyUUID" role="main"><table id="outline"></table><div id="GlobalDashboard"></div></div><footer id="PageFooter"></footer></body></html>`;
|
|
54
41
|
}
|
|
55
42
|
|
|
56
43
|
/**
|
|
@@ -61,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
|
-
|
|
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);
|
package/cth-config/jss.ttl
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
@prefix doap: <http://usefulinc.com/ns/doap#> .
|
|
2
|
-
@prefix earl: <http://www.w3.org/ns/earl#> .
|
|
3
|
-
@prefix solid-test: <https://github.com/solid-contrib/specification-tests/blob/main/vocab.ttl#> .
|
|
4
|
-
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
|
5
|
-
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
6
|
-
|
|
7
|
-
<jss>
|
|
8
|
-
a earl:Software, earl:TestSubject ;
|
|
9
|
-
doap:name "JavaScript Solid Server" ;
|
|
10
|
-
doap:description "A minimal, fast, JSON-LD native Solid server" ;
|
|
11
|
-
doap:programming-language "JavaScript" ;
|
|
12
|
-
solid-test:serverRoot <http://localhost:4000/> ;
|
|
13
|
-
solid-test:skip "acp" ;
|
|
14
|
-
rdfs:comment "Uses WAC for access control" .
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "ba3591b1-4653-4c64-9661-57dc355e5acc",
|
|
3
|
-
"username": "credtest",
|
|
4
|
-
"email": "credtest@example.com",
|
|
5
|
-
"passwordHash": "$2b$10$tFYM8KuMVTFRpVMqZOYR4OKNreNLgCBqzZVTNAhpdBFUmGH1MFNBu",
|
|
6
|
-
"webId": "http://localhost:3101/credtest/#me",
|
|
7
|
-
"podName": "credtest",
|
|
8
|
-
"createdAt": "2025-12-28T14:20:02.176Z",
|
|
9
|
-
"lastLogin": "2025-12-28T14:20:02.579Z"
|
|
10
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"jwks": {
|
|
3
|
-
"keys": [
|
|
4
|
-
{
|
|
5
|
-
"kty": "EC",
|
|
6
|
-
"x": "Aa7l5-YrS54RU8xPfEphUTRwNBzSm6lxm84aqKjfrSg",
|
|
7
|
-
"y": "tWi_lhjqQhd43KdK5YqDg7ZzRSUZo3L0ytbiBTdPOWs",
|
|
8
|
-
"crv": "P-256",
|
|
9
|
-
"d": "x6NqVSfA241O10u9Qp4m0dQZsTNYw-Hku3r0eu47VZE",
|
|
10
|
-
"kid": "ed46f7df-3010-43da-9032-e0acaee4d3e1",
|
|
11
|
-
"use": "sig",
|
|
12
|
-
"alg": "ES256",
|
|
13
|
-
"iat": 1766931602
|
|
14
|
-
}
|
|
15
|
-
]
|
|
16
|
-
},
|
|
17
|
-
"cookieKeys": [
|
|
18
|
-
"Vb3JNLAlJHCOu5u73eUA_rzlc9aJ0_WCQCu9RWV5WL4",
|
|
19
|
-
"5xCVtYihgadSlvy1QRD_DcU4_9mI_Ggn0DrngzPdiyM"
|
|
20
|
-
],
|
|
21
|
-
"createdAt": "2025-12-28T14:20:02.080Z"
|
|
22
|
-
}
|
package/test-dpop-flow.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import * as jose from 'jose';
|
|
2
|
-
import crypto from 'crypto';
|
|
3
|
-
|
|
4
|
-
const BASE = 'http://localhost:4000';
|
|
5
|
-
|
|
6
|
-
// Create DPoP proof
|
|
7
|
-
async function createDpopProof(privateKey, publicJwk, method, url, ath = null) {
|
|
8
|
-
const payload = {
|
|
9
|
-
jti: crypto.randomUUID(),
|
|
10
|
-
htm: method,
|
|
11
|
-
htu: url,
|
|
12
|
-
iat: Math.floor(Date.now() / 1000),
|
|
13
|
-
};
|
|
14
|
-
if (ath) payload.ath = ath;
|
|
15
|
-
|
|
16
|
-
return new jose.SignJWT(payload)
|
|
17
|
-
.setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk })
|
|
18
|
-
.sign(privateKey);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function main() {
|
|
22
|
-
console.log('=== Testing DPoP Auth Flow ===\n');
|
|
23
|
-
|
|
24
|
-
// 1. Generate key pair
|
|
25
|
-
const { privateKey, publicKey } = await jose.generateKeyPair('ES256');
|
|
26
|
-
const publicJwk = await jose.exportJWK(publicKey);
|
|
27
|
-
const jkt = await jose.calculateJwkThumbprint(publicJwk, 'sha256');
|
|
28
|
-
console.log('1. Generated DPoP key pair, thumbprint:', jkt.substring(0, 20) + '...\n');
|
|
29
|
-
|
|
30
|
-
// 2. Register client dynamically
|
|
31
|
-
console.log('2. Registering client...');
|
|
32
|
-
const regRes = await fetch(`${BASE}/idp/reg`, {
|
|
33
|
-
method: 'POST',
|
|
34
|
-
headers: { 'Content-Type': 'application/json' },
|
|
35
|
-
body: JSON.stringify({
|
|
36
|
-
redirect_uris: ['https://tester'],
|
|
37
|
-
token_endpoint_auth_method: 'none',
|
|
38
|
-
grant_types: ['authorization_code'],
|
|
39
|
-
response_types: ['code'],
|
|
40
|
-
}),
|
|
41
|
-
});
|
|
42
|
-
const client = await regRes.json();
|
|
43
|
-
console.log(' Client ID:', client.client_id, '\n');
|
|
44
|
-
|
|
45
|
-
// 3. Generate PKCE
|
|
46
|
-
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
47
|
-
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
48
|
-
console.log('3. Generated PKCE challenge\n');
|
|
49
|
-
|
|
50
|
-
// 4. Authorization request - WITH dpop_jkt parameter
|
|
51
|
-
console.log('4. Starting authorization (with dpop_jkt)...');
|
|
52
|
-
const authUrl = new URL(`${BASE}/idp/auth`);
|
|
53
|
-
authUrl.searchParams.set('client_id', client.client_id);
|
|
54
|
-
authUrl.searchParams.set('redirect_uri', 'https://tester');
|
|
55
|
-
authUrl.searchParams.set('response_type', 'code');
|
|
56
|
-
authUrl.searchParams.set('scope', 'openid');
|
|
57
|
-
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
58
|
-
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
59
|
-
authUrl.searchParams.set('dpop_jkt', jkt); // KEY: Include dpop_jkt!
|
|
60
|
-
|
|
61
|
-
const authRes = await fetch(authUrl, { redirect: 'manual' });
|
|
62
|
-
const interactionUrl = authRes.headers.get('location');
|
|
63
|
-
console.log(' Redirected to:', interactionUrl ? interactionUrl.substring(0, 50) + '...' : 'none');
|
|
64
|
-
console.log(' Status:', authRes.status, '\n');
|
|
65
|
-
|
|
66
|
-
// 5. Get interaction session cookie
|
|
67
|
-
const rawCookies = authRes.headers.get('set-cookie') || '';
|
|
68
|
-
// Extract just name=value from each Set-Cookie, ignore attributes
|
|
69
|
-
const cookieValues = rawCookies.split(/, (?=[^;]+=[^;]+)/).map(c => c.split(';')[0]).join('; ');
|
|
70
|
-
console.log('5. Got cookies:', cookieValues ? cookieValues.substring(0, 80) + '...' : 'none\n');
|
|
71
|
-
|
|
72
|
-
// 6. Login
|
|
73
|
-
console.log('6. Logging in...');
|
|
74
|
-
const uid = interactionUrl ? interactionUrl.match(/interaction\/([^/?]+)/)?.[1] : null;
|
|
75
|
-
if (!uid) {
|
|
76
|
-
console.log(' ERROR: No interaction UID found');
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
const loginRes = await fetch(`${BASE}/idp/interaction/${uid}`, {
|
|
80
|
-
method: 'POST',
|
|
81
|
-
headers: {
|
|
82
|
-
'Content-Type': 'application/json',
|
|
83
|
-
Cookie: cookieValues,
|
|
84
|
-
},
|
|
85
|
-
body: JSON.stringify({ email: 'alice@example.com', password: 'alicepassword123' }),
|
|
86
|
-
});
|
|
87
|
-
let loginBody;
|
|
88
|
-
const loginText = await loginRes.text();
|
|
89
|
-
try {
|
|
90
|
-
loginBody = JSON.parse(loginText);
|
|
91
|
-
} catch (e) {
|
|
92
|
-
console.log(' Login response (text):', loginText.substring(0, 200));
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
console.log(' Login response:', loginRes.status, loginBody.location ? loginBody.location.substring(0, 50) : '');
|
|
96
|
-
|
|
97
|
-
// 7. Follow auth resume
|
|
98
|
-
console.log('\n7. Following auth resume...');
|
|
99
|
-
const resumeUrl = loginBody.location;
|
|
100
|
-
if (!resumeUrl) {
|
|
101
|
-
console.log(' ERROR: No resume URL');
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
const fullResumeUrl = resumeUrl.startsWith('http') ? resumeUrl : `${BASE}${resumeUrl}`;
|
|
105
|
-
const resumeRes = await fetch(fullResumeUrl, {
|
|
106
|
-
redirect: 'manual',
|
|
107
|
-
headers: { Cookie: cookieValues },
|
|
108
|
-
});
|
|
109
|
-
const callbackUrl = resumeRes.headers.get('location');
|
|
110
|
-
console.log(' Resume status:', resumeRes.status);
|
|
111
|
-
console.log(' Callback URL:', callbackUrl ? callbackUrl.substring(0, 80) + '...' : 'none');
|
|
112
|
-
|
|
113
|
-
// 8. Extract code
|
|
114
|
-
const codeMatch = callbackUrl ? callbackUrl.match(/code=([^&]+)/) : null;
|
|
115
|
-
const code = codeMatch ? codeMatch[1] : null;
|
|
116
|
-
if (!code) {
|
|
117
|
-
console.log(' ERROR: No code in callback');
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
console.log(' Code:', code.substring(0, 20) + '...\n');
|
|
121
|
-
|
|
122
|
-
// 9. Token exchange with DPoP
|
|
123
|
-
console.log('8. Exchanging code for token (with DPoP)...');
|
|
124
|
-
const dpopProof = await createDpopProof(privateKey, publicJwk, 'POST', `${BASE}/idp/token`);
|
|
125
|
-
const tokenRes = await fetch(`${BASE}/idp/token`, {
|
|
126
|
-
method: 'POST',
|
|
127
|
-
headers: {
|
|
128
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
129
|
-
DPoP: dpopProof,
|
|
130
|
-
},
|
|
131
|
-
body: new URLSearchParams({
|
|
132
|
-
grant_type: 'authorization_code',
|
|
133
|
-
code: code,
|
|
134
|
-
redirect_uri: 'https://tester',
|
|
135
|
-
client_id: client.client_id,
|
|
136
|
-
code_verifier: codeVerifier,
|
|
137
|
-
}).toString(),
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
console.log(' Token response status:', tokenRes.status);
|
|
141
|
-
const tokenBody = await tokenRes.text();
|
|
142
|
-
console.log(' Token response:', tokenBody.substring(0, 300));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
main().catch(err => {
|
|
146
|
-
console.error('Error:', err.message);
|
|
147
|
-
console.error(err.stack);
|
|
148
|
-
});
|
package/test-nostr-acl.js
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test script for did:nostr in ACL files
|
|
3
|
-
*
|
|
4
|
-
* Tests:
|
|
5
|
-
* 1. Create a container with restricted access
|
|
6
|
-
* 2. Set ACL with did:nostr agent
|
|
7
|
-
* 3. Verify Nostr auth grants access
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools';
|
|
11
|
-
import { getToken } from 'nostr-tools/nip98';
|
|
12
|
-
|
|
13
|
-
const BASE_URL = process.env.TEST_URL || 'http://localhost:4000';
|
|
14
|
-
|
|
15
|
-
async function main() {
|
|
16
|
-
console.log('=== did:nostr ACL Authorization Test ===\n');
|
|
17
|
-
|
|
18
|
-
// Generate a keypair for testing
|
|
19
|
-
const sk = generateSecretKey();
|
|
20
|
-
const pk = getPublicKey(sk);
|
|
21
|
-
const didNostr = `did:nostr:${pk}`;
|
|
22
|
-
|
|
23
|
-
console.log('1. Generated keypair');
|
|
24
|
-
console.log(` Pubkey: ${pk.slice(0, 16)}...`);
|
|
25
|
-
console.log(` DID: ${didNostr.slice(0, 24)}...\n`);
|
|
26
|
-
|
|
27
|
-
// Create a unique test container
|
|
28
|
-
const testPath = `/demo/nostr-acl-test-${Date.now()}/`;
|
|
29
|
-
const containerUrl = `${BASE_URL}${testPath}`;
|
|
30
|
-
|
|
31
|
-
console.log(`2. Creating test container: ${testPath}`);
|
|
32
|
-
|
|
33
|
-
// Create container (unauthenticated - should work on public parent)
|
|
34
|
-
const createRes = await fetch(containerUrl, {
|
|
35
|
-
method: 'PUT',
|
|
36
|
-
headers: { 'Content-Type': 'text/turtle' },
|
|
37
|
-
body: ''
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
if (!createRes.ok && createRes.status !== 201) {
|
|
41
|
-
console.log(` Failed to create container: ${createRes.status}`);
|
|
42
|
-
// Try anyway
|
|
43
|
-
} else {
|
|
44
|
-
console.log(` Created: ${createRes.status}\n`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Create ACL with did:nostr agent (Turtle format)
|
|
48
|
-
const aclUrl = `${containerUrl}.acl`;
|
|
49
|
-
const aclContent = `
|
|
50
|
-
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
|
|
51
|
-
|
|
52
|
-
<#nostrAccess>
|
|
53
|
-
a acl:Authorization;
|
|
54
|
-
acl:agent <${didNostr}>;
|
|
55
|
-
acl:accessTo <${containerUrl}>;
|
|
56
|
-
acl:default <${containerUrl}>;
|
|
57
|
-
acl:mode acl:Read, acl:Write, acl:Control.
|
|
58
|
-
`;
|
|
59
|
-
|
|
60
|
-
console.log('3. Creating ACL with did:nostr agent');
|
|
61
|
-
console.log(` ACL URL: ${aclUrl}`);
|
|
62
|
-
console.log(` Agent: ${didNostr.slice(0, 40)}...`);
|
|
63
|
-
|
|
64
|
-
const aclRes = await fetch(aclUrl, {
|
|
65
|
-
method: 'PUT',
|
|
66
|
-
headers: { 'Content-Type': 'text/turtle' },
|
|
67
|
-
body: aclContent
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
console.log(` ACL created: ${aclRes.status}\n`);
|
|
71
|
-
|
|
72
|
-
// Verify ACL was saved correctly
|
|
73
|
-
console.log('4. Verifying ACL content');
|
|
74
|
-
const aclCheck = await fetch(aclUrl, {
|
|
75
|
-
headers: { 'Accept': 'text/turtle' }
|
|
76
|
-
});
|
|
77
|
-
const savedAcl = await aclCheck.text();
|
|
78
|
-
console.log(` ACL response: ${aclCheck.status}`);
|
|
79
|
-
console.log(` Contains did:nostr: ${savedAcl.includes('did:nostr:')}\n`);
|
|
80
|
-
|
|
81
|
-
// Test 1: Access WITHOUT auth (should be denied)
|
|
82
|
-
console.log('5. Testing access WITHOUT auth (should be 401/403)...');
|
|
83
|
-
const noAuthRes = await fetch(containerUrl);
|
|
84
|
-
console.log(` Status: ${noAuthRes.status} ${noAuthRes.status === 401 || noAuthRes.status === 403 ? '✓' : '✗'}\n`);
|
|
85
|
-
|
|
86
|
-
// Test 2: Access WITH correct Nostr auth
|
|
87
|
-
console.log('6. Testing access WITH correct Nostr auth...');
|
|
88
|
-
const token = await getToken(containerUrl, 'GET', (event) => finalizeEvent(event, sk));
|
|
89
|
-
|
|
90
|
-
const authRes = await fetch(containerUrl, {
|
|
91
|
-
headers: {
|
|
92
|
-
'Authorization': `Nostr ${token}`,
|
|
93
|
-
'Accept': 'text/turtle'
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
console.log(` Status: ${authRes.status}`);
|
|
98
|
-
|
|
99
|
-
if (authRes.status === 200) {
|
|
100
|
-
console.log(' ✓ ACCESS GRANTED - did:nostr ACL working!\n');
|
|
101
|
-
} else {
|
|
102
|
-
console.log(' ✗ Access denied');
|
|
103
|
-
const body = await authRes.text();
|
|
104
|
-
console.log(` Body: ${body.slice(0, 200)}\n`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Test 3: Access with DIFFERENT Nostr key (should be denied)
|
|
108
|
-
console.log('7. Testing with DIFFERENT Nostr key (should be denied)...');
|
|
109
|
-
const wrongSk = generateSecretKey();
|
|
110
|
-
const wrongToken = await getToken(containerUrl, 'GET', (event) => finalizeEvent(event, wrongSk));
|
|
111
|
-
|
|
112
|
-
const wrongAuthRes = await fetch(containerUrl, {
|
|
113
|
-
headers: {
|
|
114
|
-
'Authorization': `Nostr ${wrongToken}`,
|
|
115
|
-
'Accept': 'text/turtle'
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
console.log(` Status: ${wrongAuthRes.status} ${wrongAuthRes.status === 403 ? '✓' : '✗'}\n`);
|
|
120
|
-
|
|
121
|
-
// Clean up
|
|
122
|
-
console.log('8. Cleaning up test container...');
|
|
123
|
-
const deleteToken = await getToken(containerUrl, 'DELETE', (event) => finalizeEvent(event, sk));
|
|
124
|
-
const deleteRes = await fetch(containerUrl, {
|
|
125
|
-
method: 'DELETE',
|
|
126
|
-
headers: { 'Authorization': `Nostr ${deleteToken}` }
|
|
127
|
-
});
|
|
128
|
-
console.log(` Delete: ${deleteRes.status}\n`);
|
|
129
|
-
|
|
130
|
-
// Summary
|
|
131
|
-
console.log('=== Test Summary ===');
|
|
132
|
-
console.log(`No auth: ${noAuthRes.status === 401 || noAuthRes.status === 403 ? 'PASS' : 'FAIL'} (${noAuthRes.status})`);
|
|
133
|
-
console.log(`Correct key: ${authRes.status === 200 ? 'PASS' : 'FAIL'} (${authRes.status})`);
|
|
134
|
-
console.log(`Wrong key: ${wrongAuthRes.status === 403 ? 'PASS' : 'FAIL'} (${wrongAuthRes.status})`);
|
|
135
|
-
|
|
136
|
-
const allPassed = (noAuthRes.status === 401 || noAuthRes.status === 403) &&
|
|
137
|
-
authRes.status === 200 &&
|
|
138
|
-
wrongAuthRes.status === 403;
|
|
139
|
-
|
|
140
|
-
console.log(`\nOverall: ${allPassed ? 'ALL TESTS PASSED ✓' : 'SOME TESTS FAILED ✗'}`);
|
|
141
|
-
process.exit(allPassed ? 0 : 1);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
main().catch(console.error);
|