// The client: how anything reaches its own favela-peer, over the local unix socket.
//
// ⛔ SHARED, because there are two consumers and the second would otherwise copy it. favela-bridge
// reads records to project onto bsky; ap-bridge reads the same records to project onto the
// fediverse. A wire protocol written twice drifts — this monorepo already carries six copies of one
// signing format, three of which eat the first character of a hash.
//
// ⚠ It lives beside attestation because both answer the same question from outside the daemon: who
// is this identity, and what did it write. Neither needs storage, a key, or a swarm.
//
// ⛔ READ ONLY, AND PULL. The peer is never asked to push and never told a bridge exists — that is
// the boundary its own test enforces (`favela-peer/test/boundary.test.js`, hyper-social #61 §10):
// "the moment the daemon imports the projection the dependency has been inverted". So this reaches
// in, takes records in favela's own vocabulary, and the peer stays ignorant of what happens next.
//
// It is also the shape every other bridge here already uses — ap-bridge's records.js: "replicate the
// peer's cores over Hyperswarm, read records, project. No key involved." This does the same over a
// socket instead of the swarm, because for a co-located bridge a DHT rendezvous buys nothing.
//
// ⚠ Its wire format is newline-delimited JSON, `{id, cmd, args}` → `{id, ok, value|error}`, NOT
// hyper-svc. The two daemons in this path speak different protocols and that is not worth papering
// over: favela-peer's is deliberately the smallest thing that works (its rpc.js says so).
import net from 'bare-net'
export class FavelaPeer {
constructor (sock) {
if (!sock) throw new Error('FavelaPeer: a socket path is required')
this.sock = sock
}
/** One request, one connection. There is no session to keep and no benefit to holding one open. */
call (cmd, args = {}) {
return new Promise((resolve, reject) => {
const c = net.connect(this.sock)
let buf = ''
let done = false
const finish = (fn, v) => { if (!done) { done = true; try { c.destroy() } catch {} ; fn(v) } }
c.on('error', (err) => finish(reject, err))
c.on('connect', () => c.write(JSON.stringify({ id: 1, cmd, args }) + '\n'))
c.on('data', (d) => {
buf += d
const i = buf.indexOf('\n')
if (i < 0) return
let r
try { r = JSON.parse(buf.slice(0, i)) } catch (err) { return finish(reject, err) }
if (r.ok) finish(resolve, r.value)
else finish(reject, new Error(r.error || 'favela-peer refused ' + cmd))
})
// ⚠ A peer that accepts the connection and never answers would hang the bridge for ever.
setTimeout(() => finish(reject, new Error('favela-peer timed out on ' + cmd)), 30000)
})
}
async whoami () { return this.call('whoami') }
/**
* Which networks this identity has turned on, from the peer's own feed.
*
* ⛔ READ EVERY CYCLE, never cached. A person who unticks bsky in settings has withdrawn consent
* to publish, and a bridge holding a stale copy would keep publishing until someone restarted it.
*
* ⛔ AND A FAILURE READS AS OFF. If the peer cannot be asked, the answer is not "carry on with
* what we had" — it is publish nothing. Unknown consent is not consent.
*/
async reach () {
try {
const r = await this.call('reach')
return {
networks: Array.isArray(r && r.networks) ? r.networks : [],
credential: (r && r.credential) || null
}
} catch {
return { networks: [], credential: null, unknown: true }
}
}
/**
* This node's own posts, newest last.
*
* ⚠ OUR OWN ONLY. A bridge projects what its identity wrote; forwarding a followed feed's posts
* under our DID would be republishing someone else's words as ours, which is a different act with
* a different name and is not this.
*/
async ownPosts () {
const me = await this.whoami()
const rows = await this.call('posts-by', { feed: me.feed })
return { feed: me.feed, rows: Array.isArray(rows) ? rows : [] }
}
}
export default { FavelaPeer }