borg_mesh/lib.rs
1//! The team mind without a server: the members of a collective sync its lessons peer to peer.
2//!
3//! - **Identity** (identity.rs): one ed25519 key per node and collective, kept in the collective's
4//! store (mode 0600). The same key is the node's iroh id, so the transport authenticates it.
5//! - **Membership** (membership.rs): a JSON document listing member keys, roles and revocations,
6//! signed by an admin. A newer version is adopted only if it is the next one, names the held one
7//! as its parent and an admin of the held version signed it; concurrent admins' forks resolve the
8//! same way on every node, keeping every revocation. A node talks only to peers its verified
9//! membership lists.
10//! - **Data** (lines.rs, origin.rs): the append-only `lessons.jsonl`, addressed per line by blake3,
11//! each line with a signed origin (who stamped it, when). Merge is a union; the latest version
12//! of a lesson wins, as `conscience::load` expects. Raw episodes stay on the node that captured
13//! them, so each is learned exactly once. Reasoning excerpts (git notes, `borg notes`) go from
14//! their author to two member replicas, and any holder answers a member asking for a commit's.
15//! - **Sync** (sync.rs, recon.rs): iroh QUIC between two nodes, trust first (membership and
16//! revocations before any data), then range-based set reconciliation, cost ∝ difference.
17//! - **Live** (live.rs): presence of the members' drones, ephemeral, carried in every session after
18//! the logs; never reconciled into a log.
19//! - **Steering** (steer.rs): a pause, resume or veto is a message line, authenticated by its origin;
20//! another node's counts only under its member role `admin` or `steer`, admitted when stored.
21//! - **Daemon** (daemon.rs, peers.rs): `serve` accepts members and dials the known ones every
22//! interval and on local change, with backoff, until Ctrl-C / SIGTERM.
23//! - **Reach** (addr.rs): peers are dialled by key; across NAT through iroh's hole punching with
24//! relay fallback (n0's public relays and address lookup by default, a self-hosted relay, or
25//! direct addresses only), set per collective.
26//! - **Onboarding** (invite.rs): an admin's signed, expiring, use-limited invite in a ticket string
27//! or QR code; `join <ticket>` dials the admin, which adds the key to the signed membership.
28//!
29//! The gate: `borg::collective::may_sync` is asked before any byte of a collective leaves, on both
30//! sides, on the config as it is at that moment. A local collective or an off repo is refused before
31//! a connection is even opened, and so is a store a local collective also maps to.
32//!
33//! **Internal API, no semver.** The product is the `borg-mesh` binary; this library exists for borg's own
34//! crates, which pin it `=` to the same version. Any release may change or remove any item.
35
36pub mod addr;
37pub mod daemon;
38pub mod identity;
39pub mod invite;
40pub mod lines;
41pub mod live;
42pub mod membership;
43pub mod origin;
44pub mod peers;
45pub mod recon;
46pub mod steer;
47pub mod sync;
48
49/// Lowercase hex, the form keys and hashes take in files, tickets and on the wire.
50pub fn to_hex(bytes: &[u8]) -> String {
51 bytes.iter().map(|b| format!("{b:02x}")).collect()
52}
53
54pub fn from_hex<const N: usize>(s: &str) -> Result<[u8; N], String> {
55 let s = s.trim();
56 if s.len() != 2 * N || !s.is_ascii() {
57 return Err(format!("expected {} hex characters, got {:?}", 2 * N, s));
58 }
59 let mut out = [0u8; N];
60 for (i, b) in out.iter_mut().enumerate() {
61 *b = u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(|e| format!("bad hex {s:?}: {e}"))?;
62 }
63 Ok(out)
64}
65
66#[cfg(test)]
67mod tests {
68 #[test]
69 fn hex_round_trips_and_rejects_garbage() {
70 let b = [0u8, 1, 0xab, 0xff];
71 assert_eq!(super::from_hex::<4>(&super::to_hex(&b)).unwrap(), b);
72 assert!(super::from_hex::<4>("00").is_err());
73 assert!(super::from_hex::<1>("zz").is_err());
74 }
75}