thePR0M3TH3AN 94ef6a72f5 fix(security): close every finding from the audit
Critical
- Any pubkey could publish a kind:30078 roles document and become root of a
  deployment, granting itself every capability and denying anyone. Roles are now
  accepted only from the configured root, compared against the constructor
  value rather than current authority state so a bad ingest cannot move the
  goalposts. Reproduced end to end before the fix; regression test added.
- Administrative state now fails closed without signature verification. Without
  it, event.pubkey is an unauthenticated claim, so an authorship check alone
  proves nothing — a relay can put the root's key on an event the root never
  wrote. trustUnsignedEvents opts in for local and test use.

High
- Report categories could evade moderation entirely: resolveThresholds used
  bracket truthiness, so "constructor" resolved to the Object constructor (all
  gates undefined) and "__proto__" to Object.prototype, both bypassing the
  default threshold. Own-property lookups and null-prototype tables throughout
  core — policy, evaluator, evidence, authority, presets.
- Live subscriptions bypassed signature verification entirely; an attacker only
  had to wait for the subscription rather than the load.
- The default administrative query asked relays for every kind:30078 and
  kind:30000 event in existence, with no author, identifier, or limit. Now
  filtered to governance identifiers with a bounded limit.

Medium
- Cached state was trusted as authority: a cache naming a different root is now
  rejected, so same-origin tampering cannot persist a takeover.
- Provider attribute changes silently stopped working after the first one, and
  concurrent starts leaked a transport. Generation-guarded.
- Unbounded growth in the report store, mute store, decision cache, and the
  transport's per-subscription dedupe set — all now capped.

Also restores the on-wire `d` identifier to <namespace>:governance:<scope>:v1.
The BitGate rename replaced "governance:" too broadly and silently changed the
event format; both encoder and parser changed together, so round-trip tests
never noticed. The identifier names the kind of document, not the library.

Adds docs/security.md and 21 regression tests reproducing each finding.

832 tests.
2026-08-09 21:52:44 -04:00
2026-07-26 16:37:00 -04:00

BitGate

Moderation and access control for Nostr apps. Decide what to show, what to allow, and what to sell — without writing the policy code yourself.

Pairs with BitLogin: BitLogin answers "who are you." BitGate answers "what may you see, and what may you do."

The engine carries no thresholds of its own. Applications supply a policy definition; the engine supplies precedence, aggregation, composition, and explanation. That separation is what lets one engine serve a video app and a marketplace without either leaking into the other.

Why it looks like this

Four independent decision dimensions. A governance decision is not a hidden boolean. It is four answers:

Dimension Values
ranking normal → downrank (with weight)
visibility allow → warn → restrict → hide → deny
interaction allow → require-explicit-action → deny
transaction allow → require-review → deny

A marketplace listing can be visible to its seller, downranked in discovery, inspectable behind a warning on its detail page, and blocked at checkout — all from one evaluation. Collapsing that into one boolean is what forces applications to reimplement policy.

Composition is ladder-maximum per dimension, which makes it commutative and associative: the order decisions arrive in cannot change the result.

Capabilities resolve at merge time, not ingest time. Revoking a moderator's role drops their contributions from effective state immediately, without replaying or rewriting anything.

Trust seeds are a fallback, not an addition. Once a viewer has real contacts, configured seeds stop contributing — following nobody must not be equivalent to following the operator's seed list.

Packages

Package Purpose
@bitgate/core Pure evaluation. No I/O, no browser globals, no dependencies.
@bitgate/nostr Codecs: canonical v1, NIP-56, NIP-51, legacy lists, bech32.
@bitgate/runtime Stores, transport orchestration, commands.
@bitgate/testing Conformance harness.
@bitgate/widget Drop-in custom elements: viewer controls and a moderator console.
@bitgate/bitvid-compat Reference application profile for the characterization corpus.

Quick start — a static page

No npm install, no bundler. Build the widget once, self-host it, and configure everything in markup:

npm install && npm run build:widget   # produces packages/widget/dist/bitgate.js
<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>

That is the whole integration. The provider builds a runtime, loads administrative state, and descendants find it themselves — no wiring code.

Sign in with BitLogin and BitGate knows who the viewer is:

const provider = document.querySelector("bitgate-provider");
await provider.ready;
await provider.useSigner(window.nostr);   // BitLogin's NIP-07 provider

Policy presets: social, commerce, admin-only. Every number in them is a starting point — see the integration guide to write your own.

