Repository Public MIT
1import { actorLabel, userOf, type Ctx } from "./context";23export interface AuditInput {4 action: string;5 targetType?: string;6 targetId?: string;7 targetLabel?: string;8 result?: "success" | "denied" | "error";9 detail?: Record<string, unknown>;10}1112const SECRET_KEY = /pass|secret|token|hash|authorization|cookie|pepper|csrf/i;1314/** Defence in depth: values under secret-looking keys never reach the audit table. */15export function redactDetail(v: unknown, depth = 0): unknown {16 if (depth > 6) return "[truncated]";17 if (Array.isArray(v)) return v.slice(0, 100).map((x) => redactDetail(x, depth + 1));18 if (v && typeof v === "object") {19 const out: Record<string, unknown> = {};20 for (const [k, val] of Object.entries(v)) out[k] = SECRET_KEY.test(k) ? "[redacted]" : redactDetail(val, depth + 1);21 return out;22 }23 if (typeof v === "string") {24 const s = v.replace(/skr_[a-z0-9]{10}_[A-Za-z0-9_-]{43}/g, "skr_[redacted]");25 return s.length > 2000 ? s.slice(0, 2000) + "…" : s;26 }27 return v;28}2930export async function audit(ctx: Ctx, a: AuditInput): Promise<void> {31 const p = ctx.principal;32 await ctx.sql`33 INSERT INTO audit_events (actor_user_id, actor_token_id, actor_label, action, target_type, target_id, target_label, result, ip, user_agent, request_id, detail)34 VALUES (${userOf(p)?.id ?? null}, ${p.kind === "token" ? p.token.id : null}, ${actorLabel(p)}, ${a.action}, ${a.targetType ?? null},35 ${a.targetId ?? null}, ${a.targetLabel ?? null}, ${a.result ?? "success"}, ${ctx.ip}, ${ctx.userAgent.slice(0, 300)}, ${ctx.requestId},36 ${ctx.sql.json(redactDetail(a.detail ?? {}) as never)})`;37}38