import { createHash } from "node:crypto"; import { query } from "../pool.js"; export interface AuditEventInput { actorType: "user" | "system" | "service"; actorId?: string | null; action: string; resourceType: string; resourceId?: string | null; metadata?: Record; ipAddress?: string | null; } function canonicalJson(value: unknown): string { if (value === null || typeof value !== "object") return JSON.stringify(value); if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; const obj = value as Record; const keys = Object.keys(obj).sort(); return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`; } /** * Appends one event to the audit_log hash chain (see baseline-schema * migration and .claude/skills/eden-security/references/audit.md). Never * call this inside a larger transaction that might roll back for * unrelated reasons without considering that the chain must stay * contiguous — a rolled-back event never happened (fine, nothing was * recorded), but a *committed* event must never be skipped when * computing previous_event_hash for the next one. */ export async function appendAuditEvent(input: AuditEventInput): Promise { const { rows } = await query<{ event_hash: string }>( "SELECT event_hash FROM audit_log ORDER BY seq DESC LIMIT 1", ); const previousEventHash = rows[0]?.event_hash ?? ""; const fields = { actorType: input.actorType, actorId: input.actorId ?? null, action: input.action, resourceType: input.resourceType, resourceId: input.resourceId ?? null, metadata: input.metadata ?? {}, ipAddress: input.ipAddress ?? null, }; const eventHash = createHash("sha256") .update(previousEventHash + canonicalJson(fields)) .digest("hex"); await query( `INSERT INTO audit_log (actor_type, actor_id, action, resource_type, resource_id, metadata, ip_address, previous_event_hash, event_hash) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [ input.actorType, input.actorId ?? null, input.action, input.resourceType, input.resourceId ?? null, JSON.stringify(input.metadata ?? {}), input.ipAddress ?? null, previousEventHash || null, eventHash, ], ); }