"use client"; /** * Client-side auth state. * * - The **access token** lives in memory only (never localStorage) — it is a * short-lived JWT and memory storage keeps it out of XSS-extractable * storage (security-architecture.md §AuthN). On load it is re-issued from * the httpOnly refresh cookie via POST /auth/refresh (silent refresh), and * the user profile is re-fetched with GET /auth/me (Bearer). * - The **refresh token** is the httpOnly `factory_refresh` cookie set by the * API (never readable from JS); /auth/refresh rotates it server-side and * re-sets the cookie (the API must persist the rotation — see S10). * - `hasSession()` is the in-memory token check (the cookie is HttpOnly and * unreachable from JS — the old `document.cookie` read could only ever * match the wrong cookie name and was always false, W2). The middleware is * the authoritative redirect; this gates first-mount render + the * silent-refresh query. */ import { create } from "zustand"; import type { User } from "./types"; interface AuthState { user: User | null; accessToken: string | null; /** True while /auth/me is resolving on first mount. */ initializing: boolean; setUser: (user: User | null, accessToken?: string | null) => void; setAccessToken: (token: string | null) => void; setInitializing: (v: boolean) => void; clear: () => void; } export const useAuthStore = create()((set) => ({ user: null, accessToken: null, initializing: true, setUser: (user, accessToken = null) => set({ user, accessToken, initializing: false }), setAccessToken: (accessToken) => set({ accessToken }), setInitializing: (initializing) => set({ initializing }), // Logout is an explicit sign-out: also end any in-flight initialization so // the (app) layout can't keep spinning on a stale `initializing: true` if // logout fires before the bootstrap settled. clear: () => set({ user: null, accessToken: null, initializing: false }) })); /** * Hydrated session present? The access token is the only session surface JS * can see (the refresh cookie is HttpOnly). False during SSR — the store is * empty on the server by construction, and `use-auth.ts` enables the * silent-refresh query on client mount instead. */ export function hasSession(): boolean { return Boolean(useAuthStore.getState().accessToken); } /** Sync helpers for the api client (non-hook access from plain modules). */ export const authStore = { get user(): User | null { return useAuthStore.getState().user; }, get accessToken(): string | null { return useAuthStore.getState().accessToken; }, setUser(user: User | null, accessToken: string | null = null): void { useAuthStore.getState().setUser(user, accessToken); }, setAccessToken(token: string | null): void { useAuthStore.getState().setAccessToken(token); }, setInitializing(v: boolean): void { useAuthStore.getState().setInitializing(v); }, clear(): void { useAuthStore.getState().clear(); } };