@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.
@@ -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
  });
@@ -1,19 +1,22 @@
1
1
  import { ApiResponse } from './types';
2
2
  /**
3
- * Initializes the API client with token retrieval and optional 401 handler.
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
  */
@@ -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 401 handler.
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
- const response = await fetch(url, {
55
- ...options,
56
- headers
57
- });
58
- if (!response.ok) {
59
- if (response.status === 401 && onUnauthorized) {
60
- onUnauthorized();
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
- let errorMessage = 'Request failed';
63
- try {
64
- const errorData = (await response.json());
65
- errorMessage = errorData.error || errorMessage;
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
- catch {
68
- // Ignore JSON parse errors
93
+ if (error instanceof Error && error.name === 'AbortError') {
94
+ throw new apiError_1.ApiError(0, 'Request timeout');
69
95
  }
70
- throw new apiError_1.ApiError(response.status, errorMessage);
96
+ throw error;
71
97
  }
72
- // Handle empty responses
73
- const text = await response.text();
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
  }
@@ -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
+ }
@@ -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';
@@ -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
  };
@@ -14,6 +14,8 @@ exports.HTTP_STATUS = {
14
14
  NOT_FOUND: 404,
15
15
  CONFLICT: 409,
16
16
  GONE: 410,
17
+ UNPROCESSABLE_ENTITY: 422,
18
+ TOO_MANY_REQUESTS: 429,
17
19
  INTERNAL_ERROR: 500,
18
20
  SERVICE_UNAVAILABLE: 503
19
21
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markwharton/pwa-core",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Shared patterns for Azure PWA projects",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",