mirror of
https://github.com/PR0M3TH3AN/bitgate.git
synced 2026-10-27 08:31:00 +00:00
feat(core): implement policy module with decision making and effect composition
This commit is contained in:
1 parent
8492411265
commit
b24500010f
2 files changed
+808
No files matched your search
@@ -0,0 +1,380 @@
|
|||||||
|
// Policy handling for Nostr Governance
|
||||||
|
//
|
||||||
|
// This module handles policy definitions, decision making, and effect calculations
|
||||||
|
// for the governance system.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PolicyEffect
|
||||||
|
* @property {"allow"|"restrict"|"hide"|"deny"} effect - The base effect
|
||||||
|
* @property {string} [reason] - Reason for the effect
|
||||||
|
* @property {any} [details] - Additional details about the effect
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PolicyProfile
|
||||||
|
* @property {string} name - Profile name
|
||||||
|
* @property {Record<string, PolicyEffect>} effects - Map of effect types to policy effects
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PolicyDefinition
|
||||||
|
* @property {string} id - Unique policy identifier
|
||||||
|
* @property {string} name - Human-readable name
|
||||||
|
* @property {string} description - Description of what the policy does
|
||||||
|
* @property {Record<string, PolicyProfile>} profiles - Map of profile names to policy profiles
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PolicyContext
|
||||||
|
* @property {string} surface - The surface where the policy is being applied (e.g., "feed", "search", "checkout")
|
||||||
|
* @property {string} policyProfile - The policy profile to use
|
||||||
|
* @property {Object} [enforcement] - Enforcement options
|
||||||
|
* @property {boolean} [enforcement.hardHide] - Whether to hard hide denied items
|
||||||
|
* @property {boolean} [enforcement.allowOverrides] - Whether to allow viewer overrides
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} GovernanceDecision
|
||||||
|
* @property {PolicyEffect} visibility - Visibility effect
|
||||||
|
* @property {PolicyEffect} interaction - Interaction effect
|
||||||
|
* @property {PolicyEffect} [transaction] - Transaction effect (for commerce)
|
||||||
|
* @property {string[]} reasons - Array of reasons for the decision
|
||||||
|
* @property {any[]} evidence - Array of evidence supporting the decision
|
||||||
|
* @property {Object} [metadata] - Additional metadata about the decision
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Built-in policy profiles
|
||||||
|
* @type {Record<string, PolicyProfile>}
|
||||||
|
*/
|
||||||
|
export const BUILTIN_POLICY_PROFILES = {
|
||||||
|
"default": {
|
||||||
|
name: "default",
|
||||||
|
effects: {
|
||||||
|
"visibility": { effect: "allow" },
|
||||||
|
"interaction": { effect: "allow" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"moderate": {
|
||||||
|
name: "moderate",
|
||||||
|
effects: {
|
||||||
|
"visibility": { effect: "restrict" },
|
||||||
|
"interaction": { effect: "allow" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strict": {
|
||||||
|
name: "strict",
|
||||||
|
effects: {
|
||||||
|
"visibility": { effect: "hide" },
|
||||||
|
"interaction": { effect: "deny" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"commerce-default": {
|
||||||
|
name: "commerce-default",
|
||||||
|
effects: {
|
||||||
|
"visibility": { effect: "allow" },
|
||||||
|
"interaction": { effect: "allow" },
|
||||||
|
"transaction": { effect: "allow" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"commerce-transaction": {
|
||||||
|
name: "commerce-transaction",
|
||||||
|
effects: {
|
||||||
|
"visibility": { effect: "allow" },
|
||||||
|
"interaction": { effect: "allow" },
|
||||||
|
"transaction": { effect: "deny" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a policy effect
|
||||||
|
* @param {"allow"|"restrict"|"hide"|"deny"} effect
|
||||||
|
* @param {string} [reason]
|
||||||
|
* @param {any} [details]
|
||||||
|
* @returns {PolicyEffect}
|
||||||
|
*/
|
||||||
|
export function createPolicyEffect(effect, reason, details) {
|
||||||
|
if (!["allow", "restrict", "hide", "deny"].includes(effect)) {
|
||||||
|
throw new Error("Invalid policy effect: " + effect);
|
||||||
|
}
|
||||||
|
|
||||||
|
const policyEffect = { effect };
|
||||||
|
|
||||||
|
if (reason) {
|
||||||
|
policyEffect.reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details) {
|
||||||
|
policyEffect.details = details;
|
||||||
|
}
|
||||||
|
|
||||||
|
return policyEffect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a policy profile
|
||||||
|
* @param {string} name
|
||||||
|
* @param {Record<string, PolicyEffect>} effects
|
||||||
|
* @returns {PolicyProfile}
|
||||||
|
*/
|
||||||
|
export function createPolicyProfile(name, effects) {
|
||||||
|
if (typeof name !== "string" || !name.trim()) {
|
||||||
|
throw new Error("Policy profile name must be a non-empty string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!effects || typeof effects !== "object") {
|
||||||
|
throw new Error("Policy profile effects must be an object");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate each effect
|
||||||
|
for (const [effectType, effect] of Object.entries(effects)) {
|
||||||
|
if (typeof effectType !== "string" || !effectType.trim()) {
|
||||||
|
throw new Error("Effect types must be non-empty strings");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!effect || typeof effect !== "object") {
|
||||||
|
throw new Error("Effects must be objects");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!["allow", "restrict", "hide", "deny"].includes(effect.effect)) {
|
||||||
|
throw new Error("Invalid policy effect: " + effect.effect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: name.trim(),
|
||||||
|
effects
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a policy definition
|
||||||
|
* @param {string} id
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} description
|
||||||
|
* @param {Record<string, PolicyProfile>} profiles
|
||||||
|
* @returns {PolicyDefinition}
|
||||||
|
*/
|
||||||
|
export function createPolicyDefinition(id, name, description, profiles) {
|
||||||
|
if (typeof id !== "string" || !id.trim()) {
|
||||||
|
throw new Error("Policy ID must be a non-empty string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof name !== "string" || !name.trim()) {
|
||||||
|
throw new Error("Policy name must be a non-empty string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof description !== "string") {
|
||||||
|
throw new Error("Policy description must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profiles || typeof profiles !== "object") {
|
||||||
|
throw new Error("Policy profiles must be an object");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate each profile
|
||||||
|
for (const [profileName, profile] of Object.entries(profiles)) {
|
||||||
|
if (typeof profileName !== "string" || !profileName.trim()) {
|
||||||
|
throw new Error("Profile names must be non-empty strings");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profile || typeof profile !== "object") {
|
||||||
|
throw new Error("Profiles must be objects");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile.name !== profileName) {
|
||||||
|
throw new Error(`Profile name mismatch: ${profileName} !== ${profile.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id.trim(),
|
||||||
|
name: name.trim(),
|
||||||
|
description,
|
||||||
|
profiles
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a policy context
|
||||||
|
* @param {string} surface
|
||||||
|
* @param {string} policyProfile
|
||||||
|
* @param {Object} [enforcement]
|
||||||
|
* @returns {PolicyContext}
|
||||||
|
*/
|
||||||
|
export function createPolicyContext(surface, policyProfile, enforcement) {
|
||||||
|
if (typeof surface !== "string" || !surface.trim()) {
|
||||||
|
throw new Error("Surface must be a non-empty string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof policyProfile !== "string" || !policyProfile.trim()) {
|
||||||
|
throw new Error("Policy profile must be a non-empty string");
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
surface: surface.trim(),
|
||||||
|
policyProfile: policyProfile.trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (enforcement) {
|
||||||
|
context.enforcement = {};
|
||||||
|
|
||||||
|
if (typeof enforcement.hardHide === "boolean") {
|
||||||
|
context.enforcement.hardHide = enforcement.hardHide;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof enforcement.allowOverrides === "boolean") {
|
||||||
|
context.enforcement.allowOverrides = enforcement.allowOverrides;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a governance decision
|
||||||
|
* @param {PolicyEffect} visibility
|
||||||
|
* @param {PolicyEffect} interaction
|
||||||
|
* @param {PolicyEffect} [transaction]
|
||||||
|
* @param {string[]} [reasons]
|
||||||
|
* @param {any[]} [evidence]
|
||||||
|
* @param {Object} [metadata]
|
||||||
|
* @returns {GovernanceDecision}
|
||||||
|
*/
|
||||||
|
export function createGovernanceDecision(visibility, interaction, transaction, reasons, evidence, metadata) {
|
||||||
|
// Validate visibility effect
|
||||||
|
if (!visibility || typeof visibility !== "object" ||
|
||||||
|
!["allow", "restrict", "hide", "deny"].includes(visibility.effect)) {
|
||||||
|
throw new Error("Invalid visibility effect");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate interaction effect
|
||||||
|
if (!interaction || typeof interaction !== "object" ||
|
||||||
|
!["allow", "restrict", "hide", "deny"].includes(interaction.effect)) {
|
||||||
|
throw new Error("Invalid interaction effect");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate transaction effect if provided
|
||||||
|
if (transaction && (typeof transaction !== "object" ||
|
||||||
|
!["allow", "restrict", "hide", "deny"].includes(transaction.effect))) {
|
||||||
|
throw new Error("Invalid transaction effect");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate reasons
|
||||||
|
if (reasons && !Array.isArray(reasons)) {
|
||||||
|
throw new Error("Reasons must be an array");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate evidence
|
||||||
|
if (evidence && !Array.isArray(evidence)) {
|
||||||
|
throw new Error("Evidence must be an array");
|
||||||
|
}
|
||||||
|
|
||||||
|
const decision = {
|
||||||
|
visibility,
|
||||||
|
interaction,
|
||||||
|
reasons: reasons || [],
|
||||||
|
evidence: evidence || []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (transaction) {
|
||||||
|
decision.transaction = transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata) {
|
||||||
|
decision.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get effect for a policy profile
|
||||||
|
* @param {string} profileName
|
||||||
|
* @param {string} effectType
|
||||||
|
* @param {Record<string, PolicyDefinition>} [customPolicies]
|
||||||
|
* @returns {PolicyEffect}
|
||||||
|
*/
|
||||||
|
export function getPolicyEffect(profileName, effectType, customPolicies) {
|
||||||
|
// Check custom policies first
|
||||||
|
if (customPolicies) {
|
||||||
|
for (const policy of Object.values(customPolicies)) {
|
||||||
|
if (policy.profiles[profileName] && policy.profiles[profileName].effects[effectType]) {
|
||||||
|
return policy.profiles[profileName].effects[effectType];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check built-in profiles
|
||||||
|
if (BUILTIN_POLICY_PROFILES[profileName] &&
|
||||||
|
BUILTIN_POLICY_PROFILES[profileName].effects[effectType]) {
|
||||||
|
return BUILTIN_POLICY_PROFILES[profileName].effects[effectType];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to allow
|
||||||
|
return { effect: "allow" };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose multiple governance decisions into a single decision
|
||||||
|
* @param {GovernanceDecision[]} decisions
|
||||||
|
* @returns {GovernanceDecision}
|
||||||
|
*/
|
||||||
|
export function composeGovernanceDecisions(decisions) {
|
||||||
|
if (!Array.isArray(decisions) || decisions.length === 0) {
|
||||||
|
// Return a default allow decision
|
||||||
|
return createGovernanceDecision(
|
||||||
|
{ effect: "allow" },
|
||||||
|
{ effect: "allow" },
|
||||||
|
undefined,
|
||||||
|
[],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with the first decision
|
||||||
|
const composed = { ...decisions[0] };
|
||||||
|
|
||||||
|
// For each subsequent decision, apply the most restrictive effects
|
||||||
|
for (let i = 1; i < decisions.length; i++) {
|
||||||
|
const decision = decisions[i];
|
||||||
|
|
||||||
|
// Compose visibility effect (hide > restrict > deny > allow)
|
||||||
|
const visibilityOrder = { "allow": 0, "restrict": 1, "hide": 2, "deny": 3 };
|
||||||
|
if (visibilityOrder[decision.visibility.effect] > visibilityOrder[composed.visibility.effect]) {
|
||||||
|
composed.visibility = decision.visibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compose interaction effect (deny > hide > restrict > allow)
|
||||||
|
const interactionOrder = { "allow": 0, "restrict": 1, "hide": 2, "deny": 3 };
|
||||||
|
if (interactionOrder[decision.interaction.effect] > interactionOrder[composed.interaction.effect]) {
|
||||||
|
composed.interaction = decision.interaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compose transaction effect if present
|
||||||
|
if (decision.transaction) {
|
||||||
|
if (!composed.transaction) {
|
||||||
|
composed.transaction = decision.transaction;
|
||||||
|
} else {
|
||||||
|
const transactionOrder = { "allow": 0, "restrict": 1, "hide": 2, "deny": 3 };
|
||||||
|
if (transactionOrder[decision.transaction.effect] > transactionOrder[composed.transaction.effect]) {
|
||||||
|
composed.transaction = decision.transaction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine reasons and evidence
|
||||||
|
for (const reason of decision.reasons) {
|
||||||
|
if (!composed.reasons.includes(reason)) {
|
||||||
|
composed.reasons.push(reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const evidence of decision.evidence) {
|
||||||
|
composed.evidence.push(evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return composed;
|
||||||
|
}
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
BUILTIN_POLICY_PROFILES,
|
||||||
|
createPolicyEffect,
|
||||||
|
createPolicyProfile,
|
||||||
|
createPolicyDefinition,
|
||||||
|
createPolicyContext,
|
||||||
|
createGovernanceDecision,
|
||||||
|
getPolicyEffect,
|
||||||
|
composeGovernanceDecisions
|
||||||
|
} from "../src/policy.js";
|
||||||
|
|
||||||
|
describe("policy", () => {
|
||||||
|
describe("BUILTIN_POLICY_PROFILES", () => {
|
||||||
|
it("should define built-in policy profiles with effects", () => {
|
||||||
|
expect(BUILTIN_POLICY_PROFILES).toHaveProperty("default");
|
||||||
|
expect(BUILTIN_POLICY_PROFILES).toHaveProperty("moderate");
|
||||||
|
expect(BUILTIN_POLICY_PROFILES).toHaveProperty("strict");
|
||||||
|
expect(BUILTIN_POLICY_PROFILES).toHaveProperty("commerce-default");
|
||||||
|
expect(BUILTIN_POLICY_PROFILES).toHaveProperty("commerce-transaction");
|
||||||
|
|
||||||
|
// Check that each profile has a name and effects
|
||||||
|
for (const [name, profile] of Object.entries(BUILTIN_POLICY_PROFILES)) {
|
||||||
|
expect(profile.name).toBe(name);
|
||||||
|
expect(profile.effects).toBeDefined();
|
||||||
|
expect(typeof profile.effects).toBe("object");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createPolicyEffect", () => {
|
||||||
|
it("should create a valid policy effect", () => {
|
||||||
|
const effect = createPolicyEffect("hide", "User is blocked", { details: "more info" });
|
||||||
|
expect(effect).toEqual({
|
||||||
|
effect: "hide",
|
||||||
|
reason: "User is blocked",
|
||||||
|
details: { details: "more info" }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a policy effect without optional fields", () => {
|
||||||
|
const effect = createPolicyEffect("allow");
|
||||||
|
expect(effect).toEqual({ effect: "allow" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid effect", () => {
|
||||||
|
expect(() => createPolicyEffect(/** @type {any} */ ("invalid"))).toThrow();
|
||||||
|
expect(() => createPolicyEffect(/** @type {any} */ (123))).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createPolicyProfile", () => {
|
||||||
|
it("should create a valid policy profile", () => {
|
||||||
|
const profile = createPolicyProfile("test-profile", {
|
||||||
|
"visibility": { effect: "hide", reason: "Hidden" },
|
||||||
|
"interaction": { effect: "deny", reason: "Denied" }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(profile).toEqual({
|
||||||
|
name: "test-profile",
|
||||||
|
effects: {
|
||||||
|
"visibility": { effect: "hide", reason: "Hidden" },
|
||||||
|
"interaction": { effect: "deny", reason: "Denied" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid profile name", () => {
|
||||||
|
expect(() => createPolicyProfile("", {})).toThrow();
|
||||||
|
expect(() => createPolicyProfile(/** @type {any} */ (123), {})).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid effects", () => {
|
||||||
|
expect(() => createPolicyProfile("test-profile", /** @type {any} */ (null))).toThrow();
|
||||||
|
expect(() => createPolicyProfile("test-profile", /** @type {any} */ ("not-an-object"))).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid effect types", () => {
|
||||||
|
expect(() => createPolicyProfile("test-profile", {
|
||||||
|
"": { effect: "allow" }
|
||||||
|
})).toThrow();
|
||||||
|
|
||||||
|
expect(() => createPolicyProfile("test-profile", {
|
||||||
|
"visibility": /** @type {any} */ (null)
|
||||||
|
})).toThrow();
|
||||||
|
|
||||||
|
expect(() => createPolicyProfile("test-profile", {
|
||||||
|
"visibility": { effect: /** @type {any} */ ("invalid") }
|
||||||
|
})).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createPolicyDefinition", () => {
|
||||||
|
it("should create a valid policy definition", () => {
|
||||||
|
const definition = createPolicyDefinition(
|
||||||
|
"test-policy",
|
||||||
|
"Test Policy",
|
||||||
|
"A test policy",
|
||||||
|
{
|
||||||
|
"default": {
|
||||||
|
name: "default",
|
||||||
|
effects: {
|
||||||
|
"visibility": { effect: "allow" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(definition).toEqual({
|
||||||
|
id: "test-policy",
|
||||||
|
name: "Test Policy",
|
||||||
|
description: "A test policy",
|
||||||
|
profiles: {
|
||||||
|
"default": {
|
||||||
|
name: "default",
|
||||||
|
effects: {
|
||||||
|
"visibility": { effect: "allow" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid ID", () => {
|
||||||
|
expect(() => createPolicyDefinition("", "Test Policy", "A test policy", {})).toThrow();
|
||||||
|
expect(() => createPolicyDefinition(/** @type {any} */ (123), "Test Policy", "A test policy", {})).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid name", () => {
|
||||||
|
expect(() => createPolicyDefinition("test-policy", "", "A test policy", {})).toThrow();
|
||||||
|
expect(() => createPolicyDefinition("test-policy", /** @type {any} */ (123), "A test policy", {})).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid description", () => {
|
||||||
|
expect(() => createPolicyDefinition("test-policy", "Test Policy", /** @type {any} */ (123), {})).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid profiles", () => {
|
||||||
|
expect(() => createPolicyDefinition("test-policy", "Test Policy", "A test policy", /** @type {any} */ (null))).toThrow();
|
||||||
|
expect(() => createPolicyDefinition("test-policy", "Test Policy", "A test policy", /** @type {any} */ ("not-an-object"))).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid profile names", () => {
|
||||||
|
expect(() => createPolicyDefinition(
|
||||||
|
"test-policy",
|
||||||
|
"Test Policy",
|
||||||
|
"A test policy",
|
||||||
|
{
|
||||||
|
"": {
|
||||||
|
name: "",
|
||||||
|
effects: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for profile name mismatch", () => {
|
||||||
|
expect(() => createPolicyDefinition(
|
||||||
|
"test-policy",
|
||||||
|
"Test Policy",
|
||||||
|
"A test policy",
|
||||||
|
{
|
||||||
|
"default": {
|
||||||
|
name: "different-name",
|
||||||
|
effects: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createPolicyContext", () => {
|
||||||
|
it("should create a valid policy context", () => {
|
||||||
|
const context = createPolicyContext("feed", "default");
|
||||||
|
expect(context).toEqual({
|
||||||
|
surface: "feed",
|
||||||
|
policyProfile: "default"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a policy context with enforcement options", () => {
|
||||||
|
const context = createPolicyContext("feed", "default", {
|
||||||
|
hardHide: true,
|
||||||
|
allowOverrides: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(context).toEqual({
|
||||||
|
surface: "feed",
|
||||||
|
policyProfile: "default",
|
||||||
|
enforcement: {
|
||||||
|
hardHide: true,
|
||||||
|
allowOverrides: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid surface", () => {
|
||||||
|
expect(() => createPolicyContext("", "default")).toThrow();
|
||||||
|
expect(() => createPolicyContext(/** @type {any} */ (123), "default")).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid policy profile", () => {
|
||||||
|
expect(() => createPolicyContext("feed", "")).toThrow();
|
||||||
|
expect(() => createPolicyContext("feed", /** @type {any} */ (123))).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createGovernanceDecision", () => {
|
||||||
|
it("should create a valid governance decision", () => {
|
||||||
|
const decision = createGovernanceDecision(
|
||||||
|
{ effect: "hide", reason: "Hidden" },
|
||||||
|
{ effect: "deny", reason: "Denied" },
|
||||||
|
{ effect: "deny", reason: "Transaction denied" },
|
||||||
|
["reason1", "reason2"],
|
||||||
|
[{ evidence: "evidence1" }, { evidence: "evidence2" }],
|
||||||
|
{ metadata: "data" }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(decision).toEqual({
|
||||||
|
visibility: { effect: "hide", reason: "Hidden" },
|
||||||
|
interaction: { effect: "deny", reason: "Denied" },
|
||||||
|
transaction: { effect: "deny", reason: "Transaction denied" },
|
||||||
|
reasons: ["reason1", "reason2"],
|
||||||
|
evidence: [{ evidence: "evidence1" }, { evidence: "evidence2" }],
|
||||||
|
metadata: { metadata: "data" }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a governance decision without optional fields", () => {
|
||||||
|
const decision = createGovernanceDecision(
|
||||||
|
{ effect: "allow" },
|
||||||
|
{ effect: "allow" }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(decision).toEqual({
|
||||||
|
visibility: { effect: "allow" },
|
||||||
|
interaction: { effect: "allow" },
|
||||||
|
reasons: [],
|
||||||
|
evidence: []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid visibility effect", () => {
|
||||||
|
expect(() => createGovernanceDecision(
|
||||||
|
/** @type {any} */ ({ effect: "invalid" }),
|
||||||
|
{ effect: "allow" }
|
||||||
|
)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid interaction effect", () => {
|
||||||
|
expect(() => createGovernanceDecision(
|
||||||
|
{ effect: "allow" },
|
||||||
|
/** @type {any} */ ({ effect: "invalid" })
|
||||||
|
)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid transaction effect", () => {
|
||||||
|
expect(() => createGovernanceDecision(
|
||||||
|
{ effect: "allow" },
|
||||||
|
{ effect: "allow" },
|
||||||
|
/** @type {any} */ ({ effect: "invalid" })
|
||||||
|
)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid reasons", () => {
|
||||||
|
expect(() => createGovernanceDecision(
|
||||||
|
{ effect: "allow" },
|
||||||
|
{ effect: "allow" },
|
||||||
|
undefined,
|
||||||
|
/** @type {any} */ ("not-an-array")
|
||||||
|
)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid evidence", () => {
|
||||||
|
expect(() => createGovernanceDecision(
|
||||||
|
{ effect: "allow" },
|
||||||
|
{ effect: "allow" },
|
||||||
|
undefined,
|
||||||
|
[],
|
||||||
|
/** @type {any} */ ("not-an-array")
|
||||||
|
)).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getPolicyEffect", () => {
|
||||||
|
it("should return effect for built-in profiles", () => {
|
||||||
|
const effect = getPolicyEffect("strict", "visibility");
|
||||||
|
expect(effect).toEqual({ effect: "hide" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return effect for custom policies", () => {
|
||||||
|
const customPolicies = {
|
||||||
|
"custom-policy": {
|
||||||
|
id: "custom-policy",
|
||||||
|
name: "Custom Policy",
|
||||||
|
description: "A custom policy",
|
||||||
|
profiles: {
|
||||||
|
"custom-profile": {
|
||||||
|
name: "custom-profile",
|
||||||
|
effects: {
|
||||||
|
"visibility": { effect: "hide", reason: "Custom hidden" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const effect = getPolicyEffect("custom-profile", "visibility", /** @type {any} */ (customPolicies));
|
||||||
|
expect(effect).toEqual({ effect: "hide", reason: "Custom hidden" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prefer custom policies over built-in profiles", () => {
|
||||||
|
const customPolicies = {
|
||||||
|
"custom-policy": {
|
||||||
|
id: "custom-policy",
|
||||||
|
name: "Custom Policy",
|
||||||
|
description: "A custom policy",
|
||||||
|
profiles: {
|
||||||
|
"default": {
|
||||||
|
name: "default",
|
||||||
|
effects: {
|
||||||
|
"visibility": { effect: "hide", reason: "Custom default" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const effect = getPolicyEffect("default", "visibility", /** @type {any} */ (customPolicies));
|
||||||
|
expect(effect).toEqual({ effect: "hide", reason: "Custom default" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return default allow effect for unknown profiles/effects", () => {
|
||||||
|
const effect = getPolicyEffect("unknown-profile", "unknown-effect");
|
||||||
|
expect(effect).toEqual({ effect: "allow" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("composeGovernanceDecisions", () => {
|
||||||
|
it("should return default allow decision for empty array", () => {
|
||||||
|
const decision = composeGovernanceDecisions([]);
|
||||||
|
expect(decision).toEqual({
|
||||||
|
visibility: { effect: "allow" },
|
||||||
|
interaction: { effect: "allow" },
|
||||||
|
reasons: [],
|
||||||
|
evidence: []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the single decision for array with one element", () => {
|
||||||
|
const inputDecision = createGovernanceDecision(
|
||||||
|
{ effect: "hide", reason: "Hidden" },
|
||||||
|
{ effect: "deny", reason: "Denied" },
|
||||||
|
undefined,
|
||||||
|
["reason1"],
|
||||||
|
[{ evidence: "evidence1" }]
|
||||||
|
);
|
||||||
|
|
||||||
|
const decision = composeGovernanceDecisions([inputDecision]);
|
||||||
|
expect(decision).toEqual(inputDecision);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should compose multiple decisions with most restrictive effects", () => {
|
||||||
|
const decisions = [
|
||||||
|
createGovernanceDecision(
|
||||||
|
{ effect: "allow" },
|
||||||
|
{ effect: "allow" }
|
||||||
|
),
|
||||||
|
createGovernanceDecision(
|
||||||
|
{ effect: "hide", reason: "Hidden" },
|
||||||
|
{ effect: "deny", reason: "Denied" }
|
||||||
|
),
|
||||||
|
createGovernanceDecision(
|
||||||
|
{ effect: "restrict", reason: "Restricted" },
|
||||||
|
{ effect: "allow" }
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
const decision = composeGovernanceDecisions(decisions);
|
||||||
|
expect(decision.visibility).toEqual({ effect: "hide", reason: "Hidden" });
|
||||||
|
expect(decision.interaction).toEqual({ effect: "deny", reason: "Denied" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should combine reasons and evidence from all decisions", () => {
|
||||||
|
const decisions = [
|
||||||
|
createGovernanceDecision(
|
||||||
|
{ effect: "allow" },
|
||||||
|
{ effect: "allow" },
|
||||||
|
undefined,
|
||||||
|
["reason1", "reason2"],
|
||||||
|
[{ evidence: "evidence1" }]
|
||||||
|
),
|
||||||
|
createGovernanceDecision(
|
||||||
|
{ effect: "hide" },
|
||||||
|
{ effect: "deny" },
|
||||||
|
undefined,
|
||||||
|
["reason2", "reason3"],
|
||||||
|
[{ evidence: "evidence2" }]
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
const decision = composeGovernanceDecisions(decisions);
|
||||||
|
expect(decision.reasons).toEqual(["reason1", "reason2", "reason3"]);
|
||||||
|
expect(decision.evidence).toEqual([
|
||||||
|
{ evidence: "evidence1" },
|
||||||
|
{ evidence: "evidence2" }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should compose transaction effects when present", () => {
|
||||||
|
const decisions = [
|
||||||
|
createGovernanceDecision(
|
||||||
|
{ effect: "allow" },
|
||||||
|
{ effect: "allow" },
|
||||||
|
{ effect: "allow" }
|
||||||
|
),
|
||||||
|
createGovernanceDecision(
|
||||||
|
{ effect: "hide" },
|
||||||
|
{ effect: "deny" },
|
||||||
|
{ effect: "deny", reason: "Transaction denied" }
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
const decision = composeGovernanceDecisions(decisions);
|
||||||
|
expect(decision.transaction).toEqual({ effect: "deny", reason: "Transaction denied" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user