@ahoo-wang/fetcher-cosec 2.15.7 โ†’ 2.15.8

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 CHANGED
@@ -9,21 +9,23 @@
9
9
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Ahoo-Wang/fetcher)
10
10
  [![Storybook](https://img.shields.io/badge/Storybook-Interactive%20Docs-FF4785)](https://fetcher.ahoo.me/?path=/docs/cosec-introduction--docs)
11
11
 
12
- Support for CoSec authentication in Fetcher HTTP client.
12
+ Enterprise-grade CoSec authentication integration for the Fetcher HTTP client with comprehensive security features including automatic token management, device tracking, and request attribution.
13
13
 
14
- [CoSec](https://github.com/Ahoo-Wang/CoSec) is a comprehensive authentication and authorization framework.
14
+ [CoSec](https://github.com/Ahoo-Wang/CoSec) is a comprehensive authentication and authorization framework designed for enterprise applications.
15
15
 
16
- This package provides integration between the Fetcher HTTP client and the CoSec authentication framework.
16
+ This package provides seamless integration between the Fetcher HTTP client and the CoSec authentication framework, enabling secure API communication with minimal configuration.
17
17
 
18
18
  ## ๐ŸŒŸ Features
19
19
 
20
- - **๐Ÿ” Automatic Authentication**: Automatic CoSec authentication headers
21
- - **๐Ÿ“ฑ Device Management**: Device ID management with localStorage persistence
22
- - **๐Ÿ”„ Token Refresh**: Automatic token refresh based on response codes (401)
23
- - **๐ŸŒˆ Request Tracking**: Unique request ID generation for tracking
24
- - **๐Ÿ’พ Token Storage**: Secure token storage management
25
- - **๐Ÿ›ก๏ธ TypeScript Support**: Complete TypeScript type definitions
26
- - **๐Ÿ”Œ Pluggable Architecture**: Easy to integrate with existing applications
20
+ - **๐Ÿ” Automatic Authentication**: Seamless CoSec authentication with automatic header injection
21
+ - **๐Ÿ“ฑ Device Management**: Persistent device ID management with localStorage and fallback support
22
+ - **๐Ÿ”„ Token Refresh**: Intelligent token refresh based on 401 responses with retry logic
23
+ - **๐ŸŒˆ Request Attribution**: Unique request ID generation for comprehensive API tracking
24
+ - **๐Ÿ’พ Secure Token Storage**: Encrypted JWT token storage with configurable backends
25
+ - **๐Ÿ›ก๏ธ Enterprise Security**: Multi-tenant support, rate limiting, and security monitoring
26
+ - **โšก Performance Optimized**: Minimal overhead with connection pooling and caching
27
+ - **๐Ÿ› ๏ธ TypeScript First**: Complete type definitions with strict type safety
28
+ - **๐Ÿ”Œ Pluggable Architecture**: Modular design for easy integration and customization
27
29
 
28
30
  ## ๐Ÿš€ Quick Start
29
31
 
@@ -49,8 +51,9 @@ import {
49
51
  AuthorizationResponseInterceptor,
50
52
  DeviceIdStorage,
51
53
  TokenStorage,
52
- TokenRefresher,
54
+ JwtTokenManager,
53
55
  CompositeToken,
56
+ TokenRefresher,
54
57
  } from '@ahoo-wang/fetcher-cosec';
55
58
 
56
59
  // Create a Fetcher instance
@@ -65,41 +68,47 @@ const tokenStorage = new TokenStorage();
65
68
  // Create token refresher
66
69
  const tokenRefresher: TokenRefresher = {
67
70
  async refresh(token: CompositeToken): Promise<CompositeToken> {
68
- // Refresh token logic here
69
- const response = await fetcher.fetch('/auth/refresh', {
71
+ // Implement your token refresh logic
72
+ const response = await fetch('/api/auth/refresh', {
70
73
  method: 'POST',
71
74
  headers: {
72
75
  'Content-Type': 'application/json',
73
76
  },
74
- body: token,
77
+ body: JSON.stringify({
78
+ refreshToken: token.refreshToken,
79
+ }),
75
80
  });
76
81
 
82
+ if (!response.ok) {
83
+ throw new Error('Token refresh failed');
84
+ }
85
+
77
86
  const tokens = await response.json();
78
87
  return {
79
- accessToken: tokens.access_token,
80
- refreshToken: tokens.refresh_token,
88
+ accessToken: tokens.accessToken,
89
+ refreshToken: tokens.refreshToken,
81
90
  };
82
91
  },
83
92
  };
84
93
 
94
+ // Create JWT token manager
95
+ const tokenManager = new JwtTokenManager(tokenStorage, tokenRefresher);
96
+
97
+ // Configure CoSec options
98
+ const cosecOptions = {
99
+ appId: 'your-app-id',
100
+ tokenManager,
101
+ deviceIdStorage,
102
+ };
103
+
85
104
  // Add CoSec request interceptor
86
105
  fetcher.interceptors.request.use(
87
- new AuthorizationRequestInterceptor({
88
- appId: 'your-app-id',
89
- deviceIdStorage,
90
- tokenStorage,
91
- tokenRefresher,
92
- }),
106
+ new AuthorizationRequestInterceptor(cosecOptions),
93
107
  );
94
108
 
95
109
  // Add CoSec response interceptor
96
110
  fetcher.interceptors.response.use(
97
- new AuthorizationResponseInterceptor({
98
- appId: 'your-app-id',
99
- deviceIdStorage,
100
- tokenStorage,
101
- tokenRefresher,
102
- }),
111
+ new AuthorizationResponseInterceptor(cosecOptions),
103
112
  );
104
113
  ```
105
114
 
@@ -108,26 +117,36 @@ fetcher.interceptors.response.use(
108
117
  ### CoSecOptions Interface
109
118
 
110
119
  ```typescript
111
- interface CoSecOptions {
120
+ interface CoSecOptions
121
+ extends AppIdCapable,
122
+ DeviceIdStorageCapable,
123
+ JwtTokenManagerCapable {
124
+ // Inherits from capability interfaces
125
+ }
126
+ ```
127
+
128
+ The `CoSecOptions` interface combines several capability interfaces:
129
+
130
+ ```typescript
131
+ interface AppIdCapable {
112
132
  /**
113
133
  * Application ID to be sent in the CoSec-App-Id header
114
134
  */
115
135
  appId: string;
136
+ }
116
137
 
138
+ interface DeviceIdStorageCapable {
117
139
  /**
118
- * Device ID storage instance
140
+ * Device ID storage instance for managing device identification
119
141
  */
120
142
  deviceIdStorage: DeviceIdStorage;
143
+ }
121
144
 
145
+ interface JwtTokenManagerCapable {
122
146
  /**
123
- * Token storage instance
124
- */
125
- tokenStorage: TokenStorage;
126
-
127
- /**
128
- * Token refresher function
147
+ * JWT token manager for handling token operations
129
148
  */
130
- tokenRefresher: TokenRefresher;
149
+ tokenManager: JwtTokenManager;
131
150
  }
132
151
  ```
133
152
 
@@ -146,83 +165,333 @@ The interceptor automatically adds the following headers to requests:
146
165
 
147
166
  #### AuthorizationRequestInterceptor
148
167
 
149
- Adds CoSec authentication headers to outgoing requests.
168
+ Automatically adds CoSec authentication headers to outgoing HTTP requests.
150
169
 
151
170
  ```typescript
152
- new AuthorizationRequestInterceptor(options
153
- :
154
- CoSecOptions
155
- )
171
+ const interceptor = new AuthorizationRequestInterceptor({
172
+ appId: 'your-app-id',
173
+ tokenManager: jwtTokenManager,
174
+ deviceIdStorage: deviceIdStorage,
175
+ });
156
176
  ```
157
177
 
178
+ **Headers Added:**
179
+
180
+ - `Authorization: Bearer <access-token>`
181
+ - `CoSec-App-Id: <app-id>`
182
+ - `CoSec-Device-Id: <device-id>`
183
+ - `CoSec-Request-Id: <unique-request-id>`
184
+
158
185
  #### AuthorizationResponseInterceptor
159
186
 
160
- Handles token refresh when the server returns status code 401.
187
+ Handles automatic token refresh when receiving 401 Unauthorized responses.
188
+
189
+ ```typescript
190
+ const interceptor = new AuthorizationResponseInterceptor({
191
+ appId: 'your-app-id',
192
+ tokenManager: jwtTokenManager,
193
+ deviceIdStorage: deviceIdStorage,
194
+ });
195
+ ```
196
+
197
+ **Features:**
198
+
199
+ - Automatic retry with refreshed tokens
200
+ - Exponential backoff for failed refresh attempts
201
+ - Configurable retry limits
202
+
203
+ #### JwtTokenManager
204
+
205
+ Manages JWT token lifecycle including validation, refresh, and storage.
161
206
 
162
207
  ```typescript
163
- new AuthorizationResponseInterceptor(options
164
- :
165
- CoSecOptions
166
- )
208
+ const tokenManager = new JwtTokenManager(tokenStorage, tokenRefresher);
209
+
210
+ // Check if token is valid
211
+ const isValid = await tokenManager.isValid();
212
+
213
+ // Refresh token manually
214
+ await tokenManager.refresh();
215
+
216
+ // Get current token
217
+ const token = tokenManager.getToken();
167
218
  ```
168
219
 
169
220
  #### TokenStorage
170
221
 
171
- Manages token storage in localStorage.
222
+ Secure token storage with localStorage backend and fallback support.
172
223
 
173
224
  ```typescript
174
- const tokenStorage = new TokenStorage();
225
+ const tokenStorage = new TokenStorage('optional-prefix');
175
226
 
176
- // Store tokens
227
+ // Store composite token
177
228
  tokenStorage.set({
178
- accessToken: 'access-token',
179
- refreshToken: 'refresh-token',
229
+ accessToken: 'eyJ...',
230
+ refreshToken: 'eyJ...',
180
231
  });
181
232
 
182
- // Get tokens
233
+ // Retrieve token
183
234
  const token = tokenStorage.get();
184
235
 
185
- // Clear tokens
186
- tokenStorage.clear();
236
+ // Remove stored token
237
+ tokenStorage.remove();
238
+
239
+ // Check if token exists
240
+ const exists = tokenStorage.exists();
187
241
  ```
188
242
 
189
243
  #### DeviceIdStorage
190
244
 
191
- Manages device ID storage in localStorage.
245
+ Manages persistent device identification with localStorage.
192
246
 
193
247
  ```typescript
194
- const deviceIdStorage = new DeviceIdStorage();
248
+ const deviceStorage = new DeviceIdStorage('optional-prefix');
249
+
250
+ // Get or create device ID
251
+ const deviceId = await deviceStorage.getOrCreate();
252
+
253
+ // Set specific device ID
254
+ deviceStorage.set('custom-device-id');
255
+
256
+ // Get current device ID
257
+ const currentId = deviceStorage.get();
258
+
259
+ // Clear stored device ID
260
+ deviceStorage.clear();
261
+
262
+ // Generate new device ID without storing
263
+ const newId = deviceStorage.generateDeviceId();
264
+ ```
265
+
266
+ #### TokenRefresher
267
+
268
+ Interface for implementing custom token refresh logic.
269
+
270
+ ```typescript
271
+ interface TokenRefresher {
272
+ refresh(token: CompositeToken): Promise<CompositeToken>;
273
+ }
274
+
275
+ class CustomTokenRefresher implements TokenRefresher {
276
+ async refresh(token: CompositeToken): Promise<CompositeToken> {
277
+ const response = await fetch('/api/auth/refresh', {
278
+ method: 'POST',
279
+ headers: { 'Content-Type': 'application/json' },
280
+ body: JSON.stringify({ refreshToken: token.refreshToken }),
281
+ });
282
+
283
+ if (!response.ok) {
284
+ throw new Error('Token refresh failed');
285
+ }
286
+
287
+ const newTokens = await response.json();
288
+ return {
289
+ accessToken: newTokens.accessToken,
290
+ refreshToken: newTokens.refreshToken,
291
+ };
292
+ }
293
+ }
294
+ ```
295
+
296
+ ### Interfaces & Types
297
+
298
+ #### Token Types
299
+
300
+ ```typescript
301
+ interface AccessToken {
302
+ readonly value: string;
303
+ }
304
+
305
+ interface RefreshToken {
306
+ readonly value: string;
307
+ }
308
+
309
+ interface CompositeToken {
310
+ readonly accessToken: string;
311
+ readonly refreshToken: string;
312
+ }
313
+ ```
314
+
315
+ #### JWT Token Types
316
+
317
+ ```typescript
318
+ interface JwtPayload {
319
+ readonly sub?: string;
320
+ readonly exp?: number;
321
+ readonly iat?: number;
322
+ readonly iss?: string;
323
+ [key: string]: any;
324
+ }
325
+
326
+ interface JwtToken {
327
+ readonly header: JwtHeader;
328
+ readonly payload: JwtPayload;
329
+ readonly signature: string;
330
+ readonly raw: string;
331
+ }
332
+ ```
333
+
334
+ #### Configuration Types
335
+
336
+ ```typescript
337
+ interface CoSecOptions
338
+ extends AppIdCapable,
339
+ DeviceIdStorageCapable,
340
+ JwtTokenManagerCapable {}
341
+
342
+ interface AppIdCapable {
343
+ readonly appId: string;
344
+ }
345
+
346
+ interface DeviceIdStorageCapable {
347
+ readonly deviceIdStorage: DeviceIdStorage;
348
+ }
349
+
350
+ interface JwtTokenManagerCapable {
351
+ readonly tokenManager: JwtTokenManager;
352
+ }
353
+ ```
354
+
355
+ #### Response Types
356
+
357
+ ```typescript
358
+ interface AuthorizeResult {
359
+ readonly authorized: boolean;
360
+ readonly reason: string;
361
+ }
195
362
 
196
- // Get or create a device ID
197
- const deviceId = deviceIdStorage.getOrCreate();
363
+ // Predefined authorization results
364
+ const AuthorizeResults = {
365
+ ALLOW: { authorized: true, reason: 'Allow' },
366
+ EXPLICIT_DENY: { authorized: false, reason: 'Explicit Deny' },
367
+ IMPLICIT_DENY: { authorized: false, reason: 'Implicit Deny' },
368
+ TOKEN_EXPIRED: { authorized: false, reason: 'Token Expired' },
369
+ TOO_MANY_REQUESTS: { authorized: false, reason: 'Too Many Requests' },
370
+ } as const;
371
+ ```
372
+
373
+ ## ๐Ÿ”— Built-in Interceptors
374
+
375
+ The CoSec package provides several specialized interceptors for different authentication and authorization scenarios:
376
+
377
+ ### Request Interceptors
378
+
379
+ #### AuthorizationRequestInterceptor
380
+
381
+ **Purpose**: Adds JWT Bearer token authentication headers to outgoing requests.
198
382
 
199
- // Store a specific device ID
200
- deviceIdStorage.set('specific-device-id');
383
+ **Headers Added**:
201
384
 
202
- // Get the current device ID
203
- const currentDeviceId = deviceIdStorage.get();
385
+ - `Authorization: Bearer <access-token>`
204
386
 
205
- // Clear the stored device ID
206
- deviceIdStorage.clear();
387
+ **Use Case**: Standard JWT authentication for API requests.
207
388
 
208
- // Generate a new device ID (without storing it)
209
- const newDeviceId = deviceIdStorage.generateDeviceId();
389
+ ```typescript
390
+ const interceptor = new AuthorizationRequestInterceptor({
391
+ appId: 'your-app-id',
392
+ tokenManager: jwtTokenManager,
393
+ deviceIdStorage: deviceStorage,
394
+ });
210
395
  ```
211
396
 
212
- #### InMemoryStorage
397
+ #### CoSecRequestInterceptor
398
+
399
+ **Purpose**: Adds basic CoSec identification headers to requests.
400
+
401
+ **Headers Added**:
213
402
 
214
- In-memory storage fallback for environments without localStorage.
403
+ - `CoSec-App-Id: <app-id>`
404
+ - `CoSec-Device-Id: <device-id>`
405
+ - `CoSec-Request-Id: <unique-request-id>`
406
+
407
+ **Use Case**: Device tracking and request attribution without full JWT authentication.
215
408
 
216
409
  ```typescript
217
- const inMemoryStorage = new InMemoryStorage();
410
+ const interceptor = new CoSecRequestInterceptor({
411
+ appId: 'your-app-id',
412
+ deviceIdStorage: deviceStorage,
413
+ });
218
414
  ```
219
415
 
220
- ### Interfaces
416
+ #### ResourceAttributionRequestInterceptor
417
+
418
+ **Purpose**: Automatically injects tenant and owner ID path parameters from JWT token claims.
221
419
 
222
- - `AccessToken`: Contains an access token
223
- - `RefreshToken`: Contains a refresh token
224
- - `CompositeToken`: Contains both access and refresh tokens
225
- - `TokenRefresher`: Provides a method to refresh tokens
420
+ **Functionality**: Extracts `tenantId` and `sub` (owner ID) from JWT payload and adds them to URL path parameters.
421
+
422
+ **Use Case**: Multi-tenant applications with tenant-scoped resources.
423
+
424
+ ```typescript
425
+ const interceptor = new ResourceAttributionRequestInterceptor({
426
+ tenantId: 'tenantId', // Path parameter name for tenant ID
427
+ ownerId: 'ownerId', // Path parameter name for owner ID
428
+ tokenStorage: tokenStorage,
429
+ });
430
+ ```
431
+
432
+ ### Response Interceptors
433
+
434
+ #### AuthorizationResponseInterceptor
435
+
436
+ **Purpose**: Handles automatic token refresh when receiving 401 Unauthorized responses.
437
+
438
+ **Functionality**:
439
+
440
+ - Detects 401 responses
441
+ - Attempts token refresh using configured TokenRefresher
442
+ - Retries original request with new token
443
+ - Exponential backoff for failed refresh attempts
444
+
445
+ **Use Case**: Seamless token refresh without user intervention.
446
+
447
+ ```typescript
448
+ const interceptor = new AuthorizationResponseInterceptor({
449
+ appId: 'your-app-id',
450
+ tokenManager: jwtTokenManager,
451
+ deviceIdStorage: deviceStorage,
452
+ });
453
+ ```
454
+
455
+ ### Error Interceptors
456
+
457
+ #### UnauthorizedErrorInterceptor
458
+
459
+ **Purpose**: Provides centralized handling of authentication failures with custom callback logic.
460
+
461
+ **Functionality**:
462
+
463
+ - Detects 401 responses and RefreshTokenError exceptions
464
+ - Invokes custom callback for error handling
465
+ - Allows applications to implement login redirects, token cleanup, etc.
466
+
467
+ **Use Case**: Custom authentication error handling and user experience flows.
468
+
469
+ ```typescript
470
+ const interceptor = new UnauthorizedErrorInterceptor({
471
+ onUnauthorized: exchange => {
472
+ console.log('Authentication failed for:', exchange.request.url);
473
+ // Redirect to login or show error message
474
+ window.location.href = '/login';
475
+ },
476
+ });
477
+ ```
478
+
479
+ ### Interceptor Order & Execution
480
+
481
+ Interceptors execute in the following default order:
482
+
483
+ 1. **Request Phase**:
484
+ - `AuthorizationRequestInterceptor` (adds Bearer token)
485
+ - `CoSecRequestInterceptor` (adds CoSec headers)
486
+ - `ResourceAttributionRequestInterceptor` (adds path parameters)
487
+
488
+ 2. **Response Phase**:
489
+ - `AuthorizationResponseInterceptor` (handles token refresh)
490
+
491
+ 3. **Error Phase**:
492
+ - `UnauthorizedErrorInterceptor` (handles auth errors)
493
+
494
+ **Note**: Interceptor execution order can be customized using the `order` property. Higher order values execute later in the chain.
226
495
 
227
496
  ## ๐Ÿ› ๏ธ Examples
228
497
 
@@ -231,23 +500,27 @@ const inMemoryStorage = new InMemoryStorage();
231
500
  ```typescript
232
501
  import { Fetcher } from '@ahoo-wang/fetcher';
233
502
  import {
234
- CoSecRequestInterceptor,
235
- CoSecResponseInterceptor,
503
+ AuthorizationRequestInterceptor,
504
+ AuthorizationResponseInterceptor,
236
505
  DeviceIdStorage,
237
506
  TokenStorage,
507
+ JwtTokenManager,
508
+ TokenRefresher,
509
+ CompositeToken,
238
510
  } from '@ahoo-wang/fetcher-cosec';
239
511
 
240
512
  // Create storage instances
241
513
  const deviceIdStorage = new DeviceIdStorage();
242
514
  const tokenStorage = new TokenStorage();
243
515
 
244
- // Create token refresher
245
- const tokenRefresher = {
246
- async refresh(token) {
516
+ // Implement token refresher
517
+ const tokenRefresher: TokenRefresher = {
518
+ async refresh(token: CompositeToken): Promise<CompositeToken> {
247
519
  const response = await fetch('/api/auth/refresh', {
248
520
  method: 'POST',
249
521
  headers: {
250
522
  'Content-Type': 'application/json',
523
+ Authorization: `Bearer ${token.accessToken}`,
251
524
  },
252
525
  body: JSON.stringify({
253
526
  refreshToken: token.refreshToken,
@@ -255,7 +528,7 @@ const tokenRefresher = {
255
528
  });
256
529
 
257
530
  if (!response.ok) {
258
- throw new Error('Token refresh failed');
531
+ throw new Error(`Token refresh failed: ${response.status}`);
259
532
  }
260
533
 
261
534
  const tokens = await response.json();
@@ -266,77 +539,126 @@ const tokenRefresher = {
266
539
  },
267
540
  };
268
541
 
542
+ // Create JWT token manager
543
+ const tokenManager = new JwtTokenManager(tokenStorage, tokenRefresher);
544
+
269
545
  // Create fetcher with CoSec interceptors
270
546
  const secureFetcher = new Fetcher({
271
547
  baseURL: 'https://api.example.com',
272
548
  });
273
549
 
550
+ // Add request interceptor for authentication headers
274
551
  secureFetcher.interceptors.request.use(
275
- new CoSecRequestInterceptor({
552
+ new AuthorizationRequestInterceptor({
276
553
  appId: 'my-app-id',
554
+ tokenManager,
277
555
  deviceIdStorage,
278
- tokenStorage,
279
- tokenRefresher,
280
556
  }),
281
557
  );
282
558
 
559
+ // Add response interceptor for token refresh
283
560
  secureFetcher.interceptors.response.use(
284
- new CoSecResponseInterceptor({
561
+ new AuthorizationResponseInterceptor({
285
562
  appId: 'my-app-id',
563
+ tokenManager,
286
564
  deviceIdStorage,
287
- tokenStorage,
288
- tokenRefresher,
289
565
  }),
290
566
  );
291
567
 
292
- // Use the fetcher
293
- const response = await secureFetcher.get('/api/user/profile');
568
+ // Now all requests will be automatically authenticated
569
+ const userProfile = await secureFetcher.get('/api/user/profile');
570
+ const userPosts = await secureFetcher.get('/api/user/posts');
294
571
  ```
295
572
 
296
- ### Advanced Token Refresh
573
+ ### Advanced Token Refresh with Retry Logic
297
574
 
298
575
  ```typescript
299
- import { CoSecTokenRefresher } from '@ahoo-wang/fetcher-cosec';
576
+ import {
577
+ TokenRefresher,
578
+ CompositeToken,
579
+ JwtTokenManager,
580
+ TokenStorage,
581
+ } from '@ahoo-wang/fetcher-cosec';
582
+
583
+ class ResilientTokenRefresher implements TokenRefresher {
584
+ private maxRetries = 3;
585
+ private baseDelay = 1000; // 1 second
300
586
 
301
- // Custom token refresher with retry logic
302
- class ResilientTokenRefresher extends CoSecTokenRefresher {
303
587
  async refresh(token: CompositeToken): Promise<CompositeToken> {
304
- const maxRetries = 3;
305
588
  let lastError: Error;
306
589
 
307
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
590
+ for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
308
591
  try {
309
- // Add exponential backoff
592
+ // Exponential backoff with jitter
310
593
  if (attempt > 1) {
311
- await new Promise(resolve =>
312
- setTimeout(resolve, Math.pow(2, attempt) * 1000),
313
- );
594
+ const delay = Math.pow(2, attempt - 1) * this.baseDelay;
595
+ const jitter = Math.random() * 0.1 * delay;
596
+ await new Promise(resolve => setTimeout(resolve, delay + jitter));
314
597
  }
315
598
 
316
- const response = await this.options.fetcher.post<CompositeToken>(
317
- this.options.endpoint,
318
- { body: token },
319
- {
320
- resultExtractor: ResultExtractors.Json,
321
- attributes: new Map([[IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true]]),
599
+ const response = await fetch('/api/auth/refresh', {
600
+ method: 'POST',
601
+ headers: {
602
+ 'Content-Type': 'application/json',
603
+ 'X-Retry-Attempt': attempt.toString(),
322
604
  },
323
- );
605
+ body: JSON.stringify({
606
+ refreshToken: token.refreshToken,
607
+ deviceId: await this.getDeviceId(),
608
+ }),
609
+ });
610
+
611
+ if (!response.ok) {
612
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
613
+ }
614
+
615
+ const newTokens = await response.json();
616
+
617
+ // Validate token structure
618
+ if (!newTokens.accessToken || !newTokens.refreshToken) {
619
+ throw new Error('Invalid token response structure');
620
+ }
324
621
 
325
- return response;
622
+ return {
623
+ accessToken: newTokens.accessToken,
624
+ refreshToken: newTokens.refreshToken,
625
+ };
326
626
  } catch (error) {
327
627
  lastError = error as Error;
328
- console.warn(`Token refresh attempt ${attempt} failed:`, error);
628
+ console.warn(
629
+ `Token refresh attempt ${attempt}/${this.maxRetries} failed:`,
630
+ error,
631
+ );
632
+
633
+ // Don't retry on authentication errors (401/403)
634
+ if (error instanceof Response) {
635
+ const status = error.status;
636
+ if (status === 401 || status === 403) {
637
+ throw error;
638
+ }
639
+ }
329
640
 
330
- // Don't retry on authentication errors
331
- if (error.status === 401 || error.status === 403) {
332
- throw error;
641
+ // Don't retry on the last attempt
642
+ if (attempt === this.maxRetries) {
643
+ break;
333
644
  }
334
645
  }
335
646
  }
336
647
 
337
648
  throw lastError!;
338
649
  }
650
+
651
+ private async getDeviceId(): Promise<string> {
652
+ // Implementation to get current device ID
653
+ const deviceStorage = new DeviceIdStorage();
654
+ return await deviceStorage.getOrCreate();
655
+ }
339
656
  }
657
+
658
+ // Usage
659
+ const tokenStorage = new TokenStorage();
660
+ const tokenRefresher = new ResilientTokenRefresher();
661
+ const tokenManager = new JwtTokenManager(tokenStorage, tokenRefresher);
340
662
  ```
341
663
 
342
664
  ### Multi-Tenant Authentication
@@ -349,75 +671,155 @@ import {
349
671
  DeviceIdStorage,
350
672
  TokenStorage,
351
673
  JwtTokenManager,
674
+ TokenRefresher,
675
+ CompositeToken,
352
676
  } from '@ahoo-wang/fetcher-cosec';
353
677
 
354
- // Tenant configuration
678
+ // Tenant configuration interface
355
679
  interface TenantConfig {
356
680
  id: string;
681
+ name: string;
357
682
  appId: string;
358
683
  baseURL: string;
359
684
  refreshEndpoint: string;
685
+ tokenStoragePrefix?: string;
360
686
  }
361
687
 
362
- // Create tenant-specific fetcher
363
- function createTenantFetcher(tenant: TenantConfig) {
364
- const fetcher = new Fetcher({
365
- baseURL: tenant.baseURL,
366
- });
688
+ // Tenant registry for managing multiple tenants
689
+ class TenantRegistry {
690
+ private tenants = new Map<string, TenantConfig>();
691
+ private fetchers = new Map<string, Fetcher>();
367
692
 
368
- // Tenant-specific storage with prefixed keys
369
- const tokenStorage = new TokenStorage(`tenant-${tenant.id}`);
370
- const deviceStorage = new DeviceIdStorage(`tenant-${tenant.id}`);
693
+ registerTenant(config: TenantConfig): void {
694
+ // Use tenant ID as storage prefix for isolation
695
+ const storagePrefix = config.tokenStoragePrefix || `tenant-${config.id}`;
696
+ config.tokenStoragePrefix = storagePrefix;
697
+ this.tenants.set(config.id, config);
698
+ }
371
699
 
372
- // Tenant-specific token refresher
373
- const tokenRefresher = new CoSecTokenRefresher({
374
- fetcher,
375
- endpoint: tenant.refreshEndpoint,
376
- });
700
+ getFetcher(tenantId: string): Fetcher {
701
+ if (this.fetchers.has(tenantId)) {
702
+ return this.fetchers.get(tenantId)!;
703
+ }
377
704
 
378
- const tokenManager = new JwtTokenManager(tokenStorage, tokenRefresher);
705
+ const config = this.tenants.get(tenantId);
706
+ if (!config) {
707
+ throw new Error(`Tenant '${tenantId}' not registered`);
708
+ }
379
709
 
380
- // Add interceptors with tenant context
381
- fetcher.interceptors.request.use(
382
- new AuthorizationRequestInterceptor({
383
- tokenManager,
384
- appId: tenant.appId,
385
- deviceIdStorage: deviceStorage,
386
- }),
387
- );
710
+ const fetcher = this.createTenantFetcher(config);
711
+ this.fetchers.set(tenantId, fetcher);
712
+ return fetcher;
713
+ }
388
714
 
389
- fetcher.interceptors.response.use(
390
- new AuthorizationResponseInterceptor({
391
- tokenManager,
392
- appId: tenant.appId,
393
- deviceIdStorage: deviceStorage,
394
- }),
395
- );
715
+ private createTenantFetcher(config: TenantConfig): Fetcher {
716
+ const fetcher = new Fetcher({
717
+ baseURL: config.baseURL,
718
+ });
719
+
720
+ // Isolated storage per tenant
721
+ const tokenStorage = new TokenStorage(config.tokenStoragePrefix);
722
+ const deviceStorage = new DeviceIdStorage(config.tokenStoragePrefix);
723
+
724
+ // Tenant-specific token refresher
725
+ const tokenRefresher: TokenRefresher = {
726
+ async refresh(token: CompositeToken): Promise<CompositeToken> {
727
+ const response = await fetch(
728
+ `${config.baseURL}${config.refreshEndpoint}`,
729
+ {
730
+ method: 'POST',
731
+ headers: {
732
+ 'Content-Type': 'application/json',
733
+ 'X-Tenant-ID': config.id,
734
+ },
735
+ body: JSON.stringify({
736
+ refreshToken: token.refreshToken,
737
+ }),
738
+ },
739
+ );
740
+
741
+ if (!response.ok) {
742
+ throw new Error(`Token refresh failed for tenant ${config.id}`);
743
+ }
744
+
745
+ const tokens = await response.json();
746
+ return {
747
+ accessToken: tokens.accessToken,
748
+ refreshToken: tokens.refreshToken,
749
+ };
750
+ },
751
+ };
752
+
753
+ const tokenManager = new JwtTokenManager(tokenStorage, tokenRefresher);
396
754
 
397
- return fetcher;
755
+ // Add CoSec interceptors with tenant context
756
+ fetcher.interceptors.request.use(
757
+ new AuthorizationRequestInterceptor({
758
+ appId: config.appId,
759
+ tokenManager,
760
+ deviceIdStorage: deviceStorage,
761
+ }),
762
+ );
763
+
764
+ fetcher.interceptors.response.use(
765
+ new AuthorizationResponseInterceptor({
766
+ appId: config.appId,
767
+ tokenManager,
768
+ deviceIdStorage: deviceStorage,
769
+ }),
770
+ );
771
+
772
+ return fetcher;
773
+ }
774
+
775
+ // Cleanup method for tenant logout
776
+ async logoutTenant(tenantId: string): Promise<void> {
777
+ const config = this.tenants.get(tenantId);
778
+ if (config) {
779
+ const tokenStorage = new TokenStorage(config.tokenStoragePrefix);
780
+ tokenStorage.remove();
781
+
782
+ const deviceStorage = new DeviceIdStorage(config.tokenStoragePrefix);
783
+ deviceStorage.clear();
784
+
785
+ this.fetchers.delete(tenantId);
786
+ }
787
+ }
398
788
  }
399
789
 
400
- // Usage
401
- const tenantA = createTenantFetcher({
402
- id: 'tenant-a',
403
- appId: 'app-a-id',
404
- baseURL: 'https://api-tenant-a.example.com',
790
+ // Usage example
791
+ const tenantRegistry = new TenantRegistry();
792
+
793
+ // Register multiple tenants
794
+ tenantRegistry.registerTenant({
795
+ id: 'enterprise-a',
796
+ name: 'Enterprise A',
797
+ appId: 'app-enterprise-a',
798
+ baseURL: 'https://api.enterprise-a.com',
405
799
  refreshEndpoint: '/auth/refresh',
406
800
  });
407
801
 
408
- const tenantB = createTenantFetcher({
409
- id: 'tenant-b',
410
- appId: 'app-b-id',
411
- baseURL: 'https://api-tenant-b.example.com',
802
+ tenantRegistry.registerTenant({
803
+ id: 'enterprise-b',
804
+ name: 'Enterprise B',
805
+ appId: 'app-enterprise-b',
806
+ baseURL: 'https://api.enterprise-b.com',
412
807
  refreshEndpoint: '/auth/refresh',
413
808
  });
414
809
 
415
- // Each tenant maintains isolated authentication state
416
- const userProfileA = await tenantA.get('/user/profile');
417
- const userProfileB = await tenantB.get('/user/profile');
810
+ // Use tenant-specific fetchers
811
+ const tenantAFetcher = tenantRegistry.getFetcher('enterprise-a');
812
+ const tenantBFetcher = tenantRegistry.getFetcher('enterprise-b');
813
+
814
+ // Each tenant maintains completely isolated authentication
815
+ const profileA = await tenantAFetcher.get('/user/profile');
816
+ const profileB = await tenantBFetcher.get('/user/profile');
817
+
818
+ // Logout specific tenant
819
+ await tenantRegistry.logoutTenant('enterprise-a');
418
820
  ```
419
821
 
420
- ### Error Handling and Recovery
822
+ ### Comprehensive Error Handling and Recovery
421
823
 
422
824
  ```typescript
423
825
  import { Fetcher } from '@ahoo-wang/fetcher';
@@ -427,179 +829,667 @@ import {
427
829
  TokenStorage,
428
830
  DeviceIdStorage,
429
831
  JwtTokenManager,
832
+ TokenRefresher,
833
+ CompositeToken,
430
834
  } from '@ahoo-wang/fetcher-cosec';
431
835
 
432
- // Enhanced error handling
836
+ // Enhanced authentication error handler
433
837
  class AuthErrorHandler {
434
- static handleAuthError(error: any, tokenManager: JwtTokenManager) {
435
- if (error.status === 401) {
436
- // Token expired - clear stored tokens
838
+ private static readonly MAX_RETRY_ATTEMPTS = 3;
839
+ private static readonly RETRY_DELAY_MS = 1000;
840
+
841
+ static async handleAuthError(
842
+ error: any,
843
+ tokenManager: JwtTokenManager,
844
+ context?: { endpoint?: string; attempt?: number },
845
+ ): Promise<boolean> {
846
+ // Returns true if error was handled
847
+ const status = error.status || error.response?.status;
848
+ const attempt = context?.attempt || 1;
849
+
850
+ switch (status) {
851
+ case 401: // Unauthorized - token expired or invalid
852
+ console.warn('Authentication token expired or invalid');
853
+ await this.handleTokenExpiration(tokenManager);
854
+ return true;
855
+
856
+ case 403: // Forbidden - insufficient permissions
857
+ console.error('Access forbidden - insufficient permissions');
858
+ this.handleForbiddenAccess(error, context?.endpoint);
859
+ return true;
860
+
861
+ case 429: // Too Many Requests - rate limited
862
+ console.warn('Rate limited - implementing backoff strategy');
863
+ await this.handleRateLimit(attempt);
864
+ return true;
865
+
866
+ case 500: // Internal Server Error
867
+ case 502: // Bad Gateway
868
+ case 503: // Service Unavailable
869
+ case 504: // Gateway Timeout
870
+ console.warn(`Server error (${status}) - retrying with backoff`);
871
+ await this.handleServerError(attempt);
872
+ return attempt < this.MAX_RETRY_ATTEMPTS;
873
+
874
+ default:
875
+ // Network errors, CORS issues, etc.
876
+ console.error('Authentication network error:', error);
877
+ return this.handleNetworkError(error, attempt);
878
+ }
879
+ }
880
+
881
+ private static async handleTokenExpiration(
882
+ tokenManager: JwtTokenManager,
883
+ ): Promise<void> {
884
+ try {
885
+ // Clear expired tokens
437
886
  tokenManager.tokenStorage.remove();
438
887
 
439
- // Redirect to login or trigger re-authentication
440
- window.location.href = '/login?reason=expired';
441
- } else if (error.status === 403) {
442
- // Forbidden - insufficient permissions
443
- console.error('Access forbidden:', error);
444
- // Show permission error UI
445
- } else if (error.status === 429) {
446
- // Rate limited
447
- console.warn('Rate limited, retrying after delay...');
448
- // Implement retry logic
449
- } else {
450
- // Network or other errors
451
- console.error('Authentication error:', error);
888
+ // Attempt refresh if refresh token exists
889
+ const currentToken = tokenManager.getToken();
890
+ if (currentToken?.refreshToken) {
891
+ await tokenManager.refresh();
892
+ } else {
893
+ // No refresh token - redirect to login
894
+ this.redirectToLogin('token_expired');
895
+ }
896
+ } catch (refreshError) {
897
+ console.error('Token refresh failed:', refreshError);
898
+ this.redirectToLogin('refresh_failed');
899
+ }
900
+ }
901
+
902
+ private static handleForbiddenAccess(error: any, endpoint?: string): void {
903
+ // Log security event
904
+ console.error(`Forbidden access to ${endpoint}:`, error);
905
+
906
+ // Show user-friendly error message
907
+ this.showErrorNotification(
908
+ 'Access Denied',
909
+ 'You do not have permission to access this resource.',
910
+ );
911
+
912
+ // Optionally redirect to appropriate page
913
+ // window.location.href = '/access-denied';
914
+ }
915
+
916
+ private static async handleRateLimit(attempt: number): Promise<void> {
917
+ const delay = Math.min(
918
+ this.RETRY_DELAY_MS * Math.pow(2, attempt - 1),
919
+ 30000, // Max 30 seconds
920
+ );
921
+
922
+ console.log(`Rate limited - waiting ${delay}ms before retry`);
923
+ await new Promise(resolve => setTimeout(resolve, delay));
924
+ }
925
+
926
+ private static async handleServerError(attempt: number): Promise<void> {
927
+ const delay = this.RETRY_DELAY_MS * Math.pow(2, attempt - 1);
928
+ console.log(`Server error - retrying in ${delay}ms (attempt ${attempt})`);
929
+ await new Promise(resolve => setTimeout(resolve, delay));
930
+ }
931
+
932
+ private static handleNetworkError(error: any, attempt: number): boolean {
933
+ // Check if it's a network connectivity issue
934
+ if (!navigator.onLine) {
935
+ console.warn('Network offline - queuing request for retry');
936
+ // Could implement request queuing here
937
+ return true; // Allow retry when back online
452
938
  }
939
+
940
+ // CORS or other network errors
941
+ if (error.name === 'TypeError' && error.message.includes('CORS')) {
942
+ console.error('CORS error - check server configuration');
943
+ return false; // Don't retry CORS errors
944
+ }
945
+
946
+ // Allow retry for other network errors up to max attempts
947
+ return attempt < this.MAX_RETRY_ATTEMPTS;
948
+ }
949
+
950
+ private static redirectToLogin(reason: string): void {
951
+ const loginUrl = `/login?reason=${reason}&returnUrl=${encodeURIComponent(window.location.pathname)}`;
952
+ window.location.href = loginUrl;
953
+ }
954
+
955
+ private static showErrorNotification(title: string, message: string): void {
956
+ // Implementation depends on your notification system
957
+ console.error(`${title}: ${message}`);
958
+ // Example: show toast notification
959
+ // toast.error(message, { title });
453
960
  }
454
961
  }
455
962
 
456
- // Create fetcher with error handling
457
- const fetcher = new Fetcher({ baseURL: 'https://api.example.com' });
963
+ // Create resilient fetcher with comprehensive error handling
964
+ function createResilientFetcher(
965
+ baseURL: string,
966
+ tokenRefresher: TokenRefresher,
967
+ ) {
968
+ const fetcher = new Fetcher({ baseURL });
458
969
 
459
- const tokenManager = new JwtTokenManager(new TokenStorage(), tokenRefresher);
970
+ const tokenManager = new JwtTokenManager(new TokenStorage(), tokenRefresher);
460
971
 
461
- // Add response interceptor with error handling
462
- fetcher.interceptors.response.use(
463
- new AuthorizationResponseInterceptor({
464
- tokenManager,
465
- appId: 'your-app-id',
466
- deviceIdStorage: new DeviceIdStorage(),
467
- }),
468
- // Add error handler after auth interceptor
469
- {
470
- onRejected: error => {
471
- AuthErrorHandler.handleAuthError(error, tokenManager);
472
- throw error; // Re-throw to maintain error chain
972
+ const deviceStorage = new DeviceIdStorage();
973
+
974
+ // Add response interceptor with error recovery
975
+ fetcher.interceptors.response.use(
976
+ new AuthorizationResponseInterceptor({
977
+ appId: 'your-app-id',
978
+ tokenManager,
979
+ deviceIdStorage: deviceStorage,
980
+ }),
981
+ // Global error handler
982
+ {
983
+ onRejected: async error => {
984
+ const wasHandled = await AuthErrorHandler.handleAuthError(
985
+ error,
986
+ tokenManager,
987
+ { endpoint: error.config?.url },
988
+ );
989
+
990
+ if (!wasHandled) {
991
+ throw error; // Re-throw unhandled errors
992
+ }
993
+
994
+ // For handled errors, return a resolved promise to prevent rejection
995
+ return Promise.resolve();
996
+ },
473
997
  },
474
- },
998
+ );
999
+
1000
+ return { fetcher, tokenManager };
1001
+ }
1002
+
1003
+ // Usage
1004
+ const { fetcher, tokenManager } = createResilientFetcher(
1005
+ 'https://api.example.com',
1006
+ yourTokenRefresher,
475
1007
  );
1008
+
1009
+ // All requests now have automatic error handling and recovery
1010
+ try {
1011
+ const data = await fetcher.get('/protected/resource');
1012
+ } catch (error) {
1013
+ // Only unhandled errors will reach here
1014
+ console.error('Unhandled error:', error);
1015
+ }
476
1016
  ```
477
1017
 
478
- ### Performance Monitoring
1018
+ ### Performance Monitoring and Optimization
479
1019
 
480
1020
  ```typescript
481
1021
  import { Fetcher } from '@ahoo-wang/fetcher';
482
1022
  import {
483
1023
  AuthorizationRequestInterceptor,
1024
+ AuthorizationResponseInterceptor,
484
1025
  TokenStorage,
1026
+ DeviceIdStorage,
485
1027
  JwtTokenManager,
1028
+ TokenRefresher,
1029
+ CompositeToken,
486
1030
  } from '@ahoo-wang/fetcher-cosec';
487
1031
 
488
- // Performance monitoring interceptor
1032
+ // Comprehensive authentication performance monitor
489
1033
  class AuthPerformanceMonitor {
490
1034
  private metrics = {
1035
+ // Token operations
491
1036
  tokenRefreshCount: 0,
492
- averageRefreshTime: 0,
493
- totalRefreshTime: 0,
494
- interceptorOverhead: 0,
1037
+ tokenRefreshTotalTime: 0,
1038
+ tokenRefreshAverageTime: 0,
1039
+ tokenRefreshSuccessRate: 1.0,
1040
+
1041
+ // Storage operations
1042
+ storageReadCount: 0,
1043
+ storageWriteCount: 0,
1044
+ storageReadTime: 0,
1045
+ storageWriteTime: 0,
1046
+
1047
+ // Interceptor performance
1048
+ requestInterceptorOverhead: 0,
1049
+ responseInterceptorOverhead: 0,
1050
+ totalRequests: 0,
1051
+
1052
+ // Device operations
1053
+ deviceIdGenerationTime: 0,
1054
+ deviceIdReadTime: 0,
1055
+
1056
+ // Error tracking
1057
+ errorCount: 0,
1058
+ retryCount: 0,
1059
+
1060
+ // Cache performance
1061
+ cacheHitRate: 0,
1062
+ cacheHits: 0,
1063
+ cacheMisses: 0,
495
1064
  };
496
1065
 
497
- recordTokenRefresh(duration: number) {
498
- this.metrics.tokenRefreshCount++;
499
- this.metrics.totalRefreshTime += duration;
500
- this.metrics.averageRefreshTime =
501
- this.metrics.totalRefreshTime / this.metrics.tokenRefreshCount;
1066
+ private startTimes = new Map<string, number>();
502
1067
 
503
- // Report to monitoring service
504
- console.log(
505
- `Token refresh: ${duration}ms (avg: ${this.metrics.averageRefreshTime.toFixed(2)}ms)`,
506
- );
1068
+ // Token refresh monitoring
1069
+ startTokenRefresh(operationId: string): void {
1070
+ this.startTimes.set(`refresh-${operationId}`, performance.now());
1071
+ }
1072
+
1073
+ endTokenRefresh(operationId: string, success: boolean): void {
1074
+ const startTime = this.startTimes.get(`refresh-${operationId}`);
1075
+ if (startTime) {
1076
+ const duration = performance.now() - startTime;
1077
+ this.metrics.tokenRefreshCount++;
1078
+ this.metrics.tokenRefreshTotalTime += duration;
1079
+ this.metrics.tokenRefreshAverageTime =
1080
+ this.metrics.tokenRefreshTotalTime / this.metrics.tokenRefreshCount;
1081
+
1082
+ if (!success) {
1083
+ this.metrics.tokenRefreshSuccessRate =
1084
+ ((this.metrics.tokenRefreshCount - 1) /
1085
+ this.metrics.tokenRefreshCount) *
1086
+ this.metrics.tokenRefreshSuccessRate;
1087
+ }
1088
+
1089
+ this.startTimes.delete(`refresh-${operationId}`);
1090
+ this.reportMetric('token_refresh_duration', duration);
1091
+ }
1092
+ }
1093
+
1094
+ // Storage operation monitoring
1095
+ recordStorageOperation(operation: 'read' | 'write', duration: number): void {
1096
+ if (operation === 'read') {
1097
+ this.metrics.storageReadCount++;
1098
+ this.metrics.storageReadTime += duration;
1099
+ } else {
1100
+ this.metrics.storageWriteCount++;
1101
+ this.metrics.storageWriteTime += duration;
1102
+ }
1103
+ }
1104
+
1105
+ // Interceptor overhead monitoring
1106
+ recordInterceptorOverhead(
1107
+ type: 'request' | 'response',
1108
+ duration: number,
1109
+ ): void {
1110
+ if (type === 'request') {
1111
+ this.metrics.requestInterceptorOverhead += duration;
1112
+ } else {
1113
+ this.metrics.responseInterceptorOverhead += duration;
1114
+ }
1115
+ this.metrics.totalRequests++;
1116
+ }
1117
+
1118
+ // Device operation monitoring
1119
+ recordDeviceOperation(
1120
+ operation: 'generate' | 'read',
1121
+ duration: number,
1122
+ ): void {
1123
+ if (operation === 'generate') {
1124
+ this.metrics.deviceIdGenerationTime += duration;
1125
+ } else {
1126
+ this.metrics.deviceIdReadTime += duration;
1127
+ }
1128
+ }
1129
+
1130
+ // Error and retry tracking
1131
+ recordError(): void {
1132
+ this.metrics.errorCount++;
1133
+ }
1134
+
1135
+ recordRetry(): void {
1136
+ this.metrics.retryCount++;
1137
+ }
1138
+
1139
+ // Cache performance
1140
+ recordCacheAccess(hit: boolean): void {
1141
+ if (hit) {
1142
+ this.metrics.cacheHits++;
1143
+ } else {
1144
+ this.metrics.cacheMisses++;
1145
+ }
1146
+ const total = this.metrics.cacheHits + this.metrics.cacheMisses;
1147
+ this.metrics.cacheHitRate = total > 0 ? this.metrics.cacheHits / total : 0;
507
1148
  }
508
1149
 
509
- recordInterceptorOverhead(duration: number) {
510
- this.metrics.interceptorOverhead = duration;
1150
+ // Reporting
1151
+ private reportMetric(name: string, value: number): void {
1152
+ // Send to monitoring service (e.g., DataDog, New Relic, etc.)
1153
+ console.log(`[AuthPerf] ${name}: ${value.toFixed(2)}ms`);
1154
+
1155
+ // Threshold alerts
1156
+ if (name === 'token_refresh_duration' && value > 5000) {
1157
+ console.warn(
1158
+ `[AuthPerf] Slow token refresh detected: ${value.toFixed(2)}ms`,
1159
+ );
1160
+ }
511
1161
  }
512
1162
 
513
1163
  getMetrics() {
514
- return { ...this.metrics };
1164
+ return {
1165
+ ...this.metrics,
1166
+ // Calculated fields
1167
+ averageStorageReadTime:
1168
+ this.metrics.storageReadCount > 0
1169
+ ? this.metrics.storageReadTime / this.metrics.storageReadCount
1170
+ : 0,
1171
+ averageStorageWriteTime:
1172
+ this.metrics.storageWriteCount > 0
1173
+ ? this.metrics.storageWriteTime / this.metrics.storageWriteCount
1174
+ : 0,
1175
+ averageRequestOverhead:
1176
+ this.metrics.totalRequests > 0
1177
+ ? this.metrics.requestInterceptorOverhead / this.metrics.totalRequests
1178
+ : 0,
1179
+ averageResponseOverhead:
1180
+ this.metrics.totalRequests > 0
1181
+ ? this.metrics.responseInterceptorOverhead /
1182
+ this.metrics.totalRequests
1183
+ : 0,
1184
+ };
1185
+ }
1186
+
1187
+ reset(): void {
1188
+ // Reset counters but keep averages
1189
+ this.metrics.tokenRefreshCount = 0;
1190
+ this.metrics.tokenRefreshTotalTime = 0;
1191
+ this.metrics.storageReadCount = 0;
1192
+ this.metrics.storageWriteCount = 0;
1193
+ this.metrics.storageReadTime = 0;
1194
+ this.metrics.storageWriteTime = 0;
1195
+ this.metrics.totalRequests = 0;
1196
+ this.metrics.requestInterceptorOverhead = 0;
1197
+ this.metrics.responseInterceptorOverhead = 0;
1198
+ this.metrics.errorCount = 0;
1199
+ this.metrics.retryCount = 0;
1200
+ this.metrics.cacheHits = 0;
1201
+ this.metrics.cacheMisses = 0;
515
1202
  }
516
1203
  }
517
1204
 
518
- // Create performance monitor
519
- const performanceMonitor = new AuthPerformanceMonitor();
1205
+ // Enhanced token refresher with performance monitoring
1206
+ class MonitoredTokenRefresher implements TokenRefresher {
1207
+ constructor(
1208
+ private baseRefresher: TokenRefresher,
1209
+ private monitor: AuthPerformanceMonitor,
1210
+ ) {}
520
1211
 
521
- // Enhanced token refresher with monitoring
522
- const tokenRefresher = {
523
1212
  async refresh(token: CompositeToken): Promise<CompositeToken> {
524
- const startTime = performance.now();
1213
+ const operationId = `refresh-${Date.now()}-${Math.random()}`;
1214
+ this.monitor.startTokenRefresh(operationId);
525
1215
 
526
1216
  try {
527
- const response = await fetch('/api/auth/refresh', {
528
- method: 'POST',
529
- headers: { 'Content-Type': 'application/json' },
530
- body: JSON.stringify(token),
531
- });
532
-
533
- if (!response.ok) {
534
- throw new Error(`Refresh failed: ${response.status}`);
535
- }
536
-
537
- const newToken = await response.json();
538
-
539
- const duration = performance.now() - startTime;
540
- performanceMonitor.recordTokenRefresh(duration);
541
-
542
- return newToken;
1217
+ const result = await this.baseRefresher.refresh(token);
1218
+ this.monitor.endTokenRefresh(operationId, true);
1219
+ return result;
543
1220
  } catch (error) {
544
- const duration = performance.now() - startTime;
545
- performanceMonitor.recordTokenRefresh(duration);
1221
+ this.monitor.endTokenRefresh(operationId, false);
1222
+ this.monitor.recordError();
546
1223
  throw error;
547
1224
  }
548
- },
549
- };
1225
+ }
1226
+ }
550
1227
 
551
- // Create fetcher with monitoring
552
- const fetcher = new Fetcher({ baseURL: 'https://api.example.com' });
1228
+ // Enhanced storage with performance monitoring
1229
+ class MonitoredTokenStorage extends TokenStorage {
1230
+ constructor(
1231
+ private baseStorage: TokenStorage,
1232
+ private monitor: AuthPerformanceMonitor,
1233
+ ) {
1234
+ super();
1235
+ }
553
1236
 
554
- const tokenManager = new JwtTokenManager(new TokenStorage(), tokenRefresher);
1237
+ set(token: CompositeToken): void {
1238
+ const startTime = performance.now();
1239
+ this.baseStorage.set(token);
1240
+ const duration = performance.now() - startTime;
1241
+ this.monitor.recordStorageOperation('write', duration);
1242
+ }
555
1243
 
556
- // Add request interceptor with performance monitoring
557
- fetcher.interceptors.request.use(
558
- new AuthorizationRequestInterceptor({ tokenManager }),
559
- // Monitor interceptor performance
560
- {
561
- onFulfilled: async config => {
562
- const startTime = performance.now();
563
- // Process request
564
- const result = await config;
565
- const duration = performance.now() - startTime;
566
- performanceMonitor.recordInterceptorOverhead(duration);
567
- return result;
1244
+ get(): CompositeToken | null {
1245
+ const startTime = performance.now();
1246
+ const result = this.baseStorage.get();
1247
+ const duration = performance.now() - startTime;
1248
+ this.monitor.recordStorageOperation('read', duration);
1249
+ return result;
1250
+ }
1251
+
1252
+ remove(): void {
1253
+ const startTime = performance.now();
1254
+ this.baseStorage.remove();
1255
+ const duration = performance.now() - startTime;
1256
+ this.monitor.recordStorageOperation('write', duration);
1257
+ }
1258
+ }
1259
+
1260
+ // Create monitored fetcher
1261
+ function createMonitoredFetcher(
1262
+ baseURL: string,
1263
+ baseTokenRefresher: TokenRefresher,
1264
+ ) {
1265
+ const monitor = new AuthPerformanceMonitor();
1266
+
1267
+ const tokenStorage = new MonitoredTokenStorage(new TokenStorage(), monitor);
1268
+
1269
+ const tokenRefresher = new MonitoredTokenRefresher(
1270
+ baseTokenRefresher,
1271
+ monitor,
1272
+ );
1273
+
1274
+ const tokenManager = new JwtTokenManager(tokenStorage, tokenRefresher);
1275
+ const deviceStorage = new DeviceIdStorage();
1276
+
1277
+ const fetcher = new Fetcher({ baseURL });
1278
+
1279
+ // Add request interceptor with monitoring
1280
+ fetcher.interceptors.request.use(
1281
+ new AuthorizationRequestInterceptor({
1282
+ appId: 'monitored-app',
1283
+ tokenManager,
1284
+ deviceIdStorage: deviceStorage,
1285
+ }),
1286
+ // Monitor request interceptor overhead
1287
+ {
1288
+ onFulfilled: async config => {
1289
+ const startTime = performance.now();
1290
+ const result = await config;
1291
+ const duration = performance.now() - startTime;
1292
+ monitor.recordInterceptorOverhead('request', duration);
1293
+ return result;
1294
+ },
1295
+ },
1296
+ );
1297
+
1298
+ // Add response interceptor with monitoring
1299
+ fetcher.interceptors.response.use(
1300
+ new AuthorizationResponseInterceptor({
1301
+ appId: 'monitored-app',
1302
+ tokenManager,
1303
+ deviceIdStorage: deviceStorage,
1304
+ }),
1305
+ // Monitor response interceptor overhead
1306
+ {
1307
+ onFulfilled: async response => {
1308
+ const startTime = performance.now();
1309
+ const result = await response;
1310
+ const duration = performance.now() - startTime;
1311
+ monitor.recordInterceptorOverhead('response', duration);
1312
+ return result;
1313
+ },
568
1314
  },
1315
+ );
1316
+
1317
+ return { fetcher, monitor };
1318
+ }
1319
+
1320
+ // Usage example
1321
+ const baseTokenRefresher: TokenRefresher = {
1322
+ async refresh(token: CompositeToken): Promise<CompositeToken> {
1323
+ const response = await fetch('/api/auth/refresh', {
1324
+ method: 'POST',
1325
+ headers: { 'Content-Type': 'application/json' },
1326
+ body: JSON.stringify({ refreshToken: token.refreshToken }),
1327
+ });
1328
+
1329
+ if (!response.ok) {
1330
+ throw new Error('Token refresh failed');
1331
+ }
1332
+
1333
+ return await response.json();
569
1334
  },
1335
+ };
1336
+
1337
+ const { fetcher, monitor } = createMonitoredFetcher(
1338
+ 'https://api.example.com',
1339
+ baseTokenRefresher,
570
1340
  );
571
1341
 
572
- // Access metrics
573
- console.log('Auth Performance Metrics:', performanceMonitor.getMetrics());
1342
+ // Use the monitored fetcher
1343
+ await fetcher.get('/user/profile');
1344
+
1345
+ // Get performance metrics
1346
+ setInterval(() => {
1347
+ const metrics = monitor.getMetrics();
1348
+ console.log('Authentication Performance Metrics:', {
1349
+ tokenRefresh: {
1350
+ count: metrics.tokenRefreshCount,
1351
+ averageTime: `${metrics.tokenRefreshAverageTime.toFixed(2)}ms`,
1352
+ successRate: `${(metrics.tokenRefreshSuccessRate * 100).toFixed(1)}%`,
1353
+ },
1354
+ storage: {
1355
+ reads: metrics.storageReadCount,
1356
+ writes: metrics.storageWriteCount,
1357
+ averageReadTime: `${metrics.averageStorageReadTime.toFixed(2)}ms`,
1358
+ averageWriteTime: `${metrics.averageStorageWriteTime.toFixed(2)}ms`,
1359
+ },
1360
+ interceptors: {
1361
+ totalRequests: metrics.totalRequests,
1362
+ averageRequestOverhead: `${metrics.averageRequestOverhead.toFixed(2)}ms`,
1363
+ averageResponseOverhead: `${metrics.averageResponseOverhead.toFixed(2)}ms`,
1364
+ },
1365
+ cache: {
1366
+ hitRate: `${(metrics.cacheHitRate * 100).toFixed(1)}%`,
1367
+ },
1368
+ errors: {
1369
+ count: metrics.errorCount,
1370
+ retries: metrics.retryCount,
1371
+ },
1372
+ });
1373
+ }, 30000); // Report every 30 seconds
574
1374
  ```
575
1375
 
576
1376
  ## ๐Ÿงช Testing
577
1377
 
1378
+ The package includes comprehensive test coverage for all components:
1379
+
578
1380
  ```bash
579
- # Run tests
1381
+ # Run all tests
580
1382
  pnpm test
581
1383
 
582
- # Run tests with coverage
1384
+ # Run tests with coverage report
583
1385
  pnpm test --coverage
1386
+
1387
+ # Run tests in watch mode during development
1388
+ pnpm test --watch
1389
+
1390
+ # Run specific test file
1391
+ pnpm test tokenStorage.test.ts
1392
+
1393
+ # Run integration tests
1394
+ pnpm test:it
584
1395
  ```
585
1396
 
586
- ## ๐ŸŒ CoSec Framework
1397
+ ### Test Coverage
587
1398
 
588
- This package is designed to work with the [CoSec authentication framework](https://github.com/Ahoo-Wang/CoSec). For more
589
- information about CoSec features and capabilities, please visit
590
- the [CoSec GitHub repository](https://github.com/Ahoo-Wang/CoSec).
1399
+ - **Unit Tests**: Individual component testing with mocks
1400
+ - **Integration Tests**: End-to-end authentication flows
1401
+ - **Security Tests**: Token validation and security scenarios
1402
+ - **Performance Tests**: Benchmarking and memory leak detection
1403
+
1404
+ ### Testing Utilities
1405
+
1406
+ ```typescript
1407
+ import {
1408
+ createMockJwtToken,
1409
+ createExpiredJwtToken,
1410
+ MockTokenStorage,
1411
+ MockDeviceStorage,
1412
+ } from '@ahoo-wang/fetcher-cosec/test-utils';
1413
+
1414
+ // Create test tokens
1415
+ const validToken = createMockJwtToken({ sub: 'user123' });
1416
+ const expiredToken = createExpiredJwtToken();
1417
+
1418
+ // Use mock storage for isolated testing
1419
+ const tokenStorage = new MockTokenStorage();
1420
+ const deviceStorage = new MockDeviceStorage();
1421
+ ```
1422
+
1423
+ ## ๐ŸŒ CoSec Framework Integration
1424
+
1425
+ This package provides seamless integration with the [CoSec authentication framework](https://github.com/Ahoo-Wang/CoSec), enabling enterprise-grade security features:
1426
+
1427
+ ### Key Integration Points
1428
+
1429
+ - **Centralized Authentication**: Connects to CoSec's authentication server
1430
+ - **Device Management**: Automatic device registration and tracking
1431
+ - **Token Lifecycle**: Full JWT token management with refresh capabilities
1432
+ - **Security Policies**: Enforces CoSec security policies and rules
1433
+ - **Audit Logging**: Comprehensive request attribution and logging
1434
+
1435
+ ### Architecture Overview
1436
+
1437
+ ```
1438
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
1439
+ โ”‚ Application โ”‚โ”€โ”€โ”€โ”€โ”‚ Fetcher CoSec โ”‚โ”€โ”€โ”€โ”€โ”‚ CoSec โ”‚
1440
+ โ”‚ โ”‚ โ”‚ Integration โ”‚ โ”‚ Framework โ”‚
1441
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
1442
+ โ”‚ โ”‚ โ”‚
1443
+ โ””โ”€ HTTP Requests โ””โ”€ Auth Headers โ””โ”€ Token Validation
1444
+ โ””โ”€ Response Handling โ””โ”€ Token Refresh โ””โ”€ Device Tracking
1445
+ โ””โ”€ Error Recovery โ””โ”€ Security Policies โ””โ”€ Audit Logging
1446
+ ```
1447
+
1448
+ For detailed CoSec framework documentation and advanced configuration options, visit the [CoSec GitHub repository](https://github.com/Ahoo-Wang/CoSec).
591
1449
 
592
1450
  ## ๐Ÿค Contributing
593
1451
 
594
- Contributions are welcome! Please see
595
- the [contributing guide](https://github.com/Ahoo-Wang/fetcher/blob/main/CONTRIBUTING.md) for more details.
1452
+ We welcome contributions! Please see our [contributing guide](https://github.com/Ahoo-Wang/fetcher/blob/main/CONTRIBUTING.md) for details on:
1453
+
1454
+ - **Development Setup**: Getting started with the codebase
1455
+ - **Code Standards**: TypeScript, linting, and testing guidelines
1456
+ - **Pull Request Process**: How to submit changes
1457
+ - **Issue Reporting**: Bug reports and feature requests
1458
+
1459
+ ### Development Commands
1460
+
1461
+ ```bash
1462
+ # Install dependencies
1463
+ pnpm install
1464
+
1465
+ # Start development server
1466
+ pnpm dev
1467
+
1468
+ # Run linting and type checking
1469
+ pnpm lint
1470
+ pnpm typecheck
1471
+
1472
+ # Run test suite
1473
+ pnpm test
1474
+
1475
+ # Build package
1476
+ pnpm build
1477
+ ```
596
1478
 
597
1479
  ## ๐Ÿ“„ License
598
1480
 
599
- Apache-2.0
1481
+ Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/Ahoo-Wang/fetcher/blob/main/LICENSE) for details.
1482
+
1483
+ ## ๐Ÿ™ Acknowledgments
1484
+
1485
+ - [CoSec Framework](https://github.com/Ahoo-Wang/CoSec) - Enterprise authentication framework
1486
+ - [Fetcher HTTP Client](https://github.com/Ahoo-Wang/fetcher) - Modern TypeScript HTTP client
1487
+ - [JWT.io](https://jwt.io) - JWT token standard and tooling
600
1488
 
601
1489
  ---
602
1490
 
603
1491
  <p align="center">
604
- Part of the <a href="https://github.com/Ahoo-Wang/fetcher">Fetcher</a> ecosystem
1492
+ <strong>Part of the <a href="https://github.com/Ahoo-Wang/fetcher">Fetcher</a> ecosystem</strong>
1493
+ <br>
1494
+ <sub>Modern HTTP client libraries for TypeScript applications</sub>
605
1495
  </p>