@spidy092/auth-client 3.0.8 β†’ 3.1.0

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/AuthProvider.jsx","../../token.js","../../config.js","../../core.js"],"sourcesContent":["// auth-client/react/AuthProvider.jsx\nimport React, { createContext, useState, useEffect, useRef } from 'react';\nimport { getToken, setToken, clearToken } from '../token';\nimport { getConfig } from '../config';\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 }) {\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\n // Handle session invalidation (from Keycloak admin deletion or expiry)\n const handleSessionInvalid = (reason) => {\n console.log('🚨 AuthProvider: Session invalidated -', reason);\n setSessionValid(false);\n setUser(null);\n setTokenState(null);\n \n // Call custom callback if provided\n if (onSessionExpired && typeof onSessionExpired === 'function') {\n onSessionExpired(reason);\n }\n };\n\n // Start session security on mount (when we have a token)\n useEffect(() => {\n if (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 (!token && sessionSecurityRef.current) {\n sessionSecurityRef.current.stopAll();\n sessionSecurityRef.current = null;\n }\n }, [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 fetch(`${authBaseUrl}/account/profile`, {\n headers: { Authorization: `Bearer ${token}` },\n credentials: 'include',\n })\n .then(res => {\n console.log('πŸ“₯ Profile response status:', res.status);\n if (!res.ok) throw new Error('Failed to fetch user');\n return res.json();\n })\n .then(userData => {\n console.log('βœ… Profile fetched successfully:', userData.email);\n setUser(userData);\n setSessionValid(true);\n setLoading(false);\n })\n .catch(err => {\n console.error('❌ Fetch user error:', err);\n clearToken();\n setTokenState(null);\n setUser(null);\n setLoading(false);\n });\n }, [token]);\n\n const login = (clientKey, redirectUri, state) => {\n coreLogin(clientKey, redirectUri, state);\n };\n\n const logout = () => {\n // Stop session security before logout\n stopSessionSecurity();\n sessionSecurityRef.current = null;\n \n coreLogout();\n setUser(null);\n setTokenState(null);\n setSessionValid(true);\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 },\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\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\nexport function isAuthenticated() {\n const token = getToken();\n return !!token && !isExpired(token, 10);\n}\n\n\n\n","// auth-client/config.js\nimport { enableRefreshTokenPersistence } from './token';\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\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 // βœ… Wire persistRefreshToken to token.js\n if (config.persistRefreshToken) {\n enableRefreshTokenPersistence(true);\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","// auth-client/core.js - MINIMAL WORKING VERSION\n\nimport {\n setToken,\n clearToken,\n getToken,\n setRefreshToken,\n getRefreshToken,\n clearRefreshToken,\n getTimeUntilExpiry,\n} from './token';\nimport { getConfig, isRouterMode } from './config';\n\nlet callbackProcessed = false;\n\nexport function login(clientKeyArg, redirectUriArg) {\n // βœ… Reset callback state when starting new login\n resetCallbackState();\n\n const {\n clientKey: defaultClientKey,\n authBaseUrl,\n redirectUri: defaultRedirectUri,\n accountUiUrl\n } = getConfig();\n\n const clientKey = clientKeyArg || defaultClientKey;\n const redirectUri = redirectUriArg || defaultRedirectUri;\n\n console.log('πŸ”„ Smart Login initiated:', {\n mode: isRouterMode() ? 'ROUTER' : 'CLIENT',\n clientKey,\n redirectUri\n });\n\n if (!clientKey || !redirectUri) {\n throw new Error('Missing clientKey or redirectUri');\n }\n\n sessionStorage.setItem('originalApp', clientKey);\n sessionStorage.setItem('returnUrl', redirectUri);\n\n if (isRouterMode()) {\n // Router mode: Direct backend authentication\n return routerLogin(clientKey, redirectUri);\n } else {\n // Client mode: Redirect to centralized login\n return clientLogin(clientKey, redirectUri);\n }\n}\n\n// βœ… Router mode: Direct backend call\nfunction routerLogin(clientKey, redirectUri) {\n const { authBaseUrl } = getConfig();\n\n const params = new URLSearchParams();\n if (redirectUri) {\n params.append('redirect_uri', redirectUri);\n }\n const query = params.toString();\n const backendLoginUrl = `${authBaseUrl}/login/${clientKey}${query ? `?${query}` : ''}`;\n\n console.log('🏭 Router Login: Direct backend authentication', {\n clientKey,\n redirectUri,\n backendUrl: backendLoginUrl\n });\n\n window.location.href = backendLoginUrl;\n}\n\n// βœ… Client mode: Centralized login\nfunction clientLogin(clientKey, redirectUri) {\n const { accountUiUrl } = getConfig();\n\n const params = new URLSearchParams({\n client: clientKey\n });\n if (redirectUri) {\n params.append('redirect_uri', redirectUri);\n }\n const centralizedLoginUrl = `${accountUiUrl}/login?${params.toString()}`;\n\n console.log('πŸ”„ Client Login: Redirecting to centralized login', {\n clientKey,\n redirectUri,\n centralizedUrl: centralizedLoginUrl\n });\n\n window.location.href = centralizedLoginUrl;\n}\n\nexport async function logout() {\n resetCallbackState();\n\n const { clientKey, authBaseUrl, accountUiUrl } = getConfig();\n const token = getToken();\n const refreshToken = getRefreshToken();\n\n console.log('πŸšͺ Smart Logout initiated', { mode: isRouterMode() ? 'ROUTER' : 'CLIENT', clientKey });\n\n clearToken();\n clearRefreshToken();\n sessionStorage.removeItem('originalApp');\n sessionStorage.removeItem('returnUrl');\n\n // Every client β€” router or not β€” must hit the backend so it can:\n // 1. revoke the refresh token record, and\n // 2. hand back a Keycloak end-session URL so the IdP's SSO cookie is\n // actually killed. Without step 2 the browser stays signed in at\n // Keycloak and silently re-authenticates on the next login attempt.\n try {\n const response = await fetch(`${authBaseUrl}/logout/${clientKey}`, {\n method: 'POST',\n credentials: 'include',\n headers: {\n 'Authorization': token ? `Bearer ${token}` : '',\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ refreshToken })\n });\n\n const data = await response.json();\n console.log('βœ… Logout response:', data);\n\n if (data?.keycloakLogoutUrl) {\n window.location.href = data.keycloakLogoutUrl;\n return;\n }\n } catch (error) {\n console.warn('⚠️ Logout backend call failed:', error);\n }\n\n // Fallback only if the backend call failed or returned no logout URL β€”\n // the Keycloak SSO session may still be alive in this case.\n window.location.href = isRouterMode()\n ? '/login'\n : `${accountUiUrl}/login?client=${clientKey}&logout=true`;\n}\n\nexport function handleCallback() {\n const params = new URLSearchParams(window.location.search);\n const accessToken = params.get('access_token');\n const error = params.get('error');\n\n console.log('πŸ”„ Callback handling:', {\n hasAccessToken: !!accessToken,\n error\n });\n\n // βœ… Prevent duplicate callback processing\n if (callbackProcessed) {\n const existingToken = getToken();\n if (existingToken) {\n console.log('βœ… Callback already processed, returning existing token');\n return existingToken;\n }\n // Reset if no token found (might be a retry)\n callbackProcessed = false;\n }\n\n callbackProcessed = true;\n sessionStorage.removeItem('originalApp');\n sessionStorage.removeItem('returnUrl');\n\n if (error) {\n const errorDescription = params.get('error_description') || error;\n throw new Error(`Authentication failed: ${errorDescription}`);\n }\n\n if (accessToken) {\n setToken(accessToken);\n\n // βœ… Store refresh token from URL when persistence is enabled OR in HTTP dev mode\n // When persistRefreshToken is true, we always store it (needed for local HTTPS with mkcert)\n // When false, only store on HTTP (HTTPS relies on httpOnly cookies from server)\n const refreshTokenInUrl = params.get('refresh_token');\n if (refreshTokenInUrl) {\n const { persistRefreshToken } = getConfig();\n const isHttpDev = typeof window !== 'undefined' && window.location?.protocol === 'http:';\n if (persistRefreshToken || isHttpDev) {\n console.log(`πŸ“¦ Storing refresh token from callback URL (${persistRefreshToken ? 'persistence enabled' : 'HTTP dev mode'})`);\n setRefreshToken(refreshTokenInUrl);\n } else {\n console.log('πŸ”’ HTTPS mode: Refresh token is in httpOnly cookie (ignoring URL param)');\n }\n }\n\n const url = new URL(window.location);\n url.searchParams.delete('access_token');\n url.searchParams.delete('refresh_token');\n url.searchParams.delete('state');\n url.searchParams.delete('error');\n url.searchParams.delete('error_description');\n window.history.replaceState({}, '', url);\n\n console.log('βœ… Callback processed successfully, token stored');\n return accessToken;\n }\n\n throw new Error('No access token found in callback URL');\n}\n\nexport function resetCallbackState() {\n callbackProcessed = false;\n}\n\n// βœ… Add refresh lock to prevent concurrent refresh calls\nlet refreshInProgress = false;\nlet refreshPromise = null;\n\nexport async function refreshToken() {\n const { clientKey, authBaseUrl } = getConfig();\n\n // βœ… Prevent concurrent refresh calls\n if (refreshInProgress && refreshPromise) {\n console.log('πŸ”„ Token refresh already in progress, waiting...');\n return refreshPromise;\n }\n\n refreshInProgress = true;\n refreshPromise = (async () => {\n try {\n // Get stored refresh token (for HTTP development)\n const storedRefreshToken = getRefreshToken();\n\n console.log('πŸ”„ Refreshing token:', {\n clientKey,\n mode: isRouterMode() ? 'ROUTER' : 'CLIENT',\n hasStoredRefreshToken: !!storedRefreshToken\n });\n\n // Build request options - send refresh token in body and header for HTTP dev\n const requestOptions = {\n method: 'POST',\n credentials: 'include', // βœ… Include httpOnly cookies (for HTTPS)\n headers: {\n 'Content-Type': 'application/json'\n }\n };\n\n // For HTTP development, send refresh token in body ONLY (header removed per user request)\n if (storedRefreshToken) {\n // requestOptions.headers['X-Refresh-Token'] = storedRefreshToken;\n requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });\n console.log('πŸ“¦ Sending refresh token in body only (Header skipped) v3.0.2');\n }\n\n const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('❌ Token refresh failed:', response.status, errorText);\n throw new Error(`Refresh failed: ${response.status}`);\n }\n\n const data = await response.json();\n const { access_token, refresh_token: new_refresh_token } = data;\n\n if (!access_token) {\n throw new Error('No access token in refresh response');\n }\n\n // βœ… This will trigger token listeners\n setToken(access_token);\n\n // βœ… Store new refresh token if provided (token rotation)\n if (new_refresh_token) {\n setRefreshToken(new_refresh_token);\n console.log('πŸ”„ New refresh token stored from rotation');\n }\n\n console.log('βœ… Token refresh successful, listeners notified');\n return access_token;\n } catch (err) {\n console.error('❌ Token refresh error:', err);\n // Only clear tokens on definitive auth failure (server explicitly rejected).\n // Network errors / timeouts should NOT clear tokens β€” the session may still\n // be valid and the next attempt may succeed.\n const isAuthRejection = err.message?.includes('401') ||\n err.message?.includes('403') ||\n err.message?.includes('invalid_grant') ||\n err.message?.includes('Refresh failed: 4');\n if (isAuthRejection) {\n clearToken();\n clearRefreshToken();\n }\n throw err;\n } finally {\n refreshInProgress = false;\n refreshPromise = null;\n }\n })();\n\n return refreshPromise;\n}\n\nexport async function validateCurrentSession() {\n try {\n const { authBaseUrl } = getConfig();\n const token = getToken();\n\n if (!token || !authBaseUrl) {\n return false;\n }\n\n const response = await fetch(`${authBaseUrl}/account/validate-session`, {\n method: 'GET',\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/json'\n },\n credentials: 'include'\n });\n\n if (!response.ok) {\n if (response.status === 401) {\n return false;\n }\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const json = await response.json();\n // Support both wrapped { success, data: { valid } } and flat { valid } formats\n const payload = json.data || json;\n return payload.valid === true;\n } catch (error) {\n console.warn('Session validation failed:', error.message);\n if (error.message.includes('401')) {\n return false;\n }\n throw error;\n }\n}\n\n// ========== SESSION SECURITY: PROACTIVE REFRESH & VALIDATION ==========\n// These functions ensure that:\n// 1. Tokens are refreshed before they expire (proactive refresh)\n// 2. Sessions deleted in Keycloak Admin UI are detected quickly (periodic validation)\n\nlet proactiveRefreshTimer = null;\nlet sessionValidationTimer = null;\nlet visibilityHandler = null;\nlet sessionInvalidCallbacks = new Set();\n\n// Register a callback to be called when session is invalidated\nexport function onSessionInvalid(callback) {\n if (typeof callback === 'function') {\n sessionInvalidCallbacks.add(callback);\n }\n return () => sessionInvalidCallbacks.delete(callback);\n}\n\n// Notify all registered callbacks that session is invalid\nfunction notifySessionInvalid(reason = 'session_deleted') {\n console.log('🚨 Session invalidated:', reason);\n sessionInvalidCallbacks.forEach(callback => {\n try {\n callback(reason);\n } catch (err) {\n console.error('Session invalid callback error:', err);\n }\n });\n}\n\n// ========== PROACTIVE TOKEN REFRESH ==========\n// Schedules token refresh before expiry to ensure seamless UX\n\nexport function startProactiveRefresh() {\n const { enableProactiveRefresh, tokenRefreshBuffer } = getConfig();\n\n if (!enableProactiveRefresh) {\n console.log('⏸️ Proactive refresh disabled by config');\n return null;\n }\n\n // Clear any existing timer\n stopProactiveRefresh();\n\n const token = getToken();\n if (!token) {\n console.log('⏸️ No token, skipping proactive refresh setup');\n return null;\n }\n\n const timeUntilExpiry = getTimeUntilExpiry(token);\n\n if (timeUntilExpiry <= 0) {\n console.log('⚠️ Token already expired, attempting immediate refresh');\n refreshToken().catch(err => {\n console.error('❌ Immediate refresh failed:', err);\n notifySessionInvalid('token_expired');\n });\n return null;\n }\n\n // Schedule refresh for (expiry - buffer) seconds from now\n const refreshIn = Math.max(0, (timeUntilExpiry - tokenRefreshBuffer)) * 1000;\n\n console.log(`πŸ”„ Scheduling proactive refresh in ${Math.round(refreshIn / 1000)}s (token expires in ${timeUntilExpiry}s)`);\n\n proactiveRefreshTimer = setTimeout(async () => {\n try {\n console.log('πŸ”„ Proactive token refresh triggered');\n await refreshToken();\n console.log('βœ… Proactive refresh successful, scheduling next refresh');\n // Schedule next refresh after successful refresh\n startProactiveRefresh();\n } catch (err) {\n console.error('❌ Proactive refresh failed:', err);\n\n // Check if this is a permanent failure (token revoked, invalid, etc.)\n const errorMessage = err.message?.toLowerCase() || '';\n const isPermanentFailure =\n errorMessage.includes('401') ||\n errorMessage.includes('revoked') ||\n errorMessage.includes('invalid') ||\n errorMessage.includes('expired') ||\n errorMessage.includes('unauthorized');\n\n if (isPermanentFailure) {\n console.log('🚨 Token permanently invalid, triggering session expiry');\n notifySessionInvalid('refresh_token_revoked');\n } else {\n // Temporary failure (network issue), try again in 30 seconds\n proactiveRefreshTimer = setTimeout(() => startProactiveRefresh(), 30000);\n }\n }\n }, refreshIn);\n\n return proactiveRefreshTimer;\n}\n\nexport function stopProactiveRefresh() {\n if (proactiveRefreshTimer) {\n clearTimeout(proactiveRefreshTimer);\n proactiveRefreshTimer = null;\n console.log('⏹️ Proactive refresh stopped');\n }\n}\n\n// ========== PERIODIC SESSION VALIDATION ==========\n// Validates with server that session still exists in Keycloak\n// Catches session deletions from Keycloak Admin UI\n\nexport function startSessionMonitor(onInvalid) {\n const { enableSessionValidation, sessionValidationInterval, validateOnVisibility } = getConfig();\n\n if (!enableSessionValidation) {\n console.log('⏸️ Session validation disabled by config');\n return null;\n }\n\n // Register callback if provided\n if (onInvalid && typeof onInvalid === 'function') {\n sessionInvalidCallbacks.add(onInvalid);\n }\n\n // Clear any existing timer\n stopSessionMonitor();\n\n const token = getToken();\n if (!token) {\n console.log('⏸️ No token, skipping session monitor setup');\n return null;\n }\n\n console.log(`πŸ‘οΈ Starting session monitor (interval: ${sessionValidationInterval / 1000}s)`);\n\n // Track when the tab was last hidden β€” used to decide if a full\n // server-side validation is warranted after the tab becomes visible.\n let hiddenAt = null;\n\n // ── Periodic validation (catches admin-deleted sessions) ──\n sessionValidationTimer = setInterval(async () => {\n try {\n const currentToken = getToken();\n if (!currentToken) {\n console.log('⏸️ No token, stopping session validation');\n stopSessionMonitor();\n return;\n }\n\n // If token is expired, refresh first so the validation call succeeds\n const ttl = getTimeUntilExpiry(currentToken);\n if (ttl <= 0) {\n console.log('Token expired before periodic check - refreshing');\n try {\n await refreshToken();\n } catch (refreshErr) {\n console.log('Periodic refresh failed - notifying consumer');\n notifySessionInvalid('session_expired');\n return;\n }\n }\n\n console.log('Validating session...');\n const isValid = await validateCurrentSession();\n\n if (!isValid) {\n console.log('❌ Session no longer valid on server');\n notifySessionInvalid('session_deleted');\n } else {\n console.log('βœ… Session still valid');\n }\n } catch (error) {\n console.warn('⚠️ Session validation check failed:', error.message);\n // Don't invalidate on network errors - wait for next check\n }\n }, sessionValidationInterval);\n\n // ── Visibility-based validation (smart, enterprise-grade) ──\n if (validateOnVisibility && typeof document !== 'undefined') {\n visibilityHandler = async () => {\n // ── Tab hidden: record timestamp ──\n if (document.visibilityState === 'hidden') {\n hiddenAt = Date.now();\n return;\n }\n\n // ── Tab visible: decide what to do ──\n if (document.visibilityState === 'visible') {\n const currentToken = getToken();\n if (!currentToken) return;\n\n const ttl = getTimeUntilExpiry(currentToken);\n const hiddenDuration = hiddenAt ? Date.now() - hiddenAt : 0;\n hiddenAt = null;\n\n // ── Case 1: Token still valid & tab was hidden briefly ──\n // No network call needed β€” everything is fine.\n if (ttl > 0 && hiddenDuration < sessionValidationInterval) {\n console.log(`πŸ‘οΈ Tab visible β€” token valid (${Math.round(ttl)}s left), hidden for ${Math.round(hiddenDuration / 1000)}s β€” skipping validation`);\n return;\n }\n\n // ── Case 2: Token still valid BUT tab was hidden longer than validation interval ──\n // Validate with server to catch admin-deleted sessions.\n if (ttl > 0 && hiddenDuration >= sessionValidationInterval) {\n console.log(`πŸ‘οΈ Tab visible β€” token valid but hidden for ${Math.round(hiddenDuration / 1000)}s β€” server-validating`);\n try {\n const isValid = await validateCurrentSession();\n if (isValid) {\n console.log('βœ… Session confirmed valid on server');\n return;\n }\n // Server says invalid despite valid token β€” admin deleted session\n console.log('❌ Session deleted by admin while tab was hidden');\n stopSessionMonitor();\n stopProactiveRefresh();\n clearToken();\n clearRefreshToken();\n notifySessionInvalid('session_deleted_while_hidden');\n return;\n } catch (error) {\n // Network error β€” give benefit of the doubt, token is valid\n console.warn('⚠️ Server validation failed (network), token still valid β€” continuing');\n return;\n }\n }\n\n // ── Case 3: Token expired (browser throttled the refresh timer) ──\n // Try silent refresh β€” this is the most common case.\n console.log('⚠️ Token expired while tab was hidden β€” attempting silent refresh');\n try {\n await refreshToken();\n console.log('βœ… Token silently refreshed β€” session restored');\n\n // If hidden for a long time, also verify the session on server\n if (hiddenDuration >= sessionValidationInterval) {\n const isValid = await validateCurrentSession();\n if (!isValid) {\n console.log('❌ Token refreshed but session deleted on server');\n stopSessionMonitor();\n stopProactiveRefresh();\n clearToken();\n clearRefreshToken();\n notifySessionInvalid('session_deleted_while_hidden');\n }\n }\n return;\n } catch (refreshErr) {\n console.log('❌ Silent refresh failed β€” session genuinely expired:', refreshErr.message);\n }\n\n // Both refresh AND validation failed β€” session truly dead\n stopSessionMonitor();\n stopProactiveRefresh();\n clearToken();\n clearRefreshToken();\n notifySessionInvalid('session_deleted_while_hidden');\n }\n };\n document.addEventListener('visibilitychange', visibilityHandler);\n }\n\n return sessionValidationTimer;\n}\n\nexport function stopSessionMonitor() {\n if (sessionValidationTimer) {\n clearInterval(sessionValidationTimer);\n sessionValidationTimer = null;\n console.log('⏹️ Session monitor stopped');\n }\n\n if (visibilityHandler && typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', visibilityHandler);\n visibilityHandler = null;\n }\n}\n\n// ========== COMBINED SESSION SECURITY ==========\n// Start both proactive refresh and session monitoring\n\nexport function startSessionSecurity(onSessionInvalidCallback) {\n console.log('πŸ” Starting session security (proactive refresh + session monitoring)');\n\n startProactiveRefresh();\n startSessionMonitor(onSessionInvalidCallback);\n\n return {\n stopAll: () => {\n stopProactiveRefresh();\n stopSessionMonitor();\n }\n };\n}\n\nexport function stopSessionSecurity() {\n stopProactiveRefresh();\n stopSessionMonitor();\n sessionInvalidCallbacks.clear();\n console.log('πŸ” Session security stopped');\n}\n\n"],"mappings":"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","names":["refreshToken","login","logout","React"]}
1
+ {"version":3,"sources":["../../react/AuthProvider.jsx","../../token.js","../../config.js","../../diagnostics.js","../../core.js"],"sourcesContent":["// auth-client/react/AuthProvider.jsx\nimport React, { createContext, useState, useEffect, useRef } from 'react';\nimport { getToken, setToken, clearToken } from '../token';\nimport { getConfig } from '../config';\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 }) {\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\n // Handle session invalidation (from Keycloak admin deletion or expiry)\n const handleSessionInvalid = (reason) => {\n console.log('🚨 AuthProvider: Session invalidated -', reason);\n setSessionValid(false);\n setUser(null);\n setTokenState(null);\n \n // Call custom callback if provided\n if (onSessionExpired && typeof onSessionExpired === 'function') {\n onSessionExpired(reason);\n }\n };\n\n // Start session security on mount (when we have a token)\n useEffect(() => {\n if (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 (!token && sessionSecurityRef.current) {\n sessionSecurityRef.current.stopAll();\n sessionSecurityRef.current = null;\n }\n }, [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 fetch(`${authBaseUrl}/account/profile`, {\n headers: { Authorization: `Bearer ${token}` },\n credentials: 'include',\n })\n .then(res => {\n console.log('πŸ“₯ Profile response status:', res.status);\n if (!res.ok) throw new Error('Failed to fetch user');\n return res.json();\n })\n .then(responseBody => {\n const userData = responseBody?.data ?? responseBody;\n console.log('βœ… Profile fetched successfully:', userData.email);\n setUser(userData);\n setSessionValid(true);\n setLoading(false);\n })\n .catch(err => {\n console.error('❌ Fetch user error:', err);\n clearToken();\n setTokenState(null);\n setUser(null);\n setLoading(false);\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 },\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\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","const STORAGE_KEY = 'auth_diagnostic_context';\nconst LOGIN_LOCK_KEY = 'auth_login_lock';\n\nconst randomId = () => {\n if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID();\n return `auth-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n};\n\nconst fingerprint = async (value) => {\n if (!value || !globalThis.crypto?.subtle) return null;\n const bytes = new TextEncoder().encode(String(value));\n const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);\n return [...new Uint8Array(digest)].slice(0, 6)\n .map((byte) => byte.toString(16).padStart(2, '0')).join('');\n};\n\nexport function getDiagnosticContext() {\n try {\n const existing = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || 'null');\n if (existing?.correlationId) return existing;\n } catch {}\n const context = { correlationId: randomId(), startedAt: Date.now() };\n try { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(context)); } catch {}\n return context;\n}\n\nexport function resetDiagnosticContext() {\n const context = { correlationId: randomId(), startedAt: Date.now() };\n try { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(context)); } catch {}\n return context;\n}\n\nexport function diagnosticHeaders() {\n const { correlationId } = getDiagnosticContext();\n return { 'X-Correlation-ID': correlationId, 'X-Request-ID': correlationId };\n}\n\nexport function acquireLoginLock(clientKey, redirectUri, ttlMs = 5000) {\n const now = Date.now();\n const signature = `${clientKey}|${redirectUri}`;\n try {\n const current = JSON.parse(sessionStorage.getItem(LOGIN_LOCK_KEY) || 'null');\n if (current?.signature === signature && now - current.createdAt < ttlMs) return false;\n sessionStorage.setItem(LOGIN_LOCK_KEY, JSON.stringify({ signature, createdAt: now }));\n } catch {}\n return true;\n}\n\nexport function clearLoginLock() {\n try { sessionStorage.removeItem(LOGIN_LOCK_KEY); } catch {}\n}\n\nexport async function emitAuthDiagnostic(event, outcome, reasonCode, details = {}) {\n const context = getDiagnosticContext();\n const safe = {\n eventType: 'auth_client_diagnostic',\n event,\n outcome,\n reasonCode,\n correlationId: context.correlationId,\n elapsedMs: Date.now() - context.startedAt,\n clientKey: details.clientKey || null,\n status: details.status || null,\n stateFingerprint: await fingerprint(details.state),\n online: typeof navigator === 'undefined' ? null : navigator.onLine,\n };\n const method = outcome === 'FAILURE' ? 'error' : outcome === 'WARNING' ? 'warn' : 'info';\n console[method]('[auth-client]', safe);\n return safe;\n}\n","// auth-client/core.js - MINIMAL WORKING VERSION\n\nimport {\n setToken,\n clearToken,\n getToken,\n setRefreshToken,\n getRefreshToken,\n clearRefreshToken,\n getTimeUntilExpiry,\n} from './token.js';\nimport { getConfig, isRouterMode } from './config.js';\nimport {\n acquireLoginLock,\n clearLoginLock,\n diagnosticHeaders,\n emitAuthDiagnostic,\n getDiagnosticContext,\n resetDiagnosticContext,\n} from './diagnostics.js';\n\nlet callbackProcessed = false;\n\nexport function login(clientKeyArg, redirectUriArg) {\n // βœ… Reset callback state when starting new login\n resetCallbackState();\n\n const {\n clientKey: defaultClientKey,\n authBaseUrl,\n redirectUri: defaultRedirectUri,\n accountUiUrl\n } = getConfig();\n\n const clientKey = clientKeyArg || defaultClientKey;\n const redirectUri = redirectUriArg || defaultRedirectUri;\n\n if (!clientKey || !redirectUri) {\n emitAuthDiagnostic('LOGIN_REJECTED', 'FAILURE', 'CLIENT_CONFIG_MISSING', { clientKey });\n throw new Error('Missing clientKey or redirectUri');\n }\n\n if (!acquireLoginLock(clientKey, redirectUri)) {\n emitAuthDiagnostic('LOGIN_DUPLICATE_SUPPRESSED', 'WARNING', 'LOGIN_ALREADY_IN_PROGRESS', { clientKey });\n return false;\n }\n resetDiagnosticContext();\n emitAuthDiagnostic('LOGIN_INITIATED', 'PENDING', 'NONE', { clientKey });\n\n sessionStorage.setItem('originalApp', clientKey);\n sessionStorage.setItem('returnUrl', redirectUri);\n\n if (isRouterMode()) {\n // Router mode: Direct backend authentication\n return routerLogin(clientKey, redirectUri);\n } else {\n // Client mode: Redirect to centralized login\n return clientLogin(clientKey, redirectUri);\n }\n}\n\n// βœ… Router mode: Direct backend call\nfunction routerLogin(clientKey, redirectUri) {\n const { authBaseUrl } = getConfig();\n\n const params = new URLSearchParams();\n if (redirectUri) {\n params.append('redirect_uri', redirectUri);\n }\n params.append('correlation_id', getDiagnosticContext().correlationId);\n const query = params.toString();\n const backendLoginUrl = `${authBaseUrl}/login/${clientKey}${query ? `?${query}` : ''}`;\n\n window.location.href = backendLoginUrl;\n}\n\n// βœ… Client mode: Centralized login\nfunction clientLogin(clientKey, redirectUri) {\n const { accountUiUrl } = getConfig();\n\n const params = new URLSearchParams({\n client: clientKey\n });\n if (redirectUri) {\n params.append('redirect_uri', redirectUri);\n }\n const centralizedLoginUrl = `${accountUiUrl}/login?${params.toString()}`;\n\n window.location.href = centralizedLoginUrl;\n}\n\nexport async function logout(options = {}) {\n resetCallbackState();\n\n const { clientKey, authBaseUrl, accountUiUrl } = getConfig();\n const scope = options.scope === 'client' ? 'client' : 'sso';\n const token = getToken();\n const refreshToken = getRefreshToken();\n\n console.log('πŸšͺ Smart Logout initiated', {\n mode: isRouterMode() ? 'ROUTER' : 'CLIENT',\n clientKey,\n scope,\n });\n\n clearToken();\n clearRefreshToken();\n sessionStorage.removeItem('originalApp');\n sessionStorage.removeItem('returnUrl');\n\n // Every client β€” router or not β€” must hit the backend so it can:\n // 1. revoke the refresh token record, and\n // 2. hand back a Keycloak end-session URL so the IdP's SSO cookie is\n // actually killed. Without step 2 the browser stays signed in at\n // Keycloak and silently re-authenticates on the next login attempt.\n try {\n const response = await fetch(`${authBaseUrl}/logout/${clientKey}`, {\n method: 'POST',\n credentials: 'include',\n headers: {\n ...diagnosticHeaders(),\n 'Authorization': token ? `Bearer ${token}` : '',\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ refreshToken, scope })\n });\n\n if (!response.ok) {\n throw new Error(`Logout failed: ${response.status}`);\n }\n\n const data = await response.json();\n console.log('βœ… Logout response:', data);\n\n if (data?.logoutRedirectUrl) {\n window.location.replace(data.logoutRedirectUrl);\n return;\n }\n\n if (data?.keycloakLogoutUrl) {\n window.location.replace(data.keycloakLogoutUrl);\n return;\n }\n } catch (error) {\n console.warn('⚠️ Logout backend call failed:', error);\n }\n\n // Fallback only if the backend call failed or returned no logout URL β€”\n // the Keycloak SSO session may still be alive in this case.\n const fallbackUrl = isRouterMode()\n ? new URL('/login', window.location.origin)\n : new URL('/login', accountUiUrl);\n fallbackUrl.searchParams.set('logged_out', 'true');\n fallbackUrl.searchParams.set('client', clientKey);\n fallbackUrl.searchParams.set('scope', scope);\n window.location.replace(fallbackUrl.toString());\n}\n\nexport function handleCallback() {\n const params = new URLSearchParams(window.location.search);\n const accessToken = params.get('access_token');\n const error = params.get('error');\n\n console.log('πŸ”„ Callback handling:', {\n hasAccessToken: !!accessToken,\n error\n });\n\n // βœ… Prevent duplicate callback processing\n if (callbackProcessed) {\n const existingToken = getToken();\n if (existingToken) {\n console.log('βœ… Callback already processed, returning existing token');\n return existingToken;\n }\n // Reset if no token found (might be a retry)\n callbackProcessed = false;\n }\n\n callbackProcessed = true;\n clearLoginLock();\n sessionStorage.removeItem('originalApp');\n sessionStorage.removeItem('returnUrl');\n\n if (error) {\n const errorDescription = params.get('error_description') || error;\n const authError = new Error(`Authentication failed: ${errorDescription}`);\n authError.code = error;\n authError.correlationId = getDiagnosticContext().correlationId;\n emitAuthDiagnostic('CALLBACK_REJECTED', 'FAILURE', error.toUpperCase(), {\n clientKey: getConfig().clientKey,\n state: params.get('state'),\n });\n throw authError;\n }\n\n if (accessToken) {\n setToken(accessToken);\n\n // βœ… Store refresh token from URL when persistence is enabled OR in HTTP dev mode\n // When persistRefreshToken is true, we always store it (needed for local HTTPS with mkcert)\n // When false, only store on HTTP (HTTPS relies on httpOnly cookies from server)\n const refreshTokenInUrl = params.get('refresh_token');\n if (refreshTokenInUrl) {\n const { persistRefreshToken } = getConfig();\n const isHttpDev = typeof window !== 'undefined' && window.location?.protocol === 'http:';\n if (persistRefreshToken || isHttpDev) {\n console.log(`πŸ“¦ Storing refresh token from callback URL (${persistRefreshToken ? 'persistence enabled' : 'HTTP dev mode'})`);\n setRefreshToken(refreshTokenInUrl);\n } else {\n console.log('πŸ”’ HTTPS mode: Refresh token is in httpOnly cookie (ignoring URL param)');\n }\n }\n\n const url = new URL(window.location);\n url.searchParams.delete('access_token');\n url.searchParams.delete('refresh_token');\n url.searchParams.delete('state');\n url.searchParams.delete('error');\n url.searchParams.delete('error_description');\n window.history.replaceState({}, '', url);\n\n console.log('βœ… Callback processed successfully, token stored');\n emitAuthDiagnostic('CALLBACK_COMPLETED', 'SUCCESS', 'NONE', {\n clientKey: getConfig().clientKey,\n state: params.get('state'),\n });\n return accessToken;\n }\n\n emitAuthDiagnostic('CALLBACK_REJECTED', 'FAILURE', 'ACCESS_TOKEN_MISSING', {\n clientKey: getConfig().clientKey,\n state: params.get('state'),\n });\n const missingTokenError = new Error('No access token found in callback URL');\n missingTokenError.code = 'ACCESS_TOKEN_MISSING';\n throw missingTokenError;\n}\n\nexport function resetCallbackState() {\n callbackProcessed = false;\n}\n\n// βœ… Add refresh lock to prevent concurrent refresh calls\nlet refreshInProgress = false;\nlet refreshPromise = null;\n\nexport async function refreshToken() {\n const { clientKey, authBaseUrl } = getConfig();\n\n // βœ… Prevent concurrent refresh calls\n if (refreshInProgress && refreshPromise) {\n console.log('πŸ”„ Token refresh already in progress, waiting...');\n return refreshPromise;\n }\n\n refreshInProgress = true;\n refreshPromise = (async () => {\n try {\n // Get stored refresh token (for HTTP development)\n const storedRefreshToken = getRefreshToken();\n\n console.log('πŸ”„ Refreshing token:', {\n clientKey,\n mode: isRouterMode() ? 'ROUTER' : 'CLIENT',\n hasStoredRefreshToken: !!storedRefreshToken\n });\n\n // Build request options - send refresh token in body and header for HTTP dev\n const requestOptions = {\n method: 'POST',\n credentials: 'include', // βœ… Include httpOnly cookies (for HTTPS)\n headers: {\n ...diagnosticHeaders(),\n 'Content-Type': 'application/json'\n }\n };\n\n // For HTTP development, send refresh token in body ONLY (header removed per user request)\n if (storedRefreshToken) {\n // requestOptions.headers['X-Refresh-Token'] = storedRefreshToken;\n requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });\n console.log('πŸ“¦ Sending refresh token in body only (Header skipped) v3.0.2');\n }\n\n const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);\n\n if (!response.ok) {\n const errorText = await response.text();\n let serverCode = null;\n try { serverCode = JSON.parse(errorText)?.error || JSON.parse(errorText)?.code; } catch {}\n emitAuthDiagnostic('TOKEN_REFRESH_REJECTED', 'FAILURE', serverCode || `HTTP_${response.status}`, {\n clientKey,\n status: response.status,\n });\n const refreshError = new Error(`Refresh failed: ${response.status}`);\n refreshError.code = serverCode || `HTTP_${response.status}`;\n refreshError.status = response.status;\n throw refreshError;\n }\n\n const data = await response.json();\n const { access_token, refresh_token: new_refresh_token } = data;\n\n if (!access_token) {\n throw new Error('No access token in refresh response');\n }\n\n // βœ… This will trigger token listeners\n setToken(access_token);\n\n // βœ… Store new refresh token if provided (token rotation)\n if (new_refresh_token) {\n setRefreshToken(new_refresh_token);\n console.log('πŸ”„ New refresh token stored from rotation');\n }\n\n console.log('βœ… Token refresh successful, listeners notified');\n emitAuthDiagnostic('TOKEN_REFRESH_COMPLETED', 'SUCCESS', 'NONE', { clientKey });\n return access_token;\n } catch (err) {\n console.error('❌ Token refresh error:', err);\n // Only clear tokens on definitive auth failure (server explicitly rejected).\n // Network errors / timeouts should NOT clear tokens β€” the session may still\n // be valid and the next attempt may succeed.\n const isAuthRejection = err.message?.includes('401') ||\n err.message?.includes('403') ||\n err.message?.includes('invalid_grant') ||\n err.message?.includes('Refresh failed: 4');\n if (isAuthRejection) {\n clearToken();\n clearRefreshToken();\n }\n throw err;\n } finally {\n refreshInProgress = false;\n refreshPromise = null;\n }\n })();\n\n return refreshPromise;\n}\n\nexport async function validateCurrentSession() {\n try {\n const { authBaseUrl } = getConfig();\n const token = getToken();\n\n if (!token || !authBaseUrl) {\n return false;\n }\n\n const response = await fetch(`${authBaseUrl}/account/validate-session`, {\n method: 'GET',\n headers: {\n ...diagnosticHeaders(),\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/json'\n },\n credentials: 'include'\n });\n\n if (!response.ok) {\n if (response.status === 401) {\n return false;\n }\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const json = await response.json();\n // Support both wrapped { success, data: { valid } } and flat { valid } formats\n const payload = json.data || json;\n return payload.valid === true;\n } catch (error) {\n console.warn('Session validation failed:', error.message);\n if (error.message.includes('401')) {\n return false;\n }\n throw error;\n }\n}\n\n// ========== SESSION SECURITY: PROACTIVE REFRESH & VALIDATION ==========\n// These functions ensure that:\n// 1. Tokens are refreshed before they expire (proactive refresh)\n// 2. Sessions deleted in Keycloak Admin UI are detected quickly (periodic validation)\n\nlet proactiveRefreshTimer = null;\nlet sessionValidationTimer = null;\nlet visibilityHandler = null;\nlet sessionInvalidCallbacks = new Set();\n\n// Register a callback to be called when session is invalidated\nexport function onSessionInvalid(callback) {\n if (typeof callback === 'function') {\n sessionInvalidCallbacks.add(callback);\n }\n return () => sessionInvalidCallbacks.delete(callback);\n}\n\n// Notify all registered callbacks that session is invalid\nfunction notifySessionInvalid(reason = 'session_deleted') {\n console.log('🚨 Session invalidated:', reason);\n sessionInvalidCallbacks.forEach(callback => {\n try {\n callback(reason);\n } catch (err) {\n console.error('Session invalid callback error:', err);\n }\n });\n}\n\n// ========== PROACTIVE TOKEN REFRESH ==========\n// Schedules token refresh before expiry to ensure seamless UX\n\nexport function startProactiveRefresh() {\n const { enableProactiveRefresh, tokenRefreshBuffer } = getConfig();\n\n if (!enableProactiveRefresh) {\n console.log('⏸️ Proactive refresh disabled by config');\n return null;\n }\n\n // Clear any existing timer\n stopProactiveRefresh();\n\n const token = getToken();\n if (!token) {\n console.log('⏸️ No token, skipping proactive refresh setup');\n return null;\n }\n\n const timeUntilExpiry = getTimeUntilExpiry(token);\n\n if (timeUntilExpiry <= 0) {\n console.log('⚠️ Token already expired, attempting immediate refresh');\n refreshToken().catch(err => {\n console.error('❌ Immediate refresh failed:', err);\n notifySessionInvalid('token_expired');\n });\n return null;\n }\n\n // Schedule refresh for (expiry - buffer) seconds from now\n const refreshIn = Math.max(0, (timeUntilExpiry - tokenRefreshBuffer)) * 1000;\n\n console.log(`πŸ”„ Scheduling proactive refresh in ${Math.round(refreshIn / 1000)}s (token expires in ${timeUntilExpiry}s)`);\n\n proactiveRefreshTimer = setTimeout(async () => {\n try {\n console.log('πŸ”„ Proactive token refresh triggered');\n await refreshToken();\n console.log('βœ… Proactive refresh successful, scheduling next refresh');\n // Schedule next refresh after successful refresh\n startProactiveRefresh();\n } catch (err) {\n console.error('❌ Proactive refresh failed:', err);\n\n // Check if this is a permanent failure (token revoked, invalid, etc.)\n const errorMessage = err.message?.toLowerCase() || '';\n const isPermanentFailure =\n errorMessage.includes('401') ||\n errorMessage.includes('revoked') ||\n errorMessage.includes('invalid') ||\n errorMessage.includes('expired') ||\n errorMessage.includes('unauthorized');\n\n if (isPermanentFailure) {\n console.log('🚨 Token permanently invalid, triggering session expiry');\n notifySessionInvalid('refresh_token_revoked');\n } else {\n // Temporary failure (network issue), try again in 30 seconds\n proactiveRefreshTimer = setTimeout(() => startProactiveRefresh(), 30000);\n }\n }\n }, refreshIn);\n\n return proactiveRefreshTimer;\n}\n\nexport function stopProactiveRefresh() {\n if (proactiveRefreshTimer) {\n clearTimeout(proactiveRefreshTimer);\n proactiveRefreshTimer = null;\n console.log('⏹️ Proactive refresh stopped');\n }\n}\n\n// ========== PERIODIC SESSION VALIDATION ==========\n// Validates with server that session still exists in Keycloak\n// Catches session deletions from Keycloak Admin UI\n\nexport function startSessionMonitor(onInvalid) {\n const { enableSessionValidation, sessionValidationInterval, validateOnVisibility } = getConfig();\n\n if (!enableSessionValidation) {\n console.log('⏸️ Session validation disabled by config');\n return null;\n }\n\n // Register callback if provided\n if (onInvalid && typeof onInvalid === 'function') {\n sessionInvalidCallbacks.add(onInvalid);\n }\n\n // Clear any existing timer\n stopSessionMonitor();\n\n const token = getToken();\n if (!token) {\n console.log('⏸️ No token, skipping session monitor setup');\n return null;\n }\n\n console.log(`πŸ‘οΈ Starting session monitor (interval: ${sessionValidationInterval / 1000}s)`);\n\n // Track when the tab was last hidden β€” used to decide if a full\n // server-side validation is warranted after the tab becomes visible.\n let hiddenAt = null;\n\n // ── Periodic validation (catches admin-deleted sessions) ──\n sessionValidationTimer = setInterval(async () => {\n try {\n const currentToken = getToken();\n if (!currentToken) {\n console.log('⏸️ No token, stopping session validation');\n stopSessionMonitor();\n return;\n }\n\n // If token is expired, refresh first so the validation call succeeds\n const ttl = getTimeUntilExpiry(currentToken);\n if (ttl <= 0) {\n console.log('Token expired before periodic check - refreshing');\n try {\n await refreshToken();\n } catch (refreshErr) {\n console.log('Periodic refresh failed - notifying consumer');\n notifySessionInvalid('session_expired');\n return;\n }\n }\n\n console.log('Validating session...');\n const isValid = await validateCurrentSession();\n\n if (!isValid) {\n console.log('❌ Session no longer valid on server');\n notifySessionInvalid('session_deleted');\n } else {\n console.log('βœ… Session still valid');\n }\n } catch (error) {\n console.warn('⚠️ Session validation check failed:', error.message);\n // Don't invalidate on network errors - wait for next check\n }\n }, sessionValidationInterval);\n\n // ── Visibility-based validation (smart, enterprise-grade) ──\n if (validateOnVisibility && typeof document !== 'undefined') {\n visibilityHandler = async () => {\n // ── Tab hidden: record timestamp ──\n if (document.visibilityState === 'hidden') {\n hiddenAt = Date.now();\n return;\n }\n\n // ── Tab visible: decide what to do ──\n if (document.visibilityState === 'visible') {\n const currentToken = getToken();\n if (!currentToken) return;\n\n const ttl = getTimeUntilExpiry(currentToken);\n const hiddenDuration = hiddenAt ? Date.now() - hiddenAt : 0;\n hiddenAt = null;\n\n // ── Case 1: Token still valid & tab was hidden briefly ──\n // No network call needed β€” everything is fine.\n if (ttl > 0 && hiddenDuration < sessionValidationInterval) {\n console.log(`πŸ‘οΈ Tab visible β€” token valid (${Math.round(ttl)}s left), hidden for ${Math.round(hiddenDuration / 1000)}s β€” skipping validation`);\n return;\n }\n\n // ── Case 2: Token still valid BUT tab was hidden longer than validation interval ──\n // Validate with server to catch admin-deleted sessions.\n if (ttl > 0 && hiddenDuration >= sessionValidationInterval) {\n console.log(`πŸ‘οΈ Tab visible β€” token valid but hidden for ${Math.round(hiddenDuration / 1000)}s β€” server-validating`);\n try {\n const isValid = await validateCurrentSession();\n if (isValid) {\n console.log('βœ… Session confirmed valid on server');\n return;\n }\n // Server says invalid despite valid token β€” admin deleted session\n console.log('❌ Session deleted by admin while tab was hidden');\n stopSessionMonitor();\n stopProactiveRefresh();\n clearToken();\n clearRefreshToken();\n notifySessionInvalid('session_deleted_while_hidden');\n return;\n } catch (error) {\n // Network error β€” give benefit of the doubt, token is valid\n console.warn('⚠️ Server validation failed (network), token still valid β€” continuing');\n return;\n }\n }\n\n // ── Case 3: Token expired (browser throttled the refresh timer) ──\n // Try silent refresh β€” this is the most common case.\n console.log('⚠️ Token expired while tab was hidden β€” attempting silent refresh');\n try {\n await refreshToken();\n console.log('βœ… Token silently refreshed β€” session restored');\n\n // If hidden for a long time, also verify the session on server\n if (hiddenDuration >= sessionValidationInterval) {\n const isValid = await validateCurrentSession();\n if (!isValid) {\n console.log('❌ Token refreshed but session deleted on server');\n stopSessionMonitor();\n stopProactiveRefresh();\n clearToken();\n clearRefreshToken();\n notifySessionInvalid('session_deleted_while_hidden');\n }\n }\n return;\n } catch (refreshErr) {\n console.log('❌ Silent refresh failed β€” session genuinely expired:', refreshErr.message);\n }\n\n // Both refresh AND validation failed β€” session truly dead\n stopSessionMonitor();\n stopProactiveRefresh();\n clearToken();\n clearRefreshToken();\n notifySessionInvalid('session_deleted_while_hidden');\n }\n };\n document.addEventListener('visibilitychange', visibilityHandler);\n }\n\n return sessionValidationTimer;\n}\n\nexport function stopSessionMonitor() {\n if (sessionValidationTimer) {\n clearInterval(sessionValidationTimer);\n sessionValidationTimer = null;\n console.log('⏹️ Session monitor stopped');\n }\n\n if (visibilityHandler && typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', visibilityHandler);\n visibilityHandler = null;\n }\n}\n\n// ========== COMBINED SESSION SECURITY ==========\n// Start both proactive refresh and session monitoring\n\nexport function startSessionSecurity(onSessionInvalidCallback) {\n console.log('πŸ” Starting session security (proactive refresh + session monitoring)');\n\n startProactiveRefresh();\n startSessionMonitor(onSessionInvalidCallback);\n\n return {\n stopAll: () => {\n stopProactiveRefresh();\n stopSessionMonitor();\n }\n };\n}\n\nexport function stopSessionSecurity() {\n stopProactiveRefresh();\n stopSessionMonitor();\n sessionInvalidCallbacks.clear();\n console.log('πŸ” Session security stopped');\n}\n"],"mappings":"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","names":["refreshToken","login","logout","React"]}
@@ -143,6 +143,21 @@ function clearRefreshToken() {
143
143
  } catch (err) {
144
144
  }
