diff --git a/.gitignore b/.gitignore index ea8317a..0bc3d78 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ coverage/ .turbo/ .cache/ *.tsbuildinfo + +# Built site output (Vercel builds it) +packages/site/dist/ diff --git a/package-lock.json b/package-lock.json index 205026c..88e88e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,10 @@ "resolved": "packages/runtime", "link": true }, + "node_modules/@bitgate/site": { + "resolved": "packages/site", + "link": true + }, "node_modules/@bitgate/testing": { "resolved": "packages/testing", "link": true @@ -1798,6 +1802,10 @@ "@bitgate/nostr": "*" } }, + "packages/site": { + "name": "@bitgate/site", + "version": "0.1.0" + }, "packages/testing": { "name": "@bitgate/testing", "version": "0.1.0", diff --git a/package.json b/package.json index eaf3bed..35a7f8a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "check:references": "node scripts/verify-no-reference-imports.mjs", "check:types": "tsc -p tsconfig.json --noEmit && tsc -p packages/widget/tsconfig.json --noEmit", "build:widget": "node packages/widget/build.mjs", - "build": "npm run build:types && npm run build:widget" + "build": "npm run build:types && npm run build:widget && npm run build:site", + "build:site": "node packages/site/build.mjs", + "dev:site": "node packages/site/build.mjs && node packages/site/serve.mjs" }, "engines": { "node": ">=22" diff --git a/packages/site/build.mjs b/packages/site/build.mjs new file mode 100644 index 0000000..ff4c9f3 --- /dev/null +++ b/packages/site/build.mjs @@ -0,0 +1,33 @@ +#!/usr/bin/env node +/** + * Assemble the static site. + * + * Copies `public/` and drops the built widget in at `vendor/bitgate/`, which is + * the same path the docs tell integrators to use — so the site is itself an + * example of the instructions it gives. + */ +import { cpSync, mkdirSync, rmSync } from "node:fs"; +import { execSync } from "node:child_process"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const widgetDir = join(HERE, "..", "widget"); +const widgetDist = join(widgetDir, "dist"); +const publicDir = join(HERE, "public"); +const outDir = join(HERE, "dist"); + +// Always rebuild rather than reusing whatever is on disk. A stale bundle is +// exactly how a broken demo ships: the page loads, the module resolves, and an +// export added since the last build is silently missing. +console.log("Building @bitgate/widget…"); +execSync("node build.mjs", { cwd: widgetDir, stdio: "inherit" }); + +rmSync(outDir, { recursive: true, force: true }); +mkdirSync(outDir, { recursive: true }); +cpSync(publicDir, outDir, { recursive: true }); + +mkdirSync(join(outDir, "vendor", "bitgate"), { recursive: true }); +cpSync(widgetDist, join(outDir, "vendor", "bitgate"), { recursive: true }); + +console.log(`✓ Built ${outDir}`); diff --git a/packages/site/package.json b/packages/site/package.json new file mode 100644 index 0000000..481ced1 --- /dev/null +++ b/packages/site/package.json @@ -0,0 +1,11 @@ +{ + "name": "@bitgate/site", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "BitGate landing page, integration guide, and live demo — a plain static site consuming the built widget.", + "scripts": { + "build": "node build.mjs", + "dev": "node build.mjs && node serve.mjs" + } +} diff --git a/packages/site/public/assets/icon.svg b/packages/site/public/assets/icon.svg new file mode 100644 index 0000000..45b4f20 --- /dev/null +++ b/packages/site/public/assets/icon.svg @@ -0,0 +1,18 @@ + diff --git a/packages/site/public/assets/mark.svg b/packages/site/public/assets/mark.svg new file mode 100644 index 0000000..7124d68 --- /dev/null +++ b/packages/site/public/assets/mark.svg @@ -0,0 +1,14 @@ + diff --git a/packages/site/public/assets/site.css b/packages/site/public/assets/site.css new file mode 100644 index 0000000..014a691 --- /dev/null +++ b/packages/site/public/assets/site.css @@ -0,0 +1,487 @@ +/* BitGate site. + * + * Deliberately a sibling of the BitLogin design system, not a twin: the same + * ink/paper/brass foundation, with verdigris — the patina brass takes as it + * ages — where BitLogin uses its violet seal. Same hardware, different + * oxidation. + */ + +:root { + --ink: #0b100f; + --ink-soft: #131a19; + --ink-softer: #1b2422; + --paper: #f1ece0; + --paper-dim: #e2dac7; + --gate: #3fc9a4; + --gate-deep: #1f9b7d; + --brass: #d1a85a; + --brass-soft: #9c824f; + --fg: #f2f1ea; + --muted: #9aa8a3; + --line: rgba(242, 241, 234, 0.11); + --line-strong: rgba(242, 241, 234, 0.2); + --code-bg: #0d1413; + --shadow: 0 24px 60px rgba(0, 0, 0, 0.45); + --font-display: "Iowan Old Style", "Palatino Linotype", Georgia, serif; + --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace; + color-scheme: dark; +} + +@media (prefers-color-scheme: light) { + :root { + --ink: #f5f1e6; + --ink-soft: #ffffff; + --ink-softer: #ece5d6; + --paper: #ffffff; + --paper-dim: #ece5d6; + --gate: #128a6d; + --gate-deep: #0d6d55; + --brass: #97762c; + --brass-soft: #b3924a; + --fg: #16211f; + --muted: #5c6b66; + --line: rgba(22, 33, 31, 0.12); + --line-strong: rgba(22, 33, 31, 0.22); + --code-bg: #16211f; + color-scheme: light; + } +} + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + background: + radial-gradient(1100px 620px at 82% -10%, color-mix(in srgb, var(--gate) 14%, transparent), transparent 60%), + radial-gradient(900px 500px at -10% 15%, color-mix(in srgb, var(--brass) 10%, transparent), transparent 55%), + var(--ink); + color: var(--fg); + font-family: var(--font-body); + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} +a { color: var(--gate); text-decoration: none; } +a:hover { text-decoration: underline; } +:focus-visible { + outline: 2px solid var(--brass); + outline-offset: 3px; + border-radius: 4px; +} + +/* ---------- nav ---------- */ +.nav { + display: flex; + align-items: center; + justify-content: space-between; + max-width: 1120px; + margin: 0 auto; + padding: 22px 24px; + gap: 16px; + flex-wrap: wrap; +} +.brand { display: inline-flex; align-items: center; gap: 10px; color: var(--fg); } +.brand:hover { text-decoration: none; } +.brand .mark { width: 30px; height: 30px; flex: none; } +.brand .wordmark { + font-family: var(--font-display); + font-size: 21px; + font-weight: 600; + letter-spacing: -0.01em; +} +.nav .links { display: flex; gap: 6px; flex-wrap: wrap; } +.nav .links a { + color: var(--muted); + padding: 7px 12px; + border-radius: 9px; + font-size: 14px; + font-weight: 500; +} +.nav .links a:hover { color: var(--fg); background: var(--ink-soft); text-decoration: none; } +.nav .links a.active { color: var(--fg); background: var(--ink-soft); } + +/* ---------- hero ---------- */ +.hero { padding: 56px 24px 24px; } +.hero-grid { + display: grid; + grid-template-columns: minmax(0, 1.05fr) minmax(280px, 0.95fr); + gap: 56px; + align-items: center; + max-width: 1120px; + margin: 0 auto; +} +@media (max-width: 900px) { + .hero-grid { grid-template-columns: 1fr; gap: 40px; } +} +.eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 12.5px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--brass); + margin-bottom: 18px; +} +.eyebrow::before { + content: ""; + width: 7px; height: 7px; + border-radius: 50%; + background: var(--brass); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--brass) 25%, transparent); +} +.hero h1 { + font-family: var(--font-display); + font-size: clamp(38px, 5.4vw, 68px); + font-weight: 600; + letter-spacing: -0.02em; + line-height: 1.02; + margin: 0 0 22px; + color: var(--fg); +} +.hero h1 em { font-style: italic; color: var(--gate); } +.hero p.lead { + color: var(--muted); + font-size: 17.5px; + max-width: 500px; + margin: 0 0 30px; +} +.cta-row { display: flex; gap: 12px; flex-wrap: wrap; } +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 13px 24px; + border-radius: 11px; + font-weight: 600; + font-size: 14.5px; + border: 1px solid var(--line-strong); + transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease; +} +.btn.primary { + background: linear-gradient(160deg, var(--gate), var(--gate-deep)); + color: #04120e; + border: none; + box-shadow: 0 12px 28px color-mix(in srgb, var(--gate) 32%, transparent); +} +.btn.primary:hover { transform: translateY(-1px); text-decoration: none; } +.btn.secondary { color: var(--fg); } +.btn.secondary:hover { background: var(--ink-soft); text-decoration: none; transform: translateY(-1px); } + +/* ---------- signature visual: one snapshot, four verdicts ---------- + * The product's whole argument in one picture: the same item reaching four + * surfaces and getting four different answers. A single "hidden" boolean + * cannot draw this. + */ +.gate-scene { + position: relative; + display: grid; + gap: 14px; + justify-items: stretch; +} +.item-card { + background: linear-gradient(165deg, var(--ink-softer), var(--ink-soft)); + border: 1px solid color-mix(in srgb, var(--brass) 40%, var(--line)); + border-radius: 16px; + padding: 16px 18px; + box-shadow: var(--shadow); + display: grid; + gap: 10px; +} +.item-top { display: flex; align-items: center; gap: 12px; } +.item-thumb { + width: 42px; height: 42px; + border-radius: 10px; + flex: none; + background: linear-gradient(150deg, var(--gate), var(--gate-deep)); + display: grid; + place-items: center; +} +.item-thumb svg { width: 22px; height: 22px; } +.item-label { font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); } +.item-name { font-family: var(--font-display); font-size: 16px; font-weight: 600; } +.item-evidence { + font-size: 12.5px; + color: var(--muted); + border-top: 1px dashed var(--line); + padding-top: 9px; +} +.item-evidence b { color: var(--brass); font-weight: 600; } + +.verdict-rail { display: grid; gap: 8px; } +.verdict-row { + display: grid; + grid-template-columns: 20px 1fr auto; + align-items: center; + gap: 11px; + padding: 11px 14px; + background: var(--ink-soft); + border: 1px solid var(--line); + border-radius: 12px; + font-size: 13.5px; + animation: rise 0.5s ease both; + animation-delay: calc(var(--row-order, 0) * 90ms); +} +@media (prefers-reduced-motion: reduce) { + .verdict-row { animation: none; } +} +@keyframes rise { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: none; } +} +.verdict-icon { color: var(--muted); display: grid; place-items: center; } +.verdict-icon svg { width: 16px; height: 16px; } +.verdict-surface { color: var(--fg); } +.verdict-surface small { display: block; color: var(--muted); font-size: 11.5px; } +.verdict-stamp { + font-family: var(--font-mono); + font-size: 11.5px; + padding: 3px 9px; + border-radius: 999px; + border: 1px solid var(--line-strong); + white-space: nowrap; +} +.verdict-stamp.pass { color: var(--gate); border-color: color-mix(in srgb, var(--gate) 45%, transparent); } +.verdict-stamp.soft { color: var(--brass); border-color: color-mix(in srgb, var(--brass) 45%, transparent); } +.verdict-stamp.hard { color: #f08a7a; border-color: color-mix(in srgb, #f08a7a 45%, transparent); } +.scene-caption { color: var(--muted); font-size: 12.5px; text-align: center; } + +/* ---------- layout ---------- */ +.container { max-width: 1120px; margin: 0 auto; padding: 0 24px; } +.container.narrow { max-width: 760px; } +section { padding: 20px 0 40px; } +h2 { + font-family: var(--font-display); + font-size: 30px; + font-weight: 600; + letter-spacing: -0.01em; + margin: 0 0 6px; +} +p.section-sub { color: var(--muted); margin: 0 0 28px; max-width: 660px; } + +/* ---------- dimension table ---------- */ +.ladder { display: grid; gap: 10px; margin: 30px 0; } +.ladder-row { + display: grid; + grid-template-columns: minmax(120px, 160px) 1fr; + gap: 18px; + align-items: baseline; + background: var(--ink-soft); + border: 1px solid var(--line); + border-radius: 12px; + padding: 14px 18px; +} +@media (max-width: 640px) { + .ladder-row { grid-template-columns: 1fr; gap: 6px; } +} +.ladder-row .dim { + font-family: var(--font-mono); + font-size: 13px; + color: var(--brass); +} +.ladder-row .steps { font-size: 13.5px; color: var(--muted); font-family: var(--font-mono); } +.ladder-row .steps b { color: var(--fg); font-weight: 600; } + +/* ---------- feature grid ---------- */ +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); + gap: 16px; + margin: 40px 0; +} +.feature { + background: var(--ink-soft); + border: 1px solid var(--line); + border-radius: 14px; + padding: 20px; + transition: border-color 0.15s ease, transform 0.15s ease; +} +.feature:hover { border-color: var(--line-strong); transform: translateY(-2px); } +.feature h3 { margin: 0 0 8px; font-size: 15.5px; font-family: var(--font-display); font-weight: 600; } +.feature p { margin: 0; color: var(--muted); font-size: 13.5px; } + +/* ---------- pairing with bitlogin ---------- */ +.pair { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 18px; + align-items: center; + margin: 32px 0; +} +@media (max-width: 760px) { + .pair { grid-template-columns: 1fr; } + .pair .pair-join { justify-self: center; } +} +.pair-card { + background: var(--ink-soft); + border: 1px solid var(--line); + border-radius: 16px; + padding: 22px; +} +.pair-card .pair-kicker { + font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.07em; + color: var(--muted); display: block; margin-bottom: 6px; +} +.pair-card h3 { margin: 0 0 6px; font-family: var(--font-display); font-size: 17px; font-weight: 600; } +.pair-card p { margin: 0; color: var(--muted); font-size: 13.5px; } +.pair-card.is-gate { + border-color: color-mix(in srgb, var(--brass) 55%, var(--line)); + background: linear-gradient(165deg, color-mix(in srgb, var(--gate) 14%, var(--ink-soft)), var(--ink-soft)); +} +.pair-card.is-gate .pair-kicker { color: var(--brass); } +.pair-join { color: var(--brass); font-size: 22px; text-align: center; } + +/* ---------- code ---------- */ +pre { + background: var(--code-bg); + color: #e9f1ee; + border: 1px solid var(--line); + border-radius: 12px; + padding: 18px; + overflow-x: auto; + font-size: 13px; + line-height: 1.55; +} +code { font-family: var(--font-mono); } +:not(pre) > code { + background: var(--ink-soft); + border: 1px solid var(--line); + border-radius: 5px; + padding: 1px 5px; + font-size: 0.92em; +} + +/* ---------- steps and callouts ---------- */ +.step-list { counter-reset: step; list-style: none; padding: 0; margin: 0; } +.step-list > li { + counter-increment: step; + position: relative; + padding: 0 0 28px 44px; + border-left: 2px solid var(--line); + margin-left: 15px; +} +.step-list > li:last-child { border-left-color: transparent; padding-bottom: 0; } +.step-list > li::before { + content: counter(step); + position: absolute; + left: -15px; + top: 0; + width: 28px; height: 28px; + border-radius: 50%; + background: linear-gradient(155deg, var(--gate), var(--gate-deep)); + color: #04120e; + font-weight: 700; + font-size: 13px; + display: flex; + align-items: center; + justify-content: center; +} +.step-list h3 { margin: 2px 0 8px; font-size: 16px; font-family: var(--font-display); font-weight: 600; } +.step-list p { color: var(--muted); font-size: 14px; } + +.callout { + border: 1px solid var(--line); + background: var(--ink-soft); + border-radius: 12px; + padding: 14px 16px; + font-size: 13.5px; + color: var(--muted); + margin: 18px 0; +} +.callout strong { color: var(--fg); } +.callout.warn { border-color: color-mix(in srgb, var(--brass) 45%, var(--line)); } + +/* ---------- demo sandbox ---------- */ +.sandbox { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(280px, 360px); + gap: 28px; + align-items: start; + margin: 30px 0 10px; +} +@media (max-width: 900px) { + .sandbox { grid-template-columns: 1fr; } +} +.panel { + background: var(--ink-soft); + border: 1px solid var(--line); + border-radius: 16px; + padding: 20px; +} +.panel h3 { + margin: 0 0 4px; + font-family: var(--font-display); + font-size: 16px; + font-weight: 600; +} +.panel .panel-sub { color: var(--muted); font-size: 12.5px; margin: 0 0 16px; } +.control-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; } +.control-row button { + font: inherit; + font-size: 13px; + color: var(--fg); + background: var(--ink-softer); + border: 1px solid var(--line-strong); + border-radius: 9px; + padding: 8px 13px; + cursor: pointer; + transition: border-color 0.15s ease, transform 0.1s ease; +} +.control-row button:hover { border-color: var(--brass); transform: translateY(-1px); } +.control-row button.reset { color: var(--muted); } + +.demo-card { + border: 1px solid var(--line); + border-radius: 14px; + overflow: hidden; + background: var(--ink-softer); + max-width: 320px; +} +.demo-card .art { + aspect-ratio: 16 / 9; + background: linear-gradient(135deg, color-mix(in srgb, var(--gate) 35%, var(--ink-softer)), var(--ink-soft)); + display: grid; + place-items: center; + color: var(--fg); + font-family: var(--font-display); + font-size: 19px; +} +.demo-card .meta { padding: 12px 14px; font-size: 13.5px; } +.demo-card .meta .price { color: var(--muted); font-size: 12.5px; } + +.surface-table { width: 100%; border-collapse: collapse; font-size: 13px; } +.surface-table th, .surface-table td { + text-align: left; + padding: 8px 6px; + border-bottom: 1px solid var(--line); +} +.surface-table th { color: var(--muted); font-weight: 500; font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.06em; } +.surface-table td code { font-size: 12px; } +.surface-table tr:last-child td { border-bottom: none; } + +.verdict { + font-family: var(--font-mono); + font-size: 12px; + padding: 2px 8px; + border-radius: 999px; + border: 1px solid var(--line-strong); +} +.verdict.allow { color: var(--gate); border-color: color-mix(in srgb, var(--gate) 45%, transparent); } +.verdict.warn, .verdict.restrict, .verdict.require-review, .verdict.require-explicit-action, .verdict.downrank { + color: var(--brass); + border-color: color-mix(in srgb, var(--brass) 45%, transparent); +} +.verdict.hide, .verdict.deny { color: #f08a7a; border-color: color-mix(in srgb, #f08a7a 45%, transparent); } + +.reason-list { margin: 10px 0 0; padding-left: 18px; color: var(--muted); font-size: 12.5px; } + +/* ---------- footer ---------- */ +footer { + border-top: 1px solid var(--line); + padding: 28px 24px; + text-align: center; + color: var(--muted); + font-size: 13px; + margin-top: 40px; +} +footer .footer-links { margin-top: 8px; display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; } diff --git a/packages/site/public/demo.html b/packages/site/public/demo.html new file mode 100644 index 0000000..2724efa --- /dev/null +++ b/packages/site/public/demo.html @@ -0,0 +1,284 @@ + + +
+ + ++ This runs the real engine in your browser against an in-memory relay, so + nothing is published and nothing is fetched. Change the evidence on the + right and watch every surface re-decide. +
+Wrapped in a real <bitgate-veil>, using the marketplace preset.
One snapshot, evaluated four ways.
+| Surface | Visibility | Transaction | Rank |
|---|
Everything here is signed state the engine reduces.
+ +Capability checks are enforced in the command layer, not the UI.
++ In production the provider takes relay URLs. Here it is handed an in-memory + runtime so the page works offline and publishes nothing. +
+<bitgate-provider relays="wss://relay.example" root="<root>" policy="commerce">
+ <bitgate-veil profile="browse" target-address="30078:<seller>:sku-001">
+ …your listing markup…
+ </bitgate-veil>
+</bitgate-provider>
+
++ No bundler required. Build the widget once, self-host it, and configure + everything in markup. Then replace the preset with thresholds that fit + your own community. +
++ Clone the repository and build. The output is a single self-contained + file with no runtime dependencies. +
+git clone https://github.com/PR0M3TH3AN/Nostr-Governance
+cd Nostr-Governance
+npm install
+npm run build:widget # → packages/widget/dist/bitgate.js
+
+ <bitgate-provider> builds the runtime, loads
+ administrative state from your relays, and shares both with every
+ element inside it.
+
<script type="module" src="/vendor/bitgate/bitgate.js"></script>
+
+<bitgate-provider
+ relays="wss://relay-one.example,wss://relay-two.example"
+ root="<your-root-pubkey>"
+ policy="social">
+ <!-- everything governed goes in here -->
+</bitgate-provider>
+
+ root is your deployment's administrator pubkey — the key
+ that publishes the moderator roster. It is configuration, not something
+ discovered at runtime.
+
+ Elements read their target from attributes, so a server-rendered page + needs no JavaScript at all. +
+<bitgate-veil profile="feed" target-user="<author-pubkey>">
+ <article>…your post markup…</article>
+</bitgate-veil>
+
+<bitgate-veil profile="feed" target-event="<event-id>" target-author="<pubkey>">…</bitgate-veil>
+
+<bitgate-veil profile="browse" target-address="30078:<pubkey>:sku-001">…</bitgate-veil>
+
+ The veil applies the decision: blur on restrict, a
+ disclosure with a "show anyway" control on hide, a warning
+ line on warn. Assigning .target in JavaScript
+ still works and wins over the attribute.
+
+ BitGate needs to know who is looking, both to build the trust graph and + to check what they may do. BitLogin's provider fits without an adapter. +
+const provider = document.querySelector("bitgate-provider");
+await provider.ready;
+await provider.useSigner(window.nostr);
+
+// BitGate reads the follow graph from your app — it has no opinion
+// about how you fetch kind:3.
+provider.runtime.trust.setContacts(followList);
+ ready, don't listen for it. The
+ bitgate:ready event fires during element upgrade, which for a
+ page that defines elements after parsing is before a listener could be
+ attached. The promise never has that race.
+ + Presets exist so the first run works, not because the engine has an + opinion. Every number in them is a starting point. +
+import { createPolicyDefinition } from "@bitgate/core";
+
+const policy = createPolicyDefinition({
+ id: "my-app",
+ version: "1.0.0",
+ defaultProfile: "feed",
+ profiles: {
+ feed: {
+ name: "feed",
+ administrativeDeny: { visibility: "hide", interaction: "deny" },
+ reports: {
+ malware: { hide: 1, interactionDeny: 1 },
+ spam: { downrank: 2, restrict: 4, hide: 8 },
+ default: { downrank: 2, warn: 4 },
+ },
+ mutes: { default: { downrank: 1, hide: 12 } },
+ muteWindowSeconds: 60 * 24 * 60 * 60,
+ },
+ },
+});
+
+provider.runtime.policies.setLocalPolicy(policy);
+ Embed only what you need; each is independent.
+ +| Element | What it does |
|---|---|
bitgate-provider | Builds the runtime from attributes and shares it |
bitgate-veil | Applies a decision to whatever it wraps |
bitgate-report | Report dialog — needs no capability, anyone may report |
bitgate-status | Effect, reasons, and evidence for one target |
bitgate-capabilities | What the signed-in account may actually do |
bitgate-action | A capability-gated button that explains refusals |
bitgate-admin-panel | Effective state, attribution, and controls |
Set with the policy attribute.
| Name | Profiles | For |
|---|---|---|
social | feed, profile, playback | Timelines and posts. Discovery declines to hard-hide. |
commerce | browse, detail, checkout, seller-dashboard | Marketplaces. Checkout decides transactions only. |
admin-only | default | Operator deny lists, with no trust-graph signals at all. |
0 means disabled, not
+ "fires on everything" — so zeroing a value is a safe way to switch it off.
+ If your renderer compares report counts to thresholds, the point has been lost. Consume the decision.
+Evaluate transaction-critical actions against their own profile at the moment of action. State moves.
+setViewer clears viewer-scoped state by design. Set contacts, blocks, and overrides after it.
Public surfaces get counts; appeal surfaces get identities. exposeEvidence is off by default for a reason.
+ The full guide, including the adapter contract and every profile option, + lives in + docs/integration-guide.md. +
++ Ask BitGate what to do with a post, an account, or a listing. Get back an + answer built from who the viewer trusts and who you authorized — then + apply it. No thresholds to hand-roll, no ban logic scattered through + your views. +
+ +
+ Most moderation code collapses everything into hidden: true, then
+ discovers it needs "visible but unbuyable", or "shown but ranked last", and
+ grows a second system to say so. BitGate answers four questions at once, each
+ on its own ladder.
+
+ Two independent sources, deliberately kept apart. Being allowed to publish + grants no trust; being trusted grants no authority. +
+ ++ Reports and mutes count only when they come from the viewer's own trust + graph. The same content can be hidden for one person and fine for + another — because it genuinely is. +
++ A root key publishes the roster; each moderator publishes under their + own key. Revoke a role and their entries stop counting immediately, with + nothing to rewrite. +
++ Subscribe to someone else's deny list without handing them moderator + powers. A curator can contribute names and nothing more. +
++ Personal blocks always win, and "show it anyway" is available wherever + your policy permits it. Hidden content says why, instead of silently + vanishing. +
++ Two halves of the same question. Drop both in and a signed-in visitor gets + moderation shaped by their own follow graph. +
+ +A login name and password that unlock a portable Nostr identity, with no account server.
+A decision per item, per surface, built from that identity's trust graph and your roster.
+const provider = document.querySelector("bitgate-provider");
+await provider.ready;
+await provider.useSigner(window.nostr); // BitLogin's NIP-07 provider
++ One script tag and one element. The provider builds the runtime, loads your + administrative state, and every element inside finds it — no wiring code. +
+ +<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>
+
+
++ The engine carries no thresholds of its own. Presets ship in the box so the + first run takes one attribute; every number in them is a starting point you + are meant to replace. +
+ +Pure evaluation. No I/O, no browser globals, no dependencies. Runs the same in Node and a browser.
+A WebSocket transport ships for convenience. Already have a relay pool? Pass an adapter instead.
+An adapter says what a product or post is in governance terms. Deny a seller and their whole catalogue goes with them.
+Every decision carries stable reason identifiers and the evidence behind it — so appeals are possible.
+