@markwharton/pwa-core 3.3.0 → 3.4.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/dist/client.d.ts +116 -0
- package/dist/client.js +270 -0
- package/dist/server.d.ts +312 -3
- package/dist/server.js +695 -0
- package/dist/shared.d.ts +40 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -23,6 +23,15 @@ export interface ApiClientConfig {
|
|
|
23
23
|
/** Optional request timeout in milliseconds (default: 30000) */
|
|
24
24
|
timeout?: number;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Configuration for initSessionApiClient
|
|
28
|
+
*/
|
|
29
|
+
export interface SessionApiClientConfig {
|
|
30
|
+
/** Optional callback for 401 responses (e.g., redirect to login) */
|
|
31
|
+
onUnauthenticated?: () => void;
|
|
32
|
+
/** Optional request timeout in milliseconds (default: 30000) */
|
|
33
|
+
timeout?: number;
|
|
34
|
+
}
|
|
26
35
|
/**
|
|
27
36
|
* Custom error class for API errors.
|
|
28
37
|
* Preserves HTTP status code and error message from the server.
|
|
@@ -189,3 +198,110 @@ export declare function apiCallVoid(url: string, options?: RequestInit): Promise
|
|
|
189
198
|
* }
|
|
190
199
|
*/
|
|
191
200
|
export declare function apiCallSafe<T>(url: string, options?: RequestInit): Promise<ApiResponse<T>>;
|
|
201
|
+
/**
|
|
202
|
+
* Initializes the session-based API client.
|
|
203
|
+
* Uses cookies (credentials: 'include') instead of Bearer tokens.
|
|
204
|
+
* @param config - Client configuration
|
|
205
|
+
* @example
|
|
206
|
+
* initSessionApiClient({
|
|
207
|
+
* onUnauthenticated: () => window.location.href = '/login',
|
|
208
|
+
* timeout: 60000
|
|
209
|
+
* });
|
|
210
|
+
*/
|
|
211
|
+
export declare function initSessionApiClient(config: SessionApiClientConfig): void;
|
|
212
|
+
/**
|
|
213
|
+
* Makes a cookie-authenticated API call. Throws ApiError on non-2xx responses.
|
|
214
|
+
* Uses credentials: 'include' to send session cookies.
|
|
215
|
+
* @typeParam T - The expected response data type
|
|
216
|
+
* @param url - The API endpoint URL
|
|
217
|
+
* @param options - Optional fetch options (method, body, headers)
|
|
218
|
+
* @returns The parsed JSON response
|
|
219
|
+
* @throws ApiError on non-2xx HTTP status or timeout
|
|
220
|
+
* @example
|
|
221
|
+
* const user = await sessionApiCall<User>('/api/auth/me');
|
|
222
|
+
*/
|
|
223
|
+
export declare function sessionApiCall<T>(url: string, options?: RequestInit): Promise<T>;
|
|
224
|
+
/**
|
|
225
|
+
* Makes a cookie-authenticated GET request.
|
|
226
|
+
* @typeParam T - The expected response data type
|
|
227
|
+
* @param url - The API endpoint URL
|
|
228
|
+
* @returns The parsed JSON response
|
|
229
|
+
* @throws ApiError on non-2xx HTTP status
|
|
230
|
+
*/
|
|
231
|
+
export declare function sessionApiGet<T>(url: string): Promise<T>;
|
|
232
|
+
/**
|
|
233
|
+
* Makes a cookie-authenticated POST request.
|
|
234
|
+
* @typeParam T - The expected response data type
|
|
235
|
+
* @param url - The API endpoint URL
|
|
236
|
+
* @param body - Optional request body (will be JSON stringified)
|
|
237
|
+
* @returns The parsed JSON response
|
|
238
|
+
* @throws ApiError on non-2xx HTTP status
|
|
239
|
+
*/
|
|
240
|
+
export declare function sessionApiPost<T>(url: string, body?: unknown): Promise<T>;
|
|
241
|
+
/**
|
|
242
|
+
* Makes a cookie-authenticated PUT request.
|
|
243
|
+
* @typeParam T - The expected response data type
|
|
244
|
+
* @param url - The API endpoint URL
|
|
245
|
+
* @param body - Optional request body (will be JSON stringified)
|
|
246
|
+
* @returns The parsed JSON response
|
|
247
|
+
* @throws ApiError on non-2xx HTTP status
|
|
248
|
+
*/
|
|
249
|
+
export declare function sessionApiPut<T>(url: string, body?: unknown): Promise<T>;
|
|
250
|
+
/**
|
|
251
|
+
* Makes a cookie-authenticated PATCH request.
|
|
252
|
+
* @typeParam T - The expected response data type
|
|
253
|
+
* @param url - The API endpoint URL
|
|
254
|
+
* @param body - Optional request body (will be JSON stringified)
|
|
255
|
+
* @returns The parsed JSON response
|
|
256
|
+
* @throws ApiError on non-2xx HTTP status
|
|
257
|
+
*/
|
|
258
|
+
export declare function sessionApiPatch<T>(url: string, body?: unknown): Promise<T>;
|
|
259
|
+
/**
|
|
260
|
+
* Makes a cookie-authenticated DELETE request.
|
|
261
|
+
* @typeParam T - The expected response data type
|
|
262
|
+
* @param url - The API endpoint URL
|
|
263
|
+
* @returns The parsed JSON response
|
|
264
|
+
* @throws ApiError on non-2xx HTTP status
|
|
265
|
+
*/
|
|
266
|
+
export declare function sessionApiDelete<T>(url: string): Promise<T>;
|
|
267
|
+
/**
|
|
268
|
+
* Makes a cookie-authenticated API call with Result-style error handling.
|
|
269
|
+
* Unlike sessionApiCall, this never throws - errors are returned in the response.
|
|
270
|
+
* @typeParam T - The expected response data type
|
|
271
|
+
* @param url - The API endpoint URL
|
|
272
|
+
* @param options - Optional fetch options (method, body, headers)
|
|
273
|
+
* @returns ApiResponse with ok, status, data (on success), or error (on failure)
|
|
274
|
+
*/
|
|
275
|
+
export declare function sessionApiCallSafe<T>(url: string, options?: RequestInit): Promise<ApiResponse<T>>;
|
|
276
|
+
/**
|
|
277
|
+
* Makes a cookie-authenticated API call expecting no response body.
|
|
278
|
+
* @param url - The API endpoint URL
|
|
279
|
+
* @param options - Optional fetch options (method, body, headers)
|
|
280
|
+
* @throws ApiError on non-2xx HTTP status or timeout
|
|
281
|
+
*/
|
|
282
|
+
export declare function sessionApiCallVoid(url: string, options?: RequestInit): Promise<void>;
|
|
283
|
+
/**
|
|
284
|
+
* Checks if an HTTP status code indicates a retryable error.
|
|
285
|
+
* @param status - The HTTP status code (or undefined for network errors)
|
|
286
|
+
* @returns True for 408, 425, 429, 5xx, or undefined (network error)
|
|
287
|
+
* @example
|
|
288
|
+
* if (isRetryableStatus(error.status)) { retry(); }
|
|
289
|
+
*/
|
|
290
|
+
export declare function isRetryableStatus(status: number | undefined): boolean;
|
|
291
|
+
/**
|
|
292
|
+
* Makes a cookie-authenticated API call with exponential backoff retry.
|
|
293
|
+
* Retries on retryable status codes (408, 425, 429, 5xx, network errors).
|
|
294
|
+
* @typeParam T - The expected response data type
|
|
295
|
+
* @param url - The API endpoint URL
|
|
296
|
+
* @param options - Optional fetch options
|
|
297
|
+
* @param maxRetries - Maximum number of retries (default: 3)
|
|
298
|
+
* @param initialDelayMs - Initial retry delay in ms (default: 1000)
|
|
299
|
+
* @returns The parsed JSON response
|
|
300
|
+
* @throws ApiError if all retries fail
|
|
301
|
+
* @example
|
|
302
|
+
* const result = await sessionApiCallWithRetry<AuthResponse>('/api/auth/verify', {
|
|
303
|
+
* method: 'POST',
|
|
304
|
+
* body: JSON.stringify({ token })
|
|
305
|
+
* });
|
|
306
|
+
*/
|
|
307
|
+
export declare function sessionApiCallWithRetry<T>(url: string, options?: RequestInit, maxRetries?: number, initialDelayMs?: number): Promise<T>;
|
package/dist/client.js
CHANGED
|
@@ -16,6 +16,17 @@ exports.apiPatch = apiPatch;
|
|
|
16
16
|
exports.apiDelete = apiDelete;
|
|
17
17
|
exports.apiCallVoid = apiCallVoid;
|
|
18
18
|
exports.apiCallSafe = apiCallSafe;
|
|
19
|
+
exports.initSessionApiClient = initSessionApiClient;
|
|
20
|
+
exports.sessionApiCall = sessionApiCall;
|
|
21
|
+
exports.sessionApiGet = sessionApiGet;
|
|
22
|
+
exports.sessionApiPost = sessionApiPost;
|
|
23
|
+
exports.sessionApiPut = sessionApiPut;
|
|
24
|
+
exports.sessionApiPatch = sessionApiPatch;
|
|
25
|
+
exports.sessionApiDelete = sessionApiDelete;
|
|
26
|
+
exports.sessionApiCallSafe = sessionApiCallSafe;
|
|
27
|
+
exports.sessionApiCallVoid = sessionApiCallVoid;
|
|
28
|
+
exports.isRetryableStatus = isRetryableStatus;
|
|
29
|
+
exports.sessionApiCallWithRetry = sessionApiCallWithRetry;
|
|
19
30
|
// =============================================================================
|
|
20
31
|
// ApiError Class
|
|
21
32
|
// =============================================================================
|
|
@@ -360,3 +371,262 @@ async function apiCallSafe(url, options = {}) {
|
|
|
360
371
|
clearTimeout(timeoutId);
|
|
361
372
|
}
|
|
362
373
|
}
|
|
374
|
+
// =============================================================================
|
|
375
|
+
// Session API Client (cookie-based auth)
|
|
376
|
+
// =============================================================================
|
|
377
|
+
// Session API client state
|
|
378
|
+
let sessionOnUnauthenticated = null;
|
|
379
|
+
let sessionRequestTimeout = 30000;
|
|
380
|
+
/**
|
|
381
|
+
* Initializes the session-based API client.
|
|
382
|
+
* Uses cookies (credentials: 'include') instead of Bearer tokens.
|
|
383
|
+
* @param config - Client configuration
|
|
384
|
+
* @example
|
|
385
|
+
* initSessionApiClient({
|
|
386
|
+
* onUnauthenticated: () => window.location.href = '/login',
|
|
387
|
+
* timeout: 60000
|
|
388
|
+
* });
|
|
389
|
+
*/
|
|
390
|
+
function initSessionApiClient(config) {
|
|
391
|
+
sessionOnUnauthenticated = config.onUnauthenticated ?? null;
|
|
392
|
+
sessionRequestTimeout = config.timeout ?? 30000;
|
|
393
|
+
}
|
|
394
|
+
/** Handle 401 unauthenticated callback for session API */
|
|
395
|
+
function handleUnauthenticated(status) {
|
|
396
|
+
if (status === 401 && sessionOnUnauthenticated) {
|
|
397
|
+
sessionOnUnauthenticated();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Makes a cookie-authenticated API call. Throws ApiError on non-2xx responses.
|
|
402
|
+
* Uses credentials: 'include' to send session cookies.
|
|
403
|
+
* @typeParam T - The expected response data type
|
|
404
|
+
* @param url - The API endpoint URL
|
|
405
|
+
* @param options - Optional fetch options (method, body, headers)
|
|
406
|
+
* @returns The parsed JSON response
|
|
407
|
+
* @throws ApiError on non-2xx HTTP status or timeout
|
|
408
|
+
* @example
|
|
409
|
+
* const user = await sessionApiCall<User>('/api/auth/me');
|
|
410
|
+
*/
|
|
411
|
+
async function sessionApiCall(url, options = {}) {
|
|
412
|
+
const headers = {
|
|
413
|
+
'Content-Type': 'application/json',
|
|
414
|
+
...options.headers
|
|
415
|
+
};
|
|
416
|
+
const controller = new AbortController();
|
|
417
|
+
const timeoutId = setTimeout(() => controller.abort(), sessionRequestTimeout);
|
|
418
|
+
try {
|
|
419
|
+
const response = await fetch(url, {
|
|
420
|
+
...options,
|
|
421
|
+
headers,
|
|
422
|
+
credentials: 'include',
|
|
423
|
+
signal: controller.signal
|
|
424
|
+
});
|
|
425
|
+
if (!response.ok) {
|
|
426
|
+
handleUnauthenticated(response.status);
|
|
427
|
+
const errorMessage = await extractErrorMessage(response);
|
|
428
|
+
throw new ApiError(response.status, errorMessage);
|
|
429
|
+
}
|
|
430
|
+
const text = await response.text();
|
|
431
|
+
if (!text) {
|
|
432
|
+
throw new ApiError(response.status, 'Empty response body');
|
|
433
|
+
}
|
|
434
|
+
return JSON.parse(text);
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
handleApiError(error);
|
|
438
|
+
}
|
|
439
|
+
finally {
|
|
440
|
+
clearTimeout(timeoutId);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Makes a cookie-authenticated GET request.
|
|
445
|
+
* @typeParam T - The expected response data type
|
|
446
|
+
* @param url - The API endpoint URL
|
|
447
|
+
* @returns The parsed JSON response
|
|
448
|
+
* @throws ApiError on non-2xx HTTP status
|
|
449
|
+
*/
|
|
450
|
+
async function sessionApiGet(url) {
|
|
451
|
+
return sessionApiCall(url, { method: 'GET' });
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Makes a cookie-authenticated POST request.
|
|
455
|
+
* @typeParam T - The expected response data type
|
|
456
|
+
* @param url - The API endpoint URL
|
|
457
|
+
* @param body - Optional request body (will be JSON stringified)
|
|
458
|
+
* @returns The parsed JSON response
|
|
459
|
+
* @throws ApiError on non-2xx HTTP status
|
|
460
|
+
*/
|
|
461
|
+
async function sessionApiPost(url, body) {
|
|
462
|
+
return sessionApiCall(url, {
|
|
463
|
+
method: 'POST',
|
|
464
|
+
body: body ? JSON.stringify(body) : undefined
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Makes a cookie-authenticated PUT request.
|
|
469
|
+
* @typeParam T - The expected response data type
|
|
470
|
+
* @param url - The API endpoint URL
|
|
471
|
+
* @param body - Optional request body (will be JSON stringified)
|
|
472
|
+
* @returns The parsed JSON response
|
|
473
|
+
* @throws ApiError on non-2xx HTTP status
|
|
474
|
+
*/
|
|
475
|
+
async function sessionApiPut(url, body) {
|
|
476
|
+
return sessionApiCall(url, {
|
|
477
|
+
method: 'PUT',
|
|
478
|
+
body: body ? JSON.stringify(body) : undefined
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Makes a cookie-authenticated PATCH request.
|
|
483
|
+
* @typeParam T - The expected response data type
|
|
484
|
+
* @param url - The API endpoint URL
|
|
485
|
+
* @param body - Optional request body (will be JSON stringified)
|
|
486
|
+
* @returns The parsed JSON response
|
|
487
|
+
* @throws ApiError on non-2xx HTTP status
|
|
488
|
+
*/
|
|
489
|
+
async function sessionApiPatch(url, body) {
|
|
490
|
+
return sessionApiCall(url, {
|
|
491
|
+
method: 'PATCH',
|
|
492
|
+
body: body ? JSON.stringify(body) : undefined
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Makes a cookie-authenticated DELETE request.
|
|
497
|
+
* @typeParam T - The expected response data type
|
|
498
|
+
* @param url - The API endpoint URL
|
|
499
|
+
* @returns The parsed JSON response
|
|
500
|
+
* @throws ApiError on non-2xx HTTP status
|
|
501
|
+
*/
|
|
502
|
+
async function sessionApiDelete(url) {
|
|
503
|
+
return sessionApiCall(url, { method: 'DELETE' });
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Makes a cookie-authenticated API call with Result-style error handling.
|
|
507
|
+
* Unlike sessionApiCall, this never throws - errors are returned in the response.
|
|
508
|
+
* @typeParam T - The expected response data type
|
|
509
|
+
* @param url - The API endpoint URL
|
|
510
|
+
* @param options - Optional fetch options (method, body, headers)
|
|
511
|
+
* @returns ApiResponse with ok, status, data (on success), or error (on failure)
|
|
512
|
+
*/
|
|
513
|
+
async function sessionApiCallSafe(url, options = {}) {
|
|
514
|
+
const headers = {
|
|
515
|
+
'Content-Type': 'application/json',
|
|
516
|
+
...options.headers
|
|
517
|
+
};
|
|
518
|
+
const controller = new AbortController();
|
|
519
|
+
const timeoutId = setTimeout(() => controller.abort(), sessionRequestTimeout);
|
|
520
|
+
try {
|
|
521
|
+
const response = await fetch(url, {
|
|
522
|
+
...options,
|
|
523
|
+
headers,
|
|
524
|
+
credentials: 'include',
|
|
525
|
+
signal: controller.signal
|
|
526
|
+
});
|
|
527
|
+
if (!response.ok) {
|
|
528
|
+
handleUnauthenticated(response.status);
|
|
529
|
+
const errorMessage = await extractErrorMessage(response);
|
|
530
|
+
return { ok: false, status: response.status, error: errorMessage };
|
|
531
|
+
}
|
|
532
|
+
const text = await response.text();
|
|
533
|
+
const data = text ? JSON.parse(text) : undefined;
|
|
534
|
+
return { ok: true, status: response.status, data };
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
538
|
+
return { ok: false, status: 0, error: 'Request timeout' };
|
|
539
|
+
}
|
|
540
|
+
return { ok: false, status: 0, error: 'Network error' };
|
|
541
|
+
}
|
|
542
|
+
finally {
|
|
543
|
+
clearTimeout(timeoutId);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Makes a cookie-authenticated API call expecting no response body.
|
|
548
|
+
* @param url - The API endpoint URL
|
|
549
|
+
* @param options - Optional fetch options (method, body, headers)
|
|
550
|
+
* @throws ApiError on non-2xx HTTP status or timeout
|
|
551
|
+
*/
|
|
552
|
+
async function sessionApiCallVoid(url, options = {}) {
|
|
553
|
+
const headers = {
|
|
554
|
+
'Content-Type': 'application/json',
|
|
555
|
+
...options.headers
|
|
556
|
+
};
|
|
557
|
+
const controller = new AbortController();
|
|
558
|
+
const timeoutId = setTimeout(() => controller.abort(), sessionRequestTimeout);
|
|
559
|
+
try {
|
|
560
|
+
const response = await fetch(url, {
|
|
561
|
+
...options,
|
|
562
|
+
headers,
|
|
563
|
+
credentials: 'include',
|
|
564
|
+
signal: controller.signal
|
|
565
|
+
});
|
|
566
|
+
if (!response.ok) {
|
|
567
|
+
handleUnauthenticated(response.status);
|
|
568
|
+
const errorMessage = await extractErrorMessage(response);
|
|
569
|
+
throw new ApiError(response.status, errorMessage);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
handleApiError(error);
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
clearTimeout(timeoutId);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// =============================================================================
|
|
580
|
+
// Retryable Request Helper
|
|
581
|
+
// =============================================================================
|
|
582
|
+
/**
|
|
583
|
+
* Checks if an HTTP status code indicates a retryable error.
|
|
584
|
+
* @param status - The HTTP status code (or undefined for network errors)
|
|
585
|
+
* @returns True for 408, 425, 429, 5xx, or undefined (network error)
|
|
586
|
+
* @example
|
|
587
|
+
* if (isRetryableStatus(error.status)) { retry(); }
|
|
588
|
+
*/
|
|
589
|
+
function isRetryableStatus(status) {
|
|
590
|
+
if (status === undefined)
|
|
591
|
+
return true; // Network error
|
|
592
|
+
if (status === 408 || status === 425 || status === 429)
|
|
593
|
+
return true;
|
|
594
|
+
if (status >= 500)
|
|
595
|
+
return true;
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Makes a cookie-authenticated API call with exponential backoff retry.
|
|
600
|
+
* Retries on retryable status codes (408, 425, 429, 5xx, network errors).
|
|
601
|
+
* @typeParam T - The expected response data type
|
|
602
|
+
* @param url - The API endpoint URL
|
|
603
|
+
* @param options - Optional fetch options
|
|
604
|
+
* @param maxRetries - Maximum number of retries (default: 3)
|
|
605
|
+
* @param initialDelayMs - Initial retry delay in ms (default: 1000)
|
|
606
|
+
* @returns The parsed JSON response
|
|
607
|
+
* @throws ApiError if all retries fail
|
|
608
|
+
* @example
|
|
609
|
+
* const result = await sessionApiCallWithRetry<AuthResponse>('/api/auth/verify', {
|
|
610
|
+
* method: 'POST',
|
|
611
|
+
* body: JSON.stringify({ token })
|
|
612
|
+
* });
|
|
613
|
+
*/
|
|
614
|
+
async function sessionApiCallWithRetry(url, options, maxRetries = 3, initialDelayMs = 1000) {
|
|
615
|
+
let lastError;
|
|
616
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
617
|
+
try {
|
|
618
|
+
return await sessionApiCall(url, options);
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
lastError = error;
|
|
622
|
+
const status = error instanceof ApiError ? error.status : undefined;
|
|
623
|
+
if (!isRetryableStatus(status) || attempt === maxRetries) {
|
|
624
|
+
throw error;
|
|
625
|
+
}
|
|
626
|
+
// Exponential backoff
|
|
627
|
+
const delay = initialDelayMs * Math.pow(2, attempt);
|
|
628
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
throw lastError;
|
|
632
|
+
}
|