Repository Public
1/** Append-only audit trail. `detail` must never contain secrets, file contents or terminal data. */2import type { FastifyBaseLogger, FastifyRequest } from 'fastify';3import type { Db } from './db.js';45export interface AuditEntry {6 actor?: string;7 action: string;8 target?: string | null;9 ip?: string | null;10 ok?: boolean;11 detail?: Record<string, unknown>;12}1314export class Audit {15 constructor(16 private readonly db: Db,17 private readonly log: FastifyBaseLogger,18 ) {}1920 /** Never throws: an audit write failure is logged and must not break the request. */21 async record(e: AuditEntry): Promise<void> {22 try {23 await this.db.q(24 'INSERT INTO audit_events (actor, action, target, ip, ok, detail) VALUES ($1, $2, $3, $4, $5, $6)',25 [e.actor ?? 'system', e.action, e.target ?? null, e.ip ?? null, e.ok ?? true, JSON.stringify(e.detail ?? {})],26 );27 } catch (err) {28 this.log.error({ err, action: e.action }, 'audit write failed');29 }30 }31 /** Convenience: actor + ip taken from the request. */32 req(req: FastifyRequest, action: string, o: Omit<AuditEntry, 'action' | 'ip'> = {}): Promise<void> {33 return this.record({ actor: req.auth?.username ?? 'anonymous', ip: req.ip, ...o, action });34 }35}36