export type PermissionAction = 'read' | 'write' | 'admin' /** Hierarchy: admin ⊃ write ⊃ read within the same section. */ export function hasPermission( granted: readonly string[], required: string, ): boolean { if (granted.includes(required)) return true const parts = required.split(':') if (parts.length !== 3) return false const [app, section, action] = parts if (action === 'read') { return ( granted.includes(`${app}:${section}:write`) || granted.includes(`${app}:${section}:admin`) ) } if (action === 'write') { return granted.includes(`${app}:${section}:admin`) } return false } export function permissionForRequest( method: string, url: string, ): string | null { const path = url.split('?')[0] ?? url const m = method.toUpperCase() const write = m !== 'GET' && m !== 'HEAD' && m !== 'OPTIONS' // Agent statistics live under /agents/:id/… — classify as stats, not agents. if ( /^\/api\/v1\/agents\/[^/]+\/(stats|blocked-ips|blocked-ports)$/.test(path) ) { return 'fw:stats:read' } if (path.startsWith('/api/v1/agents') || path.startsWith('/api/v1/install-links')) { return write ? 'fw:agents:write' : 'fw:agents:read' } if (path.startsWith('/api/v1/lists')) { return write ? 'fw:lists:write' : 'fw:lists:read' } if ( path.startsWith('/api/v1/rules') || path.startsWith('/api/v1/policies') || path.startsWith('/api/v1/policy-sets') ) { return write ? 'fw:policies:write' : 'fw:policies:read' } if (path.startsWith('/api/v1/stats') || path.startsWith('/api/v1/dashboard')) { return 'fw:stats:read' } if (path.startsWith('/api/v1/integrations')) { return write ? 'fw:settings:admin' : 'fw:lists:read' } if (path.startsWith('/api/v1/settings') || path.startsWith('/api/v1/install-context')) { return write ? 'fw:settings:admin' : 'fw:settings:read' } if (path.startsWith('/api/v1/audit')) { return 'fw:audit:read' } return null } export type AuthUser = { id: string email: string name: string apps: string[] permissions: string[] isAdmin: boolean }