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 CHANGED
@@ -4,7 +4,7 @@ A minimal, fast, JSON-LD native Solid server.
4
4
 
5
5
  ## Features
6
6
 
7
- ### Implemented (v0.0.23)
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 structured data in HTML at pod root
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** - Optional Turtle <-> JSON-LD conversion
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 format |
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. Pod profiles (`/alice/`) continue to serve our JSON-LD-in-HTML format.
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.31",
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.25.2",
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} baseUrl - Base URL (without trailing slash)
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, baseUrl) {
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, baseUri);
255
+ await createPodStructure(name, webId, podUri, issuer);
257
256
  } catch (err) {
258
257
  console.error('Pod creation error:', err);
259
258
  // Cleanup on failure
@@ -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
- const wantsTurtle = acceptHeader.includes('text/turtle') ||
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
- const targetType = selectContentType(acceptHeader, connegEnabled);
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
  };
@@ -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
- const podUri = `${baseUrl}/${username}/`;
360
- const webId = `${podUri}profile/card#me`;
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, baseUrl);
381
+ await createPodStructure(username, webId, podUri, issuer);
371
382
 
372
383
  // Create account
373
384
  await createAccount({
@@ -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'