@spidy092/auth-client 3.1.4 → 3.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../react/useAuth.js","../../react/AuthProvider.jsx","../../token.js","../../config.js"],"sourcesContent":["import { useContext } from 'react';\nimport { AuthContext } from './AuthProvider';\n\nexport function useAuth() {\n const context = useContext(AuthContext);\n if (!context) {\n throw new Error('useAuth must be used within an AuthProvider');\n }\n return context;\n}","// auth-client/react/AuthProvider.jsx\nimport React, { createContext, useState, useEffect, useRef } from 'react';\nimport { getToken, setToken, clearToken, addTokenListener } from '../token';\nimport { getConfig } from '../config';\nimport { emitAuthDiagnostic } from '../diagnostics';\nimport { \n login as coreLogin, \n logout as coreLogout,\n startSessionSecurity,\n stopSessionSecurity,\n onSessionInvalid\n} from '../core';\n\nexport const AuthContext = createContext();\n\nexport function AuthProvider({ children, onSessionExpired, manageSessionSecurity = true }) {\n const [token, setTokenState] = useState(getToken());\n const [user, setUser] = useState(null);\n const [loading, setLoading] = useState(!!token); // Loading if we have a token to validate\n const [sessionValid, setSessionValid] = useState(true);\n const sessionSecurityRef = useRef(null);\n const onSessionExpiredRef = useRef(onSessionExpired);\n const recoveryInFlightRef = useRef(null);\n const profileRecoveryTokensRef = useRef(new Set());\n\n useEffect(() => {\n onSessionExpiredRef.current = onSessionExpired;\n }, [onSessionExpired]);\n\n // Keep React synchronized with the canonical token singleton. Refresh\n // wrappers and sibling tabs can update the singleton without calling the\n // provider's setToken helper.\n useEffect(() => {\n const unsubscribe = addTokenListener((nextToken, previousToken) => {\n if (nextToken === previousToken) return;\n\n setTokenState(nextToken);\n if (nextToken) {\n setSessionValid(true);\n setLoading(true);\n } else {\n setSessionValid(false);\n setUser(null);\n setLoading(false);\n }\n });\n\n return unsubscribe;\n }, []);\n\n const invalidateLocalSession = () => {\n clearToken();\n setTokenState(null);\n setUser(null);\n setSessionValid(false);\n setLoading(false);\n };\n\n // Handle session invalidation (from Keycloak admin deletion or expiry)\n const handleSessionInvalid = (reason) => {\n console.log('🚨 AuthProvider: Session invalidated -', reason);\n\n if (recoveryInFlightRef.current) return recoveryInFlightRef.current;\n\n const recover = async () => {\n const callback = onSessionExpiredRef.current;\n if (typeof callback === 'function') {\n try {\n await callback(reason);\n const recoveredToken = getToken();\n if (recoveredToken) {\n setTokenState(recoveredToken);\n setSessionValid(true);\n setLoading(true);\n return true;\n }\n } catch (error) {\n await emitAuthDiagnostic('SESSION_RECOVERY_FAILED', 'FAILURE', reason, {\n status: error?.status || null,\n errorCode: error?.code || null,\n });\n }\n }\n\n invalidateLocalSession();\n return false;\n };\n\n recoveryInFlightRef.current = recover().finally(() => {\n recoveryInFlightRef.current = null;\n });\n\n return recoveryInFlightRef.current;\n };\n\n // Start session security on mount (when we have a token)\n useEffect(() => {\n if (manageSessionSecurity && token && !sessionSecurityRef.current) {\n console.log('🔐 AuthProvider: Starting session security');\n \n // Register session invalid handler\n const unsubscribe = onSessionInvalid(handleSessionInvalid);\n \n // Start proactive refresh + session monitoring\n sessionSecurityRef.current = startSessionSecurity(handleSessionInvalid);\n \n return () => {\n unsubscribe();\n if (sessionSecurityRef.current) {\n sessionSecurityRef.current.stopAll();\n sessionSecurityRef.current = null;\n }\n };\n }\n \n // Cleanup when token is removed\n if ((!manageSessionSecurity || !token) && sessionSecurityRef.current) {\n sessionSecurityRef.current.stopAll();\n sessionSecurityRef.current = null;\n }\n }, [manageSessionSecurity, token]);\n\n useEffect(() => {\n console.log('🔍 AuthProvider useEffect triggered:', { \n hasToken: !!token, \n tokenLength: token?.length \n });\n \n if (!token) {\n console.log('⚠️ AuthProvider: No token, setting loading=false');\n setLoading(false);\n return;\n }\n \n const { authBaseUrl } = getConfig();\n if (!authBaseUrl) {\n console.warn('AuthProvider: No authBaseUrl configured');\n setLoading(false);\n return;\n }\n\n console.log('🌐 AuthProvider: Fetching profile with token...', {\n authBaseUrl,\n tokenPreview: token.slice(0, 50) + '...'\n });\n\n let cancelled = false;\n\n const fetchProfile = async (accessToken) => {\n const response = await fetch(`${authBaseUrl}/account/profile`, {\n headers: { Authorization: `Bearer ${accessToken}` },\n credentials: 'include',\n });\n console.log('📥 Profile response status:', response.status);\n if (!response.ok) {\n const error = new Error(`Profile request failed: ${response.status}`);\n error.status = response.status;\n throw error;\n }\n return response.json();\n };\n\n const loadProfile = async () => {\n try {\n const responseBody = await fetchProfile(token);\n if (cancelled) return;\n const userData = responseBody?.data ?? responseBody;\n console.log('✅ Profile fetched successfully:', userData.email);\n setUser(userData);\n setSessionValid(true);\n setLoading(false);\n } catch (error) {\n if (cancelled) return;\n\n const status = Number(error?.status || 0);\n const isUnauthorized = status === 401;\n const callback = onSessionExpiredRef.current;\n\n await emitAuthDiagnostic(\n 'PROFILE_REQUEST_FAILED',\n isUnauthorized ? 'FAILURE' : 'WARNING',\n isUnauthorized ? 'PROFILE_UNAUTHORIZED' : (status ? `PROFILE_HTTP_${status}` : 'PROFILE_NETWORK_ERROR'),\n { status, errorName: error?.name, errorCode: error?.code },\n );\n\n // Temporary profile/API failures must not destroy a valid session.\n if (!isUnauthorized) {\n console.warn('⚠️ Profile unavailable; retaining the current auth session', error);\n setLoading(false);\n return;\n }\n\n // Give the application one chance to refresh a rejected access token.\n // The token set prevents an infinite 401 -> refresh -> 401 loop.\n if (typeof callback === 'function' && !profileRecoveryTokensRef.current.has(token)) {\n profileRecoveryTokensRef.current.add(token);\n try {\n await callback('profile_unauthorized');\n const recoveredToken = getToken();\n if (recoveredToken && recoveredToken !== token) {\n setTokenState(recoveredToken);\n setSessionValid(true);\n setLoading(true);\n return;\n }\n if (recoveredToken) {\n const responseBody = await fetchProfile(recoveredToken);\n if (cancelled) return;\n const userData = responseBody?.data ?? responseBody;\n setUser(userData);\n setSessionValid(true);\n setLoading(false);\n return;\n }\n } catch (recoveryError) {\n await emitAuthDiagnostic('PROFILE_RECOVERY_FAILED', 'FAILURE', 'PROFILE_REFRESH_FAILED', {\n status: recoveryError?.status || null,\n errorCode: recoveryError?.code || null,\n });\n }\n }\n\n invalidateLocalSession();\n }\n };\n\n void loadProfile();\n\n return () => {\n cancelled = true;\n };\n }, [token]);\n\n const login = (clientKey, redirectUri, state) => {\n coreLogin(clientKey, redirectUri, state);\n };\n\n const logout = async (options) => {\n // Stop session security before logout\n stopSessionSecurity();\n sessionSecurityRef.current = null;\n \n try {\n await coreLogout(options);\n } finally {\n setUser(null);\n setTokenState(null);\n setSessionValid(true);\n }\n };\n\n const value = {\n token,\n user,\n loading,\n login,\n logout,\n isAuthenticated: !!token && !!user && sessionValid,\n sessionValid,\n setUser,\n setToken: (newToken) => {\n setToken(newToken);\n setTokenState(newToken);\n setSessionValid(true);\n if (newToken) setLoading(true);\n },\n clearToken: () => {\n stopSessionSecurity();\n sessionSecurityRef.current = null;\n clearToken();\n setTokenState(null);\n setUser(null);\n },\n };\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n","// auth-client/token.js - MINIMAL WORKING VERSION\n\nimport { jwtDecode } from 'jwt-decode';\n\nlet accessToken = null;\nconst listeners = new Set();\n\nconst REFRESH_COOKIE = 'account_refresh_token';\nconst COOKIE_MAX_AGE = 7 * 24 * 60 * 60;\n\nfunction secureAttribute() {\n try {\n return typeof window !== 'undefined' && window.location?.protocol === 'https:'\n ? '; Secure'\n : '';\n } catch (err) {\n return '';\n }\n}\n\n// ========== ACCESS TOKEN ==========\nfunction writeAccessToken(token) {\n if (!token) {\n try {\n localStorage.removeItem('authToken');\n } catch (err) {\n console.warn('Could not clear token from localStorage:', err);\n }\n return;\n }\n\n try {\n localStorage.setItem('authToken', token);\n } catch (err) {\n console.warn('Could not persist token to localStorage:', err);\n }\n}\n\nfunction readAccessToken() {\n try {\n return localStorage.getItem('authToken');\n } catch (err) {\n console.warn('Could not read token from localStorage:', err);\n return null;\n }\n}\n\n// ========== REFRESH TOKEN (KEEP SIMPLE) ==========\n// export function setRefreshToken(token) {\n// if (!token) {\n// clearRefreshToken();\n// return;\n// }\n\n// const expires = new Date(Date.now() + COOKIE_MAX_AGE * 1000);\n\n// try {\n// document.cookie = `${REFRESH_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax${secureAttribute()}; Expires=${expires.toUTCString()}`;\n// } catch (err) {\n// console.warn('Could not set refresh token:', err);\n// }\n// }\n\n// export function getRefreshToken() {\n// try {\n// const match = document.cookie\n// ?.split('; ')\n// ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));\n\n// if (match) {\n// return decodeURIComponent(match.split('=')[1]);\n// }\n// } catch (err) {\n// console.warn('Could not read refresh token:', err);\n// }\n\n// return null;\n// }\n\n// export function clearRefreshToken() {\n// try {\n// document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Lax${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n// } catch (err) {\n// console.warn('Could not clear refresh token:', err);\n// }\n// }\n\n// ========== ACCESS TOKEN FUNCTIONS ==========\nfunction decode(token) {\n try {\n return jwtDecode(token);\n } catch (err) {\n return null;\n }\n}\n\nfunction isExpired(token, bufferSeconds = 60) {\n if (!token) return true;\n const decoded = decode(token);\n if (!decoded?.exp) return true;\n const now = Date.now() / 1000;\n return decoded.exp < now + bufferSeconds;\n}\n\n// ========== TOKEN EXPIRY UTILITIES ==========\n// Get the exact expiry time of a token as a Date object\nexport function getTokenExpiryTime(token) {\n if (!token) return null;\n const decoded = decode(token);\n if (!decoded?.exp) return null;\n return new Date(decoded.exp * 1000);\n}\n\n// Get seconds until token expires (negative if already expired)\nexport function getTimeUntilExpiry(token) {\n if (!token) return -1;\n const decoded = decode(token);\n if (!decoded?.exp) return -1;\n const now = Date.now() / 1000;\n return Math.floor(decoded.exp - now);\n}\n\n// Check if token will expire within the next N seconds\nexport function willExpireSoon(token, withinSeconds = 60) {\n const timeLeft = getTimeUntilExpiry(token);\n return timeLeft >= 0 && timeLeft <= withinSeconds;\n}\n\nexport function setToken(token) {\n const previousToken = accessToken;\n accessToken = token || null;\n writeAccessToken(accessToken);\n\n if (previousToken !== accessToken) {\n listeners.forEach((listener) => {\n try {\n listener(accessToken, previousToken);\n } catch (err) {\n console.warn('Token listener error:', err);\n }\n });\n }\n}\n\nexport function getToken() {\n if (accessToken) return accessToken;\n accessToken = readAccessToken();\n return accessToken;\n}\n\nexport function clearToken() {\n if (!accessToken) {\n writeAccessToken(null);\n clearRefreshToken();\n return;\n }\n\n const previousToken = accessToken;\n accessToken = null;\n writeAccessToken(null);\n clearRefreshToken();\n\n listeners.forEach((listener) => {\n try {\n listener(null, previousToken);\n } catch (err) {\n console.warn('Token listener error:', err);\n }\n });\n}\n\n// ========== REFRESH TOKEN STORAGE ==========\n// By default:\n// HTTP → localStorage (cookies don't work cross-origin in dev)\n// HTTPS → httpOnly cookies (secure, managed by server)\n// When persistRefreshToken is enabled:\n// Always use localStorage (for local HTTPS with mkcert/self-signed certs)\nconst REFRESH_TOKEN_KEY = 'auth_refresh_token';\n\n// ✅ Persistence flag - controlled by config.persistRefreshToken\nlet _persistRefreshToken = false;\n\nexport function enableRefreshTokenPersistence(enabled) {\n _persistRefreshToken = !!enabled;\n console.log(`🔧 Refresh token persistence: ${_persistRefreshToken ? 'ENABLED' : 'DISABLED'}`);\n}\n\nfunction shouldUseLocalStorage() {\n // If persistence is forced, always use localStorage\n if (_persistRefreshToken) return true;\n // Otherwise, only use localStorage on HTTP (dev mode)\n try {\n return typeof window !== 'undefined' &&\n window.location?.protocol === 'http:';\n } catch (err) {\n return false;\n }\n}\n\nexport function setRefreshToken(token) {\n if (!token) {\n clearRefreshToken();\n return;\n }\n\n if (shouldUseLocalStorage()) {\n try {\n localStorage.setItem(REFRESH_TOKEN_KEY, token);\n console.log(`📦 Refresh token stored in localStorage (${_persistRefreshToken ? 'persistence enabled' : 'HTTP dev mode'})`);\n } catch (err) {\n console.warn('Could not store refresh token:', err);\n }\n } else {\n // HTTPS without persistence: refresh token is in httpOnly cookie only\n console.log('🔒 Refresh token managed by server httpOnly cookie (production mode)');\n }\n}\n\nexport function getRefreshToken() {\n if (shouldUseLocalStorage()) {\n try {\n const token = localStorage.getItem(REFRESH_TOKEN_KEY);\n return token;\n } catch (err) {\n console.warn('Could not read refresh token:', err);\n return null;\n }\n }\n\n // HTTPS without persistence: refresh token is in httpOnly cookie (not accessible via JS)\n // The refresh endpoint uses credentials: 'include' to send the cookie\n return null;\n}\n\nexport function clearRefreshToken() {\n // Clear localStorage (for HTTP dev)\n try {\n localStorage.removeItem(REFRESH_TOKEN_KEY);\n } catch (err) {\n // Ignore\n }\n\n // Clear cookie (for production)\n try {\n document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Strict${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n } catch (err) {\n console.warn('Could not clear refresh token cookie:', err);\n }\n\n // Clear sessionStorage\n try {\n sessionStorage.removeItem(REFRESH_COOKIE);\n } catch (err) {\n // Ignore\n }\n}\n\nexport function addTokenListener(listener) {\n if (typeof listener !== 'function') {\n throw new Error('Token listener must be a function');\n }\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nexport function removeTokenListener(listener) {\n listeners.delete(listener);\n}\n\nexport function getListenerCount() {\n return listeners.size;\n}\n\n// localStorage changes do not fire in the tab that made the change, but they\n// do fire in sibling tabs on the same origin. Keep the in-memory token cache\n// and React consumers synchronized when another tab logs in or out.\nif (typeof window !== 'undefined' && window.addEventListener) {\n window.addEventListener('storage', (event) => {\n if (event.key !== 'authToken') return;\n\n const previousToken = accessToken;\n accessToken = event.newValue || null;\n if (previousToken === accessToken) return;\n\n listeners.forEach((listener) => {\n try {\n listener(accessToken, previousToken);\n } catch (err) {\n console.warn('Token listener error:', err);\n }\n });\n });\n}\n\nexport function isAuthenticated() {\n const token = getToken();\n return !!token && !isExpired(token, 10);\n}\n\n\n","// auth-client/config.js\nimport { enableRefreshTokenPersistence } from './token.js';\n\n// ========== SESSION SECURITY CONFIGURATION ==========\n// These settings control how the auth-client handles token refresh and session validation\n// to ensure deleted sessions in Keycloak are detected quickly.\n\nlet config = {\n clientKey: null,\n authBaseUrl: null,\n redirectUri: null,\n accountUiUrl: null,\n isRouter: false, // ✅ Add router flag\n\n // ========== SESSION SECURITY SETTINGS ==========\n // Buffer time (in seconds) before token expiry to trigger proactive refresh\n // With 5-minute access tokens, refreshing 60s before expiry ensures seamless UX\n tokenRefreshBuffer: 60,\n\n // Interval (in milliseconds) for periodic session validation\n // Validates that the session still exists in Keycloak (not deleted by admin)\n // Default: 15 minutes (900000ms) - Increased from 2m to avoid frequent checks\n sessionValidationInterval: 15 * 60 * 1000,\n\n // Enable/disable periodic session validation\n // When enabled, the client will ping the server to verify session is still active\n enableSessionValidation: true,\n\n // Enable/disable proactive token refresh\n // When enabled, tokens are refreshed before they expire (using tokenRefreshBuffer)\n enableProactiveRefresh: true,\n\n // Validate session when browser tab becomes visible again\n // Catches session deletions that happened while the tab was inactive\n validateOnVisibility: true,\n\n // ========== REFRESH TOKEN PERSISTENCE ==========\n // When true, stores refresh token in localStorage even on HTTPS\n // Required for local dev with mkcert/self-signed certs where httpOnly cookies\n // may not work reliably across origins\n // ⚠️ In true production, set to false and rely on httpOnly cookies\n persistRefreshToken: false,\n};\n\nconst RUNTIME_POLICY_DEFAULTS = {\n tokenRefreshBuffer: 60,\n sessionValidationInterval: 15 * 60 * 1000,\n enableSessionValidation: true,\n enableProactiveRefresh: true,\n validateOnVisibility: true,\n};\n\nfunction validateRuntimePolicy(policy = {}) {\n const numberInRange = (value, min, max) => Number.isInteger(value) && value >= min && value <= max;\n const result = { ...RUNTIME_POLICY_DEFAULTS };\n if (numberInRange(policy.tokenRefreshBuffer, 10, 300)) result.tokenRefreshBuffer = policy.tokenRefreshBuffer;\n if (numberInRange(policy.sessionValidationInterval, 60000, 86400000)) result.sessionValidationInterval = policy.sessionValidationInterval;\n for (const key of ['enableSessionValidation', 'enableProactiveRefresh', 'validateOnVisibility']) {\n if (typeof policy[key] === 'boolean') result[key] = policy[key];\n }\n return result;\n}\n\nexport function applyRuntimePolicy(policy = {}) {\n config = { ...config, ...validateRuntimePolicy(policy) };\n return getConfig();\n}\n\nexport async function loadRuntimePolicy() {\n const { authBaseUrl, clientKey } = config;\n if (!authBaseUrl || !clientKey || typeof fetch !== 'function') return getConfig();\n try {\n const response = await fetch(`${authBaseUrl.replace(/\\/+$/, '')}/clients/${encodeURIComponent(clientKey)}/config`, {\n method: 'GET',\n credentials: 'include',\n headers: { Accept: 'application/json' },\n });\n if (!response.ok) throw new Error(`runtime policy HTTP ${response.status}`);\n const payload = await response.json();\n return applyRuntimePolicy(payload.authentication_policy || {});\n } catch (error) {\n // Runtime config is an enhancement. Safe bounded defaults keep login\n // available during a control-plane outage.\n console.warn('[auth-client] Runtime policy unavailable; using safe defaults', error.message);\n return applyRuntimePolicy({});\n }\n}\n\nexport function setConfig(customConfig = {}) {\n if (!customConfig.clientKey || !customConfig.authBaseUrl) {\n throw new Error('Missing required config: clientKey and authBaseUrl are required');\n }\n\n config = {\n ...config,\n ...customConfig,\n redirectUri: customConfig.redirectUri || window.location.origin + '/callback',\n // ✅ Auto-detect router mode\n isRouter: customConfig.isRouter || customConfig.clientKey === 'account-ui'\n };\n\n // Keep token storage synchronized when consumers reconfigure the singleton.\n enableRefreshTokenPersistence(config.persistRefreshToken);\n if (config.persistRefreshToken) {\n console.log('📦 Refresh token persistence ENABLED (localStorage on HTTPS)');\n }\n\n console.log(`🔧 Auth Client Mode: ${config.isRouter ? 'ROUTER' : 'CLIENT'}`, {\n clientKey: config.clientKey,\n isRouter: config.isRouter,\n persistRefreshToken: config.persistRefreshToken\n });\n}\n\nexport function getConfig() {\n return { ...config };\n}\n\n// ✅ Helper function\nexport function isRouterMode() {\n return config.isRouter;\n}\n"],"mappings":";AAAA,SAAS,kBAAkB;;;ACC3B,OAAO,SAAS,eAAe,UAAU,WAAW,cAAc;;;ACClE,SAAS,iBAAiB;AAE1B,IAAI,cAAc;AAClB,IAAM,YAAY,oBAAI,IAAI;AAG1B,IAAM,iBAAiB,IAAI,KAAK,KAAK;AA8QrC,IAAI,OAAO,WAAW,eAAe,OAAO,kBAAkB;AAC5D,SAAO,iBAAiB,WAAW,CAAC,UAAU;AAC5C,QAAI,MAAM,QAAQ,YAAa;AAE/B,UAAM,gBAAgB;AACtB,kBAAc,MAAM,YAAY;AAChC,QAAI,kBAAkB,YAAa;AAEnC,cAAU,QAAQ,CAAC,aAAa;AAC9B,UAAI;AACF,iBAAS,aAAa,aAAa;AAAA,MACrC,SAAS,KAAK;AACZ,gBAAQ,KAAK,yBAAyB,GAAG;AAAA,MAC3C;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;;;AC/RA,IAAI,SAAS;AAAA,EACX,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AAAA,EACb,cAAc;AAAA,EACd,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA,EAKV,oBAAoB;AAAA;AAAA;AAAA;AAAA,EAKpB,2BAA2B,KAAK,KAAK;AAAA;AAAA;AAAA,EAIrC,yBAAyB;AAAA;AAAA;AAAA,EAIzB,wBAAwB;AAAA;AAAA;AAAA,EAIxB,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOtB,qBAAqB;AACvB;AAEA,IAAM,0BAA0B;AAAA,EAC9B,oBAAoB;AAAA,EACpB,2BAA2B,KAAK,KAAK;AAAA,EACrC,yBAAyB;AAAA,EACzB,wBAAwB;AAAA,EACxB,sBAAsB;AACxB;;;AFrCO,IAAM,cAAc,cAAc;;;ADVlC,SAAS,UAAU;AACxB,QAAM,UAAU,WAAW,WAAW;AACtC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,6CAA6C;AAAA,EAC/D;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../react/useAuth.js","../../react/AuthProvider.jsx","../../token.js","../../config.js"],"sourcesContent":["import { useContext } from 'react';\nimport { AuthContext } from './AuthProvider';\n\nexport function useAuth() {\n const context = useContext(AuthContext);\n if (!context) {\n throw new Error('useAuth must be used within an AuthProvider');\n }\n return context;\n}","// auth-client/react/AuthProvider.jsx\nimport React, { createContext, useState, useEffect, useRef } from 'react';\nimport { getToken, setToken, clearToken, addTokenListener } from '../token';\nimport { getConfig } from '../config';\nimport { emitAuthDiagnostic } from '../diagnostics';\nimport { \n login as coreLogin, \n logout as coreLogout,\n startSessionSecurity,\n stopSessionSecurity,\n onSessionInvalid\n} from '../core';\n\nexport const AuthContext = createContext();\n\nexport function AuthProvider({ children, onSessionExpired, manageSessionSecurity = true }) {\n const [token, setTokenState] = useState(getToken());\n const [user, setUser] = useState(null);\n const [loading, setLoading] = useState(!!token); // Loading if we have a token to validate\n const [sessionValid, setSessionValid] = useState(true);\n const sessionSecurityRef = useRef(null);\n const onSessionExpiredRef = useRef(onSessionExpired);\n const recoveryInFlightRef = useRef(null);\n const profileRecoveryTokensRef = useRef(new Set());\n const profileLoadedRef = useRef(false);\n\n useEffect(() => {\n onSessionExpiredRef.current = onSessionExpired;\n }, [onSessionExpired]);\n\n // Keep React synchronized with the canonical token singleton. Refresh\n // wrappers and sibling tabs can update the singleton without calling the\n // provider's setToken helper.\n useEffect(() => {\n const unsubscribe = addTokenListener((nextToken, previousToken) => {\n if (nextToken === previousToken) return;\n\n setTokenState(nextToken);\n if (nextToken) {\n setSessionValid(true);\n // A rotated access token is background maintenance once the profile is\n // already loaded. Keep the signed-in UI mounted while the profile is\n // refreshed; only the first token/profile load should block rendering.\n if (!profileLoadedRef.current) setLoading(true);\n } else {\n profileLoadedRef.current = false;\n setSessionValid(false);\n setUser(null);\n setLoading(false);\n }\n });\n\n return unsubscribe;\n }, []);\n\n const invalidateLocalSession = () => {\n clearToken();\n setTokenState(null);\n setUser(null);\n profileLoadedRef.current = false;\n setSessionValid(false);\n setLoading(false);\n };\n\n // Handle session invalidation (from Keycloak admin deletion or expiry)\n const handleSessionInvalid = (reason) => {\n console.log('🚨 AuthProvider: Session invalidated -', reason);\n\n if (recoveryInFlightRef.current) return recoveryInFlightRef.current;\n\n const recover = async () => {\n const callback = onSessionExpiredRef.current;\n if (typeof callback === 'function') {\n try {\n await callback(reason);\n const recoveredToken = getToken();\n if (recoveredToken) {\n setTokenState(recoveredToken);\n setSessionValid(true);\n if (!profileLoadedRef.current) setLoading(true);\n return true;\n }\n } catch (error) {\n await emitAuthDiagnostic('SESSION_RECOVERY_FAILED', 'FAILURE', reason, {\n status: error?.status || null,\n errorCode: error?.code || null,\n });\n }\n }\n\n invalidateLocalSession();\n return false;\n };\n\n recoveryInFlightRef.current = recover().finally(() => {\n recoveryInFlightRef.current = null;\n });\n\n return recoveryInFlightRef.current;\n };\n\n // Start session security on mount (when we have a token)\n useEffect(() => {\n if (manageSessionSecurity && token && !sessionSecurityRef.current) {\n console.log('🔐 AuthProvider: Starting session security');\n \n // Register session invalid handler\n const unsubscribe = onSessionInvalid(handleSessionInvalid);\n \n // Start proactive refresh + session monitoring\n sessionSecurityRef.current = startSessionSecurity(handleSessionInvalid);\n \n return () => {\n unsubscribe();\n if (sessionSecurityRef.current) {\n sessionSecurityRef.current.stopAll();\n sessionSecurityRef.current = null;\n }\n };\n }\n \n // Cleanup when token is removed\n if ((!manageSessionSecurity || !token) && sessionSecurityRef.current) {\n sessionSecurityRef.current.stopAll();\n sessionSecurityRef.current = null;\n }\n }, [manageSessionSecurity, token]);\n\n useEffect(() => {\n console.log('🔍 AuthProvider useEffect triggered:', { \n hasToken: !!token, \n tokenLength: token?.length \n });\n \n if (!token) {\n console.log('⚠️ AuthProvider: No token, setting loading=false');\n setLoading(false);\n return;\n }\n \n const { authBaseUrl } = getConfig();\n if (!authBaseUrl) {\n console.warn('AuthProvider: No authBaseUrl configured');\n setLoading(false);\n return;\n }\n\n console.log('🌐 AuthProvider: Fetching profile with token...', {\n authBaseUrl,\n tokenPreview: token.slice(0, 50) + '...'\n });\n\n let cancelled = false;\n\n const fetchProfile = async (accessToken) => {\n const response = await fetch(`${authBaseUrl}/account/profile`, {\n headers: { Authorization: `Bearer ${accessToken}` },\n credentials: 'include',\n });\n console.log('📥 Profile response status:', response.status);\n if (!response.ok) {\n const error = new Error(`Profile request failed: ${response.status}`);\n error.status = response.status;\n throw error;\n }\n return response.json();\n };\n\n const loadProfile = async () => {\n try {\n const responseBody = await fetchProfile(token);\n if (cancelled) return;\n const userData = responseBody?.data ?? responseBody;\n console.log('✅ Profile fetched successfully:', userData.email);\n setUser(userData);\n profileLoadedRef.current = true;\n setSessionValid(true);\n setLoading(false);\n } catch (error) {\n if (cancelled) return;\n\n const status = Number(error?.status || 0);\n const isUnauthorized = status === 401;\n const callback = onSessionExpiredRef.current;\n\n await emitAuthDiagnostic(\n 'PROFILE_REQUEST_FAILED',\n isUnauthorized ? 'FAILURE' : 'WARNING',\n isUnauthorized ? 'PROFILE_UNAUTHORIZED' : (status ? `PROFILE_HTTP_${status}` : 'PROFILE_NETWORK_ERROR'),\n { status, errorName: error?.name, errorCode: error?.code },\n );\n\n // Temporary profile/API failures must not destroy a valid session.\n if (!isUnauthorized) {\n console.warn('⚠️ Profile unavailable; retaining the current auth session', error);\n setLoading(false);\n return;\n }\n\n // Give the application one chance to refresh a rejected access token.\n // The token set prevents an infinite 401 -> refresh -> 401 loop.\n if (typeof callback === 'function' && !profileRecoveryTokensRef.current.has(token)) {\n profileRecoveryTokensRef.current.add(token);\n try {\n await callback('profile_unauthorized');\n const recoveredToken = getToken();\n if (recoveredToken && recoveredToken !== token) {\n setTokenState(recoveredToken);\n setSessionValid(true);\n setLoading(true);\n return;\n }\n if (recoveredToken) {\n const responseBody = await fetchProfile(recoveredToken);\n if (cancelled) return;\n const userData = responseBody?.data ?? responseBody;\n setUser(userData);\n profileLoadedRef.current = true;\n setSessionValid(true);\n setLoading(false);\n return;\n }\n } catch (recoveryError) {\n await emitAuthDiagnostic('PROFILE_RECOVERY_FAILED', 'FAILURE', 'PROFILE_REFRESH_FAILED', {\n status: recoveryError?.status || null,\n errorCode: recoveryError?.code || null,\n });\n }\n }\n\n invalidateLocalSession();\n }\n };\n\n void loadProfile();\n\n return () => {\n cancelled = true;\n };\n }, [token]);\n\n const login = (clientKey, redirectUri, state) => {\n coreLogin(clientKey, redirectUri, state);\n };\n\n const logout = async (options) => {\n // Stop session security before logout\n stopSessionSecurity();\n sessionSecurityRef.current = null;\n \n try {\n await coreLogout(options);\n } finally {\n setUser(null);\n setTokenState(null);\n setSessionValid(true);\n }\n };\n\n const value = {\n token,\n user,\n loading,\n login,\n logout,\n isAuthenticated: !!token && !!user && sessionValid,\n sessionValid,\n setUser,\n setToken: (newToken) => {\n setToken(newToken);\n setTokenState(newToken);\n setSessionValid(true);\n if (newToken && !profileLoadedRef.current) setLoading(true);\n },\n clearToken: () => {\n stopSessionSecurity();\n sessionSecurityRef.current = null;\n clearToken();\n setTokenState(null);\n setUser(null);\n profileLoadedRef.current = false;\n },\n };\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n","// auth-client/token.js - MINIMAL WORKING VERSION\n\nimport { jwtDecode } from 'jwt-decode';\n\nlet accessToken = null;\nlet idToken = null;\nconst listeners = new Set();\n\nconst REFRESH_COOKIE = 'account_refresh_token';\nconst ID_TOKEN_KEY = 'auth_id_token';\nconst COOKIE_MAX_AGE = 7 * 24 * 60 * 60;\n\nfunction secureAttribute() {\n try {\n return typeof window !== 'undefined' && window.location?.protocol === 'https:'\n ? '; Secure'\n : '';\n } catch (err) {\n return '';\n }\n}\n\n// ========== ACCESS TOKEN ==========\nfunction writeAccessToken(token) {\n if (!token) {\n try {\n localStorage.removeItem('authToken');\n } catch (err) {\n console.warn('Could not clear token from localStorage:', err);\n }\n return;\n }\n\n try {\n localStorage.setItem('authToken', token);\n } catch (err) {\n console.warn('Could not persist token to localStorage:', err);\n }\n}\n\nfunction readAccessToken() {\n try {\n return localStorage.getItem('authToken');\n } catch (err) {\n console.warn('Could not read token from localStorage:', err);\n return null;\n }\n}\n\n// ========== REFRESH TOKEN (KEEP SIMPLE) ==========\n// export function setRefreshToken(token) {\n// if (!token) {\n// clearRefreshToken();\n// return;\n// }\n\n// const expires = new Date(Date.now() + COOKIE_MAX_AGE * 1000);\n\n// try {\n// document.cookie = `${REFRESH_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax${secureAttribute()}; Expires=${expires.toUTCString()}`;\n// } catch (err) {\n// console.warn('Could not set refresh token:', err);\n// }\n// }\n\n// export function getRefreshToken() {\n// try {\n// const match = document.cookie\n// ?.split('; ')\n// ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));\n\n// if (match) {\n// return decodeURIComponent(match.split('=')[1]);\n// }\n// } catch (err) {\n// console.warn('Could not read refresh token:', err);\n// }\n\n// return null;\n// }\n\n// export function clearRefreshToken() {\n// try {\n// document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Lax${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n// } catch (err) {\n// console.warn('Could not clear refresh token:', err);\n// }\n// }\n\n// ========== ACCESS TOKEN FUNCTIONS ==========\nfunction decode(token) {\n try {\n return jwtDecode(token);\n } catch (err) {\n return null;\n }\n}\n\nfunction isExpired(token, bufferSeconds = 60) {\n if (!token) return true;\n const decoded = decode(token);\n if (!decoded?.exp) return true;\n const now = Date.now() / 1000;\n return decoded.exp < now + bufferSeconds;\n}\n\n// ========== TOKEN EXPIRY UTILITIES ==========\n// Get the exact expiry time of a token as a Date object\nexport function getTokenExpiryTime(token) {\n if (!token) return null;\n const decoded = decode(token);\n if (!decoded?.exp) return null;\n return new Date(decoded.exp * 1000);\n}\n\n// Get seconds until token expires (negative if already expired)\nexport function getTimeUntilExpiry(token) {\n if (!token) return -1;\n const decoded = decode(token);\n if (!decoded?.exp) return -1;\n const now = Date.now() / 1000;\n return Math.floor(decoded.exp - now);\n}\n\n// Check if token will expire within the next N seconds\nexport function willExpireSoon(token, withinSeconds = 60) {\n const timeLeft = getTimeUntilExpiry(token);\n return timeLeft >= 0 && timeLeft <= withinSeconds;\n}\n\nexport function setToken(token) {\n const previousToken = accessToken;\n accessToken = token || null;\n writeAccessToken(accessToken);\n\n if (previousToken !== accessToken) {\n listeners.forEach((listener) => {\n try {\n listener(accessToken, previousToken);\n } catch (err) {\n console.warn('Token listener error:', err);\n }\n });\n }\n}\n\nexport function getToken() {\n if (accessToken) return accessToken;\n accessToken = readAccessToken();\n return accessToken;\n}\n\nexport function clearToken() {\n if (!accessToken) {\n writeAccessToken(null);\n clearIdToken();\n clearRefreshToken();\n return;\n }\n\n const previousToken = accessToken;\n accessToken = null;\n writeAccessToken(null);\n clearIdToken();\n clearRefreshToken();\n\n listeners.forEach((listener) => {\n try {\n listener(null, previousToken);\n } catch (err) {\n console.warn('Token listener error:', err);\n }\n });\n}\n\n// ID tokens are retained only for the current browser tab. They are needed as\n// the OIDC logout hint, but must not be placed in localStorage or sent to\n// application APIs like access tokens are.\nexport function setIdToken(token) {\n idToken = token || null;\n try {\n if (idToken) sessionStorage.setItem(ID_TOKEN_KEY, idToken);\n else sessionStorage.removeItem(ID_TOKEN_KEY);\n } catch {\n // Storage can be unavailable in privacy-restricted browser contexts.\n }\n}\n\nexport function getIdToken() {\n if (idToken) return idToken;\n try {\n idToken = sessionStorage.getItem(ID_TOKEN_KEY) || null;\n } catch {\n idToken = null;\n }\n return idToken;\n}\n\nexport function clearIdToken() {\n idToken = null;\n try {\n sessionStorage.removeItem(ID_TOKEN_KEY);\n } catch {\n // Ignore storage cleanup failures.\n }\n}\n\n// ========== REFRESH TOKEN STORAGE ==========\n// By default:\n// HTTP → localStorage (cookies don't work cross-origin in dev)\n// HTTPS → httpOnly cookies (secure, managed by server)\n// When persistRefreshToken is enabled:\n// Always use localStorage (for local HTTPS with mkcert/self-signed certs)\nconst REFRESH_TOKEN_KEY = 'auth_refresh_token';\n\n// ✅ Persistence flag - controlled by config.persistRefreshToken\nlet _persistRefreshToken = false;\n\nexport function enableRefreshTokenPersistence(enabled) {\n _persistRefreshToken = !!enabled;\n console.log(`🔧 Refresh token persistence: ${_persistRefreshToken ? 'ENABLED' : 'DISABLED'}`);\n}\n\nfunction shouldUseLocalStorage() {\n // If persistence is forced, always use localStorage\n if (_persistRefreshToken) return true;\n // Otherwise, only use localStorage on HTTP (dev mode)\n try {\n return typeof window !== 'undefined' &&\n window.location?.protocol === 'http:';\n } catch (err) {\n return false;\n }\n}\n\nexport function setRefreshToken(token) {\n if (!token) {\n clearRefreshToken();\n return;\n }\n\n if (shouldUseLocalStorage()) {\n try {\n localStorage.setItem(REFRESH_TOKEN_KEY, token);\n console.log(`📦 Refresh token stored in localStorage (${_persistRefreshToken ? 'persistence enabled' : 'HTTP dev mode'})`);\n } catch (err) {\n console.warn('Could not store refresh token:', err);\n }\n } else {\n // HTTPS without persistence: refresh token is in httpOnly cookie only\n console.log('🔒 Refresh token managed by server httpOnly cookie (production mode)');\n }\n}\n\nexport function getRefreshToken() {\n if (shouldUseLocalStorage()) {\n try {\n const token = localStorage.getItem(REFRESH_TOKEN_KEY);\n return token;\n } catch (err) {\n console.warn('Could not read refresh token:', err);\n return null;\n }\n }\n\n // HTTPS without persistence: refresh token is in httpOnly cookie (not accessible via JS)\n // The refresh endpoint uses credentials: 'include' to send the cookie\n return null;\n}\n\nexport function clearRefreshToken() {\n // Clear localStorage (for HTTP dev)\n try {\n localStorage.removeItem(REFRESH_TOKEN_KEY);\n } catch (err) {\n // Ignore\n }\n\n // Clear cookie (for production)\n try {\n document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Strict${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n } catch (err) {\n console.warn('Could not clear refresh token cookie:', err);\n }\n\n // Clear sessionStorage\n try {\n sessionStorage.removeItem(REFRESH_COOKIE);\n } catch (err) {\n // Ignore\n }\n}\n\nexport function addTokenListener(listener) {\n if (typeof listener !== 'function') {\n throw new Error('Token listener must be a function');\n }\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nexport function removeTokenListener(listener) {\n listeners.delete(listener);\n}\n\nexport function getListenerCount() {\n return listeners.size;\n}\n\n// localStorage changes do not fire in the tab that made the change, but they\n// do fire in sibling tabs on the same origin. Keep the in-memory token cache\n// and React consumers synchronized when another tab logs in or out.\nif (typeof window !== 'undefined' && window.addEventListener) {\n window.addEventListener('storage', (event) => {\n if (event.key !== 'authToken') return;\n\n const previousToken = accessToken;\n accessToken = event.newValue || null;\n if (previousToken === accessToken) return;\n\n listeners.forEach((listener) => {\n try {\n listener(accessToken, previousToken);\n } catch (err) {\n console.warn('Token listener error:', err);\n }\n });\n });\n}\n\nexport function isAuthenticated() {\n const token = getToken();\n return !!token && !isExpired(token, 10);\n}\n","// auth-client/config.js\nimport { enableRefreshTokenPersistence } from './token.js';\n\n// ========== SESSION SECURITY CONFIGURATION ==========\n// These settings control how the auth-client handles token refresh and session validation\n// to ensure deleted sessions in Keycloak are detected quickly.\n\nlet config = {\n clientKey: null,\n authBaseUrl: null,\n redirectUri: null,\n accountUiUrl: null,\n isRouter: false, // ✅ Add router flag\n\n // ========== SESSION SECURITY SETTINGS ==========\n // Buffer time (in seconds) before token expiry to trigger proactive refresh\n // With 5-minute access tokens, refreshing 60s before expiry ensures seamless UX\n tokenRefreshBuffer: 60,\n\n // Interval (in milliseconds) for periodic session validation\n // Validates that the session still exists in Keycloak (not deleted by admin)\n // Default: 15 minutes (900000ms) - Increased from 2m to avoid frequent checks\n sessionValidationInterval: 15 * 60 * 1000,\n\n // Enable/disable periodic session validation\n // When enabled, the client will ping the server to verify session is still active\n enableSessionValidation: true,\n\n // Enable/disable proactive token refresh\n // When enabled, tokens are refreshed before they expire (using tokenRefreshBuffer)\n enableProactiveRefresh: true,\n\n // Validate session when browser tab becomes visible again\n // Catches session deletions that happened while the tab was inactive\n validateOnVisibility: true,\n\n // ========== REFRESH TOKEN PERSISTENCE ==========\n // When true, stores refresh token in localStorage even on HTTPS\n // Required for local dev with mkcert/self-signed certs where httpOnly cookies\n // may not work reliably across origins\n // ⚠️ In true production, set to false and rely on httpOnly cookies\n persistRefreshToken: false,\n};\n\nconst RUNTIME_POLICY_DEFAULTS = {\n tokenRefreshBuffer: 60,\n sessionValidationInterval: 15 * 60 * 1000,\n enableSessionValidation: true,\n enableProactiveRefresh: true,\n validateOnVisibility: true,\n};\n\nfunction validateRuntimePolicy(policy = {}) {\n const numberInRange = (value, min, max) => Number.isInteger(value) && value >= min && value <= max;\n const result = { ...RUNTIME_POLICY_DEFAULTS };\n if (numberInRange(policy.tokenRefreshBuffer, 10, 300)) result.tokenRefreshBuffer = policy.tokenRefreshBuffer;\n if (numberInRange(policy.sessionValidationInterval, 60000, 86400000)) result.sessionValidationInterval = policy.sessionValidationInterval;\n for (const key of ['enableSessionValidation', 'enableProactiveRefresh', 'validateOnVisibility']) {\n if (typeof policy[key] === 'boolean') result[key] = policy[key];\n }\n return result;\n}\n\nexport function applyRuntimePolicy(policy = {}) {\n config = { ...config, ...validateRuntimePolicy(policy) };\n return getConfig();\n}\n\nexport async function loadRuntimePolicy() {\n const { authBaseUrl, clientKey } = config;\n if (!authBaseUrl || !clientKey || typeof fetch !== 'function') return getConfig();\n try {\n const response = await fetch(`${authBaseUrl.replace(/\\/+$/, '')}/clients/${encodeURIComponent(clientKey)}/config`, {\n method: 'GET',\n credentials: 'include',\n headers: { Accept: 'application/json' },\n });\n if (!response.ok) throw new Error(`runtime policy HTTP ${response.status}`);\n const payload = await response.json();\n return applyRuntimePolicy(payload.authentication_policy || {});\n } catch (error) {\n // Runtime config is an enhancement. Safe bounded defaults keep login\n // available during a control-plane outage.\n console.warn('[auth-client] Runtime policy unavailable; using safe defaults', error.message);\n return applyRuntimePolicy({});\n }\n}\n\nexport function setConfig(customConfig = {}) {\n if (!customConfig.clientKey || !customConfig.authBaseUrl) {\n throw new Error('Missing required config: clientKey and authBaseUrl are required');\n }\n\n config = {\n ...config,\n ...customConfig,\n redirectUri: customConfig.redirectUri || window.location.origin + '/callback',\n // ✅ Auto-detect router mode\n isRouter: customConfig.isRouter || customConfig.clientKey === 'account-ui'\n };\n\n // Keep token storage synchronized when consumers reconfigure the singleton.\n enableRefreshTokenPersistence(config.persistRefreshToken);\n if (config.persistRefreshToken) {\n console.log('📦 Refresh token persistence ENABLED (localStorage on HTTPS)');\n }\n\n console.log(`🔧 Auth Client Mode: ${config.isRouter ? 'ROUTER' : 'CLIENT'}`, {\n clientKey: config.clientKey,\n isRouter: config.isRouter,\n persistRefreshToken: config.persistRefreshToken\n });\n}\n\nexport function getConfig() {\n return { ...config };\n}\n\n// ✅ Helper function\nexport function isRouterMode() {\n return config.isRouter;\n}\n"],"mappings":";AAAA,SAAS,kBAAkB;;;ACC3B,OAAO,SAAS,eAAe,UAAU,WAAW,cAAc;;;ACClE,SAAS,iBAAiB;AAE1B,IAAI,cAAc;AAElB,IAAM,YAAY,oBAAI,IAAI;AAI1B,IAAM,iBAAiB,IAAI,KAAK,KAAK;AAgTrC,IAAI,OAAO,WAAW,eAAe,OAAO,kBAAkB;AAC5D,SAAO,iBAAiB,WAAW,CAAC,UAAU;AAC5C,QAAI,MAAM,QAAQ,YAAa;AAE/B,UAAM,gBAAgB;AACtB,kBAAc,MAAM,YAAY;AAChC,QAAI,kBAAkB,YAAa;AAEnC,cAAU,QAAQ,CAAC,aAAa;AAC9B,UAAI;AACF,iBAAS,aAAa,aAAa;AAAA,MACrC,SAAS,KAAK;AACZ,gBAAQ,KAAK,yBAAyB,GAAG;AAAA,MAC3C;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;;;ACnUA,IAAI,SAAS;AAAA,EACX,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AAAA,EACb,cAAc;AAAA,EACd,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA,EAKV,oBAAoB;AAAA;AAAA;AAAA;AAAA,EAKpB,2BAA2B,KAAK,KAAK;AAAA;AAAA;AAAA,EAIrC,yBAAyB;AAAA;AAAA;AAAA,EAIzB,wBAAwB;AAAA;AAAA;AAAA,EAIxB,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOtB,qBAAqB;AACvB;AAEA,IAAM,0BAA0B;AAAA,EAC9B,oBAAoB;AAAA,EACpB,2BAA2B,KAAK,KAAK;AAAA,EACrC,yBAAyB;AAAA,EACzB,wBAAwB;AAAA,EACxB,sBAAsB;AACxB;;;AFrCO,IAAM,cAAc,cAAc;;;ADVlC,SAAS,UAAU;AACxB,QAAM,UAAU,WAAW,WAAW;AACtC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,6CAA6C;AAAA,EAC/D;AACA,SAAO;AACT;","names":[]}
@@ -38,8 +38,10 @@ var import_react4 = require("react");
38
38
  // token.js
