feat(site): BitGate landing page, integration guide, and live demo

A sibling of the BitLogin design system rather than a twin: the same
ink/paper/brass foundation and serif display, with verdigris — the patina brass
takes as it ages — where BitLogin uses its violet seal.

- index: the signature visual is the product's whole argument, one listing
  reaching four surfaces and getting four different verdicts
- docs: five-step integration, elements table, presets, common mistakes
- demo: the real engine running in the browser against an in-memory transport,
  so nothing is fetched or published. Add reports, restrict the seller, switch
  moderator identities, and watch every surface re-decide.

The widget bundle now re-exports the runtime and core entry points, because a
static page has no package manager — anything it might need has to be reachable
from the one file it loads.

Also fixes the site build reusing a stale widget bundle. It only rebuilt when
the file was missing, which is precisely how a broken demo ships: the page
loads, the module resolves, and an export added since the last build is
silently absent. Caught by running it.

vercel.json mirrors BitLogin's security headers, with connect-src widened to
wss: for relay traffic.
This commit is contained in:
thePR0M3TH3AN committed 2026-08-09 20:30:03 -04:00
1 parent 729c0acc43
commit b24bf3955d
14 files changed
+1423 -1

No files matched your search

+3
View File
@@ -9,3 +9,6 @@ coverage/
.turbo/ .turbo/
.cache/ .cache/
*.tsbuildinfo *.tsbuildinfo
# Built site output (Vercel builds it)
packages/site/dist/
+8
View File
@@ -50,6 +50,10 @@
"resolved": "packages/runtime", "resolved": "packages/runtime",
"link": true "link": true
}, },
"node_modules/@bitgate/site": {
"resolved": "packages/site",
"link": true
},
"node_modules/@bitgate/testing": { "node_modules/@bitgate/testing": {
"resolved": "packages/testing", "resolved": "packages/testing",
"link": true "link": true
@@ -1798,6 +1802,10 @@
"@bitgate/nostr": "*" "@bitgate/nostr": "*"
} }
}, },
"packages/site": {
"name": "@bitgate/site",
"version": "0.1.0"
},
"packages/testing": { "packages/testing": {
"name": "@bitgate/testing", "name": "@bitgate/testing",
"version": "0.1.0", "version": "0.1.0",
+3 -1
View File
@@ -14,7 +14,9 @@
"check:references": "node scripts/verify-no-reference-imports.mjs", "check:references": "node scripts/verify-no-reference-imports.mjs",
"check:types": "tsc -p tsconfig.json --noEmit && tsc -p packages/widget/tsconfig.json --noEmit", "check:types": "tsc -p tsconfig.json --noEmit && tsc -p packages/widget/tsconfig.json --noEmit",
"build:widget": "node packages/widget/build.mjs", "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": { "engines": {
"node": ">=22" "node": ">=22"
+33
View File
@@ -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}`);
+11
View File
@@ -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"
}
}
+18
View File
@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="BitGate">
<defs>
<linearGradient id="bg-gate" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#3fc9a4"/>
<stop offset="1" stop-color="#1f9b7d"/>
</linearGradient>
<linearGradient id="bg-brass" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#e6c481"/>
<stop offset="1" stop-color="#b3924a"/>
</linearGradient>
</defs>
<!-- Brass ring: the family mark shared with BitLogin. -->
<circle cx="32" cy="32" r="21" fill="none" stroke="url(#bg-brass)" stroke-width="2.5"/>
<!-- The gate itself: an arch with one leaf swung open. -->
<path d="M21 43V27a11 11 0 0 1 22 0v16" fill="none" stroke="url(#bg-gate)" stroke-width="3.4" stroke-linecap="round"/>
<path d="M32 43V21" stroke="url(#bg-brass)" stroke-width="2.6" stroke-linecap="round"/>
<path d="M36 34.5l3.4 3.4 6.6-7.4" fill="none" stroke="url(#bg-gate)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="BitGate">
<defs>
<linearGradient id="m-gate" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#3fc9a4"/><stop offset="1" stop-color="#1f9b7d"/>
</linearGradient>
<linearGradient id="m-brass" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#e6c481"/><stop offset="1" stop-color="#b3924a"/>
</linearGradient>
</defs>
<circle cx="32" cy="32" r="21" fill="none" stroke="url(#m-brass)" stroke-width="2.5"/>
<path d="M21 43V27a11 11 0 0 1 22 0v16" fill="none" stroke="url(#m-gate)" stroke-width="3.4" stroke-linecap="round"/>
<path d="M32 43V21" stroke="url(#m-brass)" stroke-width="2.6" stroke-linecap="round"/>
<path d="M36 34.5l3.4 3.4 6.6-7.4" fill="none" stroke="url(#m-gate)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 891 B

+487
View File
@@ -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; }
+284
View File
@@ -0,0 +1,284 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Try BitGate — live demo</title>
<meta name="description" content="Add reports, restrict a seller, switch moderators — and watch four surfaces reach four different conclusions from the same evidence." />
<link rel="icon" href="./assets/icon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="./assets/site.css" />
</head>
<body>
<nav class="nav">
<a class="brand" href="./index.html">
<img class="mark" src="./assets/mark.svg" alt="" width="30" height="30" />
<span class="wordmark">BitGate</span>
</a>
<div class="links">
<a href="./index.html">Overview</a>
<a href="./docs.html">Integrate</a>
<a class="active" href="./demo.html">Try it</a>
<a href="https://github.com/PR0M3TH3AN/Nostr-Governance">Source</a>
</div>
</nav>
<header class="hero" style="padding-top:48px; padding-bottom:0">
<div class="container narrow" style="padding:0">
<span class="eyebrow">Live demo</span>
<h1 style="font-size:clamp(32px, 4.5vw, 44px)">Same evidence.<br />Different <em>answers</em>.</h1>
<p class="lead" style="max-width:560px">
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.
</p>
</div>
</header>
<section class="container">
<div class="sandbox">
<div class="panel">
<h3>The listing</h3>
<p class="panel-sub">Wrapped in a real <code>&lt;bitgate-veil&gt;</code>, using the marketplace preset.</p>
<bitgate-veil id="veil" profile="browse">
<div class="demo-card">
<div class="art">Refurbished widget</div>
<div class="meta">
<div><strong>Refurbished widget</strong></div>
<div class="price">12,000 sats · sold by d4d4…d4d4</div>
</div>
</div>
</bitgate-veil>
<h3 style="margin-top:26px">Every surface, right now</h3>
<p class="panel-sub">One snapshot, evaluated four ways.</p>
<table class="surface-table" id="surfaces">
<thead>
<tr><th>Surface</th><th>Visibility</th><th>Transaction</th><th>Rank</th></tr>
</thead>
<tbody></tbody>
</table>
<ul class="reason-list" id="reasons"></ul>
</div>
<div class="panel">
<h3>Evidence</h3>
<p class="panel-sub">Everything here is signed state the engine reduces.</p>
<div class="control-row">
<button type="button" id="add-scam">+1 scam report</button>
<button type="button" id="add-malware">+1 malware report</button>
</div>
<div class="control-row">
<button type="button" id="add-mute">+1 trusted mute</button>
<button type="button" id="deny-seller">Restrict the seller</button>
</div>
<div class="control-row">
<button type="button" id="block">Block them yourself</button>
<button type="button" class="reset" id="reset">Reset</button>
</div>
<h3 style="margin-top:24px">Signed in as</h3>
<p class="panel-sub">Capability checks are enforced in the command layer, not the UI.</p>
<div class="control-row">
<button type="button" data-viewer="shopper">Shopper</button>
<button type="button" data-viewer="listing">Listing moderator</button>
<button type="button" data-viewer="seller">Seller moderator</button>
</div>
<bitgate-capabilities id="caps"></bitgate-capabilities>
</div>
</div>
<div class="callout">
<strong>Note the seller's view.</strong> However strong the evidence, the
seller dashboard never hides the listing from its own seller — hiding it
would leave no route to appeal.
</div>
</section>
<section class="container narrow">
<h2>The markup behind this</h2>
<p class="section-sub">
In production the provider takes relay URLs. Here it is handed an in-memory
runtime so the page works offline and publishes nothing.
</p>
<pre><code>&lt;bitgate-provider relays="wss://relay.example" root="&lt;root&gt;" policy="commerce"&gt;
&lt;bitgate-veil profile="browse" target-address="30078:&lt;seller&gt;:sku-001"&gt;
…your listing markup…
&lt;/bitgate-veil&gt;
&lt;/bitgate-provider&gt;</code></pre>
<div class="cta-row" style="margin-top:20px">
<a class="btn primary" href="./docs.html">Integration guide →</a>
<a class="btn secondary" href="https://github.com/PR0M3TH3AN/Nostr-Governance">Read the source</a>
</div>
</section>
<footer>
<div>BitGate — GPL-3.0-or-later.</div>
<div class="footer-links">
<a href="./index.html">Overview</a>
<a href="./docs.html">Integrate</a>
<a href="https://github.com/PR0M3TH3AN/Nostr-Governance">Source</a>
</div>
</footer>
<script type="module">
import {
createBitGate,
createMemoryTransport,
defineBitGateElements,
COMMERCE_POLICY,
} from "./vendor/bitgate/bitgate.js";
defineBitGateElements();
const key = (seed) => seed.repeat(64).slice(0, 64);
const ROOT = key("a1");
const LISTING_MOD = key("b2");
const SELLER_MOD = key("c3");
const SELLER = key("d4");
const SHOPPER = key("e5");
const NOW = Math.floor(Date.now() / 1000);
const trusted = (index) => key(index.toString(16).padStart(2, "0"));
const TARGET = { type: "address", kind: "30078", pubkey: SELLER, identifier: "sku-001" };
const TARGET_KEY = `address:30078:${SELLER}:sku-001`;
const PROFILES = ["browse", "detail", "checkout", "seller-dashboard"];
const VIEWERS = { shopper: SHOPPER, listing: LISTING_MOD, seller: SELLER_MOD };
// An in-memory transport keeps the demo deterministic: nothing is fetched and
// nothing is published, so it works offline and cannot touch real relays.
const runtime = createBitGate({
applicationId: "bitgate-demo",
namespace: "demo",
root: ROOT,
transport: createMemoryTransport(),
policy: COMMERCE_POLICY,
now: () => NOW,
});
const COMMERCE_ROLES = {
listing_moderator: ["contribute-event-deny", "contribute-address-deny", "review-evidence"],
seller_moderator: ["contribute-user-deny", "review-evidence"],
super_admin: [
"manage-roles", "manage-policy", "manage-community-sources",
"contribute-user-allow", "contribute-user-deny", "contribute-event-deny",
"contribute-address-deny", "contribute-trust-seed", "review-evidence",
],
};
let reportCount = 0;
let muteCount = 0;
function seedRoster() {
runtime.admin.setRoles({
root: ROOT,
roles: COMMERCE_ROLES,
actors: {
[ROOT]: ["super_admin"],
[LISTING_MOD]: ["listing_moderator"],
[SELLER_MOD]: ["seller_moderator"],
},
});
}
function setViewer(name) {
runtime.setViewer(VIEWERS[name]);
// Viewer switching clears viewer-scoped state by design, so trust is
// re-seeded afterwards — the same order a real integration must use.
runtime.trust.setContacts(Array.from({ length: 24 }, (_, i) => trusted(i + 1)));
}
function reset() {
reportCount = 0;
muteCount = 0;
runtime.reports.clearTarget(TARGET_KEY);
runtime.mutes.lists.clear();
runtime.admin.setContributions([]);
seedRoster();
setViewer("shopper");
runtime.invalidateDecisions();
render();
}
const veil = document.getElementById("veil");
const caps = document.getElementById("caps");
veil.runtime = runtime;
veil.target = TARGET;
caps.runtime = runtime;
function render() {
const body = document.querySelector("#surfaces tbody");
body.innerHTML = PROFILES.map((profile) => {
const decision = runtime.evaluate(TARGET, { profile });
const transaction = decision.transaction?.effect ?? "allow";
return `<tr>
<td><code>${profile}</code></td>
<td><span class="verdict ${decision.visibility.effect}">${decision.visibility.effect}</span></td>
<td><span class="verdict ${transaction}">${transaction}</span></td>
<td><span class="verdict ${decision.ranking.effect === "downrank" ? "downrank" : "allow"}">${decision.ranking.effect}</span></td>
</tr>`;
}).join("");
const detail = runtime.evaluate(TARGET, { profile: "seller-dashboard" });
const reasons = detail.reasons.map((reason) => reason.id);
document.getElementById("reasons").innerHTML = reasons.length
? reasons.map((id) => `<li><code>${id}</code></li>`).join("")
: "<li>No governance signals yet — add some evidence.</li>";
}
runtime.on("change", render);
document.getElementById("add-scam").addEventListener("click", () => {
reportCount += 1;
runtime.reports.ingest(
{ reporter: trusted(reportCount), category: "scam", createdAt: NOW - 60 },
TARGET_KEY,
);
});
document.getElementById("add-malware").addEventListener("click", () => {
reportCount += 1;
runtime.reports.ingest(
{ reporter: trusted(reportCount), category: "malware", createdAt: NOW - 60 },
TARGET_KEY,
);
});
document.getElementById("add-mute").addEventListener("click", () => {
muteCount += 1;
runtime.mutes.replaceList({
owner: trusted(muteCount),
updatedAt: NOW - 60,
entries: [{ pubkey: SELLER }],
hasEncryptedEntries: false,
});
});
document.getElementById("deny-seller").addEventListener("click", () => {
runtime.admin.upsertContribution({
actor: SELLER_MOD,
kind: "user-deny",
targets: [{ type: "user", pubkey: SELLER }],
createdAt: NOW + reportCount + muteCount,
});
});
document.getElementById("block").addEventListener("click", () => {
runtime.trust.setBlocks([SELLER]);
});
document.getElementById("reset").addEventListener("click", reset);
for (const button of document.querySelectorAll("[data-viewer]")) {
button.addEventListener("click", () => setViewer(button.dataset.viewer));
}
reset();
</script>
</body>
</html>
+232
View File
@@ -0,0 +1,232 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Integrate BitGate — docs</title>
<meta name="description" content="Add BitGate moderation to any site: one script tag, one element, and a policy preset. Then tune the thresholds to fit your own community." />
<link rel="icon" href="./assets/icon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="./assets/site.css" />
</head>
<body>
<nav class="nav">
<a class="brand" href="./index.html">
<img class="mark" src="./assets/mark.svg" alt="" width="30" height="30" />
<span class="wordmark">BitGate</span>
</a>
<div class="links">
<a href="./index.html">Overview</a>
<a class="active" href="./docs.html">Integrate</a>
<a href="./demo.html">Try it</a>
<a href="https://github.com/PR0M3TH3AN/Nostr-Governance">Source</a>
</div>
</nav>
<header class="hero" style="padding-top:48px; padding-bottom:8px">
<div class="container narrow" style="padding:0">
<span class="eyebrow">Integration guide</span>
<h1 style="font-size:clamp(32px, 4.5vw, 44px)">Add BitGate to your site</h1>
<p class="lead" style="max-width:560px">
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.
</p>
</div>
</header>
<section class="container narrow">
<ol class="step-list">
<li>
<h3>Build the widget from source</h3>
<p>
Clone the repository and build. The output is a single self-contained
file with no runtime dependencies.
</p>
<pre><code>git clone https://github.com/PR0M3TH3AN/Nostr-Governance
cd Nostr-Governance
npm install
npm run build:widget # → packages/widget/dist/bitgate.js</code></pre>
<div class="callout warn">
<strong>The bundle is deliberately not committed.</strong> 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.
</div>
</li>
<li>
<h3>Include the script and the provider</h3>
<p>
<code>&lt;bitgate-provider&gt;</code> builds the runtime, loads
administrative state from your relays, and shares both with every
element inside it.
</p>
<pre><code>&lt;script type="module" src="/vendor/bitgate/bitgate.js"&gt;&lt;/script&gt;
&lt;bitgate-provider
relays="wss://relay-one.example,wss://relay-two.example"
root="&lt;your-root-pubkey&gt;"
policy="social"&gt;
&lt;!-- everything governed goes in here --&gt;
&lt;/bitgate-provider&gt;</code></pre>
<p style="margin-top:10px">
<code>root</code> is your deployment's administrator pubkey — the key
that publishes the moderator roster. It is configuration, not something
discovered at runtime.
</p>
</li>
<li>
<h3>Wrap what should be governed</h3>
<p>
Elements read their target from attributes, so a server-rendered page
needs no JavaScript at all.
</p>
<pre><code>&lt;bitgate-veil profile="feed" target-user="&lt;author-pubkey&gt;"&gt;
&lt;article&gt;…your post markup…&lt;/article&gt;
&lt;/bitgate-veil&gt;
&lt;bitgate-veil profile="feed" target-event="&lt;event-id&gt;" target-author="&lt;pubkey&gt;"&gt;…&lt;/bitgate-veil&gt;
&lt;bitgate-veil profile="browse" target-address="30078:&lt;pubkey&gt;:sku-001"&gt;…&lt;/bitgate-veil&gt;</code></pre>
<p style="margin-top:10px">
The veil applies the decision: blur on <code>restrict</code>, a
disclosure with a "show anyway" control on <code>hide</code>, a warning
line on <code>warn</code>. Assigning <code>.target</code> in JavaScript
still works and wins over the attribute.
</p>
</li>
<li>
<h3>Connect a signer</h3>
<p>
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.
</p>
<pre><code>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);</code></pre>
<div class="callout">
<strong>Await <code>ready</code>, don't listen for it.</strong> The
<code>bitgate:ready</code> 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.
</div>
</li>
<li>
<h3>Replace the preset</h3>
<p>
Presets exist so the first run works, not because the engine has an
opinion. Every number in them is a starting point.
</p>
<pre><code>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);</code></pre>
</li>
</ol>
</section>
<section class="container narrow">
<h2>Elements</h2>
<p class="section-sub">Embed only what you need; each is independent.</p>
<table class="surface-table">
<thead>
<tr><th>Element</th><th>What it does</th></tr>
</thead>
<tbody>
<tr><td><code>bitgate-provider</code></td><td>Builds the runtime from attributes and shares it</td></tr>
<tr><td><code>bitgate-veil</code></td><td>Applies a decision to whatever it wraps</td></tr>
<tr><td><code>bitgate-report</code></td><td>Report dialog — needs no capability, anyone may report</td></tr>
<tr><td><code>bitgate-status</code></td><td>Effect, reasons, and evidence for one target</td></tr>
<tr><td><code>bitgate-capabilities</code></td><td>What the signed-in account may actually do</td></tr>
<tr><td><code>bitgate-action</code></td><td>A capability-gated button that explains refusals</td></tr>
<tr><td><code>bitgate-admin-panel</code></td><td>Effective state, attribution, and controls</td></tr>
</tbody>
</table>
</section>
<section class="container narrow">
<h2>Policy presets</h2>
<p class="section-sub">Set with the <code>policy</code> attribute.</p>
<table class="surface-table">
<thead><tr><th>Name</th><th>Profiles</th><th>For</th></tr></thead>
<tbody>
<tr><td><code>social</code></td><td>feed, profile, playback</td><td>Timelines and posts. Discovery declines to hard-hide.</td></tr>
<tr><td><code>commerce</code></td><td>browse, detail, checkout, seller-dashboard</td><td>Marketplaces. Checkout decides transactions only.</td></tr>
<tr><td><code>admin-only</code></td><td>default</td><td>Operator deny lists, with no trust-graph signals at all.</td></tr>
</tbody>
</table>
<div class="callout">
<strong>A gate threshold of <code>0</code> means disabled</strong>, not
"fires on everything" — so zeroing a value is a safe way to switch it off.
</div>
</section>
<section class="container narrow">
<h2>Common mistakes</h2>
<div class="grid">
<div class="feature">
<h3>Recomputing policy in your views</h3>
<p>If your renderer compares report counts to thresholds, the point has been lost. Consume the decision.</p>
</div>
<div class="feature">
<h3>Reusing a listing-page verdict at checkout</h3>
<p>Evaluate transaction-critical actions against their own profile at the moment of action. State moves.</p>
</div>
<div class="feature">
<h3>Seeding trust before the viewer</h3>
<p><code>setViewer</code> clears viewer-scoped state by design. Set contacts, blocks, and overrides after it.</p>
</div>
<div class="feature">
<h3>Exposing evidence everywhere</h3>
<p>Public surfaces get counts; appeal surfaces get identities. <code>exposeEvidence</code> is off by default for a reason.</p>
</div>
</div>
<p style="color:var(--muted);font-size:14px">
The full guide, including the adapter contract and every profile option,
lives in
<a href="https://github.com/PR0M3TH3AN/Nostr-Governance/blob/main/docs/integration-guide.md">docs/integration-guide.md</a>.
</p>
</section>
<footer>
<div>BitGate — GPL-3.0-or-later.</div>
<div class="footer-links">
<a href="./index.html">Overview</a>
<a href="./demo.html">Try it</a>
<a href="https://github.com/PR0M3TH3AN/Nostr-Governance">Source</a>
</div>
</footer>
</body>
</html>
+249
View File
@@ -0,0 +1,249 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>BitGate — moderation your users actually control</title>
<meta name="description" content="Decide what to show, what to allow, and what to sell — without writing the policy code yourself. Trust-graph moderation and access control for Nostr apps, in one script tag." />
<link rel="icon" href="./assets/icon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="./assets/site.css" />
</head>
<body>
<nav class="nav">
<a class="brand" href="./index.html">
<img class="mark" src="./assets/mark.svg" alt="" width="30" height="30" />
<span class="wordmark">BitGate</span>
</a>
<div class="links">
<a class="active" href="./index.html">Overview</a>
<a href="./docs.html">Integrate</a>
<a href="./demo.html">Try it</a>
<a href="https://github.com/PR0M3TH3AN/Nostr-Governance">Source</a>
</div>
</nav>
<header class="hero">
<div class="hero-grid">
<div>
<span class="eyebrow">Moderation, without the policy code</span>
<h1>One decision.<br />Every door it <em>guards</em>.</h1>
<p class="lead">
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.
</p>
<div class="cta-row">
<a class="btn primary" href="./demo.html">Try the demo →</a>
<a class="btn secondary" href="./docs.html">Add it to your site</a>
</div>
</div>
<div class="gate-scene" aria-hidden="true">
<div class="item-card">
<div class="item-top">
<span class="item-thumb">
<svg viewBox="0 0 24 24" fill="none"><path d="M3 12.5V5a2 2 0 0 1 2-2h7.5L21 11.5 12.5 20 3 12.5z" stroke="#04120e" stroke-width="1.9" stroke-linejoin="round"/><circle cx="8" cy="8" r="1.6" fill="#04120e"/></svg>
</span>
<div>
<div class="item-label">One listing</div>
<div class="item-name">Refurbished widget</div>
</div>
</div>
<div class="item-evidence">
<b>3</b> scam reports from people the viewer follows
</div>
</div>
<div class="verdict-rail">
<div class="verdict-row" style="--row-order:0">
<span class="verdict-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M3 6h18M3 12h18M3 18h18" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg></span>
<span class="verdict-surface">Marketplace<small>discovery</small></span>
<span class="verdict-stamp hard">hidden</span>
</div>
<div class="verdict-row" style="--row-order:1">
<span class="verdict-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="4" y="4" width="16" height="16" rx="2" stroke="currentColor" stroke-width="1.7"/><path d="M4 9h16" stroke="currentColor" stroke-width="1.7"/></svg></span>
<span class="verdict-surface">Product page<small>direct link</small></span>
<span class="verdict-stamp soft">behind a warning</span>
</div>
<div class="verdict-row" style="--row-order:2">
<span class="verdict-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M6 6h13l-1.5 8H7.5zM7 6L6 3H3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="9" cy="19" r="1.4" fill="currentColor"/><circle cx="17" cy="19" r="1.4" fill="currentColor"/></svg></span>
<span class="verdict-surface">Checkout<small>the moment it matters</small></span>
<span class="verdict-stamp hard">sale blocked</span>
</div>
<div class="verdict-row" style="--row-order:3">
<span class="verdict-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M12 3l7 3v6c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6l7-3z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg></span>
<span class="verdict-surface">Seller's own view<small>with the evidence</small></span>
<span class="verdict-stamp pass">visible</span>
</div>
</div>
<div class="scene-caption">One snapshot. Four surfaces. Four different answers.</div>
</div>
</div>
</header>
<section class="container">
<h2>A decision is not a boolean</h2>
<p class="section-sub">
Most moderation code collapses everything into <code>hidden: true</code>, 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.
</p>
<div class="ladder">
<div class="ladder-row">
<span class="dim">ranking</span>
<span class="steps"><b>normal</b> → downrank</span>
</div>
<div class="ladder-row">
<span class="dim">visibility</span>
<span class="steps"><b>allow</b> → warn → restrict → hide → deny</span>
</div>
<div class="ladder-row">
<span class="dim">interaction</span>
<span class="steps"><b>allow</b> → require-explicit-action → deny</span>
</div>
<div class="ladder-row">
<span class="dim">transaction</span>
<span class="steps"><b>allow</b> → require-review → deny</span>
</div>
</div>
<div class="callout">
<strong>Downranking is a sort input, not a filter.</strong> 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.
</div>
</section>
<section class="container">
<h2>Where the answer comes from</h2>
<p class="section-sub">
Two independent sources, deliberately kept apart. Being allowed to publish
grants no trust; being trusted grants no authority.
</p>
<div class="grid">
<div class="feature">
<h3>People the viewer follows</h3>
<p>
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.
</p>
</div>
<div class="feature">
<h3>Moderators you authorized</h3>
<p>
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.
</p>
</div>
<div class="feature">
<h3>Community curators</h3>
<p>
Subscribe to someone else's deny list without handing them moderator
powers. A curator can contribute names and nothing more.
</p>
</div>
<div class="feature">
<h3>The viewer's own choices</h3>
<p>
Personal blocks always win, and "show it anyway" is available wherever
your policy permits it. Hidden content says why, instead of silently
vanishing.
</p>
</div>
</div>
</section>
<section class="container">
<h2>Pairs with BitLogin</h2>
<p class="section-sub">
Two halves of the same question. Drop both in and a signed-in visitor gets
moderation shaped by their own follow graph.
</p>
<div class="pair">
<div class="pair-card">
<span class="pair-kicker">BitLogin</span>
<h3>Who are you?</h3>
<p>A login name and password that unlock a portable Nostr identity, with no account server.</p>
</div>
<div class="pair-join">+</div>
<div class="pair-card is-gate">
<span class="pair-kicker">BitGate</span>
<h3>What may you see, and do?</h3>
<p>A decision per item, per surface, built from that identity's trust graph and your roster.</p>
</div>
</div>
<pre><code>const provider = document.querySelector("bitgate-provider");
await provider.ready;
await provider.useSigner(window.nostr); // BitLogin's NIP-07 provider</code></pre>
</section>
<section class="container">
<h2>Drop it in</h2>
<p class="section-sub">
One script tag and one element. The provider builds the runtime, loads your
administrative state, and every element inside finds it — no wiring code.
</p>
<pre><code>&lt;script type="module" src="/vendor/bitgate/bitgate.js"&gt;&lt;/script&gt;
&lt;bitgate-provider relays="wss://relay.example" root="&lt;root-pubkey&gt;" policy="social"&gt;
&lt;bitgate-veil profile="feed" target-user="&lt;author-pubkey&gt;"&gt;
&lt;img src="thumbnail.jpg" alt="" /&gt;
&lt;/bitgate-veil&gt;
&lt;/bitgate-provider&gt;</code></pre>
<div class="cta-row" style="margin-top:22px">
<a class="btn primary" href="./docs.html">Integration guide →</a>
<a class="btn secondary" href="./demo.html">See it running</a>
</div>
</section>
<section class="container">
<h2>Built to be borrowed</h2>
<p class="section-sub">
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.
</p>
<div class="grid">
<div class="feature">
<h3>Headless core</h3>
<p>Pure evaluation. No I/O, no browser globals, no dependencies. Runs the same in Node and a browser.</p>
</div>
<div class="feature">
<h3>Bring your own relays</h3>
<p>A WebSocket transport ships for convenience. Already have a relay pool? Pass an adapter instead.</p>
</div>
<div class="feature">
<h3>Your objects, your targets</h3>
<p>An adapter says what a product or post is in governance terms. Deny a seller and their whole catalogue goes with them.</p>
</div>
<div class="feature">
<h3>Explains itself</h3>
<p>Every decision carries stable reason identifiers and the evidence behind it — so appeals are possible.</p>
</div>
</div>
</section>
<footer>
<div>BitGate — GPL-3.0-or-later. Moderation infrastructure for the open web.</div>
<div class="footer-links">
<a href="./docs.html">Integrate</a>
<a href="./demo.html">Try it</a>
<a href="https://github.com/PR0M3TH3AN/Nostr-Governance">Source</a>
<a href="https://github.com/PR0M3TH3AN/bitlogin">BitLogin</a>
</div>
</footer>
</body>
</html>
+32
View File
@@ -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}`));
+24
View File
@@ -8,6 +8,30 @@
// Elements receive a runtime rather than creating one, so a host application // Elements receive a runtime rather than creating one, so a host application
// can share a single governance runtime across an entire page. // 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 { export {
BASE_STYLES, BASE_STYLES,
GovernanceElement, GovernanceElement,
+25
View File
@@ -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" }
]
}
]
}