@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 +1147 -257
- package/README.zh-CN.md +718 -139
- package/dist/index.es.js +2 -2
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/unauthorizedErrorInterceptor.d.ts +2 -2
- package/dist/unauthorizedErrorInterceptor.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,21 +9,23 @@
|
|
|
9
9
|
[](https://deepwiki.com/Ahoo-Wang/fetcher)
|
|
10
10
|
[](https://fetcher.ahoo.me/?path=/docs/cosec-introduction--docs)
|
|
11
11
|
|
|
12
|
-
|
|
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**:
|
|
21
|
-
- **๐ฑ Device Management**:
|
|
22
|
-
- **๐ Token Refresh**:
|
|
23
|
-
- **๐ Request
|
|
24
|
-
- **๐พ Token Storage**:
|
|
25
|
-
- **๐ก๏ธ
|
|
26
|
-
-
|
|
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
|
-
|
|
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
|
-
//
|
|
69
|
-
const response = await
|
|
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:
|
|
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.
|
|
80
|
-
refreshToken: tokens.
|
|
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
|
-
*
|
|
124
|
-
*/
|
|
125
|
-
tokenStorage: TokenStorage;
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Token refresher function
|
|
147
|
+
* JWT token manager for handling token operations
|
|
129
148
|
*/
|
|
130
|
-
|
|
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
|
-
|
|
168
|
+
Automatically adds CoSec authentication headers to outgoing HTTP requests.
|
|
150
169
|
|
|
151
170
|
```typescript
|
|
152
|
-
new AuthorizationRequestInterceptor(
|
|
153
|
-
:
|
|
154
|
-
|
|
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
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
|
227
|
+
// Store composite token
|
|
177
228
|
tokenStorage.set({
|
|
178
|
-
accessToken: '
|
|
179
|
-
refreshToken: '
|
|
229
|
+
accessToken: 'eyJ...',
|
|
230
|
+
refreshToken: 'eyJ...',
|
|
180
231
|
});
|
|
181
232
|
|
|
182
|
-
//
|
|
233
|
+
// Retrieve token
|
|
183
234
|
const token = tokenStorage.get();
|
|
184
235
|
|
|
185
|
-
//
|
|
186
|
-
tokenStorage.
|
|
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
|
|
245
|
+
Manages persistent device identification with localStorage.
|
|
192
246
|
|
|
193
247
|
```typescript
|
|
194
|
-
const
|
|
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
|
-
//
|
|
197
|
-
const
|
|
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
|
-
|
|
200
|
-
deviceIdStorage.set('specific-device-id');
|
|
383
|
+
**Headers Added**:
|
|
201
384
|
|
|
202
|
-
|
|
203
|
-
const currentDeviceId = deviceIdStorage.get();
|
|
385
|
+
- `Authorization: Bearer <access-token>`
|
|
204
386
|
|
|
205
|
-
|
|
206
|
-
deviceIdStorage.clear();
|
|
387
|
+
**Use Case**: Standard JWT authentication for API requests.
|
|
207
388
|
|
|
208
|
-
|
|
209
|
-
const
|
|
389
|
+
```typescript
|
|
390
|
+
const interceptor = new AuthorizationRequestInterceptor({
|
|
391
|
+
appId: 'your-app-id',
|
|
392
|
+
tokenManager: jwtTokenManager,
|
|
393
|
+
deviceIdStorage: deviceStorage,
|
|
394
|
+
});
|
|
210
395
|
```
|
|
211
396
|
|
|
212
|
-
####
|
|
397
|
+
#### CoSecRequestInterceptor
|
|
398
|
+
|
|
399
|
+
**Purpose**: Adds basic CoSec identification headers to requests.
|
|
400
|
+
|
|
401
|
+
**Headers Added**:
|
|
213
402
|
|
|
214
|
-
|
|
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
|
|
410
|
+
const interceptor = new CoSecRequestInterceptor({
|
|
411
|
+
appId: 'your-app-id',
|
|
412
|
+
deviceIdStorage: deviceStorage,
|
|
413
|
+
});
|
|
218
414
|
```
|
|
219
415
|
|
|
220
|
-
|
|
416
|
+
#### ResourceAttributionRequestInterceptor
|
|
417
|
+
|
|
418
|
+
**Purpose**: Automatically injects tenant and owner ID path parameters from JWT token claims.
|
|
221
419
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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
|
-
//
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
//
|
|
293
|
-
const
|
|
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 {
|
|
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
|
-
//
|
|
592
|
+
// Exponential backoff with jitter
|
|
310
593
|
if (attempt > 1) {
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
622
|
+
return {
|
|
623
|
+
accessToken: newTokens.accessToken,
|
|
624
|
+
refreshToken: newTokens.refreshToken,
|
|
625
|
+
};
|
|
326
626
|
} catch (error) {
|
|
327
627
|
lastError = error as Error;
|
|
328
|
-
console.warn(
|
|
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
|
|
331
|
-
if (
|
|
332
|
-
|
|
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
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
});
|
|
700
|
+
getFetcher(tenantId: string): Fetcher {
|
|
701
|
+
if (this.fetchers.has(tenantId)) {
|
|
702
|
+
return this.fetchers.get(tenantId)!;
|
|
703
|
+
}
|
|
377
704
|
|
|
378
|
-
|
|
705
|
+
const config = this.tenants.get(tenantId);
|
|
706
|
+
if (!config) {
|
|
707
|
+
throw new Error(`Tenant '${tenantId}' not registered`);
|
|
708
|
+
}
|
|
379
709
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
390
|
-
new
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
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
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
409
|
-
id: '
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
//
|
|
416
|
-
const
|
|
417
|
-
const
|
|
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
|
|
836
|
+
// Enhanced authentication error handler
|
|
433
837
|
class AuthErrorHandler {
|
|
434
|
-
static
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
//
|
|
1032
|
+
// Comprehensive authentication performance monitor
|
|
489
1033
|
class AuthPerformanceMonitor {
|
|
490
1034
|
private metrics = {
|
|
1035
|
+
// Token operations
|
|
491
1036
|
tokenRefreshCount: 0,
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
510
|
-
|
|
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 {
|
|
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
|
-
//
|
|
519
|
-
|
|
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
|
|
1213
|
+
const operationId = `refresh-${Date.now()}-${Math.random()}`;
|
|
1214
|
+
this.monitor.startTokenRefresh(operationId);
|
|
525
1215
|
|
|
526
1216
|
try {
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
545
|
-
|
|
1221
|
+
this.monitor.endTokenRefresh(operationId, false);
|
|
1222
|
+
this.monitor.recordError();
|
|
546
1223
|
throw error;
|
|
547
1224
|
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
550
1227
|
|
|
551
|
-
//
|
|
552
|
-
|
|
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
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
//
|
|
573
|
-
|
|
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
|
-
|
|
1397
|
+
### Test Coverage
|
|
587
1398
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
595
|
-
|
|
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
|
|
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>
|