@spidy092/auth-client 3.0.0 → 3.0.2

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.
package/dist/api.cjs CHANGED
@@ -34,39 +34,6 @@ __export(api_exports, {
34
34
  module.exports = __toCommonJS(api_exports);
35
35
  var import_axios = __toESM(require("axios"), 1);
36
36
 
37
- // config.js
38
- var config = {
39
- clientKey: null,
40
- authBaseUrl: null,
41
- redirectUri: null,
42
- accountUiUrl: null,
43
- isRouter: false,
44
- // ✅ Add router flag
45
- // ========== SESSION SECURITY SETTINGS ==========
46
- // Buffer time (in seconds) before token expiry to trigger proactive refresh
47
- // With 5-minute access tokens, refreshing 60s before expiry ensures seamless UX
48
- tokenRefreshBuffer: 60,
49
- // Interval (in milliseconds) for periodic session validation
50
- // Validates that the session still exists in Keycloak (not deleted by admin)
51
- // Default: 2 minutes (120000ms) - balances responsiveness vs server load
52
- sessionValidationInterval: 2 * 60 * 1e3,
53
- // Enable/disable periodic session validation
54
- // When enabled, the client will ping the server to verify session is still active
55
- enableSessionValidation: true,
56
- // Enable/disable proactive token refresh
57
- // When enabled, tokens are refreshed before they expire (using tokenRefreshBuffer)
58
- enableProactiveRefresh: true,
59
- // Validate session when browser tab becomes visible again
60
- // Catches session deletions that happened while the tab was inactive
61
- validateOnVisibility: true
62
- };
63
- function getConfig() {
64
- return { ...config };
65
- }
66
- function isRouterMode() {
67
- return config.isRouter;
68
- }
69
-
70
37
  // token.js
71
38
  var import_jwt_decode = require("jwt-decode");
72
39
  var accessToken = null;
@@ -142,8 +109,10 @@ function clearToken() {
142
109
  });
143
110
  }
144
111
  var REFRESH_TOKEN_KEY = "auth_refresh_token";
