formreader-session-timeout 0.2.3 → 0.2.5

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/index.mjs CHANGED
@@ -10,11 +10,18 @@ var DEFAULT_SESSION_CONFIG = {
10
10
  // 8 hours max
11
11
  refreshEndpoint: "/auth/refresh/",
12
12
  logoutEndpoint: "/auth/logout/",
13
+ trackIdleTime: true,
13
14
  showIdleWarning: true,
14
15
  idleWarningThresholdMs: 2 * 60 * 1e3,
15
16
  // 2 minutes before idle timeout
16
17
  autoRefresh: true,
17
- debug: false
18
+ debug: false,
19
+ accessTokenField: "access",
20
+ refreshTokenField: "refresh",
21
+ refreshAccessTokenField: "access",
22
+ refreshRefreshTokenField: "refresh",
23
+ storeRefreshToken: true,
24
+ validateOnInit: true
18
25
  };
19
26
 
20
27
  // services/tokenService.ts
@@ -64,34 +71,25 @@ function getTimeUntilExpiry(token) {
64
71
  function getStoredToken() {
65
72
  return sessionStorage.getItem("authToken") || localStorage.getItem("authToken");
66
73
  }
74
+ function getStoredRefreshToken() {
75
+ return sessionStorage.getItem("refreshToken") || localStorage.getItem("refreshToken");
76
+ }
67
77
  function storeToken(token, persistent = false) {
68
- if (persistent) {
69
- localStorage.setItem("authToken", token);
70
- sessionStorage.removeItem("authToken");
71
- } else {
72
- sessionStorage.setItem("authToken", token);
73
- localStorage.removeItem("authToken");
74
- }
78
+ const storage = persistent ? localStorage : sessionStorage;
79
+ storage.setItem("authToken", token);
80
+ }
81
+ function storeRefreshToken(token, persistent = false) {
82
+ const storage = persistent ? localStorage : sessionStorage;
83
+ storage.setItem("refreshToken", token);
75
84
  }
76
85
  function clearToken() {
77
86
  sessionStorage.removeItem("authToken");
78
87
  localStorage.removeItem("authToken");
88
+ sessionStorage.removeItem("refreshToken");
89
+ localStorage.removeItem("refreshToken");
79
90
  }
80
91
  function validateToken(token) {
81
- if (!token || typeof token !== "string") {
82
- return { valid: false, error: "Token is missing or invalid" };
83
- }
84
- const payload = decodeJWT(token);
85
- if (!payload) {
86
- return { valid: false, error: "Failed to decode token" };
87
- }
88
- if (!payload.exp) {
89
- return { valid: false, error: "Token missing expiry time" };
90
- }
91
- if (isTokenExpired(token)) {
92
- return { valid: false, error: "Token has expired" };
93
- }
94
- return { valid: true };
92
+ return decodeJWT(token) !== null;
95
93
  }
96
94
 
97
95
  // services/sessionManager.ts
@@ -112,6 +110,20 @@ function defaultFetchClient() {
112
110
  }
113
111
  };
114
112
  }
