@le-space/p2pass
v0.3.2
Published
P2Pass — peer-to-peer passkeys, UCANs, OrbitDB registry sync, and Storacha backup (Svelte)
Readme
P2Pass (@le-space/p2pass)
Standalone Svelte component for passkey-based DID identities replicating p2p between devices and Storacha decentralized backup. Published on npm as @le-space/p2pass (Le Space).
Drop in <StorachaFab /> and get:
- WebAuthn passkey authentication (hardware Ed25519, P-256, or worker Ed25519 fallback)
- UCAN delegation-based Storacha access
- OrbitDB backup/restore with progress tracking
- P2P device linking via libp2p with copy/paste peer info
- Multi-device registry with automatic device detection
- Floating action button with Storacha branding
Install
npm install @le-space/p2passUsage
<script>
import { StorachaFab } from '@le-space/p2pass';
</script>
<StorachaFab {orbitdb} {libp2p} onAuthenticate={handleAuthenticate} />The component handles everything internally:
- Click the floating Storacha button (bottom-right)
- Choose a signing mode (hardware Ed25519 with P-256 fallback, hardware P-256 only, or worker Ed25519), then Authenticate with Passkey → biometric prompt → DID created
- Two tabs appear: P2P Passkeys (device linking) and Storacha (backup/restore); P2P Passkeys is the default
- Paste a UCAN delegation → connected to Storacha → backup/restore enabled
- P2P Passkeys tab shows connection status, peer info, and linked devices
Worker Ed25519 Passkey Flow
Worker mode (preferWorkerMode={true}) uses WebAuthn purely for user verification and PRF seed extraction — the actual signing key is an Ed25519 keypair generated in a web worker, encrypted with the PRF-derived key.
First Visit (Registration)
User clicks "Authenticate with Passkey"
│
├─ navigator.credentials.create()
│ ├─ Biometric prompt (Face ID / Touch ID / PIN)
│ ├─ Creates discoverable credential (resident key)
│ └─ PRF extension: eval({ first: deterministicSalt })
│
├─ Extract PRF seed from credential response
│ └─ Deterministic 32-byte seed derived from biometric + salt
│
├─ Initialize Ed25519 keystore with PRF seed
│ └─ PRF seed used as AES-GCM encryption key for the keystore
│
├─ Generate Ed25519 keypair in web worker
│ ├─ Random Ed25519 keypair created
│ ├─ DID derived: did:key:z6Mk...
│ └─ Archive exported (private key material)
│
├─ Encrypt archive with PRF-derived AES key
│ └─ { ciphertext, iv } stored as hex strings
│
├─ Derive IPNS keypair from PRF seed (for recovery)
│ └─ Deterministic Ed25519 key for IPNS manifest publishing
│
└─ Store credentials
├─ Encrypted archive → localStorage (bootstrap cache)
├─ Keypair + archive → OrbitDB registry (when available)
└─ WebAuthn credential metadata → localStorage (for re-auth)Return Visit (Restoration)
User clicks "Authenticate with Passkey"
│
├─ Find cached archive in localStorage
│ └─ { did, ciphertext, iv, publicKeyHex }
│
├─ Load stored WebAuthn credential metadata
│
├─ navigator.credentials.get()
│ ├─ Biometric prompt (same passkey as registration)
│ └─ PRF extension: eval({ first: sameSalt })
│
├─ Extract PRF seed → same seed as registration
│
├─ Decrypt archive with PRF seed → same Ed25519 keypair
│
└─ Same DID restored: did:key:z6Mk...Recovery (New Device / Cleared Storage)
User clicks "Recover Identity"
│
├─ navigator.credentials.get() (discoverable credential)
│ ├─ Biometric prompt — passkey synced via iCloud/Google/etc.
│ └─ PRF extension → same PRF seed
│
├─ Derive IPNS keypair from PRF seed
│
├─ Resolve IPNS manifest via w3name
│ └─ Manifest contains: { ownerDid, archiveCID, delegation, registryAddress }
│
├─ Fetch encrypted archive from IPFS gateway (no auth needed)
│ └─ GET https://{archiveCID}.ipfs.w3s.link/ → { ciphertext, iv }
│
├─ Decrypt archive with PRF seed → Ed25519 keypair restored
│
├─ Connect to Storacha using delegation from manifest
│
└─ Same DID restored on new deviceKey Insight
The WebAuthn credential never signs anything — it's only used for:
- User verification (biometric gate)
- PRF seed extraction (deterministic secret derived from biometric + salt)
The PRF seed is the root of all derived keys:
- Ed25519 DID keypair — encrypted with PRF-derived AES key
- IPNS recovery key — deterministically derived from PRF seed
- Keystore encryption — PRF seed used as AES-GCM key
This means the same passkey on any device (via passkey sync) produces the same PRF seed, which unlocks the same DID identity.
Signing Modes
| Mode | Security | Key Storage | Biometric | | ---------------- | -------- | ----------------------------------- | ------------- | | Hardware Ed25519 | Highest | TPM/Secure Enclave | Per signature | | Hardware P-256 | High | TPM/Secure Enclave | Per signature | | Worker Ed25519 | Medium | Web worker + encrypted localStorage | On init only |
Worker Ed25519 matches typical OrbitDB multi-device flows (signing key in a worker). Hardware modes keep private keys in the authenticator; hardware Ed25519 lists Ed25519 first and may obtain P-256 if the device does not support hardware Ed25519. Pick the mode in the panel before authenticating, or set signingPreference="worker" / preferWorkerMode on the component.
Props
When using StorachaFab or StorachaIntegration directly:
| Prop | Type | Default | Description |
| ------------------- | -------- | --------------- | ---------------------------------------------------------------------------------------- |
| orbitdb | object | null | OrbitDB instance (for backup/restore) |
| database | object | null | Database instance to backup |
| isInitialized | boolean | false | Whether OrbitDB is ready |
| entryCount | number | 0 | Database entry count |
| databaseName | string | 'restored-db' | Name for restored database |
| onRestore | function | () => {} | Called when restore completes |
| onBackup | function | () => {} | Called when backup completes |
| onAuthenticate | function | () => {} | Called after passkey auth (receives signingMode) |
| libp2p | object | null | libp2p instance for P2P connectivity |
| signingPreference | string | null | 'hardware-ed25519', 'hardware-p256', or 'worker' — overrides the in-panel selector |
| preferWorkerMode | boolean | false | Deprecated; same as signingPreference="worker" |
Components
StorachaFab
Floating action button (bottom-right) with the Storacha rooster logo. Opens the integration panel as an overlay. Self-contained — no Tailwind or external CSS required.
StorachaIntegration
The panel component itself. Can be embedded inline instead of as a floating panel.
Programmatic API
import {
IdentityService,
createStorachaClient,
parseDelegation,
setupP2PStack,
createLibp2pInstance,
cleanupP2PStack,
} from '@le-space/p2pass';
// Create identity (worker mode for P2P)
const identity = new IdentityService();
const { mode, did, algorithm } = await identity.initialize(undefined, { preferWorkerMode: true });
// Get UCAN principal
const principal = await identity.getPrincipal();
// Connect to Storacha
const delegation = await parseDelegation(delegationBase64);
const client = await createStorachaClient(principal, delegation);
// Start P2P stack
const libp2p = await createLibp2pInstance();
// After auth, upgrade to full stack with OrbitDB:
const stack = await setupP2PStack(credential);Development
npm run dev:example # Run example app
npm test # Run unit tests
npm run package # Build libraryEnd-to-end tests
Playwright drives the example app in Chromium with virtual WebAuthn. Tests live in e2e/ (for example link-devices.spec.js for multi-device pairing).
Run:
npm run test:e2e # headless; starts relay + Vite automatically
npm run test:e2e:ui # Playwright UI mode (debugging)playwright.config.js starts scripts/e2e-with-relay.mjs, which:
- Launches orbitdb-relay-pinner (local libp2p relay).
- Fetches WebSocket bootstrap multiaddrs from the relay’s HTTP API and passes them to Vite as
VITE_BOOTSTRAP_PEERSso browsers can connect. - Runs
svelte-packageand the example Vite dev server on port 5173.
First-time setup may require browser binaries:
npx playwright install chromiumReuse a running dev server (you must still provide a relay and matching VITE_BOOTSTRAP_PEERS yourself if you skip the script):
PW_REUSE_SERVER=1 npm run test:e2eFailed runs write HTML reports, screenshots, traces, and video under test-results/ (see Playwright output for paths).
Signing mode in e2e: set E2E_SIGNING_MODE to worker, hardware-ed25519, or hardware-p256 (default in helpers is worker). CI runs the link-devices spec three times (matrix), one per mode.
E2E_SIGNING_MODE=hardware-ed25519 npm run test:e2eDependencies
@le-space/orbitdb-identity-provider-webauthn-did— WebAuthn crypto primitives@storacha/client— Storacha storage clientorbitdb-storacha-bridge— OrbitDB backup/restore- Forked
@le-space/ucanto-*packages for WebAuthn Ed25519/P-256 UCAN support
License
MIT
