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

+10 -10
View File
@@ -14,8 +14,8 @@
import { GovernanceElement, defineElement, escapeHtml, shortenKey } from "./base.js";
/**
* @typedef {import('@nostr-governance/runtime').GovernanceCommands} GovernanceCommands
* @typedef {import('@nostr-governance/core').GovernanceCapability} GovernanceCapability
* @typedef {import('@bitgate/runtime').GovernanceCommands} GovernanceCommands
* @typedef {import('@bitgate/core').GovernanceCapability} GovernanceCapability
*/
/** Human wording for capabilities, used when explaining a refusal. */
@@ -32,7 +32,7 @@ export const CAPABILITY_LABELS = {
};
/**
* `<governance-capabilities>` — what the signed-in moderator may do.
* `<bitgate-capabilities>` — what the signed-in moderator may do.
*
* Worth rendering explicitly: in the previous generation of this system,
* moderator status was a UI permission with no cryptographic meaning. Showing
@@ -81,7 +81,7 @@ export class GovernanceCapabilities extends GovernanceElement {
}
/**
* `<governance-action>` — a single capability-gated action button.
* `<bitgate-action>` — a single capability-gated action button.
*
* When the actor lacks the capability the button is disabled *and* labelled
* with what is missing, which is the difference between "broken" and "not
@@ -167,7 +167,7 @@ export class GovernanceAction extends GovernanceElement {
}
/**
* `<governance-admin-panel>` — effective administrative state plus controls.
* `<bitgate-admin-panel>` — effective administrative state plus controls.
*
* Shows what is currently in force and who contributed it, so a moderator can
* see that a denial came from a community list rather than from their own team.
@@ -298,12 +298,12 @@ export class GovernanceAdminPanel extends GovernanceElement {
*/
export function defineAdminElements() {
const registered = [];
if (defineElement("governance-capabilities", GovernanceCapabilities)) {
registered.push("governance-capabilities");
if (defineElement("bitgate-capabilities", GovernanceCapabilities)) {
registered.push("bitgate-capabilities");
}
if (defineElement("governance-action", GovernanceAction)) registered.push("governance-action");
if (defineElement("governance-admin-panel", GovernanceAdminPanel)) {
registered.push("governance-admin-panel");
if (defineElement("bitgate-action", GovernanceAction)) registered.push("bitgate-action");
if (defineElement("bitgate-admin-panel", GovernanceAdminPanel)) {
registered.push("bitgate-admin-panel");
}
return registered;
}
+115 -4
View File
@@ -10,9 +10,9 @@
// one governance runtime across its whole page.
/**
* @typedef {import('@nostr-governance/runtime').GovernanceRuntime} GovernanceRuntime
* @typedef {import('@nostr-governance/core').GovernanceDecision} GovernanceDecision
* @typedef {import('@nostr-governance/core').GovernanceTarget} GovernanceTarget
* @typedef {import('@bitgate/runtime').GovernanceRuntime} GovernanceRuntime
* @typedef {import('@bitgate/core').GovernanceDecision} GovernanceDecision
* @typedef {import('@bitgate/core').GovernanceTarget} GovernanceTarget
*/
/**
@@ -90,6 +90,73 @@ export function shortenKey(pubkey) {
return value.length > 16 ? `${value.slice(0, 8)}…${value.slice(-4)}` : value;
}
/**
* Ask the nearest provider for its runtime and commands.
*
* Declared here rather than imported from ./provider.js to keep the module
* graph acyclic: provider.js already depends on this file.
*
* @param {Element} element
* @returns {{ runtime: any, commands: any }|null}
*/
export function requestContext(element) {
/** @type {{ context: any }} */
const detail = { context: null };
element.dispatchEvent(
new CustomEvent("bitgate:context-request", { detail, bubbles: true, composed: true }),
);
return detail.context;
}
/**
* Parse a governance target from element attributes.
*
* Lets markup name a target without JavaScript:
* target-event / target-author / target-kind
* target-user
* target-address (a `kind:pubkey:d` coordinate)
*
* @param {Element} element
* @returns {import('@bitgate/core').GovernanceTarget|null}
*/
export function targetFromAttributes(element) {
const user = element.getAttribute("target-user");
if (user) {
return { type: "user", pubkey: user.trim() };
}
const eventId = element.getAttribute("target-event");
if (eventId) {
/** @type {any} */
const target = { type: "event", id: eventId.trim() };
const author = element.getAttribute("target-author");
if (author) {
target.author = author.trim();
}
const kind = element.getAttribute("target-kind");
if (kind && Number.isFinite(Number(kind))) {
target.kind = Number(kind);
}
return target;
}
const address = element.getAttribute("target-address");
if (address) {
const first = address.indexOf(":");
const second = address.indexOf(":", first + 1);
if (first > 0 && second > first) {
return {
type: "address",
kind: address.slice(0, first),
pubkey: address.slice(first + 1, second),
identifier: address.slice(second + 1),
};
}
}
return null;
}
/**
* Base element: owns a shadow root, a runtime reference, and change
* subscription lifecycle.
@@ -104,6 +171,7 @@ export class GovernanceElement extends globalThis.HTMLElement {
this._runtime = null;
/** @type {(() => void)|null} */
this._unsubscribe = null;
this._awaitingProvider = false;
}
/**
@@ -129,12 +197,55 @@ export class GovernanceElement extends globalThis.HTMLElement {
}
connectedCallback() {
// Adopt the nearest provider's runtime when one was not assigned directly.
// This is what lets a page configure BitGate entirely in markup.
if (!this._runtime) {
this.adoptContext();
}
if (this._runtime && !this._unsubscribe) {
this._subscribe();
}
this.render();
}
/**
* Find a `<bitgate-provider>` ancestor and take its runtime and commands.
*
* A provider that is still starting answers with nothing; the element waits
* for `bitgate:ready` rather than polling.
*
* @returns {boolean} Whether a runtime was adopted
*/
adoptContext() {
const context = requestContext(this);
if (context?.runtime) {
this.runtime = context.runtime;
if (context.commands && "commands" in this) {
/** @type {any} */ (this).commands = context.commands;
}
return true;
}
if (!this._awaitingProvider) {
this._awaitingProvider = true;
globalThis.document?.addEventListener(
"bitgate:ready",
(event) => {
const detail = /** @type {CustomEvent} */ (event).detail;
if (!detail?.runtime || this._runtime) {
return;
}
this.runtime = detail.runtime;
if (detail.commands && "commands" in this) {
/** @type {any} */ (this).commands = detail.commands;
}
},
{ once: true },
);
}
return false;
}
disconnectedCallback() {
// Elements are routinely moved or removed during rendering; leaving a
// subscription behind would keep a detached element re-rendering forever.
@@ -155,7 +266,7 @@ export class GovernanceElement extends globalThis.HTMLElement {
*/
emit(name, detail) {
this.dispatchEvent(
new CustomEvent(`governance:${name}`, { detail, bubbles: true, composed: true }),
new CustomEvent(`bitgate:${name}`, { detail, bubbles: true, composed: true }),
);
}
+13 -3
View File
@@ -1,4 +1,4 @@
// @nostr-governance/widget
// @bitgate/widget
//
// Custom elements built strictly on the published API. They render decisions
// and issue commands; they compute no policy. If an element here ever needs a
@@ -14,9 +14,18 @@ export {
canRegisterElements,
defineElement,
escapeHtml,
requestContext,
shortenKey,
targetFromAttributes,
} from "./base.js";
export {
BitGateError,
BitGateProvider,
CONTEXT_REQUEST,
defineProviderElements,
} from "./provider.js";
export {
DEFAULT_REASON_TEXT,
GovernanceReport,
@@ -35,6 +44,7 @@ export {
} from "./admin.js";
import { defineAdminElements } from "./admin.js";
import { defineProviderElements } from "./provider.js";
import { defineViewerElements } from "./viewer.js";
/**
@@ -46,6 +56,6 @@ import { defineViewerElements } from "./viewer.js";
*
* @returns {string[]} Element names newly registered by this call
*/
export function defineGovernanceElements() {
return [...defineViewerElements(), ...defineAdminElements()];
export function defineBitGateElements() {
return [...defineProviderElements(), ...defineViewerElements(), ...defineAdminElements()];
}
+314
View File
@@ -0,0 +1,314 @@
// `<bitgate-provider>` — attribute-driven setup.
//
// This is what makes BitGate droppable into a static page. Without it, a host
// has to construct a runtime in JavaScript and assign it to every element;
// with it, the markup carries the configuration and descendants find the
// runtime themselves.
//
// Descendants discover the provider by firing a bubbling request event rather
// than walking up the DOM. Walking up breaks across shadow boundaries, which is
// exactly where a host is most likely to place these elements.
import { createBitGate, createCommands, createRelayTransport } from "@bitgate/runtime";
import { getPolicyPreset } from "@bitgate/core";
import { GovernanceElement, defineElement, escapeHtml } from "./base.js";
/** Event a descendant fires to locate its provider. */
export const CONTEXT_REQUEST = "bitgate:context-request";
/**
* Ask the nearest provider for its runtime and commands.
*
* Returns null when no provider is present, which is a legitimate state: an
* element may be configured directly in JavaScript instead.
*
* @param {Element} element
* @returns {{ runtime: any, commands: any }|null}
*/
export function requestContext(element) {
/** @type {{ context: any }} */
const detail = { context: null };
element.dispatchEvent(new CustomEvent(CONTEXT_REQUEST, { detail, bubbles: true, composed: true }));
return detail.context;
}
export class BitGateProvider extends globalThis.HTMLElement {
static get observedAttributes() {
return ["relays", "root", "policy", "profile", "application", "namespace", "viewer"];
}
constructor() {
super();
/** @type {any} */
this.runtime = null;
/** @type {any} */
this.commands = null;
/** @type {any} */
this.transport = null;
/** @type {Error|null} */
this.error = null;
this._ready = false;
// A promise as well as an event. The event fires during element upgrade,
// which for a page that calls defineBitGateElements() after parsing is
// *before* it can attach a listener — so an event alone would be a race
// that silently loses. Awaiting `ready` always works.
/** @type {Promise<{ runtime: any, commands: any }>} */
this.ready = new Promise((resolve, reject) => {
this._resolveReady = resolve;
this._rejectReady = reject;
});
// Nothing may await this before start() runs; keep Node and browsers from
// reporting an unhandled rejection in that window.
this.ready.catch(() => {});
/** @type {Promise<void>|null} */
this.loaded = null;
// Answer descendants' context requests. Registered on the host element so
// it catches events from composed shadow trees too.
this.addEventListener(CONTEXT_REQUEST, (event) => {
const custom = /** @type {CustomEvent} */ (event);
if (!custom.detail || custom.detail.context) {
return;
}
custom.detail.context = this.runtime ? { runtime: this.runtime, commands: this.commands } : null;
event.stopPropagation();
});
}
connectedCallback() {
if (!this._ready) {
this._ready = true;
void this.start();
}
}
disconnectedCallback() {
this.stop();
}
attributeChangedCallback(name, before, after) {
// Only rebuild once connected and actually changed; the initial attribute
// pass would otherwise construct several runtimes during parsing.
if (this._ready && before !== after) {
this.stop();
void this.start();
}
}
/** @returns {string[]} */
get relayUrls() {
return (this.getAttribute("relays") ?? "")
.split(",")
.map((url) => url.trim())
.filter(Boolean);
}
/**
* Build the runtime and load administrative state.
*
* Failures are surfaced as an event and a `data-error` attribute rather than
* thrown: a page whose relays are unreachable should still render, showing
* whatever cached state exists.
*/
async start() {
try {
const relays = this.relayUrls;
const policyName = this.getAttribute("policy") ?? "admin-only";
const policy = getPolicyPreset(policyName);
if (!policy) {
throw new Error(
`Unknown policy preset "${policyName}". Use social, commerce, admin-only, or set .policy directly.`,
);
}
if (relays.length === 0) {
throw new Error("bitgate-provider requires a relays attribute");
}
this.transport = createRelayTransport(relays);
this.runtime = createBitGate({
applicationId: this.getAttribute("application") ?? globalThis.location?.hostname ?? "bitgate",
namespace: this.getAttribute("namespace") ?? "bitgate",
root: this.getAttribute("root") ?? undefined,
transport: this.transport,
policy,
storage: createLocalStorage(),
now: () => Math.floor(Date.now() / 1000),
});
this.commands = createCommands(this.runtime);
const viewer = this.getAttribute("viewer");
if (viewer) {
this.runtime.setViewer(viewer);
}
this.error = null;
this.removeAttribute("data-error");
this._resolveReady?.({ runtime: this.runtime, commands: this.commands });
this.dispatchEvent(
new CustomEvent("bitgate:ready", {
detail: { runtime: this.runtime, commands: this.commands },
bubbles: true,
composed: true,
}),
);
await this.runtime.loadAdministrativeState();
this.dispatchEvent(new CustomEvent("bitgate:loaded", { bubbles: true, composed: true }));
} catch (error) {
this.error = /** @type {Error} */ (error);
this.setAttribute("data-error", String(error));
this._rejectReady?.(error);
this.dispatchEvent(
new CustomEvent("bitgate:error", {
detail: { error: String(error) },
bubbles: true,
composed: true,
}),
);
}
}
/** Tear down the runtime and close relay sockets. */
stop() {
// A restart needs a fresh promise; the old one has already settled.
this.ready = new Promise((resolve, reject) => {
this._resolveReady = resolve;
this._rejectReady = reject;
});
this.ready.catch(() => {});
this.runtime?.destroy?.();
this.transport?.close?.();
this.runtime = null;
this.commands = null;
this.transport = null;
this._ready = false;
}
/**
* Attach a signer, typically BitLogin's `window.nostr` provider.
*
* Also sets the viewer, since a signer that cannot say who it is would leave
* the trust graph empty and every capability check failing.
*
* @param {{ getPublicKey: () => Promise<string>, signEvent: (event: any) => Promise<any> }} signer
* @returns {Promise<string>} The viewer pubkey
*/
async useSigner(signer) {
if (!this.runtime) {
throw new Error("bitgate-provider is not ready");
}
this.runtime.signer = signer;
const pubkey = await signer.getPublicKey();
this.runtime.setViewer(pubkey);
return pubkey;
}
}
/**
* Storage backed by `localStorage`, falling back to memory where it is
* unavailable or blocked (private browsing, sandboxed frames).
* @returns {import('@bitgate/runtime').GovernanceStorage}
*/
function createLocalStorage() {
/** @type {Map<string, unknown>} */
const fallback = new Map();
const available = (() => {
try {
const probe = "__bitgate_probe__";
globalThis.localStorage?.setItem(probe, "1");
globalThis.localStorage?.removeItem(probe);
return Boolean(globalThis.localStorage);
} catch {
return false;
}
})();
return {
async read(key) {
if (!available) {
return fallback.has(key) ? fallback.get(key) : null;
}
try {
const raw = globalThis.localStorage.getItem(key);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
},
async write(key, value) {
if (!available) {
fallback.set(key, value);
return;
}
try {
globalThis.localStorage.setItem(key, JSON.stringify(value));
} catch {
// A full or blocked quota degrades to no persistence, never to a throw
// that would break rendering.
fallback.set(key, value);
}
},
async remove(key) {
fallback.delete(key);
try {
globalThis.localStorage?.removeItem(key);
} catch {
// Nothing to do.
}
},
};
}
/**
* `<bitgate-error>` — renders a provider's setup failure.
*
* Optional, but worth having: a misconfigured `relays` attribute otherwise
* produces a page that silently governs nothing.
*/
export class BitGateError extends GovernanceElement {
connectedCallback() {
super.connectedCallback();
this._listener = (event) => {
this._error = event.detail?.error;
this.render();
};
globalThis.document?.addEventListener("bitgate:error", this._listener);
}
disconnectedCallback() {
super.disconnectedCallback();
if (this._listener) {
globalThis.document?.removeEventListener("bitgate:error", this._listener);
}
}
render() {
if (!this.shadowRoot) {
return;
}
if (!this._error) {
this.renderHtml("");
return;
}
this.renderHtml(
`<p class="reason" part="error" role="status">BitGate: ${escapeHtml(this._error)}</p>`,
);
}
}
/**
* Register the provider elements.
* @returns {string[]}
*/
export function defineProviderElements() {
const registered = [];
if (defineElement("bitgate-provider", BitGateProvider)) registered.push("bitgate-provider");
if (defineElement("bitgate-error", BitGateError)) registered.push("bitgate-error");
return registered;
}
+51 -26
View File
@@ -4,11 +4,17 @@
// no thresholds, no precedence, no "if reports > n". If one of these needs a
// number, the number belongs in a policy profile.
import { GovernanceElement, defineElement, escapeHtml, shortenKey } from "./base.js";
import {
GovernanceElement,
defineElement,
escapeHtml,
shortenKey,
targetFromAttributes,
} from "./base.js";
/**
* @typedef {import('@nostr-governance/core').GovernanceDecision} GovernanceDecision
* @typedef {import('@nostr-governance/core').GovernanceTarget} GovernanceTarget
* @typedef {import('@bitgate/core').GovernanceDecision} GovernanceDecision
* @typedef {import('@bitgate/core').GovernanceTarget} GovernanceTarget
*/
/**
@@ -50,7 +56,7 @@ export function describeReasons(decision, overrides = {}) {
}
/**
* `<governance-veil>` — wraps content and applies a decision to it.
* `<bitgate-veil>` — wraps content and applies a decision to it.
*
* Slotted content is always in the DOM; the element controls whether it is
* shown, blurred, or replaced. Hidden content is removed from the accessibility
@@ -58,12 +64,12 @@ export function describeReasons(decision, overrides = {}) {
* out something the viewer was not meant to see.
*
* Usage:
* <governance-veil><img src="..."></governance-veil>
* <bitgate-veil><img src="..."></bitgate-veil>
* veil.runtime = runtime; veil.target = { type: "event", id, author };
*/
export class GovernanceVeil extends GovernanceElement {
static get observedAttributes() {
return ["profile"];
return ["profile", "target-user", "target-event", "target-author", "target-kind", "target-address"];
}
constructor() {
@@ -76,7 +82,8 @@ export class GovernanceVeil extends GovernanceElement {
}
get target() {
return this._target;
// An explicitly assigned target wins; otherwise read it from markup.
return this._target ?? targetFromAttributes(this);
}
set target(target) {
@@ -95,11 +102,12 @@ export class GovernanceVeil extends GovernanceElement {
/** The decision currently applied, or null when not evaluable. */
get decision() {
if (!this.runtime || !this._target) {
const target = this.target;
if (!this.runtime || !target) {
return null;
}
try {
return this.runtime.evaluate(this._target, { profile: this.profile });
return this.runtime.evaluate(target, { profile: this.profile });
} catch {
return null;
}
@@ -136,7 +144,7 @@ export class GovernanceVeil extends GovernanceElement {
`);
this.$("#reveal")?.addEventListener("click", () => {
this._revealed = true;
this.emit("revealed", { target: this._target, decision });
this.emit("revealed", { target: this.target, decision });
this.render();
});
return;
@@ -173,14 +181,14 @@ export class GovernanceVeil extends GovernanceElement {
this.$("#reveal")?.addEventListener("click", () => {
this._revealed = true;
this.emit("revealed", { target: this._target, decision });
this.emit("revealed", { target: this.target, decision });
this.render();
});
}
}
/**
* `<governance-report>` — a report dialog.
* `<bitgate-report>` — a report dialog.
*
* Reporting requires no capability: anyone may report, and whether the report
* counts is decided later by each viewer's own trust graph. The element makes
@@ -193,7 +201,7 @@ export class GovernanceReport extends GovernanceElement {
this._target = null;
/** @type {string[]} */
this.categories = ["spam", "nudity", "profanity", "illegal", "malware", "impersonation", "other"];
/** @type {import('@nostr-governance/runtime').GovernanceCommands|null} */
/** @type {import('@bitgate/runtime').GovernanceCommands|null} */
this.commands = null;
/**
* The in-flight submission, or null when idle.
@@ -203,8 +211,12 @@ export class GovernanceReport extends GovernanceElement {
this._status = "";
}
static get observedAttributes() {
return ["target-user", "target-event", "target-author", "target-kind", "target-address"];
}
get target() {
return this._target;
return this._target ?? targetFromAttributes(this);
}
set target(target) {
@@ -213,6 +225,10 @@ export class GovernanceReport extends GovernanceElement {
this.render();
}
attributeChangedCallback() {
this.render();
}
render() {
if (!this.shadowRoot) {
return;
@@ -237,7 +253,7 @@ export class GovernanceReport extends GovernanceElement {
sees depends on their own trust graph.
</p>
<div class="row">
<button type="submit" id="submit" ${this._target && this.commands ? "" : "disabled"}>
<button type="submit" id="submit" ${this.target && this.commands ? "" : "disabled"}>
Submit report
</button>
<span class="reason" id="status">${escapeHtml(this._status)}</span>
@@ -252,7 +268,8 @@ export class GovernanceReport extends GovernanceElement {
}
async _submit() {
if (!this._target || !this.commands) {
const target = this.target;
if (!target || !this.commands) {
return;
}
@@ -262,18 +279,18 @@ export class GovernanceReport extends GovernanceElement {
this._status = "Publishing…";
this.render();
const result = await this.commands.report(this._target, category, details);
const result = await this.commands.report(target, category, details);
// Partial relay acceptance is success: the event exists on the network.
this._status = result.ok ? "Report published" : `Failed: ${result.code}`;
this.emit(result.ok ? "reported" : "report-failed", { target: this._target, category, result });
this.emit(result.ok ? "reported" : "report-failed", { target, category, result });
this.render();
this.pending = null;
}
}
/**
* `<governance-status>` — a compact readout of why something was affected.
* `<bitgate-status>` — a compact readout of why something was affected.
*
* Shows counts when the profile exposes evidence, and contributor identities
* only when it does. A surface that redacts evidence gets "reported by people
@@ -289,11 +306,11 @@ export class GovernanceStatus extends GovernanceElement {
}
static get observedAttributes() {
return ["profile"];
return ["profile", "target-user", "target-event", "target-author", "target-kind", "target-address"];
}
get target() {
return this._target;
return this._target ?? targetFromAttributes(this);
}
set target(target) {
@@ -313,19 +330,27 @@ export class GovernanceStatus extends GovernanceElement {
if (!this.shadowRoot) {
return;
}
if (!this.runtime || !this._target) {
const target = this.target;
if (!this.runtime || !target) {
this.renderHtml("");
return;
}
let decision;
try {
decision = this.runtime.evaluate(this._target, { profile: this.profile });
decision = this.runtime.evaluate(target, { profile: this.profile });
} catch {
this.renderHtml("");
return;
}
// A runtime that returns nothing usable must not take the page down with
// it; rendering no status is the correct degradation.
if (!decision?.reasons) {
this.renderHtml("");
return;
}
const reasons = describeReasons(decision, this.reasonText);
const evidence = decision.evidence;
const reporters = evidence?.trustedReporterPubkeys ?? [];
@@ -380,8 +405,8 @@ export class GovernanceStatus extends GovernanceElement {
*/
export function defineViewerElements() {
const registered = [];
if (defineElement("governance-veil", GovernanceVeil)) registered.push("governance-veil");
if (defineElement("governance-report", GovernanceReport)) registered.push("governance-report");
if (defineElement("governance-status", GovernanceStatus)) registered.push("governance-status");
if (defineElement("bitgate-veil", GovernanceVeil)) registered.push("bitgate-veil");
if (defineElement("bitgate-report", GovernanceReport)) registered.push("bitgate-report");
if (defineElement("bitgate-status", GovernanceStatus)) registered.push("bitgate-status");
return registered;
}