OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / lib / rebuild-did.js

68 lines · javascript ref:
// rebuild-did — NOTARY REVIVAL. Export a hosted DID's authoritative repo CAR from the RUNNING daemon so
// it can be REBUILT fresh as the genesis creator/notary (forceCreator + adoptCar) in isolated storage.
//
// Why a rebuild (not an in-place fix): after the poisoned creator abandons the deterministic genesis via a
// resync (forceNonCreator), the base has NO live indexer — indexedLength freezes, new writes never
// finalize. A healthy co-owner revives the notary by RE-AUTHORING the genesis from the authoritative
// snapshot. COEXISTENCE RULE (proven in test/rebuild-2node-transition.test.js): the re-authored genesis
// has the SAME deterministic key but DIFFERENT content than the old one, so if the OLD base is still on
// the swarm they COLLIDE (conflicts, finalization blocked). Therefore the rebuild MUST run with the old
// base fully OFF the swarm and in FRESH storage — this tool EXPORTS the snapshot + prints that sequence;
// it deliberately does NOT mutate the live base in place.
import fs from 'bare-fs'
import { join } from 'bare-path'
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
import { Client } from 'hyper-svc'
import { loadConfig } from './config.js'

const NS = (s) => b4a.from('atproto-base/v1/' + s)
const did16For = (did) => b4a.toString(crypto.hash([NS('room-key'), b4a.from(did)]), 'hex').slice(0, 16)

// args = the CLI args AFTER the subcommand, e.g. ['did:plc:...', '--storage', '/path']. Returns exit code.
export async function rebuildDidCommand (args = []) {
  const did = args.find((a) => a.startsWith('did:'))
  const sIdx = args.indexOf('--storage')
  const cfg = loadConfig()
  const storage = sIdx >= 0 ? args[sIdx + 1] : cfg.storage
  if (!did) {
    console.error('usage: hyper-proto-peer rebuild-did <did> [--storage <path>]')
    console.error('  Exports the DID\'s authoritative CAR (via the running daemon) for a fresh notary-revival rebuild.')
    console.error('  hosted DIDs:', JSON.stringify((cfg.dids || []).map((d) => d.did)))
    return 1
  }

  // Resolve the daemon's LOCAL control socket for this DID (primary DID = flat; else per-DID sub-path).
  const entry = (cfg.dids || []).find((d) => d.did === did)
  const primary = !!(entry && entry.primary)
  const sock = primary ? join(storage, 'control.sock') : join(storage, 'dids', did16For(did), 'control.sock')
  if (!fs.existsSync(sock)) {
    console.error('rebuild-did: control socket not found at ' + sock)
    console.error('  Is the daemon running and hosting ' + did + '? (rebuild-did reads the LIVE authoritative state.)')
    return 1
  }

  // Pull the authoritative CAR over the local UDS (owner-gated by the filesystem).
  const client = await Client.connectUds(sock)
  let car
  try { car = await client.call('get-repo-car', {}) } catch (e) { console.error('rebuild-did: get-repo-car failed: ' + e.message); try { await client.close() } catch {} ; return 1 } finally { try { await client.close() } catch {} }
  if (!car || !car.car) { console.error('rebuild-did: daemon returned no CAR for ' + did); return 1 }

  const outDir = join(storage, 'rebuild')
  fs.mkdirSync(outDir, { recursive: true })
  const outPath = join(outDir, did16For(did) + '.car')
  fs.writeFileSync(outPath, b4a.from(car.car, 'hex'))
  console.log(JSON.stringify({ msg: 'rebuild-did: exported authoritative CAR', did, rev: car.rev, records: car.recordCount, car: outPath }))
  console.log('')
  console.log('NOTARY REVIVAL — the OLD base MUST be off the swarm before the rebuilt one is served:')
  console.log('  1. RETIRE the old base: on EVERY node currently hosting ' + did + ', remove it from that')
  console.log('     daemon\'s config `dids` and restart. Old cores stay on disk (nothing deleted).')
  console.log('  2. RE-ONBOARD this node as the notary in FRESH storage (a daemon that no longer holds the')
  console.log('     old cores). Add a dids entry for ' + did + ' with:')
  console.log('        "forceCreator": true,')
  console.log('        "adoptCar": "' + outPath + '"')
  console.log('  3. START it — it becomes the live notary; new posts FINALIZE. Bridge re-fronts + relay re-crawls.')
  console.log('  (Add the retired node back later as a FRESH device for HA — do not rejoin its old base.)')
  return 0
}