// LabelSigner — the lease-holder's job for a co-authored labeler: materialize the LabelBase intent
// view into signed com.atproto.label records on the label feed. The analogue of signer.js's
// materialize(), but far simpler: a label feed is N INDEPENDENT signed leaves (no MST, no commit
// chain, no rev/prev). Every label is signed with the labeler's ONE #atproto_label key and carries
// src = the labeler DID — so N mods (the StateBase writers) produce ONE labeler identity on the wire.
//
// Because ECDSA here is RFC6979-deterministic, any lease-holder that signs the same intent produces
// byte-identical output — so a lease handoff mid-stream never forks the feed.
//
// This slice owns ONLY the pure materialize + the emit DIFF (which intents are new/changed since last
// emit). Lease-gating (only the holder emits) and the feed-append loop are wired in the daemon slice,
// exactly as signer.js deferred chain-advancement/firehose/lease to later slices.
import { buildLabel, signLabel } from 'atproto-identity/lib/labels.js'
const SEP = '\x00'
// Pure: one label intent { uri, val, neg, cts } -> one signed com.atproto.label under the labeler key.
export function materializeLabel (intent, { src, privateKey }) {
return signLabel(buildLabel({ src, uri: intent.uri, val: intent.val, cts: intent.cts, neg: intent.neg }), privateKey)
}
export class LabelSigner {
// src: the labeler DID (every label's src). privateKey: the shared #atproto_label signing key.
constructor ({ src, privateKey } = {}) {
if (!src || !privateKey) throw new Error('LabelSigner: src + privateKey required')
this.src = src
this.privateKey = privateKey
this._emitted = new Map() // "uri\x00val" -> "cts\x00neg" last emitted (so we don't re-sign unchanged state)
}
// Given the current LabelBase intents (LabelBase.intents()), return the signed labels to APPEND to
// the feed — those NEW or whose (cts, neg) state changed since the last emit. Updates internal emit
// state; the caller appends the returned labels to the feed core. Re-emitting the same state is
// suppressed (idempotent); the consumer's LWW-by-cts makes any re-emit harmless anyway.
emit (intents) {
const out = []
for (const i of intents) {
if (!i || !i.uri || !i.val) continue
const key = i.uri + SEP + i.val
const state = String(i.cts) + SEP + (i.neg ? '1' : '0')
if (this._emitted.get(key) === state) continue
this._emitted.set(key, state)
out.push(materializeLabel(i, { src: this.src, privateKey: this.privateKey }))
}
return out
}
}