@spidy092/auth-client 1.0.18 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/api.js +59 -16
  2. package/core.js +54 -19
  3. package/package.json +1 -1
  4. package/token.js +348 -90
package/api.js CHANGED
@@ -1,50 +1,93 @@
1
1
  // auth-client/api.js
2
2
  import axios from 'axios';
3
- import { getToken } from './token';
4
3
  import { getConfig } from './config';
4
+ import { getToken, setToken, clearToken } from './token';
5
+ import { refreshToken as performRefresh } from './core';
5
6
 
6
- // ✅ Fixed: Create instance without baseURL initially
7
7
  const api = axios.create({
8
8
  withCredentials: true,
9
9
  });
10
10
 
11
- // ✅ Fixed: Set baseURL dynamically in interceptor
12
11
  api.interceptors.request.use((config) => {
13
- // Set baseURL dynamically each time
12
+ const runtimeConfig = getConfig();
13
+
14
14
  if (!config.baseURL) {
15
- const authConfig = getConfig();
16
- config.baseURL = authConfig?.authBaseUrl || 'http://localhost:4000';
15
+ config.baseURL = runtimeConfig?.authBaseUrl || 'http://localhost:4000/auth';
16
+ }
17
+
18
+ if (!config.headers) {
19
+ config.headers = {};
20
+ }
21
+
22
+ if (runtimeConfig?.clientKey && !config.headers['X-Client-Key']) {
23
+ config.headers['X-Client-Key'] = runtimeConfig.clientKey;
17
24
  }
18
-
25
+
19
26
  const token = getToken();
20
27
  if (token) {
21
28
  config.headers.Authorization = `Bearer ${token}`;
22
29
  }
30
+
23
31
  return config;
24
32
  });
25
33
 
