@markwharton/pwa-core 3.5.0 → 4.0.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 +47 -89
- package/dist/client.js +102 -252
- package/dist/server.d.ts +71 -62
- package/dist/server.js +100 -69
- package/dist/shared.d.ts +4 -4
- package/dist/shared.js +4 -4
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
* pwa-core/client - Browser-side API client utilities
|
|
3
3
|
*
|
|
4
4
|
* This module has NO Node.js dependencies and is designed for browser use.
|
|
5
|
+
*
|
|
6
|
+
* Supports two auth strategies via a single API surface:
|
|
7
|
+
* - Token (Bearer): initApiClient() — sends Authorization header
|
|
8
|
+
* - Session (Cookie): initSessionApiClient() — sends credentials: 'include'
|
|
9
|
+
*
|
|
10
|
+
* All api* functions (apiCall, apiGet, apiPost, etc.) work with whichever
|
|
11
|
+
* strategy was initialized. The sessionApi* names are deprecated aliases.
|
|
5
12
|
*/
|
|
6
13
|
/**
|
|
7
|
-
* API response wrapper for safe calls
|
|
14
|
+
* API response wrapper for safe calls.
|
|
15
|
+
* @deprecated Use ApiResponse — will be removed in next major version.
|
|
8
16
|
*/
|
|
9
17
|
export interface ApiResponse<T> {
|
|
10
18
|
ok: boolean;
|
|
@@ -13,7 +21,7 @@ export interface ApiResponse<T> {
|
|
|
13
21
|
error?: string;
|
|
14
22
|
}
|
|
15
23
|
/**
|
|
16
|
-
* Configuration for initApiClient
|
|
24
|
+
* Configuration for initApiClient (Bearer token auth)
|
|
17
25
|
*/
|
|
18
26
|
export interface ApiClientConfig {
|
|
19
27
|
/** Function that returns the current auth token (or null) */
|
|
@@ -24,7 +32,7 @@ export interface ApiClientConfig {
|
|
|
24
32
|
timeout?: number;
|
|
25
33
|
}
|
|
26
34
|
/**
|
|
27
|
-
* Configuration for initSessionApiClient
|
|
35
|
+
* Configuration for initSessionApiClient (cookie auth)
|
|
28
36
|
*/
|
|
29
37
|
export interface SessionApiClientConfig {
|
|
30
38
|
/** Optional callback for 401 responses (e.g., redirect to login) */
|
|
@@ -81,7 +89,7 @@ export declare class ApiError extends Error {
|
|
|
81
89
|
isServerError(): boolean;
|
|
82
90
|
}
|
|
83
91
|
/**
|
|
84
|
-
* Initializes the API client with token
|
|
92
|
+
* Initializes the API client with Bearer token authentication.
|
|
85
93
|
* Call once at application startup.
|
|
86
94
|
* @param config - Client configuration
|
|
87
95
|
* @example
|
|
@@ -92,6 +100,18 @@ export declare class ApiError extends Error {
|
|
|
92
100
|
* });
|
|
93
101
|
*/
|
|
94
102
|
export declare function initApiClient(config: ApiClientConfig): void;
|
|
103
|
+
/**
|
|
104
|
+
* Initializes the session-based API client.
|
|
105
|
+
* Uses cookies (credentials: 'include') instead of Bearer tokens.
|
|
106
|
+
* After calling this, use apiCall/apiGet/apiPost etc. (same functions as token auth).
|
|
107
|
+
* @param config - Client configuration
|
|
108
|
+
* @example
|
|
109
|
+
* initSessionApiClient({
|
|
110
|
+
* onUnauthenticated: () => window.location.href = '/login',
|
|
111
|
+
* timeout: 60000
|
|
112
|
+
* });
|
|
113
|
+
*/
|
|
114
|
+
export declare function initSessionApiClient(config: SessionApiClientConfig): void;
|
|
95
115
|
/**
|
|
96
116
|
* Extract a user-friendly error message from an API error.
|
|
97
117
|
* Use in catch blocks to convert errors to displayable strings.
|
|
@@ -108,6 +128,7 @@ export declare function initApiClient(config: ApiClientConfig): void;
|
|
|
108
128
|
export declare function getApiErrorMessage(error: unknown, fallback: string): string;
|
|
109
129
|
/**
|
|
110
130
|
* Makes an authenticated API call. Throws ApiError on non-2xx responses.
|
|
131
|
+
* Works with both token (Bearer) and session (cookie) auth strategies.
|
|
111
132
|
* @typeParam T - The expected response data type
|
|
112
133
|
* @param url - The API endpoint URL
|
|
113
134
|
* @param options - Optional fetch options (method, body, headers)
|
|
@@ -198,88 +219,6 @@ export declare function apiCallVoid(url: string, options?: RequestInit): Promise
|
|
|
198
219
|
* }
|
|
199
220
|
*/
|
|
200
221
|
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
222
|
/**
|
|
284
223
|
* Checks if an HTTP status code indicates a retryable error.
|
|
285
224
|
* @param status - The HTTP status code (or undefined for network errors)
|
|
@@ -289,8 +228,9 @@ export declare function sessionApiCallVoid(url: string, options?: RequestInit):
|
|
|
289
228
|
*/
|
|
290
229
|
export declare function isRetryableStatus(status: number | undefined): boolean;
|
|
291
230
|
/**
|
|
292
|
-
* Makes
|
|
231
|
+
* Makes an authenticated API call with exponential backoff retry.
|
|
293
232
|
* Retries on retryable status codes (408, 425, 429, 5xx, network errors).
|
|
233
|
+
* Works with both token and session auth strategies.
|
|
294
234
|
* @typeParam T - The expected response data type
|
|
295
235
|
* @param url - The API endpoint URL
|
|
296
236
|
* @param options - Optional fetch options
|
|
@@ -299,9 +239,27 @@ export declare function isRetryableStatus(status: number | undefined): boolean;
|
|
|
299
239
|
* @returns The parsed JSON response
|
|
300
240
|
* @throws ApiError if all retries fail
|
|
301
241
|
* @example
|
|
302
|
-
* const result = await
|
|
242
|
+
* const result = await apiCallWithRetry<AuthResponse>('/api/auth/verify', {
|
|
303
243
|
* method: 'POST',
|
|
304
244
|
* body: JSON.stringify({ token })
|
|
305
245
|
* });
|
|
306
246
|
*/
|
|
307
|
-
export declare function
|
|
247
|
+
export declare function apiCallWithRetry<T>(url: string, options?: RequestInit, maxRetries?: number, initialDelayMs?: number): Promise<T>;
|
|
248
|
+
/** @deprecated Use apiCall after initSessionApiClient */
|
|
249
|
+
export declare const sessionApiCall: typeof apiCall;
|
|
250
|
+
/** @deprecated Use apiGet after initSessionApiClient */
|
|
251
|
+
export declare const sessionApiGet: typeof apiGet;
|
|
252
|
+
/** @deprecated Use apiPost after initSessionApiClient */
|
|
253
|
+
export declare const sessionApiPost: typeof apiPost;
|
|
254
|
+
/** @deprecated Use apiPut after initSessionApiClient */
|
|
255
|
+
export declare const sessionApiPut: typeof apiPut;
|
|
256
|
+
/** @deprecated Use apiPatch after initSessionApiClient */
|
|
257
|
+
export declare const sessionApiPatch: typeof apiPatch;
|
|
258
|
+
/** @deprecated Use apiDelete after initSessionApiClient */
|
|
259
|
+
export declare const sessionApiDelete: typeof apiDelete;
|
|
260
|
+
/** @deprecated Use apiCallSafe after initSessionApiClient */
|
|
261
|
+
export declare const sessionApiCallSafe: typeof apiCallSafe;
|
|
262
|
+
/** @deprecated Use apiCallVoid after initSessionApiClient */
|
|
263
|
+
export declare const sessionApiCallVoid: typeof apiCallVoid;
|
|
264
|
+
/** @deprecated Use apiCallWithRetry after initSessionApiClient */
|
|
265
|
+
export declare const sessionApiCallWithRetry: typeof apiCallWithRetry;
|
package/dist/client.js
CHANGED
|
@@ -3,10 +3,18 @@
|
|
|
3
3
|
* pwa-core/client - Browser-side API client utilities
|
|
4
4
|
*
|
|
5
5
|
* This module has NO Node.js dependencies and is designed for browser use.
|
|
6
|
+
*
|
|
7
|
+
* Supports two auth strategies via a single API surface:
|
|
8
|
+
* - Token (Bearer): initApiClient() — sends Authorization header
|
|
9
|
+
* - Session (Cookie): initSessionApiClient() — sends credentials: 'include'
|
|
10
|
+
*
|
|
11
|
+
* All api* functions (apiCall, apiGet, apiPost, etc.) work with whichever
|
|
12
|
+
* strategy was initialized. The sessionApi* names are deprecated aliases.
|
|
6
13
|
*/
|
|
7
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.ApiError = void 0;
|
|
15
|
+
exports.sessionApiCallWithRetry = exports.sessionApiCallVoid = exports.sessionApiCallSafe = exports.sessionApiDelete = exports.sessionApiPatch = exports.sessionApiPut = exports.sessionApiPost = exports.sessionApiGet = exports.sessionApiCall = exports.ApiError = void 0;
|
|
9
16
|
exports.initApiClient = initApiClient;
|
|
17
|
+
exports.initSessionApiClient = initSessionApiClient;
|
|
10
18
|
exports.getApiErrorMessage = getApiErrorMessage;
|
|
11
19
|
exports.apiCall = apiCall;
|
|
12
20
|
exports.apiGet = apiGet;
|
|
@@ -16,17 +24,8 @@ exports.apiPatch = apiPatch;
|
|
|
16
24
|
exports.apiDelete = apiDelete;
|
|
17
25
|
exports.apiCallVoid = apiCallVoid;
|
|
18
26
|
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
27
|
exports.isRetryableStatus = isRetryableStatus;
|
|
29
|
-
exports.
|
|
28
|
+
exports.apiCallWithRetry = apiCallWithRetry;
|
|
30
29
|
// =============================================================================
|
|
31
30
|
// ApiError Class
|
|
32
31
|
// =============================================================================
|
|
@@ -92,29 +91,35 @@ class ApiError extends Error {
|
|
|
92
91
|
}
|
|
93
92
|
}
|
|
94
93
|
exports.ApiError = ApiError;
|
|
95
|
-
|
|
96
|
-
// Module State
|
|
97
|
-
// =============================================================================
|
|
98
|
-
// Token getter function - set by consuming app
|
|
99
|
-
let getToken = null;
|
|
100
|
-
// Callback for 401 responses (e.g., redirect to login)
|
|
101
|
-
let onUnauthorized = null;
|
|
102
|
-
// Request timeout in milliseconds (default: 30 seconds)
|
|
103
|
-
let requestTimeout = 30000;
|
|
94
|
+
let clientState = null;
|
|
104
95
|
// =============================================================================
|
|
105
96
|
// Internal Helpers
|
|
106
97
|
// =============================================================================
|
|
107
|
-
/**
|
|
108
|
-
function
|
|
109
|
-
|
|
98
|
+
/** Get the active client state or throw */
|
|
99
|
+
function getState() {
|
|
100
|
+
if (!clientState) {
|
|
101
|
+
throw new Error('API client not initialized. Call initApiClient() or initSessionApiClient() first.');
|
|
102
|
+
}
|
|
103
|
+
return clientState;
|
|
104
|
+
}
|
|
105
|
+
/** Prepare headers and fetch options based on auth strategy */
|
|
106
|
+
function prepareFetchOptions(options) {
|
|
107
|
+
const state = getState();
|
|
110
108
|
const headers = {
|
|
111
109
|
'Content-Type': 'application/json',
|
|
112
110
|
...options.headers
|
|
113
111
|
};
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
const fetchOptions = { ...options };
|
|
113
|
+
if (state.strategy === 'token') {
|
|
114
|
+
const token = state.getToken?.();
|
|
115
|
+
if (token) {
|
|
116
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
117
|
+
}
|
|
116
118
|
}
|
|
117
|
-
|
|
119
|
+
else {
|
|
120
|
+
fetchOptions.credentials = 'include';
|
|
121
|
+
}
|
|
122
|
+
return { headers, fetchOptions };
|
|
118
123
|
}
|
|
119
124
|
/** Extract error message from failed response */
|
|
120
125
|
async function extractErrorMessage(response) {
|
|
@@ -126,10 +131,10 @@ async function extractErrorMessage(response) {
|
|
|
126
131
|
return 'Request failed';
|
|
127
132
|
}
|
|
128
133
|
}
|
|
129
|
-
/** Handle 401 unauthorized callback */
|
|
134
|
+
/** Handle 401 unauthorized/unauthenticated callback */
|
|
130
135
|
function handleUnauthorized(status) {
|
|
131
|
-
if (status === 401 && onUnauthorized) {
|
|
132
|
-
onUnauthorized();
|
|
136
|
+
if (status === 401 && clientState?.onUnauthorized) {
|
|
137
|
+
clientState.onUnauthorized();
|
|
133
138
|
}
|
|
134
139
|
}
|
|
135
140
|
/** Handle catch block for throwing API functions */
|
|
@@ -143,10 +148,10 @@ function handleApiError(error) {
|
|
|
143
148
|
throw error;
|
|
144
149
|
}
|
|
145
150
|
// =============================================================================
|
|
146
|
-
//
|
|
151
|
+
// Initialization
|
|
147
152
|
// =============================================================================
|
|
148
153
|
/**
|
|
149
|
-
* Initializes the API client with token
|
|
154
|
+
* Initializes the API client with Bearer token authentication.
|
|
150
155
|
* Call once at application startup.
|
|
151
156
|
* @param config - Client configuration
|
|
152
157
|
* @example
|
|
@@ -157,10 +162,34 @@ function handleApiError(error) {
|
|
|
157
162
|
* });
|
|
158
163
|
*/
|
|
159
164
|
function initApiClient(config) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
clientState = {
|
|
166
|
+
strategy: 'token',
|
|
167
|
+
getToken: config.getToken,
|
|
168
|
+
onUnauthorized: config.onUnauthorized,
|
|
169
|
+
timeout: config.timeout ?? 30000,
|
|
170
|
+
};
|
|
163
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Initializes the session-based API client.
|
|
174
|
+
* Uses cookies (credentials: 'include') instead of Bearer tokens.
|
|
175
|
+
* After calling this, use apiCall/apiGet/apiPost etc. (same functions as token auth).
|
|
176
|
+
* @param config - Client configuration
|
|
177
|
+
* @example
|
|
178
|
+
* initSessionApiClient({
|
|
179
|
+
* onUnauthenticated: () => window.location.href = '/login',
|
|
180
|
+
* timeout: 60000
|
|
181
|
+
* });
|
|
182
|
+
*/
|
|
183
|
+
function initSessionApiClient(config) {
|
|
184
|
+
clientState = {
|
|
185
|
+
strategy: 'session',
|
|
186
|
+
onUnauthorized: config.onUnauthenticated,
|
|
187
|
+
timeout: config.timeout ?? 30000,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// =============================================================================
|
|
191
|
+
// Public API — Works with both token and session strategies
|
|
192
|
+
// =============================================================================
|
|
164
193
|
/**
|
|
165
194
|
* Extract a user-friendly error message from an API error.
|
|
166
195
|
* Use in catch blocks to convert errors to displayable strings.
|
|
@@ -185,6 +214,7 @@ function getApiErrorMessage(error, fallback) {
|
|
|
185
214
|
}
|
|
186
215
|
/**
|
|
187
216
|
* Makes an authenticated API call. Throws ApiError on non-2xx responses.
|
|
217
|
+
* Works with both token (Bearer) and session (cookie) auth strategies.
|
|
188
218
|
* @typeParam T - The expected response data type
|
|
189
219
|
* @param url - The API endpoint URL
|
|
190
220
|
* @param options - Optional fetch options (method, body, headers)
|
|
@@ -194,12 +224,13 @@ function getApiErrorMessage(error, fallback) {
|
|
|
194
224
|
* const user = await apiCall<User>('/api/users/123');
|
|
195
225
|
*/
|
|
196
226
|
async function apiCall(url, options = {}) {
|
|
197
|
-
const
|
|
227
|
+
const state = getState();
|
|
228
|
+
const { headers, fetchOptions } = prepareFetchOptions(options);
|
|
198
229
|
const controller = new AbortController();
|
|
199
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
230
|
+
const timeoutId = setTimeout(() => controller.abort(), state.timeout);
|
|
200
231
|
try {
|
|
201
232
|
const response = await fetch(url, {
|
|
202
|
-
...
|
|
233
|
+
...fetchOptions,
|
|
203
234
|
headers,
|
|
204
235
|
signal: controller.signal
|
|
205
236
|
});
|
|
@@ -304,12 +335,13 @@ async function apiDelete(url) {
|
|
|
304
335
|
* await apiCallVoid('/api/action', { method: 'POST' });
|
|
305
336
|
*/
|
|
306
337
|
async function apiCallVoid(url, options = {}) {
|
|
307
|
-
const
|
|
338
|
+
const state = getState();
|
|
339
|
+
const { headers, fetchOptions } = prepareFetchOptions(options);
|
|
308
340
|
const controller = new AbortController();
|
|
309
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
341
|
+
const timeoutId = setTimeout(() => controller.abort(), state.timeout);
|
|
310
342
|
try {
|
|
311
343
|
const response = await fetch(url, {
|
|
312
|
-
...
|
|
344
|
+
...fetchOptions,
|
|
313
345
|
headers,
|
|
314
346
|
signal: controller.signal
|
|
315
347
|
});
|
|
@@ -343,12 +375,13 @@ async function apiCallVoid(url, options = {}) {
|
|
|
343
375
|
* }
|
|
344
376
|
*/
|
|
345
377
|
async function apiCallSafe(url, options = {}) {
|
|
346
|
-
const
|
|
378
|
+
const state = getState();
|
|
379
|
+
const { headers, fetchOptions } = prepareFetchOptions(options);
|
|
347
380
|
const controller = new AbortController();
|
|
348
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
381
|
+
const timeoutId = setTimeout(() => controller.abort(), state.timeout);
|
|
349
382
|
try {
|
|
350
383
|
const response = await fetch(url, {
|
|
351
|
-
...
|
|
384
|
+
...fetchOptions,
|
|
352
385
|
headers,
|
|
353
386
|
signal: controller.signal
|
|
354
387
|
});
|
|
@@ -372,211 +405,6 @@ async function apiCallSafe(url, options = {}) {
|
|
|
372
405
|
}
|
|
373
406
|
}
|
|
374
407
|
// =============================================================================
|
|
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
408
|
// Retryable Request Helper
|
|
581
409
|
// =============================================================================
|
|
582
410
|
/**
|
|
@@ -596,8 +424,9 @@ function isRetryableStatus(status) {
|
|
|
596
424
|
return false;
|
|
597
425
|
}
|
|
598
426
|
/**
|
|
599
|
-
* Makes
|
|
427
|
+
* Makes an authenticated API call with exponential backoff retry.
|
|
600
428
|
* Retries on retryable status codes (408, 425, 429, 5xx, network errors).
|
|
429
|
+
* Works with both token and session auth strategies.
|
|
601
430
|
* @typeParam T - The expected response data type
|
|
602
431
|
* @param url - The API endpoint URL
|
|
603
432
|
* @param options - Optional fetch options
|
|
@@ -606,16 +435,16 @@ function isRetryableStatus(status) {
|
|
|
606
435
|
* @returns The parsed JSON response
|
|
607
436
|
* @throws ApiError if all retries fail
|
|
608
437
|
* @example
|
|
609
|
-
* const result = await
|
|
438
|
+
* const result = await apiCallWithRetry<AuthResponse>('/api/auth/verify', {
|
|
610
439
|
* method: 'POST',
|
|
611
440
|
* body: JSON.stringify({ token })
|
|
612
441
|
* });
|
|
613
442
|
*/
|
|
614
|
-
async function
|
|
443
|
+
async function apiCallWithRetry(url, options, maxRetries = 3, initialDelayMs = 1000) {
|
|
615
444
|
let lastError;
|
|
616
445
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
617
446
|
try {
|
|
618
|
-
return await
|
|
447
|
+
return await apiCall(url, options);
|
|
619
448
|
}
|
|
620
449
|
catch (error) {
|
|
621
450
|
lastError = error;
|
|
@@ -630,3 +459,24 @@ async function sessionApiCallWithRetry(url, options, maxRetries = 3, initialDela
|
|
|
630
459
|
}
|
|
631
460
|
throw lastError;
|
|
632
461
|
}
|
|
462
|
+
// =============================================================================
|
|
463
|
+
// Deprecated Aliases — Use apiCall/apiGet/apiPost etc. after initSessionApiClient
|
|
464
|
+
// =============================================================================
|
|
465
|
+
/** @deprecated Use apiCall after initSessionApiClient */
|
|
466
|
+
exports.sessionApiCall = apiCall;
|
|
467
|
+
/** @deprecated Use apiGet after initSessionApiClient */
|
|
468
|
+
exports.sessionApiGet = apiGet;
|
|
469
|
+
/** @deprecated Use apiPost after initSessionApiClient */
|
|
470
|
+
exports.sessionApiPost = apiPost;
|
|
471
|
+
/** @deprecated Use apiPut after initSessionApiClient */
|
|
472
|
+
exports.sessionApiPut = apiPut;
|
|
473
|
+
/** @deprecated Use apiPatch after initSessionApiClient */
|
|
474
|
+
exports.sessionApiPatch = apiPatch;
|
|
475
|
+
/** @deprecated Use apiDelete after initSessionApiClient */
|
|
476
|
+
exports.sessionApiDelete = apiDelete;
|
|
477
|
+
/** @deprecated Use apiCallSafe after initSessionApiClient */
|
|
478
|
+
exports.sessionApiCallSafe = apiCallSafe;
|
|
479
|
+
/** @deprecated Use apiCallVoid after initSessionApiClient */
|
|
480
|
+
exports.sessionApiCallVoid = apiCallVoid;
|
|
481
|
+
/** @deprecated Use apiCallWithRetry after initSessionApiClient */
|
|
482
|
+
exports.sessionApiCallWithRetry = apiCallWithRetry;
|