Quick start — JavaScript

import { createPolicyDefinition, evaluateTarget } from "@bitgate/core";
import { createBitGate, createMemoryTransport } from "@bitgate/runtime";

const policy = createPolicyDefinition({
  id: "my-app",
  version: "1.0.0",
  defaultProfile: "feed",
  profiles: {
    feed: {
      name: "feed",
      administrativeDeny: { visibility: "hide", interaction: "deny" },
      reports: {
        spam: { downrank: 1, restrict: 3, hide: 8 },
        malware: { hide: 1, interactionDeny: 1 },
      },
      mutes: { default: { downrank: 1, hide: 20 } },
      muteWindowSeconds: 60 * 24 * 60 * 60,
    },
  },
});

const runtime = createBitGate({
  applicationId: "my-app",
  namespace: "myapp",
  root: ROOT_PUBKEY,
  transport: createRelayTransport(["wss://relay.example"]),
  policy,
  now: () => Math.floor(Date.now() / 1000),
});

runtime.setViewer(viewerPubkey);
runtime.trust.setContacts(followList);
await runtime.loadAdministrativeState();

const decision = runtime.evaluate({ type: "user", pubkey: authorPubkey }, { profile: "feed" });
if (decision.visibility.effect === "hide") return null;

Extending to a new application

Write an adapter saying what your object is in governance terms, and a policy saying what the evidence means to you. Nothing else.

import { createApplicationAdapter, evaluateObject } from "@bitgate/core";

const productAdapter = createApplicationAdapter({
  applicationId: "my-shop",
  toTargets: (product) => [
    { type: "user", pubkey: product.sellerPubkey },
    { type: "event", id: product.id, author: product.sellerPubkey },
    { type: "address", kind: "30078", pubkey: product.sellerPubkey, identifier: product.sku },
  ],
  getPrimaryTargetKey: (product) => `address:30078:${product.sellerPubkey}:${product.sku}`,
});

const decision = evaluateObject(product, productAdapter, (target) =>
  runtime.evaluate(target, { profile: "checkout" }),
);

A worked example lives in examples/commerce — a marketplace built entirely on the public API, demonstrating seller allowlists, address denial that survives republication, malware-driven checkout blocking, capability-scoped moderator roles, community curator ingestion, and seller-facing explanations. See docs/integration-guide.md.

Plug-and-play UI

@bitgate/widget ships custom elements that render decisions and issue commands — and compute no policy of their own:

<bitgate-veil profile="feed"><img src="thumbnail.jpg" alt="" /></bitgate-veil>
import { defineBitGateElements } from "@bitgate/widget";
defineBitGateElements();

const veil = document.querySelector("bitgate-veil");
veil.runtime = runtime;
veil.target = { type: "event", id, author };

Viewer elements (bitgate-veil, bitgate-report, bitgate-status) and moderator elements (bitgate-capabilities, bitgate-action, bitgate-admin-panel) are separate, so a site embeds only what it needs. See packages/widget/README.md and the runnable examples/commerce/demo.html.

Authority model

Root-authorized contributors: the root publishes the role roster, each actor publishes contributions under their own key, and a contribution is accepted only if the signing actor holds the matching capability.

manage-roles            manage-policy         manage-community-sources
contribute-user-allow   contribute-user-deny  contribute-event-deny
contribute-address-deny contribute-trust-seed review-evidence

Roles are convenience bundles over these; applications define their own (listing_moderator, seller_moderator, …). No moderator ever needs the root key. Protected actors cannot be denied by any contributor list, however many curators list them.

Development

npm install
npm run lint          # reference guard + typecheck
npm test              # unit + conformance
npm run build         # .d.ts for every package + the widget bundle

The widget bundle is not committed. Build it from source and self-host the result — a moderation widget is exactly the kind of artifact where reading the source matters.

BitVid and BitRoad are read-only reference repositories, pinned by commit in docs/reference-map.md. Behavior is copied via fixtures and characterization tests, never by importing their source; npm run check:references fails the build if anything does.

Regenerate the characterization corpus with:

node scripts/build-characterization-corpus.mjs

License

GPL-3.0-or-later. See docs/license-and-provenance.md.

S
Description
No description provided
Readme GPL-3.0
521 KiB
0 Stars 1 Watchers 0 Forks
Languages
JavaScript 94.7%
HTML 3.3%
CSS 2%