OverviewFilesCommitsIssuesDocsReleasesPeers

favela-node / test / recover.test.mjs

57 lines · javascript ref:
// favela-node RECOVERY — a community's hypercore stores can be LOST and the community fully rebuilt from
// just its saved secrets (rootMnemonic + founderDeviceMnemonic + handle + endpoints). This is the loser-club
// situation: the repo stores corrupted (nohup hard-kill → orphaned fd-lock) but ~/.config/loser-club/
// secrets.json survived. Proves: (1) the DID + #atproto signing key re-derive DETERMINISTICALLY from the
// secrets (so the already-published PLC identity is reusable — no new submit); (2) a FRESH co-owned repo
// boots under that same DID and the feed.generator + labeler records republish + read back correctly.
// The corrupted stores are ABANDONED, never touched. Run: bare test/recover.test.mjs
import '../lib/bare-globals.js'
import { communityIdentity, recoverCommunityRepo, publishFeedGenerator, publishLabelerService } from '../index.js'

const assert = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log('  ok -', m) }
const stamp = Date.now()
const tmp = (s) => `data/fn-recover-${s}-${stamp}`
// The ONLY things a real recovery has = what secrets.json holds. (Fixed test mnemonics for hermeticity.)
const SECRETS = {
  rootMnemonic: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
  founderDeviceMnemonic: 'legal winner thank year wave sausage worth useful legal winner thank yellow',
  handle: 'losertest.7rnx.net',
  members: ['did:plc:wj62ebbusln6gxo5azo6ogzo', 'did:plc:e2jevhuurrhlm36ufk7fklpg']
}
const MEMBRANE = 'https://pds.7rnx.net'
const CTS = '2026-07-15T00:00:00.000Z'

// ── (1) DETERMINISTIC identity recovery: same secrets → same DID + same signing key, every time ──
const idA = communityIdentity(SECRETS.rootMnemonic, { handle: SECRETS.handle, pdsUrl: MEMBRANE, feedGenEndpoint: MEMBRANE, labelerEndpoint: MEMBRANE })
const idB = communityIdentity(SECRETS.rootMnemonic, { handle: SECRETS.handle, pdsUrl: MEMBRANE, feedGenEndpoint: MEMBRANE, labelerEndpoint: MEMBRANE })
assert(idA.did === idB.did, 'DID re-derives deterministically from the saved rootMnemonic (recovery yields the SAME did:plc)')
assert(idA.didKey === idB.didKey, 'the #atproto did:key re-derives identically (the saved key still CONTROLS the published DID → no new PLC submit)')
assert(/^did:plc:/.test(idA.did) && /^did:key:zQ3sh/.test(idA.didKey), 'recovered identity is a valid did:plc + secp256k1 did:key')
// endpoints are part of the genesis op → part of the DID: recovering with DIFFERENT endpoints = a DIFFERENT DID
const idWrong = communityIdentity(SECRETS.rootMnemonic, { handle: SECRETS.handle, pdsUrl: 'https://other.example', feedGenEndpoint: 'https://other.example', labelerEndpoint: 'https://other.example' })
assert(idWrong.did !== idA.did, 'recovery MUST use the original endpoints — a different serviceEndpoint yields a different DID (genesis-bound)')

// ── (2) recoverCommunityRepo: boot a FRESH co-owned REPO under that recovered DID + republish records ──
// (repo ONLY — the roster room is a separate saved store, reopened at serve time, NOT recreated here; its
//  key is not mnemonic-derived so recreating it would DIVERGE. Fresh storage stands in for the lost stores.)
const rec = await recoverCommunityRepo({
  rootMnemonic: SECRETS.rootMnemonic, founderDeviceMnemonic: SECRETS.founderDeviceMnemonic,
  handle: SECRETS.handle, feedGenEndpoint: MEMBRANE, labelerEndpoint: MEMBRANE,
  repoStorage: tmp('repo'), network: false
})
assert(rec.did === idA.did, 'recoverCommunityRepo boots under the SAME recovered DID (repo only, no room)')
assert(rec.room === undefined, 'recoverCommunityRepo returns NO room (the intact saved room is reopened separately, never recreated)')

// the fresh repo starts EMPTY (proof it is genuinely new storage, not the old one) — then we publish
assert((await rec.repo.getRecord('app.bsky.feed.generator', 'losertest')) === null, 'fresh recovery repo has no records yet (abandoned the corrupted store, did not reuse it)')
const gen = await publishFeedGenerator(rec.repo, rec.did, { rkey: 'losertest', displayName: 'losertest', description: 'the recovered feed', createdAt: CTS })
assert(gen.did === rec.did, 'republished feed.generator.did = the recovered community DID')
const genBack = await rec.repo.getRecord('app.bsky.feed.generator', 'losertest')
assert(genBack && genBack.$type === 'app.bsky.feed.generator' && genBack.did === rec.did, 'feed.generator reads back from the recovered repo with the right DID (feed will resolve on bsky)')
await publishLabelerService(rec.repo, { labelValues: ['spam'], createdAt: CTS })
const labBack = await rec.repo.getRecord('app.bsky.labeler.service', 'self')
assert(labBack && labBack.$type === 'app.bsky.labeler.service', 'labeler.service republished + read back from the recovered repo')

console.log('\nFAVELA-NODE RECOVERY OK: from saved secrets alone — same DID + signing key re-derive (no new PLC submit), a fresh co-owned repo boots (repo-only; intact room untouched) + the feed.generator/labeler records republish. Corrupted stores abandoned.')
await rec.sb.close()