| … | … | @@ -23,16 +23,21 @@ const NS = 'label' |
| 23 | 23 | export class LabelBase { |
| 24 | 24 | // statebase: an async KV intent-log (StateBaseV3 or compatible). src: the labeler's DID — the ONE |
| 25 | 25 | // 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 } = {}) { | |
| 27 | 31 | if (!statebase || typeof statebase.set !== 'function' || typeof statebase.list !== 'function') { |
| 28 | 32 | throw new Error('LabelBase: a statebase with async set/del/get/list is required') |
| 29 | 33 | } |
| 30 | 34 | if (!src) throw new Error('LabelBase: src (the labeler DID) required') |
| 31 | 35 | this.sb = statebase |
| 32 | 36 | this.src = src |
| 37 | this.ns = ns | |
| 33 | 38 | } |
| 34 | 39 | |
| 35 | _key (uri, val) { return NS + SEP + uri + SEP + val } | |
| 40 | _key (uri, val) { return this.ns + SEP + uri + SEP + val } | |
| 36 | 41 | |
| 37 | 42 | // Append a label intent: label (or, with neg:true, retract) subject `uri` with value `val`. cts is |
| 38 | 43 | // caller-supplied for determinism (else now). Authority to WRITE is StateBase admission; role gating |
| … | … | @@ -60,7 +65,7 @@ export class LabelBase { |
| 60 | 65 | // All label intents in the linearized view, INCLUDING negated ones — what the signer materializes |
| 61 | 66 | // (it emits neg labels too, so consumers see retractions). [{ uri, val, neg, cts }]. |
| 62 | 67 | async intents () { |
| 63 | const map = await this.sb.list(NS + SEP) | |
| 68 | const map = await this.sb.list(this.ns + SEP) | |
| 64 | 69 | return Object.keys(map).map((k) => map[k]).filter(Boolean) |
| 65 | 70 | } |
| 66 | 71 |
| … | … | @@ -34,13 +34,13 @@ export class LabelRoom { |
| 34 | 34 | // `holds`: () => boolean | Promise<boolean> — the HOST's signer election (hpp's presence election). When |
| 35 | 35 | // given, LabelRoom runs no election of its own and writes no __lease__: hpp retired that append-forever |
| 36 | 36 | // 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 } = {}) { | |
| 38 | 38 | if (!src || !privateKey) throw new Error('LabelRoom: src + privateKey required') |
| 39 | 39 | if (!holds && !self) throw new Error('LabelRoom: self required (or pass holds: the host\'s signer election)') |
| 40 | 40 | this.sb = statebase |
| 41 | 41 | this.src = src |
| 42 | 42 | 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 | |
| 44 | 44 | this.holds = holds |
| 45 | 45 | this.lease = holds ? null : new Lease(statebase, { self, ttlMs }) |
| 46 | 46 | this._projected = new WeakMap() // feed core -> Set of label hexes already appended |
| … | … | @@ -1,6 +1,6 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "atproto-room", |
| 3 | "version": "0.2.28", | |
| 3 | "version": "0.2.29", | |
| 4 | 4 | "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.", |
| 5 | 5 | "type": "module", |
| 6 | 6 | "main": "index.js", |
| … | … | @@ -76,4 +76,21 @@ const E2 = 'did:plc:e2jevhuurrhlm36ufk7fklpg' |
| 76 | 76 | assert(uris.length === 2 && uris.includes(WJ) && uris.includes(E2), 'the feed holds both, each once') |
| 77 | 77 | await feed.close(); try { await store.close() } catch {}; rmSync(dir, { recursive: true, force: true }) |
| 78 | 78 | } |
| 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 | } | |
| 79 | 96 | console.log('\nLABEL-ROOM PRESENCE OK') |