Critical
- Any pubkey could publish a kind:30078 roles document and become root of a
deployment, granting itself every capability and denying anyone. Roles are now
accepted only from the configured root, compared against the constructor
value rather than current authority state so a bad ingest cannot move the
goalposts. Reproduced end to end before the fix; regression test added.
- Administrative state now fails closed without signature verification. Without
it, event.pubkey is an unauthenticated claim, so an authorship check alone
proves nothing — a relay can put the root's key on an event the root never
wrote. trustUnsignedEvents opts in for local and test use.
High
- Report categories could evade moderation entirely: resolveThresholds used
bracket truthiness, so "constructor" resolved to the Object constructor (all
gates undefined) and "__proto__" to Object.prototype, both bypassing the
default threshold. Own-property lookups and null-prototype tables throughout
core — policy, evaluator, evidence, authority, presets.
- Live subscriptions bypassed signature verification entirely; an attacker only
had to wait for the subscription rather than the load.
- The default administrative query asked relays for every kind:30078 and
kind:30000 event in existence, with no author, identifier, or limit. Now
filtered to governance identifiers with a bounded limit.
Medium
- Cached state was trusted as authority: a cache naming a different root is now
rejected, so same-origin tampering cannot persist a takeover.
- Provider attribute changes silently stopped working after the first one, and
concurrent starts leaked a transport. Generation-guarded.
- Unbounded growth in the report store, mute store, decision cache, and the
transport's per-subscription dedupe set — all now capped.
Also restores the on-wire `d` identifier to <namespace>:governance:<scope>:v1.
The BitGate rename replaced "governance:" too broadly and silently changed the
event format; both encoder and parser changed together, so round-trip tests
never noticed. The identifier names the kind of document, not the library.
Adds docs/security.md and 21 regression tests reproducing each finding.
832 tests.
Renames the project and makes it as easy to adopt as BitLogin, which was the
stated bar and which it did not previously meet.
Rename (ADR 0005): @nostr-governance/* -> @bitgate/*, governance-* elements ->
bitgate-*, createGovernanceRuntime -> createBitGate. "Governance" reads as DAO
voting to this audience and named a category rather than a product; BitGate
names the benefit and joins the bit- family that BitVid and BitRoad compose
with. Domain types keep their names — the product is BitGate, the subject
matter is still governance. The migration plan is left as written; this ADR
records the change rather than rewriting history.
Drop-in distribution (ADR 0006): adopting BitGate meant supplying a transport,
authoring a policy, and wiring every element in JavaScript. Now:
- createRelayTransport(urls) — WebSocket transport with reconnection, backoff,
subscription replay, and cross-relay dedupe
- policy presets (social, commerce, admin-only) addressable by name; the engine
still carries no thresholds of its own
- <bitgate-provider> — configuration in markup, with descendants finding the
runtime through a bubbling context event rather than DOM walking, which
breaks across shadow boundaries
- npm run build:widget — a self-contained dist/bitgate.js
That last one came from running it in a browser rather than from a test. Bare
specifiers resolve in Node and vitest but not in a browser, so the widget
source worked everywhere except a plain static page — the one place it exists
to serve. The bundle is not committed: consumers build from reviewed source.
Also found by running it: the provider's ready event fires during element
upgrade, before a page that defines elements after parsing can attach a
listener, so it now exposes `ready` as a promise too.
Four bugs fixed along the way, three found by new tests and one by the browser:
admin state stayed silent when a roster change altered capabilities but no
denials; the relay transport sent every subscription twice on connect; the
commerce preset computed a meaningless visibility verdict at checkout and could
hide a listing from its own seller; <bitgate-status> threw on an unusable
decision.
759 tests; lint, typecheck, and both builds clean.
The performance fixture the plan asks for found a real defect on its first run:
evaluation was quadratic in state size. Both the snapshot and its fingerprint
were rebuilt for every target, so each of 5,000 evaluations walked every report
and every mute list. A 5,000-target pass took ~47 seconds.
- the runtime memoizes the materialized snapshot and its fingerprint until a
store reports a change
- evaluateMany computes the fingerprint once and shares it across the batch
- decisions are cached per (profile, target); report and mute changes
invalidate only the targets they touch, while administrative, trust, policy,
and viewer changes drop the whole cache
- cached decisions are deeply frozen, since the cache hands the same object to
every caller
Same pass now takes ~85ms cold, ~6ms warm.
Adds tests/performance (5,000 targets, 500 authors, 100 muters per author) and
tests/security (one case per enumerated requirement testable at this layer).
605 tests; lint, typecheck, and the declaration build pass from a cold checkout.