OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-room commits / d96490c

LabelBase ns: keep label intents out of a repo's record set (0.2.29)
hpp will host the labeler on the DID's own StateBase. RepoBase lists every key with a "/" under a non-"__" collection as a record, and an intent on a post URI ("label\x00at://did/app.bsky.feed.post/rkey\x00spam") has both, so it would be signed into the repo as collection "label\x00at:". Test reproduced it. `ns` (LabelBase/LabelRoom) moves intents under a reserved "__" name; the default stays "label" for existing standalone labelers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
d96490cbddfa9368d2e523abc050624f8fe87a07
by plankatron <fleeky@gmail.com> · 2026-10-14T02:04:13.000Z

Changed files (4) +28 -6

lib/label-base.js +8 -3
……@@ -23,16 +23,21 @@ const NS = 'label'
2323export class LabelBase {
2424 // statebase: an async KV intent-log (StateBaseV3 or compatible). src: the labeler's DID — the ONE
2525 // identity every emitted label carries. Mods are WRITERS, not the src (authority is positional).
26 constructor (statebase, { src } = {}) {
26 //
27 // ns: the intent key prefix. ⛔ On a REPO's own StateBase (hpp hosts the labeler on the DID's base) pass a
28 // reserved "__" name: RepoBase lists every key holding a "/" under a non-"__" collection as a record, and
29 // an intent on a post URI has a "/" — under the default "label" it would be signed into the repo.
30 constructor (statebase, { src, ns = NS } = {}) {
2731 if (!statebase || typeof statebase.set !== 'function' || typeof statebase.list !== 'function') {
2832 throw new Error('LabelBase: a statebase with async set/del/get/list is required')
2933 }
3034 if (!src) throw new Error('LabelBase: src (the labeler DID) required')
3135 this.sb = statebase
3236 this.src = src
37 this.ns = ns
3338 }
3439
35 _key (uri, val) { return NS + SEP + uri + SEP + val }
40 _key (uri, val) { return this.ns + SEP + uri + SEP + val }
3641
3742 // Append a label intent: label (or, with neg:true, retract) subject `uri` with value `val`. cts is
3843 // caller-supplied for determinism (else now). Authority to WRITE is StateBase admission; role gating
……@@ -60,7 +65,7 @@ export class LabelBase {
6065 // All label intents in the linearized view, INCLUDING negated ones — what the signer materializes
6166 // (it emits neg labels too, so consumers see retractions). [{ uri, val, neg, cts }].
6267 async intents () {
63 const map = await this.sb.list(NS + SEP)
68 const map = await this.sb.list(this.ns + SEP)
6469 return Object.keys(map).map((k) => map[k]).filter(Boolean)
6570 }
6671
lib/label-room.js +2 -2
……@@ -34,13 +34,13 @@ export class LabelRoom {
3434 // `holds`: () => boolean | Promise<boolean> — the HOST's signer election (hpp's presence election). When
3535 // given, LabelRoom runs no election of its own and writes no __lease__: hpp retired that append-forever
3636 // heartbeat, and two elections could disagree about who signs. Without it, the original Lease (self/ttlMs).
37 constructor (statebase, { src, privateKey, self, ttlMs = 15000, holds = null } = {}) {
37 constructor (statebase, { src, privateKey, self, ttlMs = 15000, holds = null, ns } = {}) {
3838 if (!src || !privateKey) throw new Error('LabelRoom: src + privateKey required')
3939 if (!holds && !self) throw new Error('LabelRoom: self required (or pass holds: the host\'s signer election)')
4040 this.sb = statebase
4141 this.src = src
4242 this.privateKey = privateKey
43 this.base = new LabelBase(statebase, { src })
43 this.base = new LabelBase(statebase, { src, ns }) // ns: see LabelBase — reserved name on a repo's base
4444 this.holds = holds
4545 this.lease = holds ? null : new Lease(statebase, { self, ttlMs })
4646 this._projected = new WeakMap() // feed core -> Set of label hexes already appended
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "atproto-room",
3 "version": "0.2.28",
3 "version": "0.2.29",
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 +17 -0
……@@ -76,4 +76,21 @@ const E2 = 'did:plc:e2jevhuurrhlm36ufk7fklpg'
7676 assert(uris.length === 2 && uris.includes(WJ) && uris.includes(E2), 'the feed holds both, each once')
7777 await feed.close(); try { await store.close() } catch {}; rmSync(dir, { recursive: true, force: true })
7878}
79// ⛔ ON A REPO'S OWN STATEBASE, intents must not read as records. hpp hosts the labeler on the DID's
80// StateBase, where RepoBase lists every key with a "/" and a non-"__" collection as a record — and an
81// intent on a post URI ("label\x00at://did/app.bsky.feed.post/rkey\x00spam") has both. It would be signed
82// into the repo as a record of collection "label\x00at:". `ns` moves intents under a reserved name.
83{
84 const { RepoBase } = await import('../lib/repo-base.js')
85 const POST = 'at://' + WJ + '/app.bsky.feed.post/3mrhxnkbhv622'
86 const shared = mockSb()
87 const r = new LabelRoom(shared, { ...KP, holds: () => true, ns: '__labelintent__' })
88 await r.ensure(POST, 'spam')
89 await r.ensure(WJ, 'loser-club-member')
90 await r.tick()
91 const recs = await new RepoBase(shared, { did: SRC }).listRecords()
92 assert(recs.length === 0, 'no label intent or signed label lists as a repo record (got ' + JSON.stringify(recs.map((x) => x.collection)) + ')')
93 assert((await r.base.intents()).length === 2, 'the intents are still there, under the reserved name')
94 assert((await r.signedLog()).length === 2, 'and both were signed')
95}
7996console.log('\nLABEL-ROOM PRESENCE OK')