fix(site): move the demo module out of the page to satisfy CSP

script-src 'self' with no 'unsafe-inline' blocks inline module scripts, so the
demo initialized nothing on the deployed site while working perfectly locally —
the dev server sends no CSP, so only the real deployment showed it.

Extracted to assets/demo.js rather than relaxing the policy. Loosening CSP for
a page that renders moderation decisions would be the wrong trade.
This commit is contained in:
thePR0M3TH3AN committed 2026-08-09 20:32:16 -04:00
1 parent b24bf3955d
commit 015f9c3ad4
3 files changed
+155 -154

No files matched your search

+2
View File
@@ -12,3 +12,5 @@ coverage/
# Built site output (Vercel builds it)
packages/site/dist/
.vercel
.env*
+152
View File
@@ -0,0 +1,152 @@
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();
+1 -154
View File
@@ -125,160 +125,7 @@
</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>
<script type="module" src="./assets/demo.js"></script>
</body>
</html>