fetchguard 2.1.2 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -23
- package/dist/index.d.ts +3 -617
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/worker-DBL8XAZJ.d.ts +643 -0
- package/dist/worker.d.ts +2 -2
- package/dist/worker.js +34 -0
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
import { Result } from 'ts-micro-result';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FetchGuard Business Types
|
|
5
|
+
*
|
|
6
|
+
* Contains domain logic types: Provider, Config, API Response, etc.
|
|
7
|
+
* For message protocol types, see messages.ts
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Token info returned from provider
|
|
11
|
+
*
|
|
12
|
+
* All fields are optional to support various custom auth methods:
|
|
13
|
+
* - Standard login/refresh: returns token + optional fields
|
|
14
|
+
* - Update user info: may only return user (no token update)
|
|
15
|
+
* - Verify OTP: may return nothing (just validation)
|
|
16
|
+
* - Custom auth flows: flexible field combinations
|
|
17
|
+
*/
|
|
18
|
+
interface TokenInfo {
|
|
19
|
+
token?: string | null;
|
|
20
|
+
expiresAt?: number | null;
|
|
21
|
+
refreshToken?: string | null;
|
|
22
|
+
user?: unknown;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Options for token exchange operation
|
|
26
|
+
*
|
|
27
|
+
* Used when switching tenant, changing scope, or any operation
|
|
28
|
+
* that exchanges current token for a new one with different claims
|
|
29
|
+
*/
|
|
30
|
+
interface ExchangeTokenOptions {
|
|
31
|
+
/** HTTP method to use. Default: 'POST' */
|
|
32
|
+
method?: 'POST' | 'PUT';
|
|
33
|
+
/** Payload to send with the request (e.g., tenantId, scope) */
|
|
34
|
+
payload?: Record<string, unknown>;
|
|
35
|
+
/** Custom headers for this request. Overrides defaultHeaders if same key. */
|
|
36
|
+
headers?: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Auth result returned from auth operations and auth state changes
|
|
40
|
+
* Used by: login(), logout(), refreshToken(), onAuthStateChanged()
|
|
41
|
+
*/
|
|
42
|
+
interface AuthResult {
|
|
43
|
+
/** Whether user is authenticated (has valid non-expired token) */
|
|
44
|
+
authenticated: boolean;
|
|
45
|
+
/** User info from token (if available) */
|
|
46
|
+
user?: unknown;
|
|
47
|
+
/** Token expiry timestamp in milliseconds (if available) */
|
|
48
|
+
expiresAt?: number | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Interface for token provider
|
|
52
|
+
*
|
|
53
|
+
* Provider has 3 required methods:
|
|
54
|
+
* - refreshToken: Refresh access token when expired
|
|
55
|
+
* - login: Login with credentials
|
|
56
|
+
* - logout: Logout (clear tokens)
|
|
57
|
+
*
|
|
58
|
+
* User can add custom auth methods (loginWithPhone, loginWithGoogle, etc.)
|
|
59
|
+
* All custom methods must return Result<TokenInfo> for token retrieval
|
|
60
|
+
*/
|
|
61
|
+
interface TokenProvider {
|
|
62
|
+
/**
|
|
63
|
+
* Refresh tokens (required)
|
|
64
|
+
* @param refreshToken - Current refresh token (from worker memory, null if not available)
|
|
65
|
+
* @returns Result<TokenInfo> with new tokens
|
|
66
|
+
*/
|
|
67
|
+
refreshToken(refreshToken: string | null): Promise<Result<TokenInfo>>;
|
|
68
|
+
/**
|
|
69
|
+
* Login with credentials (required)
|
|
70
|
+
* @param payload - Login credentials (email/password, etc.)
|
|
71
|
+
* @param url - Optional URL override (if not provided, uses configured loginUrl)
|
|
72
|
+
* @returns Result<TokenInfo> with tokens
|
|
73
|
+
*/
|
|
74
|
+
login(payload: unknown, url?: string): Promise<Result<TokenInfo>>;
|
|
75
|
+
/**
|
|
76
|
+
* Logout - clear tokens (required)
|
|
77
|
+
* @param payload - Optional logout payload
|
|
78
|
+
* @returns Result<TokenInfo> with all fields reset (token = '', refreshToken = undefined, user = undefined)
|
|
79
|
+
*/
|
|
80
|
+
logout(payload?: unknown): Promise<Result<TokenInfo>>;
|
|
81
|
+
/**
|
|
82
|
+
* Exchange current token for a new one with different context
|
|
83
|
+
*
|
|
84
|
+
* Useful for switching tenants, changing scopes, or any operation
|
|
85
|
+
* that requires exchanging the current token for a new one.
|
|
86
|
+
*
|
|
87
|
+
* @param accessToken - Current access token (injected by worker)
|
|
88
|
+
* @param url - URL to call for token exchange
|
|
89
|
+
* @param options - Exchange options (method, payload)
|
|
90
|
+
* @returns Result<TokenInfo> with new tokens
|
|
91
|
+
*/
|
|
92
|
+
exchangeToken(accessToken: string, url: string, options?: ExchangeTokenOptions): Promise<Result<TokenInfo>>;
|
|
93
|
+
/**
|
|
94
|
+
* Custom auth methods (optional)
|
|
95
|
+
* Examples: loginWithPhone, loginWithGoogle, loginWithFacebook, etc.
|
|
96
|
+
* All must return Result<TokenInfo> for token retrieval
|
|
97
|
+
*
|
|
98
|
+
* Note: Using any[] for args to allow flexible custom auth methods
|
|
99
|
+
* while maintaining type compatibility with specific method signatures above
|
|
100
|
+
*/
|
|
101
|
+
[key: string]: (...args: any[]) => Promise<Result<TokenInfo>>;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Storage error context for debugging
|
|
105
|
+
*/
|
|
106
|
+
type StorageErrorContext = 'get' | 'set' | 'delete' | 'open';
|
|
107
|
+
/**
|
|
108
|
+
* Storage error callback type
|
|
109
|
+
* Called when IndexedDB operations fail (quota exceeded, permission denied, etc.)
|
|
110
|
+
* Storage still fails closed (returns null), but this allows logging/debugging.
|
|
111
|
+
*/
|
|
112
|
+
type StorageErrorCallback = (error: Error, context: StorageErrorContext) => void;
|
|
113
|
+
/**
|
|
114
|
+
* Interface for refresh token storage - only stores refresh token
|
|
115
|
+
*
|
|
116
|
+
* Access token is always stored in worker memory.
|
|
117
|
+
* Refresh token storage is OPTIONAL:
|
|
118
|
+
* - If available (IndexedDB): persist refresh token for reuse after reload
|
|
119
|
+
* - If not (undefined): cookie-based auth (httpOnly cookie)
|
|
120
|
+
*/
|
|
121
|
+
interface RefreshTokenStorage {
|
|
122
|
+
get(): Promise<string | null>;
|
|
123
|
+
set(token: string | null): Promise<void>;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Interface for token parser - parse token from backend response
|
|
127
|
+
* Parser returns complete TokenInfo (including user data)
|
|
128
|
+
*/
|
|
129
|
+
interface TokenParser {
|
|
130
|
+
parse(response: Response): Promise<TokenInfo>;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Interface for auth strategy - defines how to call auth APIs
|
|
134
|
+
*
|
|
135
|
+
* Strategy focuses only on API calls, returns Response
|
|
136
|
+
* Provider handles parsing and storage
|
|
137
|
+
*
|
|
138
|
+
* All methods are required
|
|
139
|
+
*/
|
|
140
|
+
interface AuthStrategy {
|
|
141
|
+
/** Refresh access token */
|
|
142
|
+
refresh(refreshToken: string | null): Promise<Response>;
|
|
143
|
+
/**
|
|
144
|
+
* Login with credentials
|
|
145
|
+
* @param payload - Login credentials
|
|
146
|
+
* @param url - Optional URL override (if not provided, uses configured loginUrl)
|
|
147
|
+
*/
|
|
148
|
+
login(payload: unknown, url?: string): Promise<Response>;
|
|
149
|
+
/** Logout */
|
|
150
|
+
logout(payload?: unknown): Promise<Response>;
|
|
151
|
+
/**
|
|
152
|
+
* Exchange current token for a new one with different context
|
|
153
|
+
* @param accessToken - Current access token
|
|
154
|
+
* @param url - URL to call for token exchange
|
|
155
|
+
* @param options - Exchange options (method, payload)
|
|
156
|
+
*/
|
|
157
|
+
exchangeToken(accessToken: string, url: string, options?: ExchangeTokenOptions): Promise<Response>;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Provider preset configuration for built-in auth strategies
|
|
161
|
+
*/
|
|
162
|
+
interface ProviderPresetConfig {
|
|
163
|
+
type: 'cookie-auth' | 'body-auth';
|
|
164
|
+
refreshUrl: string;
|
|
165
|
+
loginUrl: string;
|
|
166
|
+
logoutUrl: string;
|
|
167
|
+
refreshTokenKey?: string;
|
|
168
|
+
/** Custom headers to include in all auth requests (login, logout, refresh) */
|
|
169
|
+
headers?: Record<string, string>;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Configuration for FetchGuard client
|
|
173
|
+
*/
|
|
174
|
+
interface FetchGuardOptions {
|
|
175
|
+
/**
|
|
176
|
+
* Token provider - 3 options:
|
|
177
|
+
* 1. TokenProvider instance (for custom providers)
|
|
178
|
+
* 2. ProviderPresetConfig object (for built-in presets)
|
|
179
|
+
* 3. string (for registry lookup - advanced usage)
|
|
180
|
+
*/
|
|
181
|
+
provider: TokenProvider | ProviderPresetConfig | string;
|
|
182
|
+
/** List of allowed domains (wildcard supported) */
|
|
183
|
+
allowedDomains?: string[];
|
|
184
|
+
/** Early refresh time for tokens (ms) */
|
|
185
|
+
refreshEarlyMs?: number;
|
|
186
|
+
/** Default headers to include in all requests */
|
|
187
|
+
defaultHeaders?: Record<string, string>;
|
|
188
|
+
/**
|
|
189
|
+
* Maximum concurrent requests to worker (default: 6)
|
|
190
|
+
* Controls how many requests can be in-flight simultaneously.
|
|
191
|
+
* Set to 1 for strictly sequential processing.
|
|
192
|
+
* Higher values increase throughput but may cause worker congestion.
|
|
193
|
+
*/
|
|
194
|
+
maxConcurrent?: number;
|
|
195
|
+
/**
|
|
196
|
+
* Maximum queue size for pending requests (default: 1000)
|
|
197
|
+
* When queue is full, new requests will immediately fail with QUEUE_FULL error.
|
|
198
|
+
* Prevents memory leak if worker is unresponsive.
|
|
199
|
+
*/
|
|
200
|
+
maxQueueSize?: number;
|
|
201
|
+
/**
|
|
202
|
+
* Worker setup timeout in milliseconds (default: 10000)
|
|
203
|
+
* How long to wait for worker to be ready before failing.
|
|
204
|
+
*/
|
|
205
|
+
setupTimeout?: number;
|
|
206
|
+
/**
|
|
207
|
+
* Default request timeout in milliseconds (default: 30000)
|
|
208
|
+
* How long to wait for a request to complete before timing out.
|
|
209
|
+
* Can be overridden per-request via fetch options.
|
|
210
|
+
*/
|
|
211
|
+
requestTimeout?: number;
|
|
212
|
+
/**
|
|
213
|
+
* Debug hooks for observing operations (logging, monitoring)
|
|
214
|
+
* All hooks are observe-only - they cannot modify requests/responses.
|
|
215
|
+
*/
|
|
216
|
+
debug?: DebugHooks;
|
|
217
|
+
/**
|
|
218
|
+
* Retry configuration for network errors
|
|
219
|
+
* Only retries on transport failures, NOT on HTTP errors (4xx/5xx)
|
|
220
|
+
*/
|
|
221
|
+
retry?: RetryConfig;
|
|
222
|
+
/**
|
|
223
|
+
* Request deduplication configuration
|
|
224
|
+
* When enabled, duplicate requests to the same URL within a time window
|
|
225
|
+
* will share the same response instead of making multiple requests.
|
|
226
|
+
*/
|
|
227
|
+
dedupe?: DedupeConfig;
|
|
228
|
+
/**
|
|
229
|
+
* Custom worker factory function
|
|
230
|
+
*
|
|
231
|
+
* Use this when you need a custom provider with parser/strategy functions.
|
|
232
|
+
* Create a custom worker file that imports 'fetchguard/worker' and registers
|
|
233
|
+
* your provider, then pass a factory function that creates that worker.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```ts
|
|
237
|
+
* // my-worker.ts
|
|
238
|
+
* import 'fetchguard/worker'
|
|
239
|
+
* import { registerProvider, createProvider, ... } from 'fetchguard'
|
|
240
|
+
* const myProvider = createProvider({ parser: myParser, strategy: myStrategy, ... })
|
|
241
|
+
* registerProvider('my-auth', myProvider)
|
|
242
|
+
*
|
|
243
|
+
* // main.ts
|
|
244
|
+
* import MyWorker from './my-worker?worker'
|
|
245
|
+
* const api = createClient({
|
|
246
|
+
* provider: 'my-auth',
|
|
247
|
+
* workerFactory: () => new MyWorker()
|
|
248
|
+
* })
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
workerFactory?: () => Worker;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Internal worker configuration
|
|
255
|
+
*/
|
|
256
|
+
interface WorkerConfig {
|
|
257
|
+
allowedDomains: string[];
|
|
258
|
+
refreshEarlyMs: number;
|
|
259
|
+
defaultHeaders: Record<string, string>;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Extended RequestInit with FetchGuard-specific options
|
|
263
|
+
*/
|
|
264
|
+
interface FetchGuardRequestInit extends RequestInit {
|
|
265
|
+
/** Whether this request requires authentication. Default: true */
|
|
266
|
+
requiresAuth?: boolean;
|
|
267
|
+
/** Include response headers in result metadata and FETCH_RESULT payload */
|
|
268
|
+
includeHeaders?: boolean;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Fetch envelope - raw HTTP response from worker
|
|
272
|
+
*
|
|
273
|
+
* Worker only fetches and returns raw data, does NOT judge HTTP status.
|
|
274
|
+
* Client receives envelope and decides ok/err based on business logic.
|
|
275
|
+
*
|
|
276
|
+
* - status: HTTP status code (2xx, 3xx, 4xx, 5xx)
|
|
277
|
+
* - body: string (text/JSON) or base64 (binary)
|
|
278
|
+
* - contentType: always present, indicates how to decode body
|
|
279
|
+
* - headers: empty object if includeHeaders: false
|
|
280
|
+
*/
|
|
281
|
+
interface FetchEnvelope {
|
|
282
|
+
status: number;
|
|
283
|
+
body: string;
|
|
284
|
+
contentType: string;
|
|
285
|
+
headers: Record<string, string>;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Serialized file data for transfer over postMessage
|
|
289
|
+
* Uses ArrayBuffer for zero-copy transfer via Transferable
|
|
290
|
+
*/
|
|
291
|
+
interface SerializedFile {
|
|
292
|
+
name: string;
|
|
293
|
+
type: string;
|
|
294
|
+
/** ArrayBuffer - transferred via postMessage Transferable for zero-copy */
|
|
295
|
+
buffer: ArrayBuffer;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Serialized FormData entry - can be string or file
|
|
299
|
+
*/
|
|
300
|
+
type SerializedFormDataEntry = string | SerializedFile;
|
|
301
|
+
/**
|
|
302
|
+
* Serialized FormData for transfer over postMessage
|
|
303
|
+
*/
|
|
304
|
+
interface SerializedFormData {
|
|
305
|
+
_type: 'FormData';
|
|
306
|
+
entries: Array<[string, SerializedFormDataEntry]>;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Result of FormData serialization with transferables
|
|
310
|
+
* Used for zero-copy transfer via postMessage
|
|
311
|
+
*/
|
|
312
|
+
interface SerializedFormDataResult {
|
|
313
|
+
data: SerializedFormData;
|
|
314
|
+
/** ArrayBuffers to transfer - pass to postMessage as second argument */
|
|
315
|
+
transferables: ArrayBuffer[];
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Network error detail for transport failures
|
|
319
|
+
* Used when no HTTP response is received (connection failed, timeout, cancelled)
|
|
320
|
+
*/
|
|
321
|
+
interface NetworkErrorDetail {
|
|
322
|
+
code: 'NETWORK_ERROR' | 'REQUEST_CANCELLED' | 'RESPONSE_PARSE_FAILED';
|
|
323
|
+
message: string;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Reason for token refresh
|
|
327
|
+
*/
|
|
328
|
+
type RefreshReason = 'expired' | 'proactive' | 'manual';
|
|
329
|
+
/**
|
|
330
|
+
* Request timing metrics for performance monitoring
|
|
331
|
+
*
|
|
332
|
+
* All times are in milliseconds.
|
|
333
|
+
*/
|
|
334
|
+
interface RequestMetrics {
|
|
335
|
+
/** When request was initiated (Date.now()) */
|
|
336
|
+
startTime: number;
|
|
337
|
+
/** When response was received (Date.now()) */
|
|
338
|
+
endTime: number;
|
|
339
|
+
/** Total duration (endTime - startTime) */
|
|
340
|
+
duration: number;
|
|
341
|
+
/** Time spent waiting in queue before processing */
|
|
342
|
+
queueTime: number;
|
|
343
|
+
/** Time spent in IPC (postMessage round-trip overhead) */
|
|
344
|
+
ipcTime: number;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Debug hooks for observing FetchGuard operations
|
|
348
|
+
*
|
|
349
|
+
* All hooks are observe-only - they cannot modify requests/responses.
|
|
350
|
+
* Useful for logging, debugging, and monitoring.
|
|
351
|
+
*
|
|
352
|
+
* Note: Hooks run synchronously and should not perform heavy operations.
|
|
353
|
+
*/
|
|
354
|
+
interface DebugHooks {
|
|
355
|
+
/**
|
|
356
|
+
* Called before each request is sent to worker
|
|
357
|
+
* @param url - Request URL
|
|
358
|
+
* @param options - Request options (method, headers, etc.)
|
|
359
|
+
*/
|
|
360
|
+
onRequest?: (url: string, options: FetchGuardRequestInit) => void;
|
|
361
|
+
/**
|
|
362
|
+
* Called when response is received from worker
|
|
363
|
+
* @param url - Request URL
|
|
364
|
+
* @param envelope - Response envelope (status, body, headers)
|
|
365
|
+
* @param metrics - Request timing metrics (optional, for performance monitoring)
|
|
366
|
+
*/
|
|
367
|
+
onResponse?: (url: string, envelope: FetchEnvelope, metrics?: RequestMetrics) => void;
|
|
368
|
+
/**
|
|
369
|
+
* Called when token refresh occurs
|
|
370
|
+
* @param reason - Why refresh happened: 'expired', 'proactive', or 'manual'
|
|
371
|
+
*/
|
|
372
|
+
onRefresh?: (reason: RefreshReason) => void;
|
|
373
|
+
/**
|
|
374
|
+
* Called when transport error occurs (network failure, timeout, cancelled)
|
|
375
|
+
* @param url - Request URL
|
|
376
|
+
* @param error - Error detail with code and message
|
|
377
|
+
* @param metrics - Request timing metrics (optional)
|
|
378
|
+
*/
|
|
379
|
+
onError?: (url: string, error: NetworkErrorDetail, metrics?: RequestMetrics) => void;
|
|
380
|
+
/**
|
|
381
|
+
* Called when worker is ready after initialization
|
|
382
|
+
*/
|
|
383
|
+
onWorkerReady?: () => void;
|
|
384
|
+
/**
|
|
385
|
+
* Called when worker encounters a fatal error
|
|
386
|
+
* @param error - Error event from worker
|
|
387
|
+
*/
|
|
388
|
+
onWorkerError?: (error: ErrorEvent) => void;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Retry configuration for network errors
|
|
392
|
+
*
|
|
393
|
+
* Only retries on transport failures (network error, timeout).
|
|
394
|
+
* Does NOT retry on HTTP errors (4xx/5xx) - those are valid responses.
|
|
395
|
+
* Does NOT retry cancelled requests.
|
|
396
|
+
*/
|
|
397
|
+
interface RetryConfig {
|
|
398
|
+
/**
|
|
399
|
+
* Maximum number of retry attempts (default: 0 = no retry)
|
|
400
|
+
*/
|
|
401
|
+
maxAttempts?: number;
|
|
402
|
+
/**
|
|
403
|
+
* Delay between retries in milliseconds (default: 1000)
|
|
404
|
+
*/
|
|
405
|
+
delay?: number;
|
|
406
|
+
/**
|
|
407
|
+
* Exponential backoff multiplier (default: 1 = no backoff)
|
|
408
|
+
* Example: delay=1000, backoff=2 => 1s, 2s, 4s, 8s...
|
|
409
|
+
*/
|
|
410
|
+
backoff?: number;
|
|
411
|
+
/**
|
|
412
|
+
* Maximum delay in milliseconds (default: 30000)
|
|
413
|
+
* Caps the delay when using exponential backoff
|
|
414
|
+
*/
|
|
415
|
+
maxDelay?: number;
|
|
416
|
+
/**
|
|
417
|
+
* Jitter factor to add randomness to retry delays (default: 0 = no jitter)
|
|
418
|
+
* Range: 0 to 1 (e.g., 0.5 = ±50% randomness)
|
|
419
|
+
* Helps prevent thundering herd when many clients retry simultaneously.
|
|
420
|
+
*
|
|
421
|
+
* Note: Jitter is only applied when shouldRetry returns true.
|
|
422
|
+
* If request fails permanently, no jitter delay occurs.
|
|
423
|
+
*
|
|
424
|
+
* Example: delay=1000, jitter=0.5 => delay between 500ms and 1500ms
|
|
425
|
+
*/
|
|
426
|
+
jitter?: number;
|
|
427
|
+
/**
|
|
428
|
+
* Custom condition to determine if error should be retried
|
|
429
|
+
* Default: retry on NETWORK_ERROR only
|
|
430
|
+
* @param error - The error that occurred
|
|
431
|
+
* @returns true to retry, false to fail immediately
|
|
432
|
+
*/
|
|
433
|
+
shouldRetry?: (error: NetworkErrorDetail) => boolean;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Request deduplication configuration
|
|
437
|
+
*
|
|
438
|
+
* When enabled, identical GET requests (same URL) within a time window
|
|
439
|
+
* will share the same in-flight request instead of making duplicates.
|
|
440
|
+
*
|
|
441
|
+
* IMPORTANT:
|
|
442
|
+
* - Only applies to GET requests (POST/PUT/DELETE are never deduplicated)
|
|
443
|
+
* - Only deduplicates in-flight requests (not caching)
|
|
444
|
+
* - Safe for most read operations
|
|
445
|
+
*/
|
|
446
|
+
interface DedupeConfig {
|
|
447
|
+
/**
|
|
448
|
+
* Enable deduplication (default: false)
|
|
449
|
+
*/
|
|
450
|
+
enabled?: boolean;
|
|
451
|
+
/**
|
|
452
|
+
* Time window in milliseconds to consider requests as duplicates (default: 0)
|
|
453
|
+
* 0 = only dedupe concurrent/in-flight requests
|
|
454
|
+
* >0 = also dedupe requests within this time window after completion
|
|
455
|
+
*/
|
|
456
|
+
window?: number;
|
|
457
|
+
/**
|
|
458
|
+
* Custom key generator for deduplication
|
|
459
|
+
* Default: uses URL only for GET requests
|
|
460
|
+
* @param url - Request URL
|
|
461
|
+
* @param options - Request options
|
|
462
|
+
* @returns Key string, or null to skip deduplication for this request
|
|
463
|
+
*/
|
|
464
|
+
keyGenerator?: (url: string, options: FetchGuardRequestInit) => string | null;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Transport result - represents the outcome of a network request
|
|
468
|
+
*
|
|
469
|
+
* IMPORTANT: This is a TRANSPORT result, not a business result.
|
|
470
|
+
* - ok = HTTP response received (check envelope.status for 2xx/4xx/5xx)
|
|
471
|
+
* - err = Network failure (no response received)
|
|
472
|
+
*
|
|
473
|
+
* Example:
|
|
474
|
+
* ```typescript
|
|
475
|
+
* const result = await api.get('/users')
|
|
476
|
+
* if (result.ok) {
|
|
477
|
+
* // Transport succeeded - got HTTP response
|
|
478
|
+
* if (result.data.status >= 200 && result.data.status < 400) {
|
|
479
|
+
* // Business success
|
|
480
|
+
* } else {
|
|
481
|
+
* // Business error (4xx/5xx) - still has response body
|
|
482
|
+
* }
|
|
483
|
+
* } else {
|
|
484
|
+
* // Transport failed - no response (network error, timeout, cancelled)
|
|
485
|
+
* }
|
|
486
|
+
* ```
|
|
487
|
+
*/
|
|
488
|
+
type TransportResult = Result<FetchEnvelope>;
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Register a token provider with name
|
|
492
|
+
*/
|
|
493
|
+
declare function registerProvider(name: string, provider: TokenProvider): void;
|
|
494
|
+
/**
|
|
495
|
+
* Get provider by name
|
|
496
|
+
*/
|
|
497
|
+
declare function getProvider(name: string): TokenProvider;
|
|
498
|
+
/**
|
|
499
|
+
* Check if provider exists
|
|
500
|
+
*/
|
|
501
|
+
declare function hasProvider(name: string): boolean;
|
|
502
|
+
/**
|
|
503
|
+
* Get list of all provider names
|
|
504
|
+
*/
|
|
505
|
+
declare function listProviders(): string[];
|
|
506
|
+
/**
|
|
507
|
+
* Remove provider
|
|
508
|
+
*/
|
|
509
|
+
declare function unregisterProvider(name: string): boolean;
|
|
510
|
+
/**
|
|
511
|
+
* Remove all providers
|
|
512
|
+
*/
|
|
513
|
+
declare function clearProviders(): void;
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Custom auth method type
|
|
517
|
+
*/
|
|
518
|
+
type CustomAuthMethod = (...args: unknown[]) => Promise<Result<TokenInfo>>;
|
|
519
|
+
/**
|
|
520
|
+
* Configuration for creating provider
|
|
521
|
+
*
|
|
522
|
+
* refreshStorage: OPTIONAL - to load refresh token initially when worker starts
|
|
523
|
+
* - undefined: cookie-based auth (httpOnly cookie, no need to load)
|
|
524
|
+
* - RefreshTokenStorage: body-based auth (load from IndexedDB on startup)
|
|
525
|
+
*
|
|
526
|
+
* strategy: AuthStrategy with refresh (required), login/logout (required)
|
|
527
|
+
*
|
|
528
|
+
* customMethods: OPTIONAL - custom auth methods (loginWithPhone, loginWithGoogle, etc.)
|
|
529
|
+
*/
|
|
530
|
+
interface ProviderConfig {
|
|
531
|
+
refreshStorage?: RefreshTokenStorage;
|
|
532
|
+
parser: TokenParser;
|
|
533
|
+
strategy: AuthStrategy;
|
|
534
|
+
customMethods?: Record<string, CustomAuthMethod>;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Factory function to create TokenProvider from modular components
|
|
538
|
+
*
|
|
539
|
+
* Provider automatically handles refresh token:
|
|
540
|
+
* - If refreshToken is null and storage exists → load from storage initially
|
|
541
|
+
* - If refreshToken exists → use token from worker memory
|
|
542
|
+
* - Cookie-based (no storage) → always null
|
|
543
|
+
*
|
|
544
|
+
* Custom methods:
|
|
545
|
+
* - User can add custom auth methods (loginWithPhone, loginWithGoogle, etc.)
|
|
546
|
+
* - Custom methods will be spread into provider object
|
|
547
|
+
*/
|
|
548
|
+
declare function createProvider(config: ProviderConfig): TokenProvider;
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* IndexedDB storage options
|
|
552
|
+
*/
|
|
553
|
+
interface IndexedDBStorageOptions {
|
|
554
|
+
/** Database name (default: 'FetchGuardDB') */
|
|
555
|
+
dbName?: string;
|
|
556
|
+
/** Key for refresh token (default: 'refreshToken') */
|
|
557
|
+
refreshTokenKey?: string;
|
|
558
|
+
/**
|
|
559
|
+
* Error callback for debugging storage failures
|
|
560
|
+
* Called when IndexedDB operations fail (quota exceeded, permission denied, etc.)
|
|
561
|
+
* Storage still fails closed (returns null), but this allows logging/debugging.
|
|
562
|
+
*/
|
|
563
|
+
onError?: StorageErrorCallback;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* IndexedDB storage - only stores refresh token in IndexedDB
|
|
567
|
+
* Suitable for body-based refresh strategy
|
|
568
|
+
* Persists refresh token for reuse after reload
|
|
569
|
+
*
|
|
570
|
+
* @param options - Storage options or legacy dbName string
|
|
571
|
+
* @param legacyRefreshTokenKey - Legacy refreshTokenKey (for backward compatibility)
|
|
572
|
+
*/
|
|
573
|
+
declare function createIndexedDBStorage(options?: IndexedDBStorageOptions | string, legacyRefreshTokenKey?: string): RefreshTokenStorage;
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Body parser - parse token from response body (JSON)
|
|
577
|
+
* Expects response format: { data: { accessToken, refreshToken, expiresAt?, user? } }
|
|
578
|
+
*/
|
|
579
|
+
declare const bodyParser: TokenParser;
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Cookie parser - parse access token from response body
|
|
583
|
+
* Expects response format: { data: { accessToken, expiresAt?, user? } }
|
|
584
|
+
* Refresh token is automatically set by backend into httpOnly cookie
|
|
585
|
+
*/
|
|
586
|
+
declare const cookieParser: TokenParser;
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Cookie auth strategy - all auth operations via httpOnly cookies
|
|
590
|
+
* Suitable for SSR and cross-domain authentication
|
|
591
|
+
*
|
|
592
|
+
* Refresh token is sent automatically via httpOnly cookie
|
|
593
|
+
* Credentials are sent in request body
|
|
594
|
+
*
|
|
595
|
+
* Login URL can be:
|
|
596
|
+
* - Configured once: loginUrl: 'https://api.example.com/auth/login'
|
|
597
|
+
* - Passed per call: login(payload, 'https://...')
|
|
598
|
+
*
|
|
599
|
+
* Header priority (lowest to highest):
|
|
600
|
+
* - defaultHeaders (from FetchGuardOptions)
|
|
601
|
+
* - headers (from ProviderPresetConfig)
|
|
602
|
+
* - Content-Type: application/json
|
|
603
|
+
*/
|
|
604
|
+
declare function createCookieStrategy(config: {
|
|
605
|
+
refreshUrl: string;
|
|
606
|
+
loginUrl: string;
|
|
607
|
+
logoutUrl: string;
|
|
608
|
+
headers?: Record<string, string>;
|
|
609
|
+
defaultHeaders?: Record<string, string>;
|
|
610
|
+
}): AuthStrategy;
|
|
611
|
+
/**
|
|
612
|
+
* Standard cookie strategy
|
|
613
|
+
*/
|
|
614
|
+
declare const cookieStrategy: AuthStrategy;
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Body auth strategy - all auth operations via request body
|
|
618
|
+
* Suitable for SPA applications
|
|
619
|
+
*
|
|
620
|
+
* All tokens/credentials are sent in request body
|
|
621
|
+
*
|
|
622
|
+
* Login URL can be:
|
|
623
|
+
* - Configured once: loginUrl: 'https://api.example.com/auth/login'
|
|
624
|
+
* - Passed per call: login(payload, 'https://...')
|
|
625
|
+
*
|
|
626
|
+
* Header priority (lowest to highest):
|
|
627
|
+
* - defaultHeaders (from FetchGuardOptions)
|
|
628
|
+
* - headers (from ProviderPresetConfig)
|
|
629
|
+
* - Content-Type: application/json
|
|
630
|
+
*/
|
|
631
|
+
declare function createBodyStrategy(config: {
|
|
632
|
+
refreshUrl: string;
|
|
633
|
+
loginUrl: string;
|
|
634
|
+
logoutUrl: string;
|
|
635
|
+
headers?: Record<string, string>;
|
|
636
|
+
defaultHeaders?: Record<string, string>;
|
|
637
|
+
}): AuthStrategy;
|
|
638
|
+
/**
|
|
639
|
+
* Standard body strategy
|
|
640
|
+
*/
|
|
641
|
+
declare const bodyStrategy: AuthStrategy;
|
|
642
|
+
|
|
643
|
+
export { type AuthResult as A, cookieStrategy as B, createCookieStrategy as C, type DebugHooks as D, type ExchangeTokenOptions as E, type FetchGuardOptions as F, bodyStrategy as G, createBodyStrategy as H, type IndexedDBStorageOptions as I, type NetworkErrorDetail as N, type ProviderPresetConfig as P, type RefreshReason as R, type SerializedFormDataResult as S, type TokenProvider as T, type WorkerConfig as W, type FetchGuardRequestInit as a, type FetchEnvelope as b, type SerializedFormData as c, type RefreshTokenStorage as d, type TokenParser as e, type AuthStrategy as f, type TokenInfo as g, type SerializedFile as h, type SerializedFormDataEntry as i, type TransportResult as j, type StorageErrorContext as k, type StorageErrorCallback as l, type RetryConfig as m, type DedupeConfig as n, type RequestMetrics as o, getProvider as p, hasProvider as q, registerProvider as r, listProviders as s, clearProviders as t, unregisterProvider as u, createProvider as v, type ProviderConfig as w, createIndexedDBStorage as x, bodyParser as y, cookieParser as z };
|
package/dist/worker.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export { f as AuthStrategy, E as ExchangeTokenOptions, d as RefreshTokenStorage, e as TokenParser, T as TokenProvider, y as bodyParser, t as clearProviders, z as cookieParser, H as createBodyStrategy, C as createCookieStrategy, x as createIndexedDBStorage, v as createProvider, q as hasProvider, s as listProviders, r as registerProvider, u as unregisterProvider } from './worker-DBL8XAZJ.js';
|
|
2
|
+
import 'ts-micro-result';
|
package/dist/worker.js
CHANGED
|
@@ -162,6 +162,15 @@ function sendTokenRefreshed(reason) {
|
|
|
162
162
|
|
|
163
163
|
// src/utils/registry.ts
|
|
164
164
|
var registry = /* @__PURE__ */ new Map();
|
|
165
|
+
function registerProvider(name, provider) {
|
|
166
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
167
|
+
throw new Error("Provider name must be a non-empty string");
|
|
168
|
+
}
|
|
169
|
+
if (!provider || typeof provider.refreshToken !== "function") {
|
|
170
|
+
throw new Error("Provider must implement TokenProvider interface");
|
|
171
|
+
}
|
|
172
|
+
registry.set(name, provider);
|
|
173
|
+
}
|
|
165
174
|
function getProvider(name) {
|
|
166
175
|
const provider = registry.get(name);
|
|
167
176
|
if (!provider) {
|
|
@@ -169,6 +178,18 @@ function getProvider(name) {
|
|
|
169
178
|
}
|
|
170
179
|
return provider;
|
|
171
180
|
}
|
|
181
|
+
function hasProvider(name) {
|
|
182
|
+
return registry.has(name);
|
|
183
|
+
}
|
|
184
|
+
function listProviders() {
|
|
185
|
+
return Array.from(registry.keys());
|
|
186
|
+
}
|
|
187
|
+
function unregisterProvider(name) {
|
|
188
|
+
return registry.delete(name);
|
|
189
|
+
}
|
|
190
|
+
function clearProviders() {
|
|
191
|
+
registry.clear();
|
|
192
|
+
}
|
|
172
193
|
|
|
173
194
|
// src/provider/create-provider.ts
|
|
174
195
|
import { ok, err } from "ts-micro-result";
|
|
@@ -856,4 +877,17 @@ function isBinaryContentType(contentType) {
|
|
|
856
877
|
}
|
|
857
878
|
};
|
|
858
879
|
})();
|
|
880
|
+
export {
|
|
881
|
+
bodyParser,
|
|
882
|
+
clearProviders,
|
|
883
|
+
cookieParser,
|
|
884
|
+
createBodyStrategy,
|
|
885
|
+
createCookieStrategy,
|
|
886
|
+
createIndexedDBStorage,
|
|
887
|
+
createProvider,
|
|
888
|
+
hasProvider,
|
|
889
|
+
listProviders,
|
|
890
|
+
registerProvider,
|
|
891
|
+
unregisterProvider
|
|
892
|
+
};
|
|
859
893
|
//# sourceMappingURL=worker.js.map
|