import type { AuthzScope } from "./types.ts"; export interface AuthzAuditEvent { subjectId?: string; scope?: AuthzScope; permission: string; allowed: boolean; reason?: string; policy?: string; /** Epoch milliseconds. */ at: number; } export interface AuthzAuditSink { record(event: AuthzAuditEvent): void | Promise; } export interface MemoryAuditSink extends AuthzAuditSink { events: AuthzAuditEvent[]; clear(): void; } export function memoryAuditSink(): MemoryAuditSink { const events: AuthzAuditEvent[] = []; return { events, record: (event) => void events.push(event), clear: () => void events.splice(0, events.length), }; } /** * Subject ids, tenant ids, and denial reasons trace back to request input, so * a newline in one would forge a second audit line indistinguishable from a * real entry. Strip control characters before interpolating. */ function logSafe(value: string): string { let out = ""; for (const character of value) { const code = character.codePointAt(0)!; // C0 + DEL, plus NEL and the Unicode line/paragraph separators, which some // log shippers and JSON consumers also treat as line terminators. const isLineBreaking = code < 0x20 || code === 0x7f || code === 0x85 || code === 0x2028 || code === 0x2029; out += isLineBreaking ? " " : character; } return out; } export function consoleAuditSink(): AuthzAuditSink { return { record(event) { const verdict = event.allowed ? "allow" : "deny"; console.info( `[wrnexus:authz] ${verdict} ${logSafe(event.permission)} ` + `subject=${logSafe(event.subjectId ?? "anonymous")}` + `${event.scope?.tenantId ? ` tenant=${logSafe(event.scope.tenantId)}` : ""}` + `${event.reason ? ` reason=${logSafe(event.reason)}` : ""}` + `${event.policy ? ` policy=${logSafe(event.policy)}` : ""}`, ); }, }; } /** Record without ever letting a sink failure escape into the request path. */ export function safeRecord(sink: AuthzAuditSink | undefined, event: AuthzAuditEvent): void { if (!sink) return; try { const result = sink.record(event); if (result instanceof Promise) { result.catch((error) => console.warn("[wrnexus:authz] audit sink failed", error)); } } catch (error) { console.warn("[wrnexus:authz] audit sink failed", error); } }