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
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-08-11 22:59:50 -04:00
1 parent 1abba23df3
commit cecd70c55f
5 files changed
+214 -7

No files matched your search

+17
View File
@@ -150,6 +150,23 @@ const decision = evaluateTarget(target, snapshot, { policy, surface: "registry"
if (!decision.transaction || decision.transaction.effect === "allow") install(); // gate a non-visual action
```
Don't hand-build `honoredModerators`. "Who is a moderator" is data that should
change without a redeploy, so let the root publish it as a signed kind-30000 set
and resolve it at runtime with `resolveAuthorityRoster` — it honors only
root-signed sets (and, with `signer`, one-role-delegates-another chains), and
its output spreads straight into `createAuthorityState`:
```js
import { resolveAuthorityRoster } from "@bitgate/nostr";
const authority = createAuthorityState(resolveAuthorityRoster(roleSetEvents, {
root: ROOT,
rosters: [
{ identifier: "bitgate:moderators", role: "moderator" }, // root names moderators
{ identifier: "bitgate:curators", role: "curator", signer: "moderator" }, // moderators name curators
],
}));
```
A decision has four dimensions, so `transaction` can block an install or
checkout while `visibility` still shows the listing with a warning — something a
`hidden` boolean can't express. [`examples/headless-quickstart.mjs`](examples/headless-quickstart.mjs)