SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
5.1 KB · 122 lines typescript
Raw Blame History
1/** Client-side humanizers for audit codes, user agents and dates. No secrets, no side effects. */23const ACTION_LABELS: Record<string, string> = {4  login: "Signed in",5  logout: "Signed out",6  "account.created": "Account created",7  "account.deleted": "Account deleted",8  "account.export": "Account exported",9  "email.verified": "Email verified",10  "email.change_requested": "Email change requested",11  "email.changed": "Email changed",12  "password.reset_requested": "Password reset requested",13  "password.reset": "Password reset",14  "password.changed": "Password changed",15  "session.revoked": "Session revoked",16  "sessions.revoked": "All sessions revoked",17  "provider.key_added": "API key added",18  "provider.key_replaced": "API key replaced",19  "provider.key_removed": "API key removed",20  "provider.key_validated": "API key validated",21  "provider.key_invalid": "API key rejected",22  "onboarding.completed": "Onboarding completed",23};2425export function humanizeAction(code: string): string {26  if (ACTION_LABELS[code]) return ACTION_LABELS[code];27  // "provider.key_added" → "Provider key added"28  const words = code.replace(/[._-]+/g, " ").trim();29  return words.charAt(0).toUpperCase() + words.slice(1);30}3132export function actionTone(code: string): "default" | "success" | "warning" | "danger" | "info" {33  if (/deleted|invalid|revoked|rejected/.test(code)) return "danger";34  if (/reset|change_requested/.test(code)) return "warning";35  if (/added|validated|verified|created|changed/.test(code)) return "success";36  if (/login|export/.test(code)) return "info";37  return "default";38}3940export interface UaSummary {41  browser: string;42  os: string;43  device: "desktop" | "mobile" | "tablet" | "unknown";44  label: string;45}4647/** Best-effort user-agent summary — good enough for a sessions list, never authoritative. */48export function describeUserAgent(ua: string | null | undefined): UaSummary {49  if (!ua) return { browser: "Unknown browser", os: "Unknown OS", device: "unknown", label: "Unknown device" };50  const s = ua;51  let browser = "Browser";52  if (/Edg\//.test(s)) browser = "Edge";53  else if (/OPR\/|Opera/.test(s)) browser = "Opera";54  else if (/Arc\//.test(s)) browser = "Arc";55  else if (/Firefox\//.test(s)) browser = "Firefox";56  else if (/Chrome\//.test(s) && !/Chromium/.test(s)) browser = "Chrome";57  else if (/Chromium/.test(s)) browser = "Chromium";58  else if (/Safari\//.test(s) && /Version\//.test(s)) browser = "Safari";59  else if (/curl|node|python|okhttp|Go-http/i.test(s)) browser = "API client";6061  let os = "Unknown OS";62  if (/iPhone|iPad|iPod/.test(s)) os = "iOS";63  else if (/Android/.test(s)) os = "Android";64  else if (/Mac OS X|Macintosh/.test(s)) os = "macOS";65  else if (/Windows/.test(s)) os = "Windows";66  else if (/CrOS/.test(s)) os = "ChromeOS";67  else if (/Linux/.test(s)) os = "Linux";6869  let device: UaSummary["device"] = "desktop";70  if (/iPad|Tablet/.test(s)) device = "tablet";71  else if (/Mobi|iPhone|Android.*Mobile/.test(s)) device = "mobile";7273  return { browser, os, device, label: `${browser} on ${os}` };74}7576export function formatDateTime(date: string | Date | null | undefined, opts: Intl.DateTimeFormatOptions = {}): string {77  if (!date) return "—";78  const d = typeof date === "string" ? new Date(date) : date;79  if (Number.isNaN(d.getTime())) return "—";80  return d.toLocaleString("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit", ...opts });81}8283export function formatDate(date: string | Date | null | undefined): string {84  if (!date) return "—";85  const d = typeof date === "string" ? new Date(date) : date;86  if (Number.isNaN(d.getTime())) return "—";87  return d.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" });88}8990/** formatRelative that also accepts Date objects returned by the auth client. */91export function formatRelativeSafe(date: string | Date | null | undefined): string {92  if (!date) return "—";93  const d = typeof date === "string" ? new Date(date) : date;94  if (Number.isNaN(d.getTime())) return "—";95  const s = Math.round((Date.now() - d.getTime()) / 1000);96  if (s < 45) return "just now";97  const m = Math.round(s / 60);98  if (m < 60) return `${m} min ago`;99  const h = Math.round(m / 60);100  if (h < 24) return `${h} h ago`;101  const days = Math.round(h / 24);102  if (days < 7) return `${days} d ago`;103  return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });104}105106export function formatPercent(n: number | null | undefined, digits = 1): string {107  if (n === null || n === undefined || Number.isNaN(n)) return "—";108  return `${(n * 100).toFixed(digits)}%`;109}110111/** Turn an API error (ClientApiError, Error, Better Auth {error}) into a short user-facing string. */112export function errorMessage(e: unknown, fallback = "Something went wrong"): string {113  if (!e) return fallback;114  if (typeof e === "string") return e;115  if (typeof e === "object") {116    const o = e as { message?: unknown; error?: { message?: unknown } };117    if (typeof o.message === "string" && o.message) return o.message;118    if (o.error && typeof o.error.message === "string") return o.error.message;119  }120  return fallback;121}122