@le-space/p2pass 0.1.0

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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -0
  3. package/dist/backup/registry-backup.d.ts +26 -0
  4. package/dist/backup/registry-backup.js +51 -0
  5. package/dist/identity/identity-service.d.ts +116 -0
  6. package/dist/identity/identity-service.js +524 -0
  7. package/dist/identity/mode-detector.d.ts +29 -0
  8. package/dist/identity/mode-detector.js +124 -0
  9. package/dist/identity/signing-preference.d.ts +30 -0
  10. package/dist/identity/signing-preference.js +55 -0
  11. package/dist/index.d.ts +15 -0
  12. package/dist/index.js +91 -0
  13. package/dist/p2p/setup.d.ts +48 -0
  14. package/dist/p2p/setup.js +283 -0
  15. package/dist/recovery/ipns-key.d.ts +41 -0
  16. package/dist/recovery/ipns-key.js +127 -0
  17. package/dist/recovery/manifest.d.ts +106 -0
  18. package/dist/recovery/manifest.js +243 -0
  19. package/dist/registry/device-registry.d.ts +122 -0
  20. package/dist/registry/device-registry.js +275 -0
  21. package/dist/registry/index.d.ts +3 -0
  22. package/dist/registry/index.js +46 -0
  23. package/dist/registry/manager.d.ts +76 -0
  24. package/dist/registry/manager.js +376 -0
  25. package/dist/registry/pairing-protocol.d.ts +61 -0
  26. package/dist/registry/pairing-protocol.js +653 -0
  27. package/dist/ucan/storacha-auth.d.ts +45 -0
  28. package/dist/ucan/storacha-auth.js +164 -0
  29. package/dist/ui/StorachaFab.svelte +134 -0
  30. package/dist/ui/StorachaFab.svelte.d.ts +23 -0
  31. package/dist/ui/StorachaIntegration.svelte +2467 -0
  32. package/dist/ui/StorachaIntegration.svelte.d.ts +23 -0
  33. package/dist/ui/fonts/dm-mono-400.ttf +0 -0
  34. package/dist/ui/fonts/dm-mono-500.ttf +0 -0
  35. package/dist/ui/fonts/dm-sans-400.ttf +0 -0
  36. package/dist/ui/fonts/dm-sans-500.ttf +0 -0
  37. package/dist/ui/fonts/dm-sans-600.ttf +0 -0
  38. package/dist/ui/fonts/dm-sans-700.ttf +0 -0
  39. package/dist/ui/fonts/epilogue-400.ttf +0 -0
  40. package/dist/ui/fonts/epilogue-500.ttf +0 -0
  41. package/dist/ui/fonts/epilogue-600.ttf +0 -0
  42. package/dist/ui/fonts/epilogue-700.ttf +0 -0
  43. package/dist/ui/fonts/storacha-fonts.css +152 -0
  44. package/dist/ui/storacha-backup.d.ts +44 -0
  45. package/dist/ui/storacha-backup.js +218 -0
  46. package/package.json +112 -0
