/** * Phishing Audit Trail (Phase 20) * * Single, append-only audit-event writer used by every state-changing * phishing-triage service call (remediation-service.ts). Every write is a * parameterized INSERT into `audit_events` — no ON CONFLICT, no updates, * no deletes. This is the ONLY place that inserts into `audit_events`. * * Canonical `event_type` strings emitted across this phase (Phase 19 + 20): * - 'campaign_classified' (Phase 19 — not emitted by this file) * - 'remediation_approved' (remediation-service.ts, Task 2) * - 'remediation_completed' (remediation-service.ts, Task 2) * - 'campaign_marked_false_positive' (remediation-service.ts, Task 3) * * `writeAuditEvent` accepts an optional transaction `client` so a caller can * write its state change and its audit row inside the SAME * `postgresClient.transaction(...)` block — a rollback discards both * together, guaranteeing no state change can commit without its audit row * (REMED-06 / T-20-03). */ import { postgresClient } from './postgres-client'; export interface AuditEventInput { campaignId: string; actor: string | null; eventType: string; payload: Record; } /** * Minimal shape shared by both `postgresClient` and a transaction `PoolClient` * — only the `query` method this module needs, so a fake test client or a * real `pg.PoolClient` both satisfy it without importing `pg` here. */ export interface AuditQueryClient { query: (text: string, params?: unknown[]) => Promise<{ rows: Array<{ id: string }> }>; } /** * Inserts one row into `audit_events` and returns its id. Pass `client` * (a transaction client) to make the write commit/rollback atomically with * the caller's own state change; omit it to write standalone via the * `postgresClient` singleton. */ export async function writeAuditEvent(input: AuditEventInput, client?: AuditQueryClient): Promise { const target = client ?? postgresClient; const result = await target.query( `INSERT INTO audit_events (campaign_id, actor, event_type, payload) VALUES ($1, $2, $3, $4::jsonb) RETURNING id::text AS id`, [input.campaignId, input.actor, input.eventType, JSON.stringify(input.payload)] ); return result.rows[0].id; }