@spidy092/auth-client 3.1.2 → 3.1.3

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 } 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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,gBAA2B;;;ACC3B,mBAAkE;;;ACClE,wBAA0B;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;;;AF9BO,IAAM,kBAAc,4BAAc;;;ADTlC,SAAS,UAAU;AACxB,QAAM,cAAU,0BAAW,WAAW;AACtC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,6CAA6C;AAAA,EAC/D;AACA,SAAO;AACT;","names":["import_react"]}
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 } 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\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;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,gBAA2B;;;ACC3B,mBAAkE;;;ACClE,wBAA0B;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;;;AFtCO,IAAM,kBAAc,4BAAc;;;ADTlC,SAAS,UAAU;AACxB,QAAM,cAAU,0BAAW,WAAW;AACtC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,6CAA6C;AAAA,EAC/D;AACA,SAAO;AACT;","names":["import_react"]}
@@ -57,6 +57,13 @@ var config = {
57
57
  // ⚠️ In true production, set to false and rely on httpOnly cookies
58
58
  persistRefreshToken: false
59
59
  };
60
+ var RUNTIME_POLICY_DEFAULTS = {
61
+ tokenRefreshBuffer: 60,
62
+ sessionValidationInterval: 15 * 60 * 1e3,
63
+ enableSessionValidation: true,
64
+ enableProactiveRefresh: true,
65
+ validateOnVisibility: true
66
+ };
60
67
 
61
68
  // react/AuthProvider.jsx
62
69
  var AuthContext = createContext();
@@ -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 } 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"],"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;;;AF9BO,IAAM,cAAc,cAAc;;;ADTlC,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 } 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\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;;;AFtCO,IAAM,cAAc,cAAc;;;ADTlC,SAAS,UAAU;AACxB,QAAM,UAAU,WAAW,WAAW;AACtC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,6CAA6C;AAAA,EAC/D;AACA,SAAO;AACT;","names":[]}
@@ -254,6 +254,44 @@ var config = {
254
254
  // ⚠️ In true production, set to false and rely on httpOnly cookies
255
255
  persistRefreshToken: false
256
256
  };
