javascript-solid-server 0.0.31 → 0.0.33
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/README.md +13 -5
- package/clock-updater.mjs +58 -0
- package/package.json +2 -2
- package/src/handlers/container.js +4 -5
- package/src/handlers/resource.js +41 -2
- package/src/idp/index.js +2 -0
- package/src/idp/interactions.js +14 -3
- package/src/ldp/headers.js +1 -1
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.31)
|
|
8
8
|
|
|
9
9
|
- **LDP CRUD Operations** - GET, PUT, POST, DELETE, HEAD
|
|
10
10
|
- **N3 Patch** - Solid's native patch format for RDF updates
|
|
@@ -17,14 +17,14 @@ A minimal, fast, JSON-LD native Solid server.
|
|
|
17
17
|
- **Multi-user Pods** - Path-based (`/alice/`) or subdomain-based (`alice.example.com`)
|
|
18
18
|
- **Subdomain Mode** - XSS protection via origin isolation
|
|
19
19
|
- **Mashlib Data Browser** - Optional SolidOS UI (CDN or local hosting)
|
|
20
|
-
- **WebID Profiles** - JSON-LD
|
|
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
22
|
- **Solid-OIDC Identity Provider** - Built-in IdP with DPoP, 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
|
-
- **Content Negotiation** -
|
|
27
|
+
- **Content Negotiation** - Turtle <-> JSON-LD conversion, including HTML data islands
|
|
28
28
|
- **CORS Support** - Full cross-origin resource sharing
|
|
29
29
|
|
|
30
30
|
### HTTP Methods
|
|
@@ -36,7 +36,7 @@ A minimal, fast, JSON-LD native Solid server.
|
|
|
36
36
|
| PUT | Full - Create/update resources |
|
|
37
37
|
| POST | Full - Create in containers |
|
|
38
38
|
| DELETE | Full |
|
|
39
|
-
| PATCH | N3 Patch
|
|
39
|
+
| PATCH | N3 Patch + SPARQL Update |
|
|
40
40
|
| OPTIONS | Full with CORS |
|
|
41
41
|
|
|
42
42
|
## Getting Started
|
|
@@ -285,7 +285,15 @@ npm install && npm run build
|
|
|
285
285
|
3. Mashlib fetches the actual data via content negotiation
|
|
286
286
|
4. Mashlib renders an interactive, editable view
|
|
287
287
|
|
|
288
|
-
**Note:** Mashlib works best with `--conneg` enabled for Turtle support.
|
|
288
|
+
**Note:** Mashlib works best with `--conneg` enabled for Turtle support.
|
|
289
|
+
|
|
290
|
+
### Profile Pages
|
|
291
|
+
|
|
292
|
+
Pod profiles (`/alice/`) use HTML with embedded JSON-LD data islands and are rendered using:
|
|
293
|
+
- [mashlib-jss](https://github.com/JavaScriptSolidServer/mashlib-jss) - A fork of mashlib with `getPod()` fix for path-based pods
|
|
294
|
+
- [solidos-lite](https://github.com/SolidOS/solidos-lite) - Parses JSON-LD data islands into the RDF store
|
|
295
|
+
|
|
296
|
+
This allows profiles to work without server-side content negotiation while still providing full SolidOS editing capabilities.
|
|
289
297
|
|
|
290
298
|
### WebSocket Notifications
|
|
291
299
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clock Updater - Updates the Solid clock every second using Nostr auth
|
|
3
|
+
* Usage: node clock-updater.mjs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getPublicKey, finalizeEvent } from 'nostr-tools';
|
|
7
|
+
import { getToken } from 'nostr-tools/nip98';
|
|
8
|
+
|
|
9
|
+
// Nostr keypair (in production, load from env/file)
|
|
10
|
+
const SK_HEX = '3f188544fb81bd324ead7be9697fd9503d18345e233a7b0182915b0b582ddd70';
|
|
11
|
+
const sk = Uint8Array.from(Buffer.from(SK_HEX, 'hex'));
|
|
12
|
+
const pk = getPublicKey(sk);
|
|
13
|
+
|
|
14
|
+
const CLOCK_URL = 'https://solid.social/melvin/public/clock.json';
|
|
15
|
+
|
|
16
|
+
async function updateClock() {
|
|
17
|
+
const now = Math.floor(Date.now() / 1000);
|
|
18
|
+
const isoDate = new Date(now * 1000).toISOString();
|
|
19
|
+
|
|
20
|
+
const clockData = {
|
|
21
|
+
'@context': { 'schema': 'http://schema.org/' },
|
|
22
|
+
'@id': '#clock',
|
|
23
|
+
'@type': 'schema:Clock',
|
|
24
|
+
'schema:dateModified': isoDate,
|
|
25
|
+
'schema:value': now
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const token = await getToken(CLOCK_URL, 'PUT', (e) => finalizeEvent(e, sk));
|
|
30
|
+
|
|
31
|
+
const res = await fetch(CLOCK_URL, {
|
|
32
|
+
method: 'PUT',
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'application/ld+json',
|
|
35
|
+
'Authorization': 'Nostr ' + token
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify(clockData)
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const time = isoDate.split('T')[1].replace('Z', '');
|
|
41
|
+
if (res.ok) {
|
|
42
|
+
process.stdout.write(`\r${time} - Updated`);
|
|
43
|
+
} else {
|
|
44
|
+
console.log(`\n${time} - Error: ${res.status} ${res.statusText}`);
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.log(`\nError: ${err.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log('Clock Updater started');
|
|
52
|
+
console.log('did:nostr:', 'did:nostr:' + pk);
|
|
53
|
+
console.log('Target:', CLOCK_URL);
|
|
54
|
+
console.log('Press Ctrl+C to stop\n');
|
|
55
|
+
|
|
56
|
+
// Run immediately, then every second
|
|
57
|
+
updateClock();
|
|
58
|
+
setInterval(updateClock, 1000);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "javascript-solid-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.33",
|
|
4
4
|
"description": "A minimal, fast Solid server",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@fastify/websocket": "^8.3.1",
|
|
28
28
|
"bcrypt": "^6.0.0",
|
|
29
29
|
"commander": "^14.0.2",
|
|
30
|
-
"fastify": "^4.
|
|
30
|
+
"fastify": "^4.29.1",
|
|
31
31
|
"fs-extra": "^11.2.0",
|
|
32
32
|
"jose": "^6.1.3",
|
|
33
33
|
"n3": "^1.26.0",
|
|
@@ -125,12 +125,11 @@ export async function handlePost(request, reply) {
|
|
|
125
125
|
* Create pod directory structure (reusable for registration)
|
|
126
126
|
* @param {string} name - Pod name (username)
|
|
127
127
|
* @param {string} webId - User's WebID URI
|
|
128
|
-
* @param {string}
|
|
128
|
+
* @param {string} podUri - Pod root URI (e.g., https://alice.example.com/ or https://example.com/alice/)
|
|
129
|
+
* @param {string} issuer - OIDC issuer URI
|
|
129
130
|
*/
|
|
130
|
-
export async function createPodStructure(name, webId,
|
|
131
|
+
export async function createPodStructure(name, webId, podUri, issuer) {
|
|
131
132
|
const podPath = `/${name}/`;
|
|
132
|
-
const podUri = `${baseUrl}/${name}/`;
|
|
133
|
-
const issuer = baseUrl + '/';
|
|
134
133
|
|
|
135
134
|
// Create pod directory structure
|
|
136
135
|
await storage.createContainer(podPath);
|
|
@@ -253,7 +252,7 @@ export async function handleCreatePod(request, reply) {
|
|
|
253
252
|
|
|
254
253
|
try {
|
|
255
254
|
// Use shared pod creation function
|
|
256
|
-
await createPodStructure(name, webId,
|
|
255
|
+
await createPodStructure(name, webId, podUri, issuer);
|
|
257
256
|
} catch (err) {
|
|
258
257
|
console.error('Pod creation error:', err);
|
|
259
258
|
// Cleanup on failure
|
package/src/handlers/resource.js
CHANGED
|
@@ -146,6 +146,42 @@ export async function handleGet(request, reply) {
|
|
|
146
146
|
return reply.type('text/html').send(html);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// Check if Turtle/N3 format is requested via content negotiation
|
|
150
|
+
const acceptHeader = request.headers.accept || '';
|
|
151
|
+
const wantsTurtle = connegEnabled && (
|
|
152
|
+
acceptHeader.includes('text/turtle') ||
|
|
153
|
+
acceptHeader.includes('text/n3') ||
|
|
154
|
+
acceptHeader.includes('application/n-triples')
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (wantsTurtle) {
|
|
158
|
+
// Convert container JSON-LD to Turtle
|
|
159
|
+
try {
|
|
160
|
+
const { content: turtleContent } = await fromJsonLd(
|
|
161
|
+
jsonLd,
|
|
162
|
+
'text/turtle',
|
|
163
|
+
resourceUrl,
|
|
164
|
+
true
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const headers = getAllHeaders({
|
|
168
|
+
isContainer: true,
|
|
169
|
+
etag: stats.etag,
|
|
170
|
+
contentType: 'text/turtle',
|
|
171
|
+
origin,
|
|
172
|
+
resourceUrl,
|
|
173
|
+
connegEnabled
|
|
174
|
+
});
|
|
175
|
+
headers['Vary'] = 'Accept';
|
|
176
|
+
|
|
177
|
+
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
178
|
+
return reply.send(turtleContent);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
// Fall through to JSON-LD if conversion fails
|
|
181
|
+
console.error('Failed to convert container to Turtle:', err.message);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
149
185
|
const headers = getAllHeaders({
|
|
150
186
|
isContainer: true,
|
|
151
187
|
etag: stats.etag,
|
|
@@ -196,7 +232,9 @@ export async function handleGet(request, reply) {
|
|
|
196
232
|
if (connegEnabled) {
|
|
197
233
|
const contentStr = content.toString();
|
|
198
234
|
const acceptHeader = request.headers.accept || '';
|
|
199
|
-
|
|
235
|
+
// Serve Turtle if: URL ends with .ttl OR Accept header requests it
|
|
236
|
+
const wantsTurtle = urlPath.endsWith('.ttl') ||
|
|
237
|
+
acceptHeader.includes('text/turtle') ||
|
|
200
238
|
acceptHeader.includes('text/n3') ||
|
|
201
239
|
acceptHeader.includes('application/n-triples');
|
|
202
240
|
|
|
@@ -233,7 +271,8 @@ export async function handleGet(request, reply) {
|
|
|
233
271
|
// Plain JSON-LD file
|
|
234
272
|
try {
|
|
235
273
|
const jsonLd = JSON.parse(contentStr);
|
|
236
|
-
|
|
274
|
+
// Use Turtle if URL ends with .ttl, otherwise use Accept header preference
|
|
275
|
+
const targetType = wantsTurtle ? 'text/turtle' : selectContentType(acceptHeader, connegEnabled);
|
|
237
276
|
const { content: outputContent, contentType: outputType } = await fromJsonLd(
|
|
238
277
|
jsonLd,
|
|
239
278
|
targetType,
|
package/src/idp/index.js
CHANGED
|
@@ -184,6 +184,8 @@ export async function idpPlugin(fastify, options) {
|
|
|
184
184
|
claims_supported: ['sub', 'webid', 'name', 'email', 'email_verified'],
|
|
185
185
|
code_challenge_methods_supported: ['S256'],
|
|
186
186
|
dpop_signing_alg_values_supported: ['ES256', 'RS256'],
|
|
187
|
+
// RFC 9207 - OAuth 2.0 Authorization Server Issuer Identification
|
|
188
|
+
authorization_response_iss_parameter_supported: true,
|
|
187
189
|
// Solid-OIDC specific
|
|
188
190
|
solid_oidc_supported: 'https://solidproject.org/TR/solid-oidc',
|
|
189
191
|
};
|
package/src/idp/interactions.js
CHANGED
|
@@ -355,9 +355,20 @@ export async function handleRegisterPost(request, reply, issuer) {
|
|
|
355
355
|
|
|
356
356
|
try {
|
|
357
357
|
// Build URLs - WebID follows standard Solid convention: /profile/card#me
|
|
358
|
+
const subdomainsEnabled = request.subdomainsEnabled;
|
|
359
|
+
const baseDomain = request.baseDomain;
|
|
358
360
|
const baseUrl = issuer.endsWith('/') ? issuer.slice(0, -1) : issuer;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
+
|
|
362
|
+
let podUri, webId;
|
|
363
|
+
if (subdomainsEnabled && baseDomain) {
|
|
364
|
+
// Subdomain mode: alice.example.com/profile/card#me
|
|
365
|
+
podUri = `${request.protocol}://${username}.${baseDomain}/`;
|
|
366
|
+
webId = `${podUri}profile/card#me`;
|
|
367
|
+
} else {
|
|
368
|
+
// Path mode: example.com/alice/profile/card#me
|
|
369
|
+
podUri = `${baseUrl}/${username}/`;
|
|
370
|
+
webId = `${podUri}profile/card#me`;
|
|
371
|
+
}
|
|
361
372
|
|
|
362
373
|
// Check if pod already exists
|
|
363
374
|
const podPath = `${username}/`;
|
|
@@ -367,7 +378,7 @@ export async function handleRegisterPost(request, reply, issuer) {
|
|
|
367
378
|
}
|
|
368
379
|
|
|
369
380
|
// Create pod structure
|
|
370
|
-
await createPodStructure(username, webId,
|
|
381
|
+
await createPodStructure(username, webId, podUri, issuer);
|
|
371
382
|
|
|
372
383
|
// Create account
|
|
373
384
|
await createAccount({
|
package/src/ldp/headers.js
CHANGED
|
@@ -90,7 +90,7 @@ export function getCorsHeaders(origin) {
|
|
|
90
90
|
return {
|
|
91
91
|
'Access-Control-Allow-Origin': origin || '*',
|
|
92
92
|
'Access-Control-Allow-Methods': 'GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS',
|
|
93
|
-
'Access-Control-Allow-Headers': 'Accept, Authorization, Content-Type, If-Match, If-None-Match, Link, Slug, Origin',
|
|
93
|
+
'Access-Control-Allow-Headers': 'Accept, Authorization, Content-Type, DPoP, If-Match, If-None-Match, Link, Slug, Origin',
|
|
94
94
|
'Access-Control-Expose-Headers': 'Accept-Patch, Accept-Post, Allow, Content-Type, ETag, Link, Location, Updates-Via, WAC-Allow',
|
|
95
95
|
'Access-Control-Allow-Credentials': 'true',
|
|
96
96
|
'Access-Control-Max-Age': '86400'
|