win-portal-auth-sdk 1.2.1 → 1.3.1

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 (38) hide show
  1. package/README.md +102 -9
  2. package/dist/client/api/auth.api.d.ts +25 -1
  3. package/dist/client/api/auth.api.d.ts.map +1 -1
  4. package/dist/client/api/auth.api.js +30 -1
  5. package/dist/client/api/files.api.d.ts +0 -1
  6. package/dist/client/api/files.api.d.ts.map +1 -1
  7. package/dist/client/api/index.d.ts +2 -0
  8. package/dist/client/api/index.d.ts.map +1 -1
  9. package/dist/client/api/index.js +3 -1
  10. package/dist/client/api/license.api.d.ts +74 -0
  11. package/dist/client/api/license.api.d.ts.map +1 -0
  12. package/dist/client/api/license.api.js +50 -0
  13. package/dist/client/api/system-config.api.d.ts +11 -1
  14. package/dist/client/api/system-config.api.d.ts.map +1 -1
  15. package/dist/client/api/system-config.api.js +21 -0
  16. package/dist/client/auth-client.d.ts +278 -1
  17. package/dist/client/auth-client.d.ts.map +1 -1
  18. package/dist/client/auth-client.js +705 -10
  19. package/dist/client/index.d.ts +2 -0
  20. package/dist/client/index.d.ts.map +1 -1
  21. package/dist/client/index.js +15 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +5 -0
  25. package/dist/types/auth.types.d.ts +9 -0
  26. package/dist/types/auth.types.d.ts.map +1 -1
  27. package/dist/types/index.d.ts +18 -0
  28. package/dist/types/index.d.ts.map +1 -1
  29. package/dist/types/system-config.types.d.ts +37 -0
  30. package/dist/types/system-config.types.d.ts.map +1 -1
  31. package/dist/utils/logger.d.ts +23 -0
  32. package/dist/utils/logger.d.ts.map +1 -0
  33. package/dist/utils/logger.js +49 -0
  34. package/dist/utils/token-utils.d.ts +60 -0
  35. package/dist/utils/token-utils.d.ts.map +1 -0
  36. package/dist/utils/token-utils.js +116 -0
  37. package/package.json +1 -2
  38. package/TYPE_SAFETY.md +0 -97
@@ -13,16 +13,61 @@ Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.AuthClient = void 0;
14
14
  const axios_1 = __importDefault(require("axios"));
15
15
  const api_1 = require("./api");
