javascript-solid-server 0.0.34 → 0.0.36
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 +18 -1
- package/AGENTS.md +152 -0
- package/docs/design/nostr-relay-integration.md +353 -0
- package/package.json +1 -1
- package/src/handlers/resource.js +75 -23
- package/src/idp/credentials.js +3 -2
- package/src/idp/index.js +1 -1
- package/src/idp/keys.js +57 -8
- package/src/idp/provider.js +100 -2
- package/src/rdf/turtle.js +4 -2
- package/src/server.js +2 -1
|
@@ -97,7 +97,24 @@
|
|
|
97
97
|
"Bash(done)",
|
|
98
98
|
"Bash(for domain in jss.dev jss.sh jss.io jss.app solidserver.dev)",
|
|
99
99
|
"Bash(host:*)",
|
|
100
|
-
"WebFetch(domain:nostr-components.github.io)"
|
|
100
|
+
"WebFetch(domain:nostr-components.github.io)",
|
|
101
|
+
"Bash(ssh melvincarvalho.com \"pm2 list && echo ''---HAPROXY---'' && cat /etc/haproxy/haproxy.cfg 2>/dev/null | grep -A5 ''melvin\\|backend\\|frontend\\|acl host''\")",
|
|
102
|
+
"Bash(ssh:*)",
|
|
103
|
+
"Bash(time curl -s --connect-timeout 10 https://melvin.solid.live/credit/count.ttl)",
|
|
104
|
+
"Bash(time curl -s --connect-timeout 10 https://melvin.solid.live/)",
|
|
105
|
+
"Bash(time curl:*)",
|
|
106
|
+
"Bash(time curl -s 'https://melvin.solid.live/credit/count.ttl')",
|
|
107
|
+
"Bash(grep:*)",
|
|
108
|
+
"Bash(scp:*)",
|
|
109
|
+
"Bash(for i in 1 2 3)",
|
|
110
|
+
"Bash(do echo \"Attempt $i:\")",
|
|
111
|
+
"Bash(for i in 1 2 3 4 5)",
|
|
112
|
+
"Bash(do curl -so /dev/null -w \"%{http_code} \" https://melvincarvalho.com/js/handlemutation.js)",
|
|
113
|
+
"Bash(for i in 1 2 3 4 5 6 7 8 9 10)",
|
|
114
|
+
"Bash(if [ ! -d \"jose\" ])",
|
|
115
|
+
"Bash(then git clone --depth 1 --branch v0.7.0 https://github.com/solid/jose.git)",
|
|
116
|
+
"Bash(fi)",
|
|
117
|
+
"Bash(timeout 45 node:*)"
|
|
101
118
|
]
|
|
102
119
|
}
|
|
103
120
|
}
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# AGENTS.md - AI Assistant Context for JSS
|
|
2
|
+
|
|
3
|
+
This document provides context for AI assistants working on JavaScript Solid Server (JSS).
|
|
4
|
+
|
|
5
|
+
## What is JSS?
|
|
6
|
+
|
|
7
|
+
A lightweight Solid server implementation focused on simplicity and modern JavaScript. Alternative to Node Solid Server (NSS) and Community Solid Server (CSS).
|
|
8
|
+
|
|
9
|
+
**Key differences from other Solid servers:**
|
|
10
|
+
- Single-file JSON-LD storage (no quad stores)
|
|
11
|
+
- Content negotiation converts JSON-LD ↔ Turtle on the fly
|
|
12
|
+
- Built on Fastify (not Express)
|
|
13
|
+
- Uses oidc-provider for identity
|
|
14
|
+
- Supports Nostr NIP-98 authentication (unique to JSS)
|
|
15
|
+
|
|
16
|
+
## Architecture
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
src/
|
|
20
|
+
├── server.js # Fastify setup, route registration
|
|
21
|
+
├── handlers/ # LDP operations (GET, PUT, POST, PATCH, DELETE)
|
|
22
|
+
│ ├── resource.js # File operations
|
|
23
|
+
│ └── container.js # Directory operations, pod creation
|
|
24
|
+
├── auth/ # Authentication
|
|
25
|
+
│ ├── middleware.js # WAC authorization hook
|
|
26
|
+
│ ├── solid-oidc.js # DPoP token verification
|
|
27
|
+
│ ├── nostr.js # NIP-98 Schnorr signatures
|
|
28
|
+
│ └── token.js # Simple Bearer tokens
|
|
29
|
+
├── idp/ # Identity Provider (oidc-provider)
|
|
30
|
+
│ ├── provider.js # OIDC configuration
|
|
31
|
+
│ ├── interactions.js # Login/consent UI handlers
|
|
32
|
+
│ └── accounts.js # User account storage
|
|
33
|
+
├── wac/ # Web Access Control
|
|
34
|
+
│ ├── checker.js # Permission checking
|
|
35
|
+
│ └── parser.js # ACL file parsing/generation
|
|
36
|
+
├── rdf/ # RDF handling
|
|
37
|
+
│ ├── conneg.js # Content negotiation
|
|
38
|
+
│ └── turtle.js # Turtle ↔ JSON-LD conversion
|
|
39
|
+
├── notifications/ # WebSocket real-time updates
|
|
40
|
+
│ ├── websocket.js # solid-0.1 protocol handler
|
|
41
|
+
│ └── events.js # Event emitter for changes
|
|
42
|
+
├── ldp/ # Linked Data Platform
|
|
43
|
+
│ ├── headers.js # LDP response headers
|
|
44
|
+
│ └── container.js # Container JSON-LD generation
|
|
45
|
+
└── storage/ # File system operations
|
|
46
|
+
└── filesystem.js # Read/write/stat/list
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Key Design Decisions
|
|
50
|
+
|
|
51
|
+
### JSON-LD as canonical storage
|
|
52
|
+
All RDF is stored as JSON-LD. When clients request Turtle, we convert on the fly. This simplifies storage and allows non-RDF tools to read the data.
|
|
53
|
+
|
|
54
|
+
### HTML profiles with JSON-LD data islands
|
|
55
|
+
WebID profiles are HTML documents with embedded `<script type="application/ld+json">`. This allows:
|
|
56
|
+
- Human-readable profiles in browsers
|
|
57
|
+
- Machine-readable RDF via content negotiation
|
|
58
|
+
- Mashlib renders the profile using the embedded data
|
|
59
|
+
|
|
60
|
+
### Subdomain mode for XSS isolation
|
|
61
|
+
When `subdomains: true`, each pod gets its own subdomain (alice.example.com). This provides browser security isolation. Storage path includes pod name, but URLs use subdomains.
|
|
62
|
+
|
|
63
|
+
### Settings folder conventions
|
|
64
|
+
Mashlib expects `Settings/` (capital S) with:
|
|
65
|
+
- `Settings/Preferences.ttl`
|
|
66
|
+
- `Settings/publicTypeIndex.ttl`
|
|
67
|
+
- `Settings/privateTypeIndex.ttl`
|
|
68
|
+
|
|
69
|
+
Earlier versions used lowercase `settings/prefs` which broke mashlib.
|
|
70
|
+
|
|
71
|
+
## Common Gotchas
|
|
72
|
+
|
|
73
|
+
### Content negotiation
|
|
74
|
+
- Files stored as JSON-LD regardless of upload format
|
|
75
|
+
- `.ttl` extension triggers Turtle response regardless of Accept header
|
|
76
|
+
- Container listings need conneg too (fixed in v0.0.33)
|
|
77
|
+
|
|
78
|
+
### Authentication paths that skip auth
|
|
79
|
+
These paths bypass the auth middleware:
|
|
80
|
+
- `/.pods` - Pod creation
|
|
81
|
+
- `/.notifications` - WebSocket endpoint
|
|
82
|
+
- `/idp/*` - Identity provider routes
|
|
83
|
+
- `/.well-known/*` - Discovery endpoints
|
|
84
|
+
- OPTIONS requests
|
|
85
|
+
|
|
86
|
+
### WebSocket notifications
|
|
87
|
+
Uses legacy `solid-0.1` protocol (not Solid Notifications Protocol):
|
|
88
|
+
```
|
|
89
|
+
Server: protocol solid-0.1
|
|
90
|
+
Client: sub https://example.org/resource
|
|
91
|
+
Server: ack https://example.org/resource
|
|
92
|
+
Server: pub https://example.org/resource (on change)
|
|
93
|
+
```
|
|
94
|
+
Discovered via `Updates-Via` header.
|
|
95
|
+
|
|
96
|
+
### DPoP token verification
|
|
97
|
+
Solid-OIDC uses DPoP-bound tokens. The DPoP proof must match:
|
|
98
|
+
- HTTP method (htm)
|
|
99
|
+
- Request URL (htu)
|
|
100
|
+
- Be recent (iat within 5 minutes)
|
|
101
|
+
- Key thumbprint matches token binding (cnf.jkt)
|
|
102
|
+
|
|
103
|
+
### ACL inheritance
|
|
104
|
+
WAC ACLs use `acl:default` for inheritance. When checking permissions:
|
|
105
|
+
1. Look for resource-specific ACL (resource.acl or .acl for containers)
|
|
106
|
+
2. Walk up to parent containers checking for `acl:default` rules
|
|
107
|
+
3. Stop at pod root
|
|
108
|
+
|
|
109
|
+
## Testing
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm test # Run all tests
|
|
113
|
+
npm run test:cth # Conformance Test Harness (requires setup)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Tests use in-memory server instances. See `test/helpers.js` for test utilities.
|
|
117
|
+
|
|
118
|
+
## Deployment
|
|
119
|
+
|
|
120
|
+
### Production setup
|
|
121
|
+
```bash
|
|
122
|
+
npm install -g javascript-solid-server
|
|
123
|
+
jss --port 443 --ssl-key key.pem --ssl-cert cert.pem --idp --multiuser
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### With HAProxy (recommended)
|
|
127
|
+
HAProxy handles SSL termination, JSS runs on localhost:8443. Wildcard cert needed for subdomain mode.
|
|
128
|
+
|
|
129
|
+
### PM2 process management
|
|
130
|
+
```bash
|
|
131
|
+
pm2 start "jss --config config.json" --name solid
|
|
132
|
+
pm2 logs solid
|
|
133
|
+
pm2 restart solid
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## External Dependencies
|
|
137
|
+
|
|
138
|
+
- **oidc-provider**: OpenID Connect implementation (complex, many warnings are normal)
|
|
139
|
+
- **n3**: Turtle/N3 parsing and serialization
|
|
140
|
+
- **jose**: JWT/JWK handling for Solid-OIDC
|
|
141
|
+
- **@fastify/websocket**: WebSocket support
|
|
142
|
+
|
|
143
|
+
## Related Specs
|
|
144
|
+
|
|
145
|
+
- [Solid Protocol](https://solidproject.org/TR/protocol)
|
|
146
|
+
- [Solid-OIDC](https://solidproject.org/TR/oidc)
|
|
147
|
+
- [Web Access Control](https://solidproject.org/TR/wac)
|
|
148
|
+
- [Linked Data Platform](https://www.w3.org/TR/ldp/)
|
|
149
|
+
|
|
150
|
+
## Contact
|
|
151
|
+
|
|
152
|
+
Issues: https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# Design: JSS as Nostr Relay++
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Replace the Solid Notifications Protocol with Nostr relay functionality. JSS becomes a Nostr relay that also serves LDP resources, unifying identity, storage, and real-time notifications.
|
|
6
|
+
|
|
7
|
+
## Motivation
|
|
8
|
+
|
|
9
|
+
**Solid Notifications Protocol problems:**
|
|
10
|
+
- Complex discovery mechanism
|
|
11
|
+
- JSON-LD channel descriptions
|
|
12
|
+
- No federation
|
|
13
|
+
- No existing ecosystem
|
|
14
|
+
- Reinvents pub/sub poorly
|
|
15
|
+
|
|
16
|
+
**Nostr advantages:**
|
|
17
|
+
- Simple WebSocket protocol (NIP-01)
|
|
18
|
+
- Cryptographic identity built-in
|
|
19
|
+
- Federation via relay gossip
|
|
20
|
+
- Millions of existing users
|
|
21
|
+
- Mobile push infrastructure exists
|
|
22
|
+
- Battle-tested
|
|
23
|
+
|
|
24
|
+
**JSS already has:**
|
|
25
|
+
- NIP-98 HTTP authentication
|
|
26
|
+
- WebSocket infrastructure (solid-0.1)
|
|
27
|
+
- JSON-LD storage
|
|
28
|
+
|
|
29
|
+
## Architecture
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
33
|
+
│ JSS Server │
|
|
34
|
+
├──────────────────────┬──────────────────────────────────────┤
|
|
35
|
+
│ LDP Layer │ Nostr Relay Layer │
|
|
36
|
+
│ │ │
|
|
37
|
+
│ GET/PUT/POST/PATCH │ EVENT/REQ/CLOSE/EOSE │
|
|
38
|
+
│ DELETE/OPTIONS │ │
|
|
39
|
+
│ │ │
|
|
40
|
+
│ ┌────────────────┐ │ ┌─────────────────────────────────┐ │
|
|
41
|
+
│ │ Resources │◄─┼─►│ Events (kind:30078) │ │
|
|
42
|
+
│ │ /alice/doc.ttl│ │ │ Addressable by d-tag = URI │ │
|
|
43
|
+
│ └────────────────┘ │ └─────────────────────────────────┘ │
|
|
44
|
+
│ │ │
|
|
45
|
+
│ Auth: Solid-OIDC │ Auth: NIP-98 / NIP-42 │
|
|
46
|
+
│ NIP-98 │ │
|
|
47
|
+
└──────────────────────┴──────────────────────────────────────┘
|
|
48
|
+
│
|
|
49
|
+
▼
|
|
50
|
+
┌─────────────────┐
|
|
51
|
+
│ Other Relays │
|
|
52
|
+
│ (Federation) │
|
|
53
|
+
└─────────────────┘
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Protocol Mapping
|
|
57
|
+
|
|
58
|
+
### Resource ↔ Event Mapping
|
|
59
|
+
|
|
60
|
+
LDP resources map to Nostr replaceable events:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Resource URL: https://alice.solid.social/notes/idea.json
|
|
64
|
+
↓
|
|
65
|
+
Nostr Event:
|
|
66
|
+
{
|
|
67
|
+
"kind": 30078, // Arbitrary JSON (NIP-78)
|
|
68
|
+
"pubkey": "<alice-pubkey>",
|
|
69
|
+
"created_at": 1703888888,
|
|
70
|
+
"tags": [
|
|
71
|
+
["d", "https://alice.solid.social/notes/idea.json"],
|
|
72
|
+
["solid:type", "ldp:Resource"],
|
|
73
|
+
["solid:contentType", "application/ld+json"]
|
|
74
|
+
],
|
|
75
|
+
"content": "{\"@context\": ..., \"title\": \"My Idea\"}",
|
|
76
|
+
"sig": "<signature>"
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Kind Assignments
|
|
81
|
+
|
|
82
|
+
| Kind | Purpose | NIP |
|
|
83
|
+
|------|---------|-----|
|
|
84
|
+
| 30078 | LDP Resource (JSON content) | NIP-78 |
|
|
85
|
+
| 30079 | LDP Container listing | Custom |
|
|
86
|
+
| 30080 | ACL document | Custom |
|
|
87
|
+
| 10078 | Resource deletion marker | Custom |
|
|
88
|
+
| 1 | Social posts (optional integration) | NIP-01 |
|
|
89
|
+
|
|
90
|
+
Using 30xxx range for addressable replaceable events (d-tag = resource URI).
|
|
91
|
+
|
|
92
|
+
### Subscription Filters
|
|
93
|
+
|
|
94
|
+
Subscribe to resource changes:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
// Subscribe to single resource
|
|
98
|
+
["REQ", "sub1", {
|
|
99
|
+
"kinds": [30078],
|
|
100
|
+
"#d": ["https://alice.solid.social/notes/idea.json"]
|
|
101
|
+
}]
|
|
102
|
+
|
|
103
|
+
// Subscribe to container (all resources under path)
|
|
104
|
+
["REQ", "sub2", {
|
|
105
|
+
"kinds": [30078, 30079],
|
|
106
|
+
"#d": ["https://alice.solid.social/notes/"]
|
|
107
|
+
}]
|
|
108
|
+
|
|
109
|
+
// Subscribe to all changes by user
|
|
110
|
+
["REQ", "sub3", {
|
|
111
|
+
"kinds": [30078],
|
|
112
|
+
"authors": ["<alice-pubkey>"]
|
|
113
|
+
}]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Implementation
|
|
117
|
+
|
|
118
|
+
### Phase 1: Basic Relay
|
|
119
|
+
|
|
120
|
+
Add NIP-01 relay functionality to existing WebSocket endpoint:
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
// src/notifications/nostr-relay.js
|
|
124
|
+
|
|
125
|
+
export function handleNostrMessage(socket, message) {
|
|
126
|
+
const [type, ...params] = JSON.parse(message);
|
|
127
|
+
|
|
128
|
+
switch (type) {
|
|
129
|
+
case 'EVENT':
|
|
130
|
+
return handleEvent(socket, params[0]);
|
|
131
|
+
case 'REQ':
|
|
132
|
+
return handleSubscription(socket, params[0], params.slice(1));
|
|
133
|
+
case 'CLOSE':
|
|
134
|
+
return handleClose(socket, params[0]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Phase 2: LDP-Event Bridge
|
|
140
|
+
|
|
141
|
+
When LDP resources change, emit Nostr events:
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
// src/handlers/resource.js (modified)
|
|
145
|
+
|
|
146
|
+
export async function handlePut(request, reply) {
|
|
147
|
+
// ... existing LDP logic ...
|
|
148
|
+
|
|
149
|
+
// After successful write, emit Nostr event
|
|
150
|
+
if (request.nostrPubkey) {
|
|
151
|
+
await emitResourceEvent({
|
|
152
|
+
pubkey: request.nostrPubkey,
|
|
153
|
+
resourceUrl,
|
|
154
|
+
content,
|
|
155
|
+
contentType
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Phase 3: Federation
|
|
162
|
+
|
|
163
|
+
Connect to other relays for event propagation:
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
// src/notifications/federation.js
|
|
167
|
+
|
|
168
|
+
const FEDERATION_RELAYS = [
|
|
169
|
+
'wss://relay.damus.io',
|
|
170
|
+
'wss://nos.lol',
|
|
171
|
+
'wss://relay.nostr.band'
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
export async function federateEvent(event) {
|
|
175
|
+
// Only federate public resources
|
|
176
|
+
if (await isPublicResource(event.tags.find(t => t[0] === 'd')[1])) {
|
|
177
|
+
for (const relay of FEDERATION_RELAYS) {
|
|
178
|
+
publishToRelay(relay, event);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Phase 4: Identity Unification
|
|
185
|
+
|
|
186
|
+
WebID document includes Nostr pubkey:
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"@context": {...},
|
|
191
|
+
"@id": "https://alice.solid.social/profile/card#me",
|
|
192
|
+
"foaf:name": "Alice",
|
|
193
|
+
"nostr:pubkey": "npub1abc...",
|
|
194
|
+
"nostr:relays": ["wss://alice.solid.social"]
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Nostr profile (kind:0) links to WebID:
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"kind": 0,
|
|
203
|
+
"content": "{\"name\":\"Alice\",\"webid\":\"https://alice.solid.social/profile/card#me\"}"
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## WebSocket Endpoint
|
|
208
|
+
|
|
209
|
+
Single endpoint handles both protocols:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
wss://alice.solid.social/.notifications
|
|
213
|
+
|
|
214
|
+
Protocol detection:
|
|
215
|
+
- If first message is JSON array starting with "EVENT"/"REQ" → Nostr
|
|
216
|
+
- If first message is "sub <uri>" → Legacy solid-0.1
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
// src/notifications/websocket.js (modified)
|
|
221
|
+
|
|
222
|
+
export function handleWebSocket(socket, request) {
|
|
223
|
+
socket.on('message', (message) => {
|
|
224
|
+
const msg = message.toString().trim();
|
|
225
|
+
|
|
226
|
+
// Detect protocol
|
|
227
|
+
if (msg.startsWith('[')) {
|
|
228
|
+
// Nostr protocol
|
|
229
|
+
handleNostrMessage(socket, msg);
|
|
230
|
+
} else if (msg.startsWith('sub ') || msg.startsWith('unsub ')) {
|
|
231
|
+
// Legacy solid-0.1
|
|
232
|
+
handleSolidMessage(socket, msg);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Access Control
|
|
239
|
+
|
|
240
|
+
### Public Resources
|
|
241
|
+
- Events federate to other relays
|
|
242
|
+
- Anyone can subscribe
|
|
243
|
+
|
|
244
|
+
### Private Resources
|
|
245
|
+
- Events stay local (no federation)
|
|
246
|
+
- NIP-42 AUTH required to subscribe
|
|
247
|
+
- Subscription filter must match authorized pubkeys
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
// NIP-42 AUTH flow
|
|
251
|
+
["AUTH", "<signed-event>"]
|
|
252
|
+
|
|
253
|
+
// Server validates and restricts subscriptions
|
|
254
|
+
// to resources the pubkey has access to
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### ACL Mapping
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
acl:Read → Can subscribe to events
|
|
261
|
+
acl:Write → Can publish events (create/update)
|
|
262
|
+
acl:Control → Can modify ACL events
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Storage
|
|
266
|
+
|
|
267
|
+
Two options:
|
|
268
|
+
|
|
269
|
+
### Option A: Dual Storage (Recommended for Phase 1)
|
|
270
|
+
- LDP resources in filesystem (existing)
|
|
271
|
+
- Nostr events in SQLite/memory (relay state)
|
|
272
|
+
- Bridge syncs between them
|
|
273
|
+
|
|
274
|
+
### Option B: Event-Native Storage (Future)
|
|
275
|
+
- All resources stored as Nostr events
|
|
276
|
+
- LDP is a view over event history
|
|
277
|
+
- Full audit trail built-in
|
|
278
|
+
- Replaces filesystem storage
|
|
279
|
+
|
|
280
|
+
## Configuration
|
|
281
|
+
|
|
282
|
+
```json
|
|
283
|
+
{
|
|
284
|
+
"nostr": {
|
|
285
|
+
"enabled": true,
|
|
286
|
+
"relay": {
|
|
287
|
+
"nip01": true,
|
|
288
|
+
"nip42": true,
|
|
289
|
+
"nip78": true
|
|
290
|
+
},
|
|
291
|
+
"federation": {
|
|
292
|
+
"enabled": false,
|
|
293
|
+
"relays": [],
|
|
294
|
+
"publicOnly": true
|
|
295
|
+
},
|
|
296
|
+
"kinds": {
|
|
297
|
+
"resource": 30078,
|
|
298
|
+
"container": 30079,
|
|
299
|
+
"acl": 30080
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Migration Path
|
|
306
|
+
|
|
307
|
+
1. **Phase 1**: Add relay alongside existing WebSocket
|
|
308
|
+
- Both protocols on same endpoint
|
|
309
|
+
- No breaking changes
|
|
310
|
+
|
|
311
|
+
2. **Phase 2**: LDP-Event bridge
|
|
312
|
+
- Changes emit events
|
|
313
|
+
- Subscriptions work via Nostr
|
|
314
|
+
|
|
315
|
+
3. **Phase 3**: Federation (optional)
|
|
316
|
+
- Public resources propagate
|
|
317
|
+
- Discovery via relay network
|
|
318
|
+
|
|
319
|
+
4. **Phase 4**: Deprecate solid-0.1
|
|
320
|
+
- Nostr becomes primary notification protocol
|
|
321
|
+
- Mashlib adapter if needed
|
|
322
|
+
|
|
323
|
+
## Benefits
|
|
324
|
+
|
|
325
|
+
| Feature | Solid Notifications | Nostr Relay++ |
|
|
326
|
+
|---------|--------------------|--------------|
|
|
327
|
+
| Protocol complexity | High | Low |
|
|
328
|
+
| Existing clients | ~0 | Millions |
|
|
329
|
+
| Federation | No | Yes |
|
|
330
|
+
| Mobile push | Build it yourself | Existing infrastructure |
|
|
331
|
+
| Identity | Separate (WebID) | Integrated (npub) |
|
|
332
|
+
| Signatures | Optional | Every event |
|
|
333
|
+
| Ecosystem | Academic | Active |
|
|
334
|
+
|
|
335
|
+
## Open Questions
|
|
336
|
+
|
|
337
|
+
1. **Kind numbers**: Apply for official NIP allocation or use 30078-30080 range?
|
|
338
|
+
|
|
339
|
+
2. **Content encoding**: Store JSON-LD directly in content, or reference by hash?
|
|
340
|
+
|
|
341
|
+
3. **Large resources**: Nostr events have size limits. Use NIP-94/NIP-96 for large files?
|
|
342
|
+
|
|
343
|
+
4. **Container semantics**: How to represent ldp:contains in events?
|
|
344
|
+
|
|
345
|
+
5. **Conflict resolution**: Last-write-wins via created_at, or something smarter?
|
|
346
|
+
|
|
347
|
+
## References
|
|
348
|
+
|
|
349
|
+
- [NIP-01: Basic Protocol](https://github.com/nostr-protocol/nips/blob/master/01.md)
|
|
350
|
+
- [NIP-42: Authentication](https://github.com/nostr-protocol/nips/blob/master/42.md)
|
|
351
|
+
- [NIP-78: Arbitrary Custom App Data](https://github.com/nostr-protocol/nips/blob/master/78.md)
|
|
352
|
+
- [NIP-98: HTTP Auth](https://github.com/nostr-protocol/nips/blob/master/98.md)
|
|
353
|
+
- [Solid Protocol](https://solidproject.org/TR/protocol)
|
package/package.json
CHANGED
package/src/handlers/resource.js
CHANGED
|
@@ -75,36 +75,57 @@ export async function handleGet(request, reply) {
|
|
|
75
75
|
acceptHeader.includes('text/n3') ||
|
|
76
76
|
acceptHeader.includes('application/n-triples')
|
|
77
77
|
);
|
|
78
|
+
const wantsJsonLd = connegEnabled && (
|
|
79
|
+
acceptHeader.includes('application/ld+json') ||
|
|
80
|
+
acceptHeader.includes('application/json')
|
|
81
|
+
);
|
|
78
82
|
|
|
79
|
-
if (wantsTurtle) {
|
|
80
|
-
// Extract JSON-LD from HTML
|
|
83
|
+
if (wantsTurtle || wantsJsonLd) {
|
|
84
|
+
// Extract JSON-LD from HTML data island
|
|
81
85
|
try {
|
|
82
86
|
const htmlStr = content.toString();
|
|
83
|
-
const jsonLdMatch = htmlStr.match(/<script type="application\/ld\+json"
|
|
87
|
+
const jsonLdMatch = htmlStr.match(/<script type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/);
|
|
84
88
|
if (jsonLdMatch) {
|
|
85
89
|
const jsonLd = JSON.parse(jsonLdMatch[1]);
|
|
86
|
-
const { content: turtleContent } = await fromJsonLd(
|
|
87
|
-
jsonLd,
|
|
88
|
-
'text/turtle',
|
|
89
|
-
resourceUrl,
|
|
90
|
-
true
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
const headers = getAllHeaders({
|
|
94
|
-
isContainer: true,
|
|
95
|
-
etag: indexStats?.etag || stats.etag,
|
|
96
|
-
contentType: 'text/turtle',
|
|
97
|
-
origin,
|
|
98
|
-
resourceUrl,
|
|
99
|
-
connegEnabled
|
|
100
|
-
});
|
|
101
90
|
|
|
102
|
-
|
|
103
|
-
|
|
91
|
+
if (wantsTurtle) {
|
|
92
|
+
// Convert to Turtle
|
|
93
|
+
const { content: turtleContent } = await fromJsonLd(
|
|
94
|
+
jsonLd,
|
|
95
|
+
'text/turtle',
|
|
96
|
+
resourceUrl,
|
|
97
|
+
true
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const headers = getAllHeaders({
|
|
101
|
+
isContainer: true,
|
|
102
|
+
etag: indexStats?.etag || stats.etag,
|
|
103
|
+
contentType: 'text/turtle',
|
|
104
|
+
origin,
|
|
105
|
+
resourceUrl,
|
|
106
|
+
connegEnabled
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
110
|
+
return reply.send(turtleContent);
|
|
111
|
+
} else {
|
|
112
|
+
// Return JSON-LD directly
|
|
113
|
+
const headers = getAllHeaders({
|
|
114
|
+
isContainer: true,
|
|
115
|
+
etag: indexStats?.etag || stats.etag,
|
|
116
|
+
contentType: 'application/ld+json',
|
|
117
|
+
origin,
|
|
118
|
+
resourceUrl,
|
|
119
|
+
connegEnabled
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
123
|
+
return reply.send(JSON.stringify(jsonLd, null, 2));
|
|
124
|
+
}
|
|
104
125
|
}
|
|
105
126
|
} catch (err) {
|
|
106
127
|
// Fall through to serve HTML if conversion fails
|
|
107
|
-
console.error('Failed to convert profile to
|
|
128
|
+
console.error('Failed to convert profile to RDF:', err.message);
|
|
108
129
|
}
|
|
109
130
|
}
|
|
110
131
|
|
|
@@ -329,14 +350,45 @@ export async function handleHead(request, reply) {
|
|
|
329
350
|
}
|
|
330
351
|
|
|
331
352
|
const origin = request.headers.origin;
|
|
332
|
-
const
|
|
353
|
+
const connegEnabled = request.connegEnabled || false;
|
|
354
|
+
let contentType;
|
|
355
|
+
|
|
356
|
+
if (stats.isDirectory) {
|
|
357
|
+
// For directories with index.html, determine content type based on Accept header
|
|
358
|
+
const indexPath = storagePath.endsWith('/') ? `${storagePath}index.html` : `${storagePath}/index.html`;
|
|
359
|
+
const indexExists = await storage.exists(indexPath);
|
|
360
|
+
|
|
361
|
+
if (indexExists && connegEnabled) {
|
|
362
|
+
const acceptHeader = request.headers.accept || '';
|
|
363
|
+
const wantsTurtle = acceptHeader.includes('text/turtle') ||
|
|
364
|
+
acceptHeader.includes('text/n3') ||
|
|
365
|
+
acceptHeader.includes('application/n-triples');
|
|
366
|
+
const wantsJsonLd = acceptHeader.includes('application/ld+json') ||
|
|
367
|
+
acceptHeader.includes('application/json');
|
|
368
|
+
|
|
369
|
+
if (wantsTurtle) {
|
|
370
|
+
contentType = 'text/turtle';
|
|
371
|
+
} else if (wantsJsonLd) {
|
|
372
|
+
contentType = 'application/ld+json';
|
|
373
|
+
} else {
|
|
374
|
+
contentType = 'text/html';
|
|
375
|
+
}
|
|
376
|
+
} else if (indexExists) {
|
|
377
|
+
contentType = 'text/html';
|
|
378
|
+
} else {
|
|
379
|
+
contentType = 'application/ld+json';
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
contentType = getContentType(storagePath);
|
|
383
|
+
}
|
|
333
384
|
|
|
334
385
|
const headers = getAllHeaders({
|
|
335
386
|
isContainer: stats.isDirectory,
|
|
336
387
|
etag: stats.etag,
|
|
337
388
|
contentType,
|
|
338
389
|
origin,
|
|
339
|
-
resourceUrl
|
|
390
|
+
resourceUrl,
|
|
391
|
+
connegEnabled
|
|
340
392
|
});
|
|
341
393
|
|
|
342
394
|
if (!stats.isDirectory) {
|
package/src/idp/credentials.js
CHANGED
|
@@ -106,7 +106,8 @@ export async function handleCredentials(request, reply, issuer) {
|
|
|
106
106
|
// Always generate a proper JWT - CTH requires JWT format
|
|
107
107
|
const jwks = await getJwks();
|
|
108
108
|
const signingKey = jwks.keys[0];
|
|
109
|
-
const
|
|
109
|
+
const signingAlg = signingKey.alg || 'ES256'; // Use key's algorithm
|
|
110
|
+
const privateKey = await jose.importJWK(signingKey, signingAlg);
|
|
110
111
|
|
|
111
112
|
const now = Math.floor(Date.now() / 1000);
|
|
112
113
|
const tokenPayload = {
|
|
@@ -131,7 +132,7 @@ export async function handleCredentials(request, reply, issuer) {
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
const accessToken = await new jose.SignJWT(tokenPayload)
|
|
134
|
-
.setProtectedHeader({ alg:
|
|
135
|
+
.setProtectedHeader({ alg: signingAlg, kid: signingKey.kid })
|
|
135
136
|
.sign(privateKey);
|
|
136
137
|
|
|
137
138
|
// Response
|
package/src/idp/index.js
CHANGED
|
@@ -179,7 +179,7 @@ export async function idpPlugin(fastify, options) {
|
|
|
179
179
|
response_modes_supported: ['query', 'fragment', 'form_post'],
|
|
180
180
|
grant_types_supported: ['authorization_code', 'refresh_token', 'client_credentials'],
|
|
181
181
|
subject_types_supported: ['public'],
|
|
182
|
-
id_token_signing_alg_values_supported: ['ES256'],
|
|
182
|
+
id_token_signing_alg_values_supported: ['RS256', 'ES256'],
|
|
183
183
|
token_endpoint_auth_methods_supported: ['none', 'client_secret_basic', 'client_secret_post'],
|
|
184
184
|
claims_supported: ['sub', 'webid', 'name', 'email', 'email_verified'],
|
|
185
185
|
code_challenge_methods_supported: ['S256'],
|
package/src/idp/keys.js
CHANGED
|
@@ -21,16 +21,15 @@ function getJwksPath() {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Generate a new EC P-256 key pair for signing
|
|
24
|
+
* Generate a new EC P-256 key pair for signing (ES256)
|
|
25
25
|
* @returns {Promise<object>} - JWK key pair with private key
|
|
26
26
|
*/
|
|
27
|
-
async function
|
|
27
|
+
async function generateES256Key() {
|
|
28
28
|
const { publicKey, privateKey } = await jose.generateKeyPair('ES256', {
|
|
29
29
|
extractable: true,
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
const privateJwk = await jose.exportJWK(privateKey);
|
|
33
|
-
const publicJwk = await jose.exportJWK(publicKey);
|
|
34
33
|
|
|
35
34
|
// Add metadata
|
|
36
35
|
const kid = crypto.randomUUID();
|
|
@@ -45,6 +44,44 @@ async function generateSigningKey() {
|
|
|
45
44
|
};
|
|
46
45
|
}
|
|
47
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Generate a new RSA key pair for signing (RS256)
|
|
49
|
+
* NSS v5.x may only support RS256 for external IdP verification
|
|
50
|
+
* @returns {Promise<object>} - JWK key pair with private key
|
|
51
|
+
*/
|
|
52
|
+
async function generateRS256Key() {
|
|
53
|
+
const { publicKey, privateKey } = await jose.generateKeyPair('RS256', {
|
|
54
|
+
modulusLength: 2048,
|
|
55
|
+
extractable: true,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const privateJwk = await jose.exportJWK(privateKey);
|
|
59
|
+
|
|
60
|
+
// Add metadata
|
|
61
|
+
const kid = crypto.randomUUID();
|
|
62
|
+
const now = Math.floor(Date.now() / 1000);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
...privateJwk,
|
|
66
|
+
kid,
|
|
67
|
+
use: 'sig',
|
|
68
|
+
alg: 'RS256',
|
|
69
|
+
iat: now,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Generate signing keys (both ES256 and RS256 for compatibility)
|
|
75
|
+
* @returns {Promise<object[]>} - Array of JWK key pairs
|
|
76
|
+
*/
|
|
77
|
+
async function generateSigningKeys() {
|
|
78
|
+
// Generate RS256 first (primary, for NSS compatibility)
|
|
79
|
+
const rs256Key = await generateRS256Key();
|
|
80
|
+
// Also generate ES256 for modern clients
|
|
81
|
+
const es256Key = await generateES256Key();
|
|
82
|
+
return [rs256Key, es256Key];
|
|
83
|
+
}
|
|
84
|
+
|
|
48
85
|
/**
|
|
49
86
|
* Generate cookie signing keys
|
|
50
87
|
* @returns {string[]} - Array of random secret strings
|
|
@@ -66,25 +103,36 @@ export async function initializeKeys() {
|
|
|
66
103
|
try {
|
|
67
104
|
// Try to load existing keys
|
|
68
105
|
const data = await fs.readJson(getJwksPath());
|
|
106
|
+
|
|
107
|
+
// Check if we have RS256 key (needed for NSS compatibility)
|
|
108
|
+
const hasRS256 = data.jwks.keys.some((k) => k.alg === 'RS256');
|
|
109
|
+
if (!hasRS256) {
|
|
110
|
+
console.log('Adding RS256 key for NSS compatibility...');
|
|
111
|
+
const rs256Key = await generateRS256Key();
|
|
112
|
+
data.jwks.keys.unshift(rs256Key); // RS256 first (primary)
|
|
113
|
+
await fs.writeJson(getJwksPath(), data, { spaces: 2 });
|
|
114
|
+
console.log('RS256 key added.');
|
|
115
|
+
}
|
|
116
|
+
|
|
69
117
|
return data;
|
|
70
118
|
} catch (err) {
|
|
71
119
|
if (err.code !== 'ENOENT') throw err;
|
|
72
120
|
|
|
73
|
-
// Generate new keys
|
|
121
|
+
// Generate new keys (both RS256 and ES256)
|
|
74
122
|
console.log('Generating new IdP signing keys...');
|
|
75
|
-
const
|
|
123
|
+
const signingKeys = await generateSigningKeys();
|
|
76
124
|
const cookieKeys = generateCookieKeys();
|
|
77
125
|
|
|
78
126
|
const data = {
|
|
79
127
|
jwks: {
|
|
80
|
-
keys:
|
|
128
|
+
keys: signingKeys,
|
|
81
129
|
},
|
|
82
130
|
cookieKeys,
|
|
83
131
|
createdAt: new Date().toISOString(),
|
|
84
132
|
};
|
|
85
133
|
|
|
86
134
|
await fs.writeJson(getJwksPath(), data, { spaces: 2 });
|
|
87
|
-
console.log('IdP signing keys generated and saved.');
|
|
135
|
+
console.log('IdP signing keys generated and saved (RS256 + ES256).');
|
|
88
136
|
|
|
89
137
|
return data;
|
|
90
138
|
}
|
|
@@ -100,7 +148,8 @@ export async function getPublicJwks() {
|
|
|
100
148
|
// Return only public key components
|
|
101
149
|
const publicKeys = jwks.keys.map((key) => {
|
|
102
150
|
// For EC keys, remove 'd' (private key component)
|
|
103
|
-
|
|
151
|
+
// For RSA keys, remove 'd', 'p', 'q', 'dp', 'dq', 'qi' (private components)
|
|
152
|
+
const { d, p, q, dp, dq, qi, ...publicKey } = key;
|
|
104
153
|
return publicKey;
|
|
105
154
|
});
|
|
106
155
|
|
package/src/idp/provider.js
CHANGED
|
@@ -8,6 +8,68 @@ import { createAdapter } from './adapter.js';
|
|
|
8
8
|
import { getJwks, getCookieKeys } from './keys.js';
|
|
9
9
|
import { getAccountForProvider } from './accounts.js';
|
|
10
10
|
|
|
11
|
+
// Cache for fetched client documents
|
|
12
|
+
const clientDocumentCache = new Map();
|
|
13
|
+
const CLIENT_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fetch and validate a Solid-OIDC Client Identifier Document
|
|
17
|
+
* @param {string} clientId - URL to the client document
|
|
18
|
+
* @returns {Promise<object|null>} - Client metadata or null
|
|
19
|
+
*/
|
|
20
|
+
async function fetchClientDocument(clientId) {
|
|
21
|
+
try {
|
|
22
|
+
// Check cache
|
|
23
|
+
const cached = clientDocumentCache.get(clientId);
|
|
24
|
+
if (cached && Date.now() - cached.timestamp < CLIENT_CACHE_TTL) {
|
|
25
|
+
return cached.data;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const response = await fetch(clientId, {
|
|
29
|
+
headers: { 'Accept': 'application/json, application/ld+json' },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
console.error(`Failed to fetch client document from ${clientId}: ${response.status}`);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const doc = await response.json();
|
|
38
|
+
|
|
39
|
+
// Validate required fields for Solid-OIDC client
|
|
40
|
+
// The client_id in the document must match the URL we fetched
|
|
41
|
+
if (doc.client_id && doc.client_id !== clientId) {
|
|
42
|
+
console.error(`Client ID mismatch: document says ${doc.client_id}, URL is ${clientId}`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Build client metadata compatible with oidc-provider
|
|
47
|
+
const clientMeta = {
|
|
48
|
+
client_id: clientId,
|
|
49
|
+
client_name: doc.client_name || doc.name || 'Unknown Client',
|
|
50
|
+
redirect_uris: doc.redirect_uris || [],
|
|
51
|
+
response_types: ['code'],
|
|
52
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
53
|
+
token_endpoint_auth_method: 'none', // Public client
|
|
54
|
+
application_type: 'web',
|
|
55
|
+
// Copy other useful metadata
|
|
56
|
+
logo_uri: doc.logo_uri,
|
|
57
|
+
client_uri: doc.client_uri,
|
|
58
|
+
policy_uri: doc.policy_uri,
|
|
59
|
+
tos_uri: doc.tos_uri,
|
|
60
|
+
scope: doc.scope || 'openid webid',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Cache the result
|
|
64
|
+
clientDocumentCache.set(clientId, { data: clientMeta, timestamp: Date.now() });
|
|
65
|
+
|
|
66
|
+
return clientMeta;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error(`Error fetching client document from ${clientId}:`, err.message);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
11
73
|
/**
|
|
12
74
|
* Create and configure the OIDC provider
|
|
13
75
|
* @param {string} issuer - The issuer URL (e.g., 'https://example.com')
|
|
@@ -242,12 +304,17 @@ export async function createProvider(issuer) {
|
|
|
242
304
|
return true;
|
|
243
305
|
},
|
|
244
306
|
|
|
307
|
+
// Extra client metadata fields to allow
|
|
308
|
+
extraClientMetadata: {
|
|
309
|
+
properties: ['client_name', 'logo_uri', 'client_uri', 'policy_uri', 'tos_uri'],
|
|
310
|
+
},
|
|
311
|
+
|
|
245
312
|
// Client defaults
|
|
246
313
|
clientDefaults: {
|
|
247
314
|
grant_types: ['authorization_code', 'refresh_token'],
|
|
248
315
|
response_types: ['code'],
|
|
249
316
|
token_endpoint_auth_method: 'none', // Public clients by default
|
|
250
|
-
id_token_signed_response_alg: '
|
|
317
|
+
id_token_signed_response_alg: 'RS256', // RS256 for NSS compatibility
|
|
251
318
|
},
|
|
252
319
|
|
|
253
320
|
// Response modes
|
|
@@ -263,9 +330,12 @@ export async function createProvider(issuer) {
|
|
|
263
330
|
methods: ['S256'],
|
|
264
331
|
},
|
|
265
332
|
|
|
266
|
-
// Enable RS256 for DPoP (
|
|
333
|
+
// Enable RS256 for DPoP and ID tokens (NSS requires RS256)
|
|
267
334
|
enabledJWA: {
|
|
268
335
|
dPoPSigningAlgValues: ['ES256', 'RS256', 'Ed25519', 'EdDSA'],
|
|
336
|
+
idTokenSigningAlgValues: ['RS256', 'ES256'],
|
|
337
|
+
userinfoSigningAlgValues: ['RS256', 'ES256'],
|
|
338
|
+
introspectionSigningAlgValues: ['RS256', 'ES256'],
|
|
269
339
|
},
|
|
270
340
|
|
|
271
341
|
// Enable request parameter
|
|
@@ -337,5 +407,33 @@ export async function createProvider(issuer) {
|
|
|
337
407
|
// Allow localhost for development
|
|
338
408
|
provider.proxy = true;
|
|
339
409
|
|
|
410
|
+
// Override Client.find to support Solid-OIDC Client Identifier Documents
|
|
411
|
+
// When client_id is a URL, fetch the document and create a client from it
|
|
412
|
+
const originalClientFind = provider.Client.find.bind(provider.Client);
|
|
413
|
+
provider.Client.find = async function(id, ...args) {
|
|
414
|
+
// First try the normal lookup (registered clients)
|
|
415
|
+
let client = await originalClientFind(id, ...args);
|
|
416
|
+
if (client) {
|
|
417
|
+
return client;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// If client_id looks like a URL, try to fetch the client document
|
|
421
|
+
if (id && (id.startsWith('http://') || id.startsWith('https://'))) {
|
|
422
|
+
const clientMeta = await fetchClientDocument(id);
|
|
423
|
+
if (clientMeta) {
|
|
424
|
+
// Create a temporary client object from the fetched metadata
|
|
425
|
+
// Use the Client constructor with the metadata
|
|
426
|
+
try {
|
|
427
|
+
client = new provider.Client(clientMeta, undefined);
|
|
428
|
+
return client;
|
|
429
|
+
} catch (err) {
|
|
430
|
+
console.error('Failed to create client from document:', err.message);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return undefined;
|
|
436
|
+
};
|
|
437
|
+
|
|
340
438
|
return provider;
|
|
341
439
|
}
|
package/src/rdf/turtle.js
CHANGED
|
@@ -66,9 +66,11 @@ export async function jsonLdToTurtle(jsonLd, baseUri) {
|
|
|
66
66
|
try {
|
|
67
67
|
const quads = jsonLdToQuads(jsonLd, baseUri);
|
|
68
68
|
|
|
69
|
+
// Don't use baseIRI in writer - output absolute URIs for compatibility
|
|
70
|
+
// Some Solid servers (like NSS) may not properly resolve relative URIs
|
|
71
|
+
// when verifying oidcIssuer claims
|
|
69
72
|
const writer = new Writer({
|
|
70
|
-
prefixes: COMMON_PREFIXES
|
|
71
|
-
baseIRI: baseUri
|
|
73
|
+
prefixes: COMMON_PREFIXES
|
|
72
74
|
});
|
|
73
75
|
|
|
74
76
|
for (const q of quads) {
|
package/src/server.js
CHANGED
|
@@ -131,9 +131,10 @@ export function createServer(options = {}) {
|
|
|
131
131
|
// Authorization hook - check WAC permissions
|
|
132
132
|
// Skip for pod creation endpoint (needs special handling)
|
|
133
133
|
fastify.addHook('preHandler', async (request, reply) => {
|
|
134
|
-
// Skip auth for pod creation, OPTIONS, IdP routes, mashlib,
|
|
134
|
+
// Skip auth for pod creation, OPTIONS, IdP routes, mashlib, well-known, and notifications
|
|
135
135
|
const mashlibPaths = ['/mashlib.min.js', '/mash.css', '/841.mashlib.min.js'];
|
|
136
136
|
if (request.url === '/.pods' ||
|
|
137
|
+
request.url === '/.notifications' ||
|
|
137
138
|
request.method === 'OPTIONS' ||
|
|
138
139
|
request.url.startsWith('/idp/') ||
|
|
139
140
|
request.url.startsWith('/.well-known/') ||
|