@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,55 @@
1
+ /**
2
+ * User-selectable signing strategy for OrbitDB WebAuthn DID (hardware Ed25519 / hardware P-256 / worker Ed25519).
3
+ * Maps to {@link IdentityService} `initialize` options and WebAuthn `forceP256` in the upstream provider.
4
+ */
5
+
6
+ export const SIGNING_PREFERENCE_STORAGE_KEY = 'p2p_passkeys_signing_preference';
7
+
8
+ /** @typedef {'hardware-ed25519' | 'hardware-p256' | 'worker'} SigningPreference */
9
+
10
+ /** @type {SigningPreference[]} */
11
+ export const SIGNING_PREFERENCE_LIST = ['hardware-ed25519', 'hardware-p256', 'worker'];
12
+
13
+ /**
14
+ * @param {unknown} v
15
+ * @returns {v is SigningPreference}
16
+ */
17
+ export function isSigningPreference(v) {
18
+ return v === 'hardware-ed25519' || v === 'hardware-p256' || v === 'worker';
19
+ }
20
+
21
+ /**
22
+ * @returns {SigningPreference|null}
23
+ */
24
+ export function readSigningPreferenceFromStorage() {
25
+ if (typeof globalThis.localStorage === 'undefined') return null;
26
+ try {
27
+ const v = localStorage.getItem(SIGNING_PREFERENCE_STORAGE_KEY);
28
+ return isSigningPreference(v) ? v : null;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * @param {SigningPreference} pref
36
+ */
37
+ export function writeSigningPreferenceToStorage(pref) {
38
+ if (typeof globalThis.localStorage === 'undefined') return;
39
+ try {
40
+ localStorage.setItem(SIGNING_PREFERENCE_STORAGE_KEY, pref);
41
+ } catch {
42
+ /* ignore */
43
+ }
44
+ }
45
+
46
+ /**
47
+ * @param {{ signingPreference?: SigningPreference | null, preferWorkerMode?: boolean }} opts
48
+ * @returns {SigningPreference}
49
+ */
50
+ export function resolveSigningPreference(opts) {
51
+ const { signingPreference, preferWorkerMode } = opts;
52
+ if (preferWorkerMode) return 'worker';
53
+ if (signingPreference && isSigningPreference(signingPreference)) return signingPreference;
54
+ return 'hardware-ed25519';
55
+ }
@@ -0,0 +1,15 @@
1
+ export const VERSION: "0.1.0";
2
+ export { default as StorachaIntegration } from "./ui/StorachaIntegration.svelte";
3
+ export { default as StorachaFab } from "./ui/StorachaFab.svelte";
4
+ export { MultiDeviceManager } from "./registry/manager.js";
5
+ export { IdentityService, hasLocalPasskeyHint } from "./identity/identity-service.js";
6
+ export { detectSigningMode, getStoredSigningMode } from "./identity/mode-detector.js";
7
+ export { SIGNING_PREFERENCE_STORAGE_KEY, SIGNING_PREFERENCE_LIST, isSigningPreference, readSigningPreferenceFromStorage, writeSigningPreferenceToStorage, resolveSigningPreference } from "./identity/signing-preference.js";
8
+ export { createStorachaClient, parseDelegation, storeDelegation, loadStoredDelegation, clearStoredDelegation } from "./ucan/storacha-auth.js";
9
+ export { openDeviceRegistry, registerDevice, listDevices, getDeviceByCredentialId, getDeviceByDID, grantDeviceWriteAccess, revokeDeviceAccess, hashCredentialId, coseToJwk, storeDelegationEntry, listDelegations, getDelegation, removeDelegation, storeArchiveEntry, getArchiveEntry, storeKeypairEntry, getKeypairEntry, listKeypairs } from "./registry/device-registry.js";
10
+ export { LINK_DEVICE_PROTOCOL, registerLinkDeviceHandler, unregisterLinkDeviceHandler, sendPairingRequest, detectDeviceLabel, sortPairingMultiaddrs, filterPairingDialMultiaddrs, pairingFlow, PAIRING_HINT_ADDR_CAP } from "./registry/pairing-protocol.js";
11
+ export { setupP2PStack, createLibp2pInstance, createHeliaInstance, cleanupP2PStack } from "./p2p/setup.js";
12
+ export { listSpaces, getSpaceUsage, listStorachaFiles } from "./ui/storacha-backup.js";
13
+ export { deriveIPNSKeyPair, computeDeterministicPrfSalt, recoverPrfSeed } from "./recovery/ipns-key.js";
14
+ export { createManifest, publishManifest, resolveManifest, resolveManifestByName, uploadArchiveToIPFS, fetchArchiveFromIPFS } from "./recovery/manifest.js";
15
+ export { backupRegistryDb, restoreRegistryDb } from "./backup/registry-backup.js";
package/dist/index.js ADDED
@@ -0,0 +1,91 @@
1
+ // @le-space/p2pass — public API
2
+ export const VERSION = '0.1.0';
3
+
4
+ // UI components
5
+ export { default as StorachaIntegration } from './ui/StorachaIntegration.svelte';
6
+ export { default as StorachaFab } from './ui/StorachaFab.svelte';
7
+
8
+ // Identity
9
+ export { IdentityService, hasLocalPasskeyHint } from './identity/identity-service.js';
10
+ export { detectSigningMode, getStoredSigningMode } from './identity/mode-detector.js';
11
+ export {
12
+ SIGNING_PREFERENCE_STORAGE_KEY,
13
+ SIGNING_PREFERENCE_LIST,
14
+ isSigningPreference,
15
+ readSigningPreferenceFromStorage,
16
+ writeSigningPreferenceToStorage,
17
+ resolveSigningPreference,
18
+ } from './identity/signing-preference.js';
19
+
20
+ // UCAN / Storacha auth
21
+ export {
22
+ createStorachaClient,
23
+ parseDelegation,
24
+ storeDelegation,
25
+ loadStoredDelegation,
26
+ clearStoredDelegation,
27
+ } from './ucan/storacha-auth.js';
28
+
29
+ // Registry (multi-device + credential storage)
30
+ export { MultiDeviceManager } from './registry/manager.js';
31
+ export {
32
+ openDeviceRegistry,
33
+ registerDevice,
34
+ listDevices,
35
+ getDeviceByCredentialId,
36
+ getDeviceByDID,
37
+ grantDeviceWriteAccess,
38
+ revokeDeviceAccess,
39
+ hashCredentialId,
40
+ coseToJwk,
41
+ storeDelegationEntry,
42
+ listDelegations,
43
+ getDelegation,
44
+ removeDelegation,
45
+ storeArchiveEntry,
46
+ getArchiveEntry,
47
+ storeKeypairEntry,
48
+ getKeypairEntry,
49
+ listKeypairs,
50
+ } from './registry/device-registry.js';
51
+ export {
52
+ LINK_DEVICE_PROTOCOL,
53
+ registerLinkDeviceHandler,
54
+ unregisterLinkDeviceHandler,
55
+ sendPairingRequest,
56
+ detectDeviceLabel,
57
+ sortPairingMultiaddrs,
58
+ filterPairingDialMultiaddrs,
59
+ pairingFlow,
60
+ PAIRING_HINT_ADDR_CAP,
61
+ } from './registry/pairing-protocol.js';
62
+
63
+ // P2P stack setup
64
+ export {
65
+ setupP2PStack,
66
+ createLibp2pInstance,
67
+ createHeliaInstance,
68
+ cleanupP2PStack,
69
+ } from './p2p/setup.js';
70
+
71
+ // Legacy storacha backup utilities (will be replaced)
72
+ export { listSpaces, getSpaceUsage, listStorachaFiles } from './ui/storacha-backup.js';
73
+
74
+ // Recovery (IPNS manifest)
75
+ export {
76
+ deriveIPNSKeyPair,
77
+ computeDeterministicPrfSalt,
78
+ recoverPrfSeed,
79
+ } from './recovery/ipns-key.js';
80
+
81
+ export {
82
+ createManifest,
83
+ publishManifest,
84
+ resolveManifest,
85
+ resolveManifestByName,
86
+ uploadArchiveToIPFS,
87
+ fetchArchiveFromIPFS,
88
+ } from './recovery/manifest.js';
89
+
90
+ // Backup (registry)
91
+ export { backupRegistryDb, restoreRegistryDb } from './backup/registry-backup.js';
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Create a browser-compatible libp2p instance.
3
+ *
4
+ * @param {Object} [options]
5
+ * @param {string[]} [options.bootstrapList] - Bootstrap peer multiaddrs (defaults to relay from env)
6
+ * @param {boolean} [options.enablePeerConnections=true] - Auto-dial peers from `peer:discovery`
7
+ * @returns {Promise<Object>} libp2p instance
8
+ */
9
+ export function createLibp2pInstance(options?: {
10
+ bootstrapList?: string[] | undefined;
11
+ enablePeerConnections?: boolean | undefined;
12
+ }): Promise<Object>;
13
+ /**
14
+ * Create a Helia IPFS instance with persistent Level storage.
15
+ *
16
+ * @param {Object} libp2p - libp2p instance
17
+ * @param {string} [dbPath] - path prefix for Level storage
18
+ * @returns {Promise<Object>} Helia instance
19
+ */
20
+ export function createHeliaInstance(libp2p: Object, dbPath?: string): Promise<Object>;
21
+ /**
22
+ * Complete OrbitDB setup with WebAuthn identity.
23
+ *
24
+ * @param {Object} credential - WebAuthn credential object
25
+ * @param {Object} [options]
26
+ * @param {string[]} [options.bootstrapList] - Bootstrap peers
27
+ * @param {boolean} [options.encryptKeystore] - Enable PRF encryption (default: true)
28
+ * @param {string} [options.keystoreEncryptionMethod] - 'prf' (default)
29
+ * @param {string} [options.dbPath] - Level storage path
30
+ * @returns {Promise<{ orbitdb: Object, ipfs: Object, libp2p: Object, identity: Object }>}
31
+ */
32
+ export function setupP2PStack(credential: Object, options?: {
33
+ bootstrapList?: string[] | undefined;
34
+ encryptKeystore?: boolean | undefined;
35
+ keystoreEncryptionMethod?: string | undefined;
36
+ dbPath?: string | undefined;
37
+ }): Promise<{
38
+ orbitdb: Object;
39
+ ipfs: Object;
40
+ libp2p: Object;
41
+ identity: Object;
42
+ }>;
43
+ /**
44
+ * Cleanup all P2P resources.
45
+ *
46
+ * @param {Object} stack - { orbitdb, ipfs, libp2p }
47
+ */
48
+ export function cleanupP2PStack(stack: Object): Promise<void>;
@@ -0,0 +1,283 @@
1
+ /**
2
+ * P2P stack setup — libp2p + Helia + OrbitDB for browser environments.
3
+ *
4
+ * Relay/bootstrap config ported from NiKrause/simple-todo/src/lib/libp2p-config.js
5
+ */
6
+
7
+ import { createOrbitDB, Identities, useIdentityProvider } from '@orbitdb/core';
8
+ import { createLibp2p } from 'libp2p';
9
+ import { createHelia } from 'helia';
10
+ import { circuitRelayTransport } from '@libp2p/circuit-relay-v2';
11
+ import { webSockets } from '@libp2p/websockets';
12
+ import { webRTC, webRTCDirect } from '@libp2p/webrtc';
13
+ import { noise } from '@chainsafe/libp2p-noise';
14
+ import { yamux } from '@chainsafe/libp2p-yamux';
15
+ import { identify, identifyPush } from '@libp2p/identify';
16
+ import { gossipsub } from '@chainsafe/libp2p-gossipsub';
17
+ import { pubsubPeerDiscovery } from '@libp2p/pubsub-peer-discovery';
18
+ import { bootstrap } from '@libp2p/bootstrap';
19
+ import { autoNAT } from '@libp2p/autonat';
20
+ import { dcutr } from '@libp2p/dcutr';
21
+ import { ping } from '@libp2p/ping';
22
+ import { LevelBlockstore } from 'blockstore-level';
23
+ import { LevelDatastore } from 'datastore-level';
24
+ import { OrbitDBWebAuthnIdentityProviderFunction } from '@le-space/orbitdb-identity-provider-webauthn-did';
25
+
26
+ const parseAddrList = (value) =>
27
+ (value || '')
28
+ .split(',')
29
+ .map((s) => s.trim())
30
+ .filter(Boolean);
31
+
32
+ function getEnv(key) {
33
+ try {
34
+ return import.meta.env?.[key] || '';
35
+ } catch {
36
+ return '';
37
+ }
38
+ }
39
+
40
+ function getDefaultBootstrapList() {
41
+ const seeds = getEnv('VITE_BOOTSTRAP_PEERS');
42
+ if (seeds) return parseAddrList(seeds);
43
+ console.warn('[p2p] No VITE_BOOTSTRAP_PEERS set — node will have no relay/bootstrap peers');
44
+ return [];
45
+ }
46
+
47
+ const PUBSUB_PEER_DISCOVERY_TOPIC =
48
+ (typeof import.meta !== 'undefined' && import.meta.env?.VITE_PUBSUB_TOPICS) ||
49
+ 'p2p-passkeys._peer-discovery._p2p._pubsub';
50
+
51
+ const STUN_SERVERS = [
52
+ { urls: ['stun:stun.l.google.com:19302', 'stun:global.stun.twilio.com:3478'] },
53
+ ];
54
+
55
+ /** Last `/p2p/<peerId>` segment from each multiaddr — bootstrap list is usually the relay. */
56
+ function peerIdsFromBootstrapMultiaddrs(multiaddrs) {
57
+ const ids = new Set();
58
+ for (const ma of multiaddrs) {
59
+ const parts = String(ma).split('/p2p/');
60
+ if (parts.length > 1) {
61
+ const id = parts[parts.length - 1].split('/')[0];
62
+ if (id) ids.add(id);
63
+ }
64
+ }
65
+ return ids;
66
+ }
67
+
68
+ function attachRelayConnectionLogging(libp2p, bootstrapMultiaddrs) {
69
+ const relayPeerIds = peerIdsFromBootstrapMultiaddrs(bootstrapMultiaddrs);
70
+ if (relayPeerIds.size === 0) return;
71
+
72
+ const peerStr = (evt) => evt.detail?.toString?.() ?? String(evt.detail);
73
+
74
+ libp2p.addEventListener('peer:connect', (evt) => {
75
+ const id = peerStr(evt);
76
+ if (relayPeerIds.has(id)) {
77
+ console.log('[p2p] Relay (bootstrap) peer connected:', id);
78
+ }
79
+ });
80
+ libp2p.addEventListener('peer:disconnect', (evt) => {
81
+ const id = peerStr(evt);
82
+ if (relayPeerIds.has(id)) {
83
+ console.log('[p2p] Relay (bootstrap) peer disconnected:', id);
84
+ }
85
+ });
86
+ }
87
+
88
+ const rtcConfig = { iceServers: STUN_SERVERS };
89
+
90
+ /**
91
+ * Dial peers discovered via pubsub (and other discovery). One `dial(peerId)` per event — libp2p’s
92
+ * connection manager + peer store pick addresses; looping every multiaddr is redundant and can
93
+ * amplify failures (e.g. repeated protocol negotiation on bad paths).
94
+ */
95
+ function attachPeerDiscoveryAutoDial(libp2p, { enablePeerConnections = true } = {}) {
96
+ if (!enablePeerConnections) return;
97
+
98
+ const DIAL_MS = 30_000;
99
+
100
+ libp2p.addEventListener('peer:discovery', (event) => {
101
+ const { id: remotePeerId, multiaddrs } = event.detail || {};
102
+ if (!remotePeerId) return;
103
+
104
+ const self = libp2p.peerId;
105
+ if (remotePeerId.equals?.(self) || remotePeerId.toString() === self.toString()) return;
106
+
107
+ const n = Array.isArray(multiaddrs) ? multiaddrs.length : 0;
108
+ if (n > 0) {
109
+ console.log('[p2p] peer:discovery:', remotePeerId.toString(), `(event addrs: ${n})`);
110
+ }
111
+
112
+ const existing = libp2p.getConnections(remotePeerId);
113
+ const hasDirect = existing?.some((conn) => {
114
+ const a = conn.remoteAddr?.toString() || '';
115
+ return !a.includes('/p2p-circuit');
116
+ });
117
+ if (hasDirect) return;
118
+
119
+ (async () => {
120
+ try {
121
+ const signal =
122
+ typeof AbortSignal !== 'undefined' && AbortSignal.timeout
123
+ ? AbortSignal.timeout(DIAL_MS)
124
+ : undefined;
125
+ await libp2p.dial(remotePeerId, signal ? { signal } : {});
126
+ } catch (err) {
127
+ console.warn('[p2p] peer:discovery dial failed:', remotePeerId.toString(), err?.message || err);
128
+ }
129
+ })();
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Create a browser-compatible libp2p instance.
135
+ *
136
+ * @param {Object} [options]
137
+ * @param {string[]} [options.bootstrapList] - Bootstrap peer multiaddrs (defaults to relay from env)
138
+ * @param {boolean} [options.enablePeerConnections=true] - Auto-dial peers from `peer:discovery`
139
+ * @returns {Promise<Object>} libp2p instance
140
+ */
141
+ export async function createLibp2pInstance(options = {}) {
142
+ const { bootstrapList, enablePeerConnections = true } = options;
143
+ const peers =
144
+ bootstrapList && bootstrapList.length > 0 ? bootstrapList : getDefaultBootstrapList();
145
+
146
+ const peerDiscovery = [
147
+ pubsubPeerDiscovery({
148
+ interval: 3_000,
149
+ topics: [PUBSUB_PEER_DISCOVERY_TOPIC],
150
+ listenOnly: false,
151
+ }),
152
+ ];
153
+
154
+ if (peers.length > 0) {
155
+ peerDiscovery.push(bootstrap({ list: peers }));
156
+ console.log('[p2p] Bootstrap peers:', peers);
157
+ }
158
+
159
+ const libp2p = await createLibp2p({
160
+ addresses: {
161
+ listen: ['/p2p-circuit', '/webrtc'],
162
+ },
163
+ transports: [
164
+ webSockets(),
165
+ webRTCDirect({ rtcConfiguration: rtcConfig }),
166
+ webRTC({ rtcConfiguration: rtcConfig }),
167
+ circuitRelayTransport({ reservationCompletionTimeout: 20_000 }),
168
+ ],
169
+ connectionEncrypters: [noise()],
170
+ streamMuxers: [yamux()],
171
+ connectionGater: {
172
+ denyDialMultiaddr: async () => false,
173
+ },
174
+ connectionManager: {
175
+ /** Default 25 can throw `Peer had more than maxPeerAddrsToDial` when the peer store lists many paths (relay + LAN + IPv6). */
176
+ maxPeerAddrsToDial: 128,
177
+ inboundStreamProtocolNegotiationTimeout: 10_000,
178
+ inboundUpgradeTimeout: 10_000,
179
+ outboundStreamProtocolNegotiationTimeout: 10_000,
180
+ outboundUpgradeTimeout: 10_000,
181
+ },
182
+ peerDiscovery,
183
+ services: {
184
+ identify: identify(),
185
+ identifyPush: identifyPush(),
186
+ pubsub: gossipsub({
187
+ emitSelf: true,
188
+ allowPublishToZeroTopicPeers: true,
189
+ }),
190
+ autonat: autoNAT(),
191
+ dcutr: dcutr(),
192
+ ping: ping(),
193
+ },
194
+ });
195
+
196
+ console.log('[p2p] libp2p started, peerId:', libp2p.peerId.toString());
197
+ attachRelayConnectionLogging(libp2p, peers);
198
+ attachPeerDiscoveryAutoDial(libp2p, { enablePeerConnections });
199
+
200
+ return libp2p;
201
+ }
202
+
203
+ /**
204
+ * Create a Helia IPFS instance with persistent Level storage.
205
+ *
206
+ * @param {Object} libp2p - libp2p instance
207
+ * @param {string} [dbPath] - path prefix for Level storage
208
+ * @returns {Promise<Object>} Helia instance
209
+ */
210
+ export async function createHeliaInstance(libp2p, dbPath = './p2p-passkeys') {
211
+ const ipfs = await createHelia({
212
+ libp2p,
213
+ blockstore: new LevelBlockstore(`${dbPath}/blocks`),
214
+ datastore: new LevelDatastore(`${dbPath}/data`),
215
+ });
216
+
217
+ console.log('[p2p] Helia IPFS started');
218
+ return ipfs;
219
+ }
220
+
221
+ /**
222
+ * Complete OrbitDB setup with WebAuthn identity.
223
+ *
224
+ * @param {Object} credential - WebAuthn credential object
225
+ * @param {Object} [options]
226
+ * @param {string[]} [options.bootstrapList] - Bootstrap peers
227
+ * @param {boolean} [options.encryptKeystore] - Enable PRF encryption (default: true)
228
+ * @param {string} [options.keystoreEncryptionMethod] - 'prf' (default)
229
+ * @param {string} [options.dbPath] - Level storage path
230
+ * @returns {Promise<{ orbitdb: Object, ipfs: Object, libp2p: Object, identity: Object }>}
231
+ */
232
+ export async function setupP2PStack(credential, options = {}) {
233
+ const libp2pNode = options.libp2p || (await createLibp2pInstance(options));
234
+ const ipfs = await createHeliaInstance(libp2pNode, options.dbPath);
235
+
236
+ let identity;
237
+
238
+ // Only use WebAuthn identity provider if credential has full properties
239
+ // (i.e., a live credential from navigator.credentials, not a localStorage restoration)
240
+ if (credential?.response || credential?.getClientExtensionResults) {
241
+ useIdentityProvider(OrbitDBWebAuthnIdentityProviderFunction);
242
+ const identities = await Identities({ ipfs });
243
+ identity = await identities.createIdentity({
244
+ provider: OrbitDBWebAuthnIdentityProviderFunction({
245
+ webauthnCredential: credential,
246
+ useKeystoreDID: true,
247
+ keystore: identities.keystore,
248
+ keystoreKeyType: 'Ed25519',
249
+ encryptKeystore: options.encryptKeystore !== false,
250
+ keystoreEncryptionMethod: options.keystoreEncryptionMethod || 'prf',
251
+ }),
252
+ });
253
+ console.log('[p2p] OrbitDB identity created (WebAuthn):', identity.id);
254
+ } else {
255
+ console.log('[p2p] Using default OrbitDB identity');
256
+ }
257
+
258
+ const orbitdbOpts = { ipfs };
259
+ if (identity) {
260
+ const identities = await Identities({ ipfs });
261
+ orbitdbOpts.identities = identities;
262
+ orbitdbOpts.identity = identity;
263
+ }
264
+ const orbitdb = await createOrbitDB(orbitdbOpts);
265
+ console.log('[p2p] OrbitDB started');
266
+
267
+ return { orbitdb, ipfs, libp2p: libp2pNode, identity };
268
+ }
269
+
270
+ /**
271
+ * Cleanup all P2P resources.
272
+ *
273
+ * @param {Object} stack - { orbitdb, ipfs, libp2p }
274
+ */
275
+ export async function cleanupP2PStack(stack) {
276
+ try {
277
+ if (stack.orbitdb) await stack.orbitdb.stop();
278
+ if (stack.ipfs) await stack.ipfs.stop();
279
+ console.log('[p2p] Cleanup complete');
280
+ } catch (err) {
281
+ console.warn('[p2p] Cleanup error:', err.message);
282
+ }
283
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Compute a deterministic PRF salt by hashing "p2p-passkeys:" + hostname.
3
+ *
4
+ * The salt is used in the WebAuthn PRF extension so the same passkey on
5
+ * the same origin always produces the same seed material.
6
+ *
7
+ * @returns {Promise<Uint8Array>} 32-byte SHA-256 digest
8
+ */
9
+ export function computeDeterministicPrfSalt(): Promise<Uint8Array>;
10
+ /**
11
+ * Derive a deterministic Ed25519 keypair from a PRF seed using HKDF.
12
+ *
13
+ * The derivation chain is:
14
+ * prfSeed --> HKDF(SHA-256, salt=SHA-256(prfSeed)[0:16],
15
+ * info="p2p-passkeys/ipns-key") --> 32-byte seed
16
+ * --> Ed25519 keypair via libp2p/crypto
17
+ *
18
+ * @param {Uint8Array} prfSeed - raw PRF seed bytes
19
+ * @returns {Promise<{ privateKey: object, publicKey: object }>} libp2p key objects
20
+ */
21
+ export function deriveIPNSKeyPair(prfSeed: Uint8Array): Promise<{
22
+ privateKey: object;
23
+ publicKey: object;
24
+ }>;
25
+ /**
26
+ * Recover a PRF seed by performing a discoverable-credential WebAuthn
27
+ * assertion with the PRF extension.
28
+ *
29
+ * The browser will show its native passkey picker (no `allowCredentials`),
30
+ * letting the user choose which credential to authenticate with.
31
+ *
32
+ * If the authenticator supports PRF, the raw PRF output is used as the seed.
33
+ * Otherwise, `rawId` is used as a less-secure fallback.
34
+ *
35
+ * @returns {Promise<{ prfSeed: Uint8Array, rawCredentialId: Uint8Array, credential: object }>}
36
+ */
37
+ export function recoverPrfSeed(): Promise<{
38
+ prfSeed: Uint8Array;
39
+ rawCredentialId: Uint8Array;
40
+ credential: object;
41
+ }>;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * IPNS key derivation from WebAuthn PRF seeds.
3
+ *
4
+ * Derives a deterministic Ed25519 keypair suitable for IPNS record
5
+ * publishing/resolving, using HKDF over the PRF seed extracted during
6
+ * WebAuthn authentication.
7
+ *
8
+ * @module recovery/ipns-key
9
+ */
10
+
11
+ import { generateKeyPairFromSeed } from '@libp2p/crypto/keys';
12
+
13
+ const PREFIX = '[recovery]';
14
+
15
+ /**
16
+ * Compute a deterministic PRF salt by hashing "p2p-passkeys:" + hostname.
17
+ *
18
+ * The salt is used in the WebAuthn PRF extension so the same passkey on
19
+ * the same origin always produces the same seed material.
20
+ *
21
+ * @returns {Promise<Uint8Array>} 32-byte SHA-256 digest
22
+ */
23
+ export async function computeDeterministicPrfSalt() {
24
+ let hostname;
25
+ try {
26
+ hostname = location.hostname || 'localhost';
27
+ } catch {
28
+ hostname = 'localhost';
29
+ }
30
+
31
+ const input = `p2p-passkeys:${hostname}`;
32
+ const encoded = new TextEncoder().encode(input);
33
+ const hash = await crypto.subtle.digest('SHA-256', encoded);
34
+ return new Uint8Array(hash);
35
+ }
36
+
37
+ /**
38
+ * Derive a deterministic Ed25519 keypair from a PRF seed using HKDF.
39
+ *
40
+ * The derivation chain is:
41
+ * prfSeed --> HKDF(SHA-256, salt=SHA-256(prfSeed)[0:16],
42
+ * info="p2p-passkeys/ipns-key") --> 32-byte seed
43
+ * --> Ed25519 keypair via libp2p/crypto
44
+ *
45
+ * @param {Uint8Array} prfSeed - raw PRF seed bytes
46
+ * @returns {Promise<{ privateKey: object, publicKey: object }>} libp2p key objects
47
+ */
48
+ export async function deriveIPNSKeyPair(prfSeed) {
49
+ // Import the PRF seed as HKDF base key material
50
+ const baseKey = await crypto.subtle.importKey('raw', prfSeed, 'HKDF', false, ['deriveBits']);
51
+
52
+ // Derive a salt from the seed itself: SHA-256(prfSeed) truncated to 16 bytes
53
+ const seedHash = await crypto.subtle.digest('SHA-256', prfSeed);
54
+ const salt = new Uint8Array(seedHash).slice(0, 16);
55
+
56
+ const info = new TextEncoder().encode('p2p-passkeys/ipns-key');
57
+
58
+ // Derive 256 bits (32 bytes) of key material
59
+ const derivedBits = await crypto.subtle.deriveBits(
60
+ { name: 'HKDF', hash: 'SHA-256', salt, info },
61
+ baseKey,
62
+ 256
63
+ );
64
+
65
+ const derivedSeed = new Uint8Array(derivedBits);
66
+
67
+ // Generate the Ed25519 keypair from the deterministic seed
68
+ const keyPair = await generateKeyPairFromSeed('Ed25519', derivedSeed);
69
+
70
+ console.log(PREFIX, 'Derived IPNS Ed25519 keypair from PRF seed');
71
+
72
+ return { privateKey: keyPair, publicKey: keyPair.publicKey };
73
+ }
74
+
75
+ /**
76
+ * Recover a PRF seed by performing a discoverable-credential WebAuthn
77
+ * assertion with the PRF extension.
78
+ *
79
+ * The browser will show its native passkey picker (no `allowCredentials`),
80
+ * letting the user choose which credential to authenticate with.
81
+ *
82
+ * If the authenticator supports PRF, the raw PRF output is used as the seed.
83
+ * Otherwise, `rawId` is used as a less-secure fallback.
84
+ *
85
+ * @returns {Promise<{ prfSeed: Uint8Array, rawCredentialId: Uint8Array, credential: object }>}
86
+ */
87
+ export async function recoverPrfSeed() {
88
+ const deterministicSalt = await computeDeterministicPrfSalt();
89
+
90
+ console.log(PREFIX, 'Initiating discoverable-credential assertion for PRF seed recovery');
91
+
92
+ let hostname;
93
+ try {
94
+ hostname = location.hostname || 'localhost';
95
+ } catch {
96
+ hostname = 'localhost';
97
+ }
98
+
99
+ const assertion = await navigator.credentials.get({
100
+ publicKey: {
101
+ challenge: crypto.getRandomValues(new Uint8Array(32)),
102
+ rpId: hostname,
103
+ userVerification: 'required',
104
+ extensions: {
105
+ prf: { eval: { first: deterministicSalt } },
106
+ },
107
+ },
108
+ });
109
+
110
+ const rawCredentialId = new Uint8Array(assertion.rawId);
111
+
112
+ // Try to extract PRF result
113
+ const extResults = assertion.getClientExtensionResults?.();
114
+ const prfResult = extResults?.prf?.results?.first;
115
+
116
+ let prfSeed;
117
+ if (prfResult) {
118
+ prfSeed = new Uint8Array(prfResult);
119
+ console.log(PREFIX, 'PRF seed recovered from authenticator extension');
120
+ } else {
121
+ // Fallback: use rawId as seed (less secure but functional)
122
+ prfSeed = new Uint8Array(assertion.rawId);
123
+ console.log(PREFIX, 'PRF not available, falling back to rawId as seed');
124
+ }
125
+
126
+ return { prfSeed, rawCredentialId, credential: assertion };
127
+ }