16
+ const token_utils_1 = require("../utils/token-utils");
17
+ const logger_1 = require("../utils/logger");
18
+ // Constants - Default values
19
+ const DEFAULT_INACTIVITY_TIMEOUT_SECONDS = 30 * 60; // 30 minutes
20
+ const DEFAULT_WARNING_SECONDS = 5 * 60; // 5 minutes
21
+ const DEFAULT_SESSION_CHECK_INTERVAL_SECONDS = 30; // 30 seconds
22
+ // Internal constants - Rate limiting (ไม่ควรให้ customize)
23
+ const REFRESH_COOLDOWN_MS = 30 * 1000; // 30 seconds - rate limiting for refresh
24
+ const WARNING_COOLDOWN_MS = 60 * 1000; // 1 minute - rate limiting for warnings
25
+ // Internal constants - Performance tuning (สามารถ customize ได้ผ่าน advanced config)
26
+ const DEFAULT_ACTIVITY_THROTTLE_MS = 1000; // 1 second - throttle activity handler
27
+ const DEFAULT_REFRESH_TIMEOUT_MS = 5000; // 5 seconds - refresh timeout
16
28
  class AuthClient {
17
29
  constructor(config) {
18
30
  this.token = null;
19
31
  this.authType = 'hybrid'; // Default to hybrid
32
+ // Automatic refresh configuration
33
+ this.refreshTokenCallbacks = null;
34
+ this.jwtConfigPromise = null;
35
+ this.refreshThresholdMinutes = null;
36
+ this.automaticRefreshEnabled = null;
37
+ this.lastRefreshAttempt = 0;
38
+ this.refreshPromise = null;
39
+ // Session expiration monitoring
40
+ this.sessionExpirationCallbacks = null;
41
+ this.sessionManagementConfig = null;
42
+ this.sessionManagementConfigPromise = null;
43
+ this.sessionExpirationCheckInterval = null;
44
+ this.sessionExpirationCheckIntervalSeconds = DEFAULT_SESSION_CHECK_INTERVAL_SECONDS;
45
+ this.lastWarningTime = 0;
46
+ // Inactivity detection
47
+ this.inactivityCallbacks = null;
48
+ this.inactivityTimeout = null;
49
+ this.inactivityWarningTimeout = null;
50
+ this.lastActivityTime = 0;
51
+ this.inactivityDetectionEnabled = false;
52
+ this.inactivityTimeoutSeconds = 0;
53
+ this.inactivityWarningSeconds = 0;
54
+ this.activityEvents = [];
55
+ this.activityHandler = null;
56
+ this.activityThrottleTimeout = null;
57
+ this.activityThrottleMs = DEFAULT_ACTIVITY_THROTTLE_MS;
58
+ this.refreshTimeoutMs = DEFAULT_REFRESH_TIMEOUT_MS;
20
59
  this.apiKey = config.apiKey;
21
60
  this.apiKeyHeader = config.apiKeyHeader || 'X-API-Key';
22
- console.log('[AuthClient] Initializing with config:', {
61
+ // Apply advanced config if provided
62
+ if (config.advanced) {
63
+ this.activityThrottleMs = config.advanced.activityThrottleMs ?? DEFAULT_ACTIVITY_THROTTLE_MS;
64
+ this.refreshTimeoutMs = config.advanced.refreshTimeoutMs ?? DEFAULT_REFRESH_TIMEOUT_MS;
65
+ }
66
+ logger_1.logger.debug('Initializing with config:', {
23
67
  baseURL: config.baseURL,
24
68
  timeout: config.timeout,
25
69
  apiKeyHeader: this.apiKeyHeader,
70
+ advanced: config.advanced,
26
71
  });
27
72
  this.client = axios_1.default.create({
28
73
  baseURL: config.baseURL,
@@ -31,8 +76,8 @@ class AuthClient {
31
76
  'Content-Type': 'application/json',
32
77
  },
33
78
  });
34
- // Add request interceptor to inject API key
35
- this.client.interceptors.request.use((requestConfig) => {
79
+ // Add request interceptor to inject API key and handle automatic refresh
80
+ this.client.interceptors.request.use(async (requestConfig) => {
36
81
  if (requestConfig.headers) {
37
82
  requestConfig.headers[this.apiKeyHeader] = this.apiKey;
38
83
  // Inject JWT token if available
@@ -40,19 +85,93 @@ class AuthClient {
40
85
  requestConfig.headers['Authorization'] = `Bearer ${this.token}`;
41
86
  // ✅ Inject X-Auth-Type header for better performance
42
87
  requestConfig.headers['X-Auth-Type'] = this.authType;
88
+ // ✅ Automatic token refresh: ตรวจสอบและ refresh token ก่อนหมดอายุ
89
+ if (this.refreshTokenCallbacks &&
90
+ (0, token_utils_1.isTokenValid)(this.token) &&
91
+ this.refreshThresholdMinutes !== null &&
92
+ this.automaticRefreshEnabled === true) {
93
+ if ((0, token_utils_1.isTokenNearExpiration)(this.token, this.refreshThresholdMinutes)) {
94
+ const remainingMinutes = (0, token_utils_1.getTokenExpirationMinutes)(this.token);
95
+ const isCritical = remainingMinutes !== null && remainingMinutes <= 1;
96
+ const now = Date.now();
97
+ const timeSinceLastRefresh = now - this.lastRefreshAttempt;
98
+ if (timeSinceLastRefresh >= REFRESH_COOLDOWN_MS && !this.refreshPromise) {
99
+ logger_1.logger.debug(`Token near expiration (${remainingMinutes} min remaining), refreshing...`);
100
+ this.lastRefreshAttempt = now;
101
+ if (isCritical) {
102
+ // Critical: await refresh
103
+ try {
104
+ const newToken = await Promise.race([
105
+ this.performRefresh(),
106
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Refresh timeout')), this.refreshTimeoutMs)),
107
+ ]);
108
+ this.token = newToken;
109
+ requestConfig.headers['Authorization'] = `Bearer ${newToken}`;
110
+ logger_1.logger.info('Token refreshed successfully (critical)');
111
+ }
112
+ catch (error) {
113
+ logger_1.logger.error('Critical token refresh failed:', error);
114
+ }
115
+ }
116
+ else {
117
+ // Background refresh
118
+ this.performRefresh().catch((error) => {
119
+ logger_1.logger.error('Background token refresh failed:', error);
120
+ });
121
+ }
122
+ }
123
+ }
124
+ }
43
125
  }
44
126
  }
45
127
  return requestConfig;
46
128
  }, (error) => {
47
129
  return Promise.reject(error);
48
130
  });
49
- // Add response interceptor for error handling
50
- this.client.interceptors.response.use((response) => response, (error) => {
131
+ // Add response interceptor for error handling and token refresh on 401
132
+ this.client.interceptors.response.use((response) => response, async (error) => {
133
+ const originalRequest = error.config;
134
+ // Handle 401 errors - try refresh token before failing
135
+ if (error.response?.status === 401 && originalRequest && this.refreshTokenCallbacks) {
136
+ if (originalRequest._retry) {
137
+ logger_1.logger.warn('401 Unauthorized after retry - refresh failed');
138
+ if (this.refreshTokenCallbacks.onRefreshFailure) {
139
+ await this.refreshTokenCallbacks.onRefreshFailure();
140
+ }
141
+ return Promise.reject(error);
142
+ }
143
+ // Skip refresh for refresh endpoint itself
144
+ if (originalRequest.url?.includes('/auth/refresh') || originalRequest.url?.includes('/auth/refresh-token')) {
145
+ logger_1.logger.error('Refresh endpoint failed - session expired');
146
+ if (this.refreshTokenCallbacks.onRefreshFailure) {
147
+ await this.refreshTokenCallbacks.onRefreshFailure();
148
+ }
149
+ return Promise.reject(error);
150
+ }
151
+ // Try to refresh token
152
+ try {
153
+ originalRequest._retry = true;
154
+ logger_1.logger.debug('401 detected, attempting token refresh...');
155
+ const newToken = await this.performRefresh();
156
+ originalRequest.headers = originalRequest.headers || {};
157
+ originalRequest.headers.Authorization = `Bearer ${newToken}`;
158
+ logger_1.logger.debug('Token refreshed, retrying original request...');
159
+ return this.client(originalRequest);
160
+ }
161
+ catch (refreshError) {
162
+ logger_1.logger.error('Token refresh failed:', refreshError);
163
+ if (this.refreshTokenCallbacks.onRefreshFailure) {
164
+ await this.refreshTokenCallbacks.onRefreshFailure();
165
+ }
166
+ return Promise.reject(refreshError);
167
+ }
168
+ }
169
+ // Handle other errors
51
170
  if (error.response?.status === 401) {
52
- console.error('[AuthClient] API Key authentication failed');
171
+ logger_1.logger.error('API Key authentication failed');
53
172
  }
54
173
  else if (error.response?.status === 429) {
55
- console.error('[AuthClient] Rate limit exceeded');
174
+ logger_1.logger.error('Rate limit exceeded');
56
175
  }
57
176
  return Promise.reject(error);
58
177
  });
@@ -62,6 +181,7 @@ class AuthClient {
62
181
  this.systemConfig = new api_1.SystemConfigAPI(this);
63
182
  this.files = new api_1.FilesAPI(this);
64
183
  this.eventLog = new api_1.EventLogApi(this);
184
+ this.license = new api_1.LicenseAPI(this);
65
185
  this.line = new api_1.LineAPI(this);
66
186
  }
67
187
  /**
@@ -77,7 +197,7 @@ class AuthClient {
77
197
  * ```
78
198
  */
79
199
  initializeOAuth(config) {
80
- console.log('[AuthClient] Initializing OAuth with:', {
200
+ logger_1.logger.debug('Initializing OAuth with:', {
81
201
  clientId: config.clientId,
82
202
  redirectUri: config.redirectUri,
83
203
  scope: config.scope,
@@ -151,7 +271,13 @@ class AuthClient {
151
271
  setToken(token, type = 'jwt') {
152
272
  this.token = token;
153
273
  this.authType = type;
154
- console.log(`[AuthClient] Token set with type: ${type}`);
274
+ logger_1.logger.debug(`Token set with type: ${type}`);
275
+ // Reset warning time when token changes
276
+ this.lastWarningTime = 0;
277
+ // If session expiration monitoring is enabled, check immediately
278
+ if (this.sessionExpirationCallbacks) {
279
+ this.checkSessionExpiration();
280
+ }
155
281
  }
156
282
  /**
157
283
  * Get current token type
@@ -166,7 +292,7 @@ class AuthClient {
166
292
  */
167
293
  setAuthType(type) {
168
294
  this.authType = type;
169
- console.log(`[AuthClient] Auth type changed to: ${type}`);
295
+ logger_1.logger.debug(`Auth type changed to: ${type}`);
170
296
  }
171
297
  /**
172
298
  * Get current token (masked)
@@ -185,6 +311,7 @@ class AuthClient {
185
311
  clearToken() {
186
312
  this.token = null;
187
313
  this.authType = 'hybrid';
314
+ this.lastWarningTime = 0;
188
315
  }
189
316
  /**
190
317
  * Get axios instance for advanced usage
@@ -192,5 +319,573 @@ class AuthClient {
192
319
  getAxiosInstance() {
193
320
  return this.client;
194
321
  }
322
+ /**
323
+ * Enable automatic token refresh
324
+ * Call this after user is authenticated to enable automatic refresh
325
+ *
326
+ * @param options - Options for automatic refresh
327
+ *
328
+ * @example
329
+ * ```typescript
330
+ * // Simple: SDK manages localStorage automatically
331
+ * await authClient.enableAutomaticRefresh({
332
+ * refreshTokenKey: 'refresh_token',
333
+ * onRefreshFailure: () => window.location.href = '/login'
334
+ * });
335
+ *
336
+ * // Advanced: Custom callbacks
337
+ * await authClient.enableAutomaticRefresh({
338
+ * callbacks: {
339
+ * getRefreshToken: () => customStorage.get('refresh_token'),
340
+ * setRefreshToken: (token) => customStorage.set('refresh_token', token),
341
+ * clearRefreshToken: () => customStorage.remove('refresh_token'),
342
+ * onRefreshFailure: () => window.location.href = '/login'
343
+ * }
344
+ * });
345
+ * ```
346
+ */
347
+ async enableAutomaticRefresh(options) {
348
+ if (options.refreshTokenKey) {
349
+ // Use localStorage with provided key
350
+ const key = options.refreshTokenKey;
351
+ this.refreshTokenCallbacks = {
352
+ getRefreshToken: () => {
353
+ if (typeof window === 'undefined')
354
+ return null;
355
+ return localStorage.getItem(key);
356
+ },
357
+ setRefreshToken: (token) => {
358
+ if (typeof window === 'undefined')
359
+ return;
360
+ localStorage.setItem(key, token);
361
+ },
362
+ clearRefreshToken: () => {
363
+ if (typeof window === 'undefined')
364
+ return;
365
+ localStorage.removeItem(key);
366
+ },
367
+ onRefreshFailure: options.onRefreshFailure,
368
+ };
369
+ }
370
+ else if (options.callbacks) {
371
+ // Use custom callbacks
372
+ this.refreshTokenCallbacks = {
373
+ ...options.callbacks,
374
+ onRefreshFailure: options.onRefreshFailure || options.callbacks.onRefreshFailure,
375
+ };
376
+ }
377
+ else {
378
+ throw new Error('Either refreshTokenKey or callbacks must be provided');
379
+ }
380
+ await this.loadJwtConfig();
381
+ }
382
+ /**
383
+ * Disable automatic token refresh
384
+ */
385
+ disableAutomaticRefresh() {
386
+ this.refreshTokenCallbacks = null;
387
+ this.refreshThresholdMinutes = null;
388
+ this.automaticRefreshEnabled = null;
389
+ this.jwtConfigPromise = null;
390
+ }
391
+ /**
392
+ * Load JWT config from API
393
+ */
394
+ async loadJwtConfig() {
395
+ if (this.jwtConfigPromise) {
396
+ return this.jwtConfigPromise;
397
+ }
398
+ this.jwtConfigPromise = this.performJwtConfigLoad();
399
+ return this.jwtConfigPromise;
400
+ }
401
+ /**
402
+ * Perform JWT config load
403
+ */
404
+ async performJwtConfigLoad() {
405
+ try {
406
+ const config = await this.systemConfig.getSecurityJwtConfig();
407
+ this.refreshThresholdMinutes = config.refresh_threshold_minutes;
408
+ this.automaticRefreshEnabled = config.automatic_refresh ?? true;
409
+ logger_1.logger.debug(`JWT config loaded: refresh_threshold_minutes=${this.refreshThresholdMinutes} minutes, automatic_refresh=${this.automaticRefreshEnabled}`);
410
+ }
411
+ catch (error) {
412
+ logger_1.logger.error('Failed to load JWT config:', error);
413
+ // Reset to allow retry
414
+ this.jwtConfigPromise = null;
415
+ throw error;
416
+ }
417
+ }
418
+ /**
419
+ * Perform token refresh
420
+ */
421
+ async performRefresh() {
422
+ // If refresh is already in progress, wait for it
423
+ if (this.refreshPromise) {
424
+ return this.refreshPromise;
425
+ }
426
+ if (!this.refreshTokenCallbacks) {
427
+ throw new Error('Refresh token callbacks not configured');
428
+ }
429
+ this.refreshPromise = this.doRefresh();
430
+ try {
431
+ const newToken = await this.refreshPromise;
432
+ return newToken;
433
+ }
434
+ finally {
435
+ this.refreshPromise = null;
436
+ }
437
+ }
438
+ /**
439
+ * Execute refresh token request
440
+ */
441
+ async doRefresh() {
442
+ if (!this.refreshTokenCallbacks) {
443
+ throw new Error('Refresh token callbacks not configured');
444
+ }
445
+ const refreshToken = await this.refreshTokenCallbacks.getRefreshToken();
446
+ if (!refreshToken) {
447
+ throw new Error('No refresh token available');
448
+ }
449
+ try {
450
+ // Try refreshWithToken first (for mobile apps with refresh token)
451
+ const session = await this.auth.refreshWithToken(refreshToken);
452
+ const newAccessToken = session.access_token;
453
+ // Update token in client
454
+ this.token = newAccessToken;
455
+ // Save new refresh token if rotated
456
+ if (session.refresh_token) {
457
+ await this.refreshTokenCallbacks.setRefreshToken(session.refresh_token);
458
+ }
459
+ logger_1.logger.debug('Token refreshed successfully');
460
+ return newAccessToken;
461
+ }
462
+ catch (error) {
463
+ // If refreshWithToken fails, try regular refresh endpoint
464
+ if (error.response?.status === 404 || error.response?.status === 400) {
465
+ try {
466
+ const tokens = await this.auth.refresh(refreshToken);
467
+ const newAccessToken = tokens.access_token;
468
+ this.token = newAccessToken;
469
+ if (tokens.refresh_token) {
470
+ await this.refreshTokenCallbacks.setRefreshToken(tokens.refresh_token);
471
+ }
472
+ logger_1.logger.debug('Token refreshed successfully (fallback)');
473
+ return newAccessToken;
474
+ }
475
+ catch (fallbackError) {
476
+ logger_1.logger.error('Fallback refresh also failed:', fallbackError);
477
+ throw fallbackError;
478
+ }
479
+ }
480
+ throw error;
481
+ }
482
+ }
483
+ /**
484
+ * Reload JWT config (for when config changes)
485
+ */
486
+ async reloadJwtConfig() {
487
+ this.jwtConfigPromise = null;
488
+ this.refreshThresholdMinutes = null;
489
+ this.automaticRefreshEnabled = null;
490
+ await this.loadJwtConfig();
491
+ }
492
+ /**
493
+ * Enable session expiration monitoring
494
+ * จะตรวจสอบ session expiration ตาม config จาก settings/sessions และเรียก callback เมื่อใกล้หมดอายุ
495
+ *
496
+ * @param options - Options for session expiration monitoring
497
+ *
498
+ * @example
499
+ * ```typescript
500
+ * await authClient.enableSessionExpirationMonitoring({
501
+ * callbacks: {
502
+ * onSessionExpiring: (remainingMinutes, expirationType) => {
503
+ * console.log(`Session will expire in ${remainingMinutes} minutes (${expirationType})`);
504
+ * // แสดง notification หรือ dialog
505
+ * },
506
+ * onSessionExpired: () => {
507
+ * console.log('Session expired');
508
+ * // Redirect to login
509
+ * }
510
+ * },
511
+ * checkIntervalSeconds: 30 // ตรวจสอบทุก 30 วินาที
512
+ * });
513
+ * ```
514
+ */
515
+ async enableSessionExpirationMonitoring(options) {
516
+ this.sessionExpirationCallbacks = options.callbacks;
517
+ this.sessionExpirationCheckIntervalSeconds = options.checkIntervalSeconds ?? DEFAULT_SESSION_CHECK_INTERVAL_SECONDS;
518
+ // Load session management config
519
+ await this.loadSessionManagementConfig();
520
+ // Start monitoring
521
+ this.startSessionExpirationMonitoring();
522
+ }
523
+ /**
524
+ * Disable session expiration monitoring
525
+ */
526
+ disableSessionExpirationMonitoring() {
527
+ this.stopSessionExpirationMonitoring();
528
+ this.sessionExpirationCallbacks = null;
529
+ this.sessionManagementConfig = null;
530
+ this.sessionManagementConfigPromise = null;
531
+ }
532
+ /**
533
+ * Load session management config from API
534
+ */
535
+ async loadSessionManagementConfig() {
536
+ if (this.sessionManagementConfigPromise) {
537
+ return this.sessionManagementConfigPromise;
538
+ }
539
+ this.sessionManagementConfigPromise = this.performSessionManagementConfigLoad();
540
+ return this.sessionManagementConfigPromise;
541
+ }
542
+ /**
543
+ * Perform session management config load
544
+ */
545
+ async performSessionManagementConfigLoad() {
546
+ try {
547
+ const config = await this.systemConfig.getSessionManagement();
548
+ this.sessionManagementConfig = config;
549
+ logger_1.logger.debug('Session management config loaded:', {
550
+ inactivityEnabled: config.inactivity.enabled,
551
+ inactivityTimeout: `${config.inactivity.timeout_duration} ${config.inactivity.timeout_unit}`,
552
+ inactivityWarningMinutes: config.inactivity.warning_minutes,
553
+ lifetimeEnabled: config.lifetime.enabled,
554
+ lifetimeMax: config.lifetime.enabled ? `${config.lifetime.max_duration} ${config.lifetime.max_unit}` : 'N/A',
555
+ });
556
+ }
557
+ catch (error) {
558
+ logger_1.logger.error('Failed to load session management config:', error);
559
+ // Reset to allow retry
560
+ this.sessionManagementConfigPromise = null;
561
+ throw error;
562
+ }
563
+ }
564
+ /**
565
+ * Start session expiration monitoring interval
566
+ */
567
+ startSessionExpirationMonitoring() {
568
+ // Clear existing interval if any
569
+ this.stopSessionExpirationMonitoring();
570
+ // Check immediately
571
+ this.checkSessionExpiration();
572
+ // Set up interval
573
+ this.sessionExpirationCheckInterval = setInterval(() => {
574
+ this.checkSessionExpiration();
575
+ }, this.sessionExpirationCheckIntervalSeconds * 1000);
576
+ }
577
+ /**
578
+ * Stop session expiration monitoring interval
579
+ */
580
+ stopSessionExpirationMonitoring() {
581
+ if (this.sessionExpirationCheckInterval) {
582
+ clearInterval(this.sessionExpirationCheckInterval);
583
+ this.sessionExpirationCheckInterval = null;
584
+ }
585
+ }
586
+ /**
587
+ * Check session expiration and trigger callbacks if needed
588
+ *
589
+ * Note: ใช้ token expiration time เป็นตัวประมาณ session expiration
590
+ * เพราะเราไม่มี session creation time หรือ last activity time ใน SDK
591
+ * Token expiration จะถูก refresh อัตโนมัติเมื่อมีการใช้งาน (extend_on_activity)
592
+ */
593
+ checkSessionExpiration() {
594
+ if (!this.token || !this.sessionExpirationCallbacks || !this.sessionManagementConfig) {
595
+ return;
596
+ }
597
+ // Check if token is expired
598
+ if ((0, token_utils_1.isTokenExpired)(this.token)) {
599
+ if (this.sessionExpirationCallbacks.onSessionExpired) {
600
+ this.sessionExpirationCallbacks.onSessionExpired();
601
+ }
602
+ return;
603
+ }
604
+ const tokenExpirationMinutes = (0, token_utils_1.getTokenExpirationMinutes)(this.token);
605
+ if (tokenExpirationMinutes === null) {
606
+ return;
607
+ }
608
+ // ใช้ token expiration time เป็นตัวประมาณ session expiration
609
+ // เพราะ token จะถูก refresh อัตโนมัติเมื่อมีการใช้งาน (extend_on_activity)
610
+ // ดังนั้น token expiration time จะสะท้อน session expiration time ได้ดีพอสมควร
611
+ // ตรวจสอบว่า session expiration monitoring เปิดใช้งานหรือไม่
612
+ const inactivityEnabled = this.sessionManagementConfig.inactivity.enabled;
613
+ const lifetimeEnabled = this.sessionManagementConfig.lifetime.enabled;
614
+ // ถ้าไม่มี expiration config เปิดใช้งาน ให้ใช้ token expiration
615
+ if (!inactivityEnabled && !lifetimeEnabled) {
616
+ // ใช้ token expiration time เป็นตัวประมาณ
617
+ const warningMinutes = this.sessionManagementConfig.inactivity.warning_minutes;
618
+ if (tokenExpirationMinutes <= warningMinutes && tokenExpirationMinutes > 0) {
619
+ this.triggerExpiringWarning(tokenExpirationMinutes, 'inactivity');
620
+ }
621
+ return;
622
+ }
623
+ // คำนวณ session expiration times จาก config
624
+ const inactivityTimeoutMinutes = inactivityEnabled
625
+ ? (0, token_utils_1.convertDurationToMinutes)(this.sessionManagementConfig.inactivity.timeout_duration, this.sessionManagementConfig.inactivity.timeout_unit)
626
+ : null;
627
+ const lifetimeMaxMinutes = lifetimeEnabled
628
+ ? (0, token_utils_1.convertDurationToMinutes)(this.sessionManagementConfig.lifetime.max_duration, this.sessionManagementConfig.lifetime.max_unit)
629
+ : null;
630
+ // ใช้ token expiration time เป็นตัวประมาณ session expiration
631
+ // เพราะ token จะถูก refresh อัตโนมัติเมื่อมีการใช้งาน
632
+ // ดังนั้น token expiration time จะสะท้อน session expiration time ได้ดีพอสมควร
633
+ const warningMinutes = this.sessionManagementConfig.inactivity.warning_minutes;
634
+ // ตรวจสอบว่า token expiration time ใกล้หมดอายุตาม warning_minutes หรือไม่
635
+ if (tokenExpirationMinutes <= warningMinutes && tokenExpirationMinutes > 0) {
636
+ // กำหนด expiration type จาก config
637
+ let expirationType = 'inactivity';
638
+ if (inactivityTimeoutMinutes !== null && lifetimeMaxMinutes !== null) {
639
+ // ใช้ type ที่สั้นกว่า
640
+ expirationType = inactivityTimeoutMinutes <= lifetimeMaxMinutes ? 'inactivity' : 'lifetime';
641
+ }
642
+ else if (lifetimeMaxMinutes !== null) {
643
+ expirationType = 'lifetime';
644
+ }
645
+ this.triggerExpiringWarning(tokenExpirationMinutes, expirationType);
646
+ }
647
+ }
648
+ /**
649
+ * Trigger expiring warning callback with cooldown
650
+ */
651
+ triggerExpiringWarning(remainingMinutes, expirationType) {
652
+ if (!this.sessionExpirationCallbacks?.onSessionExpiring) {
653
+ return;
654
+ }
655
+ // Check cooldown to avoid spam
656
+ const now = Date.now();
657
+ const timeSinceLastWarning = now - this.lastWarningTime;
658
+ if (timeSinceLastWarning >= WARNING_COOLDOWN_MS) {
659
+ this.lastWarningTime = now;
660
+ this.sessionExpirationCallbacks.onSessionExpiring(remainingMinutes, expirationType);
661
+ }
662
+ }
663
+ /**
664
+ * Reload session management config (for when config changes)
665
+ */
666
+ async reloadSessionManagementConfig() {
667
+ this.sessionManagementConfigPromise = null;
668
+ this.sessionManagementConfig = null;
669
+ await this.loadSessionManagementConfig();
670
+ }
671
+ /**
672
+ * Enable inactivity detection
673
+ * จับ user activity (mouse/keyboard/touch/scroll) และ trigger callback เมื่อ inactivity timeout
674
+ *
675
+ * @param options - Options for inactivity detection
676
+ *
677
+ * @example
678
+ * ```typescript
679
+ * await authClient.enableInactivityDetection({
680
+ * callbacks: {
681
+ * onInactivityWarning: (remainingSeconds) => {
682
+ * console.log(`Inactivity warning: ${remainingSeconds} seconds remaining`);
683
+ * // แสดง notification หรือ dialog
684
+ * },
685
+ * onInactivityTimeout: () => {
686
+ * console.log('Inactivity timeout');
687
+ * // Redirect to login หรือ logout
688
+ * authClient.clearToken();
689
+ * window.location.href = '/login';
690
+ * }
691
+ * },
692
+ * timeoutSeconds: 1800, // 30 minutes (optional, default: จาก session management config)
693
+ * warningSeconds: 300, // 5 minutes warning (optional, default: จาก session management config)
694
+ * events: ['mousemove', 'keydown', 'touchstart', 'scroll'] // (optional, default: ทั้งหมด)
695
+ * });
696
+ * ```
697
+ */
698
+ async enableInactivityDetection(options) {
699
+ // Disable existing detection if any
700
+ this.disableInactivityDetection();
701
+ this.inactivityCallbacks = options.callbacks;
702
+ // Load session management config if not loaded
703
+ if (!this.sessionManagementConfig) {
704
+ await this.loadSessionManagementConfig();
705
+ }
706
+ // Determine timeout and warning seconds
707
+ if (options.timeoutSeconds !== undefined) {
708
+ this.inactivityTimeoutSeconds = options.timeoutSeconds;
709
+ }
710
+ else if (this.sessionManagementConfig?.inactivity.enabled) {
711
+ // Use session management config
712
+ this.inactivityTimeoutSeconds =
713
+ (0, token_utils_1.convertDurationToMinutes)(this.sessionManagementConfig.inactivity.timeout_duration, this.sessionManagementConfig.inactivity.timeout_unit) * 60; // Convert to seconds
714
+ }
715
+ else {
716
+ // Default: 30 minutes
717
+ this.inactivityTimeoutSeconds = DEFAULT_INACTIVITY_TIMEOUT_SECONDS;
718
+ }
719
+ if (options.warningSeconds !== undefined) {
720
+ this.inactivityWarningSeconds = options.warningSeconds;
721
+ }
722
+ else if (this.sessionManagementConfig?.inactivity.warning_minutes) {
723
+ // Use session management config (convert minutes to seconds)
724
+ this.inactivityWarningSeconds = this.sessionManagementConfig.inactivity.warning_minutes * 60;
725
+ }
726
+ else {
727
+ // Default: 5 minutes
728
+ this.inactivityWarningSeconds = DEFAULT_WARNING_SECONDS;
729
+ }
730
+ // Determine events to listen
731
+ this.activityEvents = options.events || ['mousemove', 'keydown', 'touchstart', 'scroll'];
732
+ // Initialize last activity time
733
+ this.lastActivityTime = Date.now();
734
+ // Enable detection
735
+ this.inactivityDetectionEnabled = true;
736
+ // Setup event listeners
737
+ this.setupActivityListeners();
738
+ // Start timer
739
+ this.resetInactivityTimer();
740
+ logger_1.logger.debug('Inactivity detection enabled:', {
741
+ timeoutSeconds: this.inactivityTimeoutSeconds,
742
+ warningSeconds: this.inactivityWarningSeconds,
743
+ events: this.activityEvents,
744
+ });
745
+ }
746
+ /**
747
+ * Disable inactivity detection
748
+ */
749
+ disableInactivityDetection() {
750
+ this.inactivityDetectionEnabled = false;
751
+ // Clear throttle timeout
752
+ if (this.activityThrottleTimeout) {
753
+ clearTimeout(this.activityThrottleTimeout);
754
+ this.activityThrottleTimeout = null;
755
+ }
756
+ // Clear timeouts
757
+ this.clearInactivityTimeouts();
758
+ // Remove event listeners
759
+ this.removeActivityListeners();
760
+ // Reset state
761
+ this.inactivityCallbacks = null;
762
+ this.lastActivityTime = 0;
763
+ this.inactivityTimeoutSeconds = 0;
764
+ this.inactivityWarningSeconds = 0;
765
+ this.activityEvents = [];
766
+ logger_1.logger.debug('Inactivity detection disabled');
767
+ }
768
+ /**
769
+ * Handle user activity - reset inactivity timer with throttle
770
+ */
771
+ handleUserActivity() {
772
+ if (!this.inactivityDetectionEnabled) {
773
+ return;
774
+ }
775
+ // Throttle: reset timer สูงสุดทุก 1 วินาที เพื่อลด CPU usage
776
+ if (this.activityThrottleTimeout) {
777
+ return;
778
+ }
779
+ this.lastActivityTime = Date.now();
780
+ this.resetInactivityTimer();
781
+ this.activityThrottleTimeout = setTimeout(() => {
782
+ this.activityThrottleTimeout = null;
783
+ }, this.activityThrottleMs);
784
+ }
785
+ /**
786
+ * Setup activity event listeners
787
+ */
788
+ setupActivityListeners() {
789
+ if (typeof window === 'undefined') {
790
+ logger_1.logger.warn('Cannot setup activity listeners: window is undefined (SSR)');
791
+ return;
792
+ }
793
+ // Remove existing listeners if any
794
+ this.removeActivityListeners();
795
+ // Create handler
796
+ const handler = () => {
797
+ this.handleUserActivity();
798
+ };
799
+ this.activityHandler = handler;
800
+ // Setup listeners for each event type
801
+ this.activityEvents.forEach((eventType) => {
802
+ window.addEventListener(eventType, handler, { passive: true });
803
+ });
804
+ logger_1.logger.debug('Activity listeners setup:', this.activityEvents);
805
+ }
806
+ /**
807
+ * Remove activity event listeners
808
+ */
809
+ removeActivityListeners() {
810
+ if (typeof window === 'undefined' || !this.activityHandler) {
811
+ return;
812
+ }
813
+ const handler = this.activityHandler;
814
+ this.activityEvents.forEach((eventType) => {
815
+ window.removeEventListener(eventType, handler);
816
+ });
817
+ this.activityHandler = null;
818
+ logger_1.logger.debug('Activity listeners removed');
819
+ }
820
+ /**
821
+ * Clear inactivity timeouts (helper method)
822
+ */
823
+ clearInactivityTimeouts() {
824
+ if (this.inactivityWarningTimeout) {
825
+ clearTimeout(this.inactivityWarningTimeout);
826
+ this.inactivityWarningTimeout = null;
827
+ }
828
+ if (this.inactivityTimeout) {
829
+ clearTimeout(this.inactivityTimeout);
830
+ this.inactivityTimeout = null;
831
+ }
832
+ }
833
+ /**
834
+ * Reset inactivity timer
835
+ */
836
+ resetInactivityTimer() {
837
+ // Clear existing timeouts
838
+ this.clearInactivityTimeouts();
839
+ if (!this.inactivityDetectionEnabled || !this.inactivityCallbacks) {
840
+ return;
841
+ }
842
+ const now = Date.now();
843
+ this.lastActivityTime = now;
844
+ // Setup warning timeout
845
+ if (this.inactivityWarningSeconds > 0 && this.inactivityCallbacks.onInactivityWarning) {
846
+ const warningTime = this.inactivityTimeoutSeconds - this.inactivityWarningSeconds;
847
+ if (warningTime > 0) {
848
+ const timerStartTime = now; // Capture start time for this timer
849
+ this.inactivityWarningTimeout = setTimeout(() => {
850
+ // Calculate remaining seconds based on when timer was set
851
+ const elapsedSeconds = (Date.now() - timerStartTime) / 1000;
852
+ const remainingSeconds = this.inactivityTimeoutSeconds - elapsedSeconds;
853
+ if (this.inactivityCallbacks?.onInactivityWarning && remainingSeconds > 0) {
854
+ this.inactivityCallbacks.onInactivityWarning(Math.floor(remainingSeconds));
855
+ }
856
+ }, warningTime * 1000);
857
+ }
858
+ }
859
+ // Setup timeout
860
+ this.inactivityTimeout = setTimeout(() => {
861
+ if (this.inactivityCallbacks?.onInactivityTimeout) {
862
+ this.inactivityCallbacks.onInactivityTimeout();
863
+ }
864
+ }, this.inactivityTimeoutSeconds * 1000);
865
+ }
866
+ /**
867
+ * Check inactivity timeout and trigger callbacks if needed
868
+ */
869
+ checkInactivityTimeout() {
870
+ if (!this.inactivityDetectionEnabled || !this.inactivityCallbacks) {
871
+ return;
872
+ }
873
+ const now = Date.now();
874
+ const inactiveSeconds = (now - this.lastActivityTime) / 1000;
875
+ // Check if timeout reached
876
+ if (inactiveSeconds >= this.inactivityTimeoutSeconds) {
877
+ if (this.inactivityCallbacks.onInactivityTimeout) {
878
+ this.inactivityCallbacks.onInactivityTimeout();
879
+ }
880
+ return;
881
+ }
882
+ // Check if warning should be triggered
883
+ const remainingSeconds = this.inactivityTimeoutSeconds - inactiveSeconds;
884
+ if (remainingSeconds <= this.inactivityWarningSeconds &&
885
+ remainingSeconds > 0 &&
886
+ this.inactivityCallbacks.onInactivityWarning) {
887
+ this.inactivityCallbacks.onInactivityWarning(Math.floor(remainingSeconds));
888
+ }
889
+ }
195
890
  }
196
891
  exports.AuthClient = AuthClient;