Outside review: BitGate is a policy-evaluation engine (actor/object/context/
evidence -> permitted?), broader than moderation. Record it as a positioning/
docs task, explicitly guarded against becoming a scope-creep invitation --
the engine is already general.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BXQCvJPA6sVSQ5j8uBd9bB
"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
The `ranking` dimension is the soft one — "still show this, but lower" — and
integrators reinvented turning it into a feed score. Show it instead.
Add examples/feed-ranking.mjs (runnable): it reads decision.ranking
({ effect: "normal" | "downrank", weight }) and turns it into a multiplier
on the app's own feed score (base ** weight for a downrank). Two authors
that led on base score get downranked by trusted spam reports and drop below
a clean lower-base author -- without being hidden, since visibility is a
separate dimension (capped at "restrict" on a feed). The multiplier lives in
the example, not the engine: core ships no such numbers on purpose.
Pinned by tests/feed-ranking.test.js (downrank never hides on a feed; a
high-base downranked author reorders below a clean one; a clean author is
untouched at x1). README headless section points to it.
Addresses the "BitGate feeding a feed ranking" item of the Tessera feedback
(docs/TODO.md) -- the last open BitGate item.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BXQCvJPA6sVSQ5j8uBd9bB
A no-build static site couldn't vendor BitGate the way it vendors a single
dependency: bare @bitgate/* specifiers don't resolve in a browser, and the
headless path spans two packages (the evaluator in core, the kind-1985 label
codec in nostr). The widget bundle solved this for the DOM path only.
Add @bitgate/standalone (packages/standalone): an entry re-exporting core +
nostr, esbuilt into a single self-contained dist/bitgate.js (~44 KB). It's
the same canonical code the npm packages ship, with specifiers inlined, and
dependency-free -- @bitgate/verify's @noble deps are intentionally not pulled
in, so the bundle carries no third-party code. Wired into `npm run build` via
build:standalone.
Verified: no bare imports in the output (self-contained), and a smoke test
composes a full label -> decision flow from the single entry. README Packages
table and headless section document it.
Addresses the "standalone ESM single-file" item of the Tessera integration
feedback (docs/TODO.md).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BXQCvJPA6sVSQ5j8uBd9bB
Tessera's #1 reason for not adopting BitGate: the on-ramp they found was
the <bitgate-provider>/<bitgate-veil> widgets, so they never saw the
"just the engine + events over Nostr" path, and — not finding a canonical
decision event — invented their own kind-33604 shape.
Both concerns were already solved, just not surfaced:
- NIP-32 label events (kind 1985) ARE the canonical signed decision format;
@bitgate/nostr already ships the codec and docs/labels.md documents interop.
No 33604 needed.
- The headless path is four pure functions, no runtime transport, no DOM.
Add examples/headless-quickstart.mjs (runnable): moderators publish signed
kind-1985 deny labels -> decodeLabels -> labelsToContributions -> reduce
under an honored moderator set -> evaluateTarget against a plugin-registry
policy. It shows exactly the transaction-dimension case the feedback names —
a malware plugin stays visible (warn) but not installable (transaction
deny), where a `hidden` boolean can't reach.
Add a "Headless, straight from NIP-32 labels" README subsection stating
labels are the canonical wire format and linking the example and
docs/labels.md. Pin the behavior in tests/headless-quickstart.test.js
(clean installable; malware visible-but-not-installable; an out-of-set
labeller's deny ignored).
Addresses the "no just-the-engine path", "decision format is ours", and
"headless quickstart" items of the Tessera feedback (docs/TODO.md).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BXQCvJPA6sVSQ5j8uBd9bB
The engine already honored requireAllowlist + allowlistMiss, and the
commerce example configured a seller allowlist by hand -- but there was no
reusable, named "only show allowlisted publishers" preset, the symmetric
twin of the denylist. Integrators (Tessera) punted on allowlist mode for
lack of one.
Add ALLOWLIST_POLICY (registry name "allowlist"): a publisher is shown only
when an authorized actor has contributed a user-allow for them, and every
unlisted publisher misses and is hidden across all four dimensions.
Administrative denials still apply on top, and protected actors (root,
moderators) are never gated -- the people who curate the list can always
see. Populate userAllow exactly as the denylist: signed contributions
resolved from relays (e.g. NIP-32 allow labels).
Exported from @bitgate/core, registered in POLICY_PRESETS, documented in the
README, and covered by presets.test.js (unlisted hidden, allowlisted shown,
protected bypasses).
Addresses the "allowlist / whitelist mode" item of the Tessera integration
feedback (docs/TODO.md).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BXQCvJPA6sVSQ5j8uBd9bB