// BlindRelay — a KEYLESS availability seeder for self-custody hyper-proto-peers.
//
// Problem: the atproto-bridge fronts a peer only over a LIVE connection (announce + DID-key repo
// verify). A SINGLE peer that goes offline (e.g. ashatron on a laptop) has no one serving its cores,
// so it vanishes from bsky. This relay keeps an EXPLICITLY-ALLOWLISTED set of peers' cores online:
// it replicates each peer's repo + firehose Hypercores (by key) into a PERSISTENT corestore and
// RE-ANNOUNCES them on the peer's DID discovery topic, PUSHING an announce from config + live core
// lengths. When the origin is offline the bridge connects to the RELAY instead and fronts the
// last-synced signed state.
//
// BLIND by construction: it holds NO keys and serves ONLY the read surface (announce push + core
// replication). It NEVER answers apply-write / create-session / get-service-auth, so it cannot sign
// or forge — the bridge still verifies the served repo against the DID key (PeerFront.fromEntry), so
// a relay can only make a peer's OWN signed data available, never fake it. New signed commits still
// require the origin peer (or an always-on 2nd device). It is NOT a StateBase indexer.
//
// GATED: seeds ONLY the peers in the config allowlist (`seedPeers`), never "every possible peer".
// Multi-peer: one swarm PER did (each is a distinct virtual peer so the bridge opens one connection
// per did — the announce carries exactly one did per connection).
import b4a from 'b4a'
import c from 'compact-encoding'
import Hyperswarm from 'hyperswarm'
import Protomux from 'protomux'
import { RepoStore } from 'atproto-store/lib/repo-store.js'
import { FirehoseSequencer } from 'atproto-store/lib/sequencer.js'
import { discoveryTopic } from 'atproto-identity/lib/discovery.js'
const ANNOUNCE_PROTOCOL = 'atproto/did-announce/v1' // MUST match hyper-proto-peer serve-membrane + PeerRouter
// One seeded peer = { did, handle?, repoKey, firehoseKey }. All keys are hex. (serverKey, if present in
// older config, is ignored — announce-push discovery needs only repoKey/firehoseKey, no directionality.)
function validateEntry (p) {
if (!p || !p.did || !p.repoKey || !p.firehoseKey) {
throw new Error('BlindRelay seedPeer requires { did, repoKey, firehoseKey } (hex)')
}
}
export class BlindRelay {
// corestore: ONE persistent Corestore (cores are global by key). swarmOpts: passed to each per-did
// Hyperswarm (testnet bootstrap in tests). seedPeers: the ALLOWLIST (see validateEntry).
constructor (corestore, { seedPeers = [], swarmOpts = null } = {}) {
if (!corestore) throw new Error('BlindRelay: a corestore is required')
this.corestore = corestore
this._swarmOpts = swarmOpts
this._allow = new Map() // did -> coords (the gate: only these dids are ever served)
for (const p of seedPeers) { validateEntry(p); this._allow.set(p.did, { did: p.did, handle: p.handle || null, repoKey: p.repoKey, firehoseKey: p.firehoseKey }) }
this._peers = new Map() // did -> { swarm, repo, seq, coords }
}
async ready () {
for (const [did, coords] of this._allow) { try { await this._seed(did, coords) } catch (e) { console.error(JSON.stringify({ msg: 'blind-relay seed failed', did, error: e.message })) } }
return this
}
// Seed ONE allowlisted peer: open its cores (so they persist + replicate) and re-announce on its
// DID topic, answering get-coords from config. Cores sync from the origin whenever it connects and
// stay served (persisted) when it doesn't.
async _seed (did, coords) {
// open the peer's repo + firehose cores BY KEY → corestore.replicate will sync them from the
// origin when connected, and they persist on disk for offline serving.
const repo = await new RepoStore(this.corestore, { key: b4a.from(coords.repoKey, 'hex') }).ready()
const seq = await new FirehoseSequencer(this.corestore.get({ key: b4a.from(coords.firehoseKey, 'hex') })).ready()
// FULL seeder: a sparse replica only holds blocks it happened to fetch, so it could serve NOTHING
// once the origin is offline. Actively download EVERY block + keep following appends, so the relay
// durably holds the complete repo + firehose and can serve them with the origin gone.
const seedCore = (core) => {
const pull = () => { try { core.download({ start: 0, end: -1 }) } catch {} }
pull()
core.on('append', pull)
}
seedCore(repo.core)
seedCore(seq.core)
const swarm = new Hyperswarm(this._swarmOpts || undefined)
const onconn = (conn) => {
// symmetric replication: pulls fresh blocks FROM the origin, and serves cached blocks TO the bridge.
try { this.corestore.replicate(conn) } catch (e) { console.error(JSON.stringify({ msg: 'blind-relay replicate threw', did, error: e.message })) }
// The ONLY thing we push: the announce (atproto/did-announce/v1, from the allowlist config +
// live core lengths). We deliberately open NO write RPC — no apply-write / create-session /
// get-service-auth / upload-blob — a relay is blind: it can make a peer's OWN signed data
// available (bridge verifies against the DID key), never sign or forge.
try {
const chan = Protomux.from(conn).createChannel({ protocol: ANNOUNCE_PROTOCOL })
if (chan) {
const msg = chan.addMessage({ encoding: c.json, onmessage: () => {} })
chan.open()
const send = () => { try { msg.send({ did, handle: coords.handle, repoKey: coords.repoKey, firehoseKey: coords.firehoseKey, repoLength: repo.core.length, firehoseLength: seq.core.length }) } catch {} }
send()
const onapp = () => send()
seq.core.on('append', onapp)
conn.on('close', () => { try { seq.core.off('append', onapp) } catch {} })
}
} catch (e) { console.error(JSON.stringify({ msg: 'blind-relay announce threw', did, error: e.message })) }
}
swarm.on('connection', onconn)
// server:true → the bridge (and bsky) reach us here when the origin is offline. client:true → we
// also punch out to the origin to pull fresh blocks (firewalled↔firewalled needs both, like the peer).
swarm.join(discoveryTopic(did), { server: true, client: true })
await swarm.flush().catch(() => {})
this._peers.set(did, { swarm, repo, seq, coords })
console.log(JSON.stringify({ msg: 'blind-relay seeding', did, handle: coords.handle, repoLen: repo.core.length, firehoseLen: seq.core.length }))
}
// Observability: what we're seeding + how much we hold (served even when the origin is offline).
status () {
const out = []
for (const [did, p] of this._peers) out.push({ did, handle: p.coords.handle, repoLength: p.repo.core.length, repoContiguous: p.repo.core.contiguousLength, firehoseLength: p.seq.core.length, firehoseContiguous: p.seq.core.contiguousLength })
return { seededCount: out.length, peers: out }
}
async close () {
for (const [, p] of this._peers) {
try { await p.swarm.destroy() } catch {}
try { await p.seq.close() } catch {}
try { await p.repo.close() } catch {}
}
this._peers.clear()
}
}