wulf-pulse/lib/services/phishing-audit.ts
lorentz 98d3e925e5 feat(20-01): add audit-event writer (writeAuditEvent)
- Single parameterized append-only INSERT into audit_events
- Supports optional injected transaction client for atomic writes
- Documents the four canonical event_type strings for this phase
2026-07-16 10:35:35 -04:00

55 lines
2.3 KiB
TypeScript

/**
* 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<string, unknown>;
}
/**
* 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<string> {
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;
}