formreader-session-timeout 0.2.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,553 @@
1
+ // types/index.ts
2
+ var DEFAULT_SESSION_CONFIG = {
3
+ refreshThresholdMs: 2 * 60 * 1e3,
4
+ // 2 minutes before expiry
5
+ idleCheckIntervalMs: 10 * 1e3,
6
+ // Check every 10 seconds
7
+ idleTimeoutMs: 15 * 60 * 1e3,
8
+ // 15 minutes of inactivity
9
+ maxSessionDurationMs: 8 * 60 * 60 * 1e3,
10
+ // 8 hours max
11
+ refreshEndpoint: "/auth/refresh/",
12
+ logoutEndpoint: "/auth/logout/",
13
+ showIdleWarning: true,
14
+ idleWarningThresholdMs: 2 * 60 * 1e3,
15
+ // 2 minutes before idle timeout
16
+ autoRefresh: true,
17
+ debug: false
18
+ };
19
+
20
+ // services/tokenService.ts
21
+ function decodeJWT(token) {
22
+ try {
23
+ const parts = token.split(".");
24
+ if (parts.length !== 3) {
25
+ throw new Error("Invalid JWT format");
26
+ }
27
+ const decodedPayload = JSON.parse(
28
+ atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"))
29
+ );
30
+ return decodedPayload;
31
+ } catch (error) {
32
+ console.error("Failed to decode JWT:", error);
33
+ return null;
34
+ }
35
+ }
36
+ function getTokenInfo(token) {
37
+ const payload = decodeJWT(token);
38
+ if (!payload || !payload.exp) {
39
+ return null;
40
+ }
41
+ const expiresAt = payload.exp * 1e3;
42
+ const expiresIn = expiresAt - Date.now();
43
+ return {
44
+ token,
45
+ expiresAt,
46
+ expiresIn,
47
+ payload
48
+ };
49
+ }
50
+ function isTokenExpired(token, bufferMs = 0) {
51
+ const tokenInfo = getTokenInfo(token);
52
+ if (!tokenInfo) {
53
+ return true;
54
+ }
55
+ return tokenInfo.expiresIn - bufferMs <= 0;
56
+ }
57
+ function getTimeUntilExpiry(token) {
58
+ const tokenInfo = getTokenInfo(token);
59
+ if (!tokenInfo) {
60
+ return 0;
61
+ }
62
+ return Math.max(0, tokenInfo.expiresIn);
63
+ }
64
+ function getStoredToken() {
65
+ return sessionStorage.getItem("authToken") || localStorage.getItem("authToken");
66
+ }
67
+ 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
+ }
75
+ }
76
+ function clearToken() {
77
+ sessionStorage.removeItem("authToken");
78
+ localStorage.removeItem("authToken");
79
+ }
80
+ 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 };
95
+ }
96
+
97
+ // services/sessionManager.ts
98
+ function defaultFetchClient() {
99
+ return {
100
+ post: async (url, body, options) => {
101
+ const headers = {
102
+ "Content-Type": "application/json",
103
+ ...options && options.headers ? options.headers : {}
104
+ };
105
+ const resp = await fetch(url, {
106
+ method: "POST",
107
+ headers,
108
+ body: body ? JSON.stringify(body) : void 0
109
+ });
110
+ const data = await resp.json().catch(() => ({}));
111
+ return { data };
112
+ }
113
+ };
114
+ }
115
+ var SessionManager = class {
116
+ constructor(config) {
117
+ this.refreshTimer = null;
118
+ this.idleTimer = null;
119
+ this.idleCheckTimer = null;
120
+ this.maxSessionTimer = null;
121
+ this.listeners = /* @__PURE__ */ new Map();
122
+ this.requestDeduplication = /* @__PURE__ */ new Map();
123
+ this.config = {
124
+ ...DEFAULT_SESSION_CONFIG,
125
+ ...config
126
+ };
127
+ this.state = {
128
+ isActive: false,
129
+ isIdle: false,
130
+ lastActivityTime: Date.now(),
131
+ refreshAttempts: 0,
132
+ isRefreshing: false
133
+ };
134
+ this.log("SessionManager initialized with config:", this.config);
135
+ }
136
+ /**
137
+ * Initialize session management
138
+ */
139
+ init() {
140
+ const token = getStoredToken();
141
+ if (!token) {
142
+ this.log("No token found, session not initialized");
143
+ return;
144
+ }
145
+ const validation = validateToken(token);
146
+ if (!validation.valid) {
147
+ this.log("Invalid token:", validation.error);
148
+ this.logout();
149
+ return;
150
+ }
151
+ this.state.isActive = true;
152
+ this.setupTokenRefresh();
153
+ this.setupIdleTracking();
154
+ this.setupMaxSessionDuration();
155
+ this.emit("initialized");
156
+ this.log("Session initialized successfully");
157
+ }
158
+ /**
159
+ * Setup token refresh before expiry
160
+ */
161
+ setupTokenRefresh() {
162
+ const token = getStoredToken();
163
+ if (!token)
164
+ return;
165
+ const timeUntilExpiry = getTimeUntilExpiry(token);
166
+ const refreshAt = Math.max(0, timeUntilExpiry - this.config.refreshThresholdMs);
167
+ this.log(`Token expires in ${timeUntilExpiry}ms, will refresh in ${refreshAt}ms`);
168
+ if (this.refreshTimer) {
169
+ clearTimeout(this.refreshTimer);
170
+ }
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");
178
+ this.refreshToken();
179
+ }
180
+ }
181
+ /**
182
+ * Setup idle activity tracking
183
+ */
184
+ setupIdleTracking() {
185
+ const events = ["mousedown", "keydown", "scroll", "touchstart", "click"];
186
+ const handleActivity = () => {
187
+ this.state.lastActivityTime = Date.now();
188
+ this.state.isIdle = false;
189
+ 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
+ };
201
+ events.forEach((event) => {
202
+ window.addEventListener(event, handleActivity, { passive: true });
203
+ });
204
+ this.idleTimer = setTimeout(() => {
205
+ var _a, _b;
206
+ this.state.isIdle = true;
207
+ this.emit("idle");
208
+ this.log("User idle timeout triggered");
209
+ (_b = (_a = this.config).onIdle) == null ? void 0 : _b.call(_a);
210
+ }, this.config.idleTimeoutMs);
211
+ this.idleCheckTimer = setInterval(() => {
212
+ const timeSinceActivity = Date.now() - this.state.lastActivityTime;
213
+ const timeUntilIdle = this.config.idleTimeoutMs - timeSinceActivity;
214
+ if (this.config.showIdleWarning && timeUntilIdle <= this.config.idleWarningThresholdMs && !this.state.isIdle) {
215
+ this.emit("idleWarning", { timeRemaining: timeUntilIdle });
216
+ }
217
+ }, this.config.idleCheckIntervalMs);
218
+ this.listeners.set("cleanup-events", events.map((event) => () => {
219
+ window.removeEventListener(event, handleActivity);
220
+ }));
221
+ }
222
+ /**
223
+ * Setup max session duration timer
224
+ */
225
+ setupMaxSessionDuration() {
226
+ if (this.maxSessionTimer) {
227
+ clearTimeout(this.maxSessionTimer);
228
+ }
229
+ this.maxSessionTimer = setTimeout(() => {
230
+ this.log("Max session duration reached, logging out");
231
+ this.logout();
232
+ }, this.config.maxSessionDurationMs);
233
+ }
234
+ /**
235
+ * Refresh token
236
+ */
237
+ async refreshToken() {
238
+ var _a, _b, _c, _d;
239
+ if (this.state.isRefreshing) {
240
+ this.log("Refresh already in progress, returning pending promise");
241
+ return this.requestDeduplication.get("refresh");
242
+ }
243
+ this.state.isRefreshing = true;
244
+ this.state.refreshAttempts++;
245
+ const token = getStoredToken();
246
+ if (!token) {
247
+ this.state.isRefreshing = false;
248
+ return false;
249
+ }
250
+ try {
251
+ this.log(`Refreshing token (attempt ${this.state.refreshAttempts})`);
252
+ 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}` } });
255
+ this.requestDeduplication.set("refresh", refreshPromise);
256
+ 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");
260
+ }
261
+ storeToken(newToken);
262
+ this.state.isRefreshing = false;
263
+ this.state.refreshAttempts = 0;
264
+ this.requestDeduplication.delete("refresh");
265
+ this.log("Token refreshed successfully");
266
+ this.emit("tokenRefreshed");
267
+ (_b = (_a = this.config).onRefreshSuccess) == null ? void 0 : _b.call(_a);
268
+ this.setupTokenRefresh();
269
+ return true;
270
+ } catch (error) {
271
+ this.state.isRefreshing = false;
272
+ this.log("Token refresh failed:", error);
273
+ if (this.state.refreshAttempts >= 3) {
274
+ this.log("Max refresh attempts reached, logging out");
275
+ this.logout();
276
+ }
277
+ (_d = (_c = this.config).onRefreshFailure) == null ? void 0 : _d.call(_c, error);
278
+ this.emit("refreshFailed", error);
279
+ return false;
280
+ }
281
+ }
282
+ /**
283
+ * Logout and cleanup
284
+ */
285
+ async logout() {
286
+ var _a, _b;
287
+ try {
288
+ const token = getStoredToken();
289
+ if (token) {
290
+ 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}` } });
293
+ }
294
+ } catch (error) {
295
+ this.log("Logout API call failed:", error);
296
+ }
297
+ this.cleanup();
298
+ this.state.isActive = false;
299
+ this.emit("loggedOut");
300
+ (_b = (_a = this.config).onSessionExpired) == null ? void 0 : _b.call(_a);
301
+ clearToken();
302
+ }
303
+ /**
304
+ * Extend session (reset idle timer)
305
+ */
306
+ extendSession() {
307
+ this.state.lastActivityTime = Date.now();
308
+ this.state.isIdle = false;
309
+ if (this.idleTimer) {
310
+ clearTimeout(this.idleTimer);
311
+ }
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");
318
+ }
319
+ /**
320
+ * Get current state
321
+ */
322
+ getState() {
323
+ return { ...this.state };
324
+ }
325
+ /**
326
+ * Get current config
327
+ */
328
+ getConfig() {
329
+ return { ...this.config };
330
+ }
331
+ /**
332
+ * Update config
333
+ */
334
+ updateConfig(newConfig) {
335
+ this.config = { ...this.config, ...newConfig };
336
+ this.log("Config updated:", this.config);
337
+ if (this.state.isActive) {
338
+ this.cleanup();
339
+ this.init();
340
+ }
341
+ }
342
+ /**
343
+ * Event listeners
344
+ */
345
+ on(event, callback) {
346
+ if (!this.listeners.has(event)) {
347
+ this.listeners.set(event, []);
348
+ }
349
+ this.listeners.get(event).push(callback);
350
+ return () => {
351
+ const callbacks = this.listeners.get(event);
352
+ if (callbacks) {
353
+ const index = callbacks.indexOf(callback);
354
+ if (index > -1) {
355
+ callbacks.splice(index, 1);
356
+ }
357
+ }
358
+ };
359
+ }
360
+ /**
361
+ * Emit event
362
+ */
363
+ emit(event, data) {
364
+ const callbacks = this.listeners.get(event);
365
+ if (callbacks) {
366
+ callbacks.forEach((callback) => callback(data));
367
+ }
368
+ }
369
+ /**
370
+ * Cleanup timers and listeners
371
+ */
372
+ cleanup() {
373
+ if (this.refreshTimer)
374
+ clearTimeout(this.refreshTimer);
375
+ if (this.idleTimer)
376
+ clearTimeout(this.idleTimer);
377
+ if (this.idleCheckTimer)
378
+ clearInterval(this.idleCheckTimer);
379
+ if (this.maxSessionTimer)
380
+ clearTimeout(this.maxSessionTimer);
381
+ this.refreshTimer = null;
382
+ this.idleTimer = null;
383
+ this.idleCheckTimer = null;
384
+ this.maxSessionTimer = null;
385
+ const cleanupFunctions = this.listeners.get("cleanup-events");
386
+ if (cleanupFunctions) {
387
+ cleanupFunctions.forEach((fn) => fn());
388
+ }
389
+ this.listeners.clear();
390
+ }
391
+ /**
392
+ * Destroy session manager
393
+ */
394
+ destroy() {
395
+ this.cleanup();
396
+ this.listeners.clear();
397
+ this.requestDeduplication.clear();
398
+ }
399
+ /**
400
+ * Logging utility
401
+ */
402
+ log(...args) {
403
+ if (this.config.debug) {
404
+ console.log("[SessionManager]", ...args);
405
+ }
406
+ }
407
+ };
408
+ var sessionManagerInstance = null;
409
+ function getSessionManager(config) {
410
+ if (!sessionManagerInstance) {
411
+ sessionManagerInstance = new SessionManager(config || {});
412
+ }
413
+ return sessionManagerInstance;
414
+ }
415
+ function resetSessionManager() {
416
+ if (sessionManagerInstance) {
417
+ sessionManagerInstance.destroy();
418
+ sessionManagerInstance = null;
419
+ }
420
+ }
421
+
422
+ // hooks/useSessionTimeout.ts
423
+ import { useEffect, useState, useCallback, useRef } from "react";
424
+ 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({
434
+ isActive: false,
435
+ isIdle: false,
436
+ lastActivityTime: Date.now(),
437
+ refreshAttempts: 0,
438
+ isRefreshing: false
439
+ });
440
+ const [timeUntilIdle, setTimeUntilIdle] = useState(null);
441
+ const [idleWarningVisible, setIdleWarningVisible] = useState(false);
442
+ const managerRef = useRef(null);
443
+ const unsubscribeRef = useRef(/* @__PURE__ */ new Map());
444
+ useEffect(() => {
445
+ if (!enabled)
446
+ return;
447
+ const manager = getSessionManager({
448
+ ...config,
449
+ onSessionExpiring,
450
+ onSessionExpired,
451
+ onIdle,
452
+ onRefreshSuccess
453
+ });
454
+ 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();
496
+ return () => {
497
+ subscriptions.forEach((unsub) => unsub());
498
+ unsubscribeRef.current.clear();
499
+ };
500
+ }, [enabled, config, onSessionExpiring, onSessionExpired, onIdle, onRefreshSuccess]);
501
+ useEffect(() => {
502
+ if (!enabled || !sessionState.isActive)
503
+ return;
504
+ const interval = setInterval(() => {
505
+ const manager = managerRef.current;
506
+ if (manager) {
507
+ setSessionState(manager.getState());
508
+ }
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();
515
+ }, []);
516
+ const refreshToken = useCallback(async () => {
517
+ var _a;
518
+ return (_a = managerRef.current) == null ? void 0 : _a.refreshToken();
519
+ }, []);
520
+ const logout = useCallback(async () => {
521
+ var _a;
522
+ return (_a = managerRef.current) == null ? void 0 : _a.logout();
523
+ }, []);
524
+ const updateConfig = useCallback((newConfig) => {
525
+ var _a;
526
+ (_a = managerRef.current) == null ? void 0 : _a.updateConfig(newConfig);
527
+ }, []);
528
+ return {
529
+ sessionState,
530
+ timeUntilExpiry: null,
531
+ timeUntilIdle,
532
+ idleWarningVisible,
533
+ extendSession,
534
+ refreshToken,
535
+ logout,
536
+ updateConfig,
537
+ manager: managerRef.current
538
+ };
539
+ }
540
+ export {
541
+ DEFAULT_SESSION_CONFIG,
542
+ SessionManager,
543
+ clearToken,
544
+ getSessionManager,
545
+ getStoredToken,
546
+ getTimeUntilExpiry,
547
+ getTokenInfo,
548
+ isTokenExpired,
549
+ resetSessionManager,
550
+ storeToken,
551
+ useSessionTimeout,
552
+ validateToken
553
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "formreader-session-timeout",
3
+ "version": "0.2.0",
4
+ "description": "Session timeout microfrontend: configurable JWT expiry decoding and refresh with idle tracking",
5
+ "main": "dist/index.cjs.js",
6
+ "module": "dist/index.esm.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsup index.ts --format cjs,esm --dts --out-dir dist",
13
+ "clean": "rm -rf dist"
14
+ },
15
+ "peerDependencies": {
16
+ "react": "^16 || ^17 || ^18"
17
+ },
18
+ "dependencies": {
19
+ "whatwg-fetch": "^3.6.2"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^25.0.3",
23
+ "@types/react": "^19.2.7",
24
+ "tsup": "^6.5.0",
25
+ "typescript": "^4.9.5"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": ""
30
+ },
31
+ "license": "MIT"
32
+ }