OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-room / lib / label-room.js

120 lines · javascript ref:
// LabelRoom — the runnable co-authored labeler: ties LabelBase (mod intents) + Lease (single active
// signer) over a shared StateBase. N attested MODERATORS are equal writers appending label INTENTS;
// each tick the LEASE-holder signs new/changed intents into the SHARED __label__ log (NOT a
// single-writer core — so the signer can fail over with no fork, exactly like the swarm-PDS writes
// signed #commits into the shared __commit__ log). A served label feed is PROJECTED from that shared
// log per participant (reusing serve-membrane's pattern) — the serve/projection slice.
//
// Failover-safe DEDUP: the "what's already emitted" state IS the shared __label__ log. Each tick reads
// it and signs only intents whose (uri,val,cts,neg) state isn't already there — so a new lease-holder
// never re-signs the previous holder's labels (no in-memory seed to go stale). RFC6979 determinism
// means the same intent always signs byte-identically, so the shared log is consistent across holders.
//
// Substrate INJECTED (StateBaseV3 or any KV log with async set/del/get/list) — unit-testable with a mock.
import b4a from 'b4a'
import { LabelBase } from './label-base.js'
import { Lease } from './lease.js'
import { materializeLabel } from './label-signer.js'
import { encodeLabel, decodeLabel } from 'atproto-identity/lib/labels.js'

const SIGNED_NS = '__label__' // reserved (leading __) — never a repo record or a label intent
const SEP = '\x00'
const stateOf = (l) => l.uri + SEP + l.val + SEP + String(l.cts) + SEP + (l.neg ? '1' : '0')
// ⛔ CONTENT-KEYED, not index-keyed. The log used to be keyed `pad(n)` (n = the list length the signer saw),
// so two signers during a partition both wrote __label__\x00<n> with DIFFERENT labels and last-write-wins
// silently dropped one. A key derived from the label itself cannot collide with a different label, and
// the same label signed twice (RFC6979: byte-identical) lands on the same key. cts first, so the log still
// reads roughly in time order.
const signedKey = (l) => SIGNED_NS + SEP + String(l.cts) + SEP + l.uri + SEP + l.val + SEP + (l.neg ? '1' : '0')

export class LabelRoom {
  // statebase: shared StateBase (mods = writers). src: labeler DID. privateKey: shared #atproto_label
  // key. self: this mod's device id (drives the lease). ttlMs: lease TTL.
  //
  // `holds`: () => boolean | Promise<boolean> — the HOST's signer election (hpp's presence election). When
  // given, LabelRoom runs no election of its own and writes no __lease__: hpp retired that append-forever
  // heartbeat, and two elections could disagree about who signs. Without it, the original Lease (self/ttlMs).
  constructor (statebase, { src, privateKey, self, ttlMs = 15000, holds = null, ns } = {}) {
    if (!src || !privateKey) throw new Error('LabelRoom: src + privateKey required')
    if (!holds && !self) throw new Error('LabelRoom: self required (or pass holds: the host\'s signer election)')
    this.sb = statebase
    this.src = src
    this.privateKey = privateKey
    this.base = new LabelBase(statebase, { src, ns }) // ns: see LabelBase — reserved name on a repo's base
    this.holds = holds
    this.lease = holds ? null : new Lease(statebase, { self, ttlMs })
    this._projected = new WeakMap() // feed core -> Set of label hexes already appended
    // The last projectTo run: { at, appended, coreLength, logLength }. A diagnostic (2026-10-14: re-signed labels
    // sat in the signed log while the gateway served the old ones, and nothing said whether projection had run).
    this.lastProjection = null
  }

  // A mod records a label intent (label/negate). Authority to WRITE is StateBase admission.
  label (uri, val, opts) { return this.base.label(uri, val, opts) }
  negate (uri, val, opts) { return this.base.negate(uri, val, opts) }
  ensure (uri, val, opts) { return this.base.ensure(uri, val, opts) } // idempotent: writes only on a change

  // Read the shared signed-label log (append-ordered), decoded. This is what the projection slice folds
  // into a served feed hypercore; consumers (favela readLabelFeed) get plain signed labels.
  async signedLog () {
    const map = await this.sb.list(SIGNED_NS + SEP)
    return Object.keys(map).sort().map((k) => decodeLabel(b4a.from(map[k], 'hex')))
  }

  // Project the shared __label__ log into a served feed hypercore (append-only, idempotent). Every
  // participant runs this against its OWN feed core; because __label__ is append-ordered + deterministic,
  // each participant's feed is byte-identical — so the labeler is serveable from ANY node (the
  // serve-membrane pattern). Consumers (favela readLabelFeed) get plain signed labels. Returns the count
  // appended this call.
  //
  // Appends every label in the log the core does not already hold (in key order), not "entries past the
  // core's length": with content keys a label replicated in later can sort BEFORE ones already projected.
  async projectTo (feedCore) {
    await feedCore.ready()
    let have = this._projected.get(feedCore)
    if (!have || have.size > feedCore.length) {
      have = new Set()
      for (let i = 0; i < feedCore.length; i++) have.add(b4a.toString(await feedCore.get(i), 'hex'))
      this._projected.set(feedCore, have)
    }
    const map = await this.sb.list(SIGNED_NS + SEP)
    let appended = 0
    for (const k of Object.keys(map).sort()) {
      const hex = map[k]
      if (have.has(hex)) continue
      await feedCore.append(b4a.from(hex, 'hex'))
      have.add(hex)
      appended++
    }
    this.lastProjection = { at: Date.now(), appended, coreLength: feedCore.length, logLength: Object.keys(map).length }
    return appended
  }

  // One tick: renew/claim the lease; if we hold it, sign intents not yet in the shared __label__ log
  // and append them. Returns the count appended (0 if not the lease-holder). Failover-safe.
  async tick (now = Date.now()) {
    const mine = this.holds ? await this.holds() : await this.lease.renewOrClaim(now)
    if (!mine) return 0
    const logMap = await this.sb.list(SIGNED_NS + SEP)
    // ⛔ Only a label carrying `ver` counts as emitted. The label spec requires ver: 1 on transfer and signs over
    // it; labels signed before atproto-identity 0.0.5 lack it and bsky ignores them. Such an entry is re-signed
    // below and lands on the SAME content key (signedKey has no ver), replacing the invalid one in place.
    const emitted = new Set()
    for (const k of Object.keys(logMap)) {
      const l = decodeLabel(b4a.from(logMap[k], 'hex'))
      if (l.ver === 1) emitted.add(stateOf(l))
    }
    let count = 0
    for (const i of await this.base.intents()) {
      if (emitted.has(stateOf(i))) continue
      const signed = materializeLabel(i, { src: this.src, privateKey: this.privateKey })
      await this.sb.set(signedKey(signed), b4a.toString(encodeLabel(signed), 'hex'))
      emitted.add(stateOf(i))
      count++
    }
    return count
  }
}

export { SIGNED_NS }