OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-room commits / 615fee6

signer-presence: per-DID channel id so N DIDs share one mux (S1 multi-DID)
attachPresence gains an optional `id` (the roomKey); StateBase.attachSignerPresence passes this._roomKey. protomux keys channels on (protocol, id) (toKey), so without a distinct id every DID collides on (PRESENCE_PROTOCOL, null) and createChannel returns null for all but the first — only the first DID's co-owners ever exchange presence, so the rest each see only themselves present, holdsSigner()==true for all, and they all sign → fork. Distinct per-DID ids let each DID's presence channel coexist on one shared multi-DID daemon connection. Single-DID callers are unchanged (both peers derive the same roomKey per DID; id was previously null on both sides, now roomKey on both). Bump 0.2.5 -> 0.2.6 (public attachPresence signature changed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
615fee653cbc74243e23ce215090ed5a6a0d9b07
by plankatron <fleeky@gmail.com> · 2026-07-18T23:43:49.000Z

Changed files (3) +15 -6

lib/signer-presence-channel.js +10 -4
……@@ -11,16 +11,22 @@ import c from 'compact-encoding'
1111export const PRESENCE_PROTOCOL = 'atproto-room/signer-presence/v1'
1212
1313// 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 } = {}) {
14// id: an OPTIONAL per-DID channel id (the roomKey buffer). One shared multi-DID daemon connection carries
15// several DIDs' presence over ONE mux; protomux keys channels on (protocol, id) (toKey), so distinct ids
16// let each DID's presence channel coexist. Without an id every DID collides on (PRESENCE_PROTOCOL, null) and
17// createChannel returns null for all but the first → only the first DID's co-owners ever exchange presence
18// (the rest each think they're the sole signer → fork). Single-DID callers pass no id (id=null, unchanged).
19// Returns a cleanup fn (idempotent). Safe to call on every connection AND to double-attach the same (conn,
20// id): protomux returns null on the duplicate, so the 2nd attach is a harmless no-op (the 1st stays live).
21// A non-presence peer simply never sends, so it's never marked present (and thus never eligible to sign).
22export function attachPresence (conn, { selfKey, presence, id = null } = {}) {
1723 if (!conn || !selfKey || !presence) return () => {}
1824 const self = String(selfKey)
1925 let peerKey = null
2026 let done = false
2127 const clear = () => { if (peerKey) presence.markAbsent(peerKey) }
2228 try {
23 const chan = Protomux.from(conn).createChannel({ protocol: PRESENCE_PROTOCOL })
29 const chan = Protomux.from(conn).createChannel({ protocol: PRESENCE_PROTOCOL, id })
2430 if (!chan) return () => {}
2531 const msg = chan.addMessage({
2632 encoding: c.json,
lib/state-base.js +4 -1
……@@ -390,7 +390,10 @@ export class StateBaseV3 {
390390 // peer's → feed the election). Public so the multi-DID daemon can also attach it on the shared swarm;
391391 // the single-DID path calls it from the internal connection handler. Returns a detach fn; no-op without a
392392 // member identity. Appends NOTHING — presence lives and dies with the connection.
393 attachSignerPresence (conn) { return this._presence ? attachPresence(conn, { selfKey: this._selfKey.toString('hex'), presence: this._presence }) : (() => {}) }
393 // id: this._roomKey — a per-DID channel id so N DIDs' presence coexist on ONE shared multi-DID daemon
394 // connection (protomux keys on (protocol, id); without it every DID collides on a null id and only the
395 // first opens). Single-DID and multi-DID both derive the same roomKey per DID, so peers of a DID match.
396 attachSignerPresence (conn) { return this._presence ? attachPresence(conn, { selfKey: this._selfKey.toString('hex'), presence: this._presence, id: this._roomKey }) : (() => {}) }
394397
395398 // Do WE hold the active-signer role right now? Drop-in for the old lease.holds() in the sign loop:
396399 // election is a deterministic function of who is CONNECTED (lowest-key effective co-owner), computed in
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "atproto-room",
3 "version": "0.2.5",
3 "version": "0.2.6",
44 "description": "Attestation-gated, encrypted multi-writer room engine — StateBase (keet-model admission) + Lease + signer + RepoBase + LabelBase/LabelRoom + serve-membrane. Extracted from hyper-proto-peer; consumed by the atproto peer (repo devices) and the co-authored labeler.",
55 "type": "module",
66 "main": "index.js",