Files
bitgate/docs/adr/0007-nip32-labelling.md
thePR0M3TH3AN 3220889f01 feat(nostr): NIP-32 labelling — the interop surface
BitGate read standard events but published a private vocabulary, making it an
island. NIP-32 (kind 1985) is the shared moderation vocabulary; BitGate now
reads and writes it. See ADR 0007.

The design keeps flexibility central, since the goal is a reusable building
block. A label is (namespace, value, target) and carries no built-in meaning —
"deny", "nsfw", and "MIT" are the same kind of object. What a label means is a
policy decision an application supplies via labelMapping { namespace,
denyValues, allowValues }. The codec itself is vocabulary-free; decodeLabels
returns every label regardless of namespace for generic use.

A matching label becomes a contribution authored by the labeller, gated
identically: it denies someone only if the labeller holds the capability in the
roster, exactly like a community source. An untrusted labeller's deny does
nothing, and labels fail closed without verification like any administrative
event.

- consume: ingestEvent maps kind 1985; loadLabels(labellers) fetches them
- emit: commands.publishLabel(target, value), gated by the same capability as
  denying, so the shared form is no weaker than the private one
- the widget bundle re-exports the label API for the drop-in path

Validated against 40 real kind:1985 events from public relays (ISO-639-1
language labels among them): all decoded with targets and namespaces, zero
throws. 897 tests; cold gates green; audit clean.
2026-08-10 10:01:34 -04:00

55 lines
2.5 KiB
Markdown

# ADR 0007 — NIP-32 labelling as the interop surface
**Status:** accepted
## Context
BitGate read two standard event kinds (NIP-56 reports, NIP-51 mutes) but
published its moderation output as a private vocabulary — contribution lists
under a `bitgate`-namespaced `d` identifier. Other clients could not act on a
BitGate deployment's moderator decisions, and BitGate could not act on anyone
else's. For a project whose stated goal is being a reusable building block,
that made it an island.
NIP-32 (kind 1985) is the shared vocabulary for moderation labels: an `L`
namespace, an `l` value, and `e`/`p`/`a` targets.
## Decision
Treat a label as **just another wire format for a contribution**, subject to
the identical capability gate.
A NIP-32 label is `(namespace, value, target)`. A BitGate contribution is
`(kind, target)` that only takes effect if its author holds the matching
capability. The bridge is `labelsToContributions(labels, mapping)`, where the
application supplies the namespace it trusts and the label values that mean
deny or allow. A matching label becomes a contribution authored by the
labeller, with a `source` marker, and reduces exactly like a community source:
it denies someone only if the labeller holds the capability in the roster.
The codec itself carries no vocabulary. "MIT", "nsfw", and "spam" are all
labels; whether any of them means denial is a policy decision that lives in the
caller's `labelMapping`, not in the codec. This is what keeps it a building
block: an application brings its own namespace and its own words.
- **Consume:** `ingestEvent` maps kind 1985 through the configured
`labelMapping`; `loadLabels(labellers)` fetches them.
- **Emit:** `commands.publishLabel(target, value)`, gated by the same
capability as denying the target, so the shared form is no weaker than the
private one.
## Consequences
A BitGate deployment can honour a third-party labeller by adding them to the
roster with a contribution capability — no new trust mechanism, because labels
reuse the one that already exists. Other clients can read a deployment's
`deny` labels. The two directions round-trip.
Because labels are administrative state, they fail closed without signature
verification, like every other administrative document.
`r` (relay) and `t` (topic) label targets are dropped rather than mapped: they
have no place in an allow/deny model, and representing them as something else
would be worse than ignoring them. An application that cares about topic labels
can consume the codec's generic `decodeLabels` output directly.