Files
bitgate/examples/commerce/test/proof-of-fit.test.js
T
thePR0M3TH3AN adeba151b8 feat: rename to BitGate and close the drop-in gap
Renames the project and makes it as easy to adopt as BitLogin, which was the
stated bar and which it did not previously meet.

Rename (ADR 0005): @nostr-governance/* -> @bitgate/*, governance-* elements ->
bitgate-*, createGovernanceRuntime -> createBitGate. "Governance" reads as DAO
voting to this audience and named a category rather than a product; BitGate
names the benefit and joins the bit- family that BitVid and BitRoad compose
with. Domain types keep their names — the product is BitGate, the subject
matter is still governance. The migration plan is left as written; this ADR
records the change rather than rewriting history.

Drop-in distribution (ADR 0006): adopting BitGate meant supplying a transport,
authoring a policy, and wiring every element in JavaScript. Now:

- createRelayTransport(urls) — WebSocket transport with reconnection, backoff,
  subscription replay, and cross-relay dedupe
- policy presets (social, commerce, admin-only) addressable by name; the engine
  still carries no thresholds of its own
- <bitgate-provider> — configuration in markup, with descendants finding the
  runtime through a bubbling context event rather than DOM walking, which
  breaks across shadow boundaries
- npm run build:widget — a self-contained dist/bitgate.js

That last one came from running it in a browser rather than from a test. Bare
specifiers resolve in Node and vitest but not in a browser, so the widget
source worked everywhere except a plain static page — the one place it exists
to serve. The bundle is not committed: consumers build from reviewed source.

Also found by running it: the provider's ready event fires during element
upgrade, before a page that defines elements after parsing can attach a
listener, so it now exposes `ready` as a promise too.

Four bugs fixed along the way, three found by new tests and one by the browser:
admin state stayed silent when a roster change altered capabilities but no
denials; the relay transport sent every subscription twice on connect; the
commerce preset computed a meaningless visibility verdict at checkout and could
hide a listing from its own seller; <bitgate-status> threw on an unusable
decision.

759 tests; lint, typecheck, and both builds clean.
2026-08-09 19:58:45 -04:00

509 lines
16 KiB
JavaScript

// Commerce proof-of-fit.
//
// The plan requires a commerce consumer to demonstrate ten behaviors before the
// package reaches 1.0.0, the tenth being that none of it needs a change to the
// generic core. Each numbered case below is one of those requirements.
import { describe, expect, it } from "vitest";
import { createCommands, createBitGate, createMemoryTransport } from "@bitgate/runtime";
import { COMMERCE_POLICY, COMMERCE_ROLE_CAPABILITIES } from "../src/policy.js";
import { productAdapter, reviewAdapter, sellerAdapter } from "../src/adapters.js";
import {
buildMarketplaceGrid,
checkoutVerdict,
explainToSeller,
governProductFeed,
} from "../src/marketplace.js";
const ROOT = "a1".repeat(32);
const LISTING_MOD = "b2".repeat(32);
const SELLER_MOD = "c3".repeat(32);
const CURATOR = "e5".repeat(32);
const SELLER = "d4".repeat(32);
const OTHER_SELLER = "f6".repeat(32);
const BUYER = "ab".repeat(32);
const NOW = 1_750_000_000;
const trusted = (index) => index.toString(16).padStart(2, "0").repeat(32);
const TRUSTED = Array.from({ length: 8 }, (_, index) => trusted(index + 1));
/** @type {import('../src/adapters.js').Product} */
const PRODUCT = {
id: "1b".repeat(32),
sellerPubkey: SELLER,
identifier: "sku-001",
title: "Widget",
priceSats: 1000,
};
/** @type {import('../src/adapters.js').Product} */
const OTHER_PRODUCT = {
id: "2c".repeat(32),
sellerPubkey: SELLER,
identifier: "sku-002",
title: "Gadget",
priceSats: 2000,
};
/** @type {import('../src/adapters.js').Product} */
const RIVAL_PRODUCT = {
id: "3d".repeat(32),
sellerPubkey: OTHER_SELLER,
identifier: "sku-100",
title: "Rival widget",
priceSats: 900,
};
function setup({ allowlisted = [SELLER, OTHER_SELLER] } = {}) {
const runtime = createBitGate({
applicationId: "commerce-example",
namespace: "commerce",
transport: createMemoryTransport(),
policy: COMMERCE_POLICY,
now: () => NOW,
});
runtime.admin.setRoles({
root: ROOT,
roles: COMMERCE_ROLE_CAPABILITIES,
actors: {
[ROOT]: ["super_admin"],
[LISTING_MOD]: ["listing_moderator"],
[SELLER_MOD]: ["seller_moderator"],
[CURATOR]: ["community_curator"],
},
});
// Roles here are application-defined, so super_admin needs its default bundle
// restored alongside them.
runtime.admin.setRoles({
root: ROOT,
roles: {
...COMMERCE_ROLE_CAPABILITIES,
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",
],
},
actors: {
[ROOT]: ["super_admin"],
[LISTING_MOD]: ["listing_moderator"],
[SELLER_MOD]: ["seller_moderator"],
[CURATOR]: ["community_curator"],
},
});
if (allowlisted.length) {
runtime.admin.upsertContribution({
actor: ROOT,
kind: "user-allow",
targets: allowlisted.map((pubkey) => ({ type: "user", pubkey })),
createdAt: NOW,
});
}
// Set the viewer first: switching viewers clears viewer-scoped trust state,
// so seeding contacts beforehand would be wiped.
runtime.setViewer(BUYER);
runtime.trust.setContacts(TRUSTED);
return { runtime, commands: createCommands(runtime) };
}
/** Report a target from `count` distinct trusted accounts. */
function reportFrom(runtime, targetKey, category, count) {
for (const reporter of TRUSTED.slice(0, count)) {
runtime.reports.ingest({ reporter, category, createdAt: NOW - 100 }, targetKey);
}
}
const productAddressKey = (product) =>
`address:30078:${product.sellerPubkey}:${product.identifier}`;
describe("1. seller allowlist mode", () => {
it("hides a seller who is not allowlisted", () => {
const { runtime } = setup({ allowlisted: [] });
const [governed] = governProductFeed([PRODUCT], runtime, "public-marketplace");
expect(governed.moderation.visible).toBe(false);
expect(governed.moderation.reasons).toContain("allowlist-miss");
});
it("shows an allowlisted seller", () => {
const { runtime } = setup();
const [governed] = governProductFeed([PRODUCT], runtime, "public-marketplace");
expect(governed.moderation.visible).toBe(true);
});
it("does not enforce the allowlist on checkout", () => {
const { runtime } = setup({ allowlisted: [] });
expect(checkoutVerdict(PRODUCT, runtime).allowed).toBe(true);
});
});
describe("2. administrative seller denial", () => {
it("removes every product from a denied seller", () => {
const { runtime } = setup();
runtime.admin.upsertContribution({
actor: SELLER_MOD,
kind: "user-deny",
targets: [{ type: "user", pubkey: SELLER }],
createdAt: NOW,
});
const grid = buildMarketplaceGrid([PRODUCT, OTHER_PRODUCT, RIVAL_PRODUCT], runtime);
expect(grid.map((product) => product.identifier)).toEqual(["sku-100"]);
});
it("blocks checkout for the denied seller", () => {
const { runtime } = setup();
runtime.admin.upsertContribution({
actor: SELLER_MOD,
kind: "user-deny",
targets: [{ type: "user", pubkey: SELLER }],
createdAt: NOW,
});
expect(checkoutVerdict(PRODUCT, runtime).allowed).toBe(false);
});
it("leaves the seller their own dashboard view", () => {
const { runtime } = setup();
runtime.admin.upsertContribution({
actor: SELLER_MOD,
kind: "user-deny",
targets: [{ type: "user", pubkey: SELLER }],
createdAt: NOW,
});
const explanation = explainToSeller(PRODUCT, runtime);
expect(explanation.visible).toBe(true);
expect(explanation.sellable).toBe(false);
expect(explanation.reasons).toContain("admin-user-deny");
});
});
describe("3. product-address denial", () => {
it("denies one listing without touching the seller's others", () => {
const { runtime } = setup();
runtime.admin.upsertContribution({
actor: LISTING_MOD,
kind: "address-deny",
targets: [
{ type: "address", kind: "30078", pubkey: SELLER, identifier: PRODUCT.identifier },
],
createdAt: NOW,
});
const grid = buildMarketplaceGrid([PRODUCT, OTHER_PRODUCT], runtime);
expect(grid.map((product) => product.identifier)).toEqual(["sku-002"]);
});
it("persists across a revision that changes the event id", () => {
const { runtime } = setup();
runtime.admin.upsertContribution({
actor: LISTING_MOD,
kind: "address-deny",
targets: [
{ type: "address", kind: "30078", pubkey: SELLER, identifier: PRODUCT.identifier },
],
createdAt: NOW,
});
const republished = { ...PRODUCT, id: "99".repeat(32), title: "Widget v2" };
const [governed] = governProductFeed([republished], runtime, "public-marketplace");
expect(governed.moderation.visible).toBe(false);
});
});
describe("4. exact-event denial", () => {
it("affects only the named revision", () => {
const { runtime } = setup();
runtime.admin.upsertContribution({
actor: LISTING_MOD,
kind: "event-deny",
targets: [{ type: "event", id: PRODUCT.id }],
createdAt: NOW,
});
const [denied] = governProductFeed([PRODUCT], runtime, "public-marketplace");
expect(denied.moderation.visible).toBe(false);
const republished = { ...PRODUCT, id: "99".repeat(32) };
const [fresh] = governProductFeed([republished], runtime, "public-marketplace");
expect(fresh.moderation.visible).toBe(true);
});
});
describe("5. trusted scam-report downranking", () => {
it("downranks on a single trusted scam report without hiding", () => {
const { runtime } = setup();
reportFrom(runtime, productAddressKey(PRODUCT), "scam", 1);
const [governed] = governProductFeed([PRODUCT], runtime, "public-marketplace");
expect(governed.moderation.downranked).toBe(true);
expect(governed.moderation.visible).toBe(true);
});
it("orders downranked listings after clean ones", () => {
const { runtime } = setup();
reportFrom(runtime, productAddressKey(PRODUCT), "scam", 1);
const grid = buildMarketplaceGrid([PRODUCT, RIVAL_PRODUCT], runtime);
expect(grid.map((product) => product.identifier)).toEqual(["sku-100", "sku-001"]);
});
it("escalates to review then denial as reports accumulate", () => {
const { runtime } = setup();
reportFrom(runtime, productAddressKey(PRODUCT), "scam", 2);
expect(checkoutVerdict(PRODUCT, runtime).needsReview).toBe(true);
reportFrom(runtime, productAddressKey(PRODUCT), "scam", 3);
expect(checkoutVerdict(PRODUCT, runtime).allowed).toBe(false);
});
});
describe("6. malware-based checkout denial", () => {
it("blocks checkout on a single trusted malware report", () => {
const { runtime } = setup();
reportFrom(runtime, productAddressKey(PRODUCT), "malware", 1);
expect(checkoutVerdict(PRODUCT, runtime).allowed).toBe(false);
});
it("treats malware far more harshly than misleading copy", () => {
const { runtime } = setup();
reportFrom(runtime, productAddressKey(PRODUCT), "misleading", 3);
expect(checkoutVerdict(PRODUCT, runtime).allowed).toBe(true);
reportFrom(runtime, productAddressKey(RIVAL_PRODUCT), "malware", 1);
expect(checkoutVerdict(RIVAL_PRODUCT, runtime).allowed).toBe(false);
});
});
describe("7. moderator capability checks", () => {
const signerFor = (pubkey) => ({
async getPublicKey() {
return pubkey;
},
async signEvent(template) {
return { ...template, id: "ff".repeat(32), pubkey, sig: "00".repeat(64) };
},
});
function commandsAs(pubkey) {
const runtime = createBitGate({
applicationId: "commerce-example",
namespace: "commerce",
transport: createMemoryTransport(),
signer: signerFor(pubkey),
policy: COMMERCE_POLICY,
now: () => NOW,
});
runtime.admin.setRoles({
root: ROOT,
roles: COMMERCE_ROLE_CAPABILITIES,
actors: {
[LISTING_MOD]: ["listing_moderator"],
[SELLER_MOD]: ["seller_moderator"],
[CURATOR]: ["community_curator"],
},
});
return createCommands(runtime);
}
it("lets a listing moderator deny an address", async () => {
expect((await commandsAs(LISTING_MOD).denyAddress("30078", SELLER, "sku-001")).ok).toBe(true);
});
it("stops a listing moderator denying a seller", async () => {
expect((await commandsAs(LISTING_MOD).denyUser(SELLER)).code).toBe("not-authorized");
});
it("lets a seller moderator deny a seller", async () => {
expect((await commandsAs(SELLER_MOD).denyUser(SELLER)).ok).toBe(true);
});
it("stops a seller moderator denying a listing address", async () => {
expect((await commandsAs(SELLER_MOD).denyAddress("30078", SELLER, "sku-001")).code).toBe(
"not-authorized",
);
});
it("stops a curator denying an exact event", async () => {
expect((await commandsAs(CURATOR).denyEvent("1b".repeat(32))).code).toBe("not-authorized");
});
it("stops any moderator changing roles", async () => {
expect((await commandsAs(SELLER_MOD).setRoles({ actors: {} })).code).toBe("not-authorized");
});
});
describe("8. community curator ingestion", () => {
it("honors a curator's seller denial", () => {
const { runtime } = setup();
runtime.admin.upsertContribution({
actor: CURATOR,
kind: "user-deny",
source: "curated-scam-list",
targets: [{ type: "user", pubkey: SELLER }],
createdAt: NOW,
});
const [governed] = governProductFeed([PRODUCT], runtime, "public-marketplace");
expect(governed.moderation.visible).toBe(false);
expect(governed.moderation.reasons).toContain("community-user-deny");
});
it("ignores a curator contribution beyond their capability", () => {
const { runtime } = setup();
runtime.admin.upsertContribution({
actor: CURATOR,
kind: "address-deny",
source: "curated-scam-list",
targets: [
{ type: "address", kind: "30078", pubkey: SELLER, identifier: PRODUCT.identifier },
],
createdAt: NOW,
});
const [governed] = governProductFeed([PRODUCT], runtime, "public-marketplace");
expect(governed.moderation.visible).toBe(true);
});
it("cannot deny the root through a curated list", () => {
const { runtime } = setup();
runtime.admin.upsertContribution({
actor: CURATOR,
kind: "user-deny",
source: "hostile-list",
targets: [{ type: "user", pubkey: ROOT }],
createdAt: NOW,
});
expect(runtime.admin.state.userDeny.has(`user:${ROOT}`)).toBe(false);
});
});
describe("9. seller-dashboard explanation", () => {
it("exposes evidence counts to the seller", () => {
const { runtime } = setup();
reportFrom(runtime, productAddressKey(PRODUCT), "scam", 3);
const explanation = explainToSeller(PRODUCT, runtime);
expect(explanation.evidence.trustedReportTotal).toBe(3);
expect(explanation.evidence.trustedReportsByCategory.scam).toBe(3);
expect(explanation.evidence.trustedReporterPubkeys).toHaveLength(3);
});
it("withholds reporter identities from public surfaces", () => {
const { runtime } = setup();
reportFrom(runtime, productAddressKey(PRODUCT), "scam", 3);
const decision = runtime.evaluate(
{ type: "address", kind: "30078", pubkey: SELLER, identifier: PRODUCT.identifier },
{ profile: "public-marketplace" },
);
expect(decision.evidence?.trustedReportTotal).toBe(3);
expect(decision.evidence?.trustedReporterPubkeys).toEqual([]);
});
});
describe("10. no core changes required", () => {
it("expresses four different verdicts for one product from one snapshot", () => {
const { runtime } = setup();
reportFrom(runtime, productAddressKey(PRODUCT), "scam", 3);
const target = {
type: "address",
kind: "30078",
pubkey: SELLER,
identifier: PRODUCT.identifier,
};
expect(runtime.evaluate(target, { profile: "public-marketplace" }).visibility.effect).toBe(
"restrict",
);
expect(runtime.evaluate(target, { profile: "seller-dashboard" }).visibility.effect).toBe(
"restrict",
);
expect(runtime.evaluate(target, { profile: "checkout" }).transaction?.effect).toBe("deny");
expect(runtime.evaluate(target, { profile: "product-detail" }).visibility.overridable).toBe(true);
});
it("governs reviews without letting a bad product silence them", () => {
const { runtime } = setup();
runtime.admin.upsertContribution({
actor: SELLER_MOD,
kind: "user-deny",
targets: [{ type: "user", pubkey: SELLER }],
createdAt: NOW,
});
const review = {
id: "7f".repeat(32),
reviewerPubkey: BUYER,
productIdentifier: PRODUCT.identifier,
rating: 1,
};
const decision = runtime.evaluate(reviewAdapter.toTargets(review)[0], {
profile: "product-detail",
});
expect(decision.visibility.effect).not.toBe("hide");
});
it("uses only the published core API", async () => {
// Every import in this example resolves through a package entry point; if
// one reached into a package's internals, the reference guard and this
// assertion are what would catch it.
const core = await import("@bitgate/core");
for (const name of [
"createApplicationAdapter",
"evaluateObject",
"collectTargets",
"createPolicyDefinition",
"createRoleDefinition",
]) {
expect(typeof core[name]).toBe("function");
}
});
it("governs a seller through the same engine as a product", () => {
const { runtime } = setup();
runtime.admin.upsertContribution({
actor: SELLER_MOD,
kind: "user-deny",
targets: [{ type: "user", pubkey: SELLER }],
createdAt: NOW,
});
const decision = runtime.evaluate(sellerAdapter.toTargets({ pubkey: SELLER })[0], {
profile: "public-marketplace",
});
expect(decision.visibility.effect).toBe("hide");
});
});
describe("adapter composition", () => {
it("reports decisions against the product's own identity", () => {
const { runtime } = setup();
const decision = runtime.evaluate(productAdapter.toTargets(PRODUCT)[2], {
profile: "public-marketplace",
});
expect(decision.key).toBe(productAddressKey(PRODUCT));
});
it("collects deduplicated targets across a feed", () => {
const { runtime } = setup();
governProductFeed([PRODUCT, OTHER_PRODUCT], runtime, "public-marketplace");
// Two products from one seller: 2 events + 2 addresses + 1 shared seller.
expect(runtime.activeTargets.size).toBe(5);
});
});