auth.ts

PURPOSE

Authentication service that wraps Supabase Auth. Manages the three account states (guest_anonymous, claimed_email, signed_out) and the magic-link claim flow that upgrades an anonymous identity into a permanent email-linked account.

OWNS

  • The cold-start session contract: ensureAnonymousSession() either restores an existing session or creates an anonymous user.
  • Magic-link redirect detection for both PKCE (?code= query) and implicit (#access_token= hash) flows.
  • The hard timeout that prevents the splash from hanging on an unreachable Supabase backend (AUTH_TIMEOUT_MS = 5000).
  • URL cleanup after magic-link tokens are consumed (window.history.replaceState).
  • Subscription/unsubscription bookkeeping for the auth-state listener.

READS FROM

  • ./supabase — the Supabase client singleton.
  • @supabase/supabase-jsUser type.
  • window.location.search / window.location.hash — magic-link redirect parameters.
  • window.location.origin / window.location.pathname — redirect target and URL cleanup.
  • Supabase session state via supabase.auth.getSession() and supabase.auth.getUser().

PUSHES TO

  • Supabase Auth: signInAnonymously, signInWithOtp, signOut, refreshSession, onAuthStateChange.
  • window.history — clears the magic-link token from the URL after consumption.
  • Callers registered via onAuthChange — fired on every Supabase auth state change with the resolved User | null.

DOES NOT

  • Persist user profile, display name, or any non-Supabase identity data.
  • Talk to the database directly — only Supabase Auth endpoints.
  • Render any UI; surfaces errors by throwing.
  • Retry on failure; a Supabase timeout aborts with SUPABASE_TIMEOUT: auth unreachable.
  • Track local state — every call asks Supabase fresh.
  • Handle offline mode — the caller is expected to catch the timeout and offer the offline escape hatch.
  • Validate emails before sending magic links.

Signals

  • Throws Error('SUPABASE_TIMEOUT: auth unreachable') from ensureAnonymousSession if Supabase does not respond within AUTH_TIMEOUT_MS.
  • Throws Error('Anonymous sign-in returned no user') if Supabase reports success but returns no user object.
  • Re-throws any error returned by Supabase Auth from signInAnonymously, signOut, sendMagicLink, refreshSession, getSession.
  • onAuthChange callbacks receive User | null on SIGNED_IN, TOKEN_REFRESHED, SIGNED_OUT, and other Supabase auth state transitions.
  • _waitForMagicLinkSession resolves with the authenticated user on SIGNED_IN or TOKEN_REFRESHED, or null after an 8s internal timeout.

Entry points

  • ensureAnonymousSession(): Promise<User> — call once on cold start; restores or creates a session, races against a 5s timeout.
  • signInAnonymously(): Promise<void> — explicit anonymous sign-in.
  • signOut(): Promise<void> — clears the session.
  • getUser(): Promise<User | null> — current user, or null if signed out.
  • sendMagicLink(email: string): Promise<void> — initiates email claim flow; passes shouldCreateUser: true and an explicit emailRedirectTo: window.location.origin.
  • refreshSession(): Promise<User | null> — refresh after magic-link redirect to pick up upgraded identity.
  • getSession(): Promise<Session | null> — current session including tokens.
  • onAuthChange(callback): () => void — subscribe to auth state changes; returns an unsubscribe function.

Pattern notes

  • ensureAnonymousSession uses Promise.race against an AUTH_TIMEOUT_MS (5000) timeout so the splash never freezes on a dead Supabase instance.
  • Magic-link handling is bifurcated: PKCE flow lands with ?code= in window.location.search; older implicit flow lands with access_token / refresh_token in window.location.hash. Either pattern triggers _waitForMagicLinkSession before falling back to getSession.
  • _waitForMagicLinkSession waits on onAuthStateChange for SIGNED_IN or TOKEN_REFRESHED, with an 8s safety timeout that resolves to null rather than hanging app boot.
  • After a successful magic-link consumption, window.history.replaceState(null, '', window.location.pathname) strips the tokens from the URL so a refresh does not re-trigger the flow.
  • sendMagicLink sets shouldCreateUser: true so an anonymous user clicking the link upgrades their existing identity rather than creating a fresh account.
  • emailRedirectTo is set explicitly to window.location.origin to avoid Supabase defaulting to localhost when the Site URL is misconfigured.
  • All exported async functions either return the requested value or throw — no silent-null fallbacks except getUser and refreshSession, which return null to represent the signed-out state.
  • onAuthChange returns a cleanup function that defensively reads data?.subscription?.unsubscribe, tolerating a missing subscription on unexpected Supabase responses.