javascript-solid-server 0.0.75 → 0.0.77

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.
@@ -234,7 +234,33 @@
234
234
  "Bash(timeout 90 bash -c:*)",
235
235
  "Bash(git commit -m \"$\\(cat <<''EOF''\nsecurity: add ACL check on WebSocket subscription requests\n\nCheck WAC read permission before allowing subscription to prevent\ninformation leakage via notifications. Unauthorized subscriptions\nnow receive ''err <url> forbidden'' response.\n\nSecurity improvements:\n- Check ACL read access before allowing subscription\n- Validate URLs are on this server \\(prevents SSRF-like probing\\)\n- Add subscription limit and URL length validation\n\nFixes #62\nEOF\n\\)\")",
236
236
  "Bash(gh repo fork:*)",
237
- "Bash(timeout 180 npm test:*)"
237
+ "Bash(timeout 180 npm test:*)",
238
+ "Bash(git show:*)",
239
+ "Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server log --oneline -30 -- \"test/integration/acl-tls-test.mjs\" \"test-esm/integration/acl-tls-test.js\")",
240
+ "Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server log --oneline --follow -30 -- \"test/integration/acl-tls-test.mjs\")",
241
+ "Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server show 778095ad --stat)",
242
+ "Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server show 778095ad)",
243
+ "Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server show b183c7a0)",
244
+ "Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server log --oneline --before=\"2019-10-29\" --after=\"2019-10-01\" -20)",
245
+ "Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server show 1a92a912 --stat)",
246
+ "Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server log --oneline --all --grep=jaxoncreed)",
247
+ "Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server log --oneline --author=\"jaxoncreed\" -30)",
248
+ "Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server log --oneline --author=\"[Dd]mitri\" -20)",
249
+ "Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server log --oneline --all --grep=\"oidc\" -20)",
250
+ "Bash(npm install)",
251
+ "Bash(timeout 60 npx mocha:*)",
252
+ "Bash(timeout 120 npx mocha:*)",
253
+ "Bash(timeout 30 npx mocha:*)",
254
+ "Bash(openssl x509:*)",
255
+ "Bash(gh pr checks:*)",
256
+ "Bash(gh run view:*)",
257
+ "Bash(gh pr edit:*)",
258
+ "WebFetch(domain:patch-diff.githubusercontent.com)",
259
+ "Bash(git rebase:*)",
260
+ "Bash(timeout 10 npm start)",
261
+ "Bash(node bin/jss.js start:*)",
262
+ "Bash(ssh solid.social \"cd /var/www/jss && git pull && pm2 restart jss\")",
263
+ "Bash(ssh solid.social:*)"
238
264
  ]
239
265
  }
240
266
  }
package/README.md CHANGED
@@ -6,8 +6,11 @@ A minimal, fast, JSON-LD native Solid server.
6
6
 
7
7
  ## Features
8
8
 
9
- ### Implemented (v0.0.75)
9
+ ### Implemented (v0.0.77)
10
10
 
11
+ - **Passkey Authentication** - WebAuthn/FIDO2 passwordless login with Touch ID, Face ID, or security keys
12
+ - **HTTP Range Requests** - Partial content delivery for large files and media streaming
13
+ - **Single-User Mode** - Simplified setup for personal pod servers
11
14
  - **ActivityPub Federation** - Fediverse integration with WebFinger, inbox/outbox, HTTP signatures
12
15
  - **LDP CRUD Operations** - GET, PUT, POST, DELETE, HEAD
13
16
  - **N3 Patch** - Solid's native patch format for RDF updates
@@ -122,6 +125,7 @@ jss --help # Show help
122
125
  | `--mashlib` | Enable Mashlib (local mode) | false |
123
126
  | `--mashlib-cdn` | Enable Mashlib (CDN mode) | false |
124
127
  | `--mashlib-version <ver>` | Mashlib CDN version | 2.0.0 |