145
145
  }
146
+ if (typeof window !== "undefined" && window.addEventListener) {
147
+ window.addEventListener("storage", (event) => {
148
+ if (event.key !== "authToken") return;
149
+ const previousToken = accessToken;
150
+ accessToken = event.newValue || null;
151
+ if (previousToken === accessToken) return;
152
+ listeners.forEach((listener) => {
153
+ try {
154
+ listener(accessToken, previousToken);
155
+ } catch (err) {
156
+ console.warn("Token listener error:", err);
157
+ }
158
+ });
159
+ });
160
+ }
146
161
 
147
162
  // config.js
148
163
  var config = {
@@ -183,6 +198,76 @@ function isRouterMode() {
183
198
  return config.isRouter;
184
199
  }
185
200
 
201
+ // diagnostics.js
202
+ var STORAGE_KEY = "auth_diagnostic_context";
203
+ var LOGIN_LOCK_KEY = "auth_login_lock";
204
+ var randomId = () => {
205
+ var _a;
206
+ if ((_a = globalThis.crypto) == null ? void 0 : _a.randomUUID) return globalThis.crypto.randomUUID();
207
+ return `auth-${Date.now()}-${Math.random().toString(36).slice(2)}`;
208
+ };
209
+ var fingerprint = async (value) => {
210
+ var _a;
211
+ if (!value || !((_a = globalThis.crypto) == null ? void 0 : _a.subtle)) return null;
212
+ const bytes = new TextEncoder().encode(String(value));
213
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
214
+ return [...new Uint8Array(digest)].slice(0, 6).map((byte) => byte.toString(16).padStart(2, "0")).join("");
215
+ };
216
+ function getDiagnosticContext() {
217
+ try {
218
+ const existing = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || "null");
219
+ if (existing == null ? void 0 : existing.correlationId) return existing;
220
+ } catch {
221
+ }
222
+ const context = { correlationId: randomId(), startedAt: Date.now() };
223
+ try {
224
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(context));
225
+ } catch {
226
+ }
227
+ return context;
228
+ }
229
+ function resetDiagnosticContext() {
230
+ const context = { correlationId: randomId(), startedAt: Date.now() };
231
+ try {
232
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(context));
233
+ } catch {
234
+ }
235
+ return context;
236
+ }
237
+ function diagnosticHeaders() {
238
+ const { correlationId } = getDiagnosticContext();
239
+ return { "X-Correlation-ID": correlationId, "X-Request-ID": correlationId };
240
+ }
241
+ function acquireLoginLock(clientKey, redirectUri, ttlMs = 5e3) {
242
+ const now = Date.now();
243
+ const signature = `${clientKey}|${redirectUri}`;
244
+ try {
245
+ const current = JSON.parse(sessionStorage.getItem(LOGIN_LOCK_KEY) || "null");
246
+ if ((current == null ? void 0 : current.signature) === signature && now - current.createdAt < ttlMs) return false;
247
+ sessionStorage.setItem(LOGIN_LOCK_KEY, JSON.stringify({ signature, createdAt: now }));
248
+ } catch {
249
+ }
250
+ return true;
251
+ }
252
+ async function emitAuthDiagnostic(event, outcome, reasonCode, details = {}) {
253
+ const context = getDiagnosticContext();
254
+ const safe = {
255
+ eventType: "auth_client_diagnostic",
256
+ event,
257
+ outcome,
258
+ reasonCode,
259
+ correlationId: context.correlationId,
260
+ elapsedMs: Date.now() - context.startedAt,
261
+ clientKey: details.clientKey || null,
262
+ status: details.status || null,
263
+ stateFingerprint: await fingerprint(details.state),
264
+ online: typeof navigator === "undefined" ? null : navigator.onLine
265
+ };
266
+ const method = outcome === "FAILURE" ? "error" : outcome === "WARNING" ? "warn" : "info";
267
+ console[method]("[auth-client]", safe);
268
+ return safe;
269
+ }
270
+
186
271
  // core.js
