// Journal d'audit : trace persistante des opérations sensibles (création/révocation de tokens, // changements de réglages, génération de secrets, abonnements push, CRUD groupes). Exigence de // conformité entreprise (GDPR/SOX/ISO 27001). Règle ABSOLUE : ne JAMAIS journaliser un secret en // clair — `details` ne contient que des métadonnées non sensibles (ids, labels, compteurs). import { randomUUID } from 'node:crypto'; import type { AuditLogEntry } from '@arboretum/shared'; import type { Db } from '../db/index.js'; export type AuditResult = 'ok' | 'denied' | 'error'; export interface AuditEntry { /** tokenId de l'acteur, ou 'system' (opérations automatiques), ou 'anonymous' (avant auth). */ actor: string; /** verbe.objet, ex. 'token.create', 'settings.update', 'login.failure'. */ action: string; resourceId?: string | null; details?: Record | null; result?: AuditResult; } /** Enregistre une entrée d'audit. Best-effort : une erreur d'écriture ne casse jamais l'opération métier. */ export function recordAudit(db: Db, e: AuditEntry): void { try { db.prepare( 'INSERT INTO audit_logs (id, ts, actor, action, resource_id, details, result) VALUES (?, ?, ?, ?, ?, ?, ?)', ).run( randomUUID(), new Date().toISOString(), e.actor, e.action, e.resourceId ?? null, e.details ? JSON.stringify(e.details) : null, e.result ?? 'ok', ); } catch { /* l'audit ne doit jamais faire échouer l'action auditée */ } } /** Liste paginée par date décroissante (curseur `before` = ts strictement inférieur). */ export function listAudit(db: Db, opts: { limit: number; before?: string | null }): AuditLogEntry[] { const limit = Math.min(Math.max(Math.trunc(opts.limit) || 50, 1), 200); const rows = ( opts.before ? db .prepare( 'SELECT id, ts, actor, action, resource_id AS resourceId, details, result FROM audit_logs WHERE ts < ? ORDER BY ts DESC LIMIT ?', ) .all(opts.before, limit) : db .prepare( 'SELECT id, ts, actor, action, resource_id AS resourceId, details, result FROM audit_logs ORDER BY ts DESC LIMIT ?', ) .all(limit) ) as Array<{ id: string; ts: string; actor: string; action: string; resourceId: string | null; details: string | null; result: string }>; return rows.map((r) => ({ ...r, details: r.details ? safeParse(r.details) : null })); } function safeParse(s: string): unknown { try { return JSON.parse(s); } catch { return s; } }