@digilogiclabs/platform-core 1.9.0 → 1.11.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/auth.d.mts +217 -3
- package/dist/auth.d.ts +217 -3
- package/dist/auth.js +251 -0
- package/dist/auth.js.map +1 -1
- package/dist/auth.mjs +244 -0
- package/dist/auth.mjs.map +1 -1
- package/dist/email-templates.js +13 -6
- package/dist/email-templates.js.map +1 -1
- package/dist/email-templates.mjs +13 -6
- package/dist/email-templates.mjs.map +1 -1
- package/dist/{env-DHPZR3Lv.d.mts → env-CYKVNpLl.d.mts} +6 -0
- package/dist/{env-DHPZR3Lv.d.ts → env-CYKVNpLl.d.ts} +6 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -0
- package/dist/index.mjs.map +1 -1
- package/dist/migrate.js +0 -0
- package/package.json +11 -11
package/dist/auth.js
CHANGED
|
@@ -38,9 +38,11 @@ __export(auth_exports, {
|
|
|
38
38
|
StandardAuditActions: () => StandardAuditActions,
|
|
39
39
|
StandardRateLimitPresets: () => StandardRateLimitPresets,
|
|
40
40
|
WrapperPresets: () => WrapperPresets,
|
|
41
|
+
addRateLimitHeaders: () => addRateLimitHeaders,
|
|
41
42
|
buildAllowlist: () => buildAllowlist,
|
|
42
43
|
buildAuthCookies: () => buildAuthCookies,
|
|
43
44
|
buildErrorBody: () => buildErrorBody,
|
|
45
|
+
buildFederatedLogoutHandler: () => buildFederatedLogoutHandler,
|
|
44
46
|
buildKeycloakCallbacks: () => buildKeycloakCallbacks,
|
|
45
47
|
buildPagination: () => buildPagination,
|
|
46
48
|
buildRateLimitHeaders: () => buildRateLimitHeaders,
|
|
@@ -58,6 +60,7 @@ __export(auth_exports, {
|
|
|
58
60
|
createAuditLogger: () => createAuditLogger,
|
|
59
61
|
createBetaClient: () => createBetaClient,
|
|
60
62
|
createFeatureFlags: () => createFeatureFlags,
|
|
63
|
+
createLazyRateLimitStore: () => createLazyRateLimitStore,
|
|
61
64
|
createMemoryRateLimitStore: () => createMemoryRateLimitStore,
|
|
62
65
|
createRedisRateLimitStore: () => createRedisRateLimitStore,
|
|
63
66
|
createSafeTextSchema: () => createSafeTextSchema,
|
|
@@ -72,6 +75,7 @@ __export(auth_exports, {
|
|
|
72
75
|
extractClientIp: () => extractClientIp,
|
|
73
76
|
fetchBetaSettings: () => fetchBetaSettings,
|
|
74
77
|
getBoolEnv: () => getBoolEnv,
|
|
78
|
+
getClientIp: () => getClientIp,
|
|
75
79
|
getCorrelationId: () => getCorrelationId,
|
|
76
80
|
getEndSessionEndpoint: () => getEndSessionEndpoint,
|
|
77
81
|
getEnvSummary: () => getEnvSummary,
|
|
@@ -89,15 +93,18 @@ __export(auth_exports, {
|
|
|
89
93
|
isTokenExpired: () => isTokenExpired,
|
|
90
94
|
isValidBearerToken: () => isValidBearerToken,
|
|
91
95
|
parseKeycloakRoles: () => parseKeycloakRoles,
|
|
96
|
+
rateLimitResponse: () => rateLimitResponse,
|
|
92
97
|
refreshKeycloakToken: () => refreshKeycloakToken,
|
|
93
98
|
resetRateLimitForKey: () => resetRateLimitForKey,
|
|
94
99
|
resolveIdentifier: () => resolveIdentifier,
|
|
95
100
|
resolveRateLimitIdentifier: () => resolveRateLimitIdentifier,
|
|
101
|
+
safeValidate: () => safeValidate,
|
|
96
102
|
sanitizeApiError: () => sanitizeApiError,
|
|
97
103
|
storeBetaCode: () => storeBetaCode,
|
|
98
104
|
stripHtml: () => stripHtml,
|
|
99
105
|
validateBetaCode: () => validateBetaCode,
|
|
100
106
|
validateEnvVars: () => validateEnvVars,
|
|
107
|
+
verifyCronAuth: () => verifyCronAuth,
|
|
101
108
|
zodErrorResponse: () => zodErrorResponse
|
|
102
109
|
});
|
|
103
110
|
module.exports = __toCommonJS(auth_exports);
|
|
@@ -666,6 +673,12 @@ var CommonRateLimits = {
|
|
|
666
673
|
limit: 10,
|
|
667
674
|
windowSeconds: 3600,
|
|
668
675
|
blockDurationSeconds: 3600
|
|
676
|
+
},
|
|
677
|
+
/** Beta code validation: 5/min with 5min block (prevents brute force guessing) */
|
|
678
|
+
betaValidation: {
|
|
679
|
+
limit: 5,
|
|
680
|
+
windowSeconds: 60,
|
|
681
|
+
blockDurationSeconds: 300
|
|
669
682
|
}
|
|
670
683
|
};
|
|
671
684
|
function createMemoryRateLimitStore() {
|
|
@@ -1209,6 +1222,71 @@ function isValidBearerToken(request, secret) {
|
|
|
1209
1222
|
if (!token) return false;
|
|
1210
1223
|
return constantTimeEqual(token, secret);
|
|
1211
1224
|
}
|
|
1225
|
+
function verifyCronAuth(request, secret) {
|
|
1226
|
+
const authHeader = request.headers.get("authorization");
|
|
1227
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
1228
|
+
return new Response(JSON.stringify({ error: "Missing authorization" }), {
|
|
1229
|
+
status: 401,
|
|
1230
|
+
headers: { "Content-Type": "application/json" }
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
const token = authHeader.slice(7);
|
|
1234
|
+
const expectedSecret = secret ?? process.env.CRON_SECRET;
|
|
1235
|
+
if (!expectedSecret) {
|
|
1236
|
+
console.error("[verifyCronAuth] CRON_SECRET not configured");
|
|
1237
|
+
return new Response(
|
|
1238
|
+
JSON.stringify({ error: "Server configuration error" }),
|
|
1239
|
+
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
if (!constantTimeEqual(token, expectedSecret)) {
|
|
1243
|
+
return new Response(JSON.stringify({ error: "Invalid authorization" }), {
|
|
1244
|
+
status: 401,
|
|
1245
|
+
headers: { "Content-Type": "application/json" }
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
function getClientIp(request) {
|
|
1251
|
+
return request.headers.get("cf-connecting-ip") || request.headers.get("x-real-ip") || request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
|
|
1252
|
+
}
|
|
1253
|
+
function rateLimitResponse(result) {
|
|
1254
|
+
const retryAfter = Math.ceil(result.resetMs / 1e3);
|
|
1255
|
+
const resetTimestamp = Math.ceil(Date.now() / 1e3 + result.resetMs / 1e3);
|
|
1256
|
+
return new Response(
|
|
1257
|
+
JSON.stringify({ error: "Too many requests", retryAfter }),
|
|
1258
|
+
{
|
|
1259
|
+
status: 429,
|
|
1260
|
+
headers: {
|
|
1261
|
+
"Content-Type": "application/json",
|
|
1262
|
+
"X-RateLimit-Limit": String(result.limit),
|
|
1263
|
+
"X-RateLimit-Remaining": "0",
|
|
1264
|
+
"X-RateLimit-Reset": String(resetTimestamp),
|
|
1265
|
+
"Retry-After": String(retryAfter)
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
function addRateLimitHeaders(response, result) {
|
|
1271
|
+
const resetTimestamp = Math.ceil(Date.now() / 1e3 + result.resetMs / 1e3);
|
|
1272
|
+
response.headers.set("X-RateLimit-Limit", String(result.limit));
|
|
1273
|
+
response.headers.set("X-RateLimit-Remaining", String(result.remaining));
|
|
1274
|
+
response.headers.set("X-RateLimit-Reset", String(resetTimestamp));
|
|
1275
|
+
return response;
|
|
1276
|
+
}
|
|
1277
|
+
function safeValidate(schema, data) {
|
|
1278
|
+
const result = schema.safeParse(data);
|
|
1279
|
+
if (result.success) {
|
|
1280
|
+
return { success: true, data: result.data };
|
|
1281
|
+
}
|
|
1282
|
+
return {
|
|
1283
|
+
success: false,
|
|
1284
|
+
errors: (result.error?.issues || []).map((issue) => ({
|
|
1285
|
+
field: issue.path.join("."),
|
|
1286
|
+
message: issue.message
|
|
1287
|
+
}))
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1212
1290
|
|
|
1213
1291
|
// src/auth/beta-client.ts
|
|
1214
1292
|
var DEFAULT_CONFIG = {
|
|
@@ -1317,6 +1395,172 @@ function clearStoredBetaCode(config = {}) {
|
|
|
1317
1395
|
}
|
|
1318
1396
|
}
|
|
1319
1397
|
|
|
1398
|
+
// src/auth/federated-logout.ts
|
|
1399
|
+
function expireCookie(name, options) {
|
|
1400
|
+
const parts = [
|
|
1401
|
+
`${name}=`,
|
|
1402
|
+
"Path=/",
|
|
1403
|
+
"Expires=Thu, 01 Jan 1970 00:00:00 GMT",
|
|
1404
|
+
"Max-Age=0",
|
|
1405
|
+
"SameSite=Lax"
|
|
1406
|
+
];
|
|
1407
|
+
if (options?.hostPrefix) {
|
|
1408
|
+
parts.push("Secure");
|
|
1409
|
+
} else {
|
|
1410
|
+
parts.push("HttpOnly");
|
|
1411
|
+
if (options?.domain) parts.push(`Domain=${options.domain}`);
|
|
1412
|
+
if (options?.secure) parts.push("Secure");
|
|
1413
|
+
}
|
|
1414
|
+
return parts.join("; ");
|
|
1415
|
+
}
|
|
1416
|
+
function isAllowedCallbackUrl(url, baseUrl) {
|
|
1417
|
+
if (url.startsWith("/") && !url.startsWith("//")) return true;
|
|
1418
|
+
try {
|
|
1419
|
+
const parsed = new URL(url);
|
|
1420
|
+
const base = new URL(baseUrl);
|
|
1421
|
+
const allowedHosts = [base.hostname, `www.${base.hostname}`];
|
|
1422
|
+
if (base.hostname.startsWith("www.")) {
|
|
1423
|
+
allowedHosts.push(base.hostname.slice(4));
|
|
1424
|
+
}
|
|
1425
|
+
return allowedHosts.includes(parsed.hostname);
|
|
1426
|
+
} catch {
|
|
1427
|
+
return false;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
var AUTH_COOKIE_NAMES = [
|
|
1431
|
+
"authjs.session-token",
|
|
1432
|
+
"__Secure-authjs.session-token",
|
|
1433
|
+
"authjs.callback-url",
|
|
1434
|
+
"__Secure-authjs.callback-url",
|
|
1435
|
+
"authjs.csrf-token",
|
|
1436
|
+
"__Secure-authjs.csrf-token",
|
|
1437
|
+
"authjs.pkce.code_verifier",
|
|
1438
|
+
"__Secure-authjs.pkce.code_verifier",
|
|
1439
|
+
"authjs.state",
|
|
1440
|
+
"__Secure-authjs.state",
|
|
1441
|
+
// Legacy next-auth names
|
|
1442
|
+
"next-auth.session-token",
|
|
1443
|
+
"__Secure-next-auth.session-token",
|
|
1444
|
+
"next-auth.callback-url",
|
|
1445
|
+
"__Secure-next-auth.callback-url",
|
|
1446
|
+
"next-auth.csrf-token",
|
|
1447
|
+
"__Secure-next-auth.csrf-token"
|
|
1448
|
+
];
|
|
1449
|
+
var HOST_COOKIES = [
|
|
1450
|
+
"__Host-authjs.csrf-token",
|
|
1451
|
+
"__Host-next-auth.csrf-token"
|
|
1452
|
+
];
|
|
1453
|
+
function buildFederatedLogoutHandler(config) {
|
|
1454
|
+
const { auth, domain, baseUrlFallback, extraCookies = [], onError } = config;
|
|
1455
|
+
return async function GET(request) {
|
|
1456
|
+
const session = await auth();
|
|
1457
|
+
const url = new URL(request.url);
|
|
1458
|
+
const rawCallbackUrl = url.searchParams.get("callbackUrl") || "/";
|
|
1459
|
+
const queryIdToken = url.searchParams.get("id_token_hint");
|
|
1460
|
+
const baseUrl = process.env.NEXTAUTH_URL || process.env.AUTH_URL || baseUrlFallback;
|
|
1461
|
+
const callbackUrl = isAllowedCallbackUrl(rawCallbackUrl, baseUrl) ? rawCallbackUrl : "/";
|
|
1462
|
+
const postLogoutRedirectUri = callbackUrl.startsWith("http") ? callbackUrl : `${baseUrl}${callbackUrl}`;
|
|
1463
|
+
const keycloakIssuer = process.env.AUTH_KEYCLOAK_ISSUER;
|
|
1464
|
+
if (!keycloakIssuer) {
|
|
1465
|
+
onError?.("Missing AUTH_KEYCLOAK_ISSUER");
|
|
1466
|
+
return Response.redirect(
|
|
1467
|
+
new URL(
|
|
1468
|
+
"/api/auth/signout?callbackUrl=" + encodeURIComponent(callbackUrl),
|
|
1469
|
+
request.url
|
|
1470
|
+
).toString()
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
const refreshToken = session?.refreshToken;
|
|
1474
|
+
if (refreshToken) {
|
|
1475
|
+
try {
|
|
1476
|
+
const revokeUrl = `${keycloakIssuer}/protocol/openid-connect/revoke`;
|
|
1477
|
+
const clientId2 = process.env.AUTH_KEYCLOAK_ID;
|
|
1478
|
+
const clientSecret = process.env.AUTH_KEYCLOAK_SECRET;
|
|
1479
|
+
await fetch(revokeUrl, {
|
|
1480
|
+
method: "POST",
|
|
1481
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1482
|
+
body: new URLSearchParams({
|
|
1483
|
+
token: refreshToken,
|
|
1484
|
+
token_type_hint: "refresh_token",
|
|
1485
|
+
...clientId2 && { client_id: clientId2 },
|
|
1486
|
+
...clientSecret && { client_secret: clientSecret }
|
|
1487
|
+
})
|
|
1488
|
+
});
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
onError?.("Token revocation failed", err);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
const keycloakLogoutUrl = new URL(
|
|
1494
|
+
`${keycloakIssuer}/protocol/openid-connect/logout`
|
|
1495
|
+
);
|
|
1496
|
+
keycloakLogoutUrl.searchParams.set(
|
|
1497
|
+
"post_logout_redirect_uri",
|
|
1498
|
+
postLogoutRedirectUri
|
|
1499
|
+
);
|
|
1500
|
+
const clientId = process.env.AUTH_KEYCLOAK_ID;
|
|
1501
|
+
if (clientId) keycloakLogoutUrl.searchParams.set("client_id", clientId);
|
|
1502
|
+
const idToken = session?.idToken || queryIdToken;
|
|
1503
|
+
if (idToken) keycloakLogoutUrl.searchParams.set("id_token_hint", idToken);
|
|
1504
|
+
const escapedUrl = keycloakLogoutUrl.toString().replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1505
|
+
const html = `<!DOCTYPE html>
|
|
1506
|
+
<html><head>
|
|
1507
|
+
<meta charset="utf-8">
|
|
1508
|
+
<meta http-equiv="refresh" content="0;url=${escapedUrl}">
|
|
1509
|
+
<title>Signing out...</title>
|
|
1510
|
+
<style>body{display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;font-family:system-ui,sans-serif;background:#0a0a0a;color:#fff}p{font-size:1.1rem;opacity:0.7}</style>
|
|
1511
|
+
</head><body>
|
|
1512
|
+
<p>Signing out…</p>
|
|
1513
|
+
<script>window.location.replace(${JSON.stringify(keycloakLogoutUrl.toString()).replace(/</g, "\\u003c")});</script>
|
|
1514
|
+
</body></html>`;
|
|
1515
|
+
const response = new Response(html, {
|
|
1516
|
+
status: 200,
|
|
1517
|
+
headers: {
|
|
1518
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1519
|
+
"Cache-Control": "no-store, no-cache, must-revalidate",
|
|
1520
|
+
Pragma: "no-cache"
|
|
1521
|
+
}
|
|
1522
|
+
});
|
|
1523
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
1524
|
+
const isSecure = isProduction;
|
|
1525
|
+
const allCookieNames = [...AUTH_COOKIE_NAMES, ...extraCookies];
|
|
1526
|
+
for (const name of allCookieNames) {
|
|
1527
|
+
const needsSecure = isSecure || name.startsWith("__Secure-");
|
|
1528
|
+
response.headers.append(
|
|
1529
|
+
"Set-Cookie",
|
|
1530
|
+
expireCookie(name, { secure: needsSecure })
|
|
1531
|
+
);
|
|
1532
|
+
if (isProduction) {
|
|
1533
|
+
response.headers.append(
|
|
1534
|
+
"Set-Cookie",
|
|
1535
|
+
expireCookie(name, { domain, secure: needsSecure })
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
for (const name of HOST_COOKIES) {
|
|
1540
|
+
response.headers.append(
|
|
1541
|
+
"Set-Cookie",
|
|
1542
|
+
expireCookie(name, { hostPrefix: true })
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
return response;
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// src/auth/lazy-rate-limit-store.ts
|
|
1550
|
+
function createLazyRateLimitStore(getRedis, options = {}) {
|
|
1551
|
+
const { keyPrefix = "rl:" } = options;
|
|
1552
|
+
let store;
|
|
1553
|
+
let initialized = false;
|
|
1554
|
+
return function getRateLimitStore() {
|
|
1555
|
+
if (initialized) return store;
|
|
1556
|
+
initialized = true;
|
|
1557
|
+
const redis = getRedis();
|
|
1558
|
+
if (!redis) return void 0;
|
|
1559
|
+
store = createRedisRateLimitStore(redis, { keyPrefix });
|
|
1560
|
+
return store;
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1320
1564
|
// src/env.ts
|
|
1321
1565
|
function getRequiredEnv(key) {
|
|
1322
1566
|
const value = process.env[key];
|
|
@@ -1427,9 +1671,11 @@ function getEnvSummary(keys) {
|
|
|
1427
1671
|
StandardAuditActions,
|
|
1428
1672
|
StandardRateLimitPresets,
|
|
1429
1673
|
WrapperPresets,
|
|
1674
|
+
addRateLimitHeaders,
|
|
1430
1675
|
buildAllowlist,
|
|
1431
1676
|
buildAuthCookies,
|
|
1432
1677
|
buildErrorBody,
|
|
1678
|
+
buildFederatedLogoutHandler,
|
|
1433
1679
|
buildKeycloakCallbacks,
|
|
1434
1680
|
buildPagination,
|
|
1435
1681
|
buildRateLimitHeaders,
|
|
@@ -1447,6 +1693,7 @@ function getEnvSummary(keys) {
|
|
|
1447
1693
|
createAuditLogger,
|
|
1448
1694
|
createBetaClient,
|
|
1449
1695
|
createFeatureFlags,
|
|
1696
|
+
createLazyRateLimitStore,
|
|
1450
1697
|
createMemoryRateLimitStore,
|
|
1451
1698
|
createRedisRateLimitStore,
|
|
1452
1699
|
createSafeTextSchema,
|
|
@@ -1461,6 +1708,7 @@ function getEnvSummary(keys) {
|
|
|
1461
1708
|
extractClientIp,
|
|
1462
1709
|
fetchBetaSettings,
|
|
1463
1710
|
getBoolEnv,
|
|
1711
|
+
getClientIp,
|
|
1464
1712
|
getCorrelationId,
|
|
1465
1713
|
getEndSessionEndpoint,
|
|
1466
1714
|
getEnvSummary,
|
|
@@ -1478,15 +1726,18 @@ function getEnvSummary(keys) {
|
|
|
1478
1726
|
isTokenExpired,
|
|
1479
1727
|
isValidBearerToken,
|
|
1480
1728
|
parseKeycloakRoles,
|
|
1729
|
+
rateLimitResponse,
|
|
1481
1730
|
refreshKeycloakToken,
|
|
1482
1731
|
resetRateLimitForKey,
|
|
1483
1732
|
resolveIdentifier,
|
|
1484
1733
|
resolveRateLimitIdentifier,
|
|
1734
|
+
safeValidate,
|
|
1485
1735
|
sanitizeApiError,
|
|
1486
1736
|
storeBetaCode,
|
|
1487
1737
|
stripHtml,
|
|
1488
1738
|
validateBetaCode,
|
|
1489
1739
|
validateEnvVars,
|
|
1740
|
+
verifyCronAuth,
|
|
1490
1741
|
zodErrorResponse
|
|
1491
1742
|
});
|
|
1492
1743
|
//# sourceMappingURL=auth.js.map
|