From 72243afdf9ab8f33413e4b1e0ffad586f9875699 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 10:42:45 +0000 Subject: [PATCH 1/8] Reduce userinfo endpoint calls by tightening refresh cadence useVerifiedEmail forced refetchOnWindowFocus: "always" on the refresh-user-profile query, which bypasses staleTime and refetched userinfo on every focus event from any component using useAuth. Switch to refetchOnWindowFocus: true so it respects staleTime, and set explicit staleTime / gcTime on the query so refetches don't fire on every component mount or in environments with their own QueryClient. https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT --- packages/zudoku/src/lib/authentication/hook.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/zudoku/src/lib/authentication/hook.ts b/packages/zudoku/src/lib/authentication/hook.ts index 6b3280140..189e78212 100644 --- a/packages/zudoku/src/lib/authentication/hook.ts +++ b/packages/zudoku/src/lib/authentication/hook.ts @@ -21,6 +21,8 @@ export const useRefreshUserProfile = ({ return useQuery({ refetchOnWindowFocus, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 10, queryKey: ["refresh-user-profile"], enabled: isAuthEnabled && typeof authentication?.refreshUserProfile === "function", @@ -35,7 +37,7 @@ export const useVerifiedEmail = () => { const isAuthEnabled = typeof authentication !== "undefined"; const { refetch: refreshUserProfile } = useRefreshUserProfile({ - refetchOnWindowFocus: "always", + refetchOnWindowFocus: true, }); return { From c2245e35246ee87e87ef8386ca1bbbc4389be616 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 11:24:27 +0000 Subject: [PATCH 2/8] Deduplicate concurrent OpenID discovery requests getAuthServer cached the resolved AuthorizationServer, but the check- then-await-then-assign pattern raced: any caller arriving between the discoveryRequest await and the assignment also saw the field as undefined and fired its own request. On a cold load, onPageLoad and the first userinfo refresh run concurrently and both hit the discovery endpoint. Cache the in-flight promise instead so concurrent callers share a single discovery request. https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT --- .../src/lib/authentication/providers/openid.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/zudoku/src/lib/authentication/providers/openid.tsx b/packages/zudoku/src/lib/authentication/providers/openid.tsx index 5877189d2..5a63d2c51 100644 --- a/packages/zudoku/src/lib/authentication/providers/openid.tsx +++ b/packages/zudoku/src/lib/authentication/providers/openid.tsx @@ -46,7 +46,7 @@ export class OpenIDAuthenticationProvider { protected client: oauth.Client; protected issuer: string; - protected authorizationServer: oauth.AuthorizationServer | undefined; + protected authorizationServer: Promise | undefined; protected callbackUrlPath: string; @@ -105,14 +105,11 @@ export class OpenIDAuthenticationProvider } protected async getAuthServer() { - if (!this.authorizationServer) { + this.authorizationServer ??= (async () => { const issuerUrl = new URL(this.issuer); const response = await oauth.discoveryRequest(issuerUrl); - this.authorizationServer = await oauth.processDiscoveryResponse( - issuerUrl, - response, - ); - } + return oauth.processDiscoveryResponse(issuerUrl, response); + })(); return this.authorizationServer; } From 09298314b43f973303e2416b020c505c5975872f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 14:46:16 +0000 Subject: [PATCH 3/8] Don't cache failed OpenID discovery promises MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous dedup change cached the in-flight promise on authorizationServer, but a rejection (e.g. transient network error) would stick around forever — every subsequent caller would await the same rejected promise and never retry. Clear the field on failure so the next caller starts a fresh request. https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT --- .../src/lib/authentication/providers/openid.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/zudoku/src/lib/authentication/providers/openid.tsx b/packages/zudoku/src/lib/authentication/providers/openid.tsx index 5a63d2c51..5f935d49c 100644 --- a/packages/zudoku/src/lib/authentication/providers/openid.tsx +++ b/packages/zudoku/src/lib/authentication/providers/openid.tsx @@ -106,9 +106,14 @@ export class OpenIDAuthenticationProvider protected async getAuthServer() { this.authorizationServer ??= (async () => { - const issuerUrl = new URL(this.issuer); - const response = await oauth.discoveryRequest(issuerUrl); - return oauth.processDiscoveryResponse(issuerUrl, response); + try { + const issuerUrl = new URL(this.issuer); + const response = await oauth.discoveryRequest(issuerUrl); + return await oauth.processDiscoveryResponse(issuerUrl, response); + } catch (err) { + this.authorizationServer = undefined; + throw err; + } })(); return this.authorizationServer; } From 831e1d546d74b457503018928e8a1398f699acd8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 14:51:54 +0000 Subject: [PATCH 4/8] Add regression tests for OpenID discovery caching Cover the two cache invariants: - A failed discovery request must not poison the cache; the next caller should retry. - Concurrent callers should share a single in-flight discovery request. https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT --- .../authentication/providers/openid.test.ts | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/packages/zudoku/src/lib/authentication/providers/openid.test.ts b/packages/zudoku/src/lib/authentication/providers/openid.test.ts index e63fcc53b..b9b7558b5 100644 --- a/packages/zudoku/src/lib/authentication/providers/openid.test.ts +++ b/packages/zudoku/src/lib/authentication/providers/openid.test.ts @@ -471,6 +471,67 @@ describe("OpenIDAuthenticationProvider emailVerified", () => { }); }); + describe("discovery caching", () => { + const setupAuthenticated = () => { + useAuthState.setState({ + isAuthenticated: true, + isPending: false, + profile: { + sub: "user-1", + email: "user@example.com", + emailVerified: false, + name: "Test", + pictureUrl: undefined, + }, + providerData: { + type: "openid", + accessToken: FAKE_ACCESS_TOKEN, + expiresOn: new Date(Date.now() + 3600_000), + tokenType: "bearer", + claims: undefined, + } satisfies OpenIdProviderData, + }); + + vi.mocked(oauth.userInfoRequest).mockImplementation(() => + Promise.resolve( + Response.json({ sub: "user-1", email: "user@example.com" }), + ), + ); + }; + + test("retries discovery after a failed request", async () => { + vi.mocked(oauth.discoveryRequest) + .mockReset() + .mockRejectedValueOnce(new Error("network down")) + .mockImplementation(() => Promise.resolve(new Response())); + + const provider = createProvider(); + setupAuthenticated(); + + await expect(provider.refreshUserProfile()).rejects.toThrow( + "network down", + ); + await expect(provider.refreshUserProfile()).resolves.toBe(true); + + expect(oauth.discoveryRequest).toHaveBeenCalledTimes(2); + }); + + test("deduplicates concurrent discovery requests", async () => { + vi.mocked(oauth.discoveryRequest).mockClear(); + + const provider = createProvider(); + setupAuthenticated(); + + await Promise.all([ + provider.refreshUserProfile(), + provider.refreshUserProfile(), + provider.refreshUserProfile(), + ]); + + expect(oauth.discoveryRequest).toHaveBeenCalledTimes(1); + }); + }); + test("self heals providerData when providerData.type is undefined", async () => { const provider = createProvider(); From 87dd321614ea4a29048ceb83490d275ea6f4af72 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 4 May 2026 12:54:32 +0000 Subject: [PATCH 5/8] Skip userinfo fetch on page reload via persisted profileFetchedAt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous staleTime/gcTime change only deduped fetches within a single React Query cache lifetime. Every fresh page load instantiates a new QueryClient with no cache, so userinfo was still fetched on every reload — even though the persisted Zustand profile already held the latest data. Add profileFetchedAt to the persisted auth state and pass it as initialDataUpdatedAt to the user profile query. React Query then evaluates staleTime against that timestamp on mount, so a reload within staleTime skips the fetch entirely. The auth providers that populate profile (openid, clerk, firebase, plus setLoggedIn for firebase/supabase/azureb2c) all stamp the timestamp; logout clears it alongside the profile. https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT --- packages/zudoku/src/lib/authentication/hook.ts | 10 +++++++++- .../zudoku/src/lib/authentication/providers/clerk.tsx | 1 + .../src/lib/authentication/providers/firebase.tsx | 1 + .../zudoku/src/lib/authentication/providers/openid.tsx | 3 +++ packages/zudoku/src/lib/authentication/state.ts | 5 +++++ packages/zudoku/src/lib/core/RouteGuard.test.tsx | 1 + 6 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/zudoku/src/lib/authentication/hook.ts b/packages/zudoku/src/lib/authentication/hook.ts index 189e78212..cf44a82e0 100644 --- a/packages/zudoku/src/lib/authentication/hook.ts +++ b/packages/zudoku/src/lib/authentication/hook.ts @@ -17,6 +17,8 @@ export const useRefreshUserProfile = ({ refetchOnWindowFocus?: boolean | "always"; } = {}) => { const { authentication } = useZudoku(); + const profile = useAuthState((s) => s.profile); + const profileFetchedAt = useAuthState((s) => s.profileFetchedAt); const isAuthEnabled = typeof authentication !== "undefined"; return useQuery({ @@ -26,7 +28,13 @@ export const useRefreshUserProfile = ({ queryKey: ["refresh-user-profile"], enabled: isAuthEnabled && typeof authentication?.refreshUserProfile === "function", - queryFn: () => authentication?.refreshUserProfile?.(), + queryFn: async () => { + const result = await authentication?.refreshUserProfile?.(); + useAuthState.setState({ profileFetchedAt: Date.now() }); + return result; + }, + initialData: profile ? true : undefined, + initialDataUpdatedAt: profileFetchedAt ?? undefined, }); }; diff --git a/packages/zudoku/src/lib/authentication/providers/clerk.tsx b/packages/zudoku/src/lib/authentication/providers/clerk.tsx index 91db91fb3..fd19ba368 100644 --- a/packages/zudoku/src/lib/authentication/providers/clerk.tsx +++ b/packages/zudoku/src/lib/authentication/providers/clerk.tsx @@ -127,6 +127,7 @@ const clerkAuth: AuthenticationProviderInitializer< isAuthenticated: true, isPending: false, profile, + profileFetchedAt: Date.now(), providerData: { type: "clerk", user: clerk.session?.user, diff --git a/packages/zudoku/src/lib/authentication/providers/firebase.tsx b/packages/zudoku/src/lib/authentication/providers/firebase.tsx index 15d8cd21b..7b210dd51 100644 --- a/packages/zudoku/src/lib/authentication/providers/firebase.tsx +++ b/packages/zudoku/src/lib/authentication/providers/firebase.tsx @@ -365,6 +365,7 @@ class FirebaseAuthenticationProvider isAuthenticated: false, isPending: false, profile: undefined, + profileFetchedAt: null, providerData: undefined, }); diff --git a/packages/zudoku/src/lib/authentication/providers/openid.tsx b/packages/zudoku/src/lib/authentication/providers/openid.tsx index 5f935d49c..2a6964518 100644 --- a/packages/zudoku/src/lib/authentication/providers/openid.tsx +++ b/packages/zudoku/src/lib/authentication/providers/openid.tsx @@ -241,6 +241,7 @@ export class OpenIDAuthenticationProvider isAuthenticated: true, isPending: false, profile, + profileFetchedAt: Date.now(), }); return true; @@ -440,6 +441,7 @@ export class OpenIDAuthenticationProvider isAuthenticated: false, isPending: false, profile: null, + profileFetchedAt: null, providerData: null, }); return; @@ -546,6 +548,7 @@ export class OpenIDAuthenticationProvider isAuthenticated: true, isPending: false, profile, + profileFetchedAt: Date.now(), }); await this.refreshUserProfile(); diff --git a/packages/zudoku/src/lib/authentication/state.ts b/packages/zudoku/src/lib/authentication/state.ts index d3a619f07..9279db4ea 100644 --- a/packages/zudoku/src/lib/authentication/state.ts +++ b/packages/zudoku/src/lib/authentication/state.ts @@ -25,6 +25,7 @@ export interface AuthState { isAuthenticated: boolean; isPending: boolean; profile: UserProfile | null; + profileFetchedAt: number | null; providerData: ProviderData | null; setAuthenticationPending: () => void; setLoggedOut: () => void; @@ -40,12 +41,14 @@ export const authState = create()( isAuthenticated: false, isPending: true, profile: null, + profileFetchedAt: null, providerData: null, setAuthenticationPending: () => set(() => ({ isAuthenticated: false, isPending: false, profile: null, + profileFetchedAt: null, providerData: null, })), setLoggedOut: () => @@ -53,6 +56,7 @@ export const authState = create()( isAuthenticated: false, isPending: false, profile: null, + profileFetchedAt: null, providerData: null, })), setLoggedIn: ({ profile, providerData }) => @@ -60,6 +64,7 @@ export const authState = create()( isAuthenticated: true, isPending: false, profile, + profileFetchedAt: Date.now(), providerData, })), }), diff --git a/packages/zudoku/src/lib/core/RouteGuard.test.tsx b/packages/zudoku/src/lib/core/RouteGuard.test.tsx index c8f861f85..1625c0e63 100644 --- a/packages/zudoku/src/lib/core/RouteGuard.test.tsx +++ b/packages/zudoku/src/lib/core/RouteGuard.test.tsx @@ -70,6 +70,7 @@ const createWrapper = ({ isPending: false, isAuthEnabled: false, profile: null, + profileFetchedAt: null, providerData: null, setAuthenticationPending: vi.fn(), setLoggedOut: vi.fn(), From 84cacc61335aa1ee3eea2b439af08f3fd1a7eab9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 4 May 2026 13:31:14 +0000 Subject: [PATCH 6/8] Read persisted auth state directly for query initial data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reading profile and profileFetchedAt via useAuthState selectors meant the query observed whatever state Zustand had hydrated by the time the hook ran. If anything delayed hydration (SSR boundary, concurrent renders, etc.) the first observer saw profile=null on mount and React Query started fetching userinfo before the store caught up. Read the persisted state directly from localStorage in the initialData/initialDataUpdatedAt callbacks. localStorage is sync, so this returns the latest persisted values regardless of Zustand's hydration timing. Zustand remains the runtime source of truth — the queryFn still writes profileFetchedAt through setState, which the persist middleware syncs back to localStorage for the next reload. https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT --- packages/zudoku/src/lib/authentication/hook.ts | 11 ++++++----- packages/zudoku/src/lib/authentication/state.ts | 16 +++++++++++++++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/zudoku/src/lib/authentication/hook.ts b/packages/zudoku/src/lib/authentication/hook.ts index cf44a82e0..ca403e9cf 100644 --- a/packages/zudoku/src/lib/authentication/hook.ts +++ b/packages/zudoku/src/lib/authentication/hook.ts @@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { useNavigate } from "react-router"; import { useZudoku } from "../components/context/ZudokuContext.js"; import type { AuthActionOptions } from "./authentication.js"; -import { useAuthState } from "./state.js"; +import { readPersistedAuthState, useAuthState } from "./state.js"; export type UseAuthReturn = ReturnType; @@ -17,8 +17,6 @@ export const useRefreshUserProfile = ({ refetchOnWindowFocus?: boolean | "always"; } = {}) => { const { authentication } = useZudoku(); - const profile = useAuthState((s) => s.profile); - const profileFetchedAt = useAuthState((s) => s.profileFetchedAt); const isAuthEnabled = typeof authentication !== "undefined"; return useQuery({ @@ -33,8 +31,11 @@ export const useRefreshUserProfile = ({ useAuthState.setState({ profileFetchedAt: Date.now() }); return result; }, - initialData: profile ? true : undefined, - initialDataUpdatedAt: profileFetchedAt ?? undefined, + initialData: () => (readPersistedAuthState()?.profile ? true : undefined), + initialDataUpdatedAt: () => { + const ts = readPersistedAuthState()?.profileFetchedAt; + return typeof ts === "number" ? ts : undefined; + }, }); }; diff --git a/packages/zudoku/src/lib/authentication/state.ts b/packages/zudoku/src/lib/authentication/state.ts index 9279db4ea..db8909670 100644 --- a/packages/zudoku/src/lib/authentication/state.ts +++ b/packages/zudoku/src/lib/authentication/state.ts @@ -21,6 +21,8 @@ export type ProviderData = [keyof ProviderDataRegistry] extends [never] ? unknown : ProviderDataRegistry[keyof ProviderDataRegistry]; +export const AUTH_STATE_STORAGE_KEY = "auth-state"; + export interface AuthState { isAuthenticated: boolean; isPending: boolean; @@ -76,7 +78,7 @@ export const authState = create()( ...(typeof persistedState === "object" ? persistedState : {}), }; }, - name: "auth-state", + name: AUTH_STATE_STORAGE_KEY, storage: createJSONStorage(() => localStorage), }, ), @@ -86,6 +88,18 @@ syncZustandState(authState); export const useAuthState = authState; +export const readPersistedAuthState = () => { + if (typeof window === "undefined") return undefined; + try { + const raw = window.localStorage.getItem(AUTH_STATE_STORAGE_KEY); + if (!raw) return undefined; + const parsed = JSON.parse(raw) as { state?: Partial }; + return parsed.state; + } catch { + return undefined; + } +}; + export type CustomClaim = | string | number From fc3a8fab13423e91be05969e9fb72e6368d03206 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 4 May 2026 13:55:48 +0000 Subject: [PATCH 7/8] Refetch userinfo aggressively while email is unverified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Once the staleTime/initialDataUpdatedAt caching kicked in, a user waiting on email verification could reload or refocus the tab and keep seeing the stale "unverified" state for up to 5 minutes — the exact scenario where they need fresh data. When useVerifiedEmail observes profile.emailVerified === false, override refetchOnMount and refetchOnWindowFocus to "always", which ignores staleTime. Once verification succeeds the flag flips and the hook reverts to the normal cached behavior, so it doesn't add any ongoing fetch traffic for verified users. https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT --- packages/zudoku/src/lib/authentication/hook.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/zudoku/src/lib/authentication/hook.ts b/packages/zudoku/src/lib/authentication/hook.ts index ca403e9cf..9b8e9a349 100644 --- a/packages/zudoku/src/lib/authentication/hook.ts +++ b/packages/zudoku/src/lib/authentication/hook.ts @@ -13,14 +13,17 @@ export type UseAuthReturn = ReturnType; */ export const useRefreshUserProfile = ({ refetchOnWindowFocus, + refetchOnMount, }: { refetchOnWindowFocus?: boolean | "always"; + refetchOnMount?: boolean | "always"; } = {}) => { const { authentication } = useZudoku(); const isAuthEnabled = typeof authentication !== "undefined"; return useQuery({ refetchOnWindowFocus, + refetchOnMount, staleTime: 1000 * 60 * 5, gcTime: 1000 * 60 * 10, queryKey: ["refresh-user-profile"], @@ -45,8 +48,11 @@ export const useVerifiedEmail = () => { const navigate = useNavigate(); const isAuthEnabled = typeof authentication !== "undefined"; + const isUnverified = authState.profile?.emailVerified === false; + const { refetch: refreshUserProfile } = useRefreshUserProfile({ - refetchOnWindowFocus: true, + refetchOnWindowFocus: isUnverified ? "always" : true, + refetchOnMount: isUnverified ? "always" : undefined, }); return { From bbda00ffea8626798db30721caaad212b32ed233 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 13:05:47 +0000 Subject: [PATCH 8/8] Drop direct localStorage read for query initial data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zustand 5 with createJSONStorage(() => localStorage) hydrates synchronously at module load, so useAuthState selectors return the persisted profile and profileFetchedAt on the very first render. Reading the JSON directly from localStorage was defensive and not actually load-bearing — go back to selectors and remove the helper plus the storage-key export. If reload-time fetches reappear, that points to a real Zustand hydration issue worth investigating directly rather than papering over. https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT --- packages/zudoku/src/lib/authentication/hook.ts | 11 +++++------ packages/zudoku/src/lib/authentication/state.ts | 16 +--------------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/packages/zudoku/src/lib/authentication/hook.ts b/packages/zudoku/src/lib/authentication/hook.ts index 9b8e9a349..ed11b52b9 100644 --- a/packages/zudoku/src/lib/authentication/hook.ts +++ b/packages/zudoku/src/lib/authentication/hook.ts @@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { useNavigate } from "react-router"; import { useZudoku } from "../components/context/ZudokuContext.js"; import type { AuthActionOptions } from "./authentication.js"; -import { readPersistedAuthState, useAuthState } from "./state.js"; +import { useAuthState } from "./state.js"; export type UseAuthReturn = ReturnType; @@ -19,6 +19,8 @@ export const useRefreshUserProfile = ({ refetchOnMount?: boolean | "always"; } = {}) => { const { authentication } = useZudoku(); + const profile = useAuthState((s) => s.profile); + const profileFetchedAt = useAuthState((s) => s.profileFetchedAt); const isAuthEnabled = typeof authentication !== "undefined"; return useQuery({ @@ -34,11 +36,8 @@ export const useRefreshUserProfile = ({ useAuthState.setState({ profileFetchedAt: Date.now() }); return result; }, - initialData: () => (readPersistedAuthState()?.profile ? true : undefined), - initialDataUpdatedAt: () => { - const ts = readPersistedAuthState()?.profileFetchedAt; - return typeof ts === "number" ? ts : undefined; - }, + initialData: profile ? true : undefined, + initialDataUpdatedAt: profileFetchedAt ?? undefined, }); }; diff --git a/packages/zudoku/src/lib/authentication/state.ts b/packages/zudoku/src/lib/authentication/state.ts index db8909670..9279db4ea 100644 --- a/packages/zudoku/src/lib/authentication/state.ts +++ b/packages/zudoku/src/lib/authentication/state.ts @@ -21,8 +21,6 @@ export type ProviderData = [keyof ProviderDataRegistry] extends [never] ? unknown : ProviderDataRegistry[keyof ProviderDataRegistry]; -export const AUTH_STATE_STORAGE_KEY = "auth-state"; - export interface AuthState { isAuthenticated: boolean; isPending: boolean; @@ -78,7 +76,7 @@ export const authState = create()( ...(typeof persistedState === "object" ? persistedState : {}), }; }, - name: AUTH_STATE_STORAGE_KEY, + name: "auth-state", storage: createJSONStorage(() => localStorage), }, ), @@ -88,18 +86,6 @@ syncZustandState(authState); export const useAuthState = authState; -export const readPersistedAuthState = () => { - if (typeof window === "undefined") return undefined; - try { - const raw = window.localStorage.getItem(AUTH_STATE_STORAGE_KEY); - if (!raw) return undefined; - const parsed = JSON.parse(raw) as { state?: Partial }; - return parsed.state; - } catch { - return undefined; - } -}; - export type CustomClaim = | string | number