mirror of
https://github.com/PR0M3TH3AN/bitgate.git
synced 2026-10-27 08:31:00 +00:00
The engine already honored requireAllowlist + allowlistMiss, and the commerce example configured a seller allowlist by hand -- but there was no reusable, named "only show allowlisted publishers" preset, the symmetric twin of the denylist. Integrators (Tessera) punted on allowlist mode for lack of one. Add ALLOWLIST_POLICY (registry name "allowlist"): a publisher is shown only when an authorized actor has contributed a user-allow for them, and every unlisted publisher misses and is hidden across all four dimensions. Administrative denials still apply on top, and protected actors (root, moderators) are never gated -- the people who curate the list can always see. Populate userAllow exactly as the denylist: signed contributions resolved from relays (e.g. NIP-32 allow labels). Exported from @bitgate/core, registered in POLICY_PRESETS, documented in the README, and covered by presets.test.js (unlisted hidden, allowlisted shown, protected bypasses). Addresses the "allowlist / whitelist mode" item of the Tessera integration feedback (docs/TODO.md). Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01BXQCvJPA6sVSQ5j8uBd9bB
296 lines
11 KiB
JavaScript
296 lines
11 KiB
JavaScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { createAuthorityState } from "../src/authority.js";
|
|
import { reduceAdminState } from "../src/adminState.js";
|
|
import { createSnapshot, evaluateTarget } from "../src/evaluator.js";
|
|
import {
|
|
ADMIN_ONLY_POLICY,
|
|
ALLOWLIST_POLICY,
|
|
COMMERCE_POLICY,
|
|
POLICY_PRESETS,
|
|
SOCIAL_POLICY,
|
|
getPolicyPreset,
|
|
} from "../src/presets.js";
|
|
|
|
const ROOT = "a1".repeat(32);
|
|
const MODERATOR = "b2".repeat(32);
|
|
const CREATOR = "d4".repeat(32);
|
|
const NOW = 1_750_000_000;
|
|
|
|
const trusted = (index) => index.toString(16).padStart(2, "0").repeat(32);
|
|
const TRUSTED = Array.from({ length: 16 }, (_, index) => trusted(index + 1));
|
|
|
|
const authority = () =>
|
|
createAuthorityState({
|
|
root: ROOT,
|
|
actors: { [ROOT]: ["super_admin"], [MODERATOR]: ["moderator"] },
|
|
});
|
|
|
|
/** Evaluate a user target with `count` trusted reports in `category`. */
|
|
function evaluateWithReports(policy, profile, category, count, contributions = []) {
|
|
const auth = authority();
|
|
const snapshot = createSnapshot({
|
|
authority: auth,
|
|
admin: reduceAdminState(contributions, auth),
|
|
trust: { contacts: new Set(TRUSTED) },
|
|
reports: new Map([
|
|
[
|
|
`user:${CREATOR}`,
|
|
TRUSTED.slice(0, count).map((reporter) => ({ reporter, category, createdAt: NOW - 100 })),
|
|
],
|
|
]),
|
|
});
|
|
|
|
return evaluateTarget({ type: "user", pubkey: CREATOR }, snapshot, {
|
|
surface: profile,
|
|
policyProfile: profile,
|
|
policy,
|
|
now: NOW,
|
|
});
|
|
}
|
|
|
|
describe("preset registry", () => {
|
|
it("resolves each preset by name", () => {
|
|
expect(getPolicyPreset("social")).toBe(SOCIAL_POLICY);
|
|
expect(getPolicyPreset("commerce")).toBe(COMMERCE_POLICY);
|
|
expect(getPolicyPreset("admin-only")).toBe(ADMIN_ONLY_POLICY);
|
|
});
|
|
|
|
it("is case- and whitespace-insensitive", () => {
|
|
expect(getPolicyPreset(" SOCIAL ")).toBe(SOCIAL_POLICY);
|
|
});
|
|
|
|
it("returns null for anything else", () => {
|
|
expect(getPolicyPreset("nonsense")).toBeNull();
|
|
expect(getPolicyPreset(/** @type {any} */ (null))).toBeNull();
|
|
});
|
|
|
|
it("exposes every preset in the registry", () => {
|
|
expect(Object.keys(POLICY_PRESETS).sort()).toEqual([
|
|
"admin-only",
|
|
"allowlist",
|
|
"commerce",
|
|
"social",
|
|
]);
|
|
});
|
|
|
|
it("resolves the allowlist preset by name", () => {
|
|
expect(getPolicyPreset("allowlist")).toBe(ALLOWLIST_POLICY);
|
|
});
|
|
});
|
|
|
|
describe("social preset", () => {
|
|
it("escalates malware fastest", () => {
|
|
const decision = evaluateWithReports(SOCIAL_POLICY, "playback", "malware", 1);
|
|
expect(decision.visibility.effect).toBe("restrict");
|
|
expect(decision.interaction.effect).toBe("deny");
|
|
});
|
|
|
|
it("requires more agreement before acting on spam", () => {
|
|
// One report of spam must not restrict anything: it is the category most
|
|
// easily weaponized against an unpopular account.
|
|
expect(evaluateWithReports(SOCIAL_POLICY, "playback", "spam", 1).visibility.effect).toBe("allow");
|
|
expect(evaluateWithReports(SOCIAL_POLICY, "playback", "spam", 4).visibility.effect).toBe("restrict");
|
|
});
|
|
|
|
it("declines to hard-hide on the feed", () => {
|
|
const decision = evaluateWithReports(SOCIAL_POLICY, "feed", "spam", 8);
|
|
expect(decision.visibility.effect).toBe("restrict");
|
|
expect(decision.reasons.map((reason) => reason.id)).toContain("surface-policy-bypass");
|
|
});
|
|
|
|
it("does hide on playback, where the viewer chose the item", () => {
|
|
expect(evaluateWithReports(SOCIAL_POLICY, "playback", "spam", 8).visibility.effect).toBe("hide");
|
|
});
|
|
|
|
it("exposes evidence only on playback", () => {
|
|
const playback = evaluateWithReports(SOCIAL_POLICY, "playback", "spam", 4);
|
|
const feed = evaluateWithReports(SOCIAL_POLICY, "feed", "spam", 4);
|
|
expect(playback.evidence?.trustedReporterPubkeys.length).toBe(4);
|
|
expect(feed.evidence?.trustedReporterPubkeys).toEqual([]);
|
|
});
|
|
|
|
it("leaves a clean target entirely alone", () => {
|
|
const decision = evaluateWithReports(SOCIAL_POLICY, "feed", "spam", 0);
|
|
expect(decision.visibility.effect).toBe("allow");
|
|
expect(decision.ranking.effect).toBe("normal");
|
|
});
|
|
});
|
|
|
|
describe("commerce preset", () => {
|
|
it("blocks checkout on a single malware report", () => {
|
|
expect(evaluateWithReports(COMMERCE_POLICY, "checkout", "malware", 1).transaction?.effect).toBe(
|
|
"deny",
|
|
);
|
|
});
|
|
|
|
it("escalates scam through review before denial", () => {
|
|
expect(evaluateWithReports(COMMERCE_POLICY, "checkout", "scam", 2).transaction?.effect).toBe(
|
|
"require-review",
|
|
);
|
|
expect(evaluateWithReports(COMMERCE_POLICY, "checkout", "scam", 3).transaction?.effect).toBe(
|
|
"deny",
|
|
);
|
|
});
|
|
|
|
it("keeps a denied listing visible to its own seller", () => {
|
|
const denial = [
|
|
{ actor: MODERATOR, kind: "user-deny", targets: [{ type: "user", pubkey: CREATOR }] },
|
|
];
|
|
const dashboard = evaluateWithReports(
|
|
COMMERCE_POLICY,
|
|
"seller-dashboard",
|
|
"scam",
|
|
0,
|
|
/** @type {any} */ (denial),
|
|
);
|
|
const browse = evaluateWithReports(
|
|
COMMERCE_POLICY,
|
|
"browse",
|
|
"scam",
|
|
0,
|
|
/** @type {any} */ (denial),
|
|
);
|
|
|
|
expect(dashboard.visibility.effect).toBe("warn");
|
|
expect(browse.visibility.effect).toBe("hide");
|
|
expect(dashboard.transaction?.effect).toBe("deny");
|
|
});
|
|
|
|
it("keeps visibility more generous than transaction", () => {
|
|
const decision = evaluateWithReports(COMMERCE_POLICY, "checkout", "scam", 3);
|
|
expect(decision.visibility.effect).toBe("allow");
|
|
expect(decision.transaction?.effect).toBe("deny");
|
|
});
|
|
|
|
it("never hides a listing from its own seller", () => {
|
|
// Strong evidence hides it everywhere else, but the seller still needs to
|
|
// see it to appeal.
|
|
expect(evaluateWithReports(COMMERCE_POLICY, "browse", "scam", 5).visibility.effect).toBe("hide");
|
|
expect(
|
|
evaluateWithReports(COMMERCE_POLICY, "seller-dashboard", "scam", 5).visibility.effect,
|
|
).toBe("warn");
|
|
});
|
|
|
|
it("gives one product different answers per surface from one snapshot", () => {
|
|
const byProfile = Object.fromEntries(
|
|
["browse", "detail", "checkout", "seller-dashboard"].map((profile) => [
|
|
profile,
|
|
evaluateWithReports(COMMERCE_POLICY, profile, "scam", 5),
|
|
]),
|
|
);
|
|
|
|
expect(byProfile.browse.visibility.effect).toBe("hide");
|
|
expect(byProfile["seller-dashboard"].visibility.effect).toBe("warn");
|
|
expect(byProfile.checkout.visibility.effect).toBe("allow");
|
|
expect(byProfile.checkout.transaction?.effect).toBe("deny");
|
|
});
|
|
});
|
|
|
|
describe("admin-only preset", () => {
|
|
it("enforces administrative denial", () => {
|
|
const decision = evaluateWithReports(ADMIN_ONLY_POLICY, "default", "spam", 0, [
|
|
/** @type {any} */ ({
|
|
actor: MODERATOR,
|
|
kind: "user-deny",
|
|
targets: [{ type: "user", pubkey: CREATOR }],
|
|
}),
|
|
]);
|
|
expect(decision.visibility.effect).toBe("hide");
|
|
});
|
|
|
|
it("ignores trust signals entirely", () => {
|
|
// Even sixteen trusted reports do nothing: this preset is for operators who
|
|
// do not want crowd signals affecting what anyone sees.
|
|
const decision = evaluateWithReports(ADMIN_ONLY_POLICY, "default", "malware", 16);
|
|
expect(decision.visibility.effect).toBe("allow");
|
|
expect(decision.ranking.effect).toBe("normal");
|
|
});
|
|
});
|
|
|
|
describe("allowlist preset", () => {
|
|
const evaluateUser = (pubkey, contributions = []) => {
|
|
const auth = authority();
|
|
const snapshot = createSnapshot({ authority: auth, admin: reduceAdminState(contributions, auth) });
|
|
return evaluateTarget({ type: "user", pubkey }, snapshot, {
|
|
surface: "default",
|
|
policyProfile: "default",
|
|
policy: ALLOWLIST_POLICY,
|
|
now: NOW,
|
|
});
|
|
};
|
|
|
|
it("hides an unlisted publisher across every dimension", () => {
|
|
const decision = evaluateUser(CREATOR);
|
|
expect(decision.visibility.effect).toBe("hide");
|
|
expect(decision.interaction.effect).toBe("deny");
|
|
expect(decision.transaction?.effect).toBe("deny");
|
|
expect(decision.reasons.map((reason) => reason.id)).toContain("allowlist-miss");
|
|
});
|
|
|
|
it("shows a publisher once an authorized actor allowlists them", () => {
|
|
const allow = [
|
|
{ actor: ROOT, kind: "user-allow", targets: [{ type: "user", pubkey: CREATOR }] },
|
|
];
|
|
const decision = evaluateUser(CREATOR, /** @type {any} */ (allow));
|
|
expect(decision.visibility.effect).toBe("allow");
|
|
expect(decision.transaction?.effect ?? "allow").toBe("allow");
|
|
expect(decision.evidence?.userAllowed).toBe(true);
|
|
});
|
|
|
|
it("never gates a protected actor by the allowlist", () => {
|
|
// ROOT is protected, so it shows even though nobody allowlisted it — the
|
|
// people who curate the list can always see.
|
|
expect(evaluateUser(ROOT).visibility.effect).toBe("allow");
|
|
});
|
|
});
|
|
|
|
describe("category vocabulary", () => {
|
|
// NIP-56 defines exactly these. A threshold for anything else can only fire
|
|
// if a bespoke client emits it.
|
|
const NIP56 = ["nudity", "malware", "profanity", "illegal", "spam", "impersonation", "other"];
|
|
|
|
it("uses only NIP-56 categories in the social preset", () => {
|
|
for (const category of Object.keys(SOCIAL_POLICY.profiles.feed.reports ?? {})) {
|
|
if (category === "default") continue;
|
|
expect(NIP56, `social preset declares "${category}"`).toContain(category);
|
|
}
|
|
});
|
|
|
|
it("keeps commerce extensions alongside the standard ones", () => {
|
|
const categories = Object.keys(COMMERCE_POLICY.profiles.browse.reports ?? {});
|
|
expect(categories).toContain("malware");
|
|
expect(categories).toContain("illegal");
|
|
// Marketplace-specific vocabulary is deliberate; it is documented as such.
|
|
expect(categories).toContain("scam");
|
|
});
|
|
|
|
it("gives every preset a default so unknown categories still land somewhere", () => {
|
|
for (const [name, policy] of Object.entries(POLICY_PRESETS)) {
|
|
for (const [profileName, profile] of Object.entries(policy.profiles)) {
|
|
const reports = profile.reports ?? {};
|
|
if (Object.keys(reports).length === 0) continue;
|
|
expect(reports.default, `${name}/${profileName}`).toBeDefined();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("preset hygiene", () => {
|
|
it("gives every preset a version and a named default profile", () => {
|
|
for (const [name, policy] of Object.entries(POLICY_PRESETS)) {
|
|
expect(policy.version, name).toMatch(/^\d+\.\d+\.\d+$/);
|
|
expect(policy.defaultProfile, name).toBeTruthy();
|
|
expect(policy.profiles[/** @type {string} */ (policy.defaultProfile)], name).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
it("names every profile consistently with its key", () => {
|
|
for (const policy of Object.values(POLICY_PRESETS)) {
|
|
for (const [key, profile] of Object.entries(policy.profiles)) {
|
|
expect(profile.name).toBe(key);
|
|
}
|
|
}
|
|
});
|
|
});
|