javascript-solid-server 0.0.37 → 0.0.38
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 +4 -1
- package/README.md +94 -3
- package/data/.idp/keys/jwks.json +37 -0
- package/package.json +1 -1
- package/src/auth/nostr.js +51 -6
|
@@ -116,7 +116,10 @@
|
|
|
116
116
|
"Bash(fi)",
|
|
117
117
|
"Bash(timeout 45 node:*)",
|
|
118
118
|
"Bash(gh issue list:*)",
|
|
119
|
-
"Bash(DATA_ROOT=/tmp/jss-git-test JSS_PORT=4444 timeout 3 node:*)"
|
|
119
|
+
"Bash(DATA_ROOT=/tmp/jss-git-test JSS_PORT=4444 timeout 3 node:*)",
|
|
120
|
+
"Bash(pm2 show:*)",
|
|
121
|
+
"Bash(git config:*)",
|
|
122
|
+
"Bash(npm version:*)"
|
|
120
123
|
]
|
|
121
124
|
}
|
|
122
125
|
}
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A minimal, fast, JSON-LD native Solid server.
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
### Implemented (v0.0.
|
|
7
|
+
### Implemented (v0.0.37)
|
|
8
8
|
|
|
9
9
|
- **LDP CRUD Operations** - GET, PUT, POST, DELETE, HEAD
|
|
10
10
|
- **N3 Patch** - Solid's native patch format for RDF updates
|
|
@@ -19,13 +19,15 @@ A minimal, fast, JSON-LD native Solid server.
|
|
|
19
19
|
- **Mashlib Data Browser** - Optional SolidOS UI (CDN or local hosting)
|
|
20
20
|
- **WebID Profiles** - HTML with JSON-LD data islands, rendered with mashlib-jss + solidos-lite
|
|
21
21
|
- **Web Access Control (WAC)** - `.acl` file-based authorization
|
|
22
|
-
- **Solid-OIDC Identity Provider** - Built-in IdP with DPoP, dynamic registration
|
|
22
|
+
- **Solid-OIDC Identity Provider** - Built-in IdP with DPoP, RS256/ES256, dynamic registration
|
|
23
23
|
- **Solid-OIDC Resource Server** - Accept DPoP-bound access tokens from external IdPs
|
|
24
24
|
- **NSS-style Registration** - Username/password auth compatible with Solid apps
|
|
25
25
|
- **Nostr Authentication** - NIP-98 HTTP Auth with Schnorr signatures
|
|
26
26
|
- **Simple Auth Tokens** - Built-in token authentication for development
|
|
27
27
|
- **Content Negotiation** - Turtle <-> JSON-LD conversion, including HTML data islands
|
|
28
28
|
- **CORS Support** - Full cross-origin resource sharing
|
|
29
|
+
- **Git HTTP Backend** - Clone and push to containers via `git` protocol
|
|
30
|
+
- **Security** - Blocks access to dotfiles (`.git/`, `.env`, etc.) except Solid-specific ones
|
|
29
31
|
|
|
30
32
|
### HTTP Methods
|
|
31
33
|
|
|
@@ -94,6 +96,7 @@ jss --help # Show help
|
|
|
94
96
|
| `--mashlib` | Enable Mashlib (local mode) | false |
|
|
95
97
|
| `--mashlib-cdn` | Enable Mashlib (CDN mode) | false |
|
|
96
98
|
| `--mashlib-version <ver>` | Mashlib CDN version | 2.0.0 |
|
|
99
|
+
| `--git` | Enable Git HTTP backend | false |
|
|
97
100
|
| `-q, --quiet` | Suppress logs | false |
|
|
98
101
|
|
|
99
102
|
### Environment Variables
|
|
@@ -318,6 +321,93 @@ Server: ack http://localhost:3000/alice/public/data.json
|
|
|
318
321
|
Server: pub http://localhost:3000/alice/public/data.json (on change)
|
|
319
322
|
```
|
|
320
323
|
|
|
324
|
+
## Git Support
|
|
325
|
+
|
|
326
|
+
Enable Git HTTP backend to clone and push to pod containers:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
jss start --git
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Initialize a Repository
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
# Create a git repo in a pod container
|
|
336
|
+
cd data/alice/myrepo
|
|
337
|
+
git init
|
|
338
|
+
echo "# My Project" > README.md
|
|
339
|
+
git add . && git commit -m "Initial commit"
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Clone and Push
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
# Clone (public read access)
|
|
346
|
+
git clone http://localhost:3000/alice/myrepo
|
|
347
|
+
|
|
348
|
+
# Push (requires write access via WAC)
|
|
349
|
+
cd myrepo
|
|
350
|
+
echo "New content" >> README.md
|
|
351
|
+
git add . && git commit -m "Update"
|
|
352
|
+
git push
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Git operations respect WAC permissions - clone requires Read access, push requires Write access.
|
|
356
|
+
|
|
357
|
+
### Git Push with Nostr Authentication
|
|
358
|
+
|
|
359
|
+
Git push supports NIP-98 authentication via Basic Auth. Create a credential helper:
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
// git-credential-nostr.js
|
|
363
|
+
import { getPublicKey, finalizeEvent } from 'nostr-tools/pure';
|
|
364
|
+
import { hexToBytes } from '@noble/hashes/utils';
|
|
365
|
+
import fs from 'fs';
|
|
366
|
+
import readline from 'readline';
|
|
367
|
+
|
|
368
|
+
const sk = hexToBytes(fs.readFileSync('.nostr-key', 'utf8').trim());
|
|
369
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
370
|
+
const params = {};
|
|
371
|
+
for await (const line of rl) {
|
|
372
|
+
if (!line) break;
|
|
373
|
+
const [key, ...vals] = line.split('=');
|
|
374
|
+
params[key] = vals.join('=');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (params.protocol && params.host) {
|
|
378
|
+
let path = params.path || '/';
|
|
379
|
+
path = path.replace(/\/info\/refs$/, '').replace(/\/git-.*$/, '');
|
|
380
|
+
const baseUrl = `${params.protocol}://${params.host}${path}`;
|
|
381
|
+
|
|
382
|
+
const event = finalizeEvent({
|
|
383
|
+
kind: 27235,
|
|
384
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
385
|
+
tags: [['u', baseUrl], ['method', '*']],
|
|
386
|
+
content: ''
|
|
387
|
+
}, sk);
|
|
388
|
+
|
|
389
|
+
console.log('username=nostr');
|
|
390
|
+
console.log('password=' + Buffer.from(JSON.stringify(event)).toString('base64'));
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Configure git to use it:
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
git config credential.helper 'node /path/to/git-credential-nostr.js'
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Add the Nostr identity to your ACL:
|
|
401
|
+
|
|
402
|
+
```turtle
|
|
403
|
+
<#nostr-writer>
|
|
404
|
+
a acl:Authorization;
|
|
405
|
+
acl:agent <did:nostr:YOUR_64_CHAR_HEX_PUBKEY>;
|
|
406
|
+
acl:accessTo <./>;
|
|
407
|
+
acl:default <./>;
|
|
408
|
+
acl:mode acl:Read, acl:Write.
|
|
409
|
+
```
|
|
410
|
+
|
|
321
411
|
## Authentication
|
|
322
412
|
|
|
323
413
|
### Simple Tokens (Development)
|
|
@@ -531,7 +621,8 @@ src/
|
|
|
531
621
|
├── server.js # Fastify setup
|
|
532
622
|
├── handlers/
|
|
533
623
|
│ ├── resource.js # GET, PUT, DELETE, HEAD, PATCH
|
|
534
|
-
│
|
|
624
|
+
│ ├── container.js # POST, pod creation
|
|
625
|
+
│ └── git.js # Git HTTP backend
|
|
535
626
|
├── storage/
|
|
536
627
|
│ └── filesystem.js # File operations
|
|
537
628
|
├── auth/
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"jwks": {
|
|
3
|
+
"keys": [
|
|
4
|
+
{
|
|
5
|
+
"kty": "RSA",
|
|
6
|
+
"n": "zVVbuPqzcpJI4er_VpHQQd8UgRDI-PcLlwUGRG3DGqMvAWzB4UX2m5e5Uhc2vnzBC2U_1bZ5u3ovhZZhXqqj0ItBTOWjhjOZz4YpaPiwM68sqp_JUNNZPSLljhXnEhEv8q57Wcl0N8orJeuSmfjh7shsLKN-3nbWjCbQr4oAUneQ_2HNvazUTgRLKYCniS0SaI_ogTwuIJlNdFIc3FAHs4yNoSaXZDkdGWhGn49wYdB58nkjJ2ja8wc-rwNtcKEG7HZWNhoVCredav9jswWgAGodJofAxP_PCjguqn6-TzvGodXM3p0Rj1qaf27Ok39GbtOEwX4upwFKj-yJaZETQQ",
|
|
7
|
+
"e": "AQAB",
|
|
8
|
+
"d": "BAgqbUkP876bW4NMqP-jPB3kJk4k8h2QvtIIj9iraXcdlcyzyGeCKoc5ynq99pLWzBFchebnwEaLjxcXOZ-GaLKJUVgbhEe4XBa1crQaaqNkkEOjtXh28sBAC3CS6VwIyd5C-g3-gBdyTjPwXKFiV1jcZep-c-IXv6gF9kJyk-vv3aT6VabcqRBOk4PeNxL_VJt65x5JQBqyV06fv7OBrt15ez0dZL-fYMPhMhBZom0YGCkMVAC4VHAOmoVuHk3hQQCQv5x9-hq27qfgHAlkSKxTEDp2aJi4nIfzqEnFHrBqB9BwR0JEyV3ankVPzBcfEGffi5XY3kWK3EDE3oX9SQ",
|
|
9
|
+
"p": "6ZGd_spW6xSiTNwlU-to7Qr06dr1hXzWLyFbJM9_APFxE761WJtAsN50oNg-rKRQk2JdlC0N04OFYWBED-pvJ0AIEVnKJfWsxEQaBkRueZefUcf-0L1M2Q7-LQdqz9qTR3hSZ42lJhPKMdLRI2WrxTz1v1p0w0yl3bwcbBRLtTk",
|
|
10
|
+
"q": "4Q2QFyMKQUQhr4-JlQbI3VtBhfP-65oZg-13hG5Xc9kT0qo_-79YCK_4wMUX4WaSH6fG6lobTgZento3d6MXCAFpEl8WM4uRRNlzTLCC2FDZXms8i6hJHN89QFPLEPQd5bDXsw8pmA3ANIeHcwzR243VuWAYeQfeOJ-qymUjlkk",
|
|
11
|
+
"dp": "AUldDm885VSaxEOeLQUp8cxSpwseuRqD74SGhQBjmbS6w7oUM6W_SHohOFWYmsjY7Mbo7w0Ee3rI_E1UcqX-8L9oi_frpiPhTL93STuNRDwyk3e_jpTMXJG5krPswbJZh1ZBVfKwyzHmtjmMD17bAF4imGg-JmlArKUBnxLJi_k",
|
|
12
|
+
"dq": "A6Z7qtRnqy1WuolCewdUJLsBMhIGFX43YbttT9mWU4u21ZjrVsMAw4tPJplLzN0kC51mDZEOllJmIH97nNYpXnjfYmvmaUmfPpWkWB8Y0Ddnfy-QGNfO78fzL2LsjUbYYUxgA0iArTWz42Y7XTNdCAmh6NLVMslc4mA8nfHMBPk",
|
|
13
|
+
"qi": "Wikwy4ljfyLLHDO1Fpm2hn-_G7rWbwOP0ANJzY62cIEorbyyr4zAaaPflGMrDJHadlLdww_x2L1g2lNePN7mY97kFJVL0ceuwUmxDr434LqyRZWTrTP5qP6vrkjGjMImBuJEftXdOyJElcqHgtpvltydZRjlvhugnxcOKRble3Y",
|
|
14
|
+
"kid": "509868cc-c850-4708-9331-8e38273df4db",
|
|
15
|
+
"use": "sig",
|
|
16
|
+
"alg": "RS256",
|
|
17
|
+
"iat": 1767263929
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"kty": "EC",
|
|
21
|
+
"x": "KSGjhPP_QyoqHu8v891hEk_TsF5yuQUN0hFBY_qq26o",
|
|
22
|
+
"y": "aTYYEwxOR0tM82FtJoCVJ7u8xtf3y0exoWVJzJAZQ24",
|
|
23
|
+
"crv": "P-256",
|
|
24
|
+
"d": "Mkm_Oh4_emAzg2MbxoMuHcJZo3E-O_-YlK8mlSHWU8M",
|
|
25
|
+
"kid": "92009452-c551-4389-97fd-314ddd59b7d4",
|
|
26
|
+
"use": "sig",
|
|
27
|
+
"alg": "ES256",
|
|
28
|
+
"iat": 1767263929
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"cookieKeys": [
|
|
33
|
+
"IiO299-8bui4UVILu0azGvlCnNTckSGMPMPQSG2HqHw",
|
|
34
|
+
"NbeOVkfw-xN9T4J0_1uzAmDp6tYOwFW-yTJLSd7nqDU"
|
|
35
|
+
],
|
|
36
|
+
"createdAt": "2026-01-01T10:38:49.528Z"
|
|
37
|
+
}
|
package/package.json
CHANGED
package/src/auth/nostr.js
CHANGED
|
@@ -22,24 +22,58 @@ const TIMESTAMP_TOLERANCE = 60;
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Check if request has Nostr authentication
|
|
25
|
+
* Supports both "Nostr <token>" and "Basic <base64(nostr:token)>" formats
|
|
26
|
+
* The Basic format allows git clients to authenticate via NIP-98
|
|
25
27
|
* @param {object} request - Fastify request object
|
|
26
28
|
* @returns {boolean}
|
|
27
29
|
*/
|
|
28
30
|
export function hasNostrAuth(request) {
|
|
29
31
|
const authHeader = request.headers.authorization;
|
|
30
|
-
|
|
32
|
+
if (!authHeader) return false;
|
|
33
|
+
|
|
34
|
+
// Direct Nostr header
|
|
35
|
+
if (authHeader.startsWith('Nostr ')) return true;
|
|
36
|
+
|
|
37
|
+
// Basic auth with username=nostr (for git clients)
|
|
38
|
+
if (authHeader.startsWith('Basic ')) {
|
|
39
|
+
try {
|
|
40
|
+
const decoded = Buffer.from(authHeader.slice(6), 'base64').toString('utf8');
|
|
41
|
+
return decoded.startsWith('nostr:');
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return false;
|
|
31
48
|
}
|
|
32
49
|
|
|
33
50
|
/**
|
|
34
51
|
* Extract token from Nostr authorization header
|
|
52
|
+
* Supports both "Nostr <token>" and "Basic <base64(nostr:token)>" formats
|
|
35
53
|
* @param {string} authHeader - Authorization header value
|
|
36
54
|
* @returns {string|null}
|
|
37
55
|
*/
|
|
38
56
|
export function extractNostrToken(authHeader) {
|
|
39
|
-
if (!authHeader
|
|
40
|
-
|
|
57
|
+
if (!authHeader) return null;
|
|
58
|
+
|
|
59
|
+
// Direct Nostr header
|
|
60
|
+
if (authHeader.startsWith('Nostr ')) {
|
|
61
|
+
return authHeader.slice(6).trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Basic auth with username=nostr, password=token
|
|
65
|
+
if (authHeader.startsWith('Basic ')) {
|
|
66
|
+
try {
|
|
67
|
+
const decoded = Buffer.from(authHeader.slice(6), 'base64').toString('utf8');
|
|
68
|
+
if (decoded.startsWith('nostr:')) {
|
|
69
|
+
return decoded.slice(6); // Remove "nostr:" prefix to get token
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
41
74
|
}
|
|
42
|
-
|
|
75
|
+
|
|
76
|
+
return null;
|
|
43
77
|
}
|
|
44
78
|
|
|
45
79
|
/**
|
|
@@ -125,16 +159,27 @@ export async function verifyNostrAuth(request) {
|
|
|
125
159
|
const normalizedRequestUrl = fullUrl.replace(/\/$/, '');
|
|
126
160
|
const normalizedRequestUrlNoQuery = fullUrl.split('?')[0].replace(/\/$/, '');
|
|
127
161
|
|
|
128
|
-
|
|
162
|
+
// Check for exact match first
|
|
163
|
+
let urlMatches = normalizedEventUrl === normalizedRequestUrl ||
|
|
164
|
+
normalizedEventUrl === normalizedRequestUrlNoQuery;
|
|
165
|
+
|
|
166
|
+
// For git clients: allow prefix matching (event URL is base of request URL)
|
|
167
|
+
// This enables git credential helpers that sign for the repo base URL
|
|
168
|
+
if (!urlMatches && normalizedRequestUrlNoQuery.startsWith(normalizedEventUrl + '/')) {
|
|
169
|
+
urlMatches = true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!urlMatches) {
|
|
129
173
|
return { webId: null, error: `URL mismatch: event URL "${eventUrl}" does not match request URL "${fullUrl}"` };
|
|
130
174
|
}
|
|
131
175
|
|
|
132
176
|
// Validate method tag matches request method
|
|
177
|
+
// For git clients: allow '*' as wildcard method
|
|
133
178
|
const eventMethod = getTagValue(event, 'method');
|
|
134
179
|
if (!eventMethod) {
|
|
135
180
|
return { webId: null, error: 'Missing method tag in event' };
|
|
136
181
|
}
|
|
137
|
-
if (eventMethod.toUpperCase() !== request.method.toUpperCase()) {
|
|
182
|
+
if (eventMethod !== '*' && eventMethod.toUpperCase() !== request.method.toUpperCase()) {
|
|
138
183
|
return { webId: null, error: `Method mismatch: expected ${request.method}, got ${eventMethod}` };
|
|
139
184
|
}
|
|
140
185
|
|