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

176 lines
5.6 KiB
Markdown

# @bitgate/widget
Drop-in custom elements. Framework-agnostic, no bundler required by consumers,
and built strictly on the published API.
## Install
```bash
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
```html
<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:
```html
<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
```js
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
```html
<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:
```js
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:
```css
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.