/** * End-to-end recovery flow: 5 guardians, threshold 3. * * The test boots six Shade instances (alice + bob/carol/dan/eve/faythe), * runs `setupRecovery` from alice, simulates loss + new device by * spawning `alice2` with a fresh address, then runs `requestRecovery` * from alice2. After the flow alice2's storage holds alice's original * identity. */ import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; import type { Shade } from '@shade/sdk'; import { attachGuardian, MemoryRecoveryStore, RecoveryDeclinedError, requestRecovery, setupRecovery, } from '../src/index.js'; import { MemoryRecoveryTransport, spawnShade, startTestPrekeyServer, type TestEnv, } from './helpers.js'; describe('Social key recovery — 3-of-5 end-to-end', () => { let env: TestEnv; let alice: Shade; let alice2: Shade; // new device after loss let guardians: Shade[]; let transport: MemoryRecoveryTransport; const guardianStores = new Map(); const detachers: Array<() => void> = []; beforeAll(async () => { env = await startTestPrekeyServer(); alice = await spawnShade(env.prekeyUrl, 'alice'); const guardianAddrs = ['bob', 'carol', 'dan', 'eve', 'faythe']; guardians = await Promise.all(guardianAddrs.map((a) => spawnShade(env.prekeyUrl, a))); alice2 = await spawnShade(env.prekeyUrl, 'alice-new-device'); transport = new MemoryRecoveryTransport(); transport.add(alice); transport.add(alice2); for (const g of guardians) transport.add(g); // Wire each guardian to auto-approve. We override per-test below // when we need declines. for (const g of guardians) { const store = new MemoryRecoveryStore(); guardianStores.set(g.myAddress, store); const attached = attachGuardian({ shade: g, store, approve: async () => true, deliver: transport.bind(g), }); detachers.push(attached.stop); } }); afterAll(async () => { for (const d of detachers) d(); await alice.shutdown(); await alice2.shutdown(); for (const g of guardians) await g.shutdown(); env.stop(); }); test('setup distributes shares to all 5 guardians', async () => { const result = await setupRecovery({ shade: alice, guardians: guardians.map((g) => g.myAddress), threshold: 3, deliver: transport.bind(alice), }); expect(result.threshold).toBe(3); expect(result.guardianCount).toBe(5); expect(result.allDelivered).toBe(true); expect(result.deliveries.length).toBe(5); for (const d of result.deliveries) { expect(d.error).toBeNull(); } // Each guardian must have stored its share. // Allow a microtask for the onMessage handler to finish save. await Promise.resolve(); for (const g of guardians) { const store = guardianStores.get(g.myAddress)!; const list = await store.list(); expect(list.length).toBe(1); expect(list[0]!.originalAddress).toBe('alice'); expect(list[0]!.guardianCount).toBe(5); expect(list[0]!.threshold).toBe(3); } }); test('recovery from new device with all 5 guardians available', async () => { // Find the setupId from any guardian. const sample = await guardianStores.get('bob')!.list(); const setupId = sample[0]!.setupId; const aliceFingerprintBefore = await alice.fingerprint; const result = await requestRecovery({ shade: alice2, originalAddress: 'alice', guardians: guardians.map((g) => g.myAddress), threshold: 3, setupId, deliver: transport.bind(alice2), timeoutMs: 30_000, }); expect(result.applied).toBe(true); expect(result.granted.length).toBeGreaterThanOrEqual(3); expect(result.declined.length).toBe(0); // alice2 now hosts alice's identity → fingerprints match. const recoveredFingerprint = await alice2.fingerprint; expect(recoveredFingerprint).toBe(aliceFingerprintBefore); }); });