import type { db as defaultDb } from "@tasks/database"; import { auditLog } from "@tasks/database/schema"; /** * Append a single audit-log row. Every mutation that wants accountability * (workspace state changes, member changes, invite lifecycle, object * mutations once we instrument them) calls this once on success. * * Invariants: * - Never fail silently — if the insert throws, let it propagate. An * audit miss is data-loss; the caller's transaction should roll back * too if it can. * - Keep `metadata` SMALL. Caller is responsible for picking the right * handful of fields, not dumping the whole row. The convention is * "what would a support engineer want to see at a glance?" * - `actorUserId === null` is reserved for genuine system actors * (markdown importer, scheduled jobs). Caller must also stamp * `metadata.system_actor` with a short identifier so the audit UI * can render "system: markdown-importer" instead of an empty cell. */ export type RecordAuditInput = { workspaceId: string; /** null = system actor. Pair with `metadata.system_actor` when null. */ actorUserId: string | null; /** `.` — e.g. `workspace.archive`, `invite.create`. */ action: string; /** e.g. `workspace`, `invite`, `workspace_member`, `object`. */ targetType: string; /** Natural id of the affected row, or null for batch / cascade actions. */ targetId?: string | null; metadata?: Record | null; }; export async function recordAudit( db: typeof defaultDb, input: RecordAuditInput, ): Promise { // Light validation. Procedure-layer zod schemas should already enforce // these, but the audit table is the last stop and we want garbage rows // to fail loudly rather than silently distort the trail. if (!input.workspaceId) { throw new Error("recordAudit: workspaceId is required"); } if (!input.action || !input.action.includes(".")) { throw new Error( `recordAudit: action must follow ".", got: ${input.action}`, ); } if (input.actorUserId === null && !input.metadata?.system_actor) { throw new Error( "recordAudit: actorUserId is null but metadata.system_actor is missing — name the system actor explicitly", ); } await db.insert(auditLog).values({ workspaceId: input.workspaceId, actorUserId: input.actorUserId, action: input.action, targetType: input.targetType, targetId: input.targetId ?? null, metadata: input.metadata ?? null, }); }