/** * Audit write path — A-8. The `AuditEntry` table existed but had no writer; * callers (trigger path, permission denials, slash commands) had no way to * record what happened. This module is that single write seam. * * ADR reference: spec Audit. The skeleton pins only the `runId` relation; * event type / actor / timestamp / details are content-OPEN. This helper * exposes `action` (event type) + `actorUserId` (actor) + `metadata` * (details) + `projectId` (locates audit points that don't have a run, e.g. * permission denials) + `runId` (loose string ref, not a Prisma relation — * see schema). `createdAt` is `@default(now())` so we don't pass it. * * Best-effort by design: an audit failure must never break the caller's path * (a trigger failing because the audit log is down would be a spec breach — * audit is observability, not control). Errors are swallowed here. There's no * logger in this layer yet; a future logger can be threaded in without * changing the signature. */ import type { PrismaClient, Prisma } from "@prisma/client"; export interface AuditInput { readonly runId?: string | undefined; readonly projectId?: string | undefined; readonly actorUserId?: string | undefined; readonly action: string; readonly metadata: Record; } /** * Write one audit entry. Best-effort: any Prisma error is swallowed so the * caller's path (trigger, permission gate, slash command) is unaffected. * * Optional fields are only included when defined — under * `exactOptionalPropertyTypes`, assigning `undefined` to an optional key in a * Prisma `create` payload is a type error (and would also write an explicit * NULL rather than omit the column). Conditional assignment keeps the payload * honest. The data object is typed as `AuditEntryUncheckedCreateInput` (the * direct-FK variant) so it matches one branch of Prisma's `create` input * union exactly — the relation-connect branch has no `projectId`/`runId` * columns and would otherwise reject these fields. */ export async function writeAudit( prisma: PrismaClient, entry: AuditInput, ): Promise { const data: Prisma.AuditEntryUncheckedCreateInput = { action: entry.action, metadata: entry.metadata as Prisma.InputJsonValue, }; if (entry.runId !== undefined) data.runId = entry.runId; if (entry.projectId !== undefined) data.projectId = entry.projectId; if (entry.actorUserId !== undefined) data.actorUserId = entry.actorUserId; try { await prisma.auditEntry.create({ data }); } catch { // Swallow: audit is best-effort and must not break the caller's path. } }