// Modified in the independent hardening fork; see docs/hardening/README.md. // Modified for AgentDeskFixes: invalidate stale profile/sign-out work and clear private account state. "use client" import { Fragment, createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from "react" import { fetchProfile, logout } from "@/lib/api/auth" import { AUTH_SESSION_CHANGED_EVENT, AUTH_SESSION_EXPIRED_EVENT, clearSession, readSession, writeSession, type AuthSession, } from "@/lib/auth" import { createSessionRequestGuard } from "@/lib/session-request-guard" import { resetAgentConversations } from "@/lib/stores/agent-conversations" type SessionContextValue = { session: AuthSession | null ready: boolean signingOut: boolean refreshSession: () => Promise signOut: () => Promise } const SessionContext = createContext(null) function accountKey(session: AuthSession | null) { return session ? `${session.user.userType}:${session.user.id}` : "anonymous" } export function SessionProvider({ children }: { children: ReactNode }) { const [session, setSession] = useState(null) const [ready, setReady] = useState(false) const [signingOut, setSigningOut] = useState(false) const [requestGuard] = useState(() => createSessionRequestGuard(readSession)) const identityRef = useRef(null) const signOutPendingRef = useRef(false) const signOutGenerationRef = useRef(0) const applySession = useCallback((next: AuthSession | null) => { const nextIdentity = accountKey(next) if (identityRef.current !== nextIdentity) { resetAgentConversations() identityRef.current = nextIdentity } setSession(next) setReady(true) }, []) const refreshSession = useCallback(async () => { const stored = readSession() if (!stored) { requestGuard.invalidate() applySession(null) return } const isCurrent = requestGuard.begin(stored) try { const profile = await fetchProfile() if (!isCurrent()) return const nextSession: AuthSession = { ...stored, user: profile.user, permissions: profile.permissions, roles: profile.roles, accessToken: profile.accessToken || stored.accessToken, expiresAt: profile.expiresAt || stored.expiresAt, } applySession(nextSession) writeSession(nextSession) } catch (error) { if (!isCurrent()) return const errorCode = (error as Error & { errorCode?: number }).errorCode if (errorCode === 3000 || errorCode === 3002) { clearSession() applySession(null) } else { applySession(stored) } } finally { if (isCurrent()) setReady(true) } }, [applySession, requestGuard]) const signOut = useCallback(async () => { if (signOutPendingRef.current) return signOutPendingRef.current = true const generation = ++signOutGenerationRef.current setSigningOut(true) requestGuard.invalidate() // Start the request with the old token, then immediately remove local access. const pending = logout() clearSession() applySession(null) try { await pending } catch { // Local sign-out already completed; a late error cannot clear a new login. } finally { if (generation === signOutGenerationRef.current) { signOutPendingRef.current = false setSigningOut(false) } } }, [applySession, requestGuard]) useEffect(() => { const syncSession = () => { requestGuard.invalidate() const current = readSession() if (current && signOutPendingRef.current) { signOutGenerationRef.current += 1 signOutPendingRef.current = false setSigningOut(false) } applySession(current) } const syncOtherTab = (event: StorageEvent) => { if (event.key === "agent-desk-session" || event.key === null) syncSession() } window.addEventListener(AUTH_SESSION_CHANGED_EVENT, syncSession) window.addEventListener(AUTH_SESSION_EXPIRED_EVENT, syncSession) window.addEventListener("storage", syncOtherTab) applySession(readSession()) void refreshSession() return () => { requestGuard.invalidate() signOutGenerationRef.current += 1 window.removeEventListener(AUTH_SESSION_CHANGED_EVENT, syncSession) window.removeEventListener(AUTH_SESSION_EXPIRED_EVENT, syncSession) window.removeEventListener("storage", syncOtherTab) } }, [applySession, refreshSession, requestGuard]) return ( {children} ) } export function useSession() { const context = useContext(SessionContext) if (!context) throw new Error("useSession must be used within SessionProvider") return context }