@markwharton/pwa-core 3.4.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -4,6 +4,39 @@
4
4
  *
5
5
  * Includes: JWT auth, API keys, HTTP responses, Azure Table Storage
6
6
  */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
7
40
  var __importDefault = (this && this.__importDefault) || function (mod) {
8
41
  return (mod && mod.__esModule) ? mod : { "default": mod };
9
42
  };
@@ -66,6 +99,9 @@ exports.withSessionAuth = withSessionAuth;
66
99
  exports.withSessionAdminAuth = withSessionAdminAuth;
67
100
  exports.deleteExpiredSessions = deleteExpiredSessions;
68
101
  exports.deleteExpiredMagicLinks = deleteExpiredMagicLinks;
102
+ exports.hasKeyVaultReferences = hasKeyVaultReferences;
103
+ exports.resolveKeyVaultReferences = resolveKeyVaultReferences;
104
+ exports.resultToResponse = resultToResponse;
69
105
  const crypto_1 = require("crypto");
70
106
  const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
71
107
  const data_tables_1 = require("@azure/data-tables");
@@ -95,15 +131,13 @@ function initAuth(config) {
95
131
  /**
96
132
  * Initializes JWT authentication from environment variables.
97
133
  * Reads JWT_SECRET from process.env.
98
- * @param minLength - Minimum required secret length (default: 32)
99
134
  * @throws Error if JWT_SECRET is missing or too short
100
135
  * @example
101
136
  * initAuthFromEnv(); // Uses process.env.JWT_SECRET
102
137
  */
103
- function initAuthFromEnv(minLength = 32) {
138
+ function initAuthFromEnv() {
104
139
  initAuth({
105
140
  secret: process.env.JWT_SECRET,
106
- minLength
107
141
  });
108
142
  }
109
143
  /**
@@ -190,41 +224,41 @@ exports.DEFAULT_TOKEN_EXPIRY_SECONDS = DEFAULT_TOKEN_EXPIRY_DAYS * 24 * 60 * 60;
190
224
  // Auth Helpers
191
225
  // =============================================================================
192
226
  /**
193
- * Validates auth header and returns typed payload or error response.
227
+ * Validates auth header and returns typed payload or error result.
194
228
  * @typeParam T - The expected payload type (extends BaseJwtPayload)
195
229
  * @param authHeader - The Authorization header value
196
- * @returns AuthResult with payload on success, or HTTP response on failure
230
+ * @returns Result with payload on success, or error with status on failure
197
231
  * @example
198
232
  * const auth = requireAuth<UsernameTokenPayload>(request.headers.get('Authorization'));
199
- * if (!auth.authorized) return auth.response;
200
- * console.log(auth.payload.username);
233
+ * if (!auth.ok) return resultToResponse(auth);
234
+ * console.log(auth.data.username);
201
235
  */
202
236
  function requireAuth(authHeader) {
203
237
  const token = extractToken(authHeader);
204
238
  if (!token) {
205
- return { authorized: false, response: unauthorizedResponse() };
239
+ return (0, shared_1.err)('Unauthorized', shared_1.HTTP_STATUS.UNAUTHORIZED);
206
240
  }
207
241
  const result = validateToken(token);
208
242
  if (!result.ok) {
209
- return { authorized: false, response: unauthorizedResponse(result.error) };
243
+ return (0, shared_1.err)(result.error ?? 'Unauthorized', shared_1.HTTP_STATUS.UNAUTHORIZED);
210
244
  }
211
- return { authorized: true, payload: result.data };
245
+ return (0, shared_1.ok)(result.data);
212
246
  }
213
247
  /**
214
248
  * Requires admin role. Use with RoleTokenPayload.
215
249
  * @param authHeader - The Authorization header value
216
- * @returns AuthResult with RoleTokenPayload on success, or HTTP response on failure
250
+ * @returns Result with RoleTokenPayload on success, or error with status on failure
217
251
  * @example
218
252
  * const auth = requireAdmin(request.headers.get('Authorization'));
219
- * if (!auth.authorized) return auth.response;
253
+ * if (!auth.ok) return resultToResponse(auth);
220
254
  * // User is admin
221
255
  */
222
256
  function requireAdmin(authHeader) {
223
257
  const auth = requireAuth(authHeader);
224
- if (!auth.authorized)
258
+ if (!auth.ok)
225
259
  return auth;
226
- if (!(0, shared_1.isAdmin)(auth.payload)) {
227
- return { authorized: false, response: forbiddenResponse('Admin access required') };
260
+ if (!(0, shared_1.isAdmin)(auth.data)) {
261
+ return (0, shared_1.err)('Admin access required', shared_1.HTTP_STATUS.FORBIDDEN);
228
262
  }
229
263
  return auth;
230
264
  }
@@ -424,14 +458,14 @@ function initErrorHandling(config = {}) {
424
458
  * Initializes error handling from environment variables.
425
459
  * Currently a no-op but provides consistent API for future error config
426
460
  * (e.g., ERROR_LOG_LEVEL, ERROR_INCLUDE_STACK).
427
- * @param callback - Optional callback invoked when handleFunctionError is called (fire-and-forget)
461
+ * @param config - Optional config with onError callback invoked when handleFunctionError is called (fire-and-forget)
428
462
  * @example
429
463
  * initErrorHandlingFromEnv(); // Uses process.env automatically
430
- * initErrorHandlingFromEnv((op, msg) => sendAlert(op, msg));
464
+ * initErrorHandlingFromEnv({ onError: (op, msg) => sendAlert(op, msg) });
431
465
  */
432
- function initErrorHandlingFromEnv(callback) {
466
+ function initErrorHandlingFromEnv(config) {
433
467
  // Future: read ERROR_LOG_LEVEL, ERROR_INCLUDE_STACK, etc. from process.env
434
- initErrorHandling({ callback });
468
+ initErrorHandling({ callback: config?.onError });
435
469
  }
436
470
  /**
437
471
  * Handles unexpected errors safely by logging details and returning a generic message.
@@ -660,21 +694,25 @@ async function upsertEntity(client, entity) {
660
694
  }
661
695
  /**
662
696
  * Deletes an entity from Azure Table Storage.
663
- * Returns true on success, false on error (swallows errors).
697
+ * Returns ok(true) if deleted, ok(false) if not found (not an error), err() on real failures.
664
698
  * @param client - The TableClient instance
665
699
  * @param partitionKey - The partition key
666
700
  * @param rowKey - The row key
667
- * @returns True if deleted successfully, false on error
701
+ * @returns Result with true if deleted, false if not found, or error on failure
668
702
  * @example
669
- * const deleted = await deleteEntity(client, 'session', sessionId);
703
+ * const result = await deleteEntity(client, 'session', sessionId);
704
+ * if (!result.ok) console.error(result.error);
670
705
  */
671
706
  async function deleteEntity(client, partitionKey, rowKey) {
672
707
  try {
673
708
  await client.deleteEntity(partitionKey, rowKey);
674
- return true;
709
+ return (0, shared_1.ok)(true);
675
710
  }
676
- catch {
677
- return false;
711
+ catch (error) {
712
+ if (isNotFoundError(error)) {
713
+ return (0, shared_1.ok)(false);
714
+ }
715
+ return (0, shared_1.err)((0, shared_1.getErrorMessage)(error, 'Delete failed'));
678
716
  }
679
717
  }
680
718
  /**
@@ -753,16 +791,20 @@ function initSessionAuth(config) {
753
791
  /**
754
792
  * Initializes session auth from environment variables.
755
793
  * Reads: SESSION_COOKIE_NAME, APP_BASE_URL, ALLOWED_EMAILS, ALLOWED_DOMAIN, ADMIN_EMAILS.
756
- * @param sendEmail - Required callback to send magic link emails
757
- * @param overrides - Optional config overrides (e.g., isEmailAllowed callback)
794
+ * Only callbacks go in the config object data comes from env vars.
795
+ * Use initSessionAuth() directly for full control.
796
+ * @param config - Required config with sendEmail callback and optional isEmailAllowed
758
797
  * @throws Error if sendEmail is not provided
759
798
  * @example
760
- * initSessionAuthFromEnv(async (to, magicLink) => {
761
- * await resend.emails.send({ to, html: `<a href="${magicLink}">Sign In</a>` });
762
- * return true;
763
- * }, { isEmailAllowed: async (email) => lookupInDatabase(email) });
799
+ * initSessionAuthFromEnv({
800
+ * sendEmail: async (to, magicLink) => {
801
+ * await resend.emails.send({ to, html: `<a href="${magicLink}">Sign In</a>` });
802
+ * return true;
803
+ * },
804
+ * isEmailAllowed: async (email) => lookupInDatabase(email),
805
+ * });
764
806
  */
765
- function initSessionAuthFromEnv(sendEmail, overrides) {
807
+ function initSessionAuthFromEnv(config) {
766
808
  const allowedEmailsStr = process.env.ALLOWED_EMAILS;
767
809
  const adminEmailsStr = process.env.ADMIN_EMAILS;
768
810
  initSessionAuth({
@@ -775,8 +817,8 @@ function initSessionAuthFromEnv(sendEmail, overrides) {
775
817
  adminEmails: adminEmailsStr
776
818
  ? adminEmailsStr.split(',').map(e => e.trim().toLowerCase())
777
819
  : undefined,
778
- ...overrides,
779
- sendEmail
820
+ isEmailAllowed: config.isEmailAllowed,
821
+ sendEmail: config.sendEmail,
780
822
  });
781
823
  }
782
824
  /**
@@ -972,10 +1014,11 @@ async function createMagicLink(email, request) {
972
1014
  if (!isValidEmail(normalizedEmail)) {
973
1015
  return (0, shared_1.err)('Valid email required', shared_1.HTTP_STATUS.BAD_REQUEST);
974
1016
  }
975
- // Check allowlist (custom callback overrides default)
976
- const emailAllowed = config.isEmailAllowed
977
- ? await config.isEmailAllowed(normalizedEmail)
978
- : isEmailAllowed(normalizedEmail);
1017
+ // Check allowlist (built-in first, custom extends can only widen access)
1018
+ const emailAllowed = isEmailAllowed(normalizedEmail)
1019
+ || (config.isEmailAllowed
1020
+ ? await config.isEmailAllowed(normalizedEmail)
1021
+ : false);
979
1022
  if (!emailAllowed) {
980
1023
  return (0, shared_1.err)('Email not allowed', shared_1.HTTP_STATUS.FORBIDDEN);
981
1024
  }
@@ -1088,10 +1131,10 @@ async function verifyMagicLink(token) {
1088
1131
  * Validates a session cookie and returns the user and session info.
1089
1132
  * Performs sliding window refresh if the session is close to expiry.
1090
1133
  * @param request - Request object with headers.get() method
1091
- * @returns User, session, and optional refreshed cookie, or null if invalid
1134
+ * @returns Result with user, session, and optional refreshed cookie
1092
1135
  * @example
1093
1136
  * const result = await validateSession(request);
1094
- * if (!result) return unauthorizedResponse();
1137
+ * if (!result.ok) return unauthorizedResponse();
1095
1138
  */
1096
1139
  async function validateSession(request) {
1097
1140
  const config = getSessionAuthConfig();
@@ -1099,20 +1142,20 @@ async function validateSession(request) {
1099
1142
  const cookies = parseCookies(request);
1100
1143
  const sessionId = cookies[cookieName];
1101
1144
  if (!sessionId)
1102
- return null;
1145
+ return (0, shared_1.err)('No session cookie', shared_1.HTTP_STATUS.UNAUTHORIZED);
1103
1146
  try {
1104
1147
  const sessionsClient = await getTableClient(SESSIONS_TABLE);
1105
1148
  const sessionEntity = await getEntityIfExists(sessionsClient, SESSION_PARTITION, sessionId);
1106
1149
  if (!sessionEntity)
1107
- return null;
1150
+ return (0, shared_1.err)('Invalid session', shared_1.HTTP_STATUS.UNAUTHORIZED);
1108
1151
  // Check expiry
1109
1152
  if (new Date(sessionEntity.expiresAt) < new Date())
1110
- return null;
1153
+ return (0, shared_1.err)('Session expired', shared_1.HTTP_STATUS.UNAUTHORIZED);
1111
1154
  // Get user
1112
1155
  const usersClient = await getTableClient(USERS_TABLE);
1113
1156
  const userEntity = await getEntityIfExists(usersClient, USER_PARTITION, sessionEntity.email.toLowerCase());
1114
1157
  if (!userEntity)
1115
- return null;
1158
+ return (0, shared_1.err)('User not found', shared_1.HTTP_STATUS.UNAUTHORIZED);
1116
1159
  const user = {
1117
1160
  id: userEntity.id,
1118
1161
  email: userEntity.email,
@@ -1144,30 +1187,30 @@ async function validateSession(request) {
1144
1187
  await upsertEntity(sessionsClient, updatedSession);
1145
1188
  refreshedCookie = createSessionCookie(sessionId);
1146
1189
  }
1147
- return { user, session, refreshedCookie };
1190
+ return (0, shared_1.ok)({ user, session, refreshedCookie });
1148
1191
  }
1149
1192
  catch {
1150
- return null;
1193
+ return (0, shared_1.err)('Session validation failed', shared_1.HTTP_STATUS.UNAUTHORIZED);
1151
1194
  }
1152
1195
  }
1153
1196
  // --- Session Operations ---
1154
1197
  /**
1155
1198
  * Convenience function: validates session and returns user with optional refresh headers.
1156
1199
  * @param request - Request object with headers.get() method
1157
- * @returns User and optional Set-Cookie headers, or null if not authenticated
1200
+ * @returns Result with user and optional Set-Cookie headers
1158
1201
  * @example
1159
1202
  * const result = await getSessionUser(request);
1160
- * if (!result) return unauthorizedResponse();
1161
- * return { headers: result.headers, jsonBody: { user: result.user } };
1203
+ * if (!result.ok) return resultToResponse(result);
1204
+ * const { user, headers } = result.data!;
1162
1205
  */
1163
1206
  async function getSessionUser(request) {
1164
1207
  const result = await validateSession(request);
1165
- if (!result)
1166
- return null;
1167
- const headers = result.refreshedCookie
1168
- ? { 'Set-Cookie': result.refreshedCookie }
1208
+ if (!result.ok)
1209
+ return result;
1210
+ const headers = result.data.refreshedCookie
1211
+ ? { 'Set-Cookie': result.data.refreshedCookie }
1169
1212
  : undefined;
1170
- return { user: result.user, headers };
1213
+ return (0, shared_1.ok)({ user: result.data.user, headers });
1171
1214
  }
1172
1215
  /**
1173
1216
  * Destroys the current session and returns a logout cookie string.
@@ -1204,13 +1247,13 @@ async function destroySession(request) {
1204
1247
  function withSessionAuth(handler) {
1205
1248
  return async (request, context) => {
1206
1249
  const result = await getSessionUser(request);
1207
- if (!result) {
1250
+ if (!result.ok) {
1208
1251
  return unauthorizedResponse('Not authenticated');
1209
1252
  }
1210
- const response = await handler(request, context, result);
1253
+ const response = await handler(request, context, result.data);
1211
1254
  // Merge refresh cookie headers
1212
- if (result.headers) {
1213
- response.headers = { ...result.headers, ...response.headers };
1255
+ if (result.data.headers) {
1256
+ response.headers = { ...result.data.headers, ...response.headers };
1214
1257
  }
1215
1258
  return response;
1216
1259
  };
@@ -1230,16 +1273,16 @@ function withSessionAuth(handler) {
1230
1273
  function withSessionAdminAuth(handler) {
1231
1274
  return async (request, context) => {
1232
1275
  const result = await getSessionUser(request);
1233
- if (!result) {
1276
+ if (!result.ok) {
1234
1277
  return unauthorizedResponse('Not authenticated');
1235
1278
  }
1236
- if (!result.user.isAdmin) {
1279
+ if (!result.data.user.isAdmin) {
1237
1280
  return forbiddenResponse('Admin access required');
1238
1281
  }
1239
- const response = await handler(request, context, result);
1282
+ const response = await handler(request, context, result.data);
1240
1283
  // Merge refresh cookie headers
1241
- if (result.headers) {
1242
- response.headers = { ...result.headers, ...response.headers };
1284
+ if (result.data.headers) {
1285
+ response.headers = { ...result.data.headers, ...response.headers };
1243
1286
  }
1244
1287
  return response;
1245
1288
  };
@@ -1260,8 +1303,8 @@ async function deleteExpiredSessions() {
1260
1303
  let deleted = 0;
1261
1304
  for (const session of sessions) {
1262
1305
  if (new Date(session.expiresAt) < now) {
1263
- const success = await deleteEntity(client, SESSION_PARTITION, session.rowKey);
1264
- if (success)
1306
+ const result = await deleteEntity(client, SESSION_PARTITION, session.rowKey);
1307
+ if (result.ok && result.data)
1265
1308
  deleted++;
1266
1309
  }
1267
1310
  }
@@ -1282,14 +1325,139 @@ async function deleteExpiredMagicLinks() {
1282
1325
  let deleted = 0;
1283
1326
  for (const link of links) {
1284
1327
  if (new Date(link.expiresAt) < now || link.used) {
1285
- const success = await deleteEntity(client, MAGICLINK_PARTITION, link.rowKey);
1286
- if (success)
1328
+ const result = await deleteEntity(client, MAGICLINK_PARTITION, link.rowKey);
1329
+ if (result.ok && result.data)
1287
1330
  deleted++;
1288
1331
  }
1289
1332
  }
1290
1333
  return deleted;
1291
1334
  }
1292
1335
  // =============================================================================
1336
+ // Key Vault Secret Resolution
1337
+ // =============================================================================
1338
+ /**
1339
+ * Pattern to match Azure Key Vault references in environment variables.
1340
+ * Format: @Microsoft.KeyVault(SecretUri=https://{vault}.vault.azure.net/secrets/{name}[/{version}])
1341
+ */
1342
+ const KEY_VAULT_PATTERN = /^@Microsoft\.KeyVault\(SecretUri=(.+)\)$/;
1343
+ /**
1344
+ * Parses a Key Vault secret URI into vault URL, secret name, and optional version.
1345
+ * @param secretUri - Full Key Vault secret URI
1346
+ * @returns Parsed components or null if invalid
1347
+ */
1348
+ function parseSecretUri(secretUri) {
1349
+ try {
1350
+ const url = new URL(secretUri);
1351
+ const pathParts = url.pathname.split('/').filter(Boolean);
1352
+ // Expected: /secrets/{name} or /secrets/{name}/{version}
1353
+ if (pathParts.length < 2 || pathParts[0] !== 'secrets') {
1354
+ return null;
1355
+ }
1356
+ return {
1357
+ vaultUrl: `${url.protocol}//${url.host}`,
1358
+ secretName: pathParts[1],
1359
+ secretVersion: pathParts[2]
1360
+ };
1361
+ }
1362
+ catch {
1363
+ return null;
1364
+ }
1365
+ }
1366
+ /**
1367
+ * Checks if any environment variables contain Key Vault references.
1368
+ * Useful for skipping resolution in development environments.
1369
+ * @returns True if any env vars match the @Microsoft.KeyVault pattern
1370
+ * @example
1371
+ * if (hasKeyVaultReferences()) {
1372
+ * await resolveKeyVaultReferences();
1373
+ * }
1374
+ */
1375
+ function hasKeyVaultReferences() {
1376
+ for (const value of Object.values(process.env)) {
1377
+ if (value && KEY_VAULT_PATTERN.test(value)) {
1378
+ return true;
1379
+ }
1380
+ }
1381
+ return false;
1382
+ }
1383
+ /**
1384
+ * Resolves all @Microsoft.KeyVault(SecretUri=...) references in process.env.
1385
+ * Replaces env var values in-place with the resolved secret values.
1386
+ * Uses DefaultAzureCredential for authentication (managed identity in Azure, az cli locally).
1387
+ *
1388
+ * Requires @azure/keyvault-secrets as a peer dependency.
1389
+ *
1390
+ * @returns Number of secrets resolved
1391
+ * @throws Error if any secret resolution fails (fail-fast)
1392
+ * @example
1393
+ * // In sessionAuthInit.ts (before any other initialization):
1394
+ * if (hasKeyVaultReferences()) {
1395
+ * const count = await resolveKeyVaultReferences();
1396
+ * console.log(`Resolved ${count} Key Vault secrets`);
1397
+ * }
1398
+ */
1399
+ async function resolveKeyVaultReferences() {
1400
+ // Dynamic import - only loaded when Key Vault references are present
1401
+ const { SecretClient } = await Promise.resolve().then(() => __importStar(require('@azure/keyvault-secrets')));
1402
+ // Group env vars by vault URL to reuse clients
1403
+ const vaultSecrets = new Map();
1404
+ for (const [key, value] of Object.entries(process.env)) {
1405
+ if (!value)
1406
+ continue;
1407
+ const match = value.match(KEY_VAULT_PATTERN);
1408
+ if (!match)
1409
+ continue;
1410
+ const parsed = parseSecretUri(match[1]);
1411
+ if (!parsed) {
1412
+ throw new Error(`Invalid Key Vault secret URI in ${key}: ${match[1]}`);
1413
+ }
1414
+ const existing = vaultSecrets.get(parsed.vaultUrl) ?? [];
1415
+ existing.push({ envKey: key, secretName: parsed.secretName, secretVersion: parsed.secretVersion });
1416
+ vaultSecrets.set(parsed.vaultUrl, existing);
1417
+ }
1418
+ if (vaultSecrets.size === 0) {
1419
+ return 0;
1420
+ }
1421
+ const cred = getCredential();
1422
+ let resolved = 0;
1423
+ for (const [vaultUrl, secrets] of vaultSecrets) {
1424
+ const client = new SecretClient(vaultUrl, cred);
1425
+ for (const { envKey, secretName, secretVersion } of secrets) {
1426
+ try {
1427
+ const secret = await client.getSecret(secretName, secretVersion ? { version: secretVersion } : undefined);
1428
+ if (!secret.value) {
1429
+ throw new Error(`Secret '${secretName}' in ${vaultUrl} has no value`);
1430
+ }
1431
+ process.env[envKey] = secret.value;
1432
+ resolved++;
1433
+ }
1434
+ catch (error) {
1435
+ const message = (0, shared_1.getErrorMessage)(error, 'Unknown error');
1436
+ throw new Error(`Failed to resolve Key Vault secret '${secretName}' for ${envKey}: ${message}`);
1437
+ }
1438
+ }
1439
+ }
1440
+ return resolved;
1441
+ }
1442
+ // =============================================================================
1443
+ // Result Conversion Helper
1444
+ // =============================================================================
1445
+ /**
1446
+ * Convert a failed Result to an HttpResponseInit.
1447
+ * Use after checking `!result.ok` to return the error as an HTTP response.
1448
+ * @param result - A failed Result (ok=false)
1449
+ * @returns HttpResponseInit with the error status and message
1450
+ * @example
1451
+ * const auth = requireAuth<UsernameTokenPayload>(authHeader);
1452
+ * if (!auth.ok) return resultToResponse(auth);
1453
+ */
1454
+ function resultToResponse(result) {
1455
+ return {
1456
+ status: result.status ?? shared_1.HTTP_STATUS.INTERNAL_ERROR,
1457
+ jsonBody: { error: result.error ?? 'Unknown error' }
1458
+ };
1459
+ }
1460
+ // =============================================================================
1293
1461
  // Re-exports from shared (for convenience)
1294
1462
  // =============================================================================
1295
1463
  var shared_2 = require("./shared");
package/dist/shared.d.ts CHANGED
@@ -15,13 +15,13 @@
15
15
  * { ok: false, error: 'Token expired' }
16
16
  *
17
17
  * // With status code (HTTP/push operations)
18
- * { ok: false, error: 'Subscription expired', statusCode: 410 }
18
+ * { ok: false, error: 'Subscription expired', status: 410 }
19
19
  */
20
20
  export interface Result<T> {
21
21
  ok: boolean;
22
22
  data?: T;
23
23
  error?: string;
24
- statusCode?: number;
24
+ status?: number;
25
25
  }
26
26
  /**
27
27
  * Creates a success result with data.
@@ -41,13 +41,13 @@ export declare function okVoid(): Result<void>;
41
41
  /**
42
42
  * Creates a failure result with an error message.
43
43
  * @param error - The error message
44
- * @param statusCode - Optional HTTP status code for API/push operations
44
+ * @param status - Optional HTTP status code for API/push operations
45
45
  * @returns A Result with ok=false and the error details
46
46
  * @example
47
47
  * return err('Token expired');
48
48
  * return err('Subscription gone', 410);
49
49
  */
50
- export declare function err<T>(error: string, statusCode?: number): Result<T>;
50
+ export declare function err<T = never>(error: string, status?: number): Result<T>;
51
51
  /**
52
52
  * Base JWT payload - all tokens include these fields
53
53
  * Projects extend this with their specific fields
package/dist/shared.js CHANGED
@@ -35,15 +35,15 @@ function okVoid() {
35
35
  /**
36
36
  * Creates a failure result with an error message.
37
37
  * @param error - The error message
38
- * @param statusCode - Optional HTTP status code for API/push operations
38
+ * @param status - Optional HTTP status code for API/push operations
39
39
  * @returns A Result with ok=false and the error details
40
40
  * @example
41
41
  * return err('Token expired');
42
42
  * return err('Subscription gone', 410);
43
43
  */
44
- function err(error, statusCode) {
45
- return statusCode !== undefined
46
- ? { ok: false, error, statusCode }
44
+ function err(error, status) {
45
+ return status !== undefined
46
+ ? { ok: false, error, status }
47
47
  : { ok: false, error };
48
48
  }
49
49
  // =============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markwharton/pwa-core",
3
- "version": "3.4.2",
3
+ "version": "4.0.0",
4
4
  "description": "Shared patterns for Azure PWA projects",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -30,12 +30,19 @@
30
30
  "peerDependencies": {
31
31
  "@azure/data-tables": "^13.0.0",
32
32
  "@azure/identity": "^4.0.0",
33
+ "@azure/keyvault-secrets": "^4.0.0",
33
34
  "jsonwebtoken": "^9.0.0"
34
35
  },
36
+ "peerDependenciesMeta": {
37
+ "@azure/keyvault-secrets": {
38
+ "optional": true
39
+ }
40
+ },
35
41
  "devDependencies": {
36
42
  "@azure/data-tables": "^13.2.2",
37
43
  "@azure/functions": "^4.5.0",
38
44
  "@azure/identity": "^4.5.0",
45
+ "@azure/keyvault-secrets": "^4.10.0",
39
46
  "@types/jsonwebtoken": "^9.0.5",
40
47
  "@types/node": "^20.10.0",
41
48
  "jsonwebtoken": "^9.0.2",