interface DngAuthUser { id: string email?: string | null is_anonymous?: boolean user_metadata?: Record } interface DngSession { accessToken: string refreshToken: string expiresAt: number user: DngAuthUser } interface AuthResponse { access_token: string refresh_token: string expires_in: number user: DngAuthUser } const STORAGE_KEY = 'dng-auth-session' export function useDngAuth() { const config = useRuntimeConfig() const session = useState('dng-auth-session', () => null) const hydrated = useState('dng-auth-hydrated', () => false) function authHeaders(accessToken?: string) { return { apikey: config.public.supabaseAnonKey, 'Content-Type': 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), } } function persist(next: DngSession | null) { session.value = next if (!import.meta.client) return if (next) localStorage.setItem(STORAGE_KEY, JSON.stringify(next)) else localStorage.removeItem(STORAGE_KEY) } function normalizeUser(user: DngAuthUser): DngAuthUser { return { ...user, email: user.email || undefined, } } function parseStoredSession(value: string): DngSession | null { try { const saved = JSON.parse(value) as Partial if ( typeof saved.accessToken !== 'string' || !saved.accessToken || typeof saved.refreshToken !== 'string' || !saved.refreshToken || typeof saved.expiresAt !== 'number' || !Number.isFinite(saved.expiresAt) || !saved.user || typeof saved.user.id !== 'string' || !saved.user.id ) return null return { ...saved, user: normalizeUser(saved.user) } as DngSession } catch { return null } } async function fetchUser(accessToken: string) { const user = await $fetch(`${config.public.supabaseUrl}/auth/v1/user`, { headers: authHeaders(accessToken), }) return normalizeUser(user) } function fromResponse(response: AuthResponse): DngSession { return { accessToken: response.access_token, refreshToken: response.refresh_token, expiresAt: Date.now() + response.expires_in * 1000, user: normalizeUser(response.user), } } async function refresh() { if (!session.value?.refreshToken) return null try { const response = await $fetch(`${config.public.supabaseUrl}/auth/v1/token?grant_type=refresh_token`, { method: 'POST', headers: authHeaders(), body: { refresh_token: session.value.refreshToken }, }) const next = fromResponse(response) persist(next) return next } catch { persist(null) return null } } async function restore() { if (!import.meta.client || hydrated.value) return session.value let restoredFromStorage = false const hash = new URLSearchParams(window.location.hash.replace(/^#/, '')) const hashAccessToken = hash.get('access_token') const hashRefreshToken = hash.get('refresh_token') if (hashAccessToken && hashRefreshToken) { const expiresIn = Number(hash.get('expires_in') ?? 3600) const user = await fetchUser(hashAccessToken) persist({ accessToken: hashAccessToken, refreshToken: hashRefreshToken, expiresAt: Date.now() + expiresIn * 1000, user }) history.replaceState(null, '', `${window.location.pathname}${window.location.search}`) } else { const saved = localStorage.getItem(STORAGE_KEY) if (saved) { const parsed = parseStoredSession(saved) if (parsed) { session.value = parsed restoredFromStorage = true } else { persist(null) } } } if (session.value && session.value.expiresAt <= Date.now() + 60_000) { await refresh() } else if (session.value && restoredFromStorage) { try { const user = await fetchUser(session.value.accessToken) persist({ ...session.value, user }) } catch { // A saved token may belong to an old Supabase project or have been // revoked before its local expiry. Refresh once, then discard it. await refresh() } } hydrated.value = true return session.value } async function requestMagicLink(email: string) { const redirectTo = `${window.location.origin}/auth/callback` await $fetch(`${config.public.supabaseUrl}/auth/v1/otp?redirect_to=${encodeURIComponent(redirectTo)}`, { method: 'POST', headers: authHeaders(), body: { email: email.trim().toLowerCase(), create_user: true }, }) } async function signInAnonymously() { const response = await $fetch(`${config.public.supabaseUrl}/auth/v1/signup`, { method: 'POST', headers: authHeaders(), body: { data: { display_name: 'Guest Adventurer' }, gotrue_meta_security: {}, }, }) const next = fromResponse(response) persist(next) hydrated.value = true return next } async function accessToken() { await restore() if (!session.value) throw new Error('Enter the alpha to continue.') if (session.value.expiresAt <= Date.now() + 60_000) await refresh() if (!session.value) throw new Error('Your session expired. Sign in again.') return session.value.accessToken } async function signOut() { const token = session.value?.accessToken persist(null) if (!token) return await $fetch(`${config.public.supabaseUrl}/auth/v1/logout`, { method: 'POST', headers: authHeaders(token), }).catch(() => undefined) } function invalidate() { persist(null) hydrated.value = true } return { session, hydrated, restore, refresh, requestMagicLink, signInAnonymously, accessToken, signOut, invalidate } }