@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/client.d.ts +47 -89
- package/dist/client.js +102 -252
- package/dist/server.d.ts +83 -53
- package/dist/server.js +235 -67
- package/dist/shared.d.ts +4 -4
- package/dist/shared.js +4 -4
- package/package.json +8 -1
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(
|
|
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
|
|
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
|
|
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.
|
|
200
|
-
* console.log(auth.
|
|
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
|
|
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
|
|
243
|
+
return (0, shared_1.err)(result.error ?? 'Unauthorized', shared_1.HTTP_STATUS.UNAUTHORIZED);
|
|
210
244
|
}
|
|
211
|
-
return
|
|
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
|
|
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.
|
|
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.
|
|
258
|
+
if (!auth.ok)
|
|
225
259
|
return auth;
|
|
226
|
-
if (!(0, shared_1.isAdmin)(auth.
|
|
227
|
-
return
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
701
|
+
* @returns Result with true if deleted, false if not found, or error on failure
|
|
668
702
|
* @example
|
|
669
|
-
* const
|
|
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
|
-
|
|
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
|
-
*
|
|
757
|
-
*
|
|
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(
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
*
|
|
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(
|
|
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
|
-
|
|
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
|
|
976
|
-
const emailAllowed =
|
|
977
|
-
|
|
978
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1200
|
+
* @returns Result with user and optional Set-Cookie headers
|
|
1158
1201
|
* @example
|
|
1159
1202
|
* const result = await getSessionUser(request);
|
|
1160
|
-
* if (!result) return
|
|
1161
|
-
*
|
|
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
|
|
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
|
|
1264
|
-
if (
|
|
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
|
|
1286
|
-
if (
|
|
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',
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
45
|
-
return
|
|
46
|
-
? { ok: false, error,
|
|
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
|
+
"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",
|