@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.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, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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&hellip;</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