Files
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

63 lines
1.9 KiB
Markdown

# Interoperable moderation with NIP-32
BitGate reads and writes NIP-32 labels (kind 1985), so a deployment's
moderators can be honoured by other Nostr clients, and BitGate can act on
labels published by others.
## The model
A label is `(namespace, value, target)`. It carries no built-in meaning — a
`"deny"` label and an `"nsfw"` label are the same kind of object. What a label
means to *your* application is a policy decision you supply:
```js
createBitGate({
namespace: "myapp",
labelMapping: {
namespace: "com.myapp.moderation", // only labels in this namespace map
denyValues: ["deny", "spam", "scam"], // your vocabulary for "hide this"
allowValues: ["trusted"],
},
…
});
```
A label matching that mapping becomes a **contribution**, authored by the
labeller. It denies someone only if the labeller holds the capability in your
roster — exactly like a community source. An untrusted labeller's `deny` does
nothing.
## Consuming a third-party labeller
Add the labeller to the roster with a contribution capability, then fetch their
labels:
```js
runtime.admin.setRoles({
root: ROOT,
roles: { external_labeller: ["contribute-user-deny", "contribute-event-deny"] },
actors: { [ROOT]: ["super_admin"], [LABELLER_PUBKEY]: ["external_labeller"] },
});
await runtime.loadLabels([LABELLER_PUBKEY]);
```
## Publishing labels
A moderator publishes a label with the same capability that lets them deny
directly:
```js
await commands.publishLabel({ type: "user", pubkey }, "deny");
await commands.publishLabel({ type: "event", id }, "nsfw", { content: "graphic" });
```
Other clients that understand your namespace can then act on it.
## Generic use
The codec is not BitGate-specific. `decodeLabels(event)` returns every label
(namespace, value, targets) regardless of vocabulary, so an application can
consume topic or licence labels directly without going through the allow/deny
mapping.