// Tiny JWT helpers — we never *verify* tokens client-side (the server // is the only authority), we just decode the payload to read claims // the UI uses for nav gating + tenant context. export type ArcadiaClaims = { sub?: string email?: string tenant_id?: string tenant_slug?: string roles?: string[] available_tenants?: AvailableTenantClaim[] exp?: number iat?: number [k: string]: unknown } export type AvailableTenantClaim = { id?: string slug?: string name?: string roles?: string[] } function b64urlDecode(s: string): string { const pad = "=".repeat((4 - (s.length % 4)) % 4) const b64 = (s + pad).replace(/-/g, "+").replace(/_/g, "/") if (typeof atob === "function") return atob(b64) // Node fallback (SSR / tests) return Buffer.from(b64, "base64").toString("binary") } export function decodeJwt(token: string): ArcadiaClaims | null { if (!token) return null const parts = token.split(".") if (parts.length !== 3) return null try { const raw = b64urlDecode(parts[1]) // Handle UTF-8: atob returns binary string; reconstruct UTF-8. const utf8 = typeof TextDecoder !== "undefined" ? new TextDecoder().decode( Uint8Array.from(raw, (c) => c.charCodeAt(0)), ) : raw return JSON.parse(utf8) as ArcadiaClaims } catch { return null } }