@markwharton/pwa-core 3.4.1 → 3.5.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 +30 -2
- package/dist/server.js +146 -2
- package/package.json +8 -1
package/dist/server.d.ts
CHANGED
|
@@ -481,14 +481,15 @@ export declare function initSessionAuth(config: SessionAuthConfig): void;
|
|
|
481
481
|
* Initializes session auth from environment variables.
|
|
482
482
|
* Reads: SESSION_COOKIE_NAME, APP_BASE_URL, ALLOWED_EMAILS, ALLOWED_DOMAIN, ADMIN_EMAILS.
|
|
483
483
|
* @param sendEmail - Required callback to send magic link emails
|
|
484
|
+
* @param overrides - Optional config overrides (e.g., isEmailAllowed callback)
|
|
484
485
|
* @throws Error if sendEmail is not provided
|
|
485
486
|
* @example
|
|
486
487
|
* initSessionAuthFromEnv(async (to, magicLink) => {
|
|
487
488
|
* await resend.emails.send({ to, html: `<a href="${magicLink}">Sign In</a>` });
|
|
488
489
|
* return true;
|
|
489
|
-
* });
|
|
490
|
+
* }, { isEmailAllowed: async (email) => lookupInDatabase(email) });
|
|
490
491
|
*/
|
|
491
|
-
export declare function initSessionAuthFromEnv(sendEmail: (to: string, magicLink: string) => Promise<boolean
|
|
492
|
+
export declare function initSessionAuthFromEnv(sendEmail: (to: string, magicLink: string) => Promise<boolean>, overrides?: Partial<Omit<SessionAuthConfig, 'sendEmail'>>): void;
|
|
492
493
|
/**
|
|
493
494
|
* Parses cookies from a request's Cookie header.
|
|
494
495
|
* @param request - Request object with headers.get() method
|
|
@@ -675,4 +676,31 @@ export declare function deleteExpiredSessions(): Promise<number>;
|
|
|
675
676
|
* context.log(`Cleaned up ${deleted} expired magic links`);
|
|
676
677
|
*/
|
|
677
678
|
export declare function deleteExpiredMagicLinks(): Promise<number>;
|
|
679
|
+
/**
|
|
680
|
+
* Checks if any environment variables contain Key Vault references.
|
|
681
|
+
* Useful for skipping resolution in development environments.
|
|
682
|
+
* @returns True if any env vars match the @Microsoft.KeyVault pattern
|
|
683
|
+
* @example
|
|
684
|
+
* if (hasKeyVaultReferences()) {
|
|
685
|
+
* await resolveKeyVaultReferences();
|
|
686
|
+
* }
|
|
687
|
+
*/
|
|
688
|
+
export declare function hasKeyVaultReferences(): boolean;
|
|
689
|
+
/**
|
|
690
|
+
* Resolves all @Microsoft.KeyVault(SecretUri=...) references in process.env.
|
|
691
|
+
* Replaces env var values in-place with the resolved secret values.
|
|
692
|
+
* Uses DefaultAzureCredential for authentication (managed identity in Azure, az cli locally).
|
|
693
|
+
*
|
|
694
|
+
* Requires @azure/keyvault-secrets as a peer dependency.
|
|
695
|
+
*
|
|
696
|
+
* @returns Number of secrets resolved
|
|
697
|
+
* @throws Error if any secret resolution fails (fail-fast)
|
|
698
|
+
* @example
|
|
699
|
+
* // In sessionAuthInit.ts (before any other initialization):
|
|
700
|
+
* if (hasKeyVaultReferences()) {
|
|
701
|
+
* const count = await resolveKeyVaultReferences();
|
|
702
|
+
* console.log(`Resolved ${count} Key Vault secrets`);
|
|
703
|
+
* }
|
|
704
|
+
*/
|
|
705
|
+
export declare function resolveKeyVaultReferences(): Promise<number>;
|
|
678
706
|
export { Result, ok, okVoid, err, getErrorMessage, BaseJwtPayload, UserTokenPayload, UsernameTokenPayload, RoleTokenPayload, hasUsername, hasRole, isAdmin, HTTP_STATUS, HttpStatus, ErrorResponse, SessionUser, SessionInfo, MagicLinkRequest, SessionAuthResponse } from './shared';
|
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,8 @@ 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;
|
|
69
104
|
const crypto_1 = require("crypto");
|
|
70
105
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
71
106
|
const data_tables_1 = require("@azure/data-tables");
|
|
@@ -754,14 +789,15 @@ function initSessionAuth(config) {
|
|
|
754
789
|
* Initializes session auth from environment variables.
|
|
755
790
|
* Reads: SESSION_COOKIE_NAME, APP_BASE_URL, ALLOWED_EMAILS, ALLOWED_DOMAIN, ADMIN_EMAILS.
|
|
756
791
|
* @param sendEmail - Required callback to send magic link emails
|
|
792
|
+
* @param overrides - Optional config overrides (e.g., isEmailAllowed callback)
|
|
757
793
|
* @throws Error if sendEmail is not provided
|
|
758
794
|
* @example
|
|
759
795
|
* initSessionAuthFromEnv(async (to, magicLink) => {
|
|
760
796
|
* await resend.emails.send({ to, html: `<a href="${magicLink}">Sign In</a>` });
|
|
761
797
|
* return true;
|
|
762
|
-
* });
|
|
798
|
+
* }, { isEmailAllowed: async (email) => lookupInDatabase(email) });
|
|
763
799
|
*/
|
|
764
|
-
function initSessionAuthFromEnv(sendEmail) {
|
|
800
|
+
function initSessionAuthFromEnv(sendEmail, overrides) {
|
|
765
801
|
const allowedEmailsStr = process.env.ALLOWED_EMAILS;
|
|
766
802
|
const adminEmailsStr = process.env.ADMIN_EMAILS;
|
|
767
803
|
initSessionAuth({
|
|
@@ -774,6 +810,7 @@ function initSessionAuthFromEnv(sendEmail) {
|
|
|
774
810
|
adminEmails: adminEmailsStr
|
|
775
811
|
? adminEmailsStr.split(',').map(e => e.trim().toLowerCase())
|
|
776
812
|
: undefined,
|
|
813
|
+
...overrides,
|
|
777
814
|
sendEmail
|
|
778
815
|
});
|
|
779
816
|
}
|
|
@@ -1288,6 +1325,113 @@ async function deleteExpiredMagicLinks() {
|
|
|
1288
1325
|
return deleted;
|
|
1289
1326
|
}
|
|
1290
1327
|
// =============================================================================
|
|
1328
|
+
// Key Vault Secret Resolution
|
|
1329
|
+
// =============================================================================
|
|
1330
|
+
/**
|
|
1331
|
+
* Pattern to match Azure Key Vault references in environment variables.
|
|
1332
|
+
* Format: @Microsoft.KeyVault(SecretUri=https://{vault}.vault.azure.net/secrets/{name}[/{version}])
|
|
1333
|
+
*/
|
|
1334
|
+
const KEY_VAULT_PATTERN = /^@Microsoft\.KeyVault\(SecretUri=(.+)\)$/;
|
|
1335
|
+
/**
|
|
1336
|
+
* Parses a Key Vault secret URI into vault URL, secret name, and optional version.
|
|
1337
|
+
* @param secretUri - Full Key Vault secret URI
|
|
1338
|
+
* @returns Parsed components or null if invalid
|
|
1339
|
+
*/
|
|
1340
|
+
function parseSecretUri(secretUri) {
|
|
1341
|
+
try {
|
|
1342
|
+
const url = new URL(secretUri);
|
|
1343
|
+
const pathParts = url.pathname.split('/').filter(Boolean);
|
|
1344
|
+
// Expected: /secrets/{name} or /secrets/{name}/{version}
|
|
1345
|
+
if (pathParts.length < 2 || pathParts[0] !== 'secrets') {
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
return {
|
|
1349
|
+
vaultUrl: `${url.protocol}//${url.host}`,
|
|
1350
|
+
secretName: pathParts[1],
|
|
1351
|
+
secretVersion: pathParts[2]
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
catch {
|
|
1355
|
+
return null;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Checks if any environment variables contain Key Vault references.
|
|
1360
|
+
* Useful for skipping resolution in development environments.
|
|
1361
|
+
* @returns True if any env vars match the @Microsoft.KeyVault pattern
|
|
1362
|
+
* @example
|
|
1363
|
+
* if (hasKeyVaultReferences()) {
|
|
1364
|
+
* await resolveKeyVaultReferences();
|
|
1365
|
+
* }
|
|
1366
|
+
*/
|
|
1367
|
+
function hasKeyVaultReferences() {
|
|
1368
|
+
for (const value of Object.values(process.env)) {
|
|
1369
|
+
if (value && KEY_VAULT_PATTERN.test(value)) {
|
|
1370
|
+
return true;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return false;
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Resolves all @Microsoft.KeyVault(SecretUri=...) references in process.env.
|
|
1377
|
+
* Replaces env var values in-place with the resolved secret values.
|
|
1378
|
+
* Uses DefaultAzureCredential for authentication (managed identity in Azure, az cli locally).
|
|
1379
|
+
*
|
|
1380
|
+
* Requires @azure/keyvault-secrets as a peer dependency.
|
|
1381
|
+
*
|
|
1382
|
+
* @returns Number of secrets resolved
|
|
1383
|
+
* @throws Error if any secret resolution fails (fail-fast)
|
|
1384
|
+
* @example
|
|
1385
|
+
* // In sessionAuthInit.ts (before any other initialization):
|
|
1386
|
+
* if (hasKeyVaultReferences()) {
|
|
1387
|
+
* const count = await resolveKeyVaultReferences();
|
|
1388
|
+
* console.log(`Resolved ${count} Key Vault secrets`);
|
|
1389
|
+
* }
|
|
1390
|
+
*/
|
|
1391
|
+
async function resolveKeyVaultReferences() {
|
|
1392
|
+
// Dynamic import - only loaded when Key Vault references are present
|
|
1393
|
+
const { SecretClient } = await Promise.resolve().then(() => __importStar(require('@azure/keyvault-secrets')));
|
|
1394
|
+
// Group env vars by vault URL to reuse clients
|
|
1395
|
+
const vaultSecrets = new Map();
|
|
1396
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
1397
|
+
if (!value)
|
|
1398
|
+
continue;
|
|
1399
|
+
const match = value.match(KEY_VAULT_PATTERN);
|
|
1400
|
+
if (!match)
|
|
1401
|
+
continue;
|
|
1402
|
+
const parsed = parseSecretUri(match[1]);
|
|
1403
|
+
if (!parsed) {
|
|
1404
|
+
throw new Error(`Invalid Key Vault secret URI in ${key}: ${match[1]}`);
|
|
1405
|
+
}
|
|
1406
|
+
const existing = vaultSecrets.get(parsed.vaultUrl) ?? [];
|
|
1407
|
+
existing.push({ envKey: key, secretName: parsed.secretName, secretVersion: parsed.secretVersion });
|
|
1408
|
+
vaultSecrets.set(parsed.vaultUrl, existing);
|
|
1409
|
+
}
|
|
1410
|
+
if (vaultSecrets.size === 0) {
|
|
1411
|
+
return 0;
|
|
1412
|
+
}
|
|
1413
|
+
const cred = getCredential();
|
|
1414
|
+
let resolved = 0;
|
|
1415
|
+
for (const [vaultUrl, secrets] of vaultSecrets) {
|
|
1416
|
+
const client = new SecretClient(vaultUrl, cred);
|
|
1417
|
+
for (const { envKey, secretName, secretVersion } of secrets) {
|
|
1418
|
+
try {
|
|
1419
|
+
const secret = await client.getSecret(secretName, secretVersion ? { version: secretVersion } : undefined);
|
|
1420
|
+
if (!secret.value) {
|
|
1421
|
+
throw new Error(`Secret '${secretName}' in ${vaultUrl} has no value`);
|
|
1422
|
+
}
|
|
1423
|
+
process.env[envKey] = secret.value;
|
|
1424
|
+
resolved++;
|
|
1425
|
+
}
|
|
1426
|
+
catch (error) {
|
|
1427
|
+
const message = (0, shared_1.getErrorMessage)(error, 'Unknown error');
|
|
1428
|
+
throw new Error(`Failed to resolve Key Vault secret '${secretName}' for ${envKey}: ${message}`);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return resolved;
|
|
1433
|
+
}
|
|
1434
|
+
// =============================================================================
|
|
1291
1435
|
// Re-exports from shared (for convenience)
|
|
1292
1436
|
// =============================================================================
|
|
1293
1437
|
var shared_2 = require("./shared");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markwharton/pwa-core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.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",
|