@@ -0,0 +1,524 @@
1
+ /**
2
+ * Identity service — orchestrates WebAuthn passkey auth with auto mode detection.
3
+ * Combines hardware signer and worker Ed25519 flows.
4
+ *
5
+ * Credential data is stored in an OrbitDB registry DB when available.
6
+ * Falls back to in-memory storage until setRegistry() is called.
7
+ */
8
+
9
+ import {
10
+ WebAuthnHardwareSignerService,
11
+ loadWebAuthnCredentialSafe,
12
+ getStoredWebAuthnHardwareSignerInfo,
13
+ extractPrfSeedFromCredential,
14
+ initEd25519KeystoreWithPrfSeed,
15
+ generateWorkerEd25519DID,
16
+ loadWorkerEd25519Archive,
17
+ encryptArchive,
18
+ decryptArchive,
19
+ } from '@le-space/orbitdb-identity-provider-webauthn-did/standalone';
20
+
21
+ import {
22
+ storeKeypairEntry,
23
+ getKeypairEntry,
24
+ storeArchiveEntry,
25
+ getArchiveEntry,
26
+ listKeypairs,
27
+ } from '../registry/device-registry.js';
28
+
29
+ import {
30
+ computeDeterministicPrfSalt,
31
+ deriveIPNSKeyPair,
32
+ recoverPrfSeed,
33
+ } from '../recovery/ipns-key.js';
34
+
35
+ import { resolveSigningPreference } from './signing-preference.js';
36
+
37
+ const ARCHIVE_CACHE_KEY = 'p2p_passkeys_worker_archive';
38
+
39
+ /**
40
+ * Best-effort: this origin likely already has passkey / identity material (no WebAuthn prompt).
41
+ * Uses the same signals as restore paths — false negatives are OK (same handlers still apply).
42
+ *
43
+ * @returns {boolean}
44
+ */
45
+ export function hasLocalPasskeyHint() {
46
+ if (typeof globalThis.localStorage === 'undefined') return false;
47
+ try {
48
+ const hw = getStoredWebAuthnHardwareSignerInfo();
49
+ if (hw?.did) return true;
50
+ } catch {
51
+ /* ignore */
52
+ }
53
+ try {
54
+ if (loadWebAuthnCredentialSafe()) return true;
55
+ } catch {
56
+ /* ignore */
57
+ }
58
+ try {
59
+ const raw = localStorage.getItem(ARCHIVE_CACHE_KEY);
60
+ if (!raw) return false;
61
+ const parsed = JSON.parse(raw);
62
+ if (parsed && (parsed.did || parsed.ciphertext)) return true;
63
+ } catch {
64
+ /* ignore */
65
+ }
66
+ return false;
67
+ }
68
+
69
+ export class IdentityService {
70
+ #mode = null;
71
+ #did = null;
72
+ #algorithm = null;
73
+ #signer = null;
74
+ #hardwareService = null;
75
+ #archive = null;
76
+ #registryDb = null;
77
+
78
+ // Hold credentials in memory until registry DB is available
79
+ #pendingCredentials = null;
80
+ #prfSeed = null;
81
+ #ipnsKeyPair = null;
82
+
83
+ constructor() {
84
+ this.#hardwareService = new WebAuthnHardwareSignerService();
85
+ }
86
+
87
+ /**
88
+ * Bind an OrbitDB registry database for credential storage.
89
+ * If pending credentials exist from a prior initialize() call, flushes them.
90
+ *
91
+ * @param {Object} db - OrbitDB KeyValue database (from openDeviceRegistry)
92
+ */
93
+ async setRegistry(db) {
94
+ this.#registryDb = db;
95
+ console.log('[identity] Registry DB bound');
96
+
97
+ // Flush pending credentials to registry
98
+ if (this.#pendingCredentials) {
99
+ const { publicKeyHex, did, ciphertext, iv } = this.#pendingCredentials;
100
+ await storeKeypairEntry(db, did, publicKeyHex);
101
+ await storeArchiveEntry(db, did, ciphertext, iv);
102
+ console.log('[identity] Flushed pending credentials to registry DB');
103
+ this.#pendingCredentials = null;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Get the bound registry DB (or null).
109
+ * @returns {Object|null}
110
+ */
111
+ getRegistry() {
112
+ return this.#registryDb;
113
+ }
114
+
115
+ /**
116
+ * Initialize identity — auto-detect hardware vs worker mode.
117
+ * If existing credentials found, restores them (may prompt biometric for worker PRF).
118
+ * If no credentials, creates new passkey.
119
+ *
120
+ * @param {'platform'|'cross-platform'} [authenticatorType]
121
+ * @param {{ preferWorkerMode?: boolean, signingPreference?: import('./signing-preference.js').SigningPreference }} [options]
122
+ * @returns {Promise<{ mode: string, did: string, algorithm: string }>}
123
+ */
124
+ async initialize(authenticatorType, options = {}) {
125
+ const { preferWorkerMode = false, signingPreference = null } = options;
126
+ const pref = resolveSigningPreference({ preferWorkerMode, signingPreference });
127
+ const preferWorker = pref === 'worker';
128
+ const forceP256Hardware = pref === 'hardware-p256';
129
+
130
+ console.log(
131
+ '[identity] Initializing...',
132
+ preferWorker ? '(worker)' : `(hardware, forceP256=${forceP256Hardware})`
133
+ );
134
+
135
+ // Try hardware mode first (unless worker mode is selected)
136
+ if (!preferWorker) {
137
+ try {
138
+ const signer = await this.#hardwareService.initialize({
139
+ authenticatorType,
140
+ forceP256: forceP256Hardware,
141
+ });
142
+
143
+ if (signer) {
144
+ this.#mode = 'hardware';
145
+ this.#did = this.#hardwareService.getDID();
146
+ this.#algorithm = this.#hardwareService.getAlgorithm() || 'Ed25519';
147
+ this.#signer = signer;
148
+ console.log(`[identity] Hardware mode (${this.#algorithm}), DID: ${this.#did}`);
149
+ return this.getSigningMode();
150
+ }
151
+ } catch (err) {
152
+ console.warn('[identity] Hardware mode failed, trying worker...', err.message);
153
+ }
154
+ }
155
+
156
+ // Worker mode — try to restore existing identity
157
+ const restored = await this.#tryRestoreWorkerIdentity();
158
+ if (restored) {
159
+ console.log(`[identity] Restored worker identity, DID: ${this.#did}`);
160
+ return this.getSigningMode();
161
+ }
162
+
163
+ // No existing identity — create new worker identity
164
+ await this.#createWorkerIdentity(authenticatorType);
165
+ console.log(`[identity] Created new worker identity, DID: ${this.#did}`);
166
+ return this.getSigningMode();
167
+ }
168
+
169
+ /**
170
+ * Force create a new identity (discards existing).
171
+ * @param {'platform'|'cross-platform'} [authenticatorType]
172
+ * @param {{ preferWorkerMode?: boolean, signingPreference?: import('./signing-preference.js').SigningPreference }} [options]
173
+ * @returns {Promise<{ mode: string, did: string, algorithm: string }>}
174
+ */
175
+ async createNewIdentity(authenticatorType, options = {}) {
176
+ this.#hardwareService.clear();
177
+ this.#mode = null;
178
+ this.#did = null;
179
+ this.#signer = null;
180
+ this.#archive = null;
181
+ this.#pendingCredentials = null;
182
+
183
+ return this.initialize(authenticatorType, options);
184
+ }
185
+
186
+ /**
187
+ * Recovery entry point — uses discoverable credentials to derive IPNS key.
188
+ * Does NOT restore the DID — caller must resolve manifest and restore registry first.
189
+ * @returns {Promise<{ prfSeed: Uint8Array, ipnsKeyPair: Object, rawCredentialId: Uint8Array }>}
190
+ */
191
+ async initializeFromRecovery() {
192
+ console.log('[identity] Starting recovery via discoverable credential...');
193
+ const { prfSeed, rawCredentialId, credential } = await recoverPrfSeed();
194
+
195
+ this.#prfSeed = prfSeed;
196
+ this.#ipnsKeyPair = await deriveIPNSKeyPair(prfSeed);
197
+
198
+ console.log('[identity] Recovery: IPNS keypair derived');
199
+ return { prfSeed, ipnsKeyPair: this.#ipnsKeyPair, rawCredentialId };
200
+ }
201
+
202
+ /**
203
+ * Restore DID from an encrypted archive entry (from registry DB after recovery).
204
+ * Uses the PRF seed stored during initializeFromRecovery().
205
+ * @param {Object} archiveEntry - { ciphertext: string (hex), iv: string (hex) }
206
+ * @param {string} did - The owner DID from the manifest
207
+ * @returns {Promise<void>}
208
+ */
209
+ async restoreFromManifest(archiveEntry, did) {
210
+ if (!this.#prfSeed) {
211
+ throw new Error('No PRF seed available. Call initializeFromRecovery() first.');
212
+ }
213
+
214
+ console.log('[identity] Restoring DID from manifest + registry archive...');
215
+
216
+ // Init worker keystore with PRF
217
+ await initEd25519KeystoreWithPrfSeed(this.#prfSeed);
218
+
219
+ // Decrypt archive
220
+ const ciphertext = hexToBytes(archiveEntry.ciphertext);
221
+ const iv = hexToBytes(archiveEntry.iv);
222
+ const archive = await decryptArchive(ciphertext, iv);
223
+
224
+ // Load archive into worker
225
+ await loadWorkerEd25519Archive(archive);
226
+
227
+ this.#mode = 'worker';
228
+ this.#did = did;
229
+ this.#algorithm = 'Ed25519';
230
+ this.#archive = archive;
231
+
232
+ console.log(`[identity] DID restored from manifest: ${did}`);
233
+ }
234
+
235
+ /**
236
+ * Get current signing mode info.
237
+ * @returns {{ mode: string|null, did: string|null, algorithm: string|null, secure: boolean }}
238
+ */
239
+ getSigningMode() {
240
+ return {
241
+ mode: this.#mode,
242
+ did: this.#did,
243
+ algorithm: this.#algorithm,
244
+ secure: this.#mode === 'hardware',
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Get a UCAN-compatible principal/signer.
250
+ * For hardware mode: returns varsig signer via toUcantoSigner()
251
+ * For worker mode: returns Ed25519 principal from archive
252
+ *
253
+ * @returns {Promise<any>} UCAN signer
254
+ */
255
+ async getPrincipal() {
256
+ if (this.#mode === 'hardware' && this.#signer) {
257
+ return this.#signer.toUcantoSigner();
258
+ }
259
+
260
+ if (this.#mode === 'worker' && this.#archive) {
261
+ const { from } = await import('@ucanto/principal/ed25519');
262
+ return from(this.#archive);
263
+ }
264
+
265
+ throw new Error('No identity initialized. Call initialize() first.');
266
+ }
267
+
268
+ /**
269
+ * @returns {boolean}
270
+ */
271
+ isInitialized() {
272
+ return this.#mode !== null && this.#did !== null;
273
+ }
274
+
275
+ /**
276
+ * Get the derived IPNS keypair (available after initialize or recovery).
277
+ * @returns {{ privateKey: Object, publicKey: Object }|null}
278
+ */
279
+ getIPNSKeyPair() {
280
+ return this.#ipnsKeyPair;
281
+ }
282
+
283
+ /**
284
+ * Get the stored PRF seed (available after initialize or recovery).
285
+ * @returns {Uint8Array|null}
286
+ */
287
+ getPrfSeed() {
288
+ return this.#prfSeed;
289
+ }
290
+
291
+ /**
292
+ * Get the encrypted archive data (ciphertext + iv as hex strings) for the current identity.
293
+ * Used by manifest publishing to upload the archive to IPFS for auth-free recovery.
294
+ *
295
+ * @returns {Promise<{ ciphertext: string, iv: string }|null>}
296
+ */
297
+ async getEncryptedArchiveData() {
298
+ if (!this.#did) return null;
299
+
300
+ // From pending credentials (not yet flushed to registry)
301
+ if (this.#pendingCredentials) {
302
+ return { ciphertext: this.#pendingCredentials.ciphertext, iv: this.#pendingCredentials.iv };
303
+ }
304
+
305
+ // From registry DB
306
+ if (this.#registryDb) {
307
+ const entry = await getArchiveEntry(this.#registryDb, this.#did);
308
+ if (entry) return { ciphertext: entry.ciphertext, iv: entry.iv };
309
+ }
310
+
311
+ // From localStorage cache
312
+ const cached = this.#loadCachedArchive();
313
+ if (cached && cached.did === this.#did) {
314
+ return { ciphertext: cached.ciphertext, iv: cached.iv };
315
+ }
316
+
317
+ return null;
318
+ }
319
+
320
+ /**
321
+ * Try to restore a worker identity.
322
+ * Checks registry DB first, falls back to in-memory pending credentials.
323
+ * Requires WebAuthn re-auth to get PRF seed for archive decryption.
324
+ */
325
+ async #tryRestoreWorkerIdentity() {
326
+ try {
327
+ // Try registry DB first
328
+ if (this.#registryDb) {
329
+ const keypairs = await listKeypairs(this.#registryDb);
330
+ if (keypairs.length > 0) {
331
+ const keypair = keypairs[0]; // use first keypair
332
+ const archiveEntry = await getArchiveEntry(this.#registryDb, keypair.did);
333
+
334
+ if (archiveEntry) {
335
+ return await this.#restoreFromEncryptedArchive(keypair, archiveEntry);
336
+ }
337
+ }
338
+ }
339
+
340
+ // Fallback: try localStorage cache (bootstrap before registry is available)
341
+ const cached = this.#loadCachedArchive();
342
+ if (cached) {
343
+ const credential = loadWebAuthnCredentialSafe();
344
+ if (credential) {
345
+ console.log('[identity] Found cached archive, attempting restore via biometric...');
346
+ return await this.#restoreFromEncryptedArchive(
347
+ { did: cached.did, publicKey: cached.publicKeyHex },
348
+ { ciphertext: cached.ciphertext, iv: cached.iv }
349
+ );
350
+ }
351
+ }
352
+
353
+ return false;
354
+ } catch (err) {
355
+ console.warn('[identity] Failed to restore worker identity:', err.message);
356
+ return false;
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Restore worker identity from encrypted archive data.
362
+ * @param {Object} keypair - { did, publicKey }
363
+ * @param {Object} archiveEntry - { ciphertext, iv }
364
+ * @returns {Promise<boolean>}
365
+ */
366
+ async #restoreFromEncryptedArchive(keypair, archiveEntry) {
367
+ // Need PRF seed to decrypt — requires WebAuthn re-auth
368
+ const credential = loadWebAuthnCredentialSafe();
369
+ if (!credential) {
370
+ console.warn('[identity] Stored keypair but no WebAuthn credential for PRF');
371
+ return false;
372
+ }
373
+
374
+ console.log('[identity] Restoring worker identity (biometric required)...');
375
+ const { seed: prfSeed } = await extractPrfSeedFromCredential(credential);
376
+
377
+ // Store PRF seed and derive IPNS keypair
378
+ this.#prfSeed = prfSeed;
379
+ try {
380
+ this.#ipnsKeyPair = await deriveIPNSKeyPair(prfSeed);
381
+ console.log('[identity] IPNS keypair derived (restore)');
382
+ } catch (err) {
383
+ console.warn('[identity] Failed to derive IPNS keypair:', err.message);
384
+ }
385
+
386
+ // Init worker keystore with PRF
387
+ await initEd25519KeystoreWithPrfSeed(prfSeed);
388
+
389
+ // Decrypt archive
390
+ const ciphertext = hexToBytes(archiveEntry.ciphertext);
391
+ const iv = hexToBytes(archiveEntry.iv);
392
+ const archive = await decryptArchive(ciphertext, iv);
393
+
394
+ // Load archive into worker
395
+ await loadWorkerEd25519Archive(archive);
396
+
397
+ this.#mode = 'worker';
398
+ this.#did = keypair.did;
399
+ this.#algorithm = 'Ed25519';
400
+ this.#archive = archive;
401
+
402
+ return true;
403
+ }
404
+
405
+ #loadCachedArchive() {
406
+ try {
407
+ const raw = localStorage.getItem(ARCHIVE_CACHE_KEY);
408
+ if (!raw) return null;
409
+ return JSON.parse(raw);
410
+ } catch {
411
+ return null;
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Create a new worker-mode Ed25519 identity.
417
+ */
418
+ async #createWorkerIdentity(authenticatorType) {
419
+ // Create WebAuthn credential with PRF
420
+ const credential = await this.#createWebAuthnCredential(authenticatorType);
421
+
422
+ // Extract PRF seed
423
+ const { seed: prfSeed } = await extractPrfSeedFromCredential(credential);
424
+
425
+ // Store PRF seed and derive IPNS keypair
426
+ this.#prfSeed = prfSeed;
427
+ try {
428
+ this.#ipnsKeyPair = await deriveIPNSKeyPair(prfSeed);
429
+ console.log('[identity] IPNS keypair derived');
430
+ } catch (err) {
431
+ console.warn('[identity] Failed to derive IPNS keypair:', err.message);
432
+ }
433
+
434
+ // Init worker with PRF seed
435
+ await initEd25519KeystoreWithPrfSeed(prfSeed);
436
+
437
+ // Generate Ed25519 DID in worker
438
+ const { publicKey, did, archive } = await generateWorkerEd25519DID();
439
+
440
+ // Encrypt archive for storage
441
+ const { ciphertext, iv } = await encryptArchive(archive);
442
+
443
+ const publicKeyHex = bytesToHex(publicKey);
444
+ const ciphertextHex = bytesToHex(ciphertext);
445
+ const ivHex = bytesToHex(iv);
446
+
447
+ // Store in registry DB if available, otherwise hold in memory
448
+ if (this.#registryDb) {
449
+ await storeKeypairEntry(this.#registryDb, did, publicKeyHex);
450
+ await storeArchiveEntry(this.#registryDb, did, ciphertextHex, ivHex);
451
+ console.log('[identity] Credentials stored in registry DB');
452
+ } else {
453
+ this.#pendingCredentials = {
454
+ publicKeyHex,
455
+ did,
456
+ ciphertext: ciphertextHex,
457
+ iv: ivHex,
458
+ };
459
+ console.log('[identity] Credentials held in memory (registry not yet bound)');
460
+ }
461
+
462
+ this.#mode = 'worker';
463
+ this.#did = did;
464
+ this.#algorithm = 'Ed25519';
465
+ this.#archive = archive;
466
+ }
467
+
468
+ /**
469
+ * Create a WebAuthn credential with PRF extension.
470
+ */
471
+ async #createWebAuthnCredential(authenticatorType) {
472
+ const challenge = crypto.getRandomValues(new Uint8Array(32));
473
+ const prfSalt = await computeDeterministicPrfSalt();
474
+
475
+ const createOptions = {
476
+ publicKey: {
477
+ rp: {
478
+ name: 'P2P Passkeys',
479
+ id: globalThis.location?.hostname || 'localhost',
480
+ },
481
+ user: {
482
+ id: crypto.getRandomValues(new Uint8Array(16)),
483
+ name: 'p2p-user',
484
+ displayName: 'P2P User',
485
+ },
486
+ challenge,
487
+ pubKeyCredParams: [
488
+ { type: 'public-key', alg: -7 }, // ES256 (P-256)
489
+ { type: 'public-key', alg: -257 }, // RS256
490
+ ],
491
+ authenticatorSelection: {
492
+ authenticatorAttachment: authenticatorType || 'platform',
493
+ residentKey: 'required',
494
+ userVerification: 'preferred',
495
+ },
496
+ extensions: {
497
+ prf: { eval: { first: prfSalt } },
498
+ },
499
+ },
500
+ };
501
+
502
+ const credential = await navigator.credentials.create(createOptions);
503
+
504
+ // Attach metadata for storage
505
+ credential.prfInput = prfSalt;
506
+ credential.rawCredentialId = new Uint8Array(credential.rawId);
507
+
508
+ return credential;
509
+ }
510
+ }
511
+
512
+ function bytesToHex(bytes) {
513
+ return Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes))
514
+ .map((b) => b.toString(16).padStart(2, '0'))
515
+ .join('');
516
+ }
517
+
518
+ function hexToBytes(hex) {
519
+ const bytes = new Uint8Array(hex.length / 2);
520
+ for (let i = 0; i < hex.length; i += 2) {
521
+ bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
522
+ }
523
+ return bytes;
524
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Detect the best available signing mode.
3
+ * Priority: hardware Ed25519 > hardware P-256 > stored worker keypair > null (needs setup)
4
+ *
5
+ * @param {{ authenticatorType?: 'platform'|'cross-platform', forceWorker?: boolean, registryDb?: Object }} options
6
+ * @returns {Promise<{ mode: 'hardware'|'worker'|null, signer?: any, did?: string, algorithm?: string }>}
7
+ */
8
+ export function detectSigningMode(options?: {
9
+ authenticatorType?: "platform" | "cross-platform";
10
+ forceWorker?: boolean;
11
+ registryDb?: Object;
12
+ }): Promise<{
13
+ mode: "hardware" | "worker" | null;
14
+ signer?: any;
15
+ did?: string;
16
+ algorithm?: string;
17
+ }>;
18
+ /**
19
+ * Get stored signing mode info without requiring biometric auth.
20
+ * Checks registry DB first if provided, then falls back to hardware signer storage.
21
+ *
22
+ * @param {Object} [registryDb] - OrbitDB registry database
23
+ * @returns {Promise<{ mode: 'hardware'|'worker'|null, did?: string, algorithm?: string }>}
24
+ */
25
+ export function getStoredSigningMode(registryDb?: Object): Promise<{
26
+ mode: "hardware" | "worker" | null;
27
+ did?: string;
28
+ algorithm?: string;
29
+ }>;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Signing mode detection — auto-detects hardware vs worker WebAuthn mode.
3
+ * Ported from ucan-upload-wall's ucan-delegation.ts mode detection logic.
4
+ */
5
+
6
+ import {
7
+ WebAuthnHardwareSignerService,
8
+ checkEd25519Support,
9
+ getStoredWebAuthnHardwareSignerInfo,
10
+ } from '@le-space/orbitdb-identity-provider-webauthn-did/standalone';
11
+
12
+ import { listKeypairs } from '../registry/device-registry.js';
13
+
14
+ /**
15
+ * Detect the best available signing mode.
16
+ * Priority: hardware Ed25519 > hardware P-256 > stored worker keypair > null (needs setup)
17
+ *
18
+ * @param {{ authenticatorType?: 'platform'|'cross-platform', forceWorker?: boolean, registryDb?: Object }} options
19
+ * @returns {Promise<{ mode: 'hardware'|'worker'|null, signer?: any, did?: string, algorithm?: string }>}
20
+ */
21
+ export async function detectSigningMode(options = {}) {
22
+ const { forceWorker = false, registryDb } = options;
23
+
24
+ // Check for stored mode first (fast path, no biometric)
25
+ const stored = await getStoredSigningMode(registryDb);
26
+ if (stored.mode) {
27
+ console.log(`Stored signing mode found: ${stored.mode} (${stored.algorithm})`);
28
+ return stored;
29
+ }
30
+
31
+ // Try hardware mode unless forced to worker
32
+ if (!forceWorker) {
33
+ try {
34
+ const hardwareResult = await tryHardwareMode(options);
35
+ if (hardwareResult) {
36
+ console.log(`Hardware mode initialized: ${hardwareResult.algorithm}`);
37
+ return hardwareResult;
38
+ }
39
+ } catch (err) {
40
+ console.warn('Hardware mode detection failed:', err.message);
41
+ }
42
+ }
43
+
44
+ // No stored mode and hardware failed — needs setup
45
+ return { mode: null };
46
+ }
47
+
48
+ /**
49
+ * Get stored signing mode info without requiring biometric auth.
50
+ * Checks registry DB first if provided, then falls back to hardware signer storage.
51
+ *
52
+ * @param {Object} [registryDb] - OrbitDB registry database
53
+ * @returns {Promise<{ mode: 'hardware'|'worker'|null, did?: string, algorithm?: string }>}
54
+ */
55
+ export async function getStoredSigningMode(registryDb) {
56
+ // Check hardware signer storage
57
+ try {
58
+ const hardwareInfo = getStoredWebAuthnHardwareSignerInfo();
59
+ if (hardwareInfo && hardwareInfo.did) {
60
+ return {
61
+ mode: 'hardware',
62
+ did: hardwareInfo.did,
63
+ algorithm: hardwareInfo.algorithm || 'Ed25519',
64
+ };
65
+ }
66
+ } catch {
67
+ // No stored hardware signer
68
+ }
69
+
70
+ // Check registry DB for worker keypairs
71
+ if (registryDb) {
72
+ try {
73
+ const keypairs = await listKeypairs(registryDb);
74
+ if (keypairs.length > 0 && keypairs[0].did) {
75
+ return {
76
+ mode: 'worker',
77
+ did: keypairs[0].did,
78
+ algorithm: 'Ed25519',
79
+ };
80
+ }
81
+ } catch {
82
+ // Registry read failed
83
+ }
84
+ }
85
+
86
+ return { mode: null };
87
+ }
88
+
89
+ /**
90
+ * Try to initialize hardware signing mode.
91
+ * @returns {Promise<{ mode: 'hardware', signer: any, did: string, algorithm: string }|null>}
92
+ */
93
+ async function tryHardwareMode(options = {}) {
94
+ const { authenticatorType } = options;
95
+
96
+ // Check browser support
97
+ const ed25519Supported = await checkEd25519Support();
98
+ console.log(`Ed25519 hardware support: ${ed25519Supported}`);
99
+
100
+ const service = new WebAuthnHardwareSignerService();
101
+
102
+ try {
103
+ const signer = await service.initialize({
104
+ authenticatorType,
105
+ preferEd25519: true,
106
+ });
107
+
108
+ if (signer) {
109
+ const did = service.getDID();
110
+ const algorithm = service.getAlgorithm() || 'Ed25519';
111
+
112
+ return {
113
+ mode: 'hardware',
114
+ signer,
115
+ did,
116
+ algorithm,
117
+ };
118
+ }
119
+ } catch (err) {
120
+ console.warn('Hardware signer initialization failed:', err.message);
121
+ }
122
+
123
+ return null;
124
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @param {unknown} v
3
+ * @returns {v is SigningPreference}
4
+ */
5
+ export function isSigningPreference(v: unknown): v is SigningPreference;
6
+ /**
7
+ * @returns {SigningPreference|null}
8
+ */
9
+ export function readSigningPreferenceFromStorage(): SigningPreference | null;
10
+ /**
11
+ * @param {SigningPreference} pref
12
+ */
13
+ export function writeSigningPreferenceToStorage(pref: SigningPreference): void;
14
+ /**
15
+ * @param {{ signingPreference?: SigningPreference | null, preferWorkerMode?: boolean }} opts
16
+ * @returns {SigningPreference}
17
+ */
18
+ export function resolveSigningPreference(opts: {
19
+ signingPreference?: SigningPreference | null;
20
+ preferWorkerMode?: boolean;
21
+ }): SigningPreference;
22
+ /**
23
+ * User-selectable signing strategy for OrbitDB WebAuthn DID (hardware Ed25519 / hardware P-256 / worker Ed25519).
24
+ * Maps to {@link IdentityService} `initialize` options and WebAuthn `forceP256` in the upstream provider.
25
+ */
26
+ export const SIGNING_PREFERENCE_STORAGE_KEY: "p2p_passkeys_signing_preference";
27
+ /** @typedef {'hardware-ed25519' | 'hardware-p256' | 'worker'} SigningPreference */
28
+ /** @type {SigningPreference[]} */
29
+ export const SIGNING_PREFERENCE_LIST: SigningPreference[];
30
+ export type SigningPreference = "hardware-ed25519" | "hardware-p256" | "worker";