Files
bitgate/packages/nostr/test/roster.test.js
T
thePR0M3TH3ANandClaude Opus 4.8 cecd70c55f nostr: resolveAuthorityRoster — moderator set as signed data
"Who is a moderator" should change without a redeploy, but apps had to
hand-assemble the createAuthorityState actors map. Add resolveAuthorityRoster
(@bitgate/nostr): turn signed kind-30000 "role set" events into the
actor->roles map, ready to spread into createAuthorityState.

It honors ONLY sets signed by an authorized key -- the root, or (with
`signer: <role>`) an actor already granted that role, giving one-role-
delegates-another chains in a single ordered pass. It keeps the latest set
per publisher (kind 30000 is replaceable) and ignores anyone unauthorized,
which is the security property: the roster is only as trustworthy as the key
that signed it. Signature verification stays the caller's job (inject
@bitgate/verify upstream).

Covered by roster.test.js: root-only grants, unauthorized-ignored,
delegation, replaceable-latest-wins, and that the output feeds
createAuthorityState. README headless section shows the pattern.

Addresses the "moderator-set as data" item of the Tessera feedback
(docs/TODO.md) -- previously marked PARTIAL.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BXQCvJPA6sVSQ5j8uBd9bB
2026-08-11 22:59:50 -04:00

84 lines
3.1 KiB
JavaScript

import { describe, expect, it } from "vitest";
import { createAuthorityState, getActorRoles } from "@bitgate/core";
import { ROLE_SET_KIND, resolveAuthorityRoster } from "../src/roster.js";
const ROOT = "a1".repeat(32);
const MOD_A = "b2".repeat(32);
const MOD_B = "c3".repeat(32);
const COUNCIL = "d4".repeat(32);
const CURATOR = "e5".repeat(32);
const OUTSIDER = "f6".repeat(32);
const setEvent = (pubkey, identifier, members, createdAt = 1000, id = "0".repeat(64)) => ({
kind: ROLE_SET_KIND,
pubkey,
created_at: createdAt,
id,
content: "",
tags: [["d", identifier], ...members.map((m) => ["p", m])],
});
describe("resolveAuthorityRoster", () => {
it("grants the root its role and members from a root-signed set", () => {
const { root, actors } = resolveAuthorityRoster([setEvent(ROOT, "bitgate:moderators", [MOD_A, MOD_B])], {
root: ROOT,
rosters: [{ identifier: "bitgate:moderators", role: "moderator" }],
});
expect(root).toBe(ROOT);
expect(actors[ROOT]).toEqual(["super_admin"]);
expect(actors[MOD_A]).toEqual(["moderator"]);
expect(actors[MOD_B]).toEqual(["moderator"]);
});
it("ignores a set signed by anyone but the root (the whole security property)", () => {
const { actors } = resolveAuthorityRoster([setEvent(OUTSIDER, "bitgate:moderators", [MOD_A])], {
root: ROOT,
rosters: [{ identifier: "bitgate:moderators", role: "moderator" }],
});
expect(actors[MOD_A]).toBeUndefined();
});
it("supports delegation: a granted role may publish a further set", () => {
const events = [
setEvent(ROOT, "bitgate:council", [COUNCIL]), // root names the council
setEvent(COUNCIL, "bitgate:curators", [CURATOR]), // a council member names curators
setEvent(OUTSIDER, "bitgate:curators", [OUTSIDER]), // outsider's set ignored
];
const { actors } = resolveAuthorityRoster(events, {
root: ROOT,
rosters: [
{ identifier: "bitgate:council", role: "council" },
{ identifier: "bitgate:curators", role: "curator", signer: "council" },
],
});
expect(actors[COUNCIL]).toEqual(["council"]);
expect(actors[CURATOR]).toEqual(["curator"]);
expect(actors[OUTSIDER]).toBeUndefined();
});
it("keeps the latest set per publisher (kind 30000 is replaceable)", () => {
const events = [
setEvent(ROOT, "bitgate:moderators", [MOD_A], 1000, "1".repeat(64)),
setEvent(ROOT, "bitgate:moderators", [MOD_B], 2000, "2".repeat(64)), // newer: replaces
];
const { actors } = resolveAuthorityRoster(events, {
root: ROOT,
rosters: [{ identifier: "bitgate:moderators", role: "moderator" }],
});
expect(actors[MOD_B]).toEqual(["moderator"]);
expect(actors[MOD_A]).toBeUndefined();
});
it("output feeds createAuthorityState directly", () => {
const roster = resolveAuthorityRoster([setEvent(ROOT, "bitgate:moderators", [MOD_A])], {
root: ROOT,
rosters: [{ identifier: "bitgate:moderators", role: "moderator" }],
});
const authority = createAuthorityState(roster);
expect(getActorRoles(MOD_A, authority)).toContain("moderator");
expect(getActorRoles(ROOT, authority)).toContain("super_admin");
});
});