feat: rename to BitGate and close the drop-in gap

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.
This commit is contained in:
thePR0M3TH3AN committed 2026-08-09 19:58:45 -04:00
1 parent 7f70a64d61
commit adeba151b8
63 files changed
+2774 -401

No files matched your search

@@ -15,7 +15,7 @@ The core defines the *vocabulary* — target types, capabilities, effect ladders
reason identifiers, aggregation and precedence rules — and no thresholds.
Applications supply a `PolicyDefinition` of named profiles with category
thresholds. The characterized reference values live in
`@nostr-governance/bitvid-compat` as one application's profile.
`@bitgate/bitvid-compat` as one application's profile.
`NEUTRAL_POLICY` is the fallback: it enforces administrative denial and applies
no trust thresholds, so the generic engine never hides content merely because
+2 -2
View File
@@ -11,8 +11,8 @@ meant to have no dependencies and no encoding opinions.
## Decision
`@nostr-governance/core` accepts 64-character hex only. Decoding lives in
`@nostr-governance/nostr` (`normalizePubkeyInput`, `normalizeEventIdInput`),
`@bitgate/core` accepts 64-character hex only. Decoding lives in
`@bitgate/nostr` (`normalizePubkeyInput`, `normalizeEventIdInput`),
and codecs normalize at ingest, so the core only ever sees hex.
## Consequences
+52
View File
@@ -0,0 +1,52 @@
# ADR 0005 — Rename to BitGate
**Status:** accepted (supersedes the repository-name decision in §5.1 of the
migration plan)
## Context
The project shipped as `nostr-governance`, a name chosen while it was still a
specification exercise. Three problems emerged once it became a product.
"Governance" means something specific and different to this audience. In Nostr
and crypto circles it reads as DAO voting and token-weighted proposals. The
project is a moderation and access-control policy engine; the word misdirects.
It named a category rather than a product. It could not be verbed, and it gave
no signal that BitVid and BitRoad compose with it.
It named the mechanism rather than the benefit. The sibling project BitLogin is
not called "Nostr Portable Auth" — it is called BitLogin, because *login* is
what people want.
## Decision
Rename to **BitGate**, joining the existing `bit-` family (BitVid, BitRoad,
BitLogin, BitUnlock). Gates open and close, which maps onto the allow/deny
ladder, and "gate your app" is a natural verb.
- npm scope: `@nostr-governance/*` → `@bitgate/*`
- custom elements: `governance-*` → `bitgate-*`
- custom events: `governance:*` → `bitgate:*`
- storage key prefix: `nostr-governance:` → `bitgate:`
- consumer entry points: `createGovernanceRuntime` → `createBitGate`,
`defineGovernanceElements` → `defineBitGateElements`
Domain vocabulary is unchanged. `GovernanceDecision`, `GovernanceTarget`, and
`GovernanceRuntime` keep their names: the product is BitGate, the subject
matter is still governance.
The migration plan and the `.hermes` execution plan are left as written. They
record decisions as they were made; this ADR records the change rather than
rewriting history.
## Consequences
Done at 0.1.0, unpublished, with no external consumers — the cheapest this
could ever be. The storage key prefix changed, which would have invalidated
caches had anything been deployed.
The `bit-` prefix signals membership in one author's suite, which is a mild
cost for infrastructure intended for wider adoption. BitLogin carries the same
cost and it has not hurt it; the composability signal is worth more, especially
since BitLogin and BitGate are designed to be used together.
+50
View File
@@ -0,0 +1,50 @@
# ADR 0006 — Ship a built, self-contained widget
**Status:** accepted
## Context
BitGate was a well-factored SDK that was not droppable into a page. Adopting it
required supplying a transport adapter, authoring a policy definition, and
wiring every element in JavaScript. BitLogin sets a much higher bar: copy two
files, add one script tag and one element.
Testing the widget in a real browser exposed a failure no unit test could
catch. `packages/widget/src/index.js` imports `@bitgate/core` and
`@bitgate/runtime` as bare specifiers. Node and the test runner resolve those
through `node_modules`; a browser cannot resolve them at all. The source worked
everywhere except the one place the widget exists to serve.
## Decision
Three pieces close the gap.
1. **`createRelayTransport(urls)`** — a small WebSocket transport, so nobody
writes an adapter to get started. It speaks enough of the protocol to load
state, subscribe, and publish; it is not a general-purpose relay client.
2. **Policy presets** — `social`, `commerce`, `admin-only`, addressable by name.
The engine still carries no thresholds: these are application policies that
happen to ship in the box, and every number in them is documented as a
starting point.
3. **`<bitgate-provider>`** — configuration in markup. Descendants find the
runtime by firing a bubbling context-request event rather than walking up
the DOM, which would break across shadow boundaries.
Plus a build step (`npm run build:widget`) producing a single self-contained
`dist/bitgate.js` via esbuild.
## Consequences
A static page needs one script tag and one element, matching BitLogin.
The build output is deliberately **not** committed. Consumers build from
reviewed source and self-host the result, as BitLogin instructs, because a
moderation widget is exactly the kind of artifact where reading the source
matters and trusting a prebuilt minified blob does not.
The provider exposes `ready` as a promise as well as an event. The event fires
during element upgrade — before a page that calls `defineBitGateElements()`
after parsing could attach a listener — so an event alone was a race that
silently lost. This was found by running the demo, not by a test.
+1 -1
View File
@@ -35,7 +35,7 @@ Trusted spam-report hide threshold: 5
Trusted mute validity window: 60 days
```
These live in `@nostr-governance/bitvid-compat` as one application's profile,
These live in `@bitgate/bitvid-compat` as one application's profile,
not as engine defaults.
Provenance: fixtures are authored from the reference source and its documented
+6 -6
View File
@@ -25,7 +25,7 @@ revision was bad. Most objects want both.
## 2. Write an adapter
```js
import { createApplicationAdapter } from "@nostr-governance/core";
import { createApplicationAdapter } from "@bitgate/core";
export const productAdapter = createApplicationAdapter({
applicationId: "my-shop",
@@ -64,7 +64,7 @@ A policy is a set of named **profiles**, one per surface. Profiles read the same
evidence and are free to reach different conclusions.
```js
import { createPolicyDefinition } from "@nostr-governance/core";
import { createPolicyDefinition } from "@bitgate/core";
export const policy = createPolicyDefinition({
id: "my-shop",
@@ -145,13 +145,13 @@ A signer (`getPublicKey`, `signEvent`) and storage (`read`, `write`, `remove`)
follow the same injection pattern. In-memory versions ship for testing:
```js
import { createMemoryTransport, createMemoryStorage } from "@nostr-governance/runtime";
import { createMemoryTransport, createMemoryStorage } from "@bitgate/runtime";
```
## 5. Wire the pipeline
```js
import { collectTargets, evaluateObject } from "@nostr-governance/core";
import { collectTargets, evaluateObject } from "@bitgate/core";
runtime.setViewer(viewerPubkey); // do this FIRST — it clears viewer state
runtime.trust.setContacts(followList); // then seed trust
@@ -178,7 +178,7 @@ seed contacts, blocks, and overrides *after* it, never before.
Roles are application-defined bundles over a fixed capability vocabulary:
```js
import { createRoleDefinition } from "@nostr-governance/core";
import { createRoleDefinition } from "@bitgate/core";
const roles = {
listing_moderator: createRoleDefinition("listing_moderator", [
@@ -227,7 +227,7 @@ systems are deliberately separate.
The conformance harness takes JSON fixtures and asserts decisions:
```js
import { runConformanceCase } from "@nostr-governance/testing/conformance";
import { runConformanceCase } from "@bitgate/testing/conformance";
const { passed, mismatches } = runConformanceCase(fixture, policy);
```
+5 -5
View File
@@ -23,11 +23,11 @@ Until a lawyer-ready SPDX audit is done, treat the repository as:
| Package | Intention |
| --- | --- |
| `@nostr-governance/core` | Clean API surface + pure evaluator. Avoid BitVid names. |
| `@nostr-governance/nostr` | Codecs. Legacy BitVid codec is provenance-sensitive. |
| `@nostr-governance/runtime` | Transport/storage orchestration. |
| `@nostr-governance/bitvid-compat` | Explicit BitVid-facing adapter. Most provenance-exposed. |
| `@nostr-governance/testing` | Fakes and fixture loaders. |
| `@bitgate/core` | Clean API surface + pure evaluator. Avoid BitVid names. |
| `@bitgate/nostr` | Codecs. Legacy BitVid codec is provenance-sensitive. |
| `@bitgate/runtime` | Transport/storage orchestration. |
| `@bitgate/bitvid-compat` | Explicit BitVid-facing adapter. Most provenance-exposed. |
| `@bitgate/testing` | Fakes and fixture loaders. |
## Rules