@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.
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/backup/registry-backup.d.ts +26 -0
- package/dist/backup/registry-backup.js +51 -0
- package/dist/identity/identity-service.d.ts +116 -0
- package/dist/identity/identity-service.js +524 -0
- package/dist/identity/mode-detector.d.ts +29 -0
- package/dist/identity/mode-detector.js +124 -0
- package/dist/identity/signing-preference.d.ts +30 -0
- package/dist/identity/signing-preference.js +55 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +91 -0
- package/dist/p2p/setup.d.ts +48 -0
- package/dist/p2p/setup.js +283 -0
- package/dist/recovery/ipns-key.d.ts +41 -0
- package/dist/recovery/ipns-key.js +127 -0
- package/dist/recovery/manifest.d.ts +106 -0
- package/dist/recovery/manifest.js +243 -0
- package/dist/registry/device-registry.d.ts +122 -0
- package/dist/registry/device-registry.js +275 -0
- package/dist/registry/index.d.ts +3 -0
- package/dist/registry/index.js +46 -0
- package/dist/registry/manager.d.ts +76 -0
- package/dist/registry/manager.js +376 -0
- package/dist/registry/pairing-protocol.d.ts +61 -0
- package/dist/registry/pairing-protocol.js +653 -0
- package/dist/ucan/storacha-auth.d.ts +45 -0
- package/dist/ucan/storacha-auth.js +164 -0
- package/dist/ui/StorachaFab.svelte +134 -0
- package/dist/ui/StorachaFab.svelte.d.ts +23 -0
- package/dist/ui/StorachaIntegration.svelte +2467 -0
- package/dist/ui/StorachaIntegration.svelte.d.ts +23 -0
- package/dist/ui/fonts/dm-mono-400.ttf +0 -0
- package/dist/ui/fonts/dm-mono-500.ttf +0 -0
- package/dist/ui/fonts/dm-sans-400.ttf +0 -0
- package/dist/ui/fonts/dm-sans-500.ttf +0 -0
- package/dist/ui/fonts/dm-sans-600.ttf +0 -0
- package/dist/ui/fonts/dm-sans-700.ttf +0 -0
- package/dist/ui/fonts/epilogue-400.ttf +0 -0
- package/dist/ui/fonts/epilogue-500.ttf +0 -0
- package/dist/ui/fonts/epilogue-600.ttf +0 -0
- package/dist/ui/fonts/epilogue-700.ttf +0 -0
- package/dist/ui/fonts/storacha-fonts.css +152 -0
- package/dist/ui/storacha-backup.d.ts +44 -0
- package/dist/ui/storacha-backup.js +218 -0
- 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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|