win-portal-auth-sdk 1.2.1 → 1.3.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/README.md +28 -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 +183 -1
- package/dist/client/auth-client.d.ts.map +1 -1
- package/dist/client/auth-client.js +453 -4
- 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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/types/auth.types.d.ts +9 -0
- package/dist/types/auth.types.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/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,10 +13,27 @@ 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");
|
|
16
17
|
class AuthClient {
|
|
17
18
|
constructor(config) {
|
|
18
19
|
this.token = null;
|
|
19
20
|
this.authType = 'hybrid'; // Default to hybrid
|
|
21
|
+
// Automatic refresh configuration
|
|
22
|
+
this.refreshTokenCallbacks = null;
|
|
23
|
+
this.jwtConfigPromise = null;
|
|
24
|
+
this.refreshThresholdMinutes = null;
|
|
25
|
+
this.automaticRefreshEnabled = null;
|
|
26
|
+
this.lastRefreshAttempt = 0;
|
|
27
|
+
this.refreshPromise = null;
|
|
28
|
+
this.REFRESH_COOLDOWN_MS = 30 * 1000; // 30 seconds cooldown
|
|
29
|
+
// Session expiration monitoring
|
|
30
|
+
this.sessionExpirationCallbacks = null;
|
|
31
|
+
this.sessionManagementConfig = null;
|
|
32
|
+
this.sessionManagementConfigPromise = null;
|
|
33
|
+
this.sessionExpirationCheckInterval = null;
|
|
34
|
+
this.sessionExpirationCheckIntervalSeconds = 30;
|
|
35
|
+
this.lastWarningTime = 0;
|
|
36
|
+
this.WARNING_COOLDOWN_MS = 60 * 1000; // 1 minute cooldown between warnings
|
|
20
37
|
this.apiKey = config.apiKey;
|
|
21
38
|
this.apiKeyHeader = config.apiKeyHeader || 'X-API-Key';
|
|
22
39
|
console.log('[AuthClient] Initializing with config:', {
|
|
@@ -31,8 +48,8 @@ class AuthClient {
|
|
|
31
48
|
'Content-Type': 'application/json',
|
|
32
49
|
},
|
|
33
50
|
});
|
|
34
|
-
// Add request interceptor to inject API key
|
|
35
|
-
this.client.interceptors.request.use((requestConfig) => {
|
|
51
|
+
// Add request interceptor to inject API key and handle automatic refresh
|
|
52
|
+
this.client.interceptors.request.use(async (requestConfig) => {
|
|
36
53
|
if (requestConfig.headers) {
|
|
37
54
|
requestConfig.headers[this.apiKeyHeader] = this.apiKey;
|
|
38
55
|
// Inject JWT token if available
|
|
@@ -40,14 +57,89 @@ class AuthClient {
|
|
|
40
57
|
requestConfig.headers['Authorization'] = `Bearer ${this.token}`;
|
|
41
58
|
// ✅ Inject X-Auth-Type header for better performance
|
|
42
59
|
requestConfig.headers['X-Auth-Type'] = this.authType;
|
|
60
|
+
// ✅ Automatic token refresh: ตรวจสอบและ refresh token ก่อนหมดอายุ
|
|
61
|
+
if (this.refreshTokenCallbacks &&
|
|
62
|
+
(0, token_utils_1.isTokenValid)(this.token) &&
|
|
63
|
+
this.refreshThresholdMinutes !== null &&
|
|
64
|
+
this.automaticRefreshEnabled === true) {
|
|
65
|
+
if ((0, token_utils_1.isTokenNearExpiration)(this.token, this.refreshThresholdMinutes)) {
|
|
66
|
+
const remainingMinutes = (0, token_utils_1.getTokenExpirationMinutes)(this.token);
|
|
67
|
+
const isCritical = remainingMinutes !== null && remainingMinutes <= 1;
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const timeSinceLastRefresh = now - this.lastRefreshAttempt;
|
|
70
|
+
if (timeSinceLastRefresh >= this.REFRESH_COOLDOWN_MS &&
|
|
71
|
+
!this.refreshPromise) {
|
|
72
|
+
console.log(`[AuthClient] Token near expiration (${remainingMinutes} min remaining), refreshing...`);
|
|
73
|
+
this.lastRefreshAttempt = now;
|
|
74
|
+
if (isCritical) {
|
|
75
|
+
// Critical: await refresh
|
|
76
|
+
try {
|
|
77
|
+
const newToken = await Promise.race([
|
|
78
|
+
this.performRefresh(),
|
|
79
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Refresh timeout')), 5000)),
|
|
80
|
+
]);
|
|
81
|
+
this.token = newToken;
|
|
82
|
+
requestConfig.headers['Authorization'] = `Bearer ${newToken}`;
|
|
83
|
+
console.log('[AuthClient] Token refreshed successfully (critical)');
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error('[AuthClient] Critical token refresh failed:', error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// Background refresh
|
|
91
|
+
this.performRefresh().catch((error) => {
|
|
92
|
+
console.error('[AuthClient] Background token refresh failed:', error);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
43
98
|
}
|
|
44
99
|
}
|
|
45
100
|
return requestConfig;
|
|
46
101
|
}, (error) => {
|
|
47
102
|
return Promise.reject(error);
|
|
48
103
|
});
|
|
49
|
-
// Add response interceptor for error handling
|
|
50
|
-
this.client.interceptors.response.use((response) => response, (error) => {
|
|
104
|
+
// Add response interceptor for error handling and token refresh on 401
|
|
105
|
+
this.client.interceptors.response.use((response) => response, async (error) => {
|
|
106
|
+
const originalRequest = error.config;
|
|
107
|
+
// Handle 401 errors - try refresh token before failing
|
|
108
|
+
if (error.response?.status === 401 && originalRequest && this.refreshTokenCallbacks) {
|
|
109
|
+
if (originalRequest._retry) {
|
|
110
|
+
console.warn('[AuthClient] 401 Unauthorized after retry - refresh failed');
|
|
111
|
+
if (this.refreshTokenCallbacks.onRefreshFailure) {
|
|
112
|
+
await this.refreshTokenCallbacks.onRefreshFailure();
|
|
113
|
+
}
|
|
114
|
+
return Promise.reject(error);
|
|
115
|
+
}
|
|
116
|
+
// Skip refresh for refresh endpoint itself
|
|
117
|
+
if (originalRequest.url?.includes('/auth/refresh') || originalRequest.url?.includes('/auth/refresh-token')) {
|
|
118
|
+
console.error('[AuthClient] Refresh endpoint failed - session expired');
|
|
119
|
+
if (this.refreshTokenCallbacks.onRefreshFailure) {
|
|
120
|
+
await this.refreshTokenCallbacks.onRefreshFailure();
|
|
121
|
+
}
|
|
122
|
+
return Promise.reject(error);
|
|
123
|
+
}
|
|
124
|
+
// Try to refresh token
|
|
125
|
+
try {
|
|
126
|
+
originalRequest._retry = true;
|
|
127
|
+
console.log('[AuthClient] 401 detected, attempting token refresh...');
|
|
128
|
+
const newToken = await this.performRefresh();
|
|
129
|
+
originalRequest.headers = originalRequest.headers || {};
|
|
130
|
+
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
|
131
|
+
console.log('[AuthClient] Token refreshed, retrying original request...');
|
|
132
|
+
return this.client(originalRequest);
|
|
133
|
+
}
|
|
134
|
+
catch (refreshError) {
|
|
135
|
+
console.error('[AuthClient] Token refresh failed:', refreshError);
|
|
136
|
+
if (this.refreshTokenCallbacks.onRefreshFailure) {
|
|
137
|
+
await this.refreshTokenCallbacks.onRefreshFailure();
|
|
138
|
+
}
|
|
139
|
+
return Promise.reject(refreshError);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Handle other errors
|
|
51
143
|
if (error.response?.status === 401) {
|
|
52
144
|
console.error('[AuthClient] API Key authentication failed');
|
|
53
145
|
}
|
|
@@ -62,6 +154,7 @@ class AuthClient {
|
|
|
62
154
|
this.systemConfig = new api_1.SystemConfigAPI(this);
|
|
63
155
|
this.files = new api_1.FilesAPI(this);
|
|
64
156
|
this.eventLog = new api_1.EventLogApi(this);
|
|
157
|
+
this.license = new api_1.LicenseAPI(this);
|
|
65
158
|
this.line = new api_1.LineAPI(this);
|
|
66
159
|
}
|
|
67
160
|
/**
|
|
@@ -152,6 +245,12 @@ class AuthClient {
|
|
|
152
245
|
this.token = token;
|
|
153
246
|
this.authType = type;
|
|
154
247
|
console.log(`[AuthClient] Token set with type: ${type}`);
|
|
248
|
+
// Reset warning time when token changes
|
|
249
|
+
this.lastWarningTime = 0;
|
|
250
|
+
// If session expiration monitoring is enabled, check immediately
|
|
251
|
+
if (this.sessionExpirationCallbacks) {
|
|
252
|
+
this.checkSessionExpiration();
|
|
253
|
+
}
|
|
155
254
|
}
|
|
156
255
|
/**
|
|
157
256
|
* Get current token type
|
|
@@ -185,6 +284,7 @@ class AuthClient {
|
|
|
185
284
|
clearToken() {
|
|
186
285
|
this.token = null;
|
|
187
286
|
this.authType = 'hybrid';
|
|
287
|
+
this.lastWarningTime = 0;
|
|
188
288
|
}
|
|
189
289
|
/**
|
|
190
290
|
* Get axios instance for advanced usage
|
|
@@ -192,5 +292,354 @@ class AuthClient {
|
|
|
192
292
|
getAxiosInstance() {
|
|
193
293
|
return this.client;
|
|
194
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Enable automatic token refresh
|
|
297
|
+
* Call this after user is authenticated to enable automatic refresh
|
|
298
|
+
*
|
|
299
|
+
* @param options - Options for automatic refresh
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```typescript
|
|
303
|
+
* // Simple: SDK manages localStorage automatically
|
|
304
|
+
* await authClient.enableAutomaticRefresh({
|
|
305
|
+
* refreshTokenKey: 'refresh_token',
|
|
306
|
+
* onRefreshFailure: () => window.location.href = '/login'
|
|
307
|
+
* });
|
|
308
|
+
*
|
|
309
|
+
* // Advanced: Custom callbacks
|
|
310
|
+
* await authClient.enableAutomaticRefresh({
|
|
311
|
+
* callbacks: {
|
|
312
|
+
* getRefreshToken: () => customStorage.get('refresh_token'),
|
|
313
|
+
* setRefreshToken: (token) => customStorage.set('refresh_token', token),
|
|
314
|
+
* clearRefreshToken: () => customStorage.remove('refresh_token'),
|
|
315
|
+
* onRefreshFailure: () => window.location.href = '/login'
|
|
316
|
+
* }
|
|
317
|
+
* });
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
async enableAutomaticRefresh(options) {
|
|
321
|
+
if (options.refreshTokenKey) {
|
|
322
|
+
// Use localStorage with provided key
|
|
323
|
+
const key = options.refreshTokenKey;
|
|
324
|
+
this.refreshTokenCallbacks = {
|
|
325
|
+
getRefreshToken: () => {
|
|
326
|
+
if (typeof window === 'undefined')
|
|
327
|
+
return null;
|
|
328
|
+
return localStorage.getItem(key);
|
|
329
|
+
},
|
|
330
|
+
setRefreshToken: (token) => {
|
|
331
|
+
if (typeof window === 'undefined')
|
|
332
|
+
return;
|
|
333
|
+
localStorage.setItem(key, token);
|
|
334
|
+
},
|
|
335
|
+
clearRefreshToken: () => {
|
|
336
|
+
if (typeof window === 'undefined')
|
|
337
|
+
return;
|
|
338
|
+
localStorage.removeItem(key);
|
|
339
|
+
},
|
|
340
|
+
onRefreshFailure: options.onRefreshFailure,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
else if (options.callbacks) {
|
|
344
|
+
// Use custom callbacks
|
|
345
|
+
this.refreshTokenCallbacks = {
|
|
346
|
+
...options.callbacks,
|
|
347
|
+
onRefreshFailure: options.onRefreshFailure || options.callbacks.onRefreshFailure,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
throw new Error('Either refreshTokenKey or callbacks must be provided');
|
|
352
|
+
}
|
|
353
|
+
await this.loadJwtConfig();
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Disable automatic token refresh
|
|
357
|
+
*/
|
|
358
|
+
disableAutomaticRefresh() {
|
|
359
|
+
this.refreshTokenCallbacks = null;
|
|
360
|
+
this.refreshThresholdMinutes = null;
|
|
361
|
+
this.automaticRefreshEnabled = null;
|
|
362
|
+
this.jwtConfigPromise = null;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Load JWT config from API
|
|
366
|
+
*/
|
|
367
|
+
async loadJwtConfig() {
|
|
368
|
+
if (this.jwtConfigPromise) {
|
|
369
|
+
return this.jwtConfigPromise;
|
|
370
|
+
}
|
|
371
|
+
this.jwtConfigPromise = this.performJwtConfigLoad();
|
|
372
|
+
return this.jwtConfigPromise;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Perform JWT config load
|
|
376
|
+
*/
|
|
377
|
+
async performJwtConfigLoad() {
|
|
378
|
+
try {
|
|
379
|
+
const config = await this.systemConfig.getSecurityJwtConfig();
|
|
380
|
+
this.refreshThresholdMinutes = config.refresh_threshold_minutes;
|
|
381
|
+
this.automaticRefreshEnabled = config.automatic_refresh ?? true;
|
|
382
|
+
console.log(`[AuthClient] JWT config loaded: refresh_threshold_minutes=${this.refreshThresholdMinutes} minutes, automatic_refresh=${this.automaticRefreshEnabled}`);
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
console.error('[AuthClient] Failed to load JWT config:', error);
|
|
386
|
+
// Reset to allow retry
|
|
387
|
+
this.jwtConfigPromise = null;
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Perform token refresh
|
|
393
|
+
*/
|
|
394
|
+
async performRefresh() {
|
|
395
|
+
// If refresh is already in progress, wait for it
|
|
396
|
+
if (this.refreshPromise) {
|
|
397
|
+
return this.refreshPromise;
|
|
398
|
+
}
|
|
399
|
+
if (!this.refreshTokenCallbacks) {
|
|
400
|
+
throw new Error('Refresh token callbacks not configured');
|
|
401
|
+
}
|
|
402
|
+
this.refreshPromise = this.doRefresh();
|
|
403
|
+
try {
|
|
404
|
+
const newToken = await this.refreshPromise;
|
|
405
|
+
return newToken;
|
|
406
|
+
}
|
|
407
|
+
finally {
|
|
408
|
+
this.refreshPromise = null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Execute refresh token request
|
|
413
|
+
*/
|
|
414
|
+
async doRefresh() {
|
|
415
|
+
if (!this.refreshTokenCallbacks) {
|
|
416
|
+
throw new Error('Refresh token callbacks not configured');
|
|
417
|
+
}
|
|
418
|
+
const refreshToken = await this.refreshTokenCallbacks.getRefreshToken();
|
|
419
|
+
if (!refreshToken) {
|
|
420
|
+
throw new Error('No refresh token available');
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
// Try refreshWithToken first (for mobile apps with refresh token)
|
|
424
|
+
const session = await this.auth.refreshWithToken(refreshToken);
|
|
425
|
+
const newAccessToken = session.access_token;
|
|
426
|
+
// Update token in client
|
|
427
|
+
this.token = newAccessToken;
|
|
428
|
+
// Save new refresh token if rotated
|
|
429
|
+
if (session.refresh_token) {
|
|
430
|
+
await this.refreshTokenCallbacks.setRefreshToken(session.refresh_token);
|
|
431
|
+
}
|
|
432
|
+
console.log('[AuthClient] Token refreshed successfully');
|
|
433
|
+
return newAccessToken;
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
// If refreshWithToken fails, try regular refresh endpoint
|
|
437
|
+
if (error.response?.status === 404 || error.response?.status === 400) {
|
|
438
|
+
try {
|
|
439
|
+
const tokens = await this.auth.refresh(refreshToken);
|
|
440
|
+
const newAccessToken = tokens.access_token;
|
|
441
|
+
this.token = newAccessToken;
|
|
442
|
+
if (tokens.refresh_token) {
|
|
443
|
+
await this.refreshTokenCallbacks.setRefreshToken(tokens.refresh_token);
|
|
444
|
+
}
|
|
445
|
+
console.log('[AuthClient] Token refreshed successfully (fallback)');
|
|
446
|
+
return newAccessToken;
|
|
447
|
+
}
|
|
448
|
+
catch (fallbackError) {
|
|
449
|
+
console.error('[AuthClient] Fallback refresh also failed:', fallbackError);
|
|
450
|
+
throw fallbackError;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Reload JWT config (for when config changes)
|
|
458
|
+
*/
|
|
459
|
+
async reloadJwtConfig() {
|
|
460
|
+
this.jwtConfigPromise = null;
|
|
461
|
+
this.refreshThresholdMinutes = null;
|
|
462
|
+
this.automaticRefreshEnabled = null;
|
|
463
|
+
await this.loadJwtConfig();
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Enable session expiration monitoring
|
|
467
|
+
* จะตรวจสอบ session expiration ตาม config จาก settings/sessions และเรียก callback เมื่อใกล้หมดอายุ
|
|
468
|
+
*
|
|
469
|
+
* @param options - Options for session expiration monitoring
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* ```typescript
|
|
473
|
+
* await authClient.enableSessionExpirationMonitoring({
|
|
474
|
+
* callbacks: {
|
|
475
|
+
* onSessionExpiring: (remainingMinutes, expirationType) => {
|
|
476
|
+
* console.log(`Session will expire in ${remainingMinutes} minutes (${expirationType})`);
|
|
477
|
+
* // แสดง notification หรือ dialog
|
|
478
|
+
* },
|
|
479
|
+
* onSessionExpired: () => {
|
|
480
|
+
* console.log('Session expired');
|
|
481
|
+
* // Redirect to login
|
|
482
|
+
* }
|
|
483
|
+
* },
|
|
484
|
+
* checkIntervalSeconds: 30 // ตรวจสอบทุก 30 วินาที
|
|
485
|
+
* });
|
|
486
|
+
* ```
|
|
487
|
+
*/
|
|
488
|
+
async enableSessionExpirationMonitoring(options) {
|
|
489
|
+
this.sessionExpirationCallbacks = options.callbacks;
|
|
490
|
+
this.sessionExpirationCheckIntervalSeconds = options.checkIntervalSeconds ?? 30;
|
|
491
|
+
// Load session management config
|
|
492
|
+
await this.loadSessionManagementConfig();
|
|
493
|
+
// Start monitoring
|
|
494
|
+
this.startSessionExpirationMonitoring();
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Disable session expiration monitoring
|
|
498
|
+
*/
|
|
499
|
+
disableSessionExpirationMonitoring() {
|
|
500
|
+
this.stopSessionExpirationMonitoring();
|
|
501
|
+
this.sessionExpirationCallbacks = null;
|
|
502
|
+
this.sessionManagementConfig = null;
|
|
503
|
+
this.sessionManagementConfigPromise = null;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Load session management config from API
|
|
507
|
+
*/
|
|
508
|
+
async loadSessionManagementConfig() {
|
|
509
|
+
if (this.sessionManagementConfigPromise) {
|
|
510
|
+
return this.sessionManagementConfigPromise;
|
|
511
|
+
}
|
|
512
|
+
this.sessionManagementConfigPromise = this.performSessionManagementConfigLoad();
|
|
513
|
+
return this.sessionManagementConfigPromise;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Perform session management config load
|
|
517
|
+
*/
|
|
518
|
+
async performSessionManagementConfigLoad() {
|
|
519
|
+
try {
|
|
520
|
+
const config = await this.systemConfig.getSessionManagement();
|
|
521
|
+
this.sessionManagementConfig = config;
|
|
522
|
+
console.log('[AuthClient] Session management config loaded:', {
|
|
523
|
+
inactivityEnabled: config.inactivity.enabled,
|
|
524
|
+
inactivityTimeout: `${config.inactivity.timeout_duration} ${config.inactivity.timeout_unit}`,
|
|
525
|
+
inactivityWarningMinutes: config.inactivity.warning_minutes,
|
|
526
|
+
lifetimeEnabled: config.lifetime.enabled,
|
|
527
|
+
lifetimeMax: config.lifetime.enabled ? `${config.lifetime.max_duration} ${config.lifetime.max_unit}` : 'N/A',
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
console.error('[AuthClient] Failed to load session management config:', error);
|
|
532
|
+
// Reset to allow retry
|
|
533
|
+
this.sessionManagementConfigPromise = null;
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Start session expiration monitoring interval
|
|
539
|
+
*/
|
|
540
|
+
startSessionExpirationMonitoring() {
|
|
541
|
+
// Clear existing interval if any
|
|
542
|
+
this.stopSessionExpirationMonitoring();
|
|
543
|
+
// Check immediately
|
|
544
|
+
this.checkSessionExpiration();
|
|
545
|
+
// Set up interval
|
|
546
|
+
this.sessionExpirationCheckInterval = setInterval(() => {
|
|
547
|
+
this.checkSessionExpiration();
|
|
548
|
+
}, this.sessionExpirationCheckIntervalSeconds * 1000);
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Stop session expiration monitoring interval
|
|
552
|
+
*/
|
|
553
|
+
stopSessionExpirationMonitoring() {
|
|
554
|
+
if (this.sessionExpirationCheckInterval) {
|
|
555
|
+
clearInterval(this.sessionExpirationCheckInterval);
|
|
556
|
+
this.sessionExpirationCheckInterval = null;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Check session expiration and trigger callbacks if needed
|
|
561
|
+
*
|
|
562
|
+
* Note: ใช้ token expiration time เป็นตัวประมาณ session expiration
|
|
563
|
+
* เพราะเราไม่มี session creation time หรือ last activity time ใน SDK
|
|
564
|
+
* Token expiration จะถูก refresh อัตโนมัติเมื่อมีการใช้งาน (extend_on_activity)
|
|
565
|
+
*/
|
|
566
|
+
checkSessionExpiration() {
|
|
567
|
+
if (!this.token || !this.sessionExpirationCallbacks || !this.sessionManagementConfig) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
// Check if token is expired
|
|
571
|
+
if ((0, token_utils_1.isTokenExpired)(this.token)) {
|
|
572
|
+
if (this.sessionExpirationCallbacks.onSessionExpired) {
|
|
573
|
+
this.sessionExpirationCallbacks.onSessionExpired();
|
|
574
|
+
}
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const tokenExpirationMinutes = (0, token_utils_1.getTokenExpirationMinutes)(this.token);
|
|
578
|
+
if (tokenExpirationMinutes === null) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
// ใช้ token expiration time เป็นตัวประมาณ session expiration
|
|
582
|
+
// เพราะ token จะถูก refresh อัตโนมัติเมื่อมีการใช้งาน (extend_on_activity)
|
|
583
|
+
// ดังนั้น token expiration time จะสะท้อน session expiration time ได้ดีพอสมควร
|
|
584
|
+
// ตรวจสอบว่า session expiration monitoring เปิดใช้งานหรือไม่
|
|
585
|
+
const inactivityEnabled = this.sessionManagementConfig.inactivity.enabled;
|
|
586
|
+
const lifetimeEnabled = this.sessionManagementConfig.lifetime.enabled;
|
|
587
|
+
// ถ้าไม่มี expiration config เปิดใช้งาน ให้ใช้ token expiration
|
|
588
|
+
if (!inactivityEnabled && !lifetimeEnabled) {
|
|
589
|
+
// ใช้ token expiration time เป็นตัวประมาณ
|
|
590
|
+
const warningMinutes = this.sessionManagementConfig.inactivity.warning_minutes;
|
|
591
|
+
if (tokenExpirationMinutes <= warningMinutes && tokenExpirationMinutes > 0) {
|
|
592
|
+
this.triggerExpiringWarning(tokenExpirationMinutes, 'inactivity');
|
|
593
|
+
}
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
// คำนวณ session expiration times จาก config
|
|
597
|
+
const inactivityTimeoutMinutes = inactivityEnabled
|
|
598
|
+
? (0, token_utils_1.convertDurationToMinutes)(this.sessionManagementConfig.inactivity.timeout_duration, this.sessionManagementConfig.inactivity.timeout_unit)
|
|
599
|
+
: null;
|
|
600
|
+
const lifetimeMaxMinutes = lifetimeEnabled
|
|
601
|
+
? (0, token_utils_1.convertDurationToMinutes)(this.sessionManagementConfig.lifetime.max_duration, this.sessionManagementConfig.lifetime.max_unit)
|
|
602
|
+
: null;
|
|
603
|
+
// ใช้ token expiration time เป็นตัวประมาณ session expiration
|
|
604
|
+
// เพราะ token จะถูก refresh อัตโนมัติเมื่อมีการใช้งาน
|
|
605
|
+
// ดังนั้น token expiration time จะสะท้อน session expiration time ได้ดีพอสมควร
|
|
606
|
+
const warningMinutes = this.sessionManagementConfig.inactivity.warning_minutes;
|
|
607
|
+
// ตรวจสอบว่า token expiration time ใกล้หมดอายุตาม warning_minutes หรือไม่
|
|
608
|
+
if (tokenExpirationMinutes <= warningMinutes && tokenExpirationMinutes > 0) {
|
|
609
|
+
// กำหนด expiration type จาก config
|
|
610
|
+
let expirationType = 'inactivity';
|
|
611
|
+
if (inactivityTimeoutMinutes !== null && lifetimeMaxMinutes !== null) {
|
|
612
|
+
// ใช้ type ที่สั้นกว่า
|
|
613
|
+
expirationType = inactivityTimeoutMinutes <= lifetimeMaxMinutes ? 'inactivity' : 'lifetime';
|
|
614
|
+
}
|
|
615
|
+
else if (lifetimeMaxMinutes !== null) {
|
|
616
|
+
expirationType = 'lifetime';
|
|
617
|
+
}
|
|
618
|
+
this.triggerExpiringWarning(tokenExpirationMinutes, expirationType);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Trigger expiring warning callback with cooldown
|
|
623
|
+
*/
|
|
624
|
+
triggerExpiringWarning(remainingMinutes, expirationType) {
|
|
625
|
+
if (!this.sessionExpirationCallbacks?.onSessionExpiring) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
// Check cooldown to avoid spam
|
|
629
|
+
const now = Date.now();
|
|
630
|
+
const timeSinceLastWarning = now - this.lastWarningTime;
|
|
631
|
+
if (timeSinceLastWarning >= this.WARNING_COOLDOWN_MS) {
|
|
632
|
+
this.lastWarningTime = now;
|
|
633
|
+
this.sessionExpirationCallbacks.onSessionExpiring(remainingMinutes, expirationType);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Reload session management config (for when config changes)
|
|
638
|
+
*/
|
|
639
|
+
async reloadSessionManagementConfig() {
|
|
640
|
+
this.sessionManagementConfigPromise = null;
|
|
641
|
+
this.sessionManagementConfig = null;
|
|
642
|
+
await this.loadSessionManagementConfig();
|
|
643
|
+
}
|
|
195
644
|
}
|
|
196
645
|
exports.AuthClient = AuthClient;
|
package/dist/client/index.d.ts
CHANGED
|
@@ -2,4 +2,6 @@
|
|
|
2
2
|
* Client exports for frontend applications
|
|
3
3
|
*/
|
|
4
4
|
export { AuthClient } from './auth-client';
|
|
5
|
+
export type { RefreshTokenCallbacks, AutomaticRefreshOptions, SessionExpirationCallbacks, SessionExpirationOptions, } from './auth-client';
|
|
6
|
+
export * from './api';
|
|
5
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,YAAY,EACV,qBAAqB,EACrB,uBAAuB,EACvB,0BAA0B,EAC1B,wBAAwB,GACzB,MAAM,eAAe,CAAC;AACvB,cAAc,OAAO,CAAC"}
|
package/dist/client/index.js
CHANGED
|
@@ -2,7 +2,22 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Client exports for frontend applications
|
|
4
4
|
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
17
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
18
|
+
};
|
|
5
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
20
|
exports.AuthClient = void 0;
|
|
7
21
|
var auth_client_1 = require("./auth-client");
|
|
8
22
|
Object.defineProperty(exports, "AuthClient", { enumerable: true, get: function () { return auth_client_1.AuthClient; } });
|
|
23
|
+
__exportStar(require("./api"), exports);
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,SAAS,CAAC;AAGxB,cAAc,UAAU,CAAC;AAGzB,cAAc,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,SAAS,CAAC;AAGxB,cAAc,UAAU,CAAC;AAGzB,cAAc,cAAc,CAAC;AAG7B,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -66,6 +66,15 @@ export interface User {
|
|
|
66
66
|
export interface RefreshTokenRequest {
|
|
67
67
|
refresh_token: string;
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Refresh Token Request with optional metadata
|
|
71
|
+
*/
|
|
72
|
+
export interface RefreshTokenRequestWithMetadata {
|
|
73
|
+
refresh_token: string;
|
|
74
|
+
ip_address?: string;
|
|
75
|
+
user_agent?: string;
|
|
76
|
+
device_id?: string;
|
|
77
|
+
}
|
|
69
78
|
/**
|
|
70
79
|
* Authentication Tokens
|
|
71
80
|
* Result of token refresh
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.types.d.ts","sourceRoot":"","sources":["../../src/types/auth.types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,IAAI,CAAC;IACX,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,KAAK,CAAC;QACX,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;KACd,CAAC,CAAC;IACH,SAAS,CAAC,EAAE,KAAK,CAAC;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,OAAO,CAAC;KACrB,CAAC,CAAC;IACH,gBAAgB,CAAC,EAAE;QACjB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
|
|
1
|
+
{"version":3,"file":"auth.types.d.ts","sourceRoot":"","sources":["../../src/types/auth.types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,IAAI,CAAC;IACX,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,KAAK,CAAC;QACX,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;KACd,CAAC,CAAC;IACH,SAAS,CAAC,EAAE,KAAK,CAAC;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,OAAO,CAAC;KACrB,CAAC,CAAC;IACH,gBAAgB,CAAC,EAAE;QACjB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,+BAA+B;IAC9C,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
|
|
@@ -29,4 +29,41 @@ export interface SystemConfig {
|
|
|
29
29
|
* Returns an object with key-value pairs for a category
|
|
30
30
|
*/
|
|
31
31
|
export type SystemConfigByCategory = Record<string, any>;
|
|
32
|
+
/**
|
|
33
|
+
* Security JWT Configuration
|
|
34
|
+
* Configuration for JWT token settings
|
|
35
|
+
*/
|
|
36
|
+
export interface SecurityJwtConfig {
|
|
37
|
+
jwt_secret: string;
|
|
38
|
+
access_token_expiry_minutes: number;
|
|
39
|
+
refresh_token_expiry_days: number;
|
|
40
|
+
automatic_refresh: boolean;
|
|
41
|
+
refresh_threshold_minutes: number;
|
|
42
|
+
rotate_refresh_token: boolean;
|
|
43
|
+
revoke_on_logout: boolean;
|
|
44
|
+
max_refresh_attempts: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Session Management Configuration
|
|
48
|
+
* Configuration for session timeout and expiration settings
|
|
49
|
+
*/
|
|
50
|
+
export interface SessionManagementConfig {
|
|
51
|
+
inactivity: {
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
timeout_duration: number;
|
|
54
|
+
timeout_unit: 'minutes' | 'hours' | 'days';
|
|
55
|
+
warning_minutes: number;
|
|
56
|
+
extend_on_activity: boolean;
|
|
57
|
+
};
|
|
58
|
+
lifetime: {
|
|
59
|
+
enabled: boolean;
|
|
60
|
+
max_duration: number;
|
|
61
|
+
max_unit: 'minutes' | 'hours' | 'days';
|
|
62
|
+
};
|
|
63
|
+
concurrent: {
|
|
64
|
+
max_sessions: number;
|
|
65
|
+
logout_inactive: boolean;
|
|
66
|
+
show_badge: boolean;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
32
69
|
//# sourceMappingURL=system-config.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-config.types.d.ts","sourceRoot":"","sources":["../../src/types/system-config.types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,GAAG,CAAC;IACX,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;IAC9D,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"system-config.types.d.ts","sourceRoot":"","sources":["../../src/types/system-config.types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,GAAG,CAAC;IACX,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;IAC9D,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAEzD;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,2BAA2B,EAAE,MAAM,CAAC;IACpC,yBAAyB,EAAE,MAAM,CAAC;IAClC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,yBAAyB,EAAE,MAAM,CAAC;IAClC,oBAAoB,EAAE,OAAO,CAAC;IAC9B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE;QACV,OAAO,EAAE,OAAO,CAAC;QACjB,gBAAgB,EAAE,MAAM,CAAC;QACzB,YAAY,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;QAC3C,eAAe,EAAE,MAAM,CAAC;QACxB,kBAAkB,EAAE,OAAO,CAAC;KAC7B,CAAC;IACF,QAAQ,EAAE;QACR,OAAO,EAAE,OAAO,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,QAAQ,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;KACxC,CAAC;IACF,UAAU,EAAE;QACV,YAAY,EAAE,MAAM,CAAC;QACrB,eAAe,EAAE,OAAO,CAAC;QACzB,UAAU,EAAE,OAAO,CAAC;KACrB,CAAC;CACH"}
|