187
272
  var callbackProcessed = false;
188
273
  function login(clientKeyArg, redirectUriArg) {
@@ -195,14 +280,16 @@ function login(clientKeyArg, redirectUriArg) {
195
280
  } = getConfig();
196
281
  const clientKey = clientKeyArg || defaultClientKey;
197
282
  const redirectUri = redirectUriArg || defaultRedirectUri;
198
- console.log("\u{1F504} Smart Login initiated:", {
199
- mode: isRouterMode() ? "ROUTER" : "CLIENT",
200
- clientKey,
201
- redirectUri
202
- });
203
283
  if (!clientKey || !redirectUri) {
284
+ emitAuthDiagnostic("LOGIN_REJECTED", "FAILURE", "CLIENT_CONFIG_MISSING", { clientKey });
204
285
  throw new Error("Missing clientKey or redirectUri");
205
286
  }
287
+ if (!acquireLoginLock(clientKey, redirectUri)) {
288
+ emitAuthDiagnostic("LOGIN_DUPLICATE_SUPPRESSED", "WARNING", "LOGIN_ALREADY_IN_PROGRESS", { clientKey });
289
+ return false;
290
+ }
291
+ resetDiagnosticContext();
292
+ emitAuthDiagnostic("LOGIN_INITIATED", "PENDING", "NONE", { clientKey });
206
293
  sessionStorage.setItem("originalApp", clientKey);
207
294
  sessionStorage.setItem("returnUrl", redirectUri);
208
295
  if (isRouterMode()) {
@@ -217,13 +304,9 @@ function routerLogin(clientKey, redirectUri) {
217
304
  if (redirectUri) {
218
305
  params.append("redirect_uri", redirectUri);
219
306
  }
307
+ params.append("correlation_id", getDiagnosticContext().correlationId);
220
308
  const query = params.toString();
221
309
  const backendLoginUrl = `${authBaseUrl}/login/${clientKey}${query ? `?${query}` : ""}`;
222
- console.log("\u{1F3ED} Router Login: Direct backend authentication", {
223
- clientKey,
224
- redirectUri,
225
- backendUrl: backendLoginUrl
226
- });
227
310
  window.location.href = backendLoginUrl;
228
311
  }
229
312
  function clientLogin(clientKey, redirectUri) {
@@ -235,19 +318,19 @@ function clientLogin(clientKey, redirectUri) {
235
318
  params.append("redirect_uri", redirectUri);
236
319
  }
237
320
  const centralizedLoginUrl = `${accountUiUrl}/login?${params.toString()}`;
238
- console.log("\u{1F504} Client Login: Redirecting to centralized login", {
239
- clientKey,
240
- redirectUri,
241
- centralizedUrl: centralizedLoginUrl
242
- });
243
321
  window.location.href = centralizedLoginUrl;
244
322
  }
245
- async function logout() {
323
+ async function logout(options = {}) {
246
324
  resetCallbackState();
247
325
  const { clientKey, authBaseUrl, accountUiUrl } = getConfig();
326
+ const scope = options.scope === "client" ? "client" : "sso";
248
327
  const token = getToken();
249
328
  const refreshToken2 = getRefreshToken();
250
- console.log("\u{1F6AA} Smart Logout initiated", { mode: isRouterMode() ? "ROUTER" : "CLIENT", clientKey });
329
+ console.log("\u{1F6AA} Smart Logout initiated", {
330
+ mode: isRouterMode() ? "ROUTER" : "CLIENT",
331
+ clientKey,
332
+ scope
333
+ });
251
334
  clearToken();
252
335
  clearRefreshToken();
253
336
  sessionStorage.removeItem("originalApp");
@@ -257,21 +340,33 @@ async function logout() {
257
340
  method: "POST",
258
341
  credentials: "include",
259
342
  headers: {
343
+ ...diagnosticHeaders(),
260
344
  "Authorization": token ? `Bearer ${token}` : "",
261
345
  "Content-Type": "application/json"
262
346
  },
263
- body: JSON.stringify({ refreshToken: refreshToken2 })
347
+ body: JSON.stringify({ refreshToken: refreshToken2, scope })
264
348
  });
349
+ if (!response.ok) {
350
+ throw new Error(`Logout failed: ${response.status}`);
351
+ }
265
352
  const data = await response.json();
266
353
  console.log("\u2705 Logout response:", data);
354
+ if (data == null ? void 0 : data.logoutRedirectUrl) {
355
+ window.location.replace(data.logoutRedirectUrl);
356
+ return;
357
+ }
267
358
  if (data == null ? void 0 : data.keycloakLogoutUrl) {
268
- window.location.href = data.keycloakLogoutUrl;
359
+ window.location.replace(data.keycloakLogoutUrl);
269
360
  return;
270
361
  }
271
362
  } catch (error) {
272
363
  console.warn("\u26A0\uFE0F Logout backend call failed:", error);
273
364
  }
274
- window.location.href = isRouterMode() ? "/login" : `${accountUiUrl}/login?client=${clientKey}&logout=true`;
365
+ const fallbackUrl = isRouterMode() ? new URL("/login", window.location.origin) : new URL("/login", accountUiUrl);
366
+ fallbackUrl.searchParams.set("logged_out", "true");
367
+ fallbackUrl.searchParams.set("client", clientKey);
368
+ fallbackUrl.searchParams.set("scope", scope);
369
+ window.location.replace(fallbackUrl.toString());
275
370
  }
276
371
  function resetCallbackState() {
277
372
  callbackProcessed = false;
@@ -286,7 +381,7 @@ async function refreshToken() {
286
381
  }
287
382
  refreshInProgress = true;
288
383
  refreshPromise = (async () => {
289
- var _a, _b, _c, _d;
384
+ var _a, _b, _c, _d, _e, _f;
290
385
  try {
291
386
  const storedRefreshToken = getRefreshToken();
292
387
  console.log("\u{1F504} Refreshing token:", {
@@ -299,6 +394,7 @@ async function refreshToken() {
299
394
  credentials: "include",
300
395
  // βœ… Include httpOnly cookies (for HTTPS)
301
396
  headers: {
397
+ ...diagnosticHeaders(),
302
398
  "Content-Type": "application/json"
303
399
  }
304
400
  };
@@ -309,8 +405,19 @@ async function refreshToken() {
309
405
  const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);
310
406
  if (!response.ok) {
311
407
  const errorText = await response.text();
312
- console.error("\u274C Token refresh failed:", response.status, errorText);
313
- throw new Error(`Refresh failed: ${response.status}`);
408
+ let serverCode = null;
409
+ try {
410
+ serverCode = ((_a = JSON.parse(errorText)) == null ? void 0 : _a.error) || ((_b = JSON.parse(errorText)) == null ? void 0 : _b.code);
411
+ } catch {
412
+ }
413
+ emitAuthDiagnostic("TOKEN_REFRESH_REJECTED", "FAILURE", serverCode || `HTTP_${response.status}`, {
414
+ clientKey,
415
+ status: response.status
416
+ });
417
+ const refreshError = new Error(`Refresh failed: ${response.status}`);
418
+ refreshError.code = serverCode || `HTTP_${response.status}`;
419
+ refreshError.status = response.status;
420
+ throw refreshError;
314
421
  }
315
422
  const data = await response.json();
316
423
  const { access_token, refresh_token: new_refresh_token } = data;
@@ -323,10 +430,11 @@ async function refreshToken() {
323
430
  console.log("\u{1F504} New refresh token stored from rotation");
324
431
  }
325
432
  console.log("\u2705 Token refresh successful, listeners notified");
433
+ emitAuthDiagnostic("TOKEN_REFRESH_COMPLETED", "SUCCESS", "NONE", { clientKey });
326
434
  return access_token;
327
435
  } catch (err) {
328
436
  console.error("\u274C Token refresh error:", err);
329
- const isAuthRejection = ((_a = err.message) == null ? void 0 : _a.includes("401")) || ((_b = err.message) == null ? void 0 : _b.includes("403")) || ((_c = err.message) == null ? void 0 : _c.includes("invalid_grant")) || ((_d = err.message) == null ? void 0 : _d.includes("Refresh failed: 4"));
437
+ const isAuthRejection = ((_c = err.message) == null ? void 0 : _c.includes("401")) || ((_d = err.message) == null ? void 0 : _d.includes("403")) || ((_e = err.message) == null ? void 0 : _e.includes("invalid_grant")) || ((_f = err.message) == null ? void 0 : _f.includes("Refresh failed: 4"));
330
438
  if (isAuthRejection) {
331
439
  clearToken();
332
440
  clearRefreshToken();
@@ -349,6 +457,7 @@ async function validateCurrentSession() {
349
457
  const response = await fetch(`${authBaseUrl}/account/validate-session`, {
350
458
  method: "GET",
351
459
  headers: {
460
+ ...diagnosticHeaders(),
352
461
  "Authorization": `Bearer ${token}`,
353
462
  "Content-Type": "application/json"
354
463
  },
@@ -647,7 +756,8 @@ function AuthProvider({ children, onSessionExpired }) {
647
756
  console.log("\u{1F4E5} Profile response status:", res.status);
648
757
  if (!res.ok) throw new Error("Failed to fetch user");
649
758
  return res.json();
650
- }).then((userData) => {
759
+ }).then((responseBody) => {
760
+ const userData = (responseBody == null ? void 0 : responseBody.data) ?? responseBody;
651
761
  console.log("\u2705 Profile fetched successfully:", userData.email);
652
762
  setUser(userData);
653
763
  setSessionValid(true);
@@ -663,13 +773,16 @@ function AuthProvider({ children, onSessionExpired }) {
663
773
  const login2 = (clientKey, redirectUri, state) => {
664
774
  login(clientKey, redirectUri, state);
665
775
  };
666
- const logout2 = () => {
776
+ const logout2 = async (options) => {
667
777
  stopSessionSecurity();
668
778
  sessionSecurityRef.current = null;
669
- logout();
670
- setUser(null);
671
- setTokenState(null);
672
- setSessionValid(true);
779
+ try {
780
+ await logout(options);
781
+ } finally {
782
+ setUser(null);
783
+ setTokenState(null);
784
+ setSessionValid(true);
785
+ }
673
786
  };
674
787
  const value = {
675
788
  token,