39
39
  var import_jwt_decode = require("jwt-decode");
40
40
  var accessToken = null;
41
+ var idToken = null;
41
42
  var listeners = /* @__PURE__ */ new Set();
42
43
  var REFRESH_COOKIE = "account_refresh_token";
44
+ var ID_TOKEN_KEY = "auth_id_token";
43
45
  var COOKIE_MAX_AGE = 7 * 24 * 60 * 60;
44
46
  function secureAttribute() {
45
47
  var _a;
@@ -118,12 +120,14 @@ function getToken() {
118
120
  function clearToken() {
119
121
  if (!accessToken) {
120
122
  writeAccessToken(null);
123
+ clearIdToken();
121
124
  clearRefreshToken();
122
125
  return;
123
126
  }
124
127
  const previousToken = accessToken;
125
128
  accessToken = null;
126
129
  writeAccessToken(null);
130
+ clearIdToken();
127
131
  clearRefreshToken();
128
132
  listeners.forEach((listener) => {
129
133
  try {
@@ -133,6 +137,30 @@ function clearToken() {
133
137
  }
134
138
  });
135
139
  }
140
+ function setIdToken(token) {
141
+ idToken = token || null;
142
+ try {
143
+ if (idToken) sessionStorage.setItem(ID_TOKEN_KEY, idToken);
144
+ else sessionStorage.removeItem(ID_TOKEN_KEY);
145
+ } catch {
146
+ }
147
+ }
148
+ function getIdToken() {
149
+ if (idToken) return idToken;
150
+ try {
151
+ idToken = sessionStorage.getItem(ID_TOKEN_KEY) || null;
152
+ } catch {
153
+ idToken = null;
154
+ }
155
+ return idToken;
156
+ }
157
+ function clearIdToken() {
158
+ idToken = null;
159
+ try {
160
+ sessionStorage.removeItem(ID_TOKEN_KEY);
161
+ } catch {
162
+ }
163
+ }
136
164
  var REFRESH_TOKEN_KEY = "auth_refresh_token";
137
165
  var _persistRefreshToken = false;
138
166
  function enableRefreshTokenPersistence(enabled) {
@@ -478,6 +506,7 @@ async function logout(options = {}) {
478
506
  const { clientKey, authBaseUrl, accountUiUrl } = getConfig();
479
507
  const scope = options.scope === "client" ? "client" : "sso";
480
508
  const token = getToken();
509
+ const idToken2 = getIdToken();
481
510
  const refreshToken2 = getRefreshToken();
482
511
  console.log("\u{1F6AA} Smart Logout initiated", {
483
512
  mode: isRouterMode() ? "ROUTER" : "CLIENT",
@@ -486,6 +515,7 @@ async function logout(options = {}) {
486
515
  });
487
516
  emitAuthDiagnostic("LOGOUT_INITIATED", "PENDING", "NONE", { clientKey });
488
517
  clearToken();
518
+ clearIdToken();
489
519
  clearRefreshToken();
490
520
  sessionStorage.removeItem("originalApp");
491
521
  sessionStorage.removeItem("returnUrl");
@@ -498,7 +528,7 @@ async function logout(options = {}) {
498
528
  "Authorization": token ? `Bearer ${token}` : "",
499
529
  "Content-Type": "application/json"
500
530
  },
501
- body: JSON.stringify({ refreshToken: refreshToken2, scope })
531
+ body: JSON.stringify({ refreshToken: refreshToken2, idToken: idToken2, scope })
502
532
  });
503
533
  if (!response.ok) {
504
534
  throw new Error(`Logout failed: ${response.status}`);
@@ -532,9 +562,11 @@ async function logout(options = {}) {
532
562
  function handleCallback() {
533
563
  const params = new URLSearchParams(window.location.search);
534
564
  const accessToken2 = params.get("access_token");
565
+ const idToken2 = params.get("id_token");
535
566
  const error = params.get("error");
536
567
  console.log("\u{1F504} Callback handling:", {
537
568
  hasAccessToken: !!accessToken2,
569
+ hasIdToken: !!idToken2,
538
570
  error
539
571
  });
540
572
  if (callbackProcessed) {
@@ -562,6 +594,7 @@ function handleCallback() {
562
594
  }
563
595
  if (accessToken2) {
564
596
  setToken(accessToken2);
597
+ if (idToken2) setIdToken(idToken2);
565
598
  const refreshTokenInUrl = params.get("refresh_token");
566
599
  if (refreshTokenInUrl) {
567
600
  emitAuthDiagnostic("CALLBACK_REFRESH_TOKEN_IGNORED", "WARNING", "REFRESH_TOKEN_IN_URL", {
@@ -570,6 +603,7 @@ function handleCallback() {
570
603
  }
571
604
  const url = new URL(window.location);
572
605
  url.searchParams.delete("access_token");
606
+ url.searchParams.delete("id_token");
573
607
  url.searchParams.delete("refresh_token");
574
608
  url.searchParams.delete("state");
575
609
  url.searchParams.delete("error");
@@ -1274,6 +1308,9 @@ var auth = {
1274
1308
  getToken,
1275
1309
  setToken,
1276
1310
  clearToken,
1311
+ setIdToken,
1312
+ getIdToken,
1313
+ clearIdToken,
1277
1314
  setRefreshToken,
1278
1315
  // ✅ Refresh token for HTTP dev
1279
1316
  getRefreshToken,