@markwharton/pwa-core 3.5.0 → 4.0.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 +47 -89
- package/dist/client.js +102 -252
- package/dist/server.d.ts +56 -53
- package/dist/server.js +93 -67
- 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;
|
package/dist/server.d.ts
CHANGED
|
@@ -23,12 +23,11 @@ export declare function initAuth(config: {
|
|
|
23
23
|
/**
|
|
24
24
|
* Initializes JWT authentication from environment variables.
|
|
25
25
|
* Reads JWT_SECRET from process.env.
|
|
26
|
-
* @param minLength - Minimum required secret length (default: 32)
|
|
27
26
|
* @throws Error if JWT_SECRET is missing or too short
|
|
28
27
|
* @example
|
|
29
28
|
* initAuthFromEnv(); // Uses process.env.JWT_SECRET
|
|
30
29
|
*/
|
|
31
|
-
export declare function initAuthFromEnv(
|
|
30
|
+
export declare function initAuthFromEnv(): void;
|
|
32
31
|
/**
|
|
33
32
|
* Gets the configured JWT secret.
|
|
34
33
|
* @returns The JWT secret string
|
|
@@ -75,50 +74,31 @@ export declare function generateToken<T extends object>(payload: T, expiresIn?:
|
|
|
75
74
|
* const apiToken = generateLongLivedToken({ machineId: 'server-1' });
|
|
76
75
|
*/
|
|
77
76
|
export declare function generateLongLivedToken<T extends object>(payload: T, expiresInDays?: number): string;
|
|
78
|
-
/**
|
|
79
|
-
* Successful auth result with typed payload.
|
|
80
|
-
*/
|
|
81
|
-
export interface AuthSuccess<T extends BaseJwtPayload> {
|
|
82
|
-
authorized: true;
|
|
83
|
-
payload: T;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Failed auth result with HTTP response.
|
|
87
|
-
*/
|
|
88
|
-
export interface AuthFailure {
|
|
89
|
-
authorized: false;
|
|
90
|
-
response: HttpResponseInit;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Discriminated union for auth results.
|
|
94
|
-
* Use `auth.authorized` to narrow the type.
|
|
95
|
-
*/
|
|
96
|
-
export type AuthResult<T extends BaseJwtPayload> = AuthSuccess<T> | AuthFailure;
|
|
97
77
|
/** Default token expiry string for generateToken (7 days) */
|
|
98
78
|
export declare const DEFAULT_TOKEN_EXPIRY = "7d";
|
|
99
79
|
/** Default token expiry in seconds (7 days = 604800 seconds) */
|
|
100
80
|
export declare const DEFAULT_TOKEN_EXPIRY_SECONDS: number;
|
|
101
81
|
/**
|
|
102
|
-
* Validates auth header and returns typed payload or error
|
|
82
|
+
* Validates auth header and returns typed payload or error result.
|
|
103
83
|
* @typeParam T - The expected payload type (extends BaseJwtPayload)
|
|
104
84
|
* @param authHeader - The Authorization header value
|
|
105
|
-
* @returns
|
|
85
|
+
* @returns Result with payload on success, or error with status on failure
|
|
106
86
|
* @example
|
|
107
87
|
* const auth = requireAuth<UsernameTokenPayload>(request.headers.get('Authorization'));
|
|
108
|
-
* if (!auth.
|
|
109
|
-
* console.log(auth.
|
|
88
|
+
* if (!auth.ok) return resultToResponse(auth);
|
|
89
|
+
* console.log(auth.data.username);
|
|
110
90
|
*/
|
|
111
|
-
export declare function requireAuth<T extends BaseJwtPayload>(authHeader: string | null):
|
|
91
|
+
export declare function requireAuth<T extends BaseJwtPayload>(authHeader: string | null): Result<T>;
|
|
112
92
|
/**
|
|
113
93
|
* Requires admin role. Use with RoleTokenPayload.
|
|
114
94
|
* @param authHeader - The Authorization header value
|
|
115
|
-
* @returns
|
|
95
|
+
* @returns Result with RoleTokenPayload on success, or error with status on failure
|
|
116
96
|
* @example
|
|
117
97
|
* const auth = requireAdmin(request.headers.get('Authorization'));
|
|
118
|
-
* if (!auth.
|
|
98
|
+
* if (!auth.ok) return resultToResponse(auth);
|
|
119
99
|
* // User is admin
|
|
120
100
|
*/
|
|
121
|
-
export declare function requireAdmin(authHeader: string | null):
|
|
101
|
+
export declare function requireAdmin(authHeader: string | null): Result<RoleTokenPayload>;
|
|
122
102
|
/**
|
|
123
103
|
* Extracts API key from the X-API-Key header.
|
|
124
104
|
* @param request - Request object with headers.get() method
|
|
@@ -259,12 +239,14 @@ export declare function initErrorHandling(config?: {
|
|
|
259
239
|
* Initializes error handling from environment variables.
|
|
260
240
|
* Currently a no-op but provides consistent API for future error config
|
|
261
241
|
* (e.g., ERROR_LOG_LEVEL, ERROR_INCLUDE_STACK).
|
|
262
|
-
* @param
|
|
242
|
+
* @param config - Optional config with onError callback invoked when handleFunctionError is called (fire-and-forget)
|
|
263
243
|
* @example
|
|
264
244
|
* initErrorHandlingFromEnv(); // Uses process.env automatically
|
|
265
|
-
* initErrorHandlingFromEnv((op, msg) => sendAlert(op, msg));
|
|
245
|
+
* initErrorHandlingFromEnv({ onError: (op, msg) => sendAlert(op, msg) });
|
|
266
246
|
*/
|
|
267
|
-
export declare function initErrorHandlingFromEnv(
|
|
247
|
+
export declare function initErrorHandlingFromEnv(config?: {
|
|
248
|
+
onError?: ErrorCallback;
|
|
249
|
+
}): void;
|
|
268
250
|
/**
|
|
269
251
|
* Handles unexpected errors safely by logging details and returning a generic message.
|
|
270
252
|
* Use in catch blocks to avoid exposing internal error details to clients.
|
|
@@ -397,15 +379,16 @@ export declare function upsertEntity<T extends {
|
|
|
397
379
|
}>(client: TableClient, entity: T): Promise<void>;
|
|
398
380
|
/**
|
|
399
381
|
* Deletes an entity from Azure Table Storage.
|
|
400
|
-
* Returns true
|
|
382
|
+
* Returns ok(true) if deleted, ok(false) if not found (not an error), err() on real failures.
|
|
401
383
|
* @param client - The TableClient instance
|
|
402
384
|
* @param partitionKey - The partition key
|
|
403
385
|
* @param rowKey - The row key
|
|
404
|
-
* @returns
|
|
386
|
+
* @returns Result with true if deleted, false if not found, or error on failure
|
|
405
387
|
* @example
|
|
406
|
-
* const
|
|
388
|
+
* const result = await deleteEntity(client, 'session', sessionId);
|
|
389
|
+
* if (!result.ok) console.error(result.error);
|
|
407
390
|
*/
|
|
408
|
-
export declare function deleteEntity(client: TableClient, partitionKey: string, rowKey: string): Promise<boolean
|
|
391
|
+
export declare function deleteEntity(client: TableClient, partitionKey: string, rowKey: string): Promise<Result<boolean>>;
|
|
409
392
|
/**
|
|
410
393
|
* Lists all entities in a partition.
|
|
411
394
|
* @typeParam T - The entity type
|
|
@@ -458,7 +441,8 @@ export interface SessionAuthConfig {
|
|
|
458
441
|
allowedDomain?: string;
|
|
459
442
|
/** Emails that get isAdmin=true */
|
|
460
443
|
adminEmails?: string[];
|
|
461
|
-
/**
|
|
444
|
+
/** Additional email validation. Called after built-in domain/admin checks reject.
|
|
445
|
+
* Can only widen access, never narrow it. Supports async for database lookups. */
|
|
462
446
|
isEmailAllowed?: (email: string) => boolean | Promise<boolean>;
|
|
463
447
|
/** Base URL for magic links and SWA preview URL validation */
|
|
464
448
|
appBaseUrl?: string;
|
|
@@ -480,16 +464,25 @@ export declare function initSessionAuth(config: SessionAuthConfig): void;
|
|
|
480
464
|
/**
|
|
481
465
|
* Initializes session auth from environment variables.
|
|
482
466
|
* Reads: SESSION_COOKIE_NAME, APP_BASE_URL, ALLOWED_EMAILS, ALLOWED_DOMAIN, ADMIN_EMAILS.
|
|
483
|
-
*
|
|
484
|
-
*
|
|
467
|
+
* Only callbacks go in the config object — data comes from env vars.
|
|
468
|
+
* Use initSessionAuth() directly for full control.
|
|
469
|
+
* @param config - Required config with sendEmail callback and optional isEmailAllowed
|
|
485
470
|
* @throws Error if sendEmail is not provided
|
|
486
471
|
* @example
|
|
487
|
-
* initSessionAuthFromEnv(
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
472
|
+
* initSessionAuthFromEnv({
|
|
473
|
+
* sendEmail: async (to, magicLink) => {
|
|
474
|
+
* await resend.emails.send({ to, html: `<a href="${magicLink}">Sign In</a>` });
|
|
475
|
+
* return true;
|
|
476
|
+
* },
|
|
477
|
+
* isEmailAllowed: async (email) => lookupInDatabase(email),
|
|
478
|
+
* });
|
|
491
479
|
*/
|
|
492
|
-
export declare function initSessionAuthFromEnv(
|
|
480
|
+
export declare function initSessionAuthFromEnv(config: {
|
|
481
|
+
sendEmail: (to: string, magicLink: string) => Promise<boolean>;
|
|
482
|
+
/** Additional email validation. Called after built-in domain/admin checks reject.
|
|
483
|
+
* Can only widen access, never narrow it. */
|
|
484
|
+
isEmailAllowed?: (email: string) => boolean | Promise<boolean>;
|
|
485
|
+
}): void;
|
|
493
486
|
/**
|
|
494
487
|
* Parses cookies from a request's Cookie header.
|
|
495
488
|
* @param request - Request object with headers.get() method
|
|
@@ -582,37 +575,37 @@ export declare function verifyMagicLink(token: string): Promise<Result<{
|
|
|
582
575
|
* Validates a session cookie and returns the user and session info.
|
|
583
576
|
* Performs sliding window refresh if the session is close to expiry.
|
|
584
577
|
* @param request - Request object with headers.get() method
|
|
585
|
-
* @returns
|
|
578
|
+
* @returns Result with user, session, and optional refreshed cookie
|
|
586
579
|
* @example
|
|
587
580
|
* const result = await validateSession(request);
|
|
588
|
-
* if (!result) return unauthorizedResponse();
|
|
581
|
+
* if (!result.ok) return unauthorizedResponse();
|
|
589
582
|
*/
|
|
590
583
|
export declare function validateSession(request: {
|
|
591
584
|
headers: {
|
|
592
585
|
get(name: string): string | null;
|
|
593
586
|
};
|
|
594
|
-
}): Promise<{
|
|
587
|
+
}): Promise<Result<{
|
|
595
588
|
user: SessionUser;
|
|
596
589
|
session: SessionInfo;
|
|
597
590
|
refreshedCookie?: string;
|
|
598
|
-
}
|
|
591
|
+
}>>;
|
|
599
592
|
/**
|
|
600
593
|
* Convenience function: validates session and returns user with optional refresh headers.
|
|
601
594
|
* @param request - Request object with headers.get() method
|
|
602
|
-
* @returns
|
|
595
|
+
* @returns Result with user and optional Set-Cookie headers
|
|
603
596
|
* @example
|
|
604
597
|
* const result = await getSessionUser(request);
|
|
605
|
-
* if (!result) return
|
|
606
|
-
*
|
|
598
|
+
* if (!result.ok) return resultToResponse(result);
|
|
599
|
+
* const { user, headers } = result.data!;
|
|
607
600
|
*/
|
|
608
601
|
export declare function getSessionUser(request: {
|
|
609
602
|
headers: {
|
|
610
603
|
get(name: string): string | null;
|
|
611
604
|
};
|
|
612
|
-
}): Promise<{
|
|
605
|
+
}): Promise<Result<{
|
|
613
606
|
user: SessionUser;
|
|
614
607
|
headers?: Record<string, string>;
|
|
615
|
-
}
|
|
608
|
+
}>>;
|
|
616
609
|
/**
|
|
617
610
|
* Destroys the current session and returns a logout cookie string.
|
|
618
611
|
* @param request - Request object with headers.get() method
|
|
@@ -703,4 +696,14 @@ export declare function hasKeyVaultReferences(): boolean;
|
|
|
703
696
|
* }
|
|
704
697
|
*/
|
|
705
698
|
export declare function resolveKeyVaultReferences(): Promise<number>;
|
|
699
|
+
/**
|
|
700
|
+
* Convert a failed Result to an HttpResponseInit.
|
|
701
|
+
* Use after checking `!result.ok` to return the error as an HTTP response.
|
|
702
|
+
* @param result - A failed Result (ok=false)
|
|
703
|
+
* @returns HttpResponseInit with the error status and message
|
|
704
|
+
* @example
|
|
705
|
+
* const auth = requireAuth<UsernameTokenPayload>(authHeader);
|
|
706
|
+
* if (!auth.ok) return resultToResponse(auth);
|
|
707
|
+
*/
|
|
708
|
+
export declare function resultToResponse(result: Result<unknown>): HttpResponseInit;
|
|
706
709
|
export { Result, ok, okVoid, err, getErrorMessage, BaseJwtPayload, UserTokenPayload, UsernameTokenPayload, RoleTokenPayload, hasUsername, hasRole, isAdmin, HTTP_STATUS, HttpStatus, ErrorResponse, SessionUser, SessionInfo, MagicLinkRequest, SessionAuthResponse } from './shared';
|
package/dist/server.js
CHANGED
|
@@ -101,6 +101,7 @@ exports.deleteExpiredSessions = deleteExpiredSessions;
|
|
|
101
101
|
exports.deleteExpiredMagicLinks = deleteExpiredMagicLinks;
|
|
102
102
|
exports.hasKeyVaultReferences = hasKeyVaultReferences;
|
|
103
103
|
exports.resolveKeyVaultReferences = resolveKeyVaultReferences;
|
|
104
|
+
exports.resultToResponse = resultToResponse;
|
|
104
105
|
const crypto_1 = require("crypto");
|
|
105
106
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
106
107
|
const data_tables_1 = require("@azure/data-tables");
|
|
@@ -130,15 +131,13 @@ function initAuth(config) {
|
|
|
130
131
|
/**
|
|
131
132
|
* Initializes JWT authentication from environment variables.
|
|
132
133
|
* Reads JWT_SECRET from process.env.
|
|
133
|
-
* @param minLength - Minimum required secret length (default: 32)
|
|
134
134
|
* @throws Error if JWT_SECRET is missing or too short
|
|
135
135
|
* @example
|
|
136
136
|
* initAuthFromEnv(); // Uses process.env.JWT_SECRET
|
|
137
137
|
*/
|
|
138
|
-
function initAuthFromEnv(
|
|
138
|
+
function initAuthFromEnv() {
|
|
139
139
|
initAuth({
|
|
140
140
|
secret: process.env.JWT_SECRET,
|
|
141
|
-
minLength
|
|
142
141
|
});
|
|
143
142
|
}
|
|
144
143
|
/**
|
|
@@ -225,41 +224,41 @@ exports.DEFAULT_TOKEN_EXPIRY_SECONDS = DEFAULT_TOKEN_EXPIRY_DAYS * 24 * 60 * 60;
|
|
|
225
224
|
// Auth Helpers
|
|
226
225
|
// =============================================================================
|
|
227
226
|
/**
|
|
228
|
-
* Validates auth header and returns typed payload or error
|
|
227
|
+
* Validates auth header and returns typed payload or error result.
|
|
229
228
|
* @typeParam T - The expected payload type (extends BaseJwtPayload)
|
|
230
229
|
* @param authHeader - The Authorization header value
|
|
231
|
-
* @returns
|
|
230
|
+
* @returns Result with payload on success, or error with status on failure
|
|
232
231
|
* @example
|
|
233
232
|
* const auth = requireAuth<UsernameTokenPayload>(request.headers.get('Authorization'));
|
|
234
|
-
* if (!auth.
|
|
235
|
-
* console.log(auth.
|
|
233
|
+
* if (!auth.ok) return resultToResponse(auth);
|
|
234
|
+
* console.log(auth.data.username);
|
|
236
235
|
*/
|
|
237
236
|
function requireAuth(authHeader) {
|
|
238
237
|
const token = extractToken(authHeader);
|
|
239
238
|
if (!token) {
|
|
240
|
-
return
|
|
239
|
+
return (0, shared_1.err)('Unauthorized', shared_1.HTTP_STATUS.UNAUTHORIZED);
|
|
241
240
|
}
|
|
242
241
|
const result = validateToken(token);
|
|
243
242
|
if (!result.ok) {
|
|
244
|
-
return
|
|
243
|
+
return (0, shared_1.err)(result.error ?? 'Unauthorized', shared_1.HTTP_STATUS.UNAUTHORIZED);
|
|
245
244
|
}
|
|
246
|
-
return
|
|
245
|
+
return (0, shared_1.ok)(result.data);
|
|
247
246
|
}
|
|
248
247
|
/**
|
|
249
248
|
* Requires admin role. Use with RoleTokenPayload.
|
|
250
249
|
* @param authHeader - The Authorization header value
|
|
251
|
-
* @returns
|
|
250
|
+
* @returns Result with RoleTokenPayload on success, or error with status on failure
|
|
252
251
|
* @example
|
|
253
252
|
* const auth = requireAdmin(request.headers.get('Authorization'));
|
|
254
|
-
* if (!auth.
|
|
253
|
+
* if (!auth.ok) return resultToResponse(auth);
|
|
255
254
|
* // User is admin
|
|
256
255
|
*/
|
|
257
256
|
function requireAdmin(authHeader) {
|
|
258
257
|
const auth = requireAuth(authHeader);
|
|
259
|
-
if (!auth.
|
|
258
|
+
if (!auth.ok)
|
|
260
259
|
return auth;
|
|
261
|
-
if (!(0, shared_1.isAdmin)(auth.
|
|
262
|
-
return
|
|
260
|
+
if (!(0, shared_1.isAdmin)(auth.data)) {
|
|
261
|
+
return (0, shared_1.err)('Admin access required', shared_1.HTTP_STATUS.FORBIDDEN);
|
|
263
262
|
}
|
|
264
263
|
return auth;
|
|
265
264
|
}
|
|
@@ -459,14 +458,14 @@ function initErrorHandling(config = {}) {
|
|
|
459
458
|
* Initializes error handling from environment variables.
|
|
460
459
|
* Currently a no-op but provides consistent API for future error config
|
|
461
460
|
* (e.g., ERROR_LOG_LEVEL, ERROR_INCLUDE_STACK).
|
|
462
|
-
* @param
|
|
461
|
+
* @param config - Optional config with onError callback invoked when handleFunctionError is called (fire-and-forget)
|
|
463
462
|
* @example
|
|
464
463
|
* initErrorHandlingFromEnv(); // Uses process.env automatically
|
|
465
|
-
* initErrorHandlingFromEnv((op, msg) => sendAlert(op, msg));
|
|
464
|
+
* initErrorHandlingFromEnv({ onError: (op, msg) => sendAlert(op, msg) });
|
|
466
465
|
*/
|
|
467
|
-
function initErrorHandlingFromEnv(
|
|
466
|
+
function initErrorHandlingFromEnv(config) {
|
|
468
467
|
// Future: read ERROR_LOG_LEVEL, ERROR_INCLUDE_STACK, etc. from process.env
|
|
469
|
-
initErrorHandling({ callback });
|
|
468
|
+
initErrorHandling({ callback: config?.onError });
|
|
470
469
|
}
|
|
471
470
|
/**
|
|
472
471
|
* Handles unexpected errors safely by logging details and returning a generic message.
|
|
@@ -695,21 +694,25 @@ async function upsertEntity(client, entity) {
|
|
|
695
694
|
}
|
|
696
695
|
/**
|
|
697
696
|
* Deletes an entity from Azure Table Storage.
|
|
698
|
-
* Returns true
|
|
697
|
+
* Returns ok(true) if deleted, ok(false) if not found (not an error), err() on real failures.
|
|
699
698
|
* @param client - The TableClient instance
|
|
700
699
|
* @param partitionKey - The partition key
|
|
701
700
|
* @param rowKey - The row key
|
|
702
|
-
* @returns
|
|
701
|
+
* @returns Result with true if deleted, false if not found, or error on failure
|
|
703
702
|
* @example
|
|
704
|
-
* const
|
|
703
|
+
* const result = await deleteEntity(client, 'session', sessionId);
|
|
704
|
+
* if (!result.ok) console.error(result.error);
|
|
705
705
|
*/
|
|
706
706
|
async function deleteEntity(client, partitionKey, rowKey) {
|
|
707
707
|
try {
|
|
708
708
|
await client.deleteEntity(partitionKey, rowKey);
|
|
709
|
-
return true;
|
|
709
|
+
return (0, shared_1.ok)(true);
|
|
710
710
|
}
|
|
711
|
-
catch {
|
|
712
|
-
|
|
711
|
+
catch (error) {
|
|
712
|
+
if (isNotFoundError(error)) {
|
|
713
|
+
return (0, shared_1.ok)(false);
|
|
714
|
+
}
|
|
715
|
+
return (0, shared_1.err)((0, shared_1.getErrorMessage)(error, 'Delete failed'));
|
|
713
716
|
}
|
|
714
717
|
}
|
|
715
718
|
/**
|
|
@@ -788,16 +791,20 @@ function initSessionAuth(config) {
|
|
|
788
791
|
/**
|
|
789
792
|
* Initializes session auth from environment variables.
|
|
790
793
|
* Reads: SESSION_COOKIE_NAME, APP_BASE_URL, ALLOWED_EMAILS, ALLOWED_DOMAIN, ADMIN_EMAILS.
|
|
791
|
-
*
|
|
792
|
-
*
|
|
794
|
+
* Only callbacks go in the config object — data comes from env vars.
|
|
795
|
+
* Use initSessionAuth() directly for full control.
|
|
796
|
+
* @param config - Required config with sendEmail callback and optional isEmailAllowed
|
|
793
797
|
* @throws Error if sendEmail is not provided
|
|
794
798
|
* @example
|
|
795
|
-
* initSessionAuthFromEnv(
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
+
* initSessionAuthFromEnv({
|
|
800
|
+
* sendEmail: async (to, magicLink) => {
|
|
801
|
+
* await resend.emails.send({ to, html: `<a href="${magicLink}">Sign In</a>` });
|
|
802
|
+
* return true;
|
|
803
|
+
* },
|
|
804
|
+
* isEmailAllowed: async (email) => lookupInDatabase(email),
|
|
805
|
+
* });
|
|
799
806
|
*/
|
|
800
|
-
function initSessionAuthFromEnv(
|
|
807
|
+
function initSessionAuthFromEnv(config) {
|
|
801
808
|
const allowedEmailsStr = process.env.ALLOWED_EMAILS;
|
|
802
809
|
const adminEmailsStr = process.env.ADMIN_EMAILS;
|
|
803
810
|
initSessionAuth({
|
|
@@ -810,8 +817,8 @@ function initSessionAuthFromEnv(sendEmail, overrides) {
|
|
|
810
817
|
adminEmails: adminEmailsStr
|
|
811
818
|
? adminEmailsStr.split(',').map(e => e.trim().toLowerCase())
|
|
812
819
|
: undefined,
|
|
813
|
-
|
|
814
|
-
sendEmail
|
|
820
|
+
isEmailAllowed: config.isEmailAllowed,
|
|
821
|
+
sendEmail: config.sendEmail,
|
|
815
822
|
});
|
|
816
823
|
}
|
|
817
824
|
/**
|
|
@@ -1007,10 +1014,11 @@ async function createMagicLink(email, request) {
|
|
|
1007
1014
|
if (!isValidEmail(normalizedEmail)) {
|
|
1008
1015
|
return (0, shared_1.err)('Valid email required', shared_1.HTTP_STATUS.BAD_REQUEST);
|
|
1009
1016
|
}
|
|
1010
|
-
// Check allowlist (custom
|
|
1011
|
-
const emailAllowed =
|
|
1012
|
-
|
|
1013
|
-
|
|
1017
|
+
// Check allowlist (built-in first, custom extends — can only widen access)
|
|
1018
|
+
const emailAllowed = isEmailAllowed(normalizedEmail)
|
|
1019
|
+
|| (config.isEmailAllowed
|
|
1020
|
+
? await config.isEmailAllowed(normalizedEmail)
|
|
1021
|
+
: false);
|
|
1014
1022
|
if (!emailAllowed) {
|
|
1015
1023
|
return (0, shared_1.err)('Email not allowed', shared_1.HTTP_STATUS.FORBIDDEN);
|
|
1016
1024
|
}
|
|
@@ -1123,10 +1131,10 @@ async function verifyMagicLink(token) {
|
|
|
1123
1131
|
* Validates a session cookie and returns the user and session info.
|
|
1124
1132
|
* Performs sliding window refresh if the session is close to expiry.
|
|
1125
1133
|
* @param request - Request object with headers.get() method
|
|
1126
|
-
* @returns
|
|
1134
|
+
* @returns Result with user, session, and optional refreshed cookie
|
|
1127
1135
|
* @example
|
|
1128
1136
|
* const result = await validateSession(request);
|
|
1129
|
-
* if (!result) return unauthorizedResponse();
|
|
1137
|
+
* if (!result.ok) return unauthorizedResponse();
|
|
1130
1138
|
*/
|
|
1131
1139
|
async function validateSession(request) {
|
|
1132
1140
|
const config = getSessionAuthConfig();
|
|
@@ -1134,20 +1142,20 @@ async function validateSession(request) {
|
|
|
1134
1142
|
const cookies = parseCookies(request);
|
|
1135
1143
|
const sessionId = cookies[cookieName];
|
|
1136
1144
|
if (!sessionId)
|
|
1137
|
-
return
|
|
1145
|
+
return (0, shared_1.err)('No session cookie', shared_1.HTTP_STATUS.UNAUTHORIZED);
|
|
1138
1146
|
try {
|
|
1139
1147
|
const sessionsClient = await getTableClient(SESSIONS_TABLE);
|
|
1140
1148
|
const sessionEntity = await getEntityIfExists(sessionsClient, SESSION_PARTITION, sessionId);
|
|
1141
1149
|
if (!sessionEntity)
|
|
1142
|
-
return
|
|
1150
|
+
return (0, shared_1.err)('Invalid session', shared_1.HTTP_STATUS.UNAUTHORIZED);
|
|
1143
1151
|
// Check expiry
|
|
1144
1152
|
if (new Date(sessionEntity.expiresAt) < new Date())
|
|
1145
|
-
return
|
|
1153
|
+
return (0, shared_1.err)('Session expired', shared_1.HTTP_STATUS.UNAUTHORIZED);
|
|
1146
1154
|
// Get user
|
|
1147
1155
|
const usersClient = await getTableClient(USERS_TABLE);
|
|
1148
1156
|
const userEntity = await getEntityIfExists(usersClient, USER_PARTITION, sessionEntity.email.toLowerCase());
|
|
1149
1157
|
if (!userEntity)
|
|
1150
|
-
return
|
|
1158
|
+
return (0, shared_1.err)('User not found', shared_1.HTTP_STATUS.UNAUTHORIZED);
|
|
1151
1159
|
const user = {
|
|
1152
1160
|
id: userEntity.id,
|
|
1153
1161
|
email: userEntity.email,
|
|
@@ -1179,30 +1187,30 @@ async function validateSession(request) {
|
|
|
1179
1187
|
await upsertEntity(sessionsClient, updatedSession);
|
|
1180
1188
|
refreshedCookie = createSessionCookie(sessionId);
|
|
1181
1189
|
}
|
|
1182
|
-
return { user, session, refreshedCookie };
|
|
1190
|
+
return (0, shared_1.ok)({ user, session, refreshedCookie });
|
|
1183
1191
|
}
|
|
1184
1192
|
catch {
|
|
1185
|
-
return
|
|
1193
|
+
return (0, shared_1.err)('Session validation failed', shared_1.HTTP_STATUS.UNAUTHORIZED);
|
|
1186
1194
|
}
|
|
1187
1195
|
}
|
|
1188
1196
|
// --- Session Operations ---
|
|
1189
1197
|
/**
|
|
1190
1198
|
* Convenience function: validates session and returns user with optional refresh headers.
|
|
1191
1199
|
* @param request - Request object with headers.get() method
|
|
1192
|
-
* @returns
|
|
1200
|
+
* @returns Result with user and optional Set-Cookie headers
|
|
1193
1201
|
* @example
|
|
1194
1202
|
* const result = await getSessionUser(request);
|
|
1195
|
-
* if (!result) return
|
|
1196
|
-
*
|
|
1203
|
+
* if (!result.ok) return resultToResponse(result);
|
|
1204
|
+
* const { user, headers } = result.data!;
|
|
1197
1205
|
*/
|
|
1198
1206
|
async function getSessionUser(request) {
|
|
1199
1207
|
const result = await validateSession(request);
|
|
1200
|
-
if (!result)
|
|
1201
|
-
return
|
|
1202
|
-
const headers = result.refreshedCookie
|
|
1203
|
-
? { 'Set-Cookie': result.refreshedCookie }
|
|
1208
|
+
if (!result.ok)
|
|
1209
|
+
return result;
|
|
1210
|
+
const headers = result.data.refreshedCookie
|
|
1211
|
+
? { 'Set-Cookie': result.data.refreshedCookie }
|
|
1204
1212
|
: undefined;
|
|
1205
|
-
return { user: result.user, headers };
|
|
1213
|
+
return (0, shared_1.ok)({ user: result.data.user, headers });
|
|
1206
1214
|
}
|
|
1207
1215
|
/**
|
|
1208
1216
|
* Destroys the current session and returns a logout cookie string.
|
|
@@ -1239,13 +1247,13 @@ async function destroySession(request) {
|
|
|
1239
1247
|
function withSessionAuth(handler) {
|
|
1240
1248
|
return async (request, context) => {
|
|
1241
1249
|
const result = await getSessionUser(request);
|
|
1242
|
-
if (!result) {
|
|
1250
|
+
if (!result.ok) {
|
|
1243
1251
|
return unauthorizedResponse('Not authenticated');
|
|
1244
1252
|
}
|
|
1245
|
-
const response = await handler(request, context, result);
|
|
1253
|
+
const response = await handler(request, context, result.data);
|
|
1246
1254
|
// Merge refresh cookie headers
|
|
1247
|
-
if (result.headers) {
|
|
1248
|
-
response.headers = { ...result.headers, ...response.headers };
|
|
1255
|
+
if (result.data.headers) {
|
|
1256
|
+
response.headers = { ...result.data.headers, ...response.headers };
|
|
1249
1257
|
}
|
|
1250
1258
|
return response;
|
|
1251
1259
|
};
|
|
@@ -1265,16 +1273,16 @@ function withSessionAuth(handler) {
|
|
|
1265
1273
|
function withSessionAdminAuth(handler) {
|
|
1266
1274
|
return async (request, context) => {
|
|
1267
1275
|
const result = await getSessionUser(request);
|
|
1268
|
-
if (!result) {
|
|
1276
|
+
if (!result.ok) {
|
|
1269
1277
|
return unauthorizedResponse('Not authenticated');
|
|
1270
1278
|
}
|
|
1271
|
-
if (!result.user.isAdmin) {
|
|
1279
|
+
if (!result.data.user.isAdmin) {
|
|
1272
1280
|
return forbiddenResponse('Admin access required');
|
|
1273
1281
|
}
|
|
1274
|
-
const response = await handler(request, context, result);
|
|
1282
|
+
const response = await handler(request, context, result.data);
|
|
1275
1283
|
// Merge refresh cookie headers
|
|
1276
|
-
if (result.headers) {
|
|
1277
|
-
response.headers = { ...result.headers, ...response.headers };
|
|
1284
|
+
if (result.data.headers) {
|
|
1285
|
+
response.headers = { ...result.data.headers, ...response.headers };
|
|
1278
1286
|
}
|
|
1279
1287
|
return response;
|
|
1280
1288
|
};
|
|
@@ -1295,8 +1303,8 @@ async function deleteExpiredSessions() {
|
|
|
1295
1303
|
let deleted = 0;
|
|
1296
1304
|
for (const session of sessions) {
|
|
1297
1305
|
if (new Date(session.expiresAt) < now) {
|
|
1298
|
-
const
|
|
1299
|
-
if (
|
|
1306
|
+
const result = await deleteEntity(client, SESSION_PARTITION, session.rowKey);
|
|
1307
|
+
if (result.ok && result.data)
|
|
1300
1308
|
deleted++;
|
|
1301
1309
|
}
|
|
1302
1310
|
}
|
|
@@ -1317,8 +1325,8 @@ async function deleteExpiredMagicLinks() {
|
|
|
1317
1325
|
let deleted = 0;
|
|
1318
1326
|
for (const link of links) {
|
|
1319
1327
|
if (new Date(link.expiresAt) < now || link.used) {
|
|
1320
|
-
const
|
|
1321
|
-
if (
|
|
1328
|
+
const result = await deleteEntity(client, MAGICLINK_PARTITION, link.rowKey);
|
|
1329
|
+
if (result.ok && result.data)
|
|
1322
1330
|
deleted++;
|
|
1323
1331
|
}
|
|
1324
1332
|
}
|
|
@@ -1432,6 +1440,24 @@ async function resolveKeyVaultReferences() {
|
|
|
1432
1440
|
return resolved;
|
|
1433
1441
|
}
|
|
1434
1442
|
// =============================================================================
|
|
1443
|
+
// Result Conversion Helper
|
|
1444
|
+
// =============================================================================
|
|
1445
|
+
/**
|
|
1446
|
+
* Convert a failed Result to an HttpResponseInit.
|
|
1447
|
+
* Use after checking `!result.ok` to return the error as an HTTP response.
|
|
1448
|
+
* @param result - A failed Result (ok=false)
|
|
1449
|
+
* @returns HttpResponseInit with the error status and message
|
|
1450
|
+
* @example
|
|
1451
|
+
* const auth = requireAuth<UsernameTokenPayload>(authHeader);
|
|
1452
|
+
* if (!auth.ok) return resultToResponse(auth);
|
|
1453
|
+
*/
|
|
1454
|
+
function resultToResponse(result) {
|
|
1455
|
+
return {
|
|
1456
|
+
status: result.status ?? shared_1.HTTP_STATUS.INTERNAL_ERROR,
|
|
1457
|
+
jsonBody: { error: result.error ?? 'Unknown error' }
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
// =============================================================================
|
|
1435
1461
|
// Re-exports from shared (for convenience)
|
|
1436
1462
|
// =============================================================================
|
|
1437
1463
|
var shared_2 = require("./shared");
|
package/dist/shared.d.ts
CHANGED
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
* { ok: false, error: 'Token expired' }
|
|
16
16
|
*
|
|
17
17
|
* // With status code (HTTP/push operations)
|
|
18
|
-
* { ok: false, error: 'Subscription expired',
|
|
18
|
+
* { ok: false, error: 'Subscription expired', status: 410 }
|
|
19
19
|
*/
|
|
20
20
|
export interface Result<T> {
|
|
21
21
|
ok: boolean;
|
|
22
22
|
data?: T;
|
|
23
23
|
error?: string;
|
|
24
|
-
|
|
24
|
+
status?: number;
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Creates a success result with data.
|
|
@@ -41,13 +41,13 @@ export declare function okVoid(): Result<void>;
|
|
|
41
41
|
/**
|
|
42
42
|
* Creates a failure result with an error message.
|
|
43
43
|
* @param error - The error message
|
|
44
|
-
* @param
|
|
44
|
+
* @param status - Optional HTTP status code for API/push operations
|
|
45
45
|
* @returns A Result with ok=false and the error details
|
|
46
46
|
* @example
|
|
47
47
|
* return err('Token expired');
|
|
48
48
|
* return err('Subscription gone', 410);
|
|
49
49
|
*/
|
|
50
|
-
export declare function err<T>(error: string,
|
|
50
|
+
export declare function err<T = never>(error: string, status?: number): Result<T>;
|
|
51
51
|
/**
|
|
52
52
|
* Base JWT payload - all tokens include these fields
|
|
53
53
|
* Projects extend this with their specific fields
|
package/dist/shared.js
CHANGED
|
@@ -35,15 +35,15 @@ function okVoid() {
|
|
|
35
35
|
/**
|
|
36
36
|
* Creates a failure result with an error message.
|
|
37
37
|
* @param error - The error message
|
|
38
|
-
* @param
|
|
38
|
+
* @param status - Optional HTTP status code for API/push operations
|
|
39
39
|
* @returns A Result with ok=false and the error details
|
|
40
40
|
* @example
|
|
41
41
|
* return err('Token expired');
|
|
42
42
|
* return err('Subscription gone', 410);
|
|
43
43
|
*/
|
|
44
|
-
function err(error,
|
|
45
|
-
return
|
|
46
|
-
? { ok: false, error,
|
|
44
|
+
function err(error, status) {
|
|
45
|
+
return status !== undefined
|
|
46
|
+
? { ok: false, error, status }
|
|
47
47
|
: { ok: false, error };
|
|
48
48
|
}
|
|
49
49
|
// =============================================================================
|