#!/usr/bin/env bare
// favela-bridge — projects a node's own favela records onto the networks its rooms may reach.
//
// favela-bridge plan every record and report; sends nothing
// favela-bridge --send actually project
//
// FAVELA_PEER_SOCKET favela-peer's socket (default ~/.config/favela-peer/favela.sock)
// FAVELA_BSKY_SOCKET hyper-proto-peer's control socket — REQUIRED for --send
//
// ⛔ DRY BY DEFAULT. Projection is publication: it puts words under an identity on a network that
// does not forget. The default run says what would happen and does nothing, because the cost of
// discovering the plan was wrong after publishing is not symmetric with reading it first.
//
// ⛔ ONE REACH DECISION, TWO PROJECTIONS. See lib/plan.js — each network derives from the markdown,
// never from the other's output.
//
// ⚠ v1 SENDS TO BSKY ONLY. The fediverse plan is computed, reported, and not delivered: ap-bridge
// holds the RSA key and signs at delivery, and wiring that is the next step, not this one. It is
// reported rather than omitted so the gap is visible (library.md §4).
import process from 'bare-process'
import os from 'bare-os'
import { join } from 'bare-path'
import { FavelaPeer } from './lib/peer.js'
import { BskySink } from './lib/bsky.js'
// ⛔ THE PASS ITSELF LIVES IN lib/, WITH NO TRANSPORT IN IT. It has to run in two places — here
// against unix sockets, and inside the handset worklet, where bsky records are authored and SIGNED
// next to the DID key because that key is on the phone. It used to live in this file next to argv
// parsing and process.exit, so the only way to run it on a phone was to write it a second time.
import { projectCycle } from './lib/project.js'
const argv = process.argv.slice(2)
const SEND = argv.includes('--send')
// ⛔ A CEILING ON ONE CYCLE, for the first send into a repo that has followers. A feed with a
// history projects ALL of it the first time, and "16 posts at once" is a different act from
// "post". Absent, this is Infinity and nothing changes.
const LIMIT = (() => {
const i = argv.indexOf('--limit')
if (i === -1) return Infinity
const n = Number(argv[i + 1])
if (!Number.isFinite(n) || n < 0) throw new Error('--limit needs a non-negative number')
return n
})()
// ⛔ --watch implies --send. A watch that only planned would be a process that costs power and
// changes nothing, and the flag would read as if it were working.
const WATCH = argv.includes('--watch')
// ⛔ RECONCILIATION, the counterpart to the event path — the same verb ap-bridge has. A post is
// skipped once its rkey is in the repo, which is right for delivery and wrong the moment the
// PROJECTION improves: every post made before images worked keeps its text-only version for ever,
// and no event will ever revisit it. This re-derives each one and updates the ones that CHANGED.
// Not a --force: it compares against the repo and writes only a real difference.
const REPROJECT = argv.includes('--reproject')
const EVERY = Math.max(5, Number(process.env.FAVELA_BRIDGE_INTERVAL || 15)) * 1000
const PEER_SOCK = process.env.FAVELA_PEER_SOCKET || join(os.homedir(), '.config', 'favela-peer', 'favela.sock')
const BSKY_SOCK = process.env.FAVELA_BSKY_SOCKET ||
join(os.homedir(), '.config', 'hyper-proto-peer', 'data', 'control.sock')
// ⚠ v1 assumes the node's own feed is a PUBLIC one-writer room. identity.md §7.6: "a personal feed
// is a one-writer room". That assumption is stated here rather than buried, because it is the input
// the reach lattice cannot infer — and if it is ever wrong, it is wrong in the direction of a leak.
const ROOM_TYPE = process.env.FAVELA_ROOM_TYPE || 'public'
async function main () {
if (WATCH) {
// ⛔ WATCH ALWAYS SENDS, and --send has no bearing on it. The loop below calls cycle(true)
// unconditionally; SEND is read only by the one-shot path at the end of this function. Said out
// loud because the two flags read as orthogonal and are not: `favela-bridge --watch` expecting a
// dry run would publish, and the deployed pgh args are exactly that (["--watch"], no --send).
// Deliberately NOT "fixed" by making watch honour SEND — both deployed daemons rely on this and
// would silently stop publishing. Use --limit 0 for a watch that sends nothing.
console.log('favela-bridge · watching every ' + (EVERY / 1000) + 's · SENDS on every cycle · ctrl-c to stop')
for (;;) {
// ⛔ One cycle NEVER throws the loop. A peer restarting, a sink respawning on OTA, a socket
// that vanished — all ordinary. A bridge that exited on the first of them would stop
// publishing silently, which is the failure this whole component is meant to make visible.
try { await cycle(true) } catch (err) { console.error(' cycle failed: ' + ((err && err.message) || err)) }
await new Promise((r) => setTimeout(r, EVERY))
}
}
await cycle(SEND)
}
// ⛔ THIS FILE OWNS THE PROCESS, lib/project.js owns the projection. The split is exactly the exit
// code: a refusal to send must end a CLI run non-zero and must NOT end a worklet, where exiting kills
// the app the person is holding.
async function cycle (send) {
const peer = new FavelaPeer(PEER_SOCK)
const bsky = new BskySink(BSKY_SOCK)
const r = await projectCycle({
peer,
bsky,
roomType: ROOM_TYPE,
roomTypeNote: process.env.FAVELA_ROOM_TYPE ? '' : ' (assumed)',
sinkLabel: BSKY_SOCK,
send,
limit: LIMIT,
reproject: REPROJECT,
log: (line) => console.log(line)
})
if (r.refused && send) {
console.error(' refusing to send: ' + r.refused)
process.exit(1)
}
return r
}
main().catch((err) => { console.error('favela-bridge: ' + ((err && err.message) || err)); process.exit(1) })