@markwharton/pwa-core 1.5.0 → 1.7.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/__tests__/client/api.test.js +35 -0
- package/dist/__tests__/client/apiError.test.js +33 -0
- package/dist/__tests__/server/auth/token.test.js +87 -0
- package/dist/__tests__/shared/http/status.test.js +2 -0
- package/dist/client/api.d.ts +6 -3
- package/dist/client/api.js +57 -24
- package/dist/client/apiError.d.ts +10 -0
- package/dist/client/apiError.js +14 -0
- package/dist/server/auth/index.d.ts +2 -1
- package/dist/server/auth/index.js +5 -1
- package/dist/server/auth/token.d.ts +46 -0
- package/dist/server/auth/token.js +54 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +5 -1
- package/dist/shared/http/status.d.ts +2 -0
- package/dist/shared/http/status.js +2 -0
- package/package.json +1 -1
|
@@ -69,6 +69,19 @@ global.fetch = mockFetch;
|
|
|
69
69
|
await (0, vitest_1.expect)(apiCall('/api/test')).rejects.toThrow();
|
|
70
70
|
(0, vitest_1.expect)(onUnauthorized).toHaveBeenCalled();
|
|
71
71
|
});
|
|
72
|
+
(0, vitest_1.it)('sets custom timeout', async () => {
|
|
73
|
+
const { initApiClient, apiCall } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
74
|
+
initApiClient({ getToken: () => 'token', timeout: 5000 });
|
|
75
|
+
mockFetch.mockResolvedValue({
|
|
76
|
+
ok: true,
|
|
77
|
+
text: () => Promise.resolve('{}')
|
|
78
|
+
});
|
|
79
|
+
await apiCall('/api/test');
|
|
80
|
+
// Verify signal was passed to fetch
|
|
81
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('/api/test', vitest_1.expect.objectContaining({
|
|
82
|
+
signal: vitest_1.expect.any(AbortSignal)
|
|
83
|
+
}));
|
|
84
|
+
});
|
|
72
85
|
});
|
|
73
86
|
(0, vitest_1.describe)('apiCall', () => {
|
|
74
87
|
(0, vitest_1.it)('makes authenticated request', async () => {
|
|
@@ -149,6 +162,17 @@ global.fetch = mockFetch;
|
|
|
149
162
|
message: 'Request failed'
|
|
150
163
|
});
|
|
151
164
|
});
|
|
165
|
+
(0, vitest_1.it)('handles request timeout', async () => {
|
|
166
|
+
const { initApiClient, apiCall } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
167
|
+
initApiClient({ getToken: () => 'token' });
|
|
168
|
+
const abortError = new Error('Aborted');
|
|
169
|
+
abortError.name = 'AbortError';
|
|
170
|
+
mockFetch.mockRejectedValue(abortError);
|
|
171
|
+
await (0, vitest_1.expect)(apiCall('/api/test')).rejects.toMatchObject({
|
|
172
|
+
status: 0,
|
|
173
|
+
message: 'Request timeout'
|
|
174
|
+
});
|
|
175
|
+
});
|
|
152
176
|
});
|
|
153
177
|
(0, vitest_1.describe)('HTTP method helpers', () => {
|
|
154
178
|
(0, vitest_1.beforeEach)(async () => {
|
|
@@ -330,5 +354,16 @@ global.fetch = mockFetch;
|
|
|
330
354
|
(0, vitest_1.expect)(result.ok).toBe(false);
|
|
331
355
|
(0, vitest_1.expect)(result.error).toBe('Request failed');
|
|
332
356
|
});
|
|
357
|
+
(0, vitest_1.it)('handles request timeout', async () => {
|
|
358
|
+
const { initApiClient, apiCallSafe } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
359
|
+
initApiClient({ getToken: () => 'token' });
|
|
360
|
+
const abortError = new Error('Aborted');
|
|
361
|
+
abortError.name = 'AbortError';
|
|
362
|
+
mockFetch.mockRejectedValue(abortError);
|
|
363
|
+
const result = await apiCallSafe('/api/test');
|
|
364
|
+
(0, vitest_1.expect)(result.ok).toBe(false);
|
|
365
|
+
(0, vitest_1.expect)(result.status).toBe(0);
|
|
366
|
+
(0, vitest_1.expect)(result.error).toBe('Request timeout');
|
|
367
|
+
});
|
|
333
368
|
});
|
|
334
369
|
});
|
|
@@ -55,4 +55,37 @@ const apiError_1 = require("../../client/apiError");
|
|
|
55
55
|
(0, vitest_1.expect)(new apiError_1.ApiError(500, 'Server error').isBadRequest()).toBe(false);
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
|
+
(0, vitest_1.describe)('isClientError', () => {
|
|
59
|
+
(0, vitest_1.it)('returns true for 4xx statuses', () => {
|
|
60
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(400, 'Bad request').isClientError()).toBe(true);
|
|
61
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(401, 'Unauthorized').isClientError()).toBe(true);
|
|
62
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(403, 'Forbidden').isClientError()).toBe(true);
|
|
63
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(404, 'Not found').isClientError()).toBe(true);
|
|
64
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(422, 'Unprocessable').isClientError()).toBe(true);
|
|
65
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(429, 'Too many requests').isClientError()).toBe(true);
|
|
66
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(499, 'Client closed').isClientError()).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
(0, vitest_1.it)('returns false for non-4xx statuses', () => {
|
|
69
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(200, 'OK').isClientError()).toBe(false);
|
|
70
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(301, 'Moved').isClientError()).toBe(false);
|
|
71
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(500, 'Server error').isClientError()).toBe(false);
|
|
72
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(0, 'Network error').isClientError()).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
(0, vitest_1.describe)('isServerError', () => {
|
|
76
|
+
(0, vitest_1.it)('returns true for 5xx statuses', () => {
|
|
77
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(500, 'Internal server error').isServerError()).toBe(true);
|
|
78
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(501, 'Not implemented').isServerError()).toBe(true);
|
|
79
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(502, 'Bad gateway').isServerError()).toBe(true);
|
|
80
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(503, 'Service unavailable').isServerError()).toBe(true);
|
|
81
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(504, 'Gateway timeout').isServerError()).toBe(true);
|
|
82
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(599, 'Network timeout').isServerError()).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
(0, vitest_1.it)('returns false for non-5xx statuses', () => {
|
|
85
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(200, 'OK').isServerError()).toBe(false);
|
|
86
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(400, 'Bad request').isServerError()).toBe(false);
|
|
87
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(404, 'Not found').isServerError()).toBe(false);
|
|
88
|
+
(0, vitest_1.expect)(new apiError_1.ApiError(0, 'Network error').isServerError()).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
58
91
|
});
|
|
@@ -209,4 +209,91 @@ const token_1 = require("../../../server/auth/token");
|
|
|
209
209
|
(0, vitest_1.expect)(expiresInDays).toBe(365);
|
|
210
210
|
});
|
|
211
211
|
});
|
|
212
|
+
(0, vitest_1.describe)('constants', () => {
|
|
213
|
+
(0, vitest_1.it)('DEFAULT_TOKEN_EXPIRY is 7d', () => {
|
|
214
|
+
(0, vitest_1.expect)(token_1.DEFAULT_TOKEN_EXPIRY).toBe('7d');
|
|
215
|
+
});
|
|
216
|
+
(0, vitest_1.it)('DEFAULT_TOKEN_EXPIRY_SECONDS is 7 days in seconds', () => {
|
|
217
|
+
(0, vitest_1.expect)(token_1.DEFAULT_TOKEN_EXPIRY_SECONDS).toBe(7 * 24 * 60 * 60);
|
|
218
|
+
(0, vitest_1.expect)(token_1.DEFAULT_TOKEN_EXPIRY_SECONDS).toBe(604800);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
(0, vitest_1.describe)('requireAuth', () => {
|
|
222
|
+
(0, vitest_1.it)('returns failure when no auth header', async () => {
|
|
223
|
+
const { initAuth, requireAuth } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
224
|
+
initAuth(validSecret);
|
|
225
|
+
const result = requireAuth(null);
|
|
226
|
+
(0, vitest_1.expect)(result.authorized).toBe(false);
|
|
227
|
+
if (!result.authorized) {
|
|
228
|
+
(0, vitest_1.expect)(result.response.status).toBe(401);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
(0, vitest_1.it)('returns failure when invalid token', async () => {
|
|
232
|
+
const { initAuth, requireAuth } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
233
|
+
initAuth(validSecret);
|
|
234
|
+
const result = requireAuth('Bearer invalid-token');
|
|
235
|
+
(0, vitest_1.expect)(result.authorized).toBe(false);
|
|
236
|
+
if (!result.authorized) {
|
|
237
|
+
(0, vitest_1.expect)(result.response.status).toBe(401);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
(0, vitest_1.it)('returns failure for expired token', async () => {
|
|
241
|
+
const { initAuth, requireAuth } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
242
|
+
initAuth(validSecret);
|
|
243
|
+
const expiredToken = jsonwebtoken_1.default.sign({ username: 'test' }, validSecret, { expiresIn: '-1s' });
|
|
244
|
+
const result = requireAuth(`Bearer ${expiredToken}`);
|
|
245
|
+
(0, vitest_1.expect)(result.authorized).toBe(false);
|
|
246
|
+
});
|
|
247
|
+
(0, vitest_1.it)('returns success with typed payload', async () => {
|
|
248
|
+
const { initAuth, requireAuth, generateToken } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
249
|
+
initAuth(validSecret);
|
|
250
|
+
const token = generateToken({ username: 'testuser' });
|
|
251
|
+
const result = requireAuth(`Bearer ${token}`);
|
|
252
|
+
(0, vitest_1.expect)(result.authorized).toBe(true);
|
|
253
|
+
if (result.authorized) {
|
|
254
|
+
(0, vitest_1.expect)(result.payload.username).toBe('testuser');
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
(0, vitest_1.describe)('requireAdmin', () => {
|
|
259
|
+
(0, vitest_1.it)('returns failure for non-admin user', async () => {
|
|
260
|
+
const { initAuth, requireAdmin, generateToken } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
261
|
+
initAuth(validSecret);
|
|
262
|
+
const payload = {
|
|
263
|
+
authenticated: true,
|
|
264
|
+
tokenType: 'user',
|
|
265
|
+
role: 'viewer'
|
|
266
|
+
};
|
|
267
|
+
const token = generateToken(payload);
|
|
268
|
+
const result = requireAdmin(`Bearer ${token}`);
|
|
269
|
+
(0, vitest_1.expect)(result.authorized).toBe(false);
|
|
270
|
+
if (!result.authorized) {
|
|
271
|
+
(0, vitest_1.expect)(result.response.status).toBe(403);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
(0, vitest_1.it)('returns success for admin user', async () => {
|
|
275
|
+
const { initAuth, requireAdmin, generateToken } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
276
|
+
initAuth(validSecret);
|
|
277
|
+
const payload = {
|
|
278
|
+
authenticated: true,
|
|
279
|
+
tokenType: 'user',
|
|
280
|
+
role: 'admin'
|
|
281
|
+
};
|
|
282
|
+
const token = generateToken(payload);
|
|
283
|
+
const result = requireAdmin(`Bearer ${token}`);
|
|
284
|
+
(0, vitest_1.expect)(result.authorized).toBe(true);
|
|
285
|
+
if (result.authorized) {
|
|
286
|
+
(0, vitest_1.expect)(result.payload.role).toBe('admin');
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
(0, vitest_1.it)('returns failure when no auth header', async () => {
|
|
290
|
+
const { initAuth, requireAdmin } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
291
|
+
initAuth(validSecret);
|
|
292
|
+
const result = requireAdmin(null);
|
|
293
|
+
(0, vitest_1.expect)(result.authorized).toBe(false);
|
|
294
|
+
if (!result.authorized) {
|
|
295
|
+
(0, vitest_1.expect)(result.response.status).toBe(401);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
});
|
|
212
299
|
});
|
|
@@ -13,6 +13,8 @@ const status_1 = require("../../../shared/http/status");
|
|
|
13
13
|
(0, vitest_1.expect)(status_1.HTTP_STATUS.NOT_FOUND).toBe(404);
|
|
14
14
|
(0, vitest_1.expect)(status_1.HTTP_STATUS.CONFLICT).toBe(409);
|
|
15
15
|
(0, vitest_1.expect)(status_1.HTTP_STATUS.GONE).toBe(410);
|
|
16
|
+
(0, vitest_1.expect)(status_1.HTTP_STATUS.UNPROCESSABLE_ENTITY).toBe(422);
|
|
17
|
+
(0, vitest_1.expect)(status_1.HTTP_STATUS.TOO_MANY_REQUESTS).toBe(429);
|
|
16
18
|
(0, vitest_1.expect)(status_1.HTTP_STATUS.INTERNAL_ERROR).toBe(500);
|
|
17
19
|
(0, vitest_1.expect)(status_1.HTTP_STATUS.SERVICE_UNAVAILABLE).toBe(503);
|
|
18
20
|
});
|
package/dist/client/api.d.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { ApiResponse } from './types';
|
|
2
2
|
/**
|
|
3
|
-
* Initializes the API client with token retrieval and optional
|
|
3
|
+
* Initializes the API client with token retrieval and optional configuration.
|
|
4
4
|
* Call once at application startup.
|
|
5
5
|
* @param config - Client configuration
|
|
6
6
|
* @param config.getToken - Function that returns the current auth token (or null)
|
|
7
7
|
* @param config.onUnauthorized - Optional callback for 401 responses (e.g., redirect to login)
|
|
8
|
+
* @param config.timeout - Optional request timeout in milliseconds (default: 30000)
|
|
8
9
|
* @example
|
|
9
10
|
* initApiClient({
|
|
10
11
|
* getToken: () => localStorage.getItem('token'),
|
|
11
|
-
* onUnauthorized: () => window.location.href = '/login'
|
|
12
|
+
* onUnauthorized: () => window.location.href = '/login',
|
|
13
|
+
* timeout: 60000 // 60 seconds
|
|
12
14
|
* });
|
|
13
15
|
*/
|
|
14
16
|
export declare function initApiClient(config: {
|
|
15
17
|
getToken: () => string | null;
|
|
16
18
|
onUnauthorized?: () => void;
|
|
19
|
+
timeout?: number;
|
|
17
20
|
}): void;
|
|
18
21
|
/**
|
|
19
22
|
* Makes an authenticated API call. Throws ApiError on non-2xx responses.
|
|
@@ -21,7 +24,7 @@ export declare function initApiClient(config: {
|
|
|
21
24
|
* @param url - The API endpoint URL
|
|
22
25
|
* @param options - Optional fetch options (method, body, headers)
|
|
23
26
|
* @returns The parsed JSON response
|
|
24
|
-
* @throws ApiError on non-2xx HTTP status
|
|
27
|
+
* @throws ApiError on non-2xx HTTP status or timeout
|
|
25
28
|
* @example
|
|
26
29
|
* const user = await apiCall<User>('/api/users/123');
|
|
27
30
|
*/
|
package/dist/client/api.js
CHANGED
|
@@ -16,21 +16,26 @@ const apiError_1 = require("./apiError");
|
|
|
16
16
|
let getToken = null;
|
|
17
17
|
// Callback for 401 responses (e.g., redirect to login)
|
|
18
18
|
let onUnauthorized = null;
|
|
19
|
+
// Request timeout in milliseconds (default: 30 seconds)
|
|
20
|
+
let requestTimeout = 30000;
|
|
19
21
|
/**
|
|
20
|
-
* Initializes the API client with token retrieval and optional
|
|
22
|
+
* Initializes the API client with token retrieval and optional configuration.
|
|
21
23
|
* Call once at application startup.
|
|
22
24
|
* @param config - Client configuration
|
|
23
25
|
* @param config.getToken - Function that returns the current auth token (or null)
|
|
24
26
|
* @param config.onUnauthorized - Optional callback for 401 responses (e.g., redirect to login)
|
|
27
|
+
* @param config.timeout - Optional request timeout in milliseconds (default: 30000)
|
|
25
28
|
* @example
|
|
26
29
|
* initApiClient({
|
|
27
30
|
* getToken: () => localStorage.getItem('token'),
|
|
28
|
-
* onUnauthorized: () => window.location.href = '/login'
|
|
31
|
+
* onUnauthorized: () => window.location.href = '/login',
|
|
32
|
+
* timeout: 60000 // 60 seconds
|
|
29
33
|
* });
|
|
30
34
|
*/
|
|
31
35
|
function initApiClient(config) {
|
|
32
36
|
getToken = config.getToken;
|
|
33
37
|
onUnauthorized = config.onUnauthorized ?? null;
|
|
38
|
+
requestTimeout = config.timeout ?? 30000;
|
|
34
39
|
}
|
|
35
40
|
/**
|
|
36
41
|
* Makes an authenticated API call. Throws ApiError on non-2xx responses.
|
|
@@ -38,7 +43,7 @@ function initApiClient(config) {
|
|
|
38
43
|
* @param url - The API endpoint URL
|
|
39
44
|
* @param options - Optional fetch options (method, body, headers)
|
|
40
45
|
* @returns The parsed JSON response
|
|
41
|
-
* @throws ApiError on non-2xx HTTP status
|
|
46
|
+
* @throws ApiError on non-2xx HTTP status or timeout
|
|
42
47
|
* @example
|
|
43
48
|
* const user = await apiCall<User>('/api/users/123');
|
|
44
49
|
*/
|
|
@@ -51,30 +56,48 @@ async function apiCall(url, options = {}) {
|
|
|
51
56
|
if (token) {
|
|
52
57
|
headers['Authorization'] = `Bearer ${token}`;
|
|
53
58
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// Setup timeout with AbortController
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(url, {
|
|
64
|
+
...options,
|
|
65
|
+
headers,
|
|
66
|
+
signal: controller.signal
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
if (response.status === 401 && onUnauthorized) {
|
|
70
|
+
onUnauthorized();
|
|
71
|
+
}
|
|
72
|
+
let errorMessage = 'Request failed';
|
|
73
|
+
try {
|
|
74
|
+
const errorData = (await response.json());
|
|
75
|
+
errorMessage = errorData.error || errorMessage;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Ignore JSON parse errors
|
|
79
|
+
}
|
|
80
|
+
throw new apiError_1.ApiError(response.status, errorMessage);
|
|
61
81
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
// Handle empty responses
|
|
83
|
+
const text = await response.text();
|
|
84
|
+
if (!text) {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
return JSON.parse(text);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error instanceof apiError_1.ApiError) {
|
|
91
|
+
throw error;
|
|
66
92
|
}
|
|
67
|
-
|
|
68
|
-
|
|
93
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
94
|
+
throw new apiError_1.ApiError(0, 'Request timeout');
|
|
69
95
|
}
|
|
70
|
-
throw
|
|
96
|
+
throw error;
|
|
71
97
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!text) {
|
|
75
|
-
return {};
|
|
98
|
+
finally {
|
|
99
|
+
clearTimeout(timeoutId);
|
|
76
100
|
}
|
|
77
|
-
return JSON.parse(text);
|
|
78
101
|
}
|
|
79
102
|
/**
|
|
80
103
|
* Makes an authenticated GET request.
|
|
@@ -173,10 +196,14 @@ async function apiCallSafe(url, options = {}) {
|
|
|
173
196
|
if (token) {
|
|
174
197
|
headers['Authorization'] = `Bearer ${token}`;
|
|
175
198
|
}
|
|
199
|
+
// Setup timeout with AbortController
|
|
200
|
+
const controller = new AbortController();
|
|
201
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
|
|
176
202
|
try {
|
|
177
203
|
const response = await fetch(url, {
|
|
178
204
|
...options,
|
|
179
|
-
headers
|
|
205
|
+
headers,
|
|
206
|
+
signal: controller.signal
|
|
180
207
|
});
|
|
181
208
|
if (!response.ok) {
|
|
182
209
|
if (response.status === 401 && onUnauthorized) {
|
|
@@ -197,7 +224,13 @@ async function apiCallSafe(url, options = {}) {
|
|
|
197
224
|
const data = text ? JSON.parse(text) : undefined;
|
|
198
225
|
return { ok: true, status: response.status, data };
|
|
199
226
|
}
|
|
200
|
-
catch {
|
|
227
|
+
catch (error) {
|
|
228
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
229
|
+
return { ok: false, status: 0, error: 'Request timeout' };
|
|
230
|
+
}
|
|
201
231
|
return { ok: false, status: 0, error: 'Network error' };
|
|
202
232
|
}
|
|
233
|
+
finally {
|
|
234
|
+
clearTimeout(timeoutId);
|
|
235
|
+
}
|
|
203
236
|
}
|
|
@@ -35,4 +35,14 @@ export declare class ApiError extends Error {
|
|
|
35
35
|
* @returns True if status is 400
|
|
36
36
|
*/
|
|
37
37
|
isBadRequest(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Checks if this is a client error (4xx status).
|
|
40
|
+
* @returns True if status is 400-499
|
|
41
|
+
*/
|
|
42
|
+
isClientError(): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Checks if this is a server error (5xx status).
|
|
45
|
+
* @returns True if status is 500-599
|
|
46
|
+
*/
|
|
47
|
+
isServerError(): boolean;
|
|
38
48
|
}
|
package/dist/client/apiError.js
CHANGED
|
@@ -47,5 +47,19 @@ class ApiError extends Error {
|
|
|
47
47
|
isBadRequest() {
|
|
48
48
|
return this.status === 400;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Checks if this is a client error (4xx status).
|
|
52
|
+
* @returns True if status is 400-499
|
|
53
|
+
*/
|
|
54
|
+
isClientError() {
|
|
55
|
+
return this.status >= 400 && this.status < 500;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Checks if this is a server error (5xx status).
|
|
59
|
+
* @returns True if status is 500-599
|
|
60
|
+
*/
|
|
61
|
+
isServerError() {
|
|
62
|
+
return this.status >= 500 && this.status < 600;
|
|
63
|
+
}
|
|
50
64
|
}
|
|
51
65
|
exports.ApiError = ApiError;
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { initAuth, getJwtSecret, extractToken, validateToken, generateToken, generateLongLivedToken } from './token';
|
|
1
|
+
export { initAuth, getJwtSecret, extractToken, validateToken, generateToken, generateLongLivedToken, requireAuth, requireAdmin, DEFAULT_TOKEN_EXPIRY, DEFAULT_TOKEN_EXPIRY_SECONDS } from './token';
|
|
2
|
+
export type { AuthSuccess, AuthFailure, AuthResult } from './token';
|
|
2
3
|
export { extractApiKey, hashApiKey, validateApiKey, generateApiKey } from './apiKey';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.generateApiKey = exports.validateApiKey = exports.hashApiKey = exports.extractApiKey = exports.generateLongLivedToken = exports.generateToken = exports.validateToken = exports.extractToken = exports.getJwtSecret = exports.initAuth = void 0;
|
|
3
|
+
exports.generateApiKey = exports.validateApiKey = exports.hashApiKey = exports.extractApiKey = exports.DEFAULT_TOKEN_EXPIRY_SECONDS = exports.DEFAULT_TOKEN_EXPIRY = exports.requireAdmin = exports.requireAuth = exports.generateLongLivedToken = exports.generateToken = exports.validateToken = exports.extractToken = exports.getJwtSecret = exports.initAuth = void 0;
|
|
4
4
|
var token_1 = require("./token");
|
|
5
5
|
Object.defineProperty(exports, "initAuth", { enumerable: true, get: function () { return token_1.initAuth; } });
|
|
6
6
|
Object.defineProperty(exports, "getJwtSecret", { enumerable: true, get: function () { return token_1.getJwtSecret; } });
|
|
@@ -8,6 +8,10 @@ Object.defineProperty(exports, "extractToken", { enumerable: true, get: function
|
|
|
8
8
|
Object.defineProperty(exports, "validateToken", { enumerable: true, get: function () { return token_1.validateToken; } });
|
|
9
9
|
Object.defineProperty(exports, "generateToken", { enumerable: true, get: function () { return token_1.generateToken; } });
|
|
10
10
|
Object.defineProperty(exports, "generateLongLivedToken", { enumerable: true, get: function () { return token_1.generateLongLivedToken; } });
|
|
11
|
+
Object.defineProperty(exports, "requireAuth", { enumerable: true, get: function () { return token_1.requireAuth; } });
|
|
12
|
+
Object.defineProperty(exports, "requireAdmin", { enumerable: true, get: function () { return token_1.requireAdmin; } });
|
|
13
|
+
Object.defineProperty(exports, "DEFAULT_TOKEN_EXPIRY", { enumerable: true, get: function () { return token_1.DEFAULT_TOKEN_EXPIRY; } });
|
|
14
|
+
Object.defineProperty(exports, "DEFAULT_TOKEN_EXPIRY_SECONDS", { enumerable: true, get: function () { return token_1.DEFAULT_TOKEN_EXPIRY_SECONDS; } });
|
|
11
15
|
var apiKey_1 = require("./apiKey");
|
|
12
16
|
Object.defineProperty(exports, "extractApiKey", { enumerable: true, get: function () { return apiKey_1.extractApiKey; } });
|
|
13
17
|
Object.defineProperty(exports, "hashApiKey", { enumerable: true, get: function () { return apiKey_1.hashApiKey; } });
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { HttpResponseInit } from '@azure/functions';
|
|
1
2
|
import { Result } from '../../types';
|
|
3
|
+
import { BaseJwtPayload, RoleTokenPayload } from '../../shared/auth/types';
|
|
2
4
|
/**
|
|
3
5
|
* Initializes the JWT authentication system. Call once at application startup.
|
|
4
6
|
* @param secret - The JWT secret key (from environment variable)
|
|
@@ -54,3 +56,47 @@ export declare function generateToken<T extends object>(payload: T, expiresIn?:
|
|
|
54
56
|
* const apiToken = generateLongLivedToken({ machineId: 'server-1' });
|
|
55
57
|
*/
|
|
56
58
|
export declare function generateLongLivedToken<T extends object>(payload: T, expiresInDays?: number): string;
|
|
59
|
+
/**
|
|
60
|
+
* Successful auth result with typed payload.
|
|
61
|
+
*/
|
|
62
|
+
export interface AuthSuccess<T extends BaseJwtPayload> {
|
|
63
|
+
authorized: true;
|
|
64
|
+
payload: T;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Failed auth result with HTTP response.
|
|
68
|
+
*/
|
|
69
|
+
export interface AuthFailure {
|
|
70
|
+
authorized: false;
|
|
71
|
+
response: HttpResponseInit;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Discriminated union for auth results.
|
|
75
|
+
* Use `auth.authorized` to narrow the type.
|
|
76
|
+
*/
|
|
77
|
+
export type AuthResult<T extends BaseJwtPayload> = AuthSuccess<T> | AuthFailure;
|
|
78
|
+
/** Default token expiry string for generateToken (7 days) */
|
|
79
|
+
export declare const DEFAULT_TOKEN_EXPIRY = "7d";
|
|
80
|
+
/** Default token expiry in seconds (7 days = 604800 seconds) */
|
|
81
|
+
export declare const DEFAULT_TOKEN_EXPIRY_SECONDS: number;
|
|
82
|
+
/**
|
|
83
|
+
* Validates auth header and returns typed payload or error response.
|
|
84
|
+
* @typeParam T - The expected payload type (extends BaseJwtPayload)
|
|
85
|
+
* @param authHeader - The Authorization header value
|
|
86
|
+
* @returns AuthResult with payload on success, or HTTP response on failure
|
|
87
|
+
* @example
|
|
88
|
+
* const auth = requireAuth<UsernameTokenPayload>(request.headers.get('Authorization'));
|
|
89
|
+
* if (!auth.authorized) return auth.response;
|
|
90
|
+
* console.log(auth.payload.username);
|
|
91
|
+
*/
|
|
92
|
+
export declare function requireAuth<T extends BaseJwtPayload>(authHeader: string | null): AuthResult<T>;
|
|
93
|
+
/**
|
|
94
|
+
* Requires admin role. Use with RoleTokenPayload.
|
|
95
|
+
* @param authHeader - The Authorization header value
|
|
96
|
+
* @returns AuthResult with RoleTokenPayload on success, or HTTP response on failure
|
|
97
|
+
* @example
|
|
98
|
+
* const auth = requireAdmin(request.headers.get('Authorization'));
|
|
99
|
+
* if (!auth.authorized) return auth.response;
|
|
100
|
+
* // User is admin
|
|
101
|
+
*/
|
|
102
|
+
export declare function requireAdmin(authHeader: string | null): AuthResult<RoleTokenPayload>;
|
|
@@ -3,14 +3,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_TOKEN_EXPIRY_SECONDS = exports.DEFAULT_TOKEN_EXPIRY = void 0;
|
|
6
7
|
exports.initAuth = initAuth;
|
|
7
8
|
exports.getJwtSecret = getJwtSecret;
|
|
8
9
|
exports.extractToken = extractToken;
|
|
9
10
|
exports.validateToken = validateToken;
|
|
10
11
|
exports.generateToken = generateToken;
|
|
11
12
|
exports.generateLongLivedToken = generateLongLivedToken;
|
|
13
|
+
exports.requireAuth = requireAuth;
|
|
14
|
+
exports.requireAdmin = requireAdmin;
|
|
12
15
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
13
16
|
const types_1 = require("../../types");
|
|
17
|
+
const types_2 = require("../../shared/auth/types");
|
|
18
|
+
const responses_1 = require("../http/responses");
|
|
14
19
|
/**
|
|
15
20
|
* JWT token utilities - works with any payload structure
|
|
16
21
|
* Use BaseJwtPayload or extend it for type safety
|
|
@@ -102,3 +107,52 @@ function generateToken(payload, expiresIn = '7d') {
|
|
|
102
107
|
function generateLongLivedToken(payload, expiresInDays = 3650) {
|
|
103
108
|
return jsonwebtoken_1.default.sign(payload, getJwtSecret(), { expiresIn: `${expiresInDays}d` });
|
|
104
109
|
}
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// Token Expiry Constants
|
|
112
|
+
// ============================================================================
|
|
113
|
+
/** Default token expiry string for generateToken (7 days) */
|
|
114
|
+
exports.DEFAULT_TOKEN_EXPIRY = '7d';
|
|
115
|
+
/** Default token expiry in seconds (7 days = 604800 seconds) */
|
|
116
|
+
exports.DEFAULT_TOKEN_EXPIRY_SECONDS = 7 * 24 * 60 * 60;
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Auth Helpers
|
|
119
|
+
// ============================================================================
|
|
120
|
+
/**
|
|
121
|
+
* Validates auth header and returns typed payload or error response.
|
|
122
|
+
* @typeParam T - The expected payload type (extends BaseJwtPayload)
|
|
123
|
+
* @param authHeader - The Authorization header value
|
|
124
|
+
* @returns AuthResult with payload on success, or HTTP response on failure
|
|
125
|
+
* @example
|
|
126
|
+
* const auth = requireAuth<UsernameTokenPayload>(request.headers.get('Authorization'));
|
|
127
|
+
* if (!auth.authorized) return auth.response;
|
|
128
|
+
* console.log(auth.payload.username);
|
|
129
|
+
*/
|
|
130
|
+
function requireAuth(authHeader) {
|
|
131
|
+
const token = extractToken(authHeader);
|
|
132
|
+
if (!token) {
|
|
133
|
+
return { authorized: false, response: (0, responses_1.unauthorizedResponse)() };
|
|
134
|
+
}
|
|
135
|
+
const result = validateToken(token);
|
|
136
|
+
if (!result.ok) {
|
|
137
|
+
return { authorized: false, response: (0, responses_1.unauthorizedResponse)(result.error) };
|
|
138
|
+
}
|
|
139
|
+
return { authorized: true, payload: result.data };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Requires admin role. Use with RoleTokenPayload.
|
|
143
|
+
* @param authHeader - The Authorization header value
|
|
144
|
+
* @returns AuthResult with RoleTokenPayload on success, or HTTP response on failure
|
|
145
|
+
* @example
|
|
146
|
+
* const auth = requireAdmin(request.headers.get('Authorization'));
|
|
147
|
+
* if (!auth.authorized) return auth.response;
|
|
148
|
+
* // User is admin
|
|
149
|
+
*/
|
|
150
|
+
function requireAdmin(authHeader) {
|
|
151
|
+
const auth = requireAuth(authHeader);
|
|
152
|
+
if (!auth.authorized)
|
|
153
|
+
return auth;
|
|
154
|
+
if (!(0, types_2.isAdmin)(auth.payload)) {
|
|
155
|
+
return { authorized: false, response: (0, responses_1.forbiddenResponse)('Admin access required') };
|
|
156
|
+
}
|
|
157
|
+
return auth;
|
|
158
|
+
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { initAuth, getJwtSecret, extractToken, validateToken, generateToken, generateLongLivedToken, extractApiKey, hashApiKey, validateApiKey, generateApiKey } from './auth';
|
|
1
|
+
export { initAuth, getJwtSecret, extractToken, validateToken, generateToken, generateLongLivedToken, requireAuth, requireAdmin, DEFAULT_TOKEN_EXPIRY, DEFAULT_TOKEN_EXPIRY_SECONDS, extractApiKey, hashApiKey, validateApiKey, generateApiKey } from './auth';
|
|
2
|
+
export type { AuthSuccess, AuthFailure, AuthResult } from './auth';
|
|
2
3
|
export { badRequestResponse, unauthorizedResponse, forbiddenResponse, notFoundResponse, conflictResponse, handleFunctionError, isNotFoundError, isConflictError } from './http';
|
|
3
4
|
export { initStorage, initStorageFromEnv, useManagedIdentity, getTableClient, clearTableClientCache, generateRowKey } from './storage';
|
package/dist/server/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.generateRowKey = exports.clearTableClientCache = exports.getTableClient = exports.useManagedIdentity = exports.initStorageFromEnv = exports.initStorage = exports.isConflictError = exports.isNotFoundError = exports.handleFunctionError = exports.conflictResponse = exports.notFoundResponse = exports.forbiddenResponse = exports.unauthorizedResponse = exports.badRequestResponse = exports.generateApiKey = exports.validateApiKey = exports.hashApiKey = exports.extractApiKey = exports.generateLongLivedToken = exports.generateToken = exports.validateToken = exports.extractToken = exports.getJwtSecret = exports.initAuth = void 0;
|
|
3
|
+
exports.generateRowKey = exports.clearTableClientCache = exports.getTableClient = exports.useManagedIdentity = exports.initStorageFromEnv = exports.initStorage = exports.isConflictError = exports.isNotFoundError = exports.handleFunctionError = exports.conflictResponse = exports.notFoundResponse = exports.forbiddenResponse = exports.unauthorizedResponse = exports.badRequestResponse = exports.generateApiKey = exports.validateApiKey = exports.hashApiKey = exports.extractApiKey = exports.DEFAULT_TOKEN_EXPIRY_SECONDS = exports.DEFAULT_TOKEN_EXPIRY = exports.requireAdmin = exports.requireAuth = exports.generateLongLivedToken = exports.generateToken = exports.validateToken = exports.extractToken = exports.getJwtSecret = exports.initAuth = void 0;
|
|
4
4
|
// Auth
|
|
5
5
|
var auth_1 = require("./auth");
|
|
6
6
|
Object.defineProperty(exports, "initAuth", { enumerable: true, get: function () { return auth_1.initAuth; } });
|
|
@@ -9,6 +9,10 @@ Object.defineProperty(exports, "extractToken", { enumerable: true, get: function
|
|
|
9
9
|
Object.defineProperty(exports, "validateToken", { enumerable: true, get: function () { return auth_1.validateToken; } });
|
|
10
10
|
Object.defineProperty(exports, "generateToken", { enumerable: true, get: function () { return auth_1.generateToken; } });
|
|
11
11
|
Object.defineProperty(exports, "generateLongLivedToken", { enumerable: true, get: function () { return auth_1.generateLongLivedToken; } });
|
|
12
|
+
Object.defineProperty(exports, "requireAuth", { enumerable: true, get: function () { return auth_1.requireAuth; } });
|
|
13
|
+
Object.defineProperty(exports, "requireAdmin", { enumerable: true, get: function () { return auth_1.requireAdmin; } });
|
|
14
|
+
Object.defineProperty(exports, "DEFAULT_TOKEN_EXPIRY", { enumerable: true, get: function () { return auth_1.DEFAULT_TOKEN_EXPIRY; } });
|
|
15
|
+
Object.defineProperty(exports, "DEFAULT_TOKEN_EXPIRY_SECONDS", { enumerable: true, get: function () { return auth_1.DEFAULT_TOKEN_EXPIRY_SECONDS; } });
|
|
12
16
|
Object.defineProperty(exports, "extractApiKey", { enumerable: true, get: function () { return auth_1.extractApiKey; } });
|
|
13
17
|
Object.defineProperty(exports, "hashApiKey", { enumerable: true, get: function () { return auth_1.hashApiKey; } });
|
|
14
18
|
Object.defineProperty(exports, "validateApiKey", { enumerable: true, get: function () { return auth_1.validateApiKey; } });
|
|
@@ -11,6 +11,8 @@ export declare const HTTP_STATUS: {
|
|
|
11
11
|
readonly NOT_FOUND: 404;
|
|
12
12
|
readonly CONFLICT: 409;
|
|
13
13
|
readonly GONE: 410;
|
|
14
|
+
readonly UNPROCESSABLE_ENTITY: 422;
|
|
15
|
+
readonly TOO_MANY_REQUESTS: 429;
|
|
14
16
|
readonly INTERNAL_ERROR: 500;
|
|
15
17
|
readonly SERVICE_UNAVAILABLE: 503;
|
|
16
18
|
};
|