145
- function isHttpDevelopment() {
112
+ var _persistRefreshToken = false;
113
+ function shouldUseLocalStorage() {
146
114
  var _a;
115
+ if (_persistRefreshToken) return true;
147
116
  try {
148
117
  return typeof window !== "undefined" && ((_a = window.location) == null ? void 0 : _a.protocol) === "http:";
149
118
  } catch (err) {
@@ -155,10 +124,10 @@ function setRefreshToken(token) {
155
124
  clearRefreshToken();
156
125
  return;
157
126
  }
158
- if (isHttpDevelopment()) {
127
+ if (shouldUseLocalStorage()) {
159
128
  try {
160
129
  localStorage.setItem(REFRESH_TOKEN_KEY, token);
161
- console.log("\u{1F4E6} Refresh token stored in localStorage (HTTP dev mode)");
130
+ console.log(`\u{1F4E6} Refresh token stored in localStorage (${_persistRefreshToken ? "persistence enabled" : "HTTP dev mode"})`);
162
131
  } catch (err) {
163
132
  console.warn("Could not store refresh token:", err);
164
133
  }
@@ -167,7 +136,7 @@ function setRefreshToken(token) {
167
136
  }
168
137
  }
169
138
  function getRefreshToken() {
170
- if (isHttpDevelopment()) {
139
+ if (shouldUseLocalStorage()) {
171
140
  try {
172
141
  const token = localStorage.getItem(REFRESH_TOKEN_KEY);
173
142
  return token;
@@ -194,6 +163,45 @@ function clearRefreshToken() {
194
163
  }
195
164
  }
196
165
 
166
+ // config.js
167
+ var config = {
168
+ clientKey: null,
169
+ authBaseUrl: null,
170
+ redirectUri: null,
171
+ accountUiUrl: null,
172
+ isRouter: false,
173
+ // ✅ Add router flag
174
+ // ========== SESSION SECURITY SETTINGS ==========
175
+ // Buffer time (in seconds) before token expiry to trigger proactive refresh
176
+ // With 5-minute access tokens, refreshing 60s before expiry ensures seamless UX
177
+ tokenRefreshBuffer: 60,
178
+ // Interval (in milliseconds) for periodic session validation
179
+ // Validates that the session still exists in Keycloak (not deleted by admin)
180
+ // Default: 15 minutes (900000ms) - Increased from 2m to avoid frequent checks
181
+ sessionValidationInterval: 15 * 60 * 1e3,
182
+ // Enable/disable periodic session validation
183
+ // When enabled, the client will ping the server to verify session is still active
184
+ enableSessionValidation: true,
185
+ // Enable/disable proactive token refresh
186
+ // When enabled, tokens are refreshed before they expire (using tokenRefreshBuffer)
187
+ enableProactiveRefresh: true,
188
+ // Validate session when browser tab becomes visible again
189
+ // Catches session deletions that happened while the tab was inactive
190
+ validateOnVisibility: true,
191
+ // ========== REFRESH TOKEN PERSISTENCE ==========
192
+ // When true, stores refresh token in localStorage even on HTTPS
193
+ // Required for local dev with mkcert/self-signed certs where httpOnly cookies
194
+ // may not work reliably across origins
195
+ // ⚠️ In true production, set to false and rely on httpOnly cookies
196
+ persistRefreshToken: false
197
+ };
198
+ function getConfig() {
199
+ return { ...config };
200
+ }
201
+ function isRouterMode() {
202
+ return config.isRouter;
203
+ }
204
+
197
205
  // core.js
198
206
  var refreshInProgress = false;
199
207
  var refreshPromise = null;
package/dist/api.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../api.js","../config.js","../token.js","../core.js"],"sourcesContent":["// auth-client/api.js\nimport axios from 'axios';\nimport { getConfig } from './config';\nimport { getToken, setToken, clearToken } from './token';\nimport { refreshToken as performRefresh } from './core';\n\nconst api = axios.create({\n withCredentials: true,\n});\n\napi.interceptors.request.use((config) => {\n const runtimeConfig = getConfig();\n\n if (!config.baseURL) {\n\n config.baseURL = runtimeConfig?.authBaseUrl || 'http://auth.local.test:4000/auth';\n }\n\n if (!config.headers) {\n config.headers = {};\n }\n\n if (runtimeConfig?.clientKey && !config.headers['X-Client-Key']) {\n config.headers['X-Client-Key'] = runtimeConfig.clientKey;\n }\n\n const token = getToken();\n if (token) {\n config.headers.Authorization = `Bearer ${token}`;\n }\n\n return config;\n});\n\nlet refreshPromise = null;\n\napi.interceptors.response.use(\n (response) => response,\n async (error) => {\n const { response, config } = error || {};\n\n if (!response || !config) {\n return Promise.reject(error);\n }\n\n if (response.status !== 401 || config._retry) {\n return Promise.reject(error);\n }\n\n config._retry = true;\n\n if (!refreshPromise) {\n refreshPromise = performRefresh()\n .then((newToken) => {\n refreshPromise = null;\n if (newToken) {\n setToken(newToken);\n }\n return newToken;\n })\n .catch((refreshError) => {\n refreshPromise = null;\n clearToken();\n throw refreshError;\n });\n }\n\n try {\n const refreshedToken = await refreshPromise;\n\n if (refreshedToken) {\n config.headers.Authorization = `Bearer ${refreshedToken}`;\n return api(config);\n }\n } catch (refreshErr) {\n return Promise.reject(refreshErr);\n }\n\n return Promise.reject(error);\n }\n);\n\napi.validateSession = async () => {\n try {\n const response = await api.get('/account/validate-session');\n return response.data.valid;\n } catch (err) {\n if (err.response?.status === 401) {\n return false;\n }\n throw err;\n }\n};\n\nexport default api;\n","// auth-client/config.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: 2 minutes (120000ms) - balances responsiveness vs server load\n sessionValidationInterval: 2 * 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\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 console.log(`🔧 Auth Client Mode: ${config.isRouter ? 'ROUTER' : 'CLIENT'}`, {\n clientKey: config.clientKey,\n isRouter: config.isRouter\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/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 FOR HTTP DEVELOPMENT ==========\n// In production (HTTPS), refresh tokens should ONLY be in httpOnly cookies set by server\n// For HTTP development (cross-origin cookies don't work), we store in localStorage\nconst REFRESH_TOKEN_KEY = 'auth_refresh_token';\n\nfunction isHttpDevelopment() {\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 // For HTTP development, store in localStorage (since httpOnly cookies don't work cross-origin)\n if (isHttpDevelopment()) {\n try {\n localStorage.setItem(REFRESH_TOKEN_KEY, token);\n console.log('📦 Refresh token stored in localStorage (HTTP dev mode)');\n } catch (err) {\n console.warn('Could not store refresh token:', err);\n }\n } else {\n // In production (HTTPS), refresh token should be in httpOnly cookie only\n console.log('🔒 Refresh token managed by server httpOnly cookie (production mode)');\n }\n}\n\nexport function getRefreshToken() {\n // For HTTP development, read from localStorage\n if (isHttpDevelopment()) {\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 // In production, 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, 15);\n}\n\n\n\n\n// // auth-client/token.js\n// import { jwtDecode } from 'jwt-decode';\n\n// let accessToken = null;\n// const listeners = new Set();\n\n// const REFRESH_COOKIE = 'account_refresh_token';\n// const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days in seconds\n\n// function secureAttribute() {\n// try {\n// return typeof window !== 'undefined' && window.location?.protocol === 'https:'\n// ? '; Secure'\n// : '';\n// } catch (err) {\n// return '';\n// }\n// }\n\n// function 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\n// function 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// function decode(token) {\n// try {\n// return jwtDecode(token);\n// } catch (err) {\n// return null;\n// }\n// }\n\n// function 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// export 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\n// export function getToken() {\n// if (accessToken) return accessToken;\n// accessToken = readAccessToken();\n// return accessToken;\n// }\n\n// export 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// 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// try {\n// document.cookie = `${REFRESH_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Strict${secureAttribute()}; Expires=${expires.toUTCString()}`;\n// } catch (err) {\n// console.warn('Could not persist refresh token cookie:', err);\n// }\n\n// try {\n// sessionStorage.setItem(REFRESH_COOKIE, token);\n// } catch (err) {\n// console.warn('Could not persist refresh token to sessionStorage:', err);\n// }\n// }\n\n// export function getRefreshToken() {\n// // Prefer cookie to align with server expectations\n// let cookieMatch = null;\n\n// try {\n// cookieMatch = document.cookie\n// ?.split('; ')\n// ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));\n// } catch (err) {\n// cookieMatch = null;\n// }\n\n// if (cookieMatch) {\n// return decodeURIComponent(cookieMatch.split('=')[1]);\n// }\n\n// try {\n// return sessionStorage.getItem(REFRESH_COOKIE);\n// } catch (err) {\n// console.warn('Could not read refresh token from sessionStorage:', err);\n// return null;\n// }\n// }\n\n// export function clearRefreshToken() {\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// try {\n// sessionStorage.removeItem(REFRESH_COOKIE);\n// } catch (err) {\n// console.warn('Could not clear refresh token from sessionStorage:', err);\n// }\n// }\n\n// export 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\n// export function removeTokenListener(listener) {\n// listeners.delete(listener);\n// }\n\n// export function getListenerCount() {\n// return listeners.size;\n// }\n\n// export function isAuthenticated() {\n// const token = getToken();\n// return !!token && !isExpired(token, 15);\n// }\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 function logout() {\n resetCallbackState();\n\n const { clientKey, authBaseUrl, accountUiUrl } = getConfig();\n const token = getToken();\n\n console.log('🚪 Smart Logout initiated');\n\n clearToken();\n clearRefreshToken();\n sessionStorage.removeItem('originalApp');\n sessionStorage.removeItem('returnUrl');\n\n if (isRouterMode()) {\n return routerLogout(clientKey, authBaseUrl, accountUiUrl, token);\n } else {\n return clientLogout(clientKey, accountUiUrl);\n }\n}\n\nasync function routerLogout(clientKey, authBaseUrl, accountUiUrl, token) {\n console.log('🏭 Router Logout');\n\n const refreshToken = getRefreshToken();\n\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({\n refreshToken: refreshToken\n })\n });\n\n const data = await response.json();\n console.log('✅ Logout response:', data);\n\n clearRefreshToken();\n clearToken();\n\n // Skip Keycloak confirmation page - redirect directly to login\n // Backend has already revoked the session/tokens\n console.log('🔄 Redirecting to login (skipping Keycloak confirmation)');\n window.location.href = '/login';\n\n } catch (error) {\n console.warn('⚠️ Logout failed:', error);\n clearRefreshToken();\n clearToken();\n // Still redirect to login even on error\n window.location.href = '/login';\n }\n}\n\nfunction clientLogout(clientKey, accountUiUrl) {\n console.log('🔄 Client Logout');\n const logoutUrl = `${accountUiUrl}/login?client=${clientKey}&logout=true`;\n window.location.href = logoutUrl;\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 // ✅ For HTTP development, store refresh token from URL\n // In HTTPS production, refresh token is in httpOnly cookie (more secure)\n const refreshTokenInUrl = params.get('refresh_token');\n if (refreshTokenInUrl) {\n const isHttpDev = typeof window !== 'undefined' && window.location?.protocol === 'http:';\n if (isHttpDev) {\n console.log('📦 HTTP dev mode: Storing refresh token from callback URL');\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 and header\n if (storedRefreshToken) {\n requestOptions.headers['X-Refresh-Token'] = storedRefreshToken;\n requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });\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 // ✅ This will trigger token listeners\n clearToken();\n clearRefreshToken();\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 data = await response.json();\n return data.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 // Periodic validation\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 console.log('🔍 Validating session...');\n const isValid = await validateCurrentSession();\n\n if (!isValid) {\n console.log('❌ Session no longer valid on server');\n stopSessionMonitor();\n stopProactiveRefresh();\n clearToken();\n clearRefreshToken();\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 (when tab becomes visible again)\n if (validateOnVisibility && typeof document !== 'undefined') {\n visibilityHandler = async () => {\n if (document.visibilityState === 'visible') {\n const currentToken = getToken();\n if (!currentToken) return;\n\n console.log('👁️ Tab visible - validating session');\n try {\n const isValid = await validateCurrentSession();\n if (!isValid) {\n console.log('❌ Session expired while tab was hidden');\n stopSessionMonitor();\n stopProactiveRefresh();\n clearToken();\n clearRefreshToken();\n notifySessionInvalid('session_deleted_while_hidden');\n }\n } catch (error) {\n console.warn('⚠️ Visibility check failed:', error.message);\n }\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":["axios","config","refreshPromise"]}
1
+ {"version":3,"sources":["../api.js","../token.js","../config.js","../core.js"],"sourcesContent":["// auth-client/api.js\nimport axios from 'axios';\nimport { getConfig } from './config';\nimport { getToken, setToken, clearToken } from './token';\nimport { refreshToken as performRefresh } from './core';\n\nconst api = axios.create({\n withCredentials: true,\n});\n\napi.interceptors.request.use((config) => {\n const runtimeConfig = getConfig();\n\n if (!config.baseURL) {\n\n config.baseURL = runtimeConfig?.authBaseUrl || 'http://auth.local.test:4000/auth';\n }\n\n if (!config.headers) {\n config.headers = {};\n }\n\n if (runtimeConfig?.clientKey && !config.headers['X-Client-Key']) {\n config.headers['X-Client-Key'] = runtimeConfig.clientKey;\n }\n\n const token = getToken();\n if (token) {\n config.headers.Authorization = `Bearer ${token}`;\n }\n\n return config;\n});\n\nlet refreshPromise = null;\n\napi.interceptors.response.use(\n (response) => response,\n async (error) => {\n const { response, config } = error || {};\n\n if (!response || !config) {\n return Promise.reject(error);\n }\n\n if (response.status !== 401 || config._retry) {\n return Promise.reject(error);\n }\n\n config._retry = true;\n\n if (!refreshPromise) {\n refreshPromise = performRefresh()\n .then((newToken) => {\n refreshPromise = null;\n if (newToken) {\n setToken(newToken);\n }\n return newToken;\n })\n .catch((refreshError) => {\n refreshPromise = null;\n clearToken();\n throw refreshError;\n });\n }\n\n try {\n const refreshedToken = await refreshPromise;\n\n if (refreshedToken) {\n config.headers.Authorization = `Bearer ${refreshedToken}`;\n return api(config);\n }\n } catch (refreshErr) {\n return Promise.reject(refreshErr);\n }\n\n return Promise.reject(error);\n }\n);\n\napi.validateSession = async () => {\n try {\n const response = await api.get('/account/validate-session');\n return response.data.valid;\n } catch (err) {\n if (err.response?.status === 401) {\n return false;\n }\n throw err;\n }\n};\n\nexport default api;\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, 15);\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 function logout() {\n resetCallbackState();\n\n const { clientKey, authBaseUrl, accountUiUrl } = getConfig();\n const token = getToken();\n\n console.log('🚪 Smart Logout initiated');\n\n clearToken();\n clearRefreshToken();\n sessionStorage.removeItem('originalApp');\n sessionStorage.removeItem('returnUrl');\n\n if (isRouterMode()) {\n return routerLogout(clientKey, authBaseUrl, accountUiUrl, token);\n } else {\n return clientLogout(clientKey, accountUiUrl);\n }\n}\n\nasync function routerLogout(clientKey, authBaseUrl, accountUiUrl, token) {\n console.log('🏭 Router Logout');\n\n const refreshToken = getRefreshToken();\n\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({\n refreshToken: refreshToken\n })\n });\n\n const data = await response.json();\n console.log('✅ Logout response:', data);\n\n clearRefreshToken();\n clearToken();\n\n // Skip Keycloak confirmation page - redirect directly to login\n // Backend has already revoked the session/tokens\n console.log('🔄 Redirecting to login (skipping Keycloak confirmation)');\n window.location.href = '/login';\n\n } catch (error) {\n console.warn('⚠️ Logout failed:', error);\n clearRefreshToken();\n clearToken();\n // Still redirect to login even on error\n window.location.href = '/login';\n }\n}\n\nfunction clientLogout(clientKey, accountUiUrl) {\n console.log('🔄 Client Logout');\n const logoutUrl = `${accountUiUrl}/login?client=${clientKey}&logout=true`;\n window.location.href = logoutUrl;\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 and header\n if (storedRefreshToken) {\n requestOptions.headers['X-Refresh-Token'] = storedRefreshToken;\n requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });\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 // ✅ This will trigger token listeners\n clearToken();\n clearRefreshToken();\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 data = await response.json();\n return data.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 // Periodic validation\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 console.log('🔍 Validating session...');\n const isValid = await validateCurrentSession();\n\n if (!isValid) {\n console.log('❌ Session no longer valid on server');\n stopSessionMonitor();\n stopProactiveRefresh();\n clearToken();\n clearRefreshToken();\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 (when tab becomes visible again)\n if (validateOnVisibility && typeof document !== 'undefined') {\n visibilityHandler = async () => {\n if (document.visibilityState === 'visible') {\n const currentToken = getToken();\n if (!currentToken) return;\n\n console.log('👁️ Tab visible - validating session');\n try {\n const isValid = await validateCurrentSession();\n if (!isValid) {\n console.log('❌ Session expired while tab was hidden');\n stopSessionMonitor();\n stopProactiveRefresh();\n clearToken();\n clearRefreshToken();\n notifySessionInvalid('session_deleted_while_hidden');\n }\n } catch (error) {\n console.warn('⚠️ Visibility check failed:', error.message);\n }\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":["axios","config","refreshPromise"]}
package/dist/api.js CHANGED
@@ -1,39 +1,6 @@
1
1
  // api.js
2
2
  import axios from "axios";
3
3
 
4
- // config.js
5
- var config = {
6
- clientKey: null,
7
- authBaseUrl: null,
8
- redirectUri: null,
9
- accountUiUrl: null,
10
- isRouter: false,
11
- // ✅ Add router flag
12
- // ========== SESSION SECURITY SETTINGS ==========
13
- // Buffer time (in seconds) before token expiry to trigger proactive refresh
14
- // With 5-minute access tokens, refreshing 60s before expiry ensures seamless UX
15
- tokenRefreshBuffer: 60,
16
- // Interval (in milliseconds) for periodic session validation
17
- // Validates that the session still exists in Keycloak (not deleted by admin)
18
- // Default: 2 minutes (120000ms) - balances responsiveness vs server load
19
- sessionValidationInterval: 2 * 60 * 1e3,
20
- // Enable/disable periodic session validation
21
- // When enabled, the client will ping the server to verify session is still active
22
- enableSessionValidation: true,
23
- // Enable/disable proactive token refresh
24
- // When enabled, tokens are refreshed before they expire (using tokenRefreshBuffer)
25
- enableProactiveRefresh: true,
26
- // Validate session when browser tab becomes visible again
27
- // Catches session deletions that happened while the tab was inactive
28
- validateOnVisibility: true
29
- };
30
- function getConfig() {
31
- return { ...config };
32
- }
33
- function isRouterMode() {
34
- return config.isRouter;
35
- }
36
-
37
4
  // token.js
38
5
  import { jwtDecode } from "jwt-decode";
39
6
  var accessToken = null;
@@ -109,8 +76,10 @@ function clearToken() {
109
76
  });
110
77
  }
111
78
  var REFRESH_TOKEN_KEY = "auth_refresh_token";
112
- function isHttpDevelopment() {
79
+ var _persistRefreshToken = false;
80
+ function shouldUseLocalStorage() {
113
81
  var _a;
82
+ if (_persistRefreshToken) return true;
114
83
  try {
115
84
  return typeof window !== "undefined" && ((_a = window.location) == null ? void 0 : _a.protocol) === "http:";
116
85
  } catch (err) {
@@ -122,10 +91,10 @@ function setRefreshToken(token) {
122
91
  clearRefreshToken();
123
92
  return;
124
93
  }
125
- if (isHttpDevelopment()) {
94
+ if (shouldUseLocalStorage()) {
126
95
  try {
127
96
  localStorage.setItem(REFRESH_TOKEN_KEY, token);
128
- console.log("\u{1F4E6} Refresh token stored in localStorage (HTTP dev mode)");
97
+ console.log(`\u{1F4E6} Refresh token stored in localStorage (${_persistRefreshToken ? "persistence enabled" : "HTTP dev mode"})`);
129
98
  } catch (err) {
130
99
  console.warn("Could not store refresh token:", err);
131
100
  }
@@ -134,7 +103,7 @@ function setRefreshToken(token) {
134
103
  }
135
104
  }
136
105
  function getRefreshToken() {
137
- if (isHttpDevelopment()) {
106
+ if (shouldUseLocalStorage()) {
138
107
  try {
139
108
  const token = localStorage.getItem(REFRESH_TOKEN_KEY);
140
109
  return token;
@@ -161,6 +130,45 @@ function clearRefreshToken() {
161
130
  }
162
131
  }
163
132
 
133
+ // config.js
134
+ var config = {
135
+ clientKey: null,
136
+ authBaseUrl: null,
137
+ redirectUri: null,
138
+ accountUiUrl: null,
139
+ isRouter: false,
140
+ // ✅ Add router flag
141
+ // ========== SESSION SECURITY SETTINGS ==========
142
+ // Buffer time (in seconds) before token expiry to trigger proactive refresh
143
+ // With 5-minute access tokens, refreshing 60s before expiry ensures seamless UX
144
+ tokenRefreshBuffer: 60,
145
+ // Interval (in milliseconds) for periodic session validation
146
+ // Validates that the session still exists in Keycloak (not deleted by admin)
147
+ // Default: 15 minutes (900000ms) - Increased from 2m to avoid frequent checks
148
+ sessionValidationInterval: 15 * 60 * 1e3,
149
+ // Enable/disable periodic session validation
150
+ // When enabled, the client will ping the server to verify session is still active
151
+ enableSessionValidation: true,
152
+ // Enable/disable proactive token refresh
153
+ // When enabled, tokens are refreshed before they expire (using tokenRefreshBuffer)
154
+ enableProactiveRefresh: true,
155
+ // Validate session when browser tab becomes visible again
156
+ // Catches session deletions that happened while the tab was inactive
157
+ validateOnVisibility: true,
158
+ // ========== REFRESH TOKEN PERSISTENCE ==========
159
+ // When true, stores refresh token in localStorage even on HTTPS
160
+ // Required for local dev with mkcert/self-signed certs where httpOnly cookies
161
+ // may not work reliably across origins
162
+ // ⚠️ In true production, set to false and rely on httpOnly cookies
163
+ persistRefreshToken: false
164
+ };
165
+ function getConfig() {
166
+ return { ...config };
167
+ }
168
+ function isRouterMode() {
169
+ return config.isRouter;
170
+ }
171
+
164
172
  // core.js
165
173
  var refreshInProgress = false;
166
174
  var refreshPromise = null;
package/dist/api.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../api.js","../config.js","../token.js","../core.js"],"sourcesContent":["// auth-client/api.js\nimport axios from 'axios';\nimport { getConfig } from './config';\nimport { getToken, setToken, clearToken } from './token';\nimport { refreshToken as performRefresh } from './core';\n\nconst api = axios.create({\n withCredentials: true,\n});\n\napi.interceptors.request.use((config) => {\n const runtimeConfig = getConfig();\n\n if (!config.baseURL) {\n\n config.baseURL = runtimeConfig?.authBaseUrl || 'http://auth.local.test:4000/auth';\n }\n\n if (!config.headers) {\n config.headers = {};\n }\n\n if (runtimeConfig?.clientKey && !config.headers['X-Client-Key']) {\n config.headers['X-Client-Key'] = runtimeConfig.clientKey;\n }\n\n const token = getToken();\n if (token) {\n config.headers.Authorization = `Bearer ${token}`;\n }\n\n return config;\n});\n\nlet refreshPromise = null;\n\napi.interceptors.response.use(\n (response) => response,\n async (error) => {\n const { response, config } = error || {};\n\n if (!response || !config) {\n return Promise.reject(error);\n }\n\n if (response.status !== 401 || config._retry) {\n return Promise.reject(error);\n }\n\n config._retry = true;\n\n if (!refreshPromise) {\n refreshPromise = performRefresh()\n .then((newToken) => {\n refreshPromise = null;\n if (newToken) {\n setToken(newToken);\n }\n return newToken;\n })\n .catch((refreshError) => {\n refreshPromise = null;\n clearToken();\n throw refreshError;\n });\n }\n\n try {\n const refreshedToken = await refreshPromise;\n\n if (refreshedToken) {\n config.headers.Authorization = `Bearer ${refreshedToken}`;\n return api(config);\n }\n } catch (refreshErr) {\n return Promise.reject(refreshErr);\n }\n\n return Promise.reject(error);\n }\n);\n\napi.validateSession = async () => {\n try {\n const response = await api.get('/account/validate-session');\n return response.data.valid;\n } catch (err) {\n if (err.response?.status === 401) {\n return false;\n }\n throw err;\n }\n};\n\nexport default api;\n","// auth-client/config.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: 2 minutes (120000ms) - balances responsiveness vs server load\n sessionValidationInterval: 2 * 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\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 console.log(`🔧 Auth Client Mode: ${config.isRouter ? 'ROUTER' : 'CLIENT'}`, {\n clientKey: config.clientKey,\n isRouter: config.isRouter\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/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 FOR HTTP DEVELOPMENT ==========\n// In production (HTTPS), refresh tokens should ONLY be in httpOnly cookies set by server\n// For HTTP development (cross-origin cookies don't work), we store in localStorage\nconst REFRESH_TOKEN_KEY = 'auth_refresh_token';\n\nfunction isHttpDevelopment() {\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 // For HTTP development, store in localStorage (since httpOnly cookies don't work cross-origin)\n if (isHttpDevelopment()) {\n try {\n localStorage.setItem(REFRESH_TOKEN_KEY, token);\n console.log('📦 Refresh token stored in localStorage (HTTP dev mode)');\n } catch (err) {\n console.warn('Could not store refresh token:', err);\n }\n } else {\n // In production (HTTPS), refresh token should be in httpOnly cookie only\n console.log('🔒 Refresh token managed by server httpOnly cookie (production mode)');\n }\n}\n\nexport function getRefreshToken() {\n // For HTTP development, read from localStorage\n if (isHttpDevelopment()) {\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 // In production, 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, 15);\n}\n\n\n\n\n// // auth-client/token.js\n// import { jwtDecode } from 'jwt-decode';\n\n// let accessToken = null;\n// const listeners = new Set();\n\n// const REFRESH_COOKIE = 'account_refresh_token';\n// const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days in seconds\n\n// function secureAttribute() {\n// try {\n// return typeof window !== 'undefined' && window.location?.protocol === 'https:'\n// ? '; Secure'\n// : '';\n// } catch (err) {\n// return '';\n// }\n// }\n\n// function 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\n// function 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// function decode(token) {\n// try {\n// return jwtDecode(token);\n// } catch (err) {\n// return null;\n// }\n// }\n\n// function 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// export 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\n// export function getToken() {\n// if (accessToken) return accessToken;\n// accessToken = readAccessToken();\n// return accessToken;\n// }\n\n// export 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// 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// try {\n// document.cookie = `${REFRESH_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Strict${secureAttribute()}; Expires=${expires.toUTCString()}`;\n// } catch (err) {\n// console.warn('Could not persist refresh token cookie:', err);\n// }\n\n// try {\n// sessionStorage.setItem(REFRESH_COOKIE, token);\n// } catch (err) {\n// console.warn('Could not persist refresh token to sessionStorage:', err);\n// }\n// }\n\n// export function getRefreshToken() {\n// // Prefer cookie to align with server expectations\n// let cookieMatch = null;\n\n// try {\n// cookieMatch = document.cookie\n// ?.split('; ')\n// ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));\n// } catch (err) {\n// cookieMatch = null;\n// }\n\n// if (cookieMatch) {\n// return decodeURIComponent(cookieMatch.split('=')[1]);\n// }\n\n// try {\n// return sessionStorage.getItem(REFRESH_COOKIE);\n// } catch (err) {\n// console.warn('Could not read refresh token from sessionStorage:', err);\n// return null;\n// }\n// }\n\n// export function clearRefreshToken() {\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// try {\n// sessionStorage.removeItem(REFRESH_COOKIE);\n// } catch (err) {\n// console.warn('Could not clear refresh token from sessionStorage:', err);\n// }\n// }\n\n// export 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\n// export function removeTokenListener(listener) {\n// listeners.delete(listener);\n// }\n\n// export function getListenerCount() {\n// return listeners.size;\n// }\n\n// export function isAuthenticated() {\n// const token = getToken();\n// return !!token && !isExpired(token, 15);\n// }\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 function logout() {\n resetCallbackState();\n\n const { clientKey, authBaseUrl, accountUiUrl } = getConfig();\n const token = getToken();\n\n console.log('🚪 Smart Logout initiated');\n\n clearToken();\n clearRefreshToken();\n sessionStorage.removeItem('originalApp');\n sessionStorage.removeItem('returnUrl');\n\n if (isRouterMode()) {\n return routerLogout(clientKey, authBaseUrl, accountUiUrl, token);\n } else {\n return clientLogout(clientKey, accountUiUrl);\n }\n}\n\nasync function routerLogout(clientKey, authBaseUrl, accountUiUrl, token) {\n console.log('🏭 Router Logout');\n\n const refreshToken = getRefreshToken();\n\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({\n refreshToken: refreshToken\n })\n });\n\n const data = await response.json();\n console.log('✅ Logout response:', data);\n\n clearRefreshToken();\n clearToken();\n\n // Skip Keycloak confirmation page - redirect directly to login\n // Backend has already revoked the session/tokens\n console.log('🔄 Redirecting to login (skipping Keycloak confirmation)');\n window.location.href = '/login';\n\n } catch (error) {\n console.warn('⚠️ Logout failed:', error);\n clearRefreshToken();\n clearToken();\n // Still redirect to login even on error\n window.location.href = '/login';\n }\n}\n\nfunction clientLogout(clientKey, accountUiUrl) {\n console.log('🔄 Client Logout');\n const logoutUrl = `${accountUiUrl}/login?client=${clientKey}&logout=true`;\n window.location.href = logoutUrl;\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 // ✅ For HTTP development, store refresh token from URL\n // In HTTPS production, refresh token is in httpOnly cookie (more secure)\n const refreshTokenInUrl = params.get('refresh_token');\n if (refreshTokenInUrl) {\n const isHttpDev = typeof window !== 'undefined' && window.location?.protocol === 'http:';\n if (isHttpDev) {\n console.log('📦 HTTP dev mode: Storing refresh token from callback URL');\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 and header\n if (storedRefreshToken) {\n requestOptions.headers['X-Refresh-Token'] = storedRefreshToken;\n requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });\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 // ✅ This will trigger token listeners\n clearToken();\n clearRefreshToken();\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 data = await response.json();\n return data.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 // Periodic validation\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 console.log('🔍 Validating session...');\n const isValid = await validateCurrentSession();\n\n if (!isValid) {\n console.log('❌ Session no longer valid on server');\n stopSessionMonitor();\n stopProactiveRefresh();\n clearToken();\n clearRefreshToken();\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 (when tab becomes visible again)\n if (validateOnVisibility && typeof document !== 'undefined') {\n visibilityHandler = async () => {\n if (document.visibilityState === 'visible') {\n const currentToken = getToken();\n if (!currentToken) return;\n\n console.log('👁️ Tab visible - validating session');\n try {\n const isValid = await validateCurrentSession();\n if (!isValid) {\n console.log('❌ Session expired while tab was hidden');\n stopSessionMonitor();\n stopProactiveRefresh();\n clearToken();\n clearRefreshToken();\n notifySessionInvalid('session_deleted_while_hidden');\n }\n } catch (error) {\n console.warn('⚠️ Visibility check failed:', error.message);\n }\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":["config","refreshPromise"]}
1
+ {"version":3,"sources":["../api.js","../token.js","../config.js","../core.js"],"sourcesContent":["// auth-client/api.js\nimport axios from 'axios';\nimport { getConfig } from './config';\nimport { getToken, setToken, clearToken } from './token';\nimport { refreshToken as performRefresh } from './core';\n\nconst api = axios.create({\n withCredentials: true,\n});\n\napi.interceptors.request.use((config) => {\n const runtimeConfig = getConfig();\n\n if (!config.baseURL) {\n\n config.baseURL = runtimeConfig?.authBaseUrl || 'http://auth.local.test:4000/auth';\n }\n\n if (!config.headers) {\n config.headers = {};\n }\n\n if (runtimeConfig?.clientKey && !config.headers['X-Client-Key']) {\n config.headers['X-Client-Key'] = runtimeConfig.clientKey;\n }\n\n const token = getToken();\n if (token) {\n config.headers.Authorization = `Bearer ${token}`;\n }\n\n return config;\n});\n\nlet refreshPromise = null;\n\napi.interceptors.response.use(\n (response) => response,\n async (error) => {\n const { response, config } = error || {};\n\n if (!response || !config) {\n return Promise.reject(error);\n }\n\n if (response.status !== 401 || config._retry) {\n return Promise.reject(error);\n }\n\n config._retry = true;\n\n if (!refreshPromise) {\n refreshPromise = performRefresh()\n .then((newToken) => {\n refreshPromise = null;\n if (newToken) {\n setToken(newToken);\n }\n return newToken;\n })\n .catch((refreshError) => {\n refreshPromise = null;\n clearToken();\n throw refreshError;\n });\n }\n\n try {\n const refreshedToken = await refreshPromise;\n\n if (refreshedToken) {\n config.headers.Authorization = `Bearer ${refreshedToken}`;\n return api(config);\n }\n } catch (refreshErr) {\n return Promise.reject(refreshErr);\n }\n\n return Promise.reject(error);\n }\n);\n\napi.validateSession = async () => {\n try {\n const response = await api.get('/account/validate-session');\n return response.data.valid;\n } catch (err) {\n if (err.response?.status === 401) {\n return false;\n }\n throw err;\n }\n};\n\nexport default api;\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, 15);\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 function logout() {\n resetCallbackState();\n\n const { clientKey, authBaseUrl, accountUiUrl } = getConfig();\n const token = getToken();\n\n console.log('🚪 Smart Logout initiated');\n\n clearToken();\n clearRefreshToken();\n sessionStorage.removeItem('originalApp');\n sessionStorage.removeItem('returnUrl');\n\n if (isRouterMode()) {\n return routerLogout(clientKey, authBaseUrl, accountUiUrl, token);\n } else {\n return clientLogout(clientKey, accountUiUrl);\n }\n}\n\nasync function routerLogout(clientKey, authBaseUrl, accountUiUrl, token) {\n console.log('🏭 Router Logout');\n\n const refreshToken = getRefreshToken();\n\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({\n refreshToken: refreshToken\n })\n });\n\n const data = await response.json();\n console.log('✅ Logout response:', data);\n\n clearRefreshToken();\n clearToken();\n\n // Skip Keycloak confirmation page - redirect directly to login\n // Backend has already revoked the session/tokens\n console.log('🔄 Redirecting to login (skipping Keycloak confirmation)');\n window.location.href = '/login';\n\n } catch (error) {\n console.warn('⚠️ Logout failed:', error);\n clearRefreshToken();\n clearToken();\n // Still redirect to login even on error\n window.location.href = '/login';\n }\n}\n\nfunction clientLogout(clientKey, accountUiUrl) {\n console.log('🔄 Client Logout');\n const logoutUrl = `${accountUiUrl}/login?client=${clientKey}&logout=true`;\n window.location.href = logoutUrl;\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 and header\n if (storedRefreshToken) {\n requestOptions.headers['X-Refresh-Token'] = storedRefreshToken;\n requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });\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 // ✅ This will trigger token listeners\n clearToken();\n clearRefreshToken();\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 data = await response.json();\n return data.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 // Periodic validation\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 console.log('🔍 Validating session...');\n const isValid = await validateCurrentSession();\n\n if (!isValid) {\n console.log('❌ Session no longer valid on server');\n stopSessionMonitor();\n stopProactiveRefresh();\n clearToken();\n clearRefreshToken();\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 (when tab becomes visible again)\n if (validateOnVisibility && typeof document !== 'undefined') {\n visibilityHandler = async () => {\n if (document.visibilityState === 'visible') {\n const currentToken = getToken();\n if (!currentToken) return;\n\n console.log('👁️ Tab visible - validating session');\n try {\n const isValid = await validateCurrentSession();\n if (!isValid) {\n console.log('❌ Session expired while tab was hidden');\n stopSessionMonitor();\n stopProactiveRefresh();\n clearToken();\n clearRefreshToken();\n notifySessionInvalid('session_deleted_while_hidden');\n }\n } catch (error) {\n console.warn('⚠️ Visibility check failed:', error.message);\n }\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":["config","refreshPromise"]}