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 @@ + + + + + +Try BitGate — live demo + + + + + + + + +
+
+ Live demo +

Same evidence.
Different answers.

+

+ 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. +

+
+
+ +
+
+
+

The listing

+

Wrapped in a real <bitgate-veil>, using the marketplace preset.

+ + +
+
Refurbished widget
+
+
Refurbished widget
+
12,000 sats · sold by d4d4…d4d4
+
+
+
+ +

Every surface, right now

+

One snapshot, evaluated four ways.

+ + + + + +
SurfaceVisibilityTransactionRank
+ +
    +
    + +
    +

    Evidence

    +

    Everything here is signed state the engine reduces.

    + +
    + + +
    +
    + + +
    +
    + + +
    + +

    Signed in as

    +

    Capability checks are enforced in the command layer, not the UI.

    +
    + + + +
    + + +
    +
    + +
    + Note the seller's view. However strong the evidence, the + seller dashboard never hides the listing from its own seller — hiding it + would leave no route to appeal. +
    +
    + +
    +

    The markup behind this

    +

    + 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>
    +
    + Integration guide → + Read the source +
    +
    + + + + + + + diff --git a/packages/site/public/docs.html b/packages/site/public/docs.html new file mode 100644 index 0000000..b395fe9 --- /dev/null +++ b/packages/site/public/docs.html @@ -0,0 +1,232 @@ + + + + + +Integrate BitGate — docs + + + + + + + + +
    +
    + Integration guide +

    Add BitGate to your site

    +

    + 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. +

    +
    +
    + +
    +
      +
    1. +

      Build the widget from source

      +

      + 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
      +
      + The bundle is deliberately not committed. A moderation + widget decides what people can see; build it from source you have read + rather than trusting a prebuilt minified blob. Copy the result onto your + own origin — do not hot-link this site's copy. +
      +
    2. + +
    3. +

      Include the script and the provider

      +

      + <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. +

      +
    4. + +
    5. +

      Wrap what should be governed

      +

      + 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. +

      +
    6. + +
    7. +

      Connect a signer

      +

      + 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);
      +
      + Await 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. +
      +
    8. + +
    9. +

      Replace the preset

      +

      + 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);
      +
    10. +
    +
    + +
    +

    Elements

    +

    Embed only what you need; each is independent.

    + + + + + + + + + + + + + + +
    ElementWhat it does
    bitgate-providerBuilds the runtime from attributes and shares it
    bitgate-veilApplies a decision to whatever it wraps
    bitgate-reportReport dialog — needs no capability, anyone may report
    bitgate-statusEffect, reasons, and evidence for one target
    bitgate-capabilitiesWhat the signed-in account may actually do
    bitgate-actionA capability-gated button that explains refusals
    bitgate-admin-panelEffective state, attribution, and controls
    +
    + +
    +

    Policy presets

    +

    Set with the policy attribute.

    + + + + + + + + +
    NameProfilesFor
    socialfeed, profile, playbackTimelines and posts. Discovery declines to hard-hide.
    commercebrowse, detail, checkout, seller-dashboardMarketplaces. Checkout decides transactions only.
    admin-onlydefaultOperator deny lists, with no trust-graph signals at all.
    + +
    + A gate threshold of 0 means disabled, not + "fires on everything" — so zeroing a value is a safe way to switch it off. +
    +
    + +
    +

    Common mistakes

    + +
    +
    +

    Recomputing policy in your views

    +

    If your renderer compares report counts to thresholds, the point has been lost. Consume the decision.

    +
    +
    +

    Reusing a listing-page verdict at checkout

    +

    Evaluate transaction-critical actions against their own profile at the moment of action. State moves.

    +
    +
    +

    Seeding trust before the viewer

    +

    setViewer clears viewer-scoped state by design. Set contacts, blocks, and overrides after it.

    +
    +
    +

    Exposing evidence everywhere

    +

    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. +

    +
    + + + + + diff --git a/packages/site/public/index.html b/packages/site/public/index.html new file mode 100644 index 0000000..532d8fc --- /dev/null +++ b/packages/site/public/index.html @@ -0,0 +1,249 @@ + + + + + +BitGate — moderation your users actually control + + + + + + + + +
    +
    +
    + Moderation, without the policy code +

    One decision.
    Every door it guards.

    +

    + 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. +

    + +
    + + +
    +
    + +
    +

    A decision is not a boolean

    +

    + 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. +

    + +
    +
    + ranking + normal → downrank +
    +
    + visibility + allow → warn → restrict → hide → deny +
    +
    + interaction + allow → require-explicit-action → deny +
    +
    + transaction + allow → require-review → deny +
    +
    + +
    + Downranking is a sort input, not a filter. A downranked item + still appears and can still be acted on. Conflating the two is what makes + moderation feel like censorship to the people on the receiving end. +
    +
    + +
    +

    Where the answer comes from

    +

    + Two independent sources, deliberately kept apart. Being allowed to publish + grants no trust; being trusted grants no authority. +

    + +
    +
    +

    People the viewer follows

    +

    + 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. +

    +
    +
    +

    Moderators you authorized

    +

    + 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. +

    +
    +
    +

    Community curators

    +

    + Subscribe to someone else's deny list without handing them moderator + powers. A curator can contribute names and nothing more. +

    +
    +
    +

    The viewer's own choices

    +

    + Personal blocks always win, and "show it anyway" is available wherever + your policy permits it. Hidden content says why, instead of silently + vanishing. +

    +
    +
    +
    + +
    +

    Pairs with BitLogin

    +

    + Two halves of the same question. Drop both in and a signed-in visitor gets + moderation shaped by their own follow graph. +

    + +
    +
    + BitLogin +

    Who are you?

    +

    A login name and password that unlock a portable Nostr identity, with no account server.

    +
    +
    +
    +
    + BitGate +

    What may you see, and do?

    +

    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
    +
    + +
    +

    Drop it in

    +

    + 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>
    + +
    + Integration guide → + See it running +
    +
    + +
    +

    Built to be borrowed

    +

    + 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. +

    + +
    +
    +

    Headless core

    +

    Pure evaluation. No I/O, no browser globals, no dependencies. Runs the same in Node and a browser.

    +
    +
    +

    Bring your own relays

    +

    A WebSocket transport ships for convenience. Already have a relay pool? Pass an adapter instead.

    +
    +
    +

    Your objects, your targets

    +

    An adapter says what a product or post is in governance terms. Deny a seller and their whole catalogue goes with them.

    +
    +
    +

    Explains itself

    +

    Every decision carries stable reason identifiers and the evidence behind it — so appeals are possible.

    +
    +
    +
    + + + + + diff --git a/packages/site/serve.mjs b/packages/site/serve.mjs new file mode 100644 index 0000000..0b85f8f --- /dev/null +++ b/packages/site/serve.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/** Serve the built site locally for review. */ +import { createServer } from "node:http"; +import { readFile } from "node:fs/promises"; +import { dirname, extname, join, normalize } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(dirname(fileURLToPath(import.meta.url)), "dist"); +const port = Number(process.env.PORT ?? 4174); + +const TYPES = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".map": "application/json; charset=utf-8", + ".svg": "image/svg+xml", +}; + +createServer(async (request, response) => { + const url = new URL(request.url ?? "/", "http://localhost"); + let path = normalize(decodeURIComponent(url.pathname)); + if (path.endsWith("/")) path += "index.html"; + + try { + const body = await readFile(join(root, path)); + response.writeHead(200, { "content-type": TYPES[extname(path)] ?? "application/octet-stream" }); + response.end(body); + } catch { + response.writeHead(404, { "content-type": "text/plain" }); + response.end("Not found"); + } +}).listen(port, () => console.log(`BitGate site → http://localhost:${port}`)); diff --git a/packages/widget/src/index.js b/packages/widget/src/index.js index fe89e37..b21201c 100644 --- a/packages/widget/src/index.js +++ b/packages/widget/src/index.js @@ -8,6 +8,30 @@ // Elements receive a runtime rather than creating one, so a host application // can share a single governance runtime across an entire page. +// The bundle is the browser distribution of BitGate as a whole, not only its +// elements: a static page has no package manager, so anything it might need +// has to be reachable from this one file. +export { + createBitGate, + createCommands, + createRelayTransport, + createMemoryTransport, + createMemoryStorage, + ERROR_CODES, +} from "@bitgate/runtime"; + +export { + SOCIAL_POLICY, + COMMERCE_POLICY, + ADMIN_ONLY_POLICY, + POLICY_PRESETS, + getPolicyPreset, + createPolicyDefinition, + createApplicationAdapter, + evaluateObject, + collectTargets, +} from "@bitgate/core"; + export { BASE_STYLES, GovernanceElement, diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..4be7b40 --- /dev/null +++ b/vercel.json @@ -0,0 +1,25 @@ +{ + "buildCommand": "npm run build:widget && npm run build:site", + "outputDirectory": "packages/site/dist", + "cleanUrls": true, + "headers": [ + { + "source": "/(.*)", + "headers": [ + { + "key": "Content-Security-Policy", + "value": "default-src 'none'; script-src 'self'; connect-src 'self' wss:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; object-src 'none'; upgrade-insecure-requests" + }, + { "key": "X-Content-Type-Options", "value": "nosniff" }, + { "key": "Referrer-Policy", "value": "no-referrer" }, + { + "key": "Permissions-Policy", + "value": "camera=(), microphone=(), geolocation=(), payment=(), usb=()" + }, + { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" }, + { "key": "Cross-Origin-Resource-Policy", "value": "same-origin" }, + { "key": "X-Frame-Options", "value": "DENY" } + ] + } + ] +}