@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 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 secret - The JWT secret key (from environment variable)
12
- * @param minLength - Minimum required secret length (default: 32)
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(secret: string | undefined, minLength?: number): void;
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 secret - The JWT secret key (from environment variable)
50
- * @param minLength - Minimum required secret length (default: 32)
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(secret, minLength = 32) {
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 instanceof 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 instanceof 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, credential);
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markwharton/pwa-core",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Shared patterns for Azure PWA projects",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",