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.js ADDED
@@ -0,0 +1,590 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // index.ts
20
+ var session_timeout_exports = {};
21
+ __export(session_timeout_exports, {
22
+ DEFAULT_SESSION_CONFIG: () => DEFAULT_SESSION_CONFIG,
23
+ SessionManager: () => SessionManager,
24
+ clearToken: () => clearToken,
25
+ getSessionManager: () => getSessionManager,
26
+ getStoredToken: () => getStoredToken,
27
+ getTimeUntilExpiry: () => getTimeUntilExpiry,
28
+ getTokenInfo: () => getTokenInfo,
29
+ isTokenExpired: () => isTokenExpired,
30
+ resetSessionManager: () => resetSessionManager,
31
+ storeToken: () => storeToken,
32
+ useSessionTimeout: () => useSessionTimeout,
33
+ validateToken: () => validateToken
34
+ });
35
+ module.exports = __toCommonJS(session_timeout_exports);
36
+
37
+ // types/index.ts
38
+ var DEFAULT_SESSION_CONFIG = {
39
+ refreshThresholdMs: 2 * 60 * 1e3,
40
+ // 2 minutes before expiry
41
+ idleCheckIntervalMs: 10 * 1e3,
42
+ // Check every 10 seconds
43
+ idleTimeoutMs: 15 * 60 * 1e3,
44
+ // 15 minutes of inactivity
45
+ maxSessionDurationMs: 8 * 60 * 60 * 1e3,
46
+ // 8 hours max
47
+ refreshEndpoint: "/auth/refresh/",
48
+ logoutEndpoint: "/auth/logout/",
49
+ showIdleWarning: true,
50
+ idleWarningThresholdMs: 2 * 60 * 1e3,
51
+ // 2 minutes before idle timeout
52
+ autoRefresh: true,
53
+ debug: false
54
+ };
55
+
56
+ // services/tokenService.ts
57
+ function decodeJWT(token) {
58
+ try {
59
+ const parts = token.split(".");
60
+ if (parts.length !== 3) {
61
+ throw new Error("Invalid JWT format");
62
+ }
63
+ const decodedPayload = JSON.parse(
64
+ atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"))
65
+ );
66
+ return decodedPayload;
67
+ } catch (error) {
68
+ console.error("Failed to decode JWT:", error);
69
+ return null;
70
+ }
71
+ }
72
+ function getTokenInfo(token) {
73
+ const payload = decodeJWT(token);
74
+ if (!payload || !payload.exp) {
75
+ return null;
76
+ }
77
+ const expiresAt = payload.exp * 1e3;
78
+ const expiresIn = expiresAt - Date.now();
79
+ return {
80
+ token,
81
+ expiresAt,
82
+ expiresIn,
83
+ payload
84
+ };
85
+ }
86
+ function isTokenExpired(token, bufferMs = 0) {
87
+ const tokenInfo = getTokenInfo(token);
88
+ if (!tokenInfo) {
89
+ return true;
90
+ }
91
+ return tokenInfo.expiresIn - bufferMs <= 0;
92
+ }
93
+ function getTimeUntilExpiry(token) {
94
+ const tokenInfo = getTokenInfo(token);
95
+ if (!tokenInfo) {
96
+ return 0;
97
+ }
98
+ return Math.max(0, tokenInfo.expiresIn);
99
+ }
100
+ function getStoredToken() {
101
+ return sessionStorage.getItem("authToken") || localStorage.getItem("authToken");
102
+ }
103
+ function storeToken(token, persistent = false) {
104
+ if (persistent) {
105
+ localStorage.setItem("authToken", token);
106
+ sessionStorage.removeItem("authToken");
107
+ } else {
108
+ sessionStorage.setItem("authToken", token);
109
+ localStorage.removeItem("authToken");
110
+ }
111
+ }
112
+ function clearToken() {
113
+ sessionStorage.removeItem("authToken");
114
+ localStorage.removeItem("authToken");
115
+ }
116
+ function validateToken(token) {
117
+ if (!token || typeof token !== "string") {
118
+ return { valid: false, error: "Token is missing or invalid" };
119
+ }
120
+ const payload = decodeJWT(token);
121
+ if (!payload) {
122
+ return { valid: false, error: "Failed to decode token" };
123
+ }
124
+ if (!payload.exp) {
125
+ return { valid: false, error: "Token missing expiry time" };
126
+ }
127
+ if (isTokenExpired(token)) {
128
+ return { valid: false, error: "Token has expired" };
129
+ }
130
+ return { valid: true };
131
+ }
132
+
133
+ // services/sessionManager.ts
134
+ function defaultFetchClient() {
135
+ return {
136
+ post: async (url, body, options) => {
137
+ const headers = {
138
+ "Content-Type": "application/json",
139
+ ...options && options.headers ? options.headers : {}
140
+ };
141
+ const resp = await fetch(url, {
142
+ method: "POST",
143
+ headers,
144
+ body: body ? JSON.stringify(body) : void 0
145
+ });
146
+ const data = await resp.json().catch(() => ({}));
147
+ return { data };
148
+ }
149
+ };
150
+ }
151
+ var SessionManager = class {
152
+ constructor(config) {
153
+ this.refreshTimer = null;
154
+ this.idleTimer = null;
155
+ this.idleCheckTimer = null;
156
+ this.maxSessionTimer = null;
157
+ this.listeners = /* @__PURE__ */ new Map();
158
+ this.requestDeduplication = /* @__PURE__ */ new Map();
159
+ this.config = {
160
+ ...DEFAULT_SESSION_CONFIG,
161
+ ...config
162
+ };
163
+ this.state = {
164
+ isActive: false,
165
+ isIdle: false,
166
+ lastActivityTime: Date.now(),
167
+ refreshAttempts: 0,
168
+ isRefreshing: false
169
+ };
170
+ this.log("SessionManager initialized with config:", this.config);
171
+ }
172
+ /**
173
+ * Initialize session management
174
+ */
175
+ init() {
176
+ const token = getStoredToken();
177
+ if (!token) {
178
+ this.log("No token found, session not initialized");
179
+ return;
180
+ }
181
+ const validation = validateToken(token);
182
+ if (!validation.valid) {
183
+ this.log("Invalid token:", validation.error);
184
+ this.logout();
185
+ return;
186
+ }
187
+ this.state.isActive = true;
188
+ this.setupTokenRefresh();
189
+ this.setupIdleTracking();
190
+ this.setupMaxSessionDuration();
191
+ this.emit("initialized");
192
+ this.log("Session initialized successfully");
193
+ }
194
+ /**
195
+ * Setup token refresh before expiry
196
+ */
197
+ setupTokenRefresh() {
198
+ const token = getStoredToken();
199
+ if (!token)
200
+ return;
201
+ const timeUntilExpiry = getTimeUntilExpiry(token);
202
+ const refreshAt = Math.max(0, timeUntilExpiry - this.config.refreshThresholdMs);
203
+ this.log(`Token expires in ${timeUntilExpiry}ms, will refresh in ${refreshAt}ms`);
204
+ if (this.refreshTimer) {
205
+ clearTimeout(this.refreshTimer);
206
+ }
207
+ if (this.config.autoRefresh && refreshAt > 0) {
208
+ this.refreshTimer = setTimeout(() => {
209
+ this.log("Scheduled token refresh triggered");
210
+ this.refreshToken();
211
+ }, refreshAt);
212
+ } else if (refreshAt <= 0 && this.config.autoRefresh) {
213
+ this.log("Token near expiry, refreshing immediately");
214
+ this.refreshToken();
215
+ }
216
+ }
217
+ /**
218
+ * Setup idle activity tracking
219
+ */
220
+ setupIdleTracking() {
221
+ const events = ["mousedown", "keydown", "scroll", "touchstart", "click"];
222
+ const handleActivity = () => {
223
+ this.state.lastActivityTime = Date.now();
224
+ this.state.isIdle = false;
225
+ this.emit("activity");
226
+ if (this.idleTimer) {
227
+ clearTimeout(this.idleTimer);
228
+ }
229
+ this.idleTimer = setTimeout(() => {
230
+ var _a, _b;
231
+ this.state.isIdle = true;
232
+ this.emit("idle");
233
+ this.log("User idle timeout triggered");
234
+ (_b = (_a = this.config).onIdle) == null ? void 0 : _b.call(_a);
235
+ }, this.config.idleTimeoutMs);
236
+ };
237
+ events.forEach((event) => {
238
+ window.addEventListener(event, handleActivity, { passive: true });
239
+ });
240
+ this.idleTimer = setTimeout(() => {
241
+ var _a, _b;
242
+ this.state.isIdle = true;
243
+ this.emit("idle");
244
+ this.log("User idle timeout triggered");
245
+ (_b = (_a = this.config).onIdle) == null ? void 0 : _b.call(_a);
246
+ }, this.config.idleTimeoutMs);
247
+ this.idleCheckTimer = setInterval(() => {
248
+ const timeSinceActivity = Date.now() - this.state.lastActivityTime;
249
+ const timeUntilIdle = this.config.idleTimeoutMs - timeSinceActivity;
250
+ if (this.config.showIdleWarning && timeUntilIdle <= this.config.idleWarningThresholdMs && !this.state.isIdle) {
251
+ this.emit("idleWarning", { timeRemaining: timeUntilIdle });
252
+ }
253
+ }, this.config.idleCheckIntervalMs);
254
+ this.listeners.set("cleanup-events", events.map((event) => () => {
255
+ window.removeEventListener(event, handleActivity);
256
+ }));
257
+ }
258
+ /**
259
+ * Setup max session duration timer
260
+ */
261
+ setupMaxSessionDuration() {
262
+ if (this.maxSessionTimer) {
263
+ clearTimeout(this.maxSessionTimer);
264
+ }
265
+ this.maxSessionTimer = setTimeout(() => {
266
+ this.log("Max session duration reached, logging out");
267
+ this.logout();
268
+ }, this.config.maxSessionDurationMs);
269
+ }
270
+ /**
271
+ * Refresh token
272
+ */
273
+ async refreshToken() {
274
+ var _a, _b, _c, _d;
275
+ if (this.state.isRefreshing) {
276
+ this.log("Refresh already in progress, returning pending promise");
277
+ return this.requestDeduplication.get("refresh");
278
+ }
279
+ this.state.isRefreshing = true;
280
+ this.state.refreshAttempts++;
281
+ const token = getStoredToken();
282
+ if (!token) {
283
+ this.state.isRefreshing = false;
284
+ return false;
285
+ }
286
+ try {
287
+ this.log(`Refreshing token (attempt ${this.state.refreshAttempts})`);
288
+ const client = this.config.httpClient || defaultFetchClient();
289
+ const refreshPayload = this.config.refreshPayloadFormatter ? this.config.refreshPayloadFormatter(token) : { token };
290
+ const refreshPromise = client.post(this.config.refreshEndpoint, refreshPayload, { headers: { Authorization: `Bearer ${token}` } });
291
+ this.requestDeduplication.set("refresh", refreshPromise);
292
+ const response = await refreshPromise;
293
+ const newToken = response.data.token || response.data.access_token;
294
+ if (!newToken) {
295
+ throw new Error("No token in refresh response");
296
+ }
297
+ storeToken(newToken);
298
+ this.state.isRefreshing = false;
299
+ this.state.refreshAttempts = 0;
300
+ this.requestDeduplication.delete("refresh");
301
+ this.log("Token refreshed successfully");
302
+ this.emit("tokenRefreshed");
303
+ (_b = (_a = this.config).onRefreshSuccess) == null ? void 0 : _b.call(_a);
304
+ this.setupTokenRefresh();
305
+ return true;
306
+ } catch (error) {
307
+ this.state.isRefreshing = false;
308
+ this.log("Token refresh failed:", error);
309
+ if (this.state.refreshAttempts >= 3) {
310
+ this.log("Max refresh attempts reached, logging out");
311
+ this.logout();
312
+ }
313
+ (_d = (_c = this.config).onRefreshFailure) == null ? void 0 : _d.call(_c, error);
314
+ this.emit("refreshFailed", error);
315
+ return false;
316
+ }
317
+ }
318
+ /**
319
+ * Logout and cleanup
320
+ */
321
+ async logout() {
322
+ var _a, _b;
323
+ try {
324
+ const token = getStoredToken();
325
+ if (token) {
326
+ const client = this.config.httpClient || defaultFetchClient();
327
+ const logoutPayload = this.config.logoutPayloadFormatter ? this.config.logoutPayloadFormatter(token) : { token };
328
+ await client.post(this.config.logoutEndpoint, logoutPayload, { headers: { Authorization: `Bearer ${token}` } });
329
+ }
330
+ } catch (error) {
331
+ this.log("Logout API call failed:", error);
332
+ }
333
+ this.cleanup();
334
+ this.state.isActive = false;
335
+ this.emit("loggedOut");
336
+ (_b = (_a = this.config).onSessionExpired) == null ? void 0 : _b.call(_a);
337
+ clearToken();
338
+ }
339
+ /**
340
+ * Extend session (reset idle timer)
341
+ */
342
+ extendSession() {
343
+ this.state.lastActivityTime = Date.now();
344
+ this.state.isIdle = false;
345
+ if (this.idleTimer) {
346
+ clearTimeout(this.idleTimer);
347
+ }
348
+ this.idleTimer = setTimeout(() => {
349
+ this.state.isIdle = true;
350
+ this.emit("idle");
351
+ }, this.config.idleTimeoutMs);
352
+ this.log("Session extended");
353
+ this.emit("sessionExtended");
354
+ }
355
+ /**
356
+ * Get current state
357
+ */
358
+ getState() {
359
+ return { ...this.state };
360
+ }
361
+ /**
362
+ * Get current config
363
+ */
364
+ getConfig() {
365
+ return { ...this.config };
366
+ }
367
+ /**
368
+ * Update config
369
+ */
370
+ updateConfig(newConfig) {
371
+ this.config = { ...this.config, ...newConfig };
372
+ this.log("Config updated:", this.config);
373
+ if (this.state.isActive) {
374
+ this.cleanup();
375
+ this.init();
376
+ }
377
+ }
378
+ /**
379
+ * Event listeners
380
+ */
381
+ on(event, callback) {
382
+ if (!this.listeners.has(event)) {
383
+ this.listeners.set(event, []);
384
+ }
385
+ this.listeners.get(event).push(callback);
386
+ return () => {
387
+ const callbacks = this.listeners.get(event);
388
+ if (callbacks) {
389
+ const index = callbacks.indexOf(callback);
390
+ if (index > -1) {
391
+ callbacks.splice(index, 1);
392
+ }
393
+ }
394
+ };
395
+ }
396
+ /**
397
+ * Emit event
398
+ */
399
+ emit(event, data) {
400
+ const callbacks = this.listeners.get(event);
401
+ if (callbacks) {
402
+ callbacks.forEach((callback) => callback(data));
403
+ }
404
+ }
405
+ /**
406
+ * Cleanup timers and listeners
407
+ */
408
+ cleanup() {
409
+ if (this.refreshTimer)
410
+ clearTimeout(this.refreshTimer);
411
+ if (this.idleTimer)
412
+ clearTimeout(this.idleTimer);
413
+ if (this.idleCheckTimer)
414
+ clearInterval(this.idleCheckTimer);
415
+ if (this.maxSessionTimer)
416
+ clearTimeout(this.maxSessionTimer);
417
+ this.refreshTimer = null;
418
+ this.idleTimer = null;
419
+ this.idleCheckTimer = null;
420
+ this.maxSessionTimer = null;
421
+ const cleanupFunctions = this.listeners.get("cleanup-events");
422
+ if (cleanupFunctions) {
423
+ cleanupFunctions.forEach((fn) => fn());
424
+ }
425
+ this.listeners.clear();
426
+ }
427
+ /**
428
+ * Destroy session manager
429
+ */
430
+ destroy() {
431
+ this.cleanup();
432
+ this.listeners.clear();
433
+ this.requestDeduplication.clear();
434
+ }
435
+ /**
436
+ * Logging utility
437
+ */
438
+ log(...args) {
439
+ if (this.config.debug) {
440
+ console.log("[SessionManager]", ...args);
441
+ }
442
+ }
443
+ };
444
+ var sessionManagerInstance = null;
445
+ function getSessionManager(config) {
446
+ if (!sessionManagerInstance) {
447
+ sessionManagerInstance = new SessionManager(config || {});
448
+ }
449
+ return sessionManagerInstance;
450
+ }
451
+ function resetSessionManager() {
452
+ if (sessionManagerInstance) {
453
+ sessionManagerInstance.destroy();
454
+ sessionManagerInstance = null;
455
+ }
456
+ }
457
+
458
+ // hooks/useSessionTimeout.ts
459
+ var import_react = require("react");
460
+ function useSessionTimeout(options = {}) {
461
+ const {
462
+ config,
463
+ enabled = true,
464
+ onSessionExpiring,
465
+ onSessionExpired,
466
+ onIdle,
467
+ onRefreshSuccess
468
+ } = options;
469
+ const [sessionState, setSessionState] = (0, import_react.useState)({
470
+ isActive: false,
471
+ isIdle: false,
472
+ lastActivityTime: Date.now(),
473
+ refreshAttempts: 0,
474
+ isRefreshing: false
475
+ });
476
+ const [timeUntilIdle, setTimeUntilIdle] = (0, import_react.useState)(null);
477
+ const [idleWarningVisible, setIdleWarningVisible] = (0, import_react.useState)(false);
478
+ const managerRef = (0, import_react.useRef)(null);
479
+ const unsubscribeRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
480
+ (0, import_react.useEffect)(() => {
481
+ if (!enabled)
482
+ return;
483
+ const manager = getSessionManager({
484
+ ...config,
485
+ onSessionExpiring,
486
+ onSessionExpired,
487
+ onIdle,
488
+ onRefreshSuccess
489
+ });
490
+ managerRef.current = manager;
491
+ const subscriptions = /* @__PURE__ */ new Map();
492
+ subscriptions.set(
493
+ "initialized",
494
+ manager.on("initialized", () => {
495
+ setSessionState(manager.getState());
496
+ })
497
+ );
498
+ subscriptions.set(
499
+ "tokenRefreshed",
500
+ manager.on("tokenRefreshed", () => {
501
+ setSessionState(manager.getState());
502
+ })
503
+ );
504
+ subscriptions.set(
505
+ "idle",
506
+ manager.on("idle", () => {
507
+ setSessionState(manager.getState());
508
+ })
509
+ );
510
+ subscriptions.set(
511
+ "activity",
512
+ manager.on("activity", () => {
513
+ setSessionState(manager.getState());
514
+ setIdleWarningVisible(false);
515
+ })
516
+ );
517
+ subscriptions.set(
518
+ "idleWarning",
519
+ manager.on("idleWarning", (data) => {
520
+ setIdleWarningVisible(true);
521
+ setTimeUntilIdle(data.timeRemaining);
522
+ })
523
+ );
524
+ subscriptions.set(
525
+ "loggedOut",
526
+ manager.on("loggedOut", () => {
527
+ setSessionState(manager.getState());
528
+ })
529
+ );
530
+ unsubscribeRef.current = subscriptions;
531
+ manager.init();
532
+ return () => {
533
+ subscriptions.forEach((unsub) => unsub());
534
+ unsubscribeRef.current.clear();
535
+ };
536
+ }, [enabled, config, onSessionExpiring, onSessionExpired, onIdle, onRefreshSuccess]);
537
+ (0, import_react.useEffect)(() => {
538
+ if (!enabled || !sessionState.isActive)
539
+ return;
540
+ const interval = setInterval(() => {
541
+ const manager = managerRef.current;
542
+ if (manager) {
543
+ setSessionState(manager.getState());
544
+ }
545
+ }, 5e3);
546
+ return () => clearInterval(interval);
547
+ }, [enabled, sessionState.isActive]);
548
+ const extendSession = (0, import_react.useCallback)(() => {
549
+ var _a;
550
+ (_a = managerRef.current) == null ? void 0 : _a.extendSession();
551
+ }, []);
552
+ const refreshToken = (0, import_react.useCallback)(async () => {
553
+ var _a;
554
+ return (_a = managerRef.current) == null ? void 0 : _a.refreshToken();
555
+ }, []);
556
+ const logout = (0, import_react.useCallback)(async () => {
557
+ var _a;
558
+ return (_a = managerRef.current) == null ? void 0 : _a.logout();
559
+ }, []);
560
+ const updateConfig = (0, import_react.useCallback)((newConfig) => {
561
+ var _a;
562
+ (_a = managerRef.current) == null ? void 0 : _a.updateConfig(newConfig);
563
+ }, []);
564
+ return {
565
+ sessionState,
566
+ timeUntilExpiry: null,
567
+ timeUntilIdle,
568
+ idleWarningVisible,
569
+ extendSession,
570
+ refreshToken,
571
+ logout,
572
+ updateConfig,
573
+ manager: managerRef.current
574
+ };
575
+ }
576
+ // Annotate the CommonJS export names for ESM import in node:
577
+ 0 && (module.exports = {
578
+ DEFAULT_SESSION_CONFIG,
579
+ SessionManager,
580
+ clearToken,
581
+ getSessionManager,
582
+ getStoredToken,
583
+ getTimeUntilExpiry,
584
+ getTokenInfo,
585
+ isTokenExpired,
586
+ resetSessionManager,
587
+ storeToken,
588
+ useSessionTimeout,
589
+ validateToken
590
+ });