fetchguard 1.6.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +268 -66
- package/dist/index.d.ts +594 -31
- package/dist/index.js +544 -86
- package/dist/index.js.map +1 -1
- package/dist/worker.js +177 -74
- package/dist/worker.js.map +1 -1
- package/package.json +8 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as ts_micro_result from 'ts-micro-result';
|
|
2
2
|
import { Result, ErrorDetail, ResultMeta } from 'ts-micro-result';
|
|
3
|
-
import * as ts_micro_result_factories_errors_advanced from 'ts-micro-result/factories/errors-advanced';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* FetchGuard Business Types
|
|
@@ -23,6 +22,18 @@ interface TokenInfo {
|
|
|
23
22
|
refreshToken?: string | null;
|
|
24
23
|
user?: unknown;
|
|
25
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Options for token exchange operation
|
|
27
|
+
*
|
|
28
|
+
* Used when switching tenant, changing scope, or any operation
|
|
29
|
+
* that exchanges current token for a new one with different claims
|
|
30
|
+
*/
|
|
31
|
+
interface ExchangeTokenOptions {
|
|
32
|
+
/** HTTP method to use. Default: 'POST' */
|
|
33
|
+
method?: 'POST' | 'PUT';
|
|
34
|
+
/** Payload to send with the request (e.g., tenantId, scope) */
|
|
35
|
+
payload?: Record<string, unknown>;
|
|
36
|
+
}
|
|
26
37
|
/**
|
|
27
38
|
* Auth result returned from auth operations and auth state changes
|
|
28
39
|
* Used by: login(), logout(), refreshToken(), onAuthStateChanged()
|
|
@@ -66,6 +77,18 @@ interface TokenProvider {
|
|
|
66
77
|
* @returns Result<TokenInfo> with all fields reset (token = '', refreshToken = undefined, user = undefined)
|
|
67
78
|
*/
|
|
68
79
|
logout(payload?: unknown): Promise<Result<TokenInfo>>;
|
|
80
|
+
/**
|
|
81
|
+
* Exchange current token for a new one with different context
|
|
82
|
+
*
|
|
83
|
+
* Useful for switching tenants, changing scopes, or any operation
|
|
84
|
+
* that requires exchanging the current token for a new one.
|
|
85
|
+
*
|
|
86
|
+
* @param accessToken - Current access token (injected by worker)
|
|
87
|
+
* @param url - URL to call for token exchange
|
|
88
|
+
* @param options - Exchange options (method, payload)
|
|
89
|
+
* @returns Result<TokenInfo> with new tokens
|
|
90
|
+
*/
|
|
91
|
+
exchangeToken(accessToken: string, url: string, options?: ExchangeTokenOptions): Promise<Result<TokenInfo>>;
|
|
69
92
|
/**
|
|
70
93
|
* Custom auth methods (optional)
|
|
71
94
|
* Examples: loginWithPhone, loginWithGoogle, loginWithFacebook, etc.
|
|
@@ -76,6 +99,16 @@ interface TokenProvider {
|
|
|
76
99
|
*/
|
|
77
100
|
[key: string]: (...args: any[]) => Promise<Result<TokenInfo>>;
|
|
78
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Storage error context for debugging
|
|
104
|
+
*/
|
|
105
|
+
type StorageErrorContext = 'get' | 'set' | 'delete' | 'open';
|
|
106
|
+
/**
|
|
107
|
+
* Storage error callback type
|
|
108
|
+
* Called when IndexedDB operations fail (quota exceeded, permission denied, etc.)
|
|
109
|
+
* Storage still fails closed (returns null), but this allows logging/debugging.
|
|
110
|
+
*/
|
|
111
|
+
type StorageErrorCallback = (error: Error, context: StorageErrorContext) => void;
|
|
79
112
|
/**
|
|
80
113
|
* Interface for refresh token storage - only stores refresh token
|
|
81
114
|
*
|
|
@@ -144,6 +177,46 @@ interface FetchGuardOptions {
|
|
|
144
177
|
refreshEarlyMs?: number;
|
|
145
178
|
/** Default headers to include in all requests */
|
|
146
179
|
defaultHeaders?: Record<string, string>;
|
|
180
|
+
/**
|
|
181
|
+
* Maximum concurrent requests to worker (default: 6)
|
|
182
|
+
* Controls how many requests can be in-flight simultaneously.
|
|
183
|
+
* Set to 1 for strictly sequential processing.
|
|
184
|
+
* Higher values increase throughput but may cause worker congestion.
|
|
185
|
+
*/
|
|
186
|
+
maxConcurrent?: number;
|
|
187
|
+
/**
|
|
188
|
+
* Maximum queue size for pending requests (default: 1000)
|
|
189
|
+
* When queue is full, new requests will immediately fail with QUEUE_FULL error.
|
|
190
|
+
* Prevents memory leak if worker is unresponsive.
|
|
191
|
+
*/
|
|
192
|
+
maxQueueSize?: number;
|
|
193
|
+
/**
|
|
194
|
+
* Worker setup timeout in milliseconds (default: 10000)
|
|
195
|
+
* How long to wait for worker to be ready before failing.
|
|
196
|
+
*/
|
|
197
|
+
setupTimeout?: number;
|
|
198
|
+
/**
|
|
199
|
+
* Default request timeout in milliseconds (default: 30000)
|
|
200
|
+
* How long to wait for a request to complete before timing out.
|
|
201
|
+
* Can be overridden per-request via fetch options.
|
|
202
|
+
*/
|
|
203
|
+
requestTimeout?: number;
|
|
204
|
+
/**
|
|
205
|
+
* Debug hooks for observing operations (logging, monitoring)
|
|
206
|
+
* All hooks are observe-only - they cannot modify requests/responses.
|
|
207
|
+
*/
|
|
208
|
+
debug?: DebugHooks;
|
|
209
|
+
/**
|
|
210
|
+
* Retry configuration for network errors
|
|
211
|
+
* Only retries on transport failures, NOT on HTTP errors (4xx/5xx)
|
|
212
|
+
*/
|
|
213
|
+
retry?: RetryConfig;
|
|
214
|
+
/**
|
|
215
|
+
* Request deduplication configuration
|
|
216
|
+
* When enabled, duplicate requests to the same URL within a time window
|
|
217
|
+
* will share the same response instead of making multiple requests.
|
|
218
|
+
*/
|
|
219
|
+
dedupe?: DedupeConfig;
|
|
147
220
|
}
|
|
148
221
|
/**
|
|
149
222
|
* Internal worker configuration
|
|
@@ -163,25 +236,31 @@ interface FetchGuardRequestInit extends RequestInit {
|
|
|
163
236
|
includeHeaders?: boolean;
|
|
164
237
|
}
|
|
165
238
|
/**
|
|
166
|
-
*
|
|
239
|
+
* Fetch envelope - raw HTTP response from worker
|
|
240
|
+
*
|
|
241
|
+
* Worker only fetches and returns raw data, does NOT judge HTTP status.
|
|
242
|
+
* Client receives envelope and decides ok/err based on business logic.
|
|
243
|
+
*
|
|
244
|
+
* - status: HTTP status code (2xx, 3xx, 4xx, 5xx)
|
|
167
245
|
* - body: string (text/JSON) or base64 (binary)
|
|
168
246
|
* - contentType: always present, indicates how to decode body
|
|
169
247
|
* - headers: empty object if includeHeaders: false
|
|
170
|
-
* - status: HTTP status code
|
|
171
248
|
*/
|
|
172
|
-
interface
|
|
173
|
-
body: string;
|
|
249
|
+
interface FetchEnvelope {
|
|
174
250
|
status: number;
|
|
251
|
+
body: string;
|
|
175
252
|
contentType: string;
|
|
176
253
|
headers: Record<string, string>;
|
|
177
254
|
}
|
|
178
255
|
/**
|
|
179
256
|
* Serialized file data for transfer over postMessage
|
|
257
|
+
* Uses ArrayBuffer for zero-copy transfer via Transferable
|
|
180
258
|
*/
|
|
181
259
|
interface SerializedFile {
|
|
182
260
|
name: string;
|
|
183
261
|
type: string;
|
|
184
|
-
|
|
262
|
+
/** ArrayBuffer - transferred via postMessage Transferable for zero-copy */
|
|
263
|
+
buffer: ArrayBuffer;
|
|
185
264
|
}
|
|
186
265
|
/**
|
|
187
266
|
* Serialized FormData entry - can be string or file
|
|
@@ -194,6 +273,187 @@ interface SerializedFormData {
|
|
|
194
273
|
_type: 'FormData';
|
|
195
274
|
entries: Array<[string, SerializedFormDataEntry]>;
|
|
196
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Result of FormData serialization with transferables
|
|
278
|
+
* Used for zero-copy transfer via postMessage
|
|
279
|
+
*/
|
|
280
|
+
interface SerializedFormDataResult {
|
|
281
|
+
data: SerializedFormData;
|
|
282
|
+
/** ArrayBuffers to transfer - pass to postMessage as second argument */
|
|
283
|
+
transferables: ArrayBuffer[];
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Network error detail for transport failures
|
|
287
|
+
* Used when no HTTP response is received (connection failed, timeout, cancelled)
|
|
288
|
+
*/
|
|
289
|
+
interface NetworkErrorDetail {
|
|
290
|
+
code: 'NETWORK_ERROR' | 'REQUEST_CANCELLED' | 'RESPONSE_PARSE_FAILED';
|
|
291
|
+
message: string;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Reason for token refresh
|
|
295
|
+
*/
|
|
296
|
+
type RefreshReason = 'expired' | 'proactive' | 'manual';
|
|
297
|
+
/**
|
|
298
|
+
* Request timing metrics for performance monitoring
|
|
299
|
+
*
|
|
300
|
+
* All times are in milliseconds.
|
|
301
|
+
*/
|
|
302
|
+
interface RequestMetrics {
|
|
303
|
+
/** When request was initiated (Date.now()) */
|
|
304
|
+
startTime: number;
|
|
305
|
+
/** When response was received (Date.now()) */
|
|
306
|
+
endTime: number;
|
|
307
|
+
/** Total duration (endTime - startTime) */
|
|
308
|
+
duration: number;
|
|
309
|
+
/** Time spent waiting in queue before processing */
|
|
310
|
+
queueTime: number;
|
|
311
|
+
/** Time spent in IPC (postMessage round-trip overhead) */
|
|
312
|
+
ipcTime: number;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Debug hooks for observing FetchGuard operations
|
|
316
|
+
*
|
|
317
|
+
* All hooks are observe-only - they cannot modify requests/responses.
|
|
318
|
+
* Useful for logging, debugging, and monitoring.
|
|
319
|
+
*
|
|
320
|
+
* Note: Hooks run synchronously and should not perform heavy operations.
|
|
321
|
+
*/
|
|
322
|
+
interface DebugHooks {
|
|
323
|
+
/**
|
|
324
|
+
* Called before each request is sent to worker
|
|
325
|
+
* @param url - Request URL
|
|
326
|
+
* @param options - Request options (method, headers, etc.)
|
|
327
|
+
*/
|
|
328
|
+
onRequest?: (url: string, options: FetchGuardRequestInit) => void;
|
|
329
|
+
/**
|
|
330
|
+
* Called when response is received from worker
|
|
331
|
+
* @param url - Request URL
|
|
332
|
+
* @param envelope - Response envelope (status, body, headers)
|
|
333
|
+
* @param metrics - Request timing metrics (optional, for performance monitoring)
|
|
334
|
+
*/
|
|
335
|
+
onResponse?: (url: string, envelope: FetchEnvelope, metrics?: RequestMetrics) => void;
|
|
336
|
+
/**
|
|
337
|
+
* Called when token refresh occurs
|
|
338
|
+
* @param reason - Why refresh happened: 'expired', 'proactive', or 'manual'
|
|
339
|
+
*/
|
|
340
|
+
onRefresh?: (reason: RefreshReason) => void;
|
|
341
|
+
/**
|
|
342
|
+
* Called when transport error occurs (network failure, timeout, cancelled)
|
|
343
|
+
* @param url - Request URL
|
|
344
|
+
* @param error - Error detail with code and message
|
|
345
|
+
* @param metrics - Request timing metrics (optional)
|
|
346
|
+
*/
|
|
347
|
+
onError?: (url: string, error: NetworkErrorDetail, metrics?: RequestMetrics) => void;
|
|
348
|
+
/**
|
|
349
|
+
* Called when worker is ready after initialization
|
|
350
|
+
*/
|
|
351
|
+
onWorkerReady?: () => void;
|
|
352
|
+
/**
|
|
353
|
+
* Called when worker encounters a fatal error
|
|
354
|
+
* @param error - Error event from worker
|
|
355
|
+
*/
|
|
356
|
+
onWorkerError?: (error: ErrorEvent) => void;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Retry configuration for network errors
|
|
360
|
+
*
|
|
361
|
+
* Only retries on transport failures (network error, timeout).
|
|
362
|
+
* Does NOT retry on HTTP errors (4xx/5xx) - those are valid responses.
|
|
363
|
+
* Does NOT retry cancelled requests.
|
|
364
|
+
*/
|
|
365
|
+
interface RetryConfig {
|
|
366
|
+
/**
|
|
367
|
+
* Maximum number of retry attempts (default: 0 = no retry)
|
|
368
|
+
*/
|
|
369
|
+
maxAttempts?: number;
|
|
370
|
+
/**
|
|
371
|
+
* Delay between retries in milliseconds (default: 1000)
|
|
372
|
+
*/
|
|
373
|
+
delay?: number;
|
|
374
|
+
/**
|
|
375
|
+
* Exponential backoff multiplier (default: 1 = no backoff)
|
|
376
|
+
* Example: delay=1000, backoff=2 => 1s, 2s, 4s, 8s...
|
|
377
|
+
*/
|
|
378
|
+
backoff?: number;
|
|
379
|
+
/**
|
|
380
|
+
* Maximum delay in milliseconds (default: 30000)
|
|
381
|
+
* Caps the delay when using exponential backoff
|
|
382
|
+
*/
|
|
383
|
+
maxDelay?: number;
|
|
384
|
+
/**
|
|
385
|
+
* Jitter factor to add randomness to retry delays (default: 0 = no jitter)
|
|
386
|
+
* Range: 0 to 1 (e.g., 0.5 = ±50% randomness)
|
|
387
|
+
* Helps prevent thundering herd when many clients retry simultaneously.
|
|
388
|
+
*
|
|
389
|
+
* Note: Jitter is only applied when shouldRetry returns true.
|
|
390
|
+
* If request fails permanently, no jitter delay occurs.
|
|
391
|
+
*
|
|
392
|
+
* Example: delay=1000, jitter=0.5 => delay between 500ms and 1500ms
|
|
393
|
+
*/
|
|
394
|
+
jitter?: number;
|
|
395
|
+
/**
|
|
396
|
+
* Custom condition to determine if error should be retried
|
|
397
|
+
* Default: retry on NETWORK_ERROR only
|
|
398
|
+
* @param error - The error that occurred
|
|
399
|
+
* @returns true to retry, false to fail immediately
|
|
400
|
+
*/
|
|
401
|
+
shouldRetry?: (error: NetworkErrorDetail) => boolean;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Request deduplication configuration
|
|
405
|
+
*
|
|
406
|
+
* When enabled, identical GET requests (same URL) within a time window
|
|
407
|
+
* will share the same in-flight request instead of making duplicates.
|
|
408
|
+
*
|
|
409
|
+
* IMPORTANT:
|
|
410
|
+
* - Only applies to GET requests (POST/PUT/DELETE are never deduplicated)
|
|
411
|
+
* - Only deduplicates in-flight requests (not caching)
|
|
412
|
+
* - Safe for most read operations
|
|
413
|
+
*/
|
|
414
|
+
interface DedupeConfig {
|
|
415
|
+
/**
|
|
416
|
+
* Enable deduplication (default: false)
|
|
417
|
+
*/
|
|
418
|
+
enabled?: boolean;
|
|
419
|
+
/**
|
|
420
|
+
* Time window in milliseconds to consider requests as duplicates (default: 0)
|
|
421
|
+
* 0 = only dedupe concurrent/in-flight requests
|
|
422
|
+
* >0 = also dedupe requests within this time window after completion
|
|
423
|
+
*/
|
|
424
|
+
window?: number;
|
|
425
|
+
/**
|
|
426
|
+
* Custom key generator for deduplication
|
|
427
|
+
* Default: uses URL only for GET requests
|
|
428
|
+
* @param url - Request URL
|
|
429
|
+
* @param options - Request options
|
|
430
|
+
* @returns Key string, or null to skip deduplication for this request
|
|
431
|
+
*/
|
|
432
|
+
keyGenerator?: (url: string, options: FetchGuardRequestInit) => string | null;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Transport result - represents the outcome of a network request
|
|
436
|
+
*
|
|
437
|
+
* IMPORTANT: This is a TRANSPORT result, not a business result.
|
|
438
|
+
* - ok = HTTP response received (check envelope.status for 2xx/4xx/5xx)
|
|
439
|
+
* - err = Network failure (no response received)
|
|
440
|
+
*
|
|
441
|
+
* Example:
|
|
442
|
+
* ```typescript
|
|
443
|
+
* const result = await api.get('/users')
|
|
444
|
+
* if (result.ok) {
|
|
445
|
+
* // Transport succeeded - got HTTP response
|
|
446
|
+
* if (result.data.status >= 200 && result.data.status < 400) {
|
|
447
|
+
* // Business success
|
|
448
|
+
* } else {
|
|
449
|
+
* // Business error (4xx/5xx) - still has response body
|
|
450
|
+
* }
|
|
451
|
+
* } else {
|
|
452
|
+
* // Transport failed - no response (network error, timeout, cancelled)
|
|
453
|
+
* }
|
|
454
|
+
* ```
|
|
455
|
+
*/
|
|
456
|
+
type TransportResult = Result<FetchEnvelope>;
|
|
197
457
|
|
|
198
458
|
/**
|
|
199
459
|
* FetchGuard Client - main interface cho việc gọi API thông qua Web Worker
|
|
@@ -202,14 +462,28 @@ declare class FetchGuardClient {
|
|
|
202
462
|
private worker;
|
|
203
463
|
private messageId;
|
|
204
464
|
private pendingRequests;
|
|
465
|
+
/** Track request URLs for debug hooks */
|
|
466
|
+
private requestUrls;
|
|
467
|
+
/** Track request timing for metrics */
|
|
468
|
+
private requestTimings;
|
|
205
469
|
private authListeners;
|
|
206
470
|
private readyListeners;
|
|
207
471
|
private isReady;
|
|
208
472
|
private requestQueue;
|
|
209
|
-
private
|
|
210
|
-
private
|
|
473
|
+
private activeRequests;
|
|
474
|
+
private readonly maxConcurrent;
|
|
475
|
+
private readonly maxQueueSize;
|
|
476
|
+
private readonly setupTimeout;
|
|
477
|
+
private readonly requestTimeout;
|
|
211
478
|
private setupResolve?;
|
|
212
479
|
private setupReject?;
|
|
480
|
+
private readonly debug?;
|
|
481
|
+
private readonly retry?;
|
|
482
|
+
private readonly dedupe?;
|
|
483
|
+
/** In-flight requests for deduplication */
|
|
484
|
+
private readonly inFlightRequests;
|
|
485
|
+
/** Recent completed requests for time-window deduplication */
|
|
486
|
+
private readonly recentResults;
|
|
213
487
|
constructor(options: FetchGuardOptions);
|
|
214
488
|
/**
|
|
215
489
|
* Initialize worker with config and provider
|
|
@@ -228,9 +502,57 @@ declare class FetchGuardClient {
|
|
|
228
502
|
*/
|
|
229
503
|
private generateMessageId;
|
|
230
504
|
/**
|
|
231
|
-
* Make API request
|
|
505
|
+
* Make API request with optional deduplication, retry, and AbortSignal support
|
|
506
|
+
*
|
|
507
|
+
* @param url - Full URL to fetch
|
|
508
|
+
* @param options - Request options including optional AbortSignal
|
|
509
|
+
* @returns Result with FetchEnvelope on success, error on failure
|
|
510
|
+
*
|
|
511
|
+
* @example
|
|
512
|
+
* // With AbortSignal
|
|
513
|
+
* const controller = new AbortController()
|
|
514
|
+
* setTimeout(() => controller.abort(), 5000)
|
|
515
|
+
* const result = await api.fetch('/slow', { signal: controller.signal })
|
|
516
|
+
*/
|
|
517
|
+
fetch(url: string, options?: FetchGuardRequestInit): Promise<Result<FetchEnvelope>>;
|
|
518
|
+
/**
|
|
519
|
+
* Wrap a promise with AbortSignal support
|
|
232
520
|
*/
|
|
233
|
-
|
|
521
|
+
private wrapWithAbortSignal;
|
|
522
|
+
/**
|
|
523
|
+
* Fetch with retry logic and AbortSignal support (internal)
|
|
524
|
+
*/
|
|
525
|
+
private fetchWithRetryAndSignal;
|
|
526
|
+
/**
|
|
527
|
+
* Generate deduplication key for request
|
|
528
|
+
* Returns null if request should not be deduplicated
|
|
529
|
+
*/
|
|
530
|
+
private getDedupeKey;
|
|
531
|
+
/**
|
|
532
|
+
* Apply jitter to a delay value
|
|
533
|
+
* Jitter adds ±(jitter * delay) randomness to prevent thundering herd
|
|
534
|
+
* @param delay - Base delay in milliseconds
|
|
535
|
+
* @param jitter - Jitter factor (0-1)
|
|
536
|
+
* @returns Jittered delay
|
|
537
|
+
*/
|
|
538
|
+
private applyJitter;
|
|
539
|
+
/**
|
|
540
|
+
* Default retry condition - only retry on NETWORK_ERROR
|
|
541
|
+
*/
|
|
542
|
+
private defaultShouldRetry;
|
|
543
|
+
/**
|
|
544
|
+
* Calculate request metrics from timing data
|
|
545
|
+
*/
|
|
546
|
+
private calculateMetrics;
|
|
547
|
+
/**
|
|
548
|
+
* Sleep helper for retry delay
|
|
549
|
+
*/
|
|
550
|
+
private sleep;
|
|
551
|
+
/**
|
|
552
|
+
* Sleep with abort signal support
|
|
553
|
+
* Returns true if aborted, false if completed normally
|
|
554
|
+
*/
|
|
555
|
+
private sleepWithAbort;
|
|
234
556
|
/**
|
|
235
557
|
* Fetch with id for external cancellation
|
|
236
558
|
* Returns { id, result, cancel }
|
|
@@ -238,7 +560,7 @@ declare class FetchGuardClient {
|
|
|
238
560
|
*/
|
|
239
561
|
fetchWithId(url: string, options?: FetchGuardRequestInit): {
|
|
240
562
|
id: string;
|
|
241
|
-
result: Promise<Result<
|
|
563
|
+
result: Promise<Result<FetchEnvelope>>;
|
|
242
564
|
cancel: () => void;
|
|
243
565
|
};
|
|
244
566
|
/**
|
|
@@ -248,11 +570,11 @@ declare class FetchGuardClient {
|
|
|
248
570
|
/**
|
|
249
571
|
* Convenience methods
|
|
250
572
|
*/
|
|
251
|
-
get(url: string, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<
|
|
252
|
-
post(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<
|
|
253
|
-
put(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<
|
|
254
|
-
delete(url: string, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<
|
|
255
|
-
patch(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<
|
|
573
|
+
get(url: string, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<FetchEnvelope>>;
|
|
574
|
+
post(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<FetchEnvelope>>;
|
|
575
|
+
put(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<FetchEnvelope>>;
|
|
576
|
+
delete(url: string, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<FetchEnvelope>>;
|
|
577
|
+
patch(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<FetchEnvelope>>;
|
|
256
578
|
/**
|
|
257
579
|
* Generic method to call any auth method on provider
|
|
258
580
|
* @param method - Method name (login, logout, loginWithPhone, etc.)
|
|
@@ -279,6 +601,34 @@ declare class FetchGuardClient {
|
|
|
279
601
|
* @param emitEvent - Whether to emit AUTH_STATE_CHANGED event (default: true)
|
|
280
602
|
*/
|
|
281
603
|
refreshToken(emitEvent?: boolean): Promise<Result<AuthResult>>;
|
|
604
|
+
/**
|
|
605
|
+
* Exchange current token for a new one with different context
|
|
606
|
+
*
|
|
607
|
+
* Useful for:
|
|
608
|
+
* - Switching tenants in multi-tenant apps
|
|
609
|
+
* - Changing authorization scope
|
|
610
|
+
* - Impersonating users (admin feature)
|
|
611
|
+
*
|
|
612
|
+
* @param url - URL to call for token exchange
|
|
613
|
+
* @param options - Exchange options (method, payload)
|
|
614
|
+
* @param emitEvent - Whether to emit AUTH_STATE_CHANGED event (default: true)
|
|
615
|
+
*
|
|
616
|
+
* @example
|
|
617
|
+
* // Switch tenant
|
|
618
|
+
* await api.exchangeToken('https://auth.example.com/auth/select-tenant', {
|
|
619
|
+
* payload: { tenantId: 'tenant_123' }
|
|
620
|
+
* })
|
|
621
|
+
*
|
|
622
|
+
* // Change scope with PUT method
|
|
623
|
+
* await api.exchangeToken('https://auth.example.com/auth/switch-context', {
|
|
624
|
+
* method: 'PUT',
|
|
625
|
+
* payload: { scope: 'admin' }
|
|
626
|
+
* })
|
|
627
|
+
*/
|
|
628
|
+
exchangeToken(url: string, options?: {
|
|
629
|
+
method?: 'POST' | 'PUT';
|
|
630
|
+
payload?: Record<string, unknown>;
|
|
631
|
+
}, emitEvent?: boolean): Promise<Result<AuthResult>>;
|
|
282
632
|
/**
|
|
283
633
|
* Check if worker is ready
|
|
284
634
|
*/
|
|
@@ -304,16 +654,24 @@ declare class FetchGuardClient {
|
|
|
304
654
|
/**
|
|
305
655
|
* Send message through queue system
|
|
306
656
|
* All messages go through queue for sequential processing
|
|
657
|
+
* @param transferables - Optional Transferable objects for zero-copy postMessage
|
|
307
658
|
*/
|
|
308
659
|
private sendMessageQueued;
|
|
309
660
|
/**
|
|
310
|
-
* Process message queue
|
|
661
|
+
* Process message queue with concurrency limit
|
|
662
|
+
*
|
|
663
|
+
* Uses semaphore pattern to allow N concurrent requests.
|
|
311
664
|
* Benefits:
|
|
312
|
-
* -
|
|
665
|
+
* - Higher throughput than sequential processing
|
|
666
|
+
* - Backpressure via maxConcurrent limit
|
|
313
667
|
* - Better error isolation (one failure doesn't affect others)
|
|
314
|
-
* - 50ms delay between requests for backpressure
|
|
315
668
|
*/
|
|
316
669
|
private processQueue;
|
|
670
|
+
/**
|
|
671
|
+
* Called when a request completes (success or error)
|
|
672
|
+
* Decrements active count and processes next items in queue
|
|
673
|
+
*/
|
|
674
|
+
private onRequestComplete;
|
|
317
675
|
/**
|
|
318
676
|
* Cleanup - terminate worker
|
|
319
677
|
*/
|
|
@@ -387,11 +745,14 @@ interface WorkerPayloads {
|
|
|
387
745
|
};
|
|
388
746
|
AUTH_STATE_CHANGED: AuthResult;
|
|
389
747
|
AUTH_CALL_RESULT: AuthResult;
|
|
390
|
-
FETCH_RESULT:
|
|
748
|
+
FETCH_RESULT: FetchEnvelope;
|
|
391
749
|
FETCH_ERROR: {
|
|
392
750
|
error: string;
|
|
393
751
|
status?: number;
|
|
394
752
|
};
|
|
753
|
+
TOKEN_REFRESHED: {
|
|
754
|
+
reason: RefreshReason;
|
|
755
|
+
};
|
|
395
756
|
}
|
|
396
757
|
/**
|
|
397
758
|
* Generate message type from payload definition
|
|
@@ -447,6 +808,7 @@ declare const InitErrors: {
|
|
|
447
808
|
*/
|
|
448
809
|
declare const AuthErrors: {
|
|
449
810
|
readonly TokenRefreshFailed: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
|
|
811
|
+
readonly TokenExchangeFailed: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
|
|
450
812
|
readonly LoginFailed: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
|
|
451
813
|
readonly LogoutFailed: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
|
|
452
814
|
readonly NotAuthenticated: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
|
|
@@ -456,8 +818,8 @@ declare const AuthErrors: {
|
|
|
456
818
|
*/
|
|
457
819
|
declare const DomainErrors: {
|
|
458
820
|
readonly NotAllowed: (params?: ({
|
|
459
|
-
url:
|
|
460
|
-
} & Partial<
|
|
821
|
+
url: string | number;
|
|
822
|
+
} & Partial<ts_micro_result.BaseErrorParams>) | undefined) => ts_micro_result.ErrorDetail;
|
|
461
823
|
};
|
|
462
824
|
/**
|
|
463
825
|
* Request/Response errors (network, HTTP, parsing)
|
|
@@ -466,11 +828,57 @@ declare const RequestErrors: {
|
|
|
466
828
|
readonly NetworkError: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
|
|
467
829
|
readonly Cancelled: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
|
|
468
830
|
readonly HttpError: (params?: ({
|
|
469
|
-
status:
|
|
470
|
-
} & Partial<
|
|
831
|
+
status: string | number;
|
|
832
|
+
} & Partial<ts_micro_result.BaseErrorParams>) | undefined) => ts_micro_result.ErrorDetail;
|
|
471
833
|
readonly ResponseParseFailed: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
|
|
834
|
+
readonly QueueFull: (params?: ({
|
|
835
|
+
size: string | number;
|
|
836
|
+
maxSize: string | number;
|
|
837
|
+
} & Partial<ts_micro_result.BaseErrorParams>) | undefined) => ts_micro_result.ErrorDetail;
|
|
838
|
+
readonly Timeout: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
|
|
472
839
|
};
|
|
473
840
|
|
|
841
|
+
/**
|
|
842
|
+
* Error codes as constants for type-safe error matching
|
|
843
|
+
*
|
|
844
|
+
* Usage:
|
|
845
|
+
* ```typescript
|
|
846
|
+
* import { ERROR_CODES } from 'fetchguard'
|
|
847
|
+
*
|
|
848
|
+
* if (result.errors[0]?.code === ERROR_CODES.NETWORK_ERROR) {
|
|
849
|
+
* // Handle network error
|
|
850
|
+
* }
|
|
851
|
+
* ```
|
|
852
|
+
*/
|
|
853
|
+
declare const ERROR_CODES: {
|
|
854
|
+
readonly UNEXPECTED: "UNEXPECTED";
|
|
855
|
+
readonly UNKNOWN_MESSAGE: "UNKNOWN_MESSAGE";
|
|
856
|
+
readonly RESULT_PARSE_ERROR: "RESULT_PARSE_ERROR";
|
|
857
|
+
readonly INIT_ERROR: "INIT_ERROR";
|
|
858
|
+
readonly PROVIDER_INIT_FAILED: "PROVIDER_INIT_FAILED";
|
|
859
|
+
readonly INIT_FAILED: "INIT_FAILED";
|
|
860
|
+
readonly TOKEN_REFRESH_FAILED: "TOKEN_REFRESH_FAILED";
|
|
861
|
+
readonly TOKEN_EXCHANGE_FAILED: "TOKEN_EXCHANGE_FAILED";
|
|
862
|
+
readonly LOGIN_FAILED: "LOGIN_FAILED";
|
|
863
|
+
readonly LOGOUT_FAILED: "LOGOUT_FAILED";
|
|
864
|
+
readonly NOT_AUTHENTICATED: "NOT_AUTHENTICATED";
|
|
865
|
+
readonly DOMAIN_NOT_ALLOWED: "DOMAIN_NOT_ALLOWED";
|
|
866
|
+
readonly NETWORK_ERROR: "NETWORK_ERROR";
|
|
867
|
+
readonly REQUEST_CANCELLED: "REQUEST_CANCELLED";
|
|
868
|
+
readonly HTTP_ERROR: "HTTP_ERROR";
|
|
869
|
+
readonly RESPONSE_PARSE_FAILED: "RESPONSE_PARSE_FAILED";
|
|
870
|
+
readonly QUEUE_FULL: "QUEUE_FULL";
|
|
871
|
+
readonly REQUEST_TIMEOUT: "REQUEST_TIMEOUT";
|
|
872
|
+
};
|
|
873
|
+
/**
|
|
874
|
+
* Union type of all error code values
|
|
875
|
+
*/
|
|
876
|
+
type ErrorCode = typeof ERROR_CODES[keyof typeof ERROR_CODES];
|
|
877
|
+
/**
|
|
878
|
+
* Union type of all error code keys (useful for telemetry mapping)
|
|
879
|
+
*/
|
|
880
|
+
type ErrorCodeKey = keyof typeof ERROR_CODES;
|
|
881
|
+
|
|
474
882
|
/**
|
|
475
883
|
* Register a token provider with name
|
|
476
884
|
*/
|
|
@@ -531,12 +939,30 @@ interface ProviderConfig {
|
|
|
531
939
|
*/
|
|
532
940
|
declare function createProvider(config: ProviderConfig): TokenProvider;
|
|
533
941
|
|
|
942
|
+
/**
|
|
943
|
+
* IndexedDB storage options
|
|
944
|
+
*/
|
|
945
|
+
interface IndexedDBStorageOptions {
|
|
946
|
+
/** Database name (default: 'FetchGuardDB') */
|
|
947
|
+
dbName?: string;
|
|
948
|
+
/** Key for refresh token (default: 'refreshToken') */
|
|
949
|
+
refreshTokenKey?: string;
|
|
950
|
+
/**
|
|
951
|
+
* Error callback for debugging storage failures
|
|
952
|
+
* Called when IndexedDB operations fail (quota exceeded, permission denied, etc.)
|
|
953
|
+
* Storage still fails closed (returns null), but this allows logging/debugging.
|
|
954
|
+
*/
|
|
955
|
+
onError?: StorageErrorCallback;
|
|
956
|
+
}
|
|
534
957
|
/**
|
|
535
958
|
* IndexedDB storage - only stores refresh token in IndexedDB
|
|
536
959
|
* Suitable for body-based refresh strategy
|
|
537
960
|
* Persists refresh token for reuse after reload
|
|
961
|
+
*
|
|
962
|
+
* @param options - Storage options or legacy dbName string
|
|
963
|
+
* @param legacyRefreshTokenKey - Legacy refreshTokenKey (for backward compatibility)
|
|
538
964
|
*/
|
|
539
|
-
declare function createIndexedDBStorage(
|
|
965
|
+
declare function createIndexedDBStorage(options?: IndexedDBStorageOptions | string, legacyRefreshTokenKey?: string): RefreshTokenStorage;
|
|
540
966
|
|
|
541
967
|
/**
|
|
542
968
|
* Body parser - parse token from response body (JSON)
|
|
@@ -638,15 +1064,18 @@ declare function createBodyProvider(config: {
|
|
|
638
1064
|
|
|
639
1065
|
/**
|
|
640
1066
|
* Serialize FormData for transfer over postMessage
|
|
641
|
-
* Inspired by api-worker.js:484-518
|
|
642
1067
|
*
|
|
643
|
-
* FormData cannot be cloned via postMessage, so we need to serialize it first
|
|
644
|
-
* Files are converted to ArrayBuffer
|
|
1068
|
+
* FormData cannot be cloned via postMessage, so we need to serialize it first.
|
|
1069
|
+
* Files are converted to ArrayBuffer and returned as transferables for zero-copy transfer.
|
|
1070
|
+
*
|
|
1071
|
+
* IMPORTANT: Preserves original field order by using single-pass iteration.
|
|
1072
|
+
*
|
|
1073
|
+
* @returns SerializedFormDataResult with data and transferables array
|
|
645
1074
|
*/
|
|
646
|
-
declare function serializeFormData(formData: FormData): Promise<
|
|
1075
|
+
declare function serializeFormData(formData: FormData): Promise<SerializedFormDataResult>;
|
|
647
1076
|
/**
|
|
648
1077
|
* Deserialize SerializedFormData back to FormData in worker
|
|
649
|
-
* Reconstructs File objects from
|
|
1078
|
+
* Reconstructs File objects from transferred ArrayBuffers
|
|
650
1079
|
*/
|
|
651
1080
|
declare function deserializeFormData(serialized: SerializedFormData): FormData;
|
|
652
1081
|
/**
|
|
@@ -669,4 +1098,138 @@ declare function base64ToArrayBuffer(base64: string): ArrayBuffer;
|
|
|
669
1098
|
*/
|
|
670
1099
|
declare function isBinaryContentType(contentType: string): boolean;
|
|
671
1100
|
|
|
672
|
-
|
|
1101
|
+
/**
|
|
1102
|
+
* Helper functions for common Result patterns
|
|
1103
|
+
*
|
|
1104
|
+
* These utilities simplify working with FetchGuard's Result-based API
|
|
1105
|
+
* by providing type-safe helpers for common operations.
|
|
1106
|
+
*/
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Check if result is a network/transport error (not an HTTP response)
|
|
1110
|
+
*
|
|
1111
|
+
* @example
|
|
1112
|
+
* const result = await api.fetch('/users')
|
|
1113
|
+
* if (isNetworkError(result)) {
|
|
1114
|
+
* console.error('Network failed:', result.errors[0]?.message)
|
|
1115
|
+
* }
|
|
1116
|
+
*/
|
|
1117
|
+
declare function isNetworkError(result: Result<FetchEnvelope>): result is Result<FetchEnvelope> & {
|
|
1118
|
+
ok: false;
|
|
1119
|
+
};
|
|
1120
|
+
/**
|
|
1121
|
+
* Check if response is successful (2xx status)
|
|
1122
|
+
*
|
|
1123
|
+
* @example
|
|
1124
|
+
* if (isSuccess(result)) {
|
|
1125
|
+
* const data = parseJson(result)
|
|
1126
|
+
* }
|
|
1127
|
+
*/
|
|
1128
|
+
declare function isSuccess(result: Result<FetchEnvelope>): boolean;
|
|
1129
|
+
/**
|
|
1130
|
+
* Check if response is a client error (4xx status)
|
|
1131
|
+
*
|
|
1132
|
+
* @example
|
|
1133
|
+
* if (isClientError(result)) {
|
|
1134
|
+
* console.error('Bad request:', getErrorMessage(result))
|
|
1135
|
+
* }
|
|
1136
|
+
*/
|
|
1137
|
+
declare function isClientError(result: Result<FetchEnvelope>): boolean;
|
|
1138
|
+
/**
|
|
1139
|
+
* Check if response is a server error (5xx status)
|
|
1140
|
+
*
|
|
1141
|
+
* @example
|
|
1142
|
+
* if (isServerError(result)) {
|
|
1143
|
+
* // Maybe retry?
|
|
1144
|
+
* }
|
|
1145
|
+
*/
|
|
1146
|
+
declare function isServerError(result: Result<FetchEnvelope>): boolean;
|
|
1147
|
+
/**
|
|
1148
|
+
* Parse JSON body safely with optional type inference
|
|
1149
|
+
*
|
|
1150
|
+
* Returns null if:
|
|
1151
|
+
* - Result is a network error (no response)
|
|
1152
|
+
* - Body is not valid JSON
|
|
1153
|
+
*
|
|
1154
|
+
* @example
|
|
1155
|
+
* const users = parseJson<User[]>(result)
|
|
1156
|
+
* if (users) {
|
|
1157
|
+
* // Use users array
|
|
1158
|
+
* }
|
|
1159
|
+
*/
|
|
1160
|
+
declare function parseJson<T = unknown>(result: Result<FetchEnvelope>): T | null;
|
|
1161
|
+
/**
|
|
1162
|
+
* Get human-readable error message from result
|
|
1163
|
+
*
|
|
1164
|
+
* For network errors: returns the error message
|
|
1165
|
+
* For HTTP errors: tries to parse message from body, falls back to status code
|
|
1166
|
+
*
|
|
1167
|
+
* @example
|
|
1168
|
+
* if (!isSuccess(result)) {
|
|
1169
|
+
* toast.error(getErrorMessage(result))
|
|
1170
|
+
* }
|
|
1171
|
+
*/
|
|
1172
|
+
declare function getErrorMessage(result: Result<FetchEnvelope>): string;
|
|
1173
|
+
/**
|
|
1174
|
+
* Get error body with type safety (best-effort parsing)
|
|
1175
|
+
*
|
|
1176
|
+
* NOTE: This is best-effort parsing. The error body comes from the server
|
|
1177
|
+
* and may not match the expected shape. Always handle null return and
|
|
1178
|
+
* validate before using typed properties.
|
|
1179
|
+
*
|
|
1180
|
+
* @example
|
|
1181
|
+
* interface ApiError {
|
|
1182
|
+
* code: string
|
|
1183
|
+
* message: string
|
|
1184
|
+
* errors?: { field: string; message: string }[]
|
|
1185
|
+
* }
|
|
1186
|
+
*
|
|
1187
|
+
* const errorBody = getErrorBody<ApiError>(result)
|
|
1188
|
+
* if (errorBody?.errors) {
|
|
1189
|
+
* errorBody.errors.forEach(e => console.error(`${e.field}: ${e.message}`))
|
|
1190
|
+
* }
|
|
1191
|
+
*/
|
|
1192
|
+
declare function getErrorBody<T = unknown>(result: Result<FetchEnvelope>): T | null;
|
|
1193
|
+
/**
|
|
1194
|
+
* Get the HTTP status code from result
|
|
1195
|
+
*
|
|
1196
|
+
* Returns null if result is a network error (no HTTP response)
|
|
1197
|
+
*
|
|
1198
|
+
* @example
|
|
1199
|
+
* const status = getStatus(result)
|
|
1200
|
+
* if (status === 401) {
|
|
1201
|
+
* // Redirect to login
|
|
1202
|
+
* }
|
|
1203
|
+
*/
|
|
1204
|
+
declare function getStatus(result: Result<FetchEnvelope>): number | null;
|
|
1205
|
+
/**
|
|
1206
|
+
* Check if result has a specific HTTP status
|
|
1207
|
+
*
|
|
1208
|
+
* @example
|
|
1209
|
+
* if (hasStatus(result, 404)) {
|
|
1210
|
+
* console.log('Not found')
|
|
1211
|
+
* }
|
|
1212
|
+
*/
|
|
1213
|
+
declare function hasStatus(result: Result<FetchEnvelope>, status: number): boolean;
|
|
1214
|
+
/**
|
|
1215
|
+
* Match result against multiple handlers
|
|
1216
|
+
*
|
|
1217
|
+
* @example
|
|
1218
|
+
* matchResult(result, {
|
|
1219
|
+
* success: (data) => console.log('Success:', data),
|
|
1220
|
+
* clientError: (data) => console.error('Client error:', data.status),
|
|
1221
|
+
* serverError: (data) => console.error('Server error:', data.status),
|
|
1222
|
+
* networkError: (errors) => console.error('Network:', errors[0]?.message)
|
|
1223
|
+
* })
|
|
1224
|
+
*/
|
|
1225
|
+
declare function matchResult<T>(result: Result<FetchEnvelope>, handlers: {
|
|
1226
|
+
success?: (data: FetchEnvelope) => T;
|
|
1227
|
+
clientError?: (data: FetchEnvelope) => T;
|
|
1228
|
+
serverError?: (data: FetchEnvelope) => T;
|
|
1229
|
+
networkError?: (errors: readonly {
|
|
1230
|
+
code: string;
|
|
1231
|
+
message: string;
|
|
1232
|
+
}[]) => T;
|
|
1233
|
+
}): T | undefined;
|
|
1234
|
+
|
|
1235
|
+
export { AuthErrors, type AuthResult, type AuthStrategy, type DebugHooks, type DedupeConfig, DomainErrors, ERROR_CODES, type ErrorCode, type ErrorCodeKey, type ExchangeTokenOptions, type FetchEnvelope, FetchGuardClient, type FetchGuardOptions, type FetchGuardRequestInit, GeneralErrors, type IndexedDBStorageOptions, InitErrors, MSG, type MainToWorkerMessage, type MessageType, type NetworkErrorDetail, type ProviderConfig, type ProviderPresetConfig, type RefreshReason, type RefreshTokenStorage, RequestErrors, type RequestMetrics, type RetryConfig, type SerializedFile, type SerializedFormData, type SerializedFormDataEntry, type StorageErrorCallback, type StorageErrorContext, type TokenInfo, type TokenParser, type TokenProvider, type TransportResult, type WorkerConfig, type WorkerToMainMessage, base64ToArrayBuffer, bodyParser, bodyStrategy, clearProviders, cookieParser, cookieStrategy, createBodyProvider, createBodyStrategy, createClient, createCookieProvider, createCookieStrategy, createIndexedDBStorage, createProvider, deserializeFormData, getErrorBody, getErrorMessage, getProvider, getStatus, hasProvider, hasStatus, isBinaryContentType, isClientError, isFormData, isNetworkError, isSerializedFormData, isServerError, isSuccess, listProviders, matchResult, parseJson, registerProvider, serializeFormData, unregisterProvider };
|