thePR0M3TH3AN c4477ae84b perf: memoize snapshot and fingerprint; add performance and security fixtures
The performance fixture the plan asks for found a real defect on its first run:
evaluation was quadratic in state size. Both the snapshot and its fingerprint
were rebuilt for every target, so each of 5,000 evaluations walked every report
and every mute list. A 5,000-target pass took ~47 seconds.

- the runtime memoizes the materialized snapshot and its fingerprint until a
  store reports a change
- evaluateMany computes the fingerprint once and shares it across the batch
- decisions are cached per (profile, target); report and mute changes
  invalidate only the targets they touch, while administrative, trust, policy,
  and viewer changes drop the whole cache
- cached decisions are deeply frozen, since the cache hands the same object to
  every caller

Same pass now takes ~85ms cold, ~6ms warm.

Adds tests/performance (5,000 targets, 500 authors, 100 muters per author) and
tests/security (one case per enumerated requirement testable at this layer).

605 tests; lint, typecheck, and the declaration build pass from a cold checkout.
2026-08-09 18:07:23 -04:00
2026-07-26 16:37:00 -04:00

nostr-governance

A headless, application-agnostic governance engine for Nostr: moderation, access control, and trust-graph policy, with no opinion about what your application is.

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
@nostr-governance/core Pure evaluation. No I/O, no browser globals, no dependencies.
@nostr-governance/nostr Codecs: canonical v1, NIP-56, NIP-51, legacy lists, bech32.
@nostr-governance/runtime Stores, transport orchestration, commands.
@nostr-governance/testing Conformance harness.
@nostr-governance/bitvid-compat Reference application profile for the characterization corpus.

Quick start

import { createPolicyDefinition, evaluateTarget } from "@nostr-governance/core";
import { createGovernanceRuntime, createMemoryTransport } from "@nostr-governance/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 = createGovernanceRuntime({
  applicationId: "my-app",
  namespace: "myapp",
  transport: myRelayAdapter,
  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 "@nostr-governance/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.

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:types   # per-package .d.ts

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%