113
+ function extractTokenFromResponse(response, fieldName = "access") {
114
+ if (!response)
115
+ return null;
116
+ const parts = fieldName.split(".");
117
+ let value = response;
118
+ for (const part of parts) {
119
+ if (value && typeof value === "object") {
120
+ value = value[part];
121
+ } else {
122
+ return null;
123
+ }
124
+ }
125
+ return typeof value === "string" ? value : null;
126
+ }
115
127
  var SessionManager = class {
116
128
  constructor(config) {
117
129
  this.refreshTimer = null;
@@ -134,20 +146,50 @@ var SessionManager = class {
134
146
  this.log("SessionManager initialized with config:", this.config);
135
147
  }
136
148
  /**
137
- * Initialize session management
149
+ * Initialize session management with optional login response
150
+ * Extracts and stores tokens from login response using configured field names
138
151
  */
139
- init() {
140
- const token = getStoredToken();
152
+ init(loginResponse) {
153
+ const token = loginResponse ? extractTokenFromResponse(loginResponse, this.config.accessTokenField || "access") : getStoredToken();
141
154
  if (!token) {
142
155
  this.log("No token found, session not initialized");
156
+ this.emit("noToken");
143
157
  return;
144
158
  }
159
+ storeToken(token);
160
+ if (this.config.storeRefreshToken && loginResponse) {
161
+ const refreshToken = extractTokenFromResponse(
162
+ loginResponse,
163
+ this.config.refreshTokenField || "refresh"
164
+ );
165
+ if (refreshToken) {
166
+ storeRefreshToken(refreshToken);
167
+ this.log("Refresh token stored");
168
+ }
169
+ }
145
170
  const validation = validateToken(token);
146
- if (!validation.valid) {
147
- this.log("Invalid token:", validation.error);
171
+ if (!validation) {
172
+ this.log("Invalid token format");
173
+ this.emit("invalidToken");
148
174
  this.logout();
149
175
  return;
150
176
  }
177
+ if (isTokenExpired(token)) {
178
+ this.log("Token is expired, attempting refresh");
179
+ this.emit("tokenExpired");
180
+ const refreshToken = getStoredRefreshToken();
181
+ if (refreshToken && this.config.autoRefresh) {
182
+ this.refreshToken().then((success) => {
183
+ if (!success) {
184
+ this.logout();
185
+ }
186
+ });
187
+ return;
188
+ } else {
189
+ this.logout();
190
+ return;
191
+ }
192
+ }
151
193
  this.state.isActive = true;
152
194
  this.setupTokenRefresh();
153
195
  this.setupIdleTracking();
@@ -164,43 +206,33 @@ var SessionManager = class {
164
206
  return;
165
207
  const timeUntilExpiry = getTimeUntilExpiry(token);
166
208
  const refreshAt = Math.max(0, timeUntilExpiry - this.config.refreshThresholdMs);
167
- this.log(`Token expires in ${timeUntilExpiry}ms, will refresh in ${refreshAt}ms`);
168
209
  if (this.refreshTimer) {
169
210
  clearTimeout(this.refreshTimer);
170
211
  }
171
- if (this.config.autoRefresh && refreshAt > 0) {
172
- this.refreshTimer = setTimeout(() => {
173
- this.log("Scheduled token refresh triggered");
174
- this.refreshToken();
175
- }, refreshAt);
176
- } else if (refreshAt <= 0 && this.config.autoRefresh) {
177
- this.log("Token near expiry, refreshing immediately");
212
+ this.refreshTimer = setTimeout(() => {
213
+ this.log("Token refresh threshold reached, triggering refresh");
178
214
  this.refreshToken();
179
- }
215
+ }, refreshAt);
216
+ this.log(`Token refresh scheduled in ${refreshAt}ms`);
180
217
  }
181
218
  /**
182
- * Setup idle activity tracking
219
+ * Setup idle tracking with activity listeners
183
220
  */
184
221
  setupIdleTracking() {
185
- const events = ["mousedown", "keydown", "scroll", "touchstart", "click"];
222
+ if (!this.config.trackIdleTime)
223
+ return;
186
224
  const handleActivity = () => {
187
225
  this.state.lastActivityTime = Date.now();
188
226
  this.state.isIdle = false;
189
227
  this.emit("activity");
190
- if (this.idleTimer) {
191
- clearTimeout(this.idleTimer);
192
- }
193
- this.idleTimer = setTimeout(() => {
194
- var _a, _b;
195
- this.state.isIdle = true;
196
- this.emit("idle");
197
- this.log("User idle timeout triggered");
198
- (_b = (_a = this.config).onIdle) == null ? void 0 : _b.call(_a);
199
- }, this.config.idleTimeoutMs);
200
228
  };
229
+ const events = ["mousedown", "keydown", "scroll", "touchstart", "click"];
201
230
  events.forEach((event) => {
202
- window.addEventListener(event, handleActivity, { passive: true });
231
+ window.addEventListener(event, handleActivity, true);
203
232
  });
233
+ if (this.idleTimer) {
234
+ clearTimeout(this.idleTimer);
235
+ }
204
236
  this.idleTimer = setTimeout(() => {
205
237
  var _a, _b;
206
238
  this.state.isIdle = true;
@@ -232,7 +264,7 @@ var SessionManager = class {
232
264
  }, this.config.maxSessionDurationMs);
233
265
  }
234
266
  /**
235
- * Refresh token
267
+ * Refresh token using stored refresh token
236
268
  */
237
269
  async refreshToken() {
238
270
  var _a, _b, _c, _d;
@@ -242,23 +274,49 @@ var SessionManager = class {
242
274
  }
243
275
  this.state.isRefreshing = true;
244
276
  this.state.refreshAttempts++;
245
- const token = getStoredToken();
246
- if (!token) {
277
+ const accessToken = getStoredToken();
278
+ const refreshToken = getStoredRefreshToken();
279
+ if (!accessToken && !refreshToken) {
247
280
  this.state.isRefreshing = false;
248
281
  return false;
249
282
  }
250
283
  try {
251
284
  this.log(`Refreshing token (attempt ${this.state.refreshAttempts})`);
252
285
  const client = this.config.httpClient || defaultFetchClient();
253
- const refreshPayload = this.config.refreshPayloadFormatter ? this.config.refreshPayloadFormatter(token) : { token };
254
- const refreshPromise = client.post(this.config.refreshEndpoint, refreshPayload, { headers: { Authorization: `Bearer ${token}` } });
286
+ const refreshPayload = this.config.refreshPayloadFormatter ? this.config.refreshPayloadFormatter(accessToken || "", refreshToken) : {
287
+ refresh: refreshToken || accessToken,
288
+ token: accessToken
289
+ };
290
+ const refreshPromise = client.post(
291
+ this.config.refreshEndpoint,
292
+ refreshPayload,
293
+ {
294
+ headers: {
295
+ Authorization: `Bearer ${accessToken || refreshToken}`
296
+ }
297
+ }
298
+ );
255
299
  this.requestDeduplication.set("refresh", refreshPromise);
256
300
  const response = await refreshPromise;
257
- const newToken = response.data.token || response.data.access_token;
258
- if (!newToken) {
259
- throw new Error("No token in refresh response");
301
+ const responseData = response.data;
302
+ const newAccessToken = extractTokenFromResponse(
303
+ responseData,
304
+ this.config.refreshAccessTokenField || "access"
305
+ );
306
+ if (!newAccessToken) {
307
+ throw new Error("No access token in refresh response");
308
+ }
309
+ storeToken(newAccessToken);
310
+ if (this.config.storeRefreshToken) {
311
+ const newRefreshToken = extractTokenFromResponse(
312
+ responseData,
313
+ this.config.refreshRefreshTokenField || "refresh"
314
+ );
315
+ if (newRefreshToken) {
316
+ storeRefreshToken(newRefreshToken);
317
+ this.log("Refresh token updated");
318
+ }
260
319
  }
261
- storeToken(newToken);
262
320
  this.state.isRefreshing = false;
263
321
  this.state.refreshAttempts = 0;
264
322
  this.requestDeduplication.delete("refresh");
@@ -280,41 +338,55 @@ var SessionManager = class {
280
338
  }
281
339
  }
282
340
  /**
283
- * Logout and cleanup
341
+ * Manual logout
284
342
  */
285
343
  async logout() {
286
344
  var _a, _b;
345
+ this.log("Logging out");
287
346
  try {
288
347
  const token = getStoredToken();
289
- if (token) {
348
+ if (token && this.config.logoutEndpoint) {
290
349
  const client = this.config.httpClient || defaultFetchClient();
291
- const logoutPayload = this.config.logoutPayloadFormatter ? this.config.logoutPayloadFormatter(token) : { token };
292
- await client.post(this.config.logoutEndpoint, logoutPayload, { headers: { Authorization: `Bearer ${token}` } });
350
+ const logoutPayload = this.config.logoutPayloadFormatter ? this.config.logoutPayloadFormatter(token, getStoredRefreshToken()) : { token };
351
+ await client.post(this.config.logoutEndpoint, logoutPayload, {
352
+ headers: { Authorization: `Bearer ${token}` }
353
+ });
293
354
  }
294
355
  } catch (error) {
295
- this.log("Logout API call failed:", error);
356
+ this.log("Logout request failed:", error);
296
357
  }
297
- this.cleanup();
358
+ clearToken();
298
359
  this.state.isActive = false;
299
360
  this.emit("loggedOut");
300
- (_b = (_a = this.config).onSessionExpired) == null ? void 0 : _b.call(_a);
301
- clearToken();
361
+ (_b = (_a = this.config).onLogout) == null ? void 0 : _b.call(_a);
362
+ this.cleanup();
302
363
  }
303
364
  /**
304
- * Extend session (reset idle timer)
365
+ * Check if session is active and token is valid
305
366
  */
306
- extendSession() {
307
- this.state.lastActivityTime = Date.now();
308
- this.state.isIdle = false;
309
- if (this.idleTimer) {
310
- clearTimeout(this.idleTimer);
367
+ isActive() {
368
+ if (!this.state.isActive)
369
+ return false;
370
+ const token = getStoredToken();
371
+ if (!token)
372
+ return false;
373
+ return !isTokenExpired(token);
374
+ }
375
+ /**
376
+ * Validate current session without initializing
377
+ */
378
+ validateSession() {
379
+ const token = getStoredToken();
380
+ if (!token) {
381
+ return { isValid: false, reason: "no_token" };
311
382
  }
312
- this.idleTimer = setTimeout(() => {
313
- this.state.isIdle = true;
314
- this.emit("idle");
315
- }, this.config.idleTimeoutMs);
316
- this.log("Session extended");
317
- this.emit("sessionExtended");
383
+ if (!validateToken(token)) {
384
+ return { isValid: false, reason: "invalid_token" };
385
+ }
386
+ if (isTokenExpired(token)) {
387
+ return { isValid: false, reason: "token_expired" };
388
+ }
389
+ return { isValid: true };
318
390
  }
319
391
  /**
320
392
  * Get current state
@@ -323,13 +395,7 @@ var SessionManager = class {
323
395
  return { ...this.state };
324
396
  }
325
397
  /**
326
- * Get current config
327
- */
328
- getConfig() {
329
- return { ...this.config };
330
- }
331
- /**
332
- * Update config
398
+ * Update config at runtime
333
399
  */
334
400
  updateConfig(newConfig) {
335
401
  this.config = { ...this.config, ...newConfig };
@@ -419,122 +485,151 @@ function resetSessionManager() {
419
485
  }
420
486
  }
421
487
 
488
+ // utils/authUtils.ts
489
+ function isAuthenticated() {
490
+ const token = getStoredToken();
491
+ if (!token) {
492
+ return false;
493
+ }
494
+ if (!validateToken(token)) {
495
+ return false;
496
+ }
497
+ if (isTokenExpired(token)) {
498
+ return false;
499
+ }
500
+ return true;
501
+ }
502
+ async function initializeAuth(config) {
503
+ const manager = getSessionManager(config);
504
+ return new Promise((resolve) => {
505
+ const cleanup = [];
506
+ cleanup.push(manager.on("initialized", () => {
507
+ cleanup.forEach((fn) => fn());
508
+ resolve(true);
509
+ }));
510
+ cleanup.push(manager.on("noToken", () => {
511
+ cleanup.forEach((fn) => fn());
512
+ resolve(false);
513
+ }));
514
+ cleanup.push(manager.on("invalidToken", () => {
515
+ cleanup.forEach((fn) => fn());
516
+ resolve(false);
517
+ }));
518
+ cleanup.push(manager.on("tokenExpired", () => {
519
+ setTimeout(() => {
520
+ cleanup.forEach((fn) => fn());
521
+ resolve(manager.isActive());
522
+ }, 1e3);
523
+ }));
524
+ cleanup.push(manager.on("loggedOut", () => {
525
+ cleanup.forEach((fn) => fn());
526
+ resolve(false);
527
+ }));
528
+ manager.init();
529
+ setTimeout(() => {
530
+ cleanup.forEach((fn) => fn());
531
+ resolve(manager.isActive());
532
+ }, 3e3);
533
+ });
534
+ }
535
+ function triggerAuthChange() {
536
+ window.dispatchEvent(new CustomEvent("authChange"));
537
+ }
538
+
422
539
  // hooks/useSessionTimeout.ts
423
540
  import { useEffect, useState, useCallback, useRef } from "react";
424
541
  function useSessionTimeout(options = {}) {
425
- const {
426
- config,
427
- enabled = true,
428
- onSessionExpiring,
429
- onSessionExpired,
430
- onIdle,
431
- onRefreshSuccess
432
- } = options;
433
- const [sessionState, setSessionState] = useState({
542
+ const [state, setState] = useState({
434
543
  isActive: false,
435
544
  isIdle: false,
436
545
  lastActivityTime: Date.now(),
437
546
  refreshAttempts: 0,
438
547
  isRefreshing: false
439
548
  });
440
- const [timeUntilIdle, setTimeUntilIdle] = useState(null);
441
- const [idleWarningVisible, setIdleWarningVisible] = useState(false);
549
+ const [error, setError] = useState(null);
550
+ const [timeUntilTimeout, setTimeUntilTimeout] = useState(0);
551
+ const [timeUntilIdle, setTimeUntilIdle] = useState(0);
442
552
  const managerRef = useRef(null);
443
- const unsubscribeRef = useRef(/* @__PURE__ */ new Map());
553
+ const updateTimerRef = useRef(null);
444
554
  useEffect(() => {
445
- if (!enabled)
446
- return;
447
- const manager = getSessionManager({
448
- ...config,
449
- onSessionExpiring,
450
- onSessionExpired,
451
- onIdle,
452
- onRefreshSuccess
453
- });
555
+ const { loginResponse, autoInit, ...config } = options;
556
+ const manager = getSessionManager(config);
454
557
  managerRef.current = manager;
455
- const subscriptions = /* @__PURE__ */ new Map();
456
- subscriptions.set(
457
- "initialized",
458
- manager.on("initialized", () => {
459
- setSessionState(manager.getState());
460
- })
461
- );
462
- subscriptions.set(
463
- "tokenRefreshed",
464
- manager.on("tokenRefreshed", () => {
465
- setSessionState(manager.getState());
466
- })
467
- );
468
- subscriptions.set(
469
- "idle",
470
- manager.on("idle", () => {
471
- setSessionState(manager.getState());
472
- })
473
- );
474
- subscriptions.set(
475
- "activity",
476
- manager.on("activity", () => {
477
- setSessionState(manager.getState());
478
- setIdleWarningVisible(false);
479
- })
480
- );
481
- subscriptions.set(
482
- "idleWarning",
483
- manager.on("idleWarning", (data) => {
484
- setIdleWarningVisible(true);
485
- setTimeUntilIdle(data.timeRemaining);
486
- })
487
- );
488
- subscriptions.set(
489
- "loggedOut",
490
- manager.on("loggedOut", () => {
491
- setSessionState(manager.getState());
492
- })
493
- );
494
- unsubscribeRef.current = subscriptions;
495
- manager.init();
558
+ const unsubscribeInitialized = manager.on("initialized", () => {
559
+ setState(manager.getState());
560
+ });
561
+ const unsubscribeTokenRefreshed = manager.on("tokenRefreshed", () => {
562
+ setState(manager.getState());
563
+ });
564
+ const unsubscribeRefreshFailed = manager.on("refreshFailed", (error2) => {
565
+ setError(error2);
566
+ setState(manager.getState());
567
+ });
568
+ const unsubscribeLogout = manager.on("logout", () => {
569
+ setState(manager.getState());
570
+ });
571
+ const unsubscribeIdle = manager.on("idle", () => {
572
+ setState(manager.getState());
573
+ });
574
+ const unsubscribeIdleWarning = manager.on("idleWarning", (data) => {
575
+ setTimeUntilIdle(data.timeRemaining);
576
+ });
577
+ const unsubscribeActivity = manager.on("activity", () => {
578
+ setTimeUntilIdle(0);
579
+ });
580
+ if (autoInit !== false && (autoInit === true || loginResponse)) {
581
+ manager.init(loginResponse);
582
+ }
496
583
  return () => {
497
- subscriptions.forEach((unsub) => unsub());
498
- unsubscribeRef.current.clear();
584
+ unsubscribeInitialized();
585
+ unsubscribeTokenRefreshed();
586
+ unsubscribeRefreshFailed();
587
+ unsubscribeLogout();
588
+ unsubscribeIdle();
589
+ unsubscribeIdleWarning();
590
+ unsubscribeActivity();
499
591
  };
500
- }, [enabled, config, onSessionExpiring, onSessionExpired, onIdle, onRefreshSuccess]);
592
+ }, [options]);
501
593
  useEffect(() => {
502
- if (!enabled || !sessionState.isActive)
594
+ var _a;
595
+ if (!((_a = managerRef.current) == null ? void 0 : _a.isActive())) {
503
596
  return;
504
- const interval = setInterval(() => {
505
- const manager = managerRef.current;
506
- if (manager) {
507
- setSessionState(manager.getState());
597
+ }
598
+ const updateCountdown = () => {
599
+ setState(managerRef.current.getState());
600
+ };
601
+ updateTimerRef.current = setInterval(updateCountdown, 1e3);
602
+ return () => {
603
+ if (updateTimerRef.current) {
604
+ clearInterval(updateTimerRef.current);
508
605
  }
509
- }, 5e3);
510
- return () => clearInterval(interval);
511
- }, [enabled, sessionState.isActive]);
512
- const extendSession = useCallback(() => {
513
- var _a;
514
- (_a = managerRef.current) == null ? void 0 : _a.extendSession();
606
+ };
515
607
  }, []);
516
608
  const refreshToken = useCallback(async () => {
517
- var _a;
518
- return (_a = managerRef.current) == null ? void 0 : _a.refreshToken();
609
+ if (managerRef.current) {
610
+ return managerRef.current.refreshToken();
611
+ }
612
+ return false;
519
613
  }, []);
520
614
  const logout = useCallback(async () => {
521
- var _a;
522
- return (_a = managerRef.current) == null ? void 0 : _a.logout();
615
+ if (managerRef.current) {
616
+ return managerRef.current.logout();
617
+ }
523
618
  }, []);
524
- const updateConfig = useCallback((newConfig) => {
525
- var _a;
526
- (_a = managerRef.current) == null ? void 0 : _a.updateConfig(newConfig);
619
+ const dismissIdleWarning = useCallback(() => {
620
+ setTimeUntilIdle(0);
527
621
  }, []);
528
622
  return {
529
- sessionState,
530
- timeUntilExpiry: null,
623
+ isActive: state.isActive,
624
+ isIdle: state.isIdle,
625
+ isRefreshing: state.isRefreshing,
626
+ refreshAttempts: state.refreshAttempts,
627
+ timeUntilTimeout,
531
628
  timeUntilIdle,
532
- idleWarningVisible,
533
- extendSession,
629
+ error,
534
630
  refreshToken,
535
631
  logout,
536
- updateConfig,
537
- manager: managerRef.current
632
+ dismissIdleWarning
538
633
  };
539
634
  }
540
635
 
@@ -585,12 +680,17 @@ export {
585
680
  SessionStatus,
586
681
  clearToken,
587
682
  getSessionManager,
683
+ getStoredRefreshToken,
588
684
  getStoredToken,
589
685
  getTimeUntilExpiry,
590
686
  getTokenInfo,
687
+ initializeAuth,
688
+ isAuthenticated,
591
689
  isTokenExpired,
592
690
  resetSessionManager,
691
+ storeRefreshToken,
593
692
  storeToken,
693
+ triggerAuthChange,
594
694
  useSessionTimeout,
595
695
  validateToken
596
696
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "formreader-session-timeout",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Session timeout microfrontend: configurable JWT expiry decoding and refresh with idle tracking",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",