@markwharton/pwa-core 2.0.0 → 3.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/server.d.ts +76 -6
- package/dist/server.js +123 -15
- package/package.json +1 -1
package/dist/server.d.ts
CHANGED
|
@@ -8,13 +8,27 @@ import { TableClient } from '@azure/data-tables';
|
|
|
8
8
|
import { Result, BaseJwtPayload, RoleTokenPayload } from './shared';
|
|
9
9
|
/**
|
|
10
10
|
* Initializes the JWT authentication system. Call once at application startup.
|
|
11
|
-
* @param
|
|
12
|
-
* @param
|
|
11
|
+
* @param config - Configuration object
|
|
12
|
+
* @param config.secret - The JWT secret key (from environment variable)
|
|
13
|
+
* @param config.minLength - Minimum required secret length (default: 32)
|
|
13
14
|
* @throws Error if secret is missing or too short
|
|
14
15
|
* @example
|
|
15
|
-
* initAuth(process.env.JWT_SECRET);
|
|
16
|
+
* initAuth({ secret: process.env.JWT_SECRET });
|
|
17
|
+
* initAuth({ secret: process.env.JWT_SECRET, minLength: 64 });
|
|
16
18
|
*/
|
|
17
|
-
export declare function initAuth(
|
|
19
|
+
export declare function initAuth(config: {
|
|
20
|
+
secret?: string;
|
|
21
|
+
minLength?: number;
|
|
22
|
+
}): void;
|
|
23
|
+
/**
|
|
24
|
+
* Initializes JWT authentication from environment variables.
|
|
25
|
+
* Reads JWT_SECRET from process.env.
|
|
26
|
+
* @param minLength - Minimum required secret length (default: 32)
|
|
27
|
+
* @throws Error if JWT_SECRET is missing or too short
|
|
28
|
+
* @example
|
|
29
|
+
* initAuthFromEnv(); // Uses process.env.JWT_SECRET
|
|
30
|
+
*/
|
|
31
|
+
export declare function initAuthFromEnv(minLength?: number): void;
|
|
18
32
|
/**
|
|
19
33
|
* Gets the configured JWT secret.
|
|
20
34
|
* @returns The JWT secret string
|
|
@@ -186,20 +200,62 @@ export declare function notFoundResponse(resource: string): HttpResponseInit;
|
|
|
186
200
|
* if (existingUser) return conflictResponse('Email already registered');
|
|
187
201
|
*/
|
|
188
202
|
export declare function conflictResponse(message: string): HttpResponseInit;
|
|
203
|
+
/**
|
|
204
|
+
* Validates that a required parameter is present.
|
|
205
|
+
* Returns an error response if missing, null if valid.
|
|
206
|
+
* @param value - The value to check
|
|
207
|
+
* @param paramName - The parameter name for the error message
|
|
208
|
+
* @returns HttpResponseInit if invalid, null if valid
|
|
209
|
+
* @example
|
|
210
|
+
* const error = validateRequired(userId, 'userId');
|
|
211
|
+
* if (error) return error;
|
|
212
|
+
* // userId is guaranteed to be defined here
|
|
213
|
+
*/
|
|
214
|
+
export declare function validateRequired(value: string | undefined, paramName: string): HttpResponseInit | null;
|
|
215
|
+
/**
|
|
216
|
+
* Callback type for error handling hooks (e.g., alerting, monitoring).
|
|
217
|
+
*/
|
|
218
|
+
export type ErrorCallback = (operation: string, message: string) => void;
|
|
219
|
+
/**
|
|
220
|
+
* Sets a callback to be invoked when handleFunctionError is called.
|
|
221
|
+
* Use this to integrate with alerting or monitoring systems.
|
|
222
|
+
* @param callback - The callback to invoke (fire-and-forget)
|
|
223
|
+
* @example
|
|
224
|
+
* setErrorCallback((operation, message) => {
|
|
225
|
+
* sendAlert(operation, message); // Fire-and-forget
|
|
226
|
+
* });
|
|
227
|
+
*/
|
|
228
|
+
export declare function setErrorCallback(callback: ErrorCallback | null): void;
|
|
189
229
|
/**
|
|
190
230
|
* Handles unexpected errors safely by logging details and returning a generic message.
|
|
191
231
|
* Use in catch blocks to avoid exposing internal error details to clients.
|
|
192
232
|
* @param error - The caught error
|
|
193
233
|
* @param context - Azure Functions InvocationContext for logging
|
|
234
|
+
* @param operation - Optional operation name for error callback
|
|
194
235
|
* @returns Azure Functions HttpResponseInit with 500 status
|
|
195
236
|
* @example
|
|
196
237
|
* try {
|
|
197
238
|
* await riskyOperation();
|
|
198
239
|
* } catch (error) {
|
|
199
|
-
* return handleFunctionError(error, context);
|
|
240
|
+
* return handleFunctionError(error, context, 'riskyOperation');
|
|
200
241
|
* }
|
|
201
242
|
*/
|
|
202
|
-
export declare function handleFunctionError(error: unknown, context: InvocationContext): HttpResponseInit;
|
|
243
|
+
export declare function handleFunctionError(error: unknown, context: InvocationContext, operation?: string): HttpResponseInit;
|
|
244
|
+
/**
|
|
245
|
+
* Checks if an error is an Azure Table Storage error with a specific status code.
|
|
246
|
+
* @param error - The caught error
|
|
247
|
+
* @param statusCode - The HTTP status code to check for
|
|
248
|
+
* @returns True if error has the specified statusCode
|
|
249
|
+
* @example
|
|
250
|
+
* try {
|
|
251
|
+
* await tableClient.upsertEntity(entity);
|
|
252
|
+
* } catch (error) {
|
|
253
|
+
* if (isTableStorageError(error, 412)) {
|
|
254
|
+
* // Precondition failed - ETag mismatch
|
|
255
|
+
* }
|
|
256
|
+
* }
|
|
257
|
+
*/
|
|
258
|
+
export declare function isTableStorageError(error: unknown, statusCode: number): boolean;
|
|
203
259
|
/**
|
|
204
260
|
* Checks if an error is an Azure Table Storage "not found" error.
|
|
205
261
|
* @param error - The caught error
|
|
@@ -273,6 +329,20 @@ export declare function getTableClient(tableName: string): Promise<TableClient>;
|
|
|
273
329
|
* });
|
|
274
330
|
*/
|
|
275
331
|
export declare function clearTableClientCache(): void;
|
|
332
|
+
/**
|
|
333
|
+
* Gets an entity from Azure Table Storage if it exists, or returns null.
|
|
334
|
+
* Eliminates the common try-catch-isNotFoundError pattern.
|
|
335
|
+
* @typeParam T - The entity type (extends object)
|
|
336
|
+
* @param client - The TableClient instance
|
|
337
|
+
* @param partitionKey - The partition key
|
|
338
|
+
* @param rowKey - The row key
|
|
339
|
+
* @returns The entity if found, null if not found
|
|
340
|
+
* @throws Re-throws non-404 errors
|
|
341
|
+
* @example
|
|
342
|
+
* const user = await getEntityIfExists<UserEntity>(client, 'users', id);
|
|
343
|
+
* if (!user) return notFoundResponse('User');
|
|
344
|
+
*/
|
|
345
|
+
export declare function getEntityIfExists<T extends object>(client: TableClient, partitionKey: string, rowKey: string): Promise<T | null>;
|
|
276
346
|
/**
|
|
277
347
|
* Generate a unique row key from an identifier string.
|
|
278
348
|
* Uses SHA-256 hash for consistent, URL-safe keys.
|
package/dist/server.js
CHANGED
|
@@ -10,6 +10,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.HTTP_STATUS = exports.isAdmin = exports.hasRole = exports.hasUsername = exports.getErrorMessage = exports.err = exports.okVoid = exports.ok = exports.DEFAULT_TOKEN_EXPIRY_SECONDS = exports.DEFAULT_TOKEN_EXPIRY = void 0;
|
|
12
12
|
exports.initAuth = initAuth;
|
|
13
|
+
exports.initAuthFromEnv = initAuthFromEnv;
|
|
13
14
|
exports.getJwtSecret = getJwtSecret;
|
|
14
15
|
exports.extractToken = extractToken;
|
|
15
16
|
exports.validateToken = validateToken;
|
|
@@ -26,7 +27,10 @@ exports.unauthorizedResponse = unauthorizedResponse;
|
|
|
26
27
|
exports.forbiddenResponse = forbiddenResponse;
|
|
27
28
|
exports.notFoundResponse = notFoundResponse;
|
|
28
29
|
exports.conflictResponse = conflictResponse;
|
|
30
|
+
exports.validateRequired = validateRequired;
|
|
31
|
+
exports.setErrorCallback = setErrorCallback;
|
|
29
32
|
exports.handleFunctionError = handleFunctionError;
|
|
33
|
+
exports.isTableStorageError = isTableStorageError;
|
|
30
34
|
exports.isNotFoundError = isNotFoundError;
|
|
31
35
|
exports.isConflictError = isConflictError;
|
|
32
36
|
exports.initStorage = initStorage;
|
|
@@ -34,6 +38,7 @@ exports.initStorageFromEnv = initStorageFromEnv;
|
|
|
34
38
|
exports.useManagedIdentity = useManagedIdentity;
|
|
35
39
|
exports.getTableClient = getTableClient;
|
|
36
40
|
exports.clearTableClientCache = clearTableClientCache;
|
|
41
|
+
exports.getEntityIfExists = getEntityIfExists;
|
|
37
42
|
exports.generateRowKey = generateRowKey;
|
|
38
43
|
const crypto_1 = require("crypto");
|
|
39
44
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
@@ -46,18 +51,35 @@ const shared_1 = require("./shared");
|
|
|
46
51
|
let jwtSecret = null;
|
|
47
52
|
/**
|
|
48
53
|
* Initializes the JWT authentication system. Call once at application startup.
|
|
49
|
-
* @param
|
|
50
|
-
* @param
|
|
54
|
+
* @param config - Configuration object
|
|
55
|
+
* @param config.secret - The JWT secret key (from environment variable)
|
|
56
|
+
* @param config.minLength - Minimum required secret length (default: 32)
|
|
51
57
|
* @throws Error if secret is missing or too short
|
|
52
58
|
* @example
|
|
53
|
-
* initAuth(process.env.JWT_SECRET);
|
|
59
|
+
* initAuth({ secret: process.env.JWT_SECRET });
|
|
60
|
+
* initAuth({ secret: process.env.JWT_SECRET, minLength: 64 });
|
|
54
61
|
*/
|
|
55
|
-
function initAuth(
|
|
62
|
+
function initAuth(config) {
|
|
63
|
+
const { secret, minLength = 32 } = config;
|
|
56
64
|
if (!secret || secret.length < minLength) {
|
|
57
65
|
throw new Error(`JWT_SECRET must be at least ${minLength} characters`);
|
|
58
66
|
}
|
|
59
67
|
jwtSecret = secret;
|
|
60
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Initializes JWT authentication from environment variables.
|
|
71
|
+
* Reads JWT_SECRET from process.env.
|
|
72
|
+
* @param minLength - Minimum required secret length (default: 32)
|
|
73
|
+
* @throws Error if JWT_SECRET is missing or too short
|
|
74
|
+
* @example
|
|
75
|
+
* initAuthFromEnv(); // Uses process.env.JWT_SECRET
|
|
76
|
+
*/
|
|
77
|
+
function initAuthFromEnv(minLength = 32) {
|
|
78
|
+
initAuth({
|
|
79
|
+
secret: process.env.JWT_SECRET,
|
|
80
|
+
minLength
|
|
81
|
+
});
|
|
82
|
+
}
|
|
61
83
|
/**
|
|
62
84
|
* Gets the configured JWT secret.
|
|
63
85
|
* @returns The JWT secret string
|
|
@@ -300,27 +322,86 @@ function conflictResponse(message) {
|
|
|
300
322
|
jsonBody: { error: message }
|
|
301
323
|
};
|
|
302
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* Validates that a required parameter is present.
|
|
327
|
+
* Returns an error response if missing, null if valid.
|
|
328
|
+
* @param value - The value to check
|
|
329
|
+
* @param paramName - The parameter name for the error message
|
|
330
|
+
* @returns HttpResponseInit if invalid, null if valid
|
|
331
|
+
* @example
|
|
332
|
+
* const error = validateRequired(userId, 'userId');
|
|
333
|
+
* if (error) return error;
|
|
334
|
+
* // userId is guaranteed to be defined here
|
|
335
|
+
*/
|
|
336
|
+
function validateRequired(value, paramName) {
|
|
337
|
+
if (!value) {
|
|
338
|
+
return badRequestResponse(`${paramName} is required`);
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
let errorCallback = null;
|
|
343
|
+
/**
|
|
344
|
+
* Sets a callback to be invoked when handleFunctionError is called.
|
|
345
|
+
* Use this to integrate with alerting or monitoring systems.
|
|
346
|
+
* @param callback - The callback to invoke (fire-and-forget)
|
|
347
|
+
* @example
|
|
348
|
+
* setErrorCallback((operation, message) => {
|
|
349
|
+
* sendAlert(operation, message); // Fire-and-forget
|
|
350
|
+
* });
|
|
351
|
+
*/
|
|
352
|
+
function setErrorCallback(callback) {
|
|
353
|
+
errorCallback = callback;
|
|
354
|
+
}
|
|
303
355
|
/**
|
|
304
356
|
* Handles unexpected errors safely by logging details and returning a generic message.
|
|
305
357
|
* Use in catch blocks to avoid exposing internal error details to clients.
|
|
306
358
|
* @param error - The caught error
|
|
307
359
|
* @param context - Azure Functions InvocationContext for logging
|
|
360
|
+
* @param operation - Optional operation name for error callback
|
|
308
361
|
* @returns Azure Functions HttpResponseInit with 500 status
|
|
309
362
|
* @example
|
|
310
363
|
* try {
|
|
311
364
|
* await riskyOperation();
|
|
312
365
|
* } catch (error) {
|
|
313
|
-
* return handleFunctionError(error, context);
|
|
366
|
+
* return handleFunctionError(error, context, 'riskyOperation');
|
|
314
367
|
* }
|
|
315
368
|
*/
|
|
316
|
-
function handleFunctionError(error, context) {
|
|
369
|
+
function handleFunctionError(error, context, operation) {
|
|
317
370
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
318
371
|
context.error(`Function error: ${message}`);
|
|
372
|
+
// Fire-and-forget callback (for alerting, monitoring)
|
|
373
|
+
if (errorCallback && operation) {
|
|
374
|
+
try {
|
|
375
|
+
errorCallback(operation, message);
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// Swallow callback errors to prevent cascading failures
|
|
379
|
+
}
|
|
380
|
+
}
|
|
319
381
|
return {
|
|
320
382
|
status: shared_1.HTTP_STATUS.INTERNAL_ERROR,
|
|
321
383
|
jsonBody: { error: 'Internal server error' }
|
|
322
384
|
};
|
|
323
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* Checks if an error is an Azure Table Storage error with a specific status code.
|
|
388
|
+
* @param error - The caught error
|
|
389
|
+
* @param statusCode - The HTTP status code to check for
|
|
390
|
+
* @returns True if error has the specified statusCode
|
|
391
|
+
* @example
|
|
392
|
+
* try {
|
|
393
|
+
* await tableClient.upsertEntity(entity);
|
|
394
|
+
* } catch (error) {
|
|
395
|
+
* if (isTableStorageError(error, 412)) {
|
|
396
|
+
* // Precondition failed - ETag mismatch
|
|
397
|
+
* }
|
|
398
|
+
* }
|
|
399
|
+
*/
|
|
400
|
+
function isTableStorageError(error, statusCode) {
|
|
401
|
+
return (error instanceof Error &&
|
|
402
|
+
'statusCode' in error &&
|
|
403
|
+
error.statusCode === statusCode);
|
|
404
|
+
}
|
|
324
405
|
/**
|
|
325
406
|
* Checks if an error is an Azure Table Storage "not found" error.
|
|
326
407
|
* @param error - The caught error
|
|
@@ -334,9 +415,7 @@ function handleFunctionError(error, context) {
|
|
|
334
415
|
* }
|
|
335
416
|
*/
|
|
336
417
|
function isNotFoundError(error) {
|
|
337
|
-
return (error
|
|
338
|
-
'statusCode' in error &&
|
|
339
|
-
error.statusCode === 404);
|
|
418
|
+
return isTableStorageError(error, shared_1.HTTP_STATUS.NOT_FOUND);
|
|
340
419
|
}
|
|
341
420
|
/**
|
|
342
421
|
* Checks if an error is an Azure Table Storage "conflict" error.
|
|
@@ -351,15 +430,21 @@ function isNotFoundError(error) {
|
|
|
351
430
|
* }
|
|
352
431
|
*/
|
|
353
432
|
function isConflictError(error) {
|
|
354
|
-
return (error
|
|
355
|
-
'statusCode' in error &&
|
|
356
|
-
error.statusCode === 409);
|
|
433
|
+
return isTableStorageError(error, shared_1.HTTP_STATUS.CONFLICT);
|
|
357
434
|
}
|
|
358
435
|
// =============================================================================
|
|
359
436
|
// Azure Table Storage
|
|
360
437
|
// =============================================================================
|
|
361
438
|
// Cache table clients to avoid repeated connections
|
|
362
439
|
const tableClients = new Map();
|
|
440
|
+
// Shared credential instance for managed identity (avoids repeated instantiation)
|
|
441
|
+
let credential = null;
|
|
442
|
+
function getCredential() {
|
|
443
|
+
if (!credential) {
|
|
444
|
+
credential = new identity_1.DefaultAzureCredential();
|
|
445
|
+
}
|
|
446
|
+
return credential;
|
|
447
|
+
}
|
|
363
448
|
// Configuration
|
|
364
449
|
let storageAccountName;
|
|
365
450
|
let storageConnectionString;
|
|
@@ -416,10 +501,9 @@ async function getTableClient(tableName) {
|
|
|
416
501
|
}
|
|
417
502
|
let client;
|
|
418
503
|
if (useManagedIdentity()) {
|
|
419
|
-
// Azure: Use managed identity
|
|
420
|
-
const credential = new identity_1.DefaultAzureCredential();
|
|
504
|
+
// Azure: Use managed identity (credential is cached)
|
|
421
505
|
const endpoint = `https://${storageAccountName}.table.core.windows.net`;
|
|
422
|
-
client = new data_tables_1.TableClient(endpoint, tableName,
|
|
506
|
+
client = new data_tables_1.TableClient(endpoint, tableName, getCredential());
|
|
423
507
|
}
|
|
424
508
|
else if (storageConnectionString) {
|
|
425
509
|
// Local/Azurite: Use connection string
|
|
@@ -453,6 +537,30 @@ async function getTableClient(tableName) {
|
|
|
453
537
|
function clearTableClientCache() {
|
|
454
538
|
tableClients.clear();
|
|
455
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* Gets an entity from Azure Table Storage if it exists, or returns null.
|
|
542
|
+
* Eliminates the common try-catch-isNotFoundError pattern.
|
|
543
|
+
* @typeParam T - The entity type (extends object)
|
|
544
|
+
* @param client - The TableClient instance
|
|
545
|
+
* @param partitionKey - The partition key
|
|
546
|
+
* @param rowKey - The row key
|
|
547
|
+
* @returns The entity if found, null if not found
|
|
548
|
+
* @throws Re-throws non-404 errors
|
|
549
|
+
* @example
|
|
550
|
+
* const user = await getEntityIfExists<UserEntity>(client, 'users', id);
|
|
551
|
+
* if (!user) return notFoundResponse('User');
|
|
552
|
+
*/
|
|
553
|
+
async function getEntityIfExists(client, partitionKey, rowKey) {
|
|
554
|
+
try {
|
|
555
|
+
return await client.getEntity(partitionKey, rowKey);
|
|
556
|
+
}
|
|
557
|
+
catch (error) {
|
|
558
|
+
if (isNotFoundError(error)) {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
throw error;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
456
564
|
/**
|
|
457
565
|
* Generate a unique row key from an identifier string.
|
|
458
566
|
* Uses SHA-256 hash for consistent, URL-safe keys.
|