OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-room commits / f5a0334

0.2.30: a signed label without ver is re-signed in place
Labels signed before atproto-identity 0.0.5 lack ver, which the label spec requires and signs over, so bsky ignores them. The tick now counts only ver:1 entries as emitted; a ver-less one is re-signed and lands on the same content key (signedKey has no ver), replacing it. Repairs loser-club's two member labels with no manual step. Test-first; units + bare test/all.js 83/83 green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f5a0334412133e2188566ba3526cc78c0602c440
by plankatron <fleeky@gmail.com> · 2026-10-14T15:29:48.000Z

Changed files (3) +28 -2

lib/label-room.js +8 -1
……@@ -92,7 +92,14 @@ export class LabelRoom {
9292 const mine = this.holds ? await this.holds() : await this.lease.renewOrClaim(now)
9393 if (!mine) return 0
9494 const logMap = await this.sb.list(SIGNED_NS + SEP)
95 const emitted = new Set(Object.keys(logMap).map((k) => stateOf(decodeLabel(b4a.from(logMap[k], 'hex')))))
95 // ⛔ Only a label carrying `ver` counts as emitted. The label spec requires ver: 1 on transfer and signs over
96 // it; labels signed before atproto-identity 0.0.5 lack it and bsky ignores them. Such an entry is re-signed
97 // below and lands on the SAME content key (signedKey has no ver), replacing the invalid one in place.
98 const emitted = new Set()
99 for (const k of Object.keys(logMap)) {
100 const l = decodeLabel(b4a.from(logMap[k], 'hex'))
101 if (l.ver === 1) emitted.add(stateOf(l))
102 }
96103 let count = 0
97104 for (const i of await this.base.intents()) {
98105 if (emitted.has(stateOf(i))) continue
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "atproto-room",
3 "version": "0.2.29",
3 "version": "0.2.30",
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",
test/label-room-presence.test.mjs +19 -0
……@@ -93,4 +93,23 @@ const E2 = 'did:plc:e2jevhuurrhlm36ufk7fklpg'
9393 assert((await r.base.intents()).length === 2, 'the intents are still there, under the reserved name')
9494 assert((await r.signedLog()).length === 2, 'and both were signed')
9595}
96// ⛔ A SIGNED LABEL WITHOUT `ver` IS NOT EMITTED. The label spec requires ver: 1 on every label that moves
97// between services, and signs over it. Before atproto-identity 0.0.5 buildLabel omitted it, and loser-club's two
98// member labels sit in the shared log in that shape — bsky's AppView never showed them. The tick must treat such
99// an entry as not yet signed and re-sign it, under the same content key, so the log converges on valid labels.
100{
101 const { signLabel, encodeLabel } = await import('atproto-identity/lib/labels.js')
102 const cts = '2026-10-14T15:09:59.328Z'
103 const sb = mockSb()
104 const r = new LabelRoom(sb, { ...KP, holds: () => true })
105 await r.label(WJ, 'loser-club-member', { cts })
106 const legacy = signLabel({ src: SRC, uri: WJ, val: 'loser-club-member', cts }, labeler.privateKey) // no ver
107 const key = '__label__\x00' + cts + '\x00' + WJ + '\x00loser-club-member\x000'
108 await sb.set(key, Buffer.from(encodeLabel(legacy)).toString('hex'))
109 assert(await r.tick() === 1, 'a log entry without ver is re-signed')
110 const log = await r.signedLog()
111 assert(log.length === 1, 'under the SAME content key — the log holds one label, not two')
112 assert(log[0].ver === 1 && verifyLabel(log[0], labeler.didKey), 'and it now carries ver: 1 and verifies')
113 assert(await r.tick() === 0, 'a second tick re-signs nothing')
114}
96115console.log('\nLABEL-ROOM PRESENCE OK')