mirror of
https://github.com/PR0M3TH3AN/bitgate.git
synced 2026-11-02 19:41:00 +00:00
core: add the allowlist policy preset
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
This commit is contained in:
1 parent
8674d6b33e
commit
af2818fbce
5 files changed
+143
-4
No files matched your search
@@ -89,6 +89,7 @@ export {
|
||||
SOCIAL_POLICY,
|
||||
COMMERCE_POLICY,
|
||||
ADMIN_ONLY_POLICY,
|
||||
ALLOWLIST_POLICY,
|
||||
POLICY_PRESETS,
|
||||
getPolicyPreset,
|
||||
} from "./presets.js";
|
||||
|
||||
@@ -247,6 +247,41 @@ export const ADMIN_ONLY_POLICY = createPolicyDefinition({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Allowlist-only — the symmetric twin of the denylist.
|
||||
*
|
||||
* A denylist names who is OUT; this names who is IN. A publisher is shown only
|
||||
* when an authorized actor has contributed a `user-allow` for them (from a
|
||||
* NIP-32 allow label, or an admin allowlist), and every unlisted publisher
|
||||
* misses the allowlist and is hidden across all four dimensions. It composes
|
||||
* with everything else: administrative denials still apply on top of an
|
||||
* allowlisted publisher, and protected actors (root, moderators) are never
|
||||
* gated by the allowlist — the people who curate the list can always see.
|
||||
*
|
||||
* Use it for invite-only or curated surfaces where "unknown until vouched for"
|
||||
* is the correct default. Populate `userAllow` exactly as you populate the
|
||||
* denylist: signed contributions resolved from relays.
|
||||
*/
|
||||
export const ALLOWLIST_POLICY = createPolicyDefinition({
|
||||
id: "bitgate-allowlist",
|
||||
name: "Allowlist only",
|
||||
description: "Only allowlisted publishers are shown; every unlisted one is hidden.",
|
||||
version: "1.0.0",
|
||||
defaultProfile: "default",
|
||||
profiles: {
|
||||
default: {
|
||||
name: "default",
|
||||
requireAllowlist: true,
|
||||
allowlistMiss: { visibility: "hide", interaction: "deny", transaction: "deny" },
|
||||
administrativeDeny: { visibility: "hide", interaction: "deny", transaction: "deny" },
|
||||
viewerBlock: { visibility: "hide", interaction: "deny" },
|
||||
allowViewerOverride: true,
|
||||
reports: {},
|
||||
mutes: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Presets addressable by name, for attribute-driven setup.
|
||||
* @type {Record<string, import('./policy.js').PolicyDefinition>}
|
||||
@@ -255,6 +290,7 @@ export const POLICY_PRESETS = {
|
||||
social: SOCIAL_POLICY,
|
||||
commerce: COMMERCE_POLICY,
|
||||
"admin-only": ADMIN_ONLY_POLICY,
|
||||
allowlist: ALLOWLIST_POLICY,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ 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,
|
||||
@@ -65,7 +66,16 @@ describe("preset registry", () => {
|
||||
});
|
||||
|
||||
it("exposes every preset in the registry", () => {
|
||||
expect(Object.keys(POLICY_PRESETS).sort()).toEqual(["admin-only", "commerce", "social"]);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -198,6 +208,43 @@ describe("admin-only preset", () => {
|
||||
});
|
||||
});
|
||||
|
||||
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.
|
||||
|
||||
Reference in new issue
Block a user