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.
@@ -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.31)
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
- └── container.js # POST, pod creation
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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
- return authHeader && authHeader.startsWith('Nostr ');
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 || !authHeader.startsWith('Nostr ')) {
40
- return null;
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
- return authHeader.slice(6).trim();
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
- if (normalizedEventUrl !== normalizedRequestUrl && normalizedEventUrl !== normalizedRequestUrlNoQuery) {
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