26
- // Added: Response interceptor for token refresh/error handling
34
+ let refreshPromise = null;
35
+
27
36
  api.interceptors.response.use(
28
37
  (response) => response,
29
- (error) => {
30
- if (error.response?.status === 401) {
31
- console.warn('API request failed with 401, token may be expired');
32
- // You could trigger token refresh or logout here
38
+ async (error) => {
39
+ const { response, config } = error || {};
40
+
41
+ if (!response || !config) {
42
+ return Promise.reject(error);
33
43
  }
44
+
45
+ if (response.status !== 401 || config._retry) {
46
+ return Promise.reject(error);
47
+ }
48
+
49
+ config._retry = true;
50
+
51
+ if (!refreshPromise) {
52
+ refreshPromise = performRefresh()
53
+ .then((newToken) => {
54
+ refreshPromise = null;
55
+ if (newToken) {
56
+ setToken(newToken);
57
+ }
58
+ return newToken;
59
+ })
60
+ .catch((refreshError) => {
61
+ refreshPromise = null;
62
+ clearToken();
63
+ throw refreshError;
64
+ });
65
+ }
66
+
67
+ try {
68
+ const refreshedToken = await refreshPromise;
69
+
70
+ if (refreshedToken) {
71
+ config.headers.Authorization = `Bearer ${refreshedToken}`;
72
+ return api(config);
73
+ }
74
+ } catch (refreshErr) {
75
+ return Promise.reject(refreshErr);
76
+ }
77
+
34
78
  return Promise.reject(error);
35
79
  }
36
80
  );
37
81
 
38
-
39
82
  api.validateSession = async () => {
40
83
  try {
41
84
  const response = await api.get('/account/validate-session');
42
85
  return response.data.valid;
43
- } catch (error) {
44
- if (error.response?.status === 401) {
86
+ } catch (err) {
87
+ if (err.response?.status === 401) {
45
88
  return false;
46
89
  }
47
- throw error;
90
+ throw err;
48
91
  }
49
92
  };
50
93
 
package/core.js CHANGED
@@ -1,5 +1,12 @@
1
1
  // auth-client/core.js
2
- import { setToken, clearToken, getToken } from './token';
2
+ import {
3
+ setToken,
4
+ clearToken,
5
+ getToken,
6
+ setRefreshToken,
7
+ getRefreshToken,
8
+ clearRefreshToken,
9
+ } from './token';
3
10
  import { getConfig, isRouterMode } from './config';
4
11
 
5
12
  // ✅ Track callback state with listeners
@@ -86,7 +93,9 @@ export function logout() {
86
93
 
87
94
  // Clear local storage immediately (this will trigger listeners)
88
95
  clearToken();
89
- sessionStorage.clear();
96
+ clearRefreshToken();
97
+ sessionStorage.removeItem('originalApp');
98
+ sessionStorage.removeItem('returnUrl');
90
99
 
91
100
  if (isRouterMode()) {
92
101
  return routerLogout(clientKey, authBaseUrl, accountUiUrl, token);
@@ -99,8 +108,8 @@ export function logout() {
99
108
  async function routerLogout(clientKey, authBaseUrl, accountUiUrl, token) {
100
109
  console.log('🏭 Enhanced Router Logout with sessionStorage');
101
110
 
102
- const refreshToken = sessionStorage.getItem('refreshToken');
103
- console.log('Refresh token from storage:', refreshToken ? 'FOUND' : 'MISSING');
111
+ const refreshToken = getRefreshToken();
112
+ console.log('Refresh token available:', refreshToken ? 'FOUND' : 'MISSING');
104
113
 
105
114
  try {
106
115
  const response = await fetch(`${authBaseUrl}/logout/${clientKey}`, {
@@ -119,7 +128,7 @@ async function routerLogout(clientKey, authBaseUrl, accountUiUrl, token) {
119
128
  console.log('✅ Logout response:', data);
120
129
 
121
130
  // Clear stored tokens
122
- sessionStorage.removeItem('refreshToken');
131
+ clearRefreshToken();
123
132
  clearToken();
124
133
 
125
134
  // Delay before redirect
@@ -132,7 +141,7 @@ async function routerLogout(clientKey, authBaseUrl, accountUiUrl, token) {
132
141
 
133
142
  } catch (error) {
134
143
  console.warn('⚠️ Logout failed:', error);
135
- sessionStorage.removeItem('refreshToken');
144
+ clearRefreshToken();
136
145
  clearToken();
137
146
  }
138
147
 
@@ -177,11 +186,11 @@ export function handleCallback() {
177
186
 
178
187
  if (accessToken) {
179
188
  setToken(accessToken);
180
-
181
- // STORE REFRESH TOKEN in sessionStorage
189
+
190
+ // Store refresh token for future refresh calls
182
191
  if (refreshToken) {
183
- sessionStorage.setItem('refreshToken', refreshToken);
184
- console.log('✅ Refresh token stored in sessionStorage');
192
+ setRefreshToken(refreshToken);
193
+ console.log('✅ Refresh token persisted');
185
194
  }
186
195
 
187
196
  // Clean URL parameters
@@ -205,37 +214,63 @@ export function resetCallbackState() {
205
214
  console.log('🔄 Callback state reset');
206
215
  }
207
216
 
217
+ // auth-client/core.js
208
218
  export async function refreshToken() {
209
219
  const { clientKey, authBaseUrl } = getConfig();
210
-
211
- console.log('🔄 Refreshing token:', {
212
- clientKey,
213
- mode: isRouterMode() ? 'ROUTER' : 'CLIENT'
220
+ const refreshTokenValue = getRefreshToken(); // ✅ Now checks both cookie & localStorage
221
+
222
+ console.log('🔄 Refreshing token:', {
223
+ clientKey,
224
+ mode: isRouterMode() ? 'ROUTER' : 'CLIENT',
225
+ hasRefreshToken: !!refreshTokenValue
214
226
  });
215
-
227
+
228
+ if (!refreshTokenValue) {
229
+ console.warn('⚠️ No refresh token available for refresh');
230
+ clearToken();
231
+ throw new Error('No refresh token available');
232
+ }
233
+
216
234
  try {
217
235
  const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, {
218
236
  method: 'POST',
219
- credentials: 'include',
237
+ credentials: 'include', // ✅ Sends cookie if available
238
+ headers: {
239
+ 'Content-Type': 'application/json',
240
+ 'X-Refresh-Token': refreshTokenValue // ✅ Also send in header as fallback
241
+ },
242
+ body: JSON.stringify({
243
+ refreshToken: refreshTokenValue // ✅ Also send in body
244
+ })
220
245
  });
221
246
 
222
247
  if (!response.ok) {
223
248
  throw new Error('Refresh failed');
224
249
  }
225
250
 
226
- const { access_token } = await response.json();
227
- // ✅ This will trigger token listeners
251
+ const { access_token, refresh_token: new_refresh_token } = await response.json();
252
+
253
+ // ✅ Update access token (triggers listeners)
228
254
  setToken(access_token);
255
+
256
+ // ✅ Update refresh token in BOTH storages if backend returned new one
257
+ if (new_refresh_token) {
258
+ setRefreshToken(new_refresh_token);
259
+ }
260
+
229
261
  console.log('✅ Token refresh successful, listeners notified');
230
262
  return access_token;
231
263
  } catch (err) {
232
- // This will trigger token listeners
264
+ console.error('❌ Token refresh failed:', err);
265
+ // ✅ Clear everything on refresh failure
233
266
  clearToken();
267
+ clearRefreshToken();
234
268
  throw err;
235
269
  }
236
270
  }
237
271
 
238
272
 
273
+
239
274
  export async function validateCurrentSession() {
240
275
  try {
241
276
  const { authBaseUrl } = getConfig();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spidy092/auth-client",
3
- "version": "1.0.18",
3
+ "version": "2.0.0",
4
4
  "description": "Scalable frontend auth SDK for centralized login using Keycloak + Auth Service.",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
package/token.js CHANGED
@@ -1,66 +1,150 @@
1
+
1
2
  // auth-client/token.js
2
- let memoryToken = null;
3
- const listeners = new Set(); // ✅ Add listeners
4
3
 
5
- export function setToken(token) {
6
- const previousToken = memoryToken;
7
- memoryToken = token;
8
-
4
+ import { jwtDecode } from 'jwt-decode';
5
+
6
+ let accessToken = null;
7
+ const listeners = new Set();
8
+
9
+ const REFRESH_COOKIE = 'account_refresh_token';
10
+ const REFRESH_STORAGE_KEY = 'refresh_token'; // localStorage key
11
+ const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days in seconds
12
+
13
+ function secureAttribute() {
9
14
  try {
10
- localStorage.setItem('authToken', token);
15
+ return typeof window !== 'undefined' && window.location?.protocol === 'https:'
16
+ ? '; Secure'
17
+ : '';
11
18
  } catch (err) {
12
- console.warn('Could not write token to localStorage:', err);
19
+ return '';
13
20
  }
21
+ }
14
22
 
15
- // Notify listeners when token changes
16
- if (previousToken !== token) {
17
- console.log('🔔 Token changed, notifying listeners:', {
18
- listenerCount: listeners.size,
19
- hadToken: !!previousToken,
20
- hasToken: !!token
21
- });
22
-
23
- listeners.forEach(listener => {
24
- try {
25
- listener(token, previousToken);
26
- } catch (err) {
27
- console.warn('Token listener error:', err);
28
- }
29
- });
23
+ // ========== ACCESS TOKEN (localStorage only) ==========
24
+ function writeAccessToken(token) {
25
+ if (!token) {
26
+ try {
27
+ localStorage.removeItem('authToken');
28
+ } catch (err) {
29
+ console.warn('Could not clear token from localStorage:', err);
30
+ }
31
+ return;
32
+ }
33
+
34
+ try {
35
+ localStorage.setItem('authToken', token);
36
+ } catch (err) {
37
+ console.warn('Could not persist token to localStorage:', err);
30
38
  }
31
39
  }
32
40
 
33
- export function getToken() {
34
- if (memoryToken) return memoryToken;
41
+ function readAccessToken() {
35
42
  try {
36
- const stored = localStorage.getItem('authToken');
37
- memoryToken = stored;
38
- return stored;
43
+ return localStorage.getItem('authToken');
39
44
  } catch (err) {
40
45
  console.warn('Could not read token from localStorage:', err);
41
46
  return null;
42
47
  }
43
48
  }
44
49
 
45
- export function clearToken() {
46
- const previousToken = memoryToken;
47
- memoryToken = null;
48
-
50
+ // ========== REFRESH TOKEN (localStorage + Cookie dual storage) ==========
51
+
52
+ /**
53
+ * Store refresh token in BOTH localStorage AND cookie
54
+ * Whichever survives (cross-domain, privacy settings) will work
55
+ */
56
+ export function setRefreshToken(token) {
57
+ if (!token) {
58
+ clearRefreshToken();
59
+ return;
60
+ }
61
+
62
+ const expires = new Date(Date.now() + COOKIE_MAX_AGE * 1000);
63
+
64
+ // 1. Try to set cookie (works for same-domain, cross-subdomain)
49
65
  try {
50
- localStorage.removeItem('authToken');
66
+ document.cookie = `${REFRESH_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax${secureAttribute()}; Expires=${expires.toUTCString()}`;
67
+ console.log('✅ Refresh token stored in cookie');
51
68
  } catch (err) {
52
- console.warn('Could not clear token from localStorage:', err);
69
+ console.warn('⚠️ Could not persist refresh token cookie:', err);
53
70
  }
54
71
 
55
- // Notify listeners when token is cleared
56
- if (previousToken) {
57
- console.log('🔔 Token cleared, notifying listeners:', {
58
- listenerCount: listeners.size
59
- });
60
-
61
- listeners.forEach(listener => {
72
+ // 2. Also store in localStorage as backup (survives browser privacy settings)
73
+ try {
74
+ localStorage.setItem(REFRESH_STORAGE_KEY, token);
75
+ console.log('✅ Refresh token stored in localStorage');
76
+ } catch (err) {
77
+ console.warn('⚠️ Could not persist refresh token to localStorage:', err);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Get refresh token from cookie OR localStorage (whichever works)
83
+ * Priority: Cookie > localStorage
84
+ */
85
+ export function getRefreshToken() {
86
+ // 1. Try cookie first (preferred for httpOnly scenario)
87
+ let cookieMatch = null;
88
+ try {
89
+ cookieMatch = document.cookie
90
+ ?.split('; ')
91
+ ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));
92
+ } catch (err) {
93
+ cookieMatch = null;
94
+ }
95
+
96
+ if (cookieMatch) {
97
+ const token = decodeURIComponent(cookieMatch.split('=')[1]);
98
+ console.log('✅ Retrieved refresh token from cookie');
99
+ return token;
100
+ }
101
+
102
+ // 2. Fallback to localStorage
103
+ try {
104
+ const token = localStorage.getItem(REFRESH_STORAGE_KEY);
105
+ if (token) {
106
+ console.log('✅ Retrieved refresh token from localStorage (fallback)');
107
+ return token;
108
+ }
109
+ } catch (err) {
110
+ console.warn('⚠️ Could not read refresh token from localStorage:', err);
111
+ }
112
+
113
+ console.warn('⚠️ No refresh token found in cookie or localStorage');
114
+ return null;
115
+ }
116
+
117
+ /**
118
+ * Clear refresh token from BOTH cookie AND localStorage
119
+ */
120
+ export function clearRefreshToken() {
121
+ // Clear cookie
122
+ try {
123
+ document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Lax${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;
124
+ console.log('✅ Cleared refresh token cookie');
125
+ } catch (err) {
126
+ console.warn('⚠️ Could not clear refresh token cookie:', err);
127
+ }
128
+
129
+ // Clear localStorage
130
+ try {
131
+ localStorage.removeItem(REFRESH_STORAGE_KEY);
132
+ console.log('✅ Cleared refresh token from localStorage');
133
+ } catch (err) {
134
+ console.warn('⚠️ Could not clear refresh token from localStorage:', err);
135
+ }
136
+ }
137
+
138
+ // ========== ACCESS TOKEN MANAGEMENT ==========
139
+ export function setToken(token) {
140
+ const previousToken = accessToken;
141
+ accessToken = token || null;
142
+ writeAccessToken(accessToken);
143
+
144
+ if (previousToken !== accessToken) {
145
+ listeners.forEach((listener) => {
62
146
  try {
63
- listener(null, previousToken);
147
+ listener(accessToken, previousToken);
64
148
  } catch (err) {
65
149
  console.warn('Token listener error:', err);
66
150
  }
@@ -68,82 +152,256 @@ export function clearToken() {
68
152
  }
69
153
  }
70
154
 
71
- // Add listener management
155
+ export function getToken() {
156
+ if (accessToken) return accessToken;
157
+ accessToken = readAccessToken();
158
+ return accessToken;
159
+ }
160
+
161
+ export function clearToken() {
162
+ if (!accessToken) {
163
+ writeAccessToken(null);
164
+ clearRefreshToken();
165
+ return;
166
+ }
167
+
168
+ const previousToken = accessToken;
169
+ accessToken = null;
170
+ writeAccessToken(null);
171
+ clearRefreshToken();
172
+
173
+ listeners.forEach((listener) => {
174
+ try {
175
+ listener(null, previousToken);
176
+ } catch (err) {
177
+ console.warn('Token listener error:', err);
178
+ }
179
+ });
180
+ }
181
+
182
+ // ========== HELPER FUNCTIONS ==========
183
+ function decode(token) {
184
+ try {
185
+ return jwtDecode(token);
186
+ } catch (err) {
187
+ return null;
188
+ }
189
+ }
190
+
191
+ function isExpired(token, bufferSeconds = 60) {
192
+ if (!token) return true;
193
+ const decoded = decode(token);
194
+ if (!decoded?.exp) return true;
195
+ const now = Date.now() / 1000;
196
+ return decoded.exp < now + bufferSeconds;
197
+ }
198
+
72
199
  export function addTokenListener(listener) {
73
200
  if (typeof listener !== 'function') {
74
201
  throw new Error('Token listener must be a function');
75
202
  }
76
-
77
203
  listeners.add(listener);
78
- console.log('📎 Token listener added, total listeners:', listeners.size);
79
-
80
- // Return cleanup function
81
204
  return () => {
82
- const removed = listeners.delete(listener);
83
- if (removed) {
84
- console.log('📎 Token listener removed, remaining listeners:', listeners.size);
85
- }
86
- return removed;
205
+ listeners.delete(listener);
87
206
  };
88
207
  }
89
208
 
90
209
  export function removeTokenListener(listener) {
91
- const removed = listeners.delete(listener);
92
- if (removed) {
93
- console.log('📎 Token listener removed, remaining listeners:', listeners.size);
94
- }
95
- return removed;
96
-
97
-
210
+ listeners.delete(listener);
98
211
  }
99
212
 
100
213
  export function getListenerCount() {
101
- return listeners.size;
214
+ return listeners.size;
102
215
  }
103
216
 
217
+ export function isAuthenticated() {
218
+ const token = getToken();
219
+ return !!token && !isExpired(token, 15);
220
+ }
221
+
222
+
104
223
 
224
+ // // auth-client/token.js
225
+ // import { jwtDecode } from 'jwt-decode';
226
+
227
+ // let accessToken = null;
228
+ // const listeners = new Set();
229
+
230
+ // const REFRESH_COOKIE = 'account_refresh_token';
231
+ // const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days in seconds
232
+
233
+ // function secureAttribute() {
234
+ // try {
235
+ // return typeof window !== 'undefined' && window.location?.protocol === 'https:'
236
+ // ? '; Secure'
237
+ // : '';
238
+ // } catch (err) {
239
+ // return '';
240
+ // }
241
+ // }
242
+
243
+ // function writeAccessToken(token) {
244
+ // if (!token) {
245
+ // try {
246
+ // localStorage.removeItem('authToken');
247
+ // } catch (err) {
248
+ // console.warn('Could not clear token from localStorage:', err);
249
+ // }
250
+ // return;
251
+ // }
252
+
253
+ // try {
254
+ // localStorage.setItem('authToken', token);
255
+ // } catch (err) {
256
+ // console.warn('Could not persist token to localStorage:', err);
257
+ // }
258
+ // }
105
259
 
106
- // ✅ Debug function to see current listeners
107
- // ✅ Decode JWT payload safely
108
- // export function decodeToken(token) {
109
- // if (!token) return null;
260
+ // function readAccessToken() {
261
+ // try {
262
+ // return localStorage.getItem('authToken');
263
+ // } catch (err) {
264
+ // console.warn('Could not read token from localStorage:', err);
265
+ // return null;
266
+ // }
267
+ // }
110
268
 
269
+ // function decode(token) {
111
270
  // try {
112
- // const base64Url = token.split('.')[1]; // JWT payload is 2nd part
113
- // if (!base64Url) return null;
114
-
115
- // const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
116
- // const jsonPayload = decodeURIComponent(
117
- // atob(base64)
118
- // .split('')
119
- // .map((c) => `%${('00' + c.charCodeAt(0).toString(16)).slice(-2)}`)
120
- // .join('')
121
- // );
122
-
123
- // return JSON.parse(jsonPayload);
271
+ // return jwtDecode(token);
124
272
  // } catch (err) {
125
- // console.warn('Could not decode token:', err);
126
273
  // return null;
127
274
  // }
128
275
  // }
129
276
 
130
- // // Check if JWT is expired (with optional buffer)
131
- // export function isTokenExpired(token, bufferSeconds = 0) {
277
+ // function isExpired(token, bufferSeconds = 60) {
132
278
  // if (!token) return true;
279
+ // const decoded = decode(token);
280
+ // if (!decoded?.exp) return true;
281
+ // const now = Date.now() / 1000;
282
+ // return decoded.exp < now + bufferSeconds;
283
+ // }
133
284
 
134
- // const decoded = decodeToken(token);
135
- // if (!decoded || !decoded.exp) return true; // no exp claim → treat as expired
285
+ // export function setToken(token) {
286
+ // const previousToken = accessToken;
287
+ // accessToken = token || null;
288
+ // writeAccessToken(accessToken);
136
289
 
137
- // const expiryTime = decoded.exp * 1000; // exp is in seconds → convert to ms
138
- // const currentTime = Date.now();
290
+ // if (previousToken !== accessToken) {
291
+ // listeners.forEach((listener) => {
292
+ // try {
293
+ // listener(accessToken, previousToken);
294
+ // } catch (err) {
295
+ // console.warn('Token listener error:', err);
296
+ // }
297
+ // });
298
+ // }
299
+ // }
139
300
 
140
- // // Add buffer (e.g., 60s) to expire slightly earlier
141
- // return currentTime >= expiryTime - bufferSeconds * 1000;
301
+ // export function getToken() {
302
+ // if (accessToken) return accessToken;
303
+ // accessToken = readAccessToken();
304
+ // return accessToken;
142
305
  // }
143
306
 
144
- // Check if user is authenticated
145
- export function isAuthenticated() {
146
- const token = getToken();
147
- return !!token && !isTokenExpired(token);
148
- }
307
+ // export function clearToken() {
308
+ // if (!accessToken) {
309
+ // writeAccessToken(null);
310
+ // clearRefreshToken();
311
+ // return;
312
+ // }
313
+
314
+ // const previousToken = accessToken;
315
+ // accessToken = null;
316
+ // writeAccessToken(null);
317
+ // clearRefreshToken();
318
+
319
+ // listeners.forEach((listener) => {
320
+ // try {
321
+ // listener(null, previousToken);
322
+ // } catch (err) {
323
+ // console.warn('Token listener error:', err);
324
+ // }
325
+ // });
326
+ // }
327
+
328
+ // export function setRefreshToken(token) {
329
+ // if (!token) {
330
+ // clearRefreshToken();
331
+ // return;
332
+ // }
333
+
334
+ // const expires = new Date(Date.now() + COOKIE_MAX_AGE * 1000);
335
+ // try {
336
+ // document.cookie = `${REFRESH_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Strict${secureAttribute()}; Expires=${expires.toUTCString()}`;
337
+ // } catch (err) {
338
+ // console.warn('Could not persist refresh token cookie:', err);
339
+ // }
340
+
341
+ // try {
342
+ // sessionStorage.setItem(REFRESH_COOKIE, token);
343
+ // } catch (err) {
344
+ // console.warn('Could not persist refresh token to sessionStorage:', err);
345
+ // }
346
+ // }
347
+
348
+ // export function getRefreshToken() {
349
+ // // Prefer cookie to align with server expectations
350
+ // let cookieMatch = null;
351
+
352
+ // try {
353
+ // cookieMatch = document.cookie
354
+ // ?.split('; ')
355
+ // ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));
356
+ // } catch (err) {
357
+ // cookieMatch = null;
358
+ // }
359
+
360
+ // if (cookieMatch) {
361
+ // return decodeURIComponent(cookieMatch.split('=')[1]);
362
+ // }
363
+
364
+ // try {
365
+ // return sessionStorage.getItem(REFRESH_COOKIE);
366
+ // } catch (err) {
367
+ // console.warn('Could not read refresh token from sessionStorage:', err);
368
+ // return null;
369
+ // }
370
+ // }
371
+
372
+ // export function clearRefreshToken() {
373
+ // try {
374
+ // document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Strict${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;
375
+ // } catch (err) {
376
+ // console.warn('Could not clear refresh token cookie:', err);
377
+ // }
378
+ // try {
379
+ // sessionStorage.removeItem(REFRESH_COOKIE);
380
+ // } catch (err) {
381
+ // console.warn('Could not clear refresh token from sessionStorage:', err);
382
+ // }
383
+ // }
384
+
385
+ // export function addTokenListener(listener) {
386
+ // if (typeof listener !== 'function') {
387
+ // throw new Error('Token listener must be a function');
388
+ // }
389
+ // listeners.add(listener);
390
+ // return () => {
391
+ // listeners.delete(listener);
392
+ // };
393
+ // }
394
+
395
+ // export function removeTokenListener(listener) {
396
+ // listeners.delete(listener);
397
+ // }
398
+
399
+ // export function getListenerCount() {
400
+ // return listeners.size;
401
+ // }
402
+
403
+ // export function isAuthenticated() {
404
+ // const token = getToken();
405
+ // return !!token && !isExpired(token, 15);
406
+ // }
149
407