OverviewFilesCommitsIssuesDocsReleasesPeers

favela-node / scripts / recover-loser-club.mjs

68 lines · javascript ref:
// Recover loser-club from its saved secrets into FRESH storage — the repo stores corrupted (nohup hard-kill
// → orphaned corestore fd-lock), but ~/.config/loser-club/secrets.json survived and the DID is already
// published (plc.directory) with #bsky_fg/#atproto_labeler → the membrane. This ONE-SHOT (it exits; serving
// is a pgh app's job, NOT nohup) re-derives the SAME DID from the saved rootMnemonic, boots a FRESH co-owned
// repo, and publishes the app.bsky.feed.generator + app.bsky.labeler.service records that make the feed
// resolve. NO new PLC submit (the identity already exists + is correct). The corrupted stores are ABANDONED,
// never touched.
//
//   bare scripts/recover-loser-club.mjs --repo <fresh-repo-path> [--room <fresh-room-path>] [--network]
//
// Defaults are OFFLINE + throwaway so a dry run proves recovery without touching prod. For a REAL recovery,
// point --repo at a NEW path (e.g. ~/.config/loser-club/repo-recovered — NOT the corrupted repo/repo-v3) and
// serve it via a pgh app. network defaults FALSE (no swarm) unless --network.
import '../lib/bare-globals.js'
import process from 'bare-process'
import fs from 'bare-fs'
import { communityIdentity, recoverCommunityRepo, publishFeedGenerator, publishLabelerService } from '../index.js'

const HOME = process.env.HOME
const SECRETS = HOME + '/.config/loser-club/secrets.json'
const MEMBRANE = 'https://pds.7rnx.net'
const RKEY = 'loser-club'

function arg (name, dflt = null) { const i = process.argv.indexOf(name); return i >= 0 ? (process.argv[i + 1] && !process.argv[i + 1].startsWith('--') ? process.argv[i + 1] : true) : dflt }
const repoPath = arg('--repo')
const network = !!arg('--network', false)
const createdAt = arg('--created-at', '2026-07-15T00:00:00.000Z') // deterministic unless overridden

if (!fs.existsSync(SECRETS)) { console.error('no secrets at', SECRETS); process.exit(1) }
if (!repoPath) { console.error('--repo <fresh-repo-path> is required (a NEW path — never the corrupted repo/repo-v3)'); process.exit(1) }
const s = JSON.parse(fs.readFileSync(SECRETS))

// 1. Confirm the saved secrets re-derive the SAME published DID BEFORE booting anything (fail-fast guard).
const id = communityIdentity(s.rootMnemonic, { handle: s.handle, pdsUrl: MEMBRANE, feedGenEndpoint: MEMBRANE, labelerEndpoint: MEMBRANE })
if (id.did !== s.did) { console.error('ABORT: re-derived DID', id.did, '!== saved DID', s.did, '(wrong secrets/endpoints — recovery would create a DIFFERENT identity)'); process.exit(1) }
console.log('✓ recovered identity matches the published DID:', id.did, '(' + id.didKey + ')')

// 2. Boot a FRESH co-owned REPO under that DID (recoverCommunityRepo re-derives from the same mnemonics into
//    NEW storage). REPO ONLY — the roster room is the SAVED secrets.roomKey (an intact separate store); we do
//    NOT recreate it (its key is not mnemonic-derived → a new room would DIVERGE). Serving reopens the saved
//    room in place; recovery must not touch it.
const c = await recoverCommunityRepo({
  rootMnemonic: s.rootMnemonic, founderDeviceMnemonic: s.founderDeviceMnemonic,
  handle: s.handle, feedGenEndpoint: MEMBRANE, labelerEndpoint: MEMBRANE,
  repoStorage: repoPath, network
})
if (c.did !== s.did) { console.error('ABORT: booted repo DID mismatch'); process.exit(1) }

// 3. Publish the lens-export records the original mint never wrote (why the feed never resolved).
await publishFeedGenerator(c.repo, c.did, { rkey: RKEY, displayName: 'loser-club', description: 'a favela community feed', createdAt })
await publishLabelerService(c.repo, { labelValues: [], createdAt })

// 4. Verify the records; report the roster + saved roomKey from secrets (the room is served separately).
const gen = await c.repo.getRecord('app.bsky.feed.generator', RKEY)
const lab = await c.repo.getRecord('app.bsky.labeler.service', 'self')
console.log('✓ feed.generator:', gen && gen.$type, 'did=' + (gen && gen.did), 'rkey=' + RKEY)
console.log('✓ labeler.service:', lab && lab.$type)
console.log('✓ roster (from secrets, served via the saved room):', (s.members || []).join(', '))
console.log('\nRECOVERED (repo at ' + repoPath + ', network=' + network + ').')
console.log('feed uri: at://' + c.did + '/app.bsky.feed.generator/' + RKEY)
console.log('saved roomKey (favela-web reads the roster keylessly from this):', s.roomKey)
console.log('NEXT (prod, needs go-ahead): serve this repo + REOPEN the saved room (~/.config/loser-club/room)')
console.log('  in place via a pgh app (NOT nohup); confirm membrane frontedDids includes ' + c.did + ';')
console.log('  POST requestCrawl → verify the feed resolves on bsky.')

await c.sb.close()
process.exit(0)