/** * Phase 09 Task #82 — Thin Audit-Event wrapper for admin-sso paths. * * Centralises action names + metadata sanitization for the login / logout * / fallback / mfa-challenge paths. Sits on top of * `@toolctrl/db/audit-events.server` which already handles redaction. */ import { createAuditEvent } from "@toolctrl/db/audit-events.server"; import type { Prisma } from "@toolctrl/db/prisma-browser"; export type AdminSsoAuditAction = | "admin.login.sso.success" | "admin.login.sso.failure" | "admin.login.fallback.success" | "admin.login.fallback.failure" | "admin.logout.sso" | "admin.mfa.app.challenge.success" | "admin.mfa.app.challenge.failure"; export type AdminSsoAuditInput = { readonly action: AdminSsoAuditAction; readonly actorAdminUserId?: string | null; readonly ssoSubjectId?: string | null; readonly request?: Request; readonly meta?: Record; }; function clientMeta(request: Request | undefined): { ip: string | null; userAgent: string | null } { if (!request) return { ip: null, userAgent: null }; const fwd = request.headers.get("x-forwarded-for") ?? ""; const ip = fwd.split(",")[0]?.trim() || request.headers.get("x-real-ip") || null; const userAgent = request.headers.get("user-agent") || null; return { ip, userAgent }; } export async function writeAdminSsoAuditEvent(input: AdminSsoAuditInput): Promise { const { ip, userAgent } = clientMeta(input.request); try { await createAuditEvent({ action: input.action, actorType: "admin", actorId: input.actorAdminUserId ?? null, metadata: { ssoSubjectId: input.ssoSubjectId ?? null, ip, userAgent, ...input.meta, } as unknown as Prisma.InputJsonValue, }); } catch { // Audit failures must never block the auth flow. The DB-level error // already lands in Sentry via the audit-events boundary. } }