OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-room commits / f346344

signer-presence: presence handshake channel (attachPresence)
The transport glue for SignerPresence: on a StateBase swarm connection, announce our member key + learn the peer's, feeding the election. Mirrors serve-membrane's announce-push channel (Protomux.from(conn).createChannel — the keet one-mux-per-conn model), so it coexists with base.replicate on the same stream and appends nothing. Test (signer-presence-channel): 8/8 over a REAL SecretStream pair (= a hyperswarm connection) — keys exchanged, both elect the lowest-key signer, survivor fails over after grace on disconnect, no premature thrash. Next: wire into StateBaseV3 (own a SignerPresence, attach on connect, expose holdsSigner()) + swap the device-node sign loop, with a two-StateBase failover test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
f346344c68aa4d391eec43f1112f9974120ba28c
by plankatron <fleeky@gmail.com> · 2026-07-18T22:29:57.000Z

Changed files (2) +79 -0

lib/signer-presence-channel.js +37 -0
……@@ -0,0 +1,37 @@
1// attachPresence — the transport glue for SignerPresence. On a StateBase swarm connection, announce OUR
2// member key and learn the PEER's, feeding a SignerPresence so the signer election tracks who is connected.
3// Ephemeral by construction — it appends NOTHING to any log; presence lives and dies with the connection.
4//
5// Mirrors serve-membrane's announce-push channel exactly (`Protomux.from(conn).createChannel` → addMessage →
6// open → send), the keet "one mux per connection" model, so it coexists with base.replicate on the same
7// stream. The peer's key arrives via onmessage → markPresent; the connection closing → markAbsent (grace).
8import Protomux from 'protomux'
9import c from 'compact-encoding'
10
11export const PRESENCE_PROTOCOL = 'atproto-room/signer-presence/v1'
12
13// conn: a hyperswarm/replication stream. selfKey: our member key (hex string). presence: a SignerPresence.
14// Returns a cleanup fn (idempotent). Safe to call on every connection; a non-presence peer simply never
15// sends, so it's never marked present (and thus never eligible to sign).
16export function attachPresence (conn, { selfKey, presence } = {}) {
17 if (!conn || !selfKey || !presence) return () => {}
18 const self = String(selfKey)
19 let peerKey = null
20 let done = false
21 const clear = () => { if (peerKey) presence.markAbsent(peerKey) }
22 try {
23 const chan = Protomux.from(conn).createChannel({ protocol: PRESENCE_PROTOCOL })
24 if (!chan) return () => {}
25 const msg = chan.addMessage({
26 encoding: c.json,
27 onmessage: (data) => {
28 if (data && data.key) { peerKey = String(data.key); presence.markPresent(peerKey) }
29 }
30 })
31 chan.open()
32 try { msg.send({ key: self }) } catch {}
33 const onclose = () => { if (done) return; done = true; clear() }
34 conn.on('close', onclose)
35 return () => { if (done) return; done = true; try { conn.off('close', onclose) } catch {}; clear() }
36 } catch { return () => {} }
37}
test/signer-presence-channel.test.mjs +42 -0
……@@ -0,0 +1,42 @@
1// attachPresence (S1 wiring) — proves the presence handshake over a REAL encrypted connection (a
2// SecretStream pair = exactly a hyperswarm connection). Two peers exchange member keys, both elect the
3// lowest-key signer, and the survivor fails over (after grace) when the holder disconnects — appending
4// nothing to any log. Run: `node test/signer-presence-channel.test.mjs`
5import test from 'brittle'
6import SecretStream from '@hyperswarm/secret-stream'
7import { SignerPresence } from '../lib/signer-presence.js'
8import { attachPresence } from '../lib/signer-presence-channel.js'
9
10const A = 'aaaa000000000000000000000000000000000000000000000000000000000000'
11const B = 'bbbb000000000000000000000000000000000000000000000000000000000000'
12const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
13const waitFor = async (fn, timeout = 3000) => { const end = Date.now() + timeout; while (Date.now() < end) { if (fn()) return true; await sleep(20) } return false }
14
15test('presence handshake over a real connection: elect lowest-key signer, fail over on disconnect', async (t) => {
16 const grace = 400
17 const spA = new SignerPresence({ self: A, graceMs: grace })
18 const spB = new SignerPresence({ self: B, graceMs: grace })
19
20 const a = new SecretStream(true)
21 const b = new SecretStream(false)
22 a.rawStream.pipe(b.rawStream).pipe(a.rawStream) // cross-pipe = a connected pair
23
24 const detachA = attachPresence(a, { selfKey: A, presence: spA })
25 const detachB = attachPresence(b, { selfKey: B, presence: spB })
26 t.teardown(() => { detachA(); detachB(); try { a.destroy() } catch {} try { b.destroy() } catch {} })
27
28 // member keys exchanged over the wire → both compute the same signer (A, lowest)
29 t.ok(await waitFor(() => spB.effective().includes(A) && spA.effective().includes(B)), 'peers exchanged member keys over the connection')
30 t.is(spA.currentSigner(), A, 'A (lowest key) is elected signer')
31 t.is(spB.currentSigner(), A, 'B independently agrees A is the signer')
32 t.ok(spA.holds(), 'A holds the signer role')
33 t.absent(spB.holds(), 'B does not hold while A is present')
34
35 // A disconnects → B's connection closes → B fails over, but only AFTER the grace window
36 a.destroy()
37 await sleep(150) // close fires + A marked absent, but 150ms < grace(400) → still in grace
38 t.absent(spB.holds(), 'immediately after disconnect B stays in grace (no premature/thrashing failover)')
39 await sleep(grace) // now past grace
40 t.ok(spB.holds(), 'after grace, B fails over to signer')
41 t.is(spB.currentSigner(), B, 'B is now the sole signer')
42})