@markwharton/pwa-core 3.3.0 → 3.4.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/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
+ }