257
+ var RUNTIME_POLICY_DEFAULTS = {
258
+ tokenRefreshBuffer: 60,
259
+ sessionValidationInterval: 15 * 60 * 1e3,
260
+ enableSessionValidation: true,
261
+ enableProactiveRefresh: true,
262
+ validateOnVisibility: true
263
+ };
264
+ function validateRuntimePolicy(policy = {}) {
265
+ const numberInRange = (value, min, max) => Number.isInteger(value) && value >= min && value <= max;
266
+ const result = { ...RUNTIME_POLICY_DEFAULTS };
267
+ if (numberInRange(policy.tokenRefreshBuffer, 10, 300)) result.tokenRefreshBuffer = policy.tokenRefreshBuffer;
268
+ if (numberInRange(policy.sessionValidationInterval, 6e4, 864e5)) result.sessionValidationInterval = policy.sessionValidationInterval;
269
+ for (const key of ["enableSessionValidation", "enableProactiveRefresh", "validateOnVisibility"]) {
270
+ if (typeof policy[key] === "boolean") result[key] = policy[key];
271
+ }
272
+ return result;
273
+ }
274
+ function applyRuntimePolicy(policy = {}) {
275
+ config = { ...config, ...validateRuntimePolicy(policy) };
276
+ return getConfig();
277
+ }
278
+ async function loadRuntimePolicy() {
279
+ const { authBaseUrl, clientKey } = config;
280
+ if (!authBaseUrl || !clientKey || typeof fetch !== "function") return getConfig();
281
+ try {
282
+ const response = await fetch(`${authBaseUrl.replace(/\/+$/, "")}/clients/${encodeURIComponent(clientKey)}/config`, {
283
+ method: "GET",
284
+ credentials: "include",
285
+ headers: { Accept: "application/json" }
286
+ });
287
+ if (!response.ok) throw new Error(`runtime policy HTTP ${response.status}`);
288
+ const payload = await response.json();
289
+ return applyRuntimePolicy(payload.authentication_policy || {});
290
+ } catch (error) {
291
+ console.warn("[auth-client] Runtime policy unavailable; using safe defaults", error.message);
292
+ return applyRuntimePolicy({});
293
+ }
294
+ }
257
295
  function setConfig(customConfig = {}) {
258
296
  if (!customConfig.clientKey || !customConfig.authBaseUrl) {
259
297
  throw new Error("Missing required config: clientKey and authBaseUrl are required");
@@ -360,7 +398,7 @@ async function emitAuthDiagnostic(event, outcome, reasonCode, details = {}) {
360
398
 
361
399
  // core.js
362
400
  var callbackProcessed = false;
363
- function login(clientKeyArg, redirectUriArg) {
401
+ function login(clientKeyArg, redirectUriArg, options = {}) {
364
402
  resetCallbackState();
365
403
  const {
366
404
  clientKey: defaultClientKey,
@@ -383,23 +421,24 @@ function login(clientKeyArg, redirectUriArg) {
383
421
  sessionStorage.setItem("originalApp", clientKey);
384
422
  sessionStorage.setItem("returnUrl", redirectUri);
385
423
  if (isRouterMode()) {
386
- return routerLogin(clientKey, redirectUri);
424
+ return routerLogin(clientKey, redirectUri, options);
387
425
  } else {
388
- return clientLogin(clientKey, redirectUri);
426
+ return clientLogin(clientKey, redirectUri, options);
389
427
  }
390
428
  }
391
- function routerLogin(clientKey, redirectUri) {
429
+ function routerLogin(clientKey, redirectUri, options = {}) {
392
430
  const { authBaseUrl } = getConfig();
393
431
  const params = new URLSearchParams();
394
432
  if (redirectUri) {
395
433
  params.append("redirect_uri", redirectUri);
396
434
  }
435
+ if (options.switchAccount || options.switch_account) params.append("switch_account", "true");
397
436
  params.append("correlation_id", getDiagnosticContext().correlationId);
398
437
  const query = params.toString();
399
438
  const backendLoginUrl = `${authBaseUrl}/login/${clientKey}${query ? `?${query}` : ""}`;
400
439
  window.location.href = backendLoginUrl;
401
440
  }
402
- function clientLogin(clientKey, redirectUri) {
441
+ function clientLogin(clientKey, redirectUri, options = {}) {
403
442
  const { accountUiUrl } = getConfig();
404
443
  const params = new URLSearchParams({
405
444
  client: clientKey
@@ -407,6 +446,7 @@ function clientLogin(clientKey, redirectUri) {
407
446
  if (redirectUri) {
408
447
  params.append("redirect_uri", redirectUri);
409
448
  }
449
+ if (options.switchAccount || options.switch_account) params.append("switch_account", "true");
410
450
  const centralizedLoginUrl = `${accountUiUrl}/login?${params.toString()}`;
411
451
  window.location.href = centralizedLoginUrl;
412
452
  }
@@ -532,6 +572,28 @@ function resetCallbackState() {
532
572
  }
533
573
  var refreshInProgress = false;
534
574
  var refreshPromise = null;
575
+ async function withCrossTabRefreshLock(clientKey, tokenBeforeRefresh, refreshRequest) {
576
+ var _a;
577
+ const lockName = `auth-refresh-${clientKey}`;
578
+ const run = async () => {
579
+ try {
580
+ const persistedToken = localStorage.getItem("authToken");
581
+ if (tokenBeforeRefresh && persistedToken && persistedToken !== tokenBeforeRefresh) {
582
+ setToken(persistedToken);
583
+ emitAuthDiagnostic("TOKEN_REFRESH_REUSED_CROSS_TAB", "SUCCESS", "CROSS_TAB_ROTATION", {
584
+ clientKey
585
+ });
586
+ return persistedToken;
587
+ }
588
+ } catch {
589
+ }
590
+ return refreshRequest();
591
+ };
592
+ if (typeof navigator !== "undefined" && ((_a = navigator.locks) == null ? void 0 : _a.request)) {
593
+ return navigator.locks.request(lockName, run);
594
+ }
595
+ return run();
596
+ }
535
597
  async function refreshToken() {
536
598
  const { clientKey, authBaseUrl } = getConfig();
537
599
  if (refreshInProgress && refreshPromise) {
@@ -540,70 +602,74 @@ async function refreshToken() {
540
602
  }
541
603
  refreshInProgress = true;
542
604
  refreshPromise = (async () => {
543
- var _a, _b, _c, _d, _e, _f;
544
- try {
545
- const storedRefreshToken = getRefreshToken();
546
- console.log("\u{1F504} Refreshing token:", {
547
- clientKey,
548
- mode: isRouterMode() ? "ROUTER" : "CLIENT",
549
- hasStoredRefreshToken: !!storedRefreshToken
550
- });
551
- const requestOptions = {
552
- method: "POST",
553
- credentials: "include",
554
- // ✅ Include httpOnly cookies (for HTTPS)
555
- headers: {
556
- ...diagnosticHeaders(),
557
- "Content-Type": "application/json"
558
- }
559
- };
560
- if (storedRefreshToken) {
561
- requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });
562
- console.log("\u{1F4E6} Sending refresh token in body only (Header skipped) v3.0.2");
563
- }
564
- const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);
565
- if (!response.ok) {
566
- const errorText = await response.text();
567
- let serverCode = null;
568
- try {
569
- serverCode = ((_a = JSON.parse(errorText)) == null ? void 0 : _a.error) || ((_b = JSON.parse(errorText)) == null ? void 0 : _b.code);
570
- } catch {
571
- }
572
- emitAuthDiagnostic("TOKEN_REFRESH_REJECTED", "FAILURE", serverCode || `HTTP_${response.status}`, {
605
+ const tokenBeforeRefresh = getToken();
606
+ const refreshRequest = async () => {
607
+ var _a, _b, _c, _d, _e, _f;
608
+ try {
609
+ const storedRefreshToken = getRefreshToken();
610
+ console.log("\u{1F504} Refreshing token:", {
573
611
  clientKey,
574
- status: response.status
612
+ mode: isRouterMode() ? "ROUTER" : "CLIENT",
613
+ hasStoredRefreshToken: !!storedRefreshToken
575
614
  });
576
- const refreshError = new Error(`Refresh failed: ${response.status}`);
577
- refreshError.code = serverCode || `HTTP_${response.status}`;
578
- refreshError.status = response.status;
579
- throw refreshError;
580
- }
581
- const data = await response.json();
582
- const { access_token, refresh_token: new_refresh_token } = data;
583
- if (!access_token) {
584
- throw new Error("No access token in refresh response");
585
- }
586
- setToken(access_token);
587
- if (new_refresh_token) {
588
- setRefreshToken(new_refresh_token);
589
- console.log("\u{1F504} New refresh token stored from rotation");
590
- }
591
- console.log("\u2705 Token refresh successful, listeners notified");
592
- emitAuthDiagnostic("TOKEN_REFRESH_COMPLETED", "SUCCESS", "NONE", { clientKey });
593
- return access_token;
594
- } catch (err) {
595
- console.error("\u274C Token refresh error:", err);
596
- 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"));
597
- if (isAuthRejection) {
598
- clearToken();
599
- clearRefreshToken();
615
+ const requestOptions = {
616
+ method: "POST",
617
+ credentials: "include",
618
+ // ✅ Include httpOnly cookies (for HTTPS)
619
+ headers: {
620
+ ...diagnosticHeaders(),
621
+ "Content-Type": "application/json"
622
+ }
623
+ };
624
+ if (storedRefreshToken) {
625
+ requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });
626
+ console.log("\u{1F4E6} Sending refresh token in body only (Header skipped) v3.0.2");
627
+ }
628
+ const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);
629
+ if (!response.ok) {
630
+ const errorText = await response.text();
631
+ let serverCode = null;
632
+ try {
633
+ serverCode = ((_a = JSON.parse(errorText)) == null ? void 0 : _a.error) || ((_b = JSON.parse(errorText)) == null ? void 0 : _b.code);
634
+ } catch {
635
+ }
636
+ emitAuthDiagnostic("TOKEN_REFRESH_REJECTED", "FAILURE", serverCode || `HTTP_${response.status}`, {
637
+ clientKey,
638
+ status: response.status
639
+ });
640
+ const refreshError = new Error(`Refresh failed: ${response.status}`);
641
+ refreshError.code = serverCode || `HTTP_${response.status}`;
642
+ refreshError.status = response.status;
643
+ throw refreshError;
644
+ }
645
+ const data = await response.json();
646
+ const { access_token, refresh_token: new_refresh_token } = data;
647
+ if (!access_token) {
648
+ throw new Error("No access token in refresh response");
649
+ }
650
+ setToken(access_token);
651
+ if (new_refresh_token) {
652
+ setRefreshToken(new_refresh_token);
653
+ console.log("\u{1F504} New refresh token stored from rotation");
654
+ }
655
+ console.log("\u2705 Token refresh successful, listeners notified");
656
+ emitAuthDiagnostic("TOKEN_REFRESH_COMPLETED", "SUCCESS", "NONE", { clientKey });
657
+ return access_token;
658
+ } catch (err) {
659
+ console.error("\u274C Token refresh error:", err);
660
+ 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"));
661
+ if (isAuthRejection) {
662
+ clearToken();
663
+ clearRefreshToken();
664
+ }
665
+ throw err;
600
666
  }
601
- throw err;
602
- } finally {
603
- refreshInProgress = false;
604
- refreshPromise = null;
605
- }
606
- })();
667
+ };
668
+ return withCrossTabRefreshLock(clientKey, tokenBeforeRefresh, refreshRequest);
669
+ })().finally(() => {
670
+ refreshInProgress = false;
671
+ refreshPromise = null;
672
+ });
607
673
  return refreshPromise;
608
674
  }
609
675
  async function validateCurrentSession() {
@@ -1168,6 +1234,8 @@ var PreferencesContext = (0, import_react3.createContext)(null);
1168
1234
  var auth = {
1169
1235
  // 🔧 Config
1170
1236
  setConfig,
1237
+ applyRuntimePolicy,
1238
+ loadRuntimePolicy,
1171
1239
  getConfig,
1172
1240
  isRouterMode,
1173
1241
  // 🔐 Core flows