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.
- package/README.md +102 -9
- package/dist/client/api/auth.api.d.ts +25 -1
- package/dist/client/api/auth.api.d.ts.map +1 -1
- package/dist/client/api/auth.api.js +30 -1
- package/dist/client/api/files.api.d.ts +0 -1
- package/dist/client/api/files.api.d.ts.map +1 -1
- package/dist/client/api/index.d.ts +2 -0
- package/dist/client/api/index.d.ts.map +1 -1
- package/dist/client/api/index.js +3 -1
- package/dist/client/api/license.api.d.ts +74 -0
- package/dist/client/api/license.api.d.ts.map +1 -0
- package/dist/client/api/license.api.js +50 -0
- package/dist/client/api/system-config.api.d.ts +11 -1
- package/dist/client/api/system-config.api.d.ts.map +1 -1
- package/dist/client/api/system-config.api.js +21 -0
- package/dist/client/auth-client.d.ts +278 -1
- package/dist/client/auth-client.d.ts.map +1 -1
- package/dist/client/auth-client.js +705 -10
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +15 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/types/auth.types.d.ts +9 -0
- package/dist/types/auth.types.d.ts.map +1 -1
- package/dist/types/index.d.ts +18 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/system-config.types.d.ts +37 -0
- package/dist/types/system-config.types.d.ts.map +1 -1
- package/dist/utils/logger.d.ts +23 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +49 -0
- package/dist/utils/token-utils.d.ts +60 -0
- package/dist/utils/token-utils.d.ts.map +1 -0
- package/dist/utils/token-utils.js +116 -0
- package/package.json +1 -2
- 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
|
-
|
|
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
|
-
|
|
171
|
+
logger_1.logger.error('API Key authentication failed');
|
|
53
172
|
}
|
|
54
173
|
else if (error.response?.status === 429) {
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|