128
+ | `--solidos-ui` | Enable modern SolidOS UI (requires --mashlib) | false |
125
129
  | `--git` | Enable Git HTTP backend | false |
126
130
  | `--nostr` | Enable Nostr relay | false |
127
131
  | `--nostr-path <path>` | Nostr relay WebSocket path | /relay |
@@ -333,6 +337,18 @@ npm install && npm run build
333
337
 
334
338
  **Note:** Mashlib works best with `--conneg` enabled for Turtle support.
335
339
 
340
+ **Modern UI (SolidOS UI):**
341
+ ```bash
342
+ jss start --mashlib --solidos-ui --conneg
343
+ ```
344
+ Serves a modern Nextcloud-style UI shell while reusing mashlib's data layer. The `--solidos-ui` flag swaps the classic databrowser interface for a cleaner, mobile-friendly design with:
345
+ - Modern file browser with breadcrumb navigation
346
+ - Profile, Contacts, Sharing, and Settings views
347
+ - Path-based URLs (browser URL reflects current resource)
348
+ - Responsive design for mobile devices
349
+
350
+ Requires solidos-ui dist files in `src/mashlib-local/dist/solidos-ui/`. See [solidos-ui](https://github.com/solidos/solidos/tree/main/workspaces/solidos-ui) for details.
351
+
336
352
  ### Profile Pages
337
353
 
338
354
  Pod profiles (`/alice/`) use HTML with embedded JSON-LD data islands and are rendered using:
@@ -657,6 +673,26 @@ Response:
657
673
 
658
674
  For DPoP-bound tokens (Solid-OIDC compliant), include a DPoP proof header.
659
675
 
676
+ ### Passkey Authentication (v0.0.77+)
677
+
678
+ Enable passwordless login with WebAuthn/FIDO2:
679
+
680
+ ```bash
681
+ jss start --idp
682
+ ```
683
+
684
+ **How it works:**
685
+ 1. User logs in with username/password
686
+ 2. Prompted to add a passkey (Touch ID, Face ID, security key)
687
+ 3. Future logins: tap "Sign in with Passkey" → biometric → done!
688
+
689
+ **Benefits:**
690
+ - Phishing-resistant (bound to domain)
691
+ - No passwords to remember or leak
692
+ - Works on mobile and desktop
693
+
694
+ Passkeys are stored per-account and work across devices via platform sync (iCloud Keychain, Google Password Manager, etc.).
695
+
660
696
  ### Solid-OIDC (External IdP)
661
697
 
662
698
  The server also accepts DPoP-bound access tokens from external Solid identity providers:
@@ -856,7 +892,7 @@ npm run benchmark
856
892
  npm test
857
893
  ```
858
894
 
859
- Currently passing: **213 tests** (including 27 conformance tests)
895
+ Currently passing: **223 tests** (including 27 conformance tests)
860
896
 
861
897
  ### Conformance Test Harness (CTH)
862
898
 
package/bin/jss.js CHANGED
@@ -57,6 +57,7 @@ program
57
57
  .option('--mashlib-cdn', 'Enable Mashlib data browser (CDN mode, no local files needed)')
58
58
  .option('--no-mashlib', 'Disable Mashlib data browser')
59
59
  .option('--mashlib-version <version>', 'Mashlib version for CDN mode (default: 2.0.0)')
60
+ .option('--solidos-ui', 'Enable modern Nextcloud-style UI (requires --mashlib)')
60
61
  .option('--git', 'Enable Git HTTP backend (clone/push support)')
61
62
  .option('--no-git', 'Disable Git HTTP backend')
62
63
  .option('--nostr', 'Enable Nostr relay')
@@ -114,6 +115,7 @@ program
114
115
  mashlib: config.mashlib || config.mashlibCdn,
115
116
  mashlibCdn: config.mashlibCdn,
116
117
  mashlibVersion: config.mashlibVersion,
118
+ solidosUi: config.solidosUi,
117
119
  git: config.git,
118
120
  nostr: config.nostr,
119
121
  nostrPath: config.nostrPath,
@@ -143,6 +145,7 @@ program
143
145
  } else if (config.mashlib) {
144
146
  console.log(` Mashlib: local (data browser enabled)`);
145
147
  }
148
+ if (config.solidosUi) console.log(' SolidOS UI: enabled (modern interface)');
146
149
  if (config.git) console.log(' Git: enabled (clone/push support)');
147
150
  if (config.nostr) console.log(` Nostr: enabled (${config.nostrPath})`);
148
151
  if (config.activitypub) console.log(` ActivityPub: enabled (@${config.apUsername || 'me'})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.75",
3
+ "version": "0.0.77",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -26,6 +26,7 @@
26
26
  "@fastify/middie": "^8.3.3",
27
27
  "@fastify/rate-limit": "^9.1.0",
28
28
  "@fastify/websocket": "^8.3.1",
29
+ "@simplewebauthn/server": "^13.2.2",
29
30
  "bcrypt": "^6.0.0",
30
31
  "bcryptjs": "^3.0.3",
31
32
  "better-sqlite3": "^12.5.0",
@@ -9,7 +9,7 @@ import { checkAccess, getRequiredMode } from '../wac/checker.js';
9
9
  import { AccessMode } from '../wac/parser.js';
10
10
  import * as storage from '../storage/filesystem.js';
11
11
  import { getEffectiveUrlPath } from '../utils/url.js';
12
- import { generateDatabrowserHtml } from '../mashlib/index.js';
12
+ import { generateDatabrowserHtml, generateSolidosUiHtml } from '../mashlib/index.js';
13
13
 
14
14
  /**
15
15
  * Check if request is authorized
@@ -117,8 +117,11 @@ export function handleUnauthorized(request, reply, isAuthenticated, wacAllow, au
117
117
  // If mashlib is enabled, serve mashlib instead of static error page
118
118
  // Mashlib has built-in login functionality via panes.runDataBrowser()
119
119
  if (request.mashlibEnabled) {
120
- const cdnVersion = request.mashlibCdn ? request.mashlibVersion : null;
121
- return reply.code(statusCode).type('text/html').send(generateDatabrowserHtml(request.url, cdnVersion));
120
+ // Use SolidOS UI if enabled, otherwise fallback to classic mashlib
121
+ const html = request.solidosUiEnabled
122
+ ? generateSolidosUiHtml()
123
+ : generateDatabrowserHtml(request.url, request.mashlibCdn ? request.mashlibVersion : null);
124
+ return reply.code(statusCode).type('text/html').send(html);
122
125
  }
123
126
  return reply.code(statusCode).type('text/html').send(getErrorPage(statusCode, isAuthenticated, request));
124
127
  }
package/src/config.js CHANGED
@@ -42,6 +42,9 @@ export const defaults = {
42
42
  mashlibCdn: false,
43
43
  mashlibVersion: '2.0.0',
44
44
 
45
+ // SolidOS UI (modern Nextcloud-style interface)
46
+ solidosUi: false,
47
+
45
48
  // Git HTTP backend
46
49
  git: false,
47
50
 
@@ -95,6 +98,7 @@ const envMap = {
95
98
  JSS_MASHLIB: 'mashlib',
96
99
  JSS_MASHLIB_CDN: 'mashlibCdn',
97
100
  JSS_MASHLIB_VERSION: 'mashlibVersion',
101
+ JSS_SOLIDOS_UI: 'solidosUi',
98
102
  JSS_GIT: 'git',
99
103
  JSS_NOSTR: 'nostr',
100
104
  JSS_NOSTR_PATH: 'nostrPath',
@@ -258,5 +262,6 @@ export function printConfig(config) {
258
262
  console.log(` IdP: ${config.idp ? (config.idpIssuer || 'enabled') : 'disabled'}`);
259
263
  console.log(` Subdomains: ${config.subdomains ? (config.baseDomain || 'enabled') : 'disabled'}`);
260
264
  console.log(` Mashlib: ${config.mashlibCdn ? `CDN v${config.mashlibVersion}` : config.mashlib ? 'local' : 'disabled'}`);
265
+ console.log(` SolidOS UI: ${config.solidosUi ? 'enabled' : 'disabled'}`);
261
266
  console.log('─'.repeat(40));
262
267
  }
@@ -15,7 +15,7 @@ import {
15
15
  } from '../rdf/conneg.js';
16
16
  import { emitChange } from '../notifications/events.js';
17
17
  import { checkIfMatch, checkIfNoneMatchForGet, checkIfNoneMatchForWrite } from '../utils/conditional.js';
18
- import { generateDatabrowserHtml, shouldServeMashlib } from '../mashlib/index.js';
18
+ import { generateDatabrowserHtml, generateSolidosUiHtml, shouldServeMashlib } from '../mashlib/index.js';
19
19
 
20
20
  /**
21
21
  * Get the storage path and resource URL for a request
@@ -30,6 +30,64 @@ function getRequestPaths(request) {
30
30
  return { urlPath, storagePath, resourceUrl };
31
31
  }
32
32
 
33
+ /**
34
+ * Parse HTTP Range header
35
+ * @param {string} rangeHeader - The Range header value (e.g., "bytes=0-1023")
36
+ * @param {number} fileSize - Total file size in bytes
37
+ * @returns {{ start: number, end: number } | null}
38
+ */
39
+ function parseRangeHeader(rangeHeader, fileSize) {
40
+ if (!rangeHeader || !rangeHeader.startsWith('bytes=')) {
41
+ return null;
42
+ }
43
+
44
+ const range = rangeHeader.slice(6); // Remove 'bytes='
45
+
46
+ // Multi-range requests (e.g., "0-100,200-300") are not supported
47
+ // Per RFC 7233, ignore Range header and serve full content instead of 416
48
+ if (range.includes(',')) {
49
+ return null;
50
+ }
51
+
52
+ const parts = range.split('-');
53
+
54
+ if (parts.length !== 2) {
55
+ return null;
56
+ }
57
+
58
+ let start, end;
59
+
60
+ if (parts[0] === '') {
61
+ // Suffix range: bytes=-500 (last 500 bytes)
62
+ const suffix = parseInt(parts[1], 10);
63
+ if (isNaN(suffix) || suffix <= 0) return null;
64
+ start = Math.max(0, fileSize - suffix);
65
+ end = fileSize - 1;
66
+ } else if (parts[1] === '') {
67
+ // Open-ended range: bytes=1024- (from 1024 to end)
68
+ start = parseInt(parts[0], 10);
69
+ if (isNaN(start) || start < 0) return null;
70
+ end = fileSize - 1;
71
+ } else {
72
+ // Normal range: bytes=0-1023
73
+ start = parseInt(parts[0], 10);
74
+ end = parseInt(parts[1], 10);
75
+ if (isNaN(start) || isNaN(end) || start < 0 || end < start) return null;
76
+ }
77
+
78
+ // Clamp end to file size
79
+ if (end >= fileSize) {
80
+ end = fileSize - 1;
81
+ }
82
+
83
+ // Check if range is satisfiable
84
+ if (start > end || start >= fileSize) {
85
+ return null;
86
+ }
87
+
88
+ return { start, end };
89
+ }
90
+
33
91
  /**
34
92
  * Handle GET request
35
93
  */
@@ -149,8 +207,10 @@ export async function handleGet(request, reply) {
149
207
 
150
208
  // Check if we should serve Mashlib data browser for containers
151
209
  if (shouldServeMashlib(request, request.mashlibEnabled, 'application/ld+json')) {
152
- const cdnVersion = request.mashlibCdn ? request.mashlibVersion : null;
153
- const html = generateDatabrowserHtml(resourceUrl, cdnVersion);
210
+ // Use SolidOS UI if enabled, otherwise fallback to classic mashlib
211
+ const html = request.solidosUiEnabled
212
+ ? generateSolidosUiHtml()
213
+ : generateDatabrowserHtml(resourceUrl, request.mashlibCdn ? request.mashlibVersion : null);
154
214
  const headers = getAllHeaders({
155
215
  isContainer: true,
156
216
  etag: stats.etag,
@@ -224,9 +284,10 @@ export async function handleGet(request, reply) {
224
284
  // Check if we should serve Mashlib data browser
225
285
  // Only for RDF resources when Accept: text/html is requested
226
286
  if (shouldServeMashlib(request, request.mashlibEnabled, storedContentType)) {
227
- // Pass CDN version if using CDN mode, null for local mode
228
- const cdnVersion = request.mashlibCdn ? request.mashlibVersion : null;
229
- const html = generateDatabrowserHtml(resourceUrl, cdnVersion);
287
+ // Use SolidOS UI if enabled, otherwise fallback to classic mashlib
288
+ const html = request.solidosUiEnabled
289
+ ? generateSolidosUiHtml()
290
+ : generateDatabrowserHtml(resourceUrl, request.mashlibCdn ? request.mashlibVersion : null);
230
291
  const headers = getAllHeaders({
231
292
  isContainer: false,
232
293
  etag: stats.etag,
@@ -245,6 +306,43 @@ export async function handleGet(request, reply) {
245
306
  return reply.type('text/html').send(html);
246
307
  }
247
308
 
309
+ // Handle Range requests for media files (video, audio, etc.)
310
+ const rangeHeader = request.headers.range;
311
+ if (rangeHeader && !isRdfContentType(storedContentType)) {
312
+ const range = parseRangeHeader(rangeHeader, stats.size);
313
+
314
+ if (range) {
315
+ const { start, end } = range;
316
+ const chunkSize = end - start + 1;
317
+
318
+ const headers = getAllHeaders({
319
+ isContainer: false,
320
+ etag: stats.etag,
321
+ contentType: storedContentType,
322
+ origin,
323
+ resourceUrl,
324
+ connegEnabled
325
+ });
326
+ headers['Content-Range'] = `bytes ${start}-${end}/${stats.size}`;
327
+ headers['Content-Length'] = chunkSize;
328
+
329
+ Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
330
+
331
+ const streamResult = storage.createReadStream(storagePath, { start, end });
332
+ if (!streamResult) {
333
+ return reply.code(500).send({ error: 'Stream error' });
334
+ }
335
+
336
+ // Handle stream errors that occur during response
337
+ streamResult.stream.on('error', (err) => {
338
+ console.error('Stream error during range response:', err.message);
339
+ });
340
+
341
+ return reply.code(206).send(streamResult.stream);
342
+ }
343
+ // If range is null (unsupported format or multi-range), fall through to serve full content
344
+ }
345
+
248
346
  const content = await storage.read(storagePath);
249
347
  if (content === null) {
250
348
  return reply.code(500).send({ error: 'Read error' });
@@ -38,6 +38,10 @@ function getWebIdIndexPath() {
38
38
  return path.join(getAccountsDir(), '_webid_index.json');
39
39
  }
40
40
 
41
+ function getCredentialIndexPath() {
42
+ return path.join(getAccountsDir(), '_credential_index.json');
43
+ }
44
+
41
45
  const SALT_ROUNDS = 10;
42
46
 
43
47
  /**
@@ -270,6 +274,135 @@ export async function deleteAccount(id) {
270
274
  await fs.remove(accountPath);
271
275
  }
272
276
 
277
+ /**
278
+ * Save an account (internal helper)
279
+ * @param {object} account - Account object
280
+ */
281
+ async function saveAccount(account) {
282
+ const accountPath = path.join(getAccountsDir(), `${account.id}.json`);
283
+ await fs.writeJson(accountPath, account, { spaces: 2 });
284
+ }
285
+
286
+ /**
287
+ * Update last login timestamp
288
+ * @param {string} id - Account ID
289
+ */
290
+ export async function updateLastLogin(id) {
291
+ const account = await findById(id);
292
+ if (!account) return;
293
+ account.lastLogin = new Date().toISOString();
294
+ await saveAccount(account);
295
+ }
296
+
297
+ /**
298
+ * Add a passkey credential to an account
299
+ * @param {string} accountId - Account ID
300
+ * @param {object} credential - Passkey credential
301
+ * @param {string} credential.credentialId - Base64url encoded credential ID
302
+ * @param {string} credential.publicKey - Base64url encoded public key
303
+ * @param {number} credential.counter - Authenticator counter
304
+ * @param {string[]} [credential.transports] - Supported transports
305
+ * @param {string} [credential.name] - User-friendly name
306
+ * @returns {Promise<boolean>} - Success
307
+ */
308
+ export async function addPasskey(accountId, credential) {
309
+ const account = await findById(accountId);
310
+ if (!account) return false;
311
+
312
+ account.passkeys = account.passkeys || [];
313
+
314
+ // Check for duplicate credentialId
315
+ const existingPasskey = account.passkeys.find(pk => pk.credentialId === credential.credentialId);
316
+ if (existingPasskey) {
317
+ return false; // Already registered
318
+ }
319
+
320
+ account.passkeys.push({
321
+ credentialId: credential.credentialId,
322
+ publicKey: credential.publicKey,
323
+ counter: credential.counter || 0,
324
+ transports: credential.transports || [],
325
+ createdAt: new Date().toISOString(),
326
+ lastUsed: null,
327
+ name: credential.name || 'Security Key'
328
+ });
329
+
330
+ await saveAccount(account);
331
+
332
+ // Update credential index
333
+ const credentialIndex = await loadIndex(getCredentialIndexPath());
334
+ credentialIndex[credential.credentialId] = accountId;
335
+ await saveIndex(getCredentialIndexPath(), credentialIndex);
336
+
337
+ return true;
338
+ }
339
+
340
+ /**
341
+ * Find an account by passkey credential ID
342
+ * @param {string} credentialId - Base64url encoded credential ID
343
+ * @returns {Promise<object|null>} - Account or null
344
+ */
345
+ export async function findByCredentialId(credentialId) {
346
+ const credentialIndex = await loadIndex(getCredentialIndexPath());
347
+ const id = credentialIndex[credentialId];
348
+ if (!id) return null;
349
+ return findById(id);
350
+ }
351
+
352
+ /**
353
+ * Update passkey counter after successful authentication
354
+ * @param {string} accountId - Account ID
355
+ * @param {string} credentialId - Credential ID
356
+ * @param {number} newCounter - New counter value
357
+ */
358
+ export async function updatePasskeyCounter(accountId, credentialId, newCounter) {
359
+ const account = await findById(accountId);
360
+ if (!account || !account.passkeys) return;
361
+
362
+ const passkey = account.passkeys.find(p => p.credentialId === credentialId);
363
+ if (passkey) {
364
+ passkey.counter = newCounter;
365
+ passkey.lastUsed = new Date().toISOString();
366
+ await saveAccount(account);
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Remove a passkey from an account
372
+ * @param {string} accountId - Account ID
373
+ * @param {string} credentialId - Credential ID to remove
374
+ * @returns {Promise<boolean>} - Success
375
+ */
376
+ export async function removePasskey(accountId, credentialId) {
377
+ const account = await findById(accountId);
378
+ if (!account || !account.passkeys) return false;
379
+
380
+ const index = account.passkeys.findIndex(p => p.credentialId === credentialId);
381
+ if (index === -1) return false;
382
+
383
+ account.passkeys.splice(index, 1);
384
+ await saveAccount(account);
385
+
386
+ // Update credential index
387
+ const credentialIndex = await loadIndex(getCredentialIndexPath());
388
+ delete credentialIndex[credentialId];
389
+ await saveIndex(getCredentialIndexPath(), credentialIndex);
390
+
391
+ return true;
392
+ }
393
+
394
+ /**
395
+ * Set passkey prompt dismissed flag
396
+ * @param {string} accountId - Account ID
397
+ * @param {boolean} dismissed - Whether prompt was dismissed
398
+ */
399
+ export async function setPasskeyPromptDismissed(accountId, dismissed = true) {
400
+ const account = await findById(accountId);
401
+ if (!account) return;
402
+ account.passkeyPromptDismissed = dismissed;
403
+ await saveAccount(account);
404
+ }
405
+
273
406
  /**
274
407
  * Get account for oidc-provider's findAccount
275
408
  * This is the interface oidc-provider expects
package/src/idp/index.js CHANGED
@@ -13,11 +13,14 @@ import {
13
13
  handleAbort,
14
14
  handleRegisterGet,
15
15
  handleRegisterPost,
16
+ handlePasskeyComplete,
17
+ handlePasskeySkip,
16
18
  } from './interactions.js';
17
19
  import {
18
20
  handleCredentials,
19
21
  handleCredentialsInfo,
20
22
  } from './credentials.js';
23
+ import * as passkey from './passkey.js';
21
24
  import { addTrustedIssuer } from '../auth/solid-oidc.js';
22
25
 
23
26
  /**
@@ -290,6 +293,68 @@ export async function idpPlugin(fastify, options) {
290
293
  return handleRegisterPost(request, reply, issuer, inviteOnly);
291
294
  });
292
295
 
296
+ // Passkey routes
297
+ // Registration options - rate limited to prevent DoS
298
+ fastify.post('/idp/passkey/register/options', {
299
+ config: {
300
+ rateLimit: {
301
+ max: 10,
302
+ timeWindow: '1 minute',
303
+ keyGenerator: (request) => request.ip
304
+ }
305
+ }
306
+ }, async (request, reply) => {
307
+ return passkey.registrationOptions(request, reply);
308
+ });
309
+
310
+ // Registration verify - rate limited
311
+ fastify.post('/idp/passkey/register/verify', {
312
+ config: {
313
+ rateLimit: {
314
+ max: 10,
315
+ timeWindow: '1 minute',
316
+ keyGenerator: (request) => request.ip
317
+ }
318
+ }
319
+ }, async (request, reply) => {
320
+ return passkey.registrationVerify(request, reply);
321
+ });
322
+
323
+ // Login options - rate limited to prevent DoS
324
+ fastify.post('/idp/passkey/login/options', {
325
+ config: {
326
+ rateLimit: {
327
+ max: 10,
328
+ timeWindow: '1 minute',
329
+ keyGenerator: (request) => request.ip
330
+ }
331
+ }
332
+ }, async (request, reply) => {
333
+ return passkey.authenticationOptions(request, reply);
334
+ });
335
+
336
+ // Login verify - rate limited
337
+ fastify.post('/idp/passkey/login/verify', {
338
+ config: {
339
+ rateLimit: {
340
+ max: 10,
341
+ timeWindow: '1 minute',
342
+ keyGenerator: (request) => request.ip
343
+ }
344
+ }
345
+ }, async (request, reply) => {
346
+ return passkey.authenticationVerify(request, reply);
347
+ });
348
+
349
+ // Passkey interaction handlers
350
+ fastify.get('/idp/interaction/:uid/passkey-complete', async (request, reply) => {
351
+ return handlePasskeyComplete(request, reply, provider);
352
+ });
353
+
354
+ fastify.get('/idp/interaction/:uid/passkey-skip', async (request, reply) => {
355
+ return handlePasskeySkip(request, reply, provider);
356
+ });
357
+
293
358
  fastify.log.info(`IdP initialized with issuer: ${issuer}`);
294
359
  }
295
360