mirror of
https://github.com/PR0M3TH3AN/bitgate.git
synced 2026-10-27 08:31:00 +00:00
Widget styles now prefer --bitgate-* custom properties, falling back to the deprecated --gov-* names and then the existing defaults so current pages and overrides keep working. Browser bundles and ESM entry points expose VERSION (and window.bitgate.version) from package.json. Document allowlist on the widget policy preset list. Sync the lockfile so npm ci sees @bitgate/standalone. Co-authored-by: thePR0M3TH3AN <[email protected]>
289 lines
12 KiB
Markdown
289 lines
12 KiB
Markdown
# 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](https://github.com/PR0M3TH3AN/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, NIP-32, NIP-65, NIP-02, bech32. |
|
|
| `@bitgate/runtime` | Stores, transport orchestration, commands. |
|
|
| `@bitgate/verify` | Optional NIP-01 signature verification, so the core stays dependency-free. |
|
|
| `@bitgate/testing` | Conformance harness. |
|
|
| `@bitgate/widget` | Drop-in custom elements: viewer controls and a moderator console. |
|
|
| `@bitgate/standalone` | The headless engine + Nostr codecs bundled into one self-contained ESM file for no-build static sites. |
|
|
| `@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:
|
|
|
|
```bash
|
|
npm install && npm run build:widget # produces packages/widget/dist/bitgate.js
|
|
```
|
|
|
|
```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>
|
|
```
|
|
|
|
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:
|
|
|
|
```js
|
|
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`, and `allowlist` (only
|
|
allowlisted publishers are shown — the symmetric twin of a denylist, for
|
|
invite-only or curated surfaces). Every number in them is a starting point —
|
|
see the [integration guide](docs/integration-guide.md) to write your own.
|
|
|
|
The package version is exported as `VERSION` and, in browser bundles, as
|
|
`window.bitgate.version`. Both are injected from `package.json` at build time.
|
|
Widget theme tokens use `--bitgate-*` custom properties; the older `--gov-*`
|
|
names remain as fallbacks (see [`packages/widget/README.md`](packages/widget/README.md)).
|
|
|
|
## Quick start — JavaScript
|
|
|
|
```js
|
|
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;
|
|
```
|
|
|
|
### Headless, straight from NIP-32 labels
|
|
|
|
If you don't want the runtime's transport orchestration either — you already
|
|
have relay events in hand and just want the engine — the whole path is four
|
|
pure functions. **NIP-32 label events (kind 1985) are the canonical, signed
|
|
decision format**; you don't invent one:
|
|
|
|
```js
|
|
import { createSnapshot, evaluateTarget, reduceAdminState, createAuthorityState } from "@bitgate/core";
|
|
import { decodeLabels, labelsToContributions } from "@bitgate/nostr";
|
|
|
|
const authority = createAuthorityState({ root: ROOT, actors: honoredModerators }); // moderator set as data
|
|
const contributions = labelsToContributions(
|
|
labelEvents.flatMap(decodeLabels), // kind-1985 events off your relays
|
|
{ namespace: "org.bitblocks.plugins", denyValues: ["deny"], allowValues: ["allow"] },
|
|
);
|
|
const snapshot = createSnapshot({ authority, admin: reduceAdminState(contributions, authority) });
|
|
const decision = evaluateTarget(target, snapshot, { policy, surface: "registry" });
|
|
|
|
if (!decision.transaction || decision.transaction.effect === "allow") install(); // gate a non-visual action
|
|
```
|
|
|
|
Don't hand-build `honoredModerators`. "Who is a moderator" is data that should
|
|
change without a redeploy, so let the root publish it as a signed kind-30000 set
|
|
and resolve it at runtime with `resolveAuthorityRoster` — it honors only
|
|
root-signed sets (and, with `signer`, one-role-delegates-another chains), and
|
|
its output spreads straight into `createAuthorityState`:
|
|
|
|
```js
|
|
import { resolveAuthorityRoster } from "@bitgate/nostr";
|
|
const authority = createAuthorityState(resolveAuthorityRoster(roleSetEvents, {
|
|
root: ROOT,
|
|
rosters: [
|
|
{ identifier: "bitgate:moderators", role: "moderator" }, // root names moderators
|
|
{ identifier: "bitgate:curators", role: "curator", signer: "moderator" }, // moderators name curators
|
|
],
|
|
}));
|
|
```
|
|
|
|
A decision has four dimensions, so `transaction` can block an install or
|
|
checkout while `visibility` still shows the listing with a warning — something a
|
|
`hidden` boolean can't express. [`examples/headless-quickstart.mjs`](examples/headless-quickstart.mjs)
|
|
is a runnable end-to-end walkthrough (`node examples/headless-quickstart.mjs`);
|
|
[docs/labels.md](docs/labels.md) covers the NIP-32 wire format and interop.
|
|
|
|
No bundler? `npm run build:standalone` produces `packages/standalone/dist/bitgate.js`
|
|
— the engine and the Nostr codecs above in one self-contained, dependency-free
|
|
ESM file. Self-host it and `import { evaluateTarget, decodeLabels } from "./bitgate.js"`;
|
|
everything in this section works from that single file.
|
|
|
|
The soft dimension, `ranking`, feeds an ordering rather than a gate:
|
|
`decision.ranking` is `{ effect: "normal" | "downrank", weight }`, which an app
|
|
turns into a multiplier on its own feed score — a reported author drops in the
|
|
feed without being hidden. [`examples/feed-ranking.mjs`](examples/feed-ranking.mjs)
|
|
is a runnable walkthrough.
|
|
|
|
## 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.
|
|
|
|
```js
|
|
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`](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`](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:
|
|
|
|
```html
|
|
<bitgate-veil profile="feed"><img src="thumbnail.jpg" alt="" /></bitgate-veil>
|
|
```
|
|
|
|
```js
|
|
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`](packages/widget/README.md) and the runnable
|
|
[`examples/commerce/demo.html`](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
|
|
|
|
```bash
|
|
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`](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:
|
|
|
|
```bash
|
|
node scripts/build-characterization-corpus.mjs
|
|
```
|
|
|
|
## License
|
|
|
|
GPL-3.0-or-later. See [`docs/license-and-provenance.md`](docs/license-and-provenance.md).
|