feat(core): implement authority module with role and capability management

This commit is contained in:
thePR0M3TH3AN committed 2026-07-26 19:09:07 -04:00
1 parent 2c1ae75b71
commit 919d94c7a9
2 files changed
+670

No files matched your search

+278
View File
@@ -0,0 +1,278 @@
// Authority handling for Nostr Governance
//
// This module handles role resolution, capability checking, and authority
// validation for the governance system.
/**
* @typedef {Object} RoleDefinition
* @property {string} name - Role name
* @property {string[]} capabilities - Array of capability names this role has
*/
/**
* @typedef {Object} Actor
* @property {string} pubkey - 64-character hex pubkey of the actor
* @property {string[]} roles - Array of role names assigned to this actor
*/
/**
* @typedef {Object} AuthorityState
* @property {Record<string, RoleDefinition>} roles - Map of role names to definitions
* @property {Record<string, Actor>} actors - Map of pubkeys to actors
*/
/**
* Built-in role definitions
* @type {Record<string, RoleDefinition>}
*/
export const BUILTIN_ROLES = {
"root": {
name: "root",
capabilities: [
"manage-roles",
"manage-capabilities",
"manage-actors",
"manage-policies",
"manage-admin-lists",
"manage-trust-graph",
"manage-overrides",
"view-reports",
"view-trust-graph",
"view-admin-lists",
"view-policies"
]
},
"administrator": {
name: "administrator",
capabilities: [
"manage-admin-lists",
"manage-trust-graph",
"view-reports",
"view-trust-graph",
"view-admin-lists"
]
},
"moderator": {
name: "moderator",
capabilities: [
"submit-reports",
"submit-trust-endorsements",
"view-reports"
]
},
"trusted-user": {
name: "trusted-user",
capabilities: [
"submit-reports",
"view-reports"
]
},
"user": {
name: "user",
capabilities: [
"submit-reports"
]
}
};
/**
* Get capabilities for a role
* @param {string} roleName
* @param {AuthorityState} [authorityState]
* @returns {string[]}
*/
export function getRoleCapabilities(roleName, authorityState) {
// Check custom roles first
if (authorityState && authorityState.roles[roleName]) {
return authorityState.roles[roleName].capabilities;
}
// Check built-in roles
if (BUILTIN_ROLES[roleName]) {
return BUILTIN_ROLES[roleName].capabilities;
}
// Unknown role has no capabilities
return [];
}
/**
* Check if an actor has a specific role
* @param {string} pubkey - 64-character hex pubkey
* @param {string} roleName
* @param {AuthorityState} [authorityState]
* @returns {boolean}
*/
export function hasRole(pubkey, roleName, authorityState) {
// Normalize pubkey
const normalizedPubkey = pubkey.toLowerCase();
// Check custom actors first
if (authorityState && authorityState.actors[normalizedPubkey]) {
return authorityState.actors[normalizedPubkey].roles.includes(roleName);
}
// No actor found means no roles
return false;
}
/**
* Get all roles for an actor
* @param {string} pubkey - 64-character hex pubkey
* @param {AuthorityState} [authorityState]
* @returns {string[]}
*/
export function getActorRoles(pubkey, authorityState) {
// Normalize pubkey
const normalizedPubkey = pubkey.toLowerCase();
// Check custom actors first
if (authorityState && authorityState.actors[normalizedPubkey]) {
return authorityState.actors[normalizedPubkey].roles;
}
// No actor found means no roles
return [];
}
/**
* Check if an actor has a specific capability
* @param {string} pubkey - 64-character hex pubkey
* @param {string} capability
* @param {AuthorityState} [authorityState]
* @returns {boolean}
*/
export function hasCapability(pubkey, capability, authorityState) {
// Get all roles for the actor
const roles = getActorRoles(pubkey, authorityState);
// Check if any role has the capability
for (const role of roles) {
const capabilities = getRoleCapabilities(role, authorityState);
if (capabilities.includes(capability)) {
return true;
}
}
return false;
}
/**
* Get all capabilities for an actor
* @param {string} pubkey - 64-character hex pubkey
* @param {AuthorityState} [authorityState]
* @returns {string[]}
*/
export function getActorCapabilities(pubkey, authorityState) {
// Get all roles for the actor
const roles = getActorRoles(pubkey, authorityState);
// Collect all capabilities from all roles
const capabilities = new Set();
for (const role of roles) {
const roleCapabilities = getRoleCapabilities(role, authorityState);
for (const capability of roleCapabilities) {
capabilities.add(capability);
}
}
return Array.from(capabilities);
}
/**
* Create a new role definition
* @param {string} name - Role name
* @param {string[]} capabilities - Array of capability names
* @returns {RoleDefinition}
*/
export function createRoleDefinition(name, capabilities) {
// Validate input
if (typeof name !== "string" || !name.trim()) {
throw new Error("Role name must be a non-empty string");
}
if (!Array.isArray(capabilities)) {
throw new Error("Capabilities must be an array");
}
// Validate each capability
for (const capability of capabilities) {
if (typeof capability !== "string" || !capability.trim()) {
throw new Error("All capabilities must be non-empty strings");
}
}
return {
name: name.trim(),
capabilities: capabilities.map(c => c.trim())
};
}
/**
* Create a new actor
* @param {string} pubkey - 64-character hex pubkey
* @param {string[]} roles - Array of role names
* @returns {Actor}
*/
export function createActor(pubkey, roles) {
// Validate pubkey
if (typeof pubkey !== "string" || !/^[0-9a-f]{64}$/i.test(pubkey)) {
throw new Error("Pubkey must be a 64-character hex string");
}
// Validate roles
if (!Array.isArray(roles)) {
throw new Error("Roles must be an array");
}
// Validate each role
for (const role of roles) {
if (typeof role !== "string" || !role.trim()) {
throw new Error("All roles must be non-empty strings");
}
}
return {
pubkey: pubkey.toLowerCase(),
roles: roles.map(r => r.trim())
};
}
/**
* Create a new authority state
* @param {Record<string, RoleDefinition>} [roles]
* @param {Record<string, Actor>} [actors]
* @returns {AuthorityState}
*/
export function createAuthorityState(roles = {}, actors = {}) {
// Validate roles
for (const [name, role] of Object.entries(roles)) {
if (typeof name !== "string" || !name.trim()) {
throw new Error("Role names must be non-empty strings");
}
if (!role || typeof role !== "object") {
throw new Error("Role definitions must be objects");
}
if (role.name !== name) {
throw new Error(`Role name mismatch: ${name} !== ${role.name}`);
}
}
// Validate actors
for (const [pubkey, actor] of Object.entries(actors)) {
if (typeof pubkey !== "string" || !/^[0-9a-f]{64}$/i.test(pubkey)) {
throw new Error("Actor pubkeys must be 64-character hex strings");
}
if (!actor || typeof actor !== "object") {
throw new Error("Actors must be objects");
}
if (actor.pubkey !== pubkey.toLowerCase()) {
throw new Error(`Actor pubkey mismatch: ${pubkey} !== ${actor.pubkey}`);
}
}
return {
roles,
actors
};
}
+392
View File
@@ -0,0 +1,392 @@
import { describe, it, expect } from "vitest";
import {
BUILTIN_ROLES,
getRoleCapabilities,
hasRole,
getActorRoles,
hasCapability,
getActorCapabilities,
createRoleDefinition,
createActor,
createAuthorityState
} from "../src/authority.js";
describe("authority", () => {
describe("BUILTIN_ROLES", () => {
it("should define built-in roles with capabilities", () => {
expect(BUILTIN_ROLES).toHaveProperty("root");
expect(BUILTIN_ROLES).toHaveProperty("administrator");
expect(BUILTIN_ROLES).toHaveProperty("moderator");
expect(BUILTIN_ROLES).toHaveProperty("trusted-user");
expect(BUILTIN_ROLES).toHaveProperty("user");
// Check that each role has a name and capabilities
for (const [name, role] of Object.entries(BUILTIN_ROLES)) {
expect(role.name).toBe(name);
expect(Array.isArray(role.capabilities)).toBe(true);
expect(role.capabilities.length).toBeGreaterThan(0);
}
});
});
describe("getRoleCapabilities", () => {
it("should return capabilities for built-in roles", () => {
const capabilities = getRoleCapabilities("root");
expect(capabilities).toEqual(BUILTIN_ROLES.root.capabilities);
});
it("should return capabilities for custom roles", () => {
const authorityState = createAuthorityState({
"custom-role": {
name: "custom-role",
capabilities: ["custom-capability"]
}
});
const capabilities = getRoleCapabilities("custom-role", authorityState);
expect(capabilities).toEqual(["custom-capability"]);
});
it("should prefer custom roles over built-in roles", () => {
const authorityState = createAuthorityState({
"root": {
name: "root",
capabilities: ["custom-root-capability"]
}
});
const capabilities = getRoleCapabilities("root", authorityState);
expect(capabilities).toEqual(["custom-root-capability"]);
});
it("should return empty array for unknown roles", () => {
const capabilities = getRoleCapabilities("unknown-role");
expect(capabilities).toEqual([]);
});
});
describe("hasRole", () => {
it("should return true for actors with the specified role", () => {
const authorityState = createAuthorityState({}, {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["moderator"]
}
});
const result = hasRole(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"moderator",
authorityState
);
expect(result).toBe(true);
});
it("should return false for actors without the specified role", () => {
const authorityState = createAuthorityState({}, {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["user"]
}
});
const result = hasRole(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"moderator",
authorityState
);
expect(result).toBe(false);
});
it("should return false for unknown actors", () => {
const authorityState = createAuthorityState();
const result = hasRole(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"moderator",
authorityState
);
expect(result).toBe(false);
});
it("should normalize pubkeys to lowercase", () => {
const authorityState = createAuthorityState({}, {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["moderator"]
}
});
const result = hasRole(
"ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789",
"moderator",
authorityState
);
expect(result).toBe(true);
});
});
describe("getActorRoles", () => {
it("should return roles for known actors", () => {
const authorityState = createAuthorityState({}, {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["user", "trusted-user"]
}
});
const roles = getActorRoles(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
authorityState
);
expect(roles).toEqual(["user", "trusted-user"]);
});
it("should return empty array for unknown actors", () => {
const authorityState = createAuthorityState();
const roles = getActorRoles(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
authorityState
);
expect(roles).toEqual([]);
});
});
describe("hasCapability", () => {
it("should return true for actors with the specified capability", () => {
const authorityState = createAuthorityState({}, {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["moderator"]
}
});
const result = hasCapability(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"submit-reports",
authorityState
);
expect(result).toBe(true);
});
it("should return false for actors without the specified capability", () => {
const authorityState = createAuthorityState({}, {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["user"]
}
});
const result = hasCapability(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"manage-admin-lists",
authorityState
);
expect(result).toBe(false);
});
it("should return false for unknown actors", () => {
const authorityState = createAuthorityState();
const result = hasCapability(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"submit-reports",
authorityState
);
expect(result).toBe(false);
});
});
describe("getActorCapabilities", () => {
it("should return all capabilities for an actor's roles", () => {
const authorityState = createAuthorityState({}, {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["moderator", "trusted-user"]
}
});
const capabilities = getActorCapabilities(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
authorityState
);
// Should have capabilities from both roles, with duplicates removed
expect(capabilities).toContain("submit-reports");
expect(capabilities).toContain("view-reports");
expect(capabilities).toContain("submit-trust-endorsements");
});
it("should return empty array for unknown actors", () => {
const authorityState = createAuthorityState();
const capabilities = getActorCapabilities(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
authorityState
);
expect(capabilities).toEqual([]);
});
});
describe("createRoleDefinition", () => {
it("should create a valid role definition", () => {
const role = createRoleDefinition("test-role", ["capability1", "capability2"]);
expect(role).toEqual({
name: "test-role",
capabilities: ["capability1", "capability2"]
});
});
it("should trim whitespace from role name and capabilities", () => {
const role = createRoleDefinition(" test-role ", [" capability1 ", " capability2 "]);
expect(role).toEqual({
name: "test-role",
capabilities: ["capability1", "capability2"]
});
});
it("should throw error for invalid role name", () => {
expect(() => createRoleDefinition("", ["capability"])).toThrow();
expect(() => createRoleDefinition(/** @type {any} */ (123), ["capability"])).toThrow();
});
it("should throw error for invalid capabilities", () => {
expect(() => createRoleDefinition("test-role", /** @type {any} */ ("not-an-array"))).toThrow();
expect(() => createRoleDefinition("test-role", [/** @type {any} */ (123)])).toThrow();
expect(() => createRoleDefinition("test-role", [""])).toThrow();
});
});
describe("createActor", () => {
it("should create a valid actor", () => {
const actor = createActor(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
["role1", "role2"]
);
expect(actor).toEqual({
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["role1", "role2"]
});
});
it("should normalize pubkey to lowercase", () => {
const actor = createActor(
"ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789",
["role1"]
);
expect(actor.pubkey).toBe("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
});
it("should trim whitespace from roles", () => {
const actor = createActor(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
[" role1 ", " role2 "]
);
expect(actor.roles).toEqual(["role1", "role2"]);
});
it("should throw error for invalid pubkey", () => {
expect(() => createActor("invalid-pubkey", ["role"])).toThrow();
expect(() => createActor(/** @type {any} */ (123), ["role"])).toThrow();
expect(() => createActor("", ["role"])).toThrow();
});
it("should throw error for invalid roles", () => {
expect(() => createActor(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
/** @type {any} */ ("not-an-array")
)).toThrow();
expect(() => createActor(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
[/** @type {any} */ (123)]
)).toThrow();
expect(() => createActor(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
[""]
)).toThrow();
});
});
describe("createAuthorityState", () => {
it("should create a valid authority state", () => {
const roles = {
"test-role": {
name: "test-role",
capabilities: ["test-capability"]
}
};
const actors = {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["test-role"]
}
};
const state = createAuthorityState(roles, actors);
expect(state).toEqual({ roles, actors });
});
it("should create empty authority state with no arguments", () => {
const state = createAuthorityState();
expect(state).toEqual({ roles: {}, actors: {} });
});
it("should validate role names", () => {
const roles = {
"": {
name: "",
capabilities: ["test-capability"]
}
};
expect(() => createAuthorityState(roles)).toThrow();
});
it("should validate role definitions", () => {
const roles = {
"test-role": /** @type {any} */ (null)
};
expect(() => createAuthorityState(roles)).toThrow();
});
it("should validate role name consistency", () => {
const roles = {
"test-role": {
name: "different-name",
capabilities: ["test-capability"]
}
};
expect(() => createAuthorityState(roles)).toThrow();
});
it("should validate actor pubkeys", () => {
const actors = {
"invalid-pubkey": {
pubkey: "invalid-pubkey",
roles: ["test-role"]
}
};
expect(() => createAuthorityState({}, actors)).toThrow();
});
it("should validate actors", () => {
const actors = {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": /** @type {any} */ (null)
};
expect(() => createAuthorityState({}, actors)).toThrow();
});
it("should validate actor pubkey consistency", () => {
const actors = {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "different-pubkey",
roles: ["test-role"]
}
};
expect(() => createAuthorityState({}, actors)).toThrow();
});
});
});