OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / lib / labeler.js

62 lines · javascript ref:
// favela labeler feed — the single-authority labeler (DESIGN §8): a signed hypercore of
// com.atproto.label records on its own key. The producer SIGNS labels with its secp256k1 key;
// consumers replicate the feed by key and VERIFY each label against the labeler's did:key. Simpler
// than a repo (no MST/CAR/commit chain) — a label feed is just N independently-signed leaves.
//
// Multi-mod (co-authored labels) is a later variant — an autobase room + a signer lease (DESIGN §8),
// same as the peer's writer model. This is the single-mod first cut.
import { buildLabel, signLabel, verifyLabel, encodeLabel, decodeLabel } from 'atproto-identity/lib/labels.js'
import { keyFromPrivate } from 'atproto-identity/lib/atproto-identity.js'

export class Labeler {
  // corestore: for the feed core. privateKey: the labeler's raw secp256k1 signing key. src: the
  // labeler's DID published in each label (defaults to its did:key when no did:plc is minted).
  constructor (corestore, { privateKey, src, name = 'label-feed' } = {}) {
    this._key = keyFromPrivate(privateKey) // { privateKey, publicKey, didKey }
    this.src = src || this._key.didKey
    this.core = corestore.get({ name, valueEncoding: 'binary' })
  }

  async ready () { await this.core.ready(); return this }
  get feedKey () { return this.core.key } // share with consumers to replicate the feed
  get didKey () { return this._key.didKey } // share with consumers to verify signatures

  // Append a signed label. uri = subject (an AT-URI or account DID); val = label value; neg retracts a
  // prior (src,uri,val); cts defaults to now. Returns the signed label.
  async label (uri, val, { neg, cts } = {}) {
    const signed = signLabel(buildLabel({ src: this.src, uri, val, cts: cts || new Date().toISOString(), neg }), this._key.privateKey)
    await this.core.append(encodeLabel(signed))
    return signed
  }

  async close () { await this.core.close() }
}

// Consume a labeler feed: decode + VERIFY each label against `didKey` and addLabel the valid ones into
// the LabelStore. Downloads the whole feed then live-tails. `core` is a read replica opened by the
// labeler's feedKey (the caller replicates it over the swarm). Returns a stop() function.
export function readLabelFeed (core, didKey, store, { onError, onLabel } = {}) {
  let stopped = false
  ;(async () => {
    await core.ready()
    try { core.download({ start: 0, end: -1 }) } catch {}
    let i = 0
    while (!stopped) {
      if (i < core.length) {
        try {
          const label = decodeLabel(await core.get(i))
          if (verifyLabel(label, didKey)) { const changed = await store.addLabel(label); if (changed && onLabel) onLabel(label) }
        } catch (e) { if (onError) onError(e) }
        i++
        continue
      }
      await new Promise((resolve) => {
        const on = () => { core.off('append', on); resolve() }
        core.on('append', on)
        if (stopped) { core.off('append', on); resolve() }
      })
    }
  })().catch((e) => { if (onError) onError(e) })
  return () => { stopped = true }
}