OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer commits / 133b02e

0.3.132: get-state reports the election as the sign gate sees it; label-list the projection
loser-club's two co-owners both report holdsSigner:true while replicating one base. get-state now adds isSigner and election {netRev, withHead, noHead} (present co-owners + known heads); label-list adds projection (atproto-room 0.2.31 lastProjection). Read-only diagnostics. 178/178. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
133b02efd3259edff6f5918df4f9ae02ab215430
by plankatron <fleeky@gmail.com> · 2026-10-14T17:08:17.000Z

Changed files (5) +35 -1

lib/serve-device-uds.js +3 -0
……@@ -90,6 +90,9 @@ export async function serveDeviceUds (device, { uds, blobs = null, onRelease = n
9090 intents: await l.base.intents(),
9191 signed: log.length,
9292 holds,
93 // Did the signed log reach the label core the membrane serves? (LabelRoom.lastProjection — null until the
94 // first projectTo since boot.)
95 projection: l.lastProjection === undefined ? null : l.lastProjection,
9396 labels: log.map((x) => ({ uri: x.uri, val: x.val, cts: x.cts, neg: !!x.neg, ver: x.ver === undefined ? null : x.ver }))
9497 }
9598 })
lib/swarm-device.js +12 -0
……@@ -850,6 +850,18 @@ export class SwarmDevice {
850850 // The field that ACTUALLY gates signing: the presence election. Null when the substrate has no
851851 // presence (unit mocks / single-writer), where sign() defaults to signing.
852852 holdsSigner: (() => { try { return this.sb.holdsSigner ? !!this.sb.holdsSigner() : null } catch { return null } })(),
853 // ⛔ THE GATE'S VIEW (2026-10-14: both loser-club co-owners said holdsSigner:true). holdsSigner above asks
854 // WITHOUT the network head; sign() and the label tick ask WITH it (isSigner). Report both, with who is
855 // present and which heads are known, so a split election is a read, not an inference. Non-blocking (listNow).
856 isSigner: await this.isSigner().catch(() => null),
857 election: await (async () => {
858 if (!this.sb.signerState) return null
859 try {
860 let netRev = null
861 if (this.firehose) { const h = commitLine(Object.values(await this.sb.listNow(FIREHOSE_PREFIX)).filter((f) => f && f.rev)).head; netRev = h ? h.rev : null }
862 return { netRev, withHead: this.sb.signerState(undefined, netRev), noHead: this.sb.signerState() }
863 } catch { return null }
864 })(),
853865 indexers: (this.sb.base && this.sb.base.linearizer && this.sb.base.linearizer.indexers) ? this.sb.base.linearizer.indexers.length : null,
854866 writable: !!(this.sb.base && this.sb.base.writable),
855867 head: this.head ? { rev: this.head.rev, commitCid: this.head.commitCid.toString() } : null,
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "hyper-proto-peer",
3 "version": "0.3.131",
3 "version": "0.3.132",
44 "description": "Self-custody, P2P-native atproto peer \u2014 holds the user's key + content as Hypercores and authors+signs its own repo. The source of truth in the peer/bridge split (Model B); atproto-bridge projects it to bsky.",
55 "type": "module",
66 "main": "lib/peer.js",
test/label-uds.test.mjs +1 -0
……@@ -78,6 +78,7 @@ test('label-list reports the label gate and each signed label with its ver', asy
7878 t.is(l.labels.length, 1)
7979 t.alike({ uri: l.labels[0].uri, val: l.labels[0].val, ver: l.labels[0].ver, neg: l.labels[0].neg }, { uri: WJ, val: 'loser-club-member', ver: 1, neg: false })
8080 t.ok(typeof l.labels[0].cts === 'string', 'with its cts')
81 t.is(l.projection, null, 'projection: null until the label room has projected into a core')
8182 gate = false
8283 t.is((await c.call('label-list', {})).holds, false, 'and it follows the election')
8384})
test/swarm-device-is-signer.test.mjs +18 -0
……@@ -41,6 +41,24 @@ test('★ isSigner asks the presence election with the NETWORK head rev, as sign
4141 t.alike(asked, [h.rev, h.rev], 'each time asked with the network head rev')
4242})
4343
44// ⛔ THE ELECTION MUST BE READABLE AS THE SIGN GATE SEES IT. 2026-10-14: loser-club's two co-owners BOTH reported
45// holdsSigner:true while replicating the same base; get-state asked the election without the network head, the
46// gate asks with it, and neither showed who was present or which heads were known. state() now reports both.
47test('state() reports the election with and without the network head, and isSigner', async (t) => {
48 const sb = new MemKV()
49 const A = new SwarmDevice(sb, { did: DID, privateKey: KEY, self: 'A', firehose: true })
50 await A.putRecord('app.bsky.feed.post', 'r1', post('one'))
51 const h = await A.sign({ force: true })
52 sb.holdsSigner = (now, netHead) => netHead === null || netHead === undefined
53 sb.signerState = (now, netHead) => ({ self: 'A', signer: netHead ? 'B' : 'A', effective: ['A', 'B'], holds: !netHead, heads: { A: null, B: h.rev } })
54 const s = await A.state()
55 t.is(s.isSigner, false, 'isSigner: the gate (asked with the network head)')
56 t.is(s.election.netRev, h.rev, 'the network head rev the gate uses')
57 t.is(s.election.withHead.signer, 'B')
58 t.is(s.election.noHead.signer, 'A')
59 t.alike(s.election.withHead.heads, { A: null, B: h.rev }, 'with each co-owner\'s known head')
60})
61
4462test('a substrate with no election (single writer) signs — the same default as sign()', async (t) => {
4563 const A = new SwarmDevice(new MemKV(), { did: DID, privateKey: KEY, self: 'A', firehose: true })
4664 t.is(await A.isSigner(), true)