/** Client-side humanizers for audit codes, user agents and dates. No secrets, no side effects. */ const ACTION_LABELS: Record = { login: "Signed in", logout: "Signed out", "account.created": "Account created", "account.deleted": "Account deleted", "account.export": "Account exported", "email.verified": "Email verified", "email.change_requested": "Email change requested", "email.changed": "Email changed", "password.reset_requested": "Password reset requested", "password.reset": "Password reset", "password.changed": "Password changed", "session.revoked": "Session revoked", "sessions.revoked": "All sessions revoked", "provider.key_added": "API key added", "provider.key_replaced": "API key replaced", "provider.key_removed": "API key removed", "provider.key_validated": "API key validated", "provider.key_invalid": "API key rejected", "onboarding.completed": "Onboarding completed", }; export function humanizeAction(code: string): string { if (ACTION_LABELS[code]) return ACTION_LABELS[code]; // "provider.key_added" → "Provider key added" const words = code.replace(/[._-]+/g, " ").trim(); return words.charAt(0).toUpperCase() + words.slice(1); } export function actionTone(code: string): "default" | "success" | "warning" | "danger" | "info" { if (/deleted|invalid|revoked|rejected/.test(code)) return "danger"; if (/reset|change_requested/.test(code)) return "warning"; if (/added|validated|verified|created|changed/.test(code)) return "success"; if (/login|export/.test(code)) return "info"; return "default"; } export interface UaSummary { browser: string; os: string; device: "desktop" | "mobile" | "tablet" | "unknown"; label: string; } /** Best-effort user-agent summary — good enough for a sessions list, never authoritative. */ export function describeUserAgent(ua: string | null | undefined): UaSummary { if (!ua) return { browser: "Unknown browser", os: "Unknown OS", device: "unknown", label: "Unknown device" }; const s = ua; let browser = "Browser"; if (/Edg\//.test(s)) browser = "Edge"; else if (/OPR\/|Opera/.test(s)) browser = "Opera"; else if (/Arc\//.test(s)) browser = "Arc"; else if (/Firefox\//.test(s)) browser = "Firefox"; else if (/Chrome\//.test(s) && !/Chromium/.test(s)) browser = "Chrome"; else if (/Chromium/.test(s)) browser = "Chromium"; else if (/Safari\//.test(s) && /Version\//.test(s)) browser = "Safari"; else if (/curl|node|python|okhttp|Go-http/i.test(s)) browser = "API client"; let os = "Unknown OS"; if (/iPhone|iPad|iPod/.test(s)) os = "iOS"; else if (/Android/.test(s)) os = "Android"; else if (/Mac OS X|Macintosh/.test(s)) os = "macOS"; else if (/Windows/.test(s)) os = "Windows"; else if (/CrOS/.test(s)) os = "ChromeOS"; else if (/Linux/.test(s)) os = "Linux"; let device: UaSummary["device"] = "desktop"; if (/iPad|Tablet/.test(s)) device = "tablet"; else if (/Mobi|iPhone|Android.*Mobile/.test(s)) device = "mobile"; return { browser, os, device, label: `${browser} on ${os}` }; } export function formatDateTime(date: string | Date | null | undefined, opts: Intl.DateTimeFormatOptions = {}): string { if (!date) return "—"; const d = typeof date === "string" ? new Date(date) : date; if (Number.isNaN(d.getTime())) return "—"; return d.toLocaleString("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit", ...opts }); } export function formatDate(date: string | Date | null | undefined): string { if (!date) return "—"; const d = typeof date === "string" ? new Date(date) : date; if (Number.isNaN(d.getTime())) return "—"; return d.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }); } /** formatRelative that also accepts Date objects returned by the auth client. */ export function formatRelativeSafe(date: string | Date | null | undefined): string { if (!date) return "—"; const d = typeof date === "string" ? new Date(date) : date; if (Number.isNaN(d.getTime())) return "—"; const s = Math.round((Date.now() - d.getTime()) / 1000); if (s < 45) return "just now"; const m = Math.round(s / 60); if (m < 60) return `${m} min ago`; const h = Math.round(m / 60); if (h < 24) return `${h} h ago`; const days = Math.round(h / 24); if (days < 7) return `${days} d ago`; return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); } export function formatPercent(n: number | null | undefined, digits = 1): string { if (n === null || n === undefined || Number.isNaN(n)) return "—"; return `${(n * 100).toFixed(digits)}%`; } /** Turn an API error (ClientApiError, Error, Better Auth {error}) into a short user-facing string. */ export function errorMessage(e: unknown, fallback = "Something went wrong"): string { if (!e) return fallback; if (typeof e === "string") return e; if (typeof e === "object") { const o = e as { message?: unknown; error?: { message?: unknown } }; if (typeof o.message === "string" && o.message) return o.message; if (o.error && typeof o.error.message === "string") return o.error.message; } return fallback; }