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

230 lines
7.7 KiB
JavaScript

// NIP-32 label ingestion in the runtime.
//
// The property that matters most: a label is a contribution, so it is subject
// to the identical capability gate. An untrusted labeller's "deny" must do
// nothing.
import { describe, expect, it, vi } from "vitest";
import { createPolicyDefinition } from "@bitgate/core";
import { LABEL_KIND, encodeLabel } from "@bitgate/nostr";
import { createBitGate, createCommands, createMemoryTransport } from "@bitgate/runtime";
const ROOT = "a1".repeat(32);
const MODERATOR = "b2".repeat(32);
const STRANGER = "f6".repeat(32);
const VICTIM = "d4".repeat(32);
const NOW = 1_750_000_000;
const POLICY = createPolicyDefinition({
id: "labels",
version: "1.0.0",
defaultProfile: "feed",
profiles: {
feed: {
name: "feed",
administrativeDeny: { visibility: "hide", interaction: "deny" },
reports: {},
mutes: {},
},
},
});
function makeRuntime(options = {}) {
const runtime = createBitGate({
applicationId: "labels",
namespace: "app",
root: ROOT,
transport: createMemoryTransport(),
policy: POLICY,
now: () => NOW,
trustUnsignedEvents: true,
labelMapping: { namespace: "app.mod", denyValues: ["deny"], allowValues: ["allow"] },
...options,
});
runtime.admin.setRoles({
root: ROOT,
actors: { [ROOT]: ["super_admin"], [MODERATOR]: ["moderator"] },
});
return runtime;
}
const labelEvent = (pubkey, value, target, id = "01") => ({
id: id.repeat(32),
pubkey,
created_at: NOW,
...encodeLabel({ value, namespace: "app.mod", targets: [target] }),
});
describe("label ingestion", () => {
it("denies a user when an authorized moderator labels them", () => {
const runtime = makeRuntime();
const accepted = runtime.ingestEvent(labelEvent(MODERATOR, "deny", { type: "user", pubkey: VICTIM }));
expect(accepted).toBe(true);
expect(runtime.admin.state.userDeny.has(`user:${VICTIM}`)).toBe(true);
expect(runtime.evaluate({ type: "user", pubkey: VICTIM }, { profile: "feed" }).visibility.effect).toBe(
"hide",
);
});
it("does nothing when the labeller lacks the capability", () => {
// The whole safety story: a label is not privileged. A stranger's deny
// label reduces to nothing because they hold no capability.
const runtime = makeRuntime();
runtime.ingestEvent(labelEvent(STRANGER, "deny", { type: "user", pubkey: VICTIM }));
expect(runtime.admin.state.userDeny.has(`user:${VICTIM}`)).toBe(false);
});
it("stops honouring a labeller's labels once their role is revoked", () => {
const runtime = makeRuntime();
runtime.ingestEvent(labelEvent(MODERATOR, "deny", { type: "user", pubkey: VICTIM }));
expect(runtime.admin.state.userDeny.size).toBe(1);
runtime.admin.setRoles({ root: ROOT, actors: { [ROOT]: ["super_admin"] } });
expect(runtime.admin.state.userDeny.size).toBe(0);
});
it("ignores labels outside the configured namespace", () => {
const runtime = makeRuntime({ labelMapping: { namespace: "other", denyValues: ["deny"] } });
expect(runtime.ingestEvent(labelEvent(MODERATOR, "deny", { type: "user", pubkey: VICTIM }))).toBe(
false,
);
expect(runtime.admin.state.userDeny.size).toBe(0);
});
it("records the labeller as a community source", () => {
const runtime = makeRuntime();
runtime.ingestEvent(labelEvent(MODERATOR, "deny", { type: "user", pubkey: VICTIM }));
expect(runtime.admin.state.communitySources.get(`user:${VICTIM}`)).toBeDefined();
});
it("maps an event-deny label", () => {
const runtime = makeRuntime();
const eventId = "1b".repeat(32);
runtime.ingestEvent(labelEvent(MODERATOR, "deny", { type: "event", id: eventId }));
expect(runtime.admin.state.eventDeny.has(`event:${eventId}`)).toBe(true);
});
it("fails closed without a verifier, like any administrative event", () => {
const runtime = createBitGate({
applicationId: "labels",
namespace: "app",
root: ROOT,
transport: createMemoryTransport(),
policy: POLICY,
now: () => NOW,
labelMapping: { namespace: "app.mod", denyValues: ["deny"] },
});
runtime.admin.setRoles({ root: ROOT, actors: { [MODERATOR]: ["moderator"] } });
expect(runtime.ingestEvent(labelEvent(MODERATOR, "deny", { type: "user", pubkey: VICTIM }))).toBe(
false,
);
});
});
describe("loadLabels", () => {
it("fetches and applies a labeller's labels", async () => {
const transport = createMemoryTransport([
labelEvent(MODERATOR, "deny", { type: "user", pubkey: VICTIM }),
]);
const runtime = createBitGate({
applicationId: "labels",
namespace: "app",
root: ROOT,
transport,
policy: POLICY,
now: () => NOW,
trustUnsignedEvents: true,
labelMapping: { namespace: "app.mod", denyValues: ["deny"] },
});
runtime.admin.setRoles({ root: ROOT, actors: { [MODERATOR]: ["moderator"] } });
expect(await runtime.loadLabels([MODERATOR])).toBe(1);
expect(runtime.admin.state.userDeny.has(`user:${VICTIM}`)).toBe(true);
});
it("does nothing for an empty labeller list", async () => {
expect(await makeRuntime().loadLabels([])).toBe(0);
});
});
describe("publishLabel command", () => {
function signerFor(pubkey) {
return {
async getPublicKey() {
return pubkey;
},
async signEvent(template) {
return { ...template, id: "ff".repeat(32), pubkey, sig: "00".repeat(64) };
},
};
}
function commandsAs(pubkey) {
const runtime = createBitGate({
applicationId: "labels",
namespace: "app",
root: ROOT,
transport: createMemoryTransport(),
signer: signerFor(pubkey),
policy: POLICY,
now: () => NOW,
labelMapping: { namespace: "app.mod", denyValues: ["deny"] },
});
runtime.admin.setRoles({ root: ROOT, actors: { [MODERATOR]: ["moderator"] } });
return { runtime, commands: createCommands(runtime) };
}
it("publishes a label for an authorized moderator", async () => {
const { commands } = commandsAs(MODERATOR);
const result = await commands.publishLabel({ type: "user", pubkey: VICTIM }, "deny");
expect(result.ok).toBe(true);
expect(result.event?.kind).toBe(LABEL_KIND);
});
it("refuses a labeller who lacks the capability", async () => {
const { commands } = commandsAs(STRANGER);
const result = await commands.publishLabel({ type: "user", pubkey: VICTIM }, "deny");
expect(result.code).toBe("not-authorized");
});
it("defaults the namespace to the runtime's label mapping", async () => {
const { commands } = commandsAs(MODERATOR);
const result = await commands.publishLabel({ type: "user", pubkey: VICTIM }, "deny");
expect(result.event?.tags).toContainEqual(["L", "app.mod"]);
});
it("rejects a malformed target", async () => {
const { commands } = commandsAs(MODERATOR);
const result = await commands.publishLabel(
/** @type {any} */ ({ type: "user", pubkey: "bad" }),
"deny",
);
expect(result.code).toBe("invalid-target");
});
it("round-trips: a published label denies when read back", async () => {
const { commands, runtime } = commandsAs(MODERATOR);
const result = await commands.publishLabel({ type: "user", pubkey: VICTIM }, "deny");
const fresh = createBitGate({
applicationId: "labels",
namespace: "app",
root: ROOT,
transport: createMemoryTransport(),
policy: POLICY,
now: () => NOW,
trustUnsignedEvents: true,
labelMapping: { namespace: "app.mod", denyValues: ["deny"] },
});
fresh.admin.setRoles({ root: ROOT, actors: { [MODERATOR]: ["moderator"] } });
fresh.ingestEvent(/** @type {any} */ ({ ...result.event, pubkey: MODERATOR }));
expect(fresh.admin.state.userDeny.has(`user:${VICTIM}`)).toBe(true);
expect(runtime).toBeDefined();
});
});