CI / Build & test (Node 24) (push) Has been cancelled
CI / Pack & boot smoke (Node 22) (push) Has been cancelled
CI / No em/en dashes (push) Has been cancelled
CI / Build & test (Node 22) (push) Has been cancelled
Deploy site (production) / build-and-deploy (push) Successful in 20s
Remplace les 547 tirets cadratins (U+2014) et demi-cadratins (U+2013) des fichiers versionnés par la ponctuation contextuelle adaptée (point médian, deux-points, virgule, parenthèses ; tiret simple pour les plages), sur 122 fichiers (appli, vitrine, doc, tests, workflows, scripts). Ajoute le job CI « lint-dashes » (git grep -P) qui échoue si un tiret cadratin/demi-cadratin réapparaît, hors logo binaire et captures brutes du terminal (fidélité des fixtures de détection de dialogue).
66 lines
2.5 KiB
TypeScript
66 lines
2.5 KiB
TypeScript
// 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<string, unknown> | 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;
|
|
}
|
|
}
|