Files
bitgate/packages/widget/README.md
T
thePR0M3TH3AN adeba151b8 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.
2026-08-09 19:58:45 -04:00

5.6 KiB

@bitgate/widget

Drop-in custom elements. Framework-agnostic, no bundler required by consumers, and built strictly on the published API.

Install

npm install && npm run build:widget

Copy packages/widget/dist/bitgate.js (and its .map) onto your own origin. The bundle is deliberately not committed: build it from reviewed source rather than trusting a prebuilt minified blob for a moderation widget.

The one-element version

<script type="module" src="/vendor/bitgate/bitgate.js"></script>

<bitgate-provider relays="wss://relay.example" root="<root-pubkey>" policy="social">
  <bitgate-veil profile="feed" target-user="<author-pubkey>">
    <img src="thumbnail.jpg" alt="" />
  </bitgate-veil>
</bitgate-provider>

<bitgate-provider> builds the runtime, loads administrative state, and shares both with every descendant. Nothing else to wire.

Provider attributes

Attribute Meaning
relays Comma-separated relay URLs (required)
root Root administrator pubkey, from your deployment config
policy social, commerce, or admin-only (default)
viewer Viewer pubkey, if known at render time
application / namespace Storage and event namespacing

Target attributes

Any target-bearing element reads its target from markup:

<bitgate-veil target-user="<pubkey>">…</bitgate-veil>
<bitgate-veil target-event="<id>" target-author="<pubkey>">…</bitgate-veil>
<bitgate-veil target-address="30078:<pubkey>:sku-001">…</bitgate-veil>

Assigning .target in JavaScript still works and takes precedence.

With BitLogin

const provider = document.querySelector("bitgate-provider");
await provider.ready;
await provider.useSigner(window.nostr);

Await ready rather than listening for bitgate:ready — the event fires during element upgrade, which is before a script that runs after parsing could attach a listener.

The rule these elements follow

They render decisions and issue commands. They compute no policy.

If an element here ever needed to compare a count to a threshold, that would mean the policy definition is missing a profile — not that the widget needs a number. Keeping that line is the whole reason the engine was extracted.

Elements

Viewer-facing

Element Purpose
<bitgate-veil> Wraps content and applies the decision: blur on restrict, disclosure on hide, warning on warn
<bitgate-report> Report dialog. Requires no capability — anyone may report
<bitgate-status> Readout of effect, reasons, and evidence

Moderator-facing

Element Purpose
<bitgate-capabilities> What the signed-in account may actually do
<bitgate-action> A capability-gated button that explains refusals
<bitgate-admin-panel> Effective state, contributor attribution, and controls

Usage

<script type="module">
  import { createBitGate, createCommands } from "@bitgate/runtime";
  import { defineBitGateElements } from "@bitgate/widget";

  defineBitGateElements();

  const runtime = createBitGate({
    applicationId: "my-app",
    namespace: "myapp",
    root: ROOT_PUBKEY,
    transport: myRelayAdapter,
    policy: myPolicy,
    now: () => Math.floor(Date.now() / 1000),
  });

  await runtime.loadAdministrativeState();

  const veil = document.querySelector("bitgate-veil");
  veil.runtime = runtime;
  veil.target = { type: "event", id: eventId, author: authorPubkey };
</script>

<bitgate-veil profile="feed">
  <img src="thumbnail.jpg" alt="" />
</bitgate-veil>

Elements receive a runtime; they never construct one. A widget that owned relay connections would make it impossible to share one governance runtime across a page.

Events

All events are composed so they cross shadow boundaries.

Event Fired when
bitgate:revealed The viewer chose to show hidden content
bitgate:reported / bitgate:report-failed A report was published or refused
bitgate:action-completed / :action-refused / :action-failed An action resolved
bitgate:account-restricted The admin panel restricted an account

Wording

The engine emits stable reason identifiers, never prose, so applications phrase them for their own audience:

veil.reasonText = {
  "admin-user-deny": "This seller is suspended",
  "trusted-report-threshold": "Flagged by several buyers you follow",
};

Design decisions worth knowing

Hidden content is removed from the accessibility tree, not merely covered. A screen reader must not read out something the viewer was not meant to see.

A hidden decision renders a disclosure, not nothing. Content that silently vanishes is indistinguishable from a bug and leaves no route to appeal. Where the profile permits an override, a "Show anyway" control appears; where it does not, the reason still does.

Refused actions stay visible and explain themselves. A moderator who cannot see that they lack a capability will assume the tool is broken, and a hidden button teaches nothing about the authority model. The command layer enforces the same check independently — the UI is an explanation, never the gate.

Styling is yours. Elements expose part attributes and inherit color and font from the host. Override the CSS custom properties (--gov-fg, --gov-surface, --gov-warn, --gov-danger, --gov-radius) or target parts:

bitgate-veil::part(veil) { border: 1px solid red; }

Environment

defineBitGateElements() is safe to call repeatedly and safe to import in Node — registration is a no-op without a DOM, so a server-rendering host can import the helpers without guarding.