mirror of
https://github.com/PR0M3TH3AN/bitgate.git
synced 2026-10-27 08:31:00 +00:00
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:
1 parent
729c0acc43
commit
b24bf3955d
14 files changed
+1423
-1
No files matched your search
@@ -9,3 +9,6 @@ coverage/
|
|||||||
.turbo/
|
.turbo/
|
||||||
.cache/
|
.cache/
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Built site output (Vercel builds it)
|
||||||
|
packages/site/dist/
|
||||||
Generated
+8
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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}`);
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 |
@@ -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 |
@@ -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; }
|
||||||
@@ -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><bitgate-veil></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><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></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>
|
||||||
@@ -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><bitgate-provider></code> builds the runtime, loads
|
||||||
|
administrative state from your relays, and shares both with every
|
||||||
|
element inside it.
|
||||||
|
</p>
|
||||||
|
<pre><code><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></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><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></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>
|
||||||
@@ -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><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></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>
|
||||||
@@ -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}`));
|
||||||
@@ -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
@@ -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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in new issue
Block a user