Closes the four spec gaps from the audit, verified against the NIPs repo.
NIP-65 (kind 10002) — the one that was actually breaking things. Mute lists are
published by each contact to their own write relays, so querying a fixed relay
set finds only the subset who happen to write where this deployment reads. The
trust graph was silently under-fed and counts came out low, which looks like
"nobody muted them" rather than like a bug. Adds a relay-list codec, per-author
grouping with a fallback for accounts that have published no list, and
on-demand relay connections in the transport so an outbox query can reach
relays that were never configured.
NIP-51 private entries — the viewer's own private mutes are now decrypted
through their signer (NIP-44) and applied as viewer mutes. Someone who mutes
privately previously got no effect at all. The privacy boundary is asymmetric
and enforced at the codec: another account's private list is never decoded,
and the check lives there rather than in the caller's discipline.
NIP-02 (kind 3) — the runtime can build the trust graph itself, so
<bitgate-provider> no longer needs a host to supply a follow list. Set
trust="manual" to opt out.
NIP-56 vocabulary — SOCIAL_POLICY declared thresholds for "harassment", which
is not a NIP-56 type, so no standard client could ever trigger it. Replaced
with the defined vocabulary and asserted in tests. Commerce keeps its
marketplace-specific categories, now documented as deliberate, with defaults so
unclassified reports are never silently dropped.
Two bugs found by the new tests:
- checkout had no default threshold, so a flood of "other" reports did nothing
at the one place money changes hands. Now earns review, never auto-denial.
- describe() spread diagnostics after a derived field, so a stale zero shadowed
the live relay-list count.
Adds docs/nips.md recording coverage, deliberate choices, and remaining gaps —
NIP-32 labelling being the largest.
811 tests.
A sibling of the BitLogin design system rather than a twin: the same
ink/paper/brass foundation and serif display, with verdigris — the patina brass
takes as it ages — where BitLogin uses its violet seal.
- index: the signature visual is the product's whole argument, one listing
reaching four surfaces and getting four different verdicts
- docs: five-step integration, elements table, presets, common mistakes
- demo: the real engine running in the browser against an in-memory transport,
so nothing is fetched or published. Add reports, restrict the seller, switch
moderator identities, and watch every surface re-decide.
The widget bundle now re-exports the runtime and core entry points, because a
static page has no package manager — anything it might need has to be reachable
from the one file it loads.
Also fixes the site build reusing a stale widget bundle. It only rebuilt when
the file was missing, which is precisely how a broken demo ships: the page
loads, the module resolves, and an export added since the last build is
silently absent. Caught by running it.
vercel.json mirrors BitLogin's security headers, with connect-src widened to
wss: for relay traffic.
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.
A full-suite run failed intermittently, roughly one in six. Two causes, both
mine, both in test code rather than the library.
The performance fixture's setup repeated the same 100 mute-list writes 50 times
over with identical data — 5,000 calls where 100 give the same fixture — which
made construction expensive enough that individual tests occasionally exceeded
their timeout under load. The failures looked like assertion failures on
deterministic checks, which is what made them confusing. The fixture's shape
(5,000 targets, 500 authors, 100 muters per author, multiple categories and
contributors) is now asserted directly so it cannot silently degrade again.
The wall-clock assertion is gone. It measured the machine as much as the code —
85ms standalone versus 1606ms under vitest on the same commit — so it was flaky
rather than informative. Replaced with structural assertions that the snapshot
and its record maps are materialized once per batch, which is precisely the
regression that made a 5,000-target pass take ~47 seconds, and is deterministic.
Widget tests polled with vi.waitFor and its ~1s timeout. The async elements now
expose the in-flight promise as `pending`, so tests await completion directly.
That is also useful to hosts wanting to disable surrounding UI mid-action.
Six consecutive full-suite runs, 690 tests each, no failures.
Follows bitlogin's layering: the engine stays headless and the widget is a
consumer of the published API, never a dependency of it. Building it was also a
test of that API — an element needing something unexposed would have been a
finding, the way the commerce example was.
Viewer elements: governance-veil (blur on restrict, disclosure on hide, warning
on warn), governance-report, governance-status. Moderator elements:
governance-capabilities, governance-action, governance-admin-panel.
Deliberate behaviors:
- hidden content leaves the accessibility tree rather than being visually
covered, so a screen reader does not read out what the viewer must not see
- a hidden decision renders a disclosure, not nothing: content that silently
vanishes is indistinguishable from a bug and leaves no route to appeal
- refused moderator actions stay visible and name the missing capability. A
hidden button teaches nothing about the authority model, and the command
layer enforces the same check independently — the UI is an explanation,
never the gate
- no element computes policy; wording maps from stable reason identifiers and
is overridable per application
Found while building it: GovernanceAdminStore emitted only when reduced denial
state changed, so revoking a role that denied nobody announced nothing, leaving
capability-gated UI and the decision cache stale.
The root tsconfig deliberately omits the DOM lib — that absence is what keeps
core, nostr, and runtime from reaching for browser globals — so the widget
carries its own DOM-aware config rather than relaxing the guarantee globally.
687 tests; lint, typecheck, and the declaration build pass from a cold checkout.