@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.mjs CHANGED
@@ -562,6 +562,12 @@ var CommonRateLimits = {
562
562
  limit: 10,
563
563
  windowSeconds: 3600,
564
564
  blockDurationSeconds: 3600
565
+ },
566
+ /** Beta code validation: 5/min with 5min block (prevents brute force guessing) */
567
+ betaValidation: {
568
+ limit: 5,
569
+ windowSeconds: 60,
570
+ blockDurationSeconds: 300
565
571
  }
566
572
  };
567
573
  function createMemoryRateLimitStore() {
@@ -1105,6 +1111,71 @@ function isValidBearerToken(request, secret) {
1105
1111
  if (!token) return false;
1106
1112
  return constantTimeEqual(token, secret);
1107
1113
  }
1114
+ function verifyCronAuth(request, secret) {
1115
+ const authHeader = request.headers.get("authorization");
1116
+ if (!authHeader?.startsWith("Bearer ")) {
1117
+ return new Response(JSON.stringify({ error: "Missing authorization" }), {
1118
+ status: 401,
1119
+ headers: { "Content-Type": "application/json" }
1120
+ });
1121
+ }
1122
+ const token = authHeader.slice(7);
1123
+ const expectedSecret = secret ?? process.env.CRON_SECRET;
1124
+ if (!expectedSecret) {
1125
+ console.error("[verifyCronAuth] CRON_SECRET not configured");
1126
+ return new Response(
1127
+ JSON.stringify({ error: "Server configuration error" }),
1128
+ { status: 500, headers: { "Content-Type": "application/json" } }
1129
+ );
1130
+ }
1131
+ if (!constantTimeEqual(token, expectedSecret)) {
1132
+ return new Response(JSON.stringify({ error: "Invalid authorization" }), {
1133
+ status: 401,
1134
+ headers: { "Content-Type": "application/json" }
1135
+ });
1136
+ }
1137
+ return null;
1138
+ }
1139
+ function getClientIp(request) {
1140
+ return request.headers.get("cf-connecting-ip") || request.headers.get("x-real-ip") || request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
1141
+ }
1142
+ function rateLimitResponse(result) {
1143
+ const retryAfter = Math.ceil(result.resetMs / 1e3);
1144
+ const resetTimestamp = Math.ceil(Date.now() / 1e3 + result.resetMs / 1e3);
1145
+ return new Response(
1146
+ JSON.stringify({ error: "Too many requests", retryAfter }),
1147
+ {
1148
+ status: 429,
1149
+ headers: {
1150
+ "Content-Type": "application/json",
1151
+ "X-RateLimit-Limit": String(result.limit),
1152
+ "X-RateLimit-Remaining": "0",
1153
+ "X-RateLimit-Reset": String(resetTimestamp),
1154
+ "Retry-After": String(retryAfter)
1155
+ }
1156
+ }
1157
+ );
1158
+ }
1159
+ function addRateLimitHeaders(response, result) {
1160
+ const resetTimestamp = Math.ceil(Date.now() / 1e3 + result.resetMs / 1e3);
1161
+ response.headers.set("X-RateLimit-Limit", String(result.limit));
1162
+ response.headers.set("X-RateLimit-Remaining", String(result.remaining));
1163
+ response.headers.set("X-RateLimit-Reset", String(resetTimestamp));
1164
+ return response;
1165
+ }
1166
+ function safeValidate(schema, data) {
1167
+ const result = schema.safeParse(data);
1168
+ if (result.success) {
1169
+ return { success: true, data: result.data };
1170
+ }
1171
+ return {
1172
+ success: false,
1173
+ errors: (result.error?.issues || []).map((issue) => ({
1174
+ field: issue.path.join("."),
1175
+ message: issue.message
1176
+ }))
1177
+ };
1178
+ }
1108
1179
 
1109
1180
  // src/auth/beta-client.ts
1110
1181
  var DEFAULT_CONFIG = {
@@ -1213,6 +1284,172 @@ function clearStoredBetaCode(config = {}) {
1213
1284
  }
1214
1285
  }
1215
1286
 
1287
+ // src/auth/federated-logout.ts
1288
+ function expireCookie(name, options) {
1289
+ const parts = [
1290
+ `${name}=`,
1291
+ "Path=/",
1292
+ "Expires=Thu, 01 Jan 1970 00:00:00 GMT",
1293
+ "Max-Age=0",
1294
+ "SameSite=Lax"
1295
+ ];
1296
+ if (options?.hostPrefix) {
1297
+ parts.push("Secure");
1298
+ } else {
1299
+ parts.push("HttpOnly");
1300
+ if (options?.domain) parts.push(`Domain=${options.domain}`);
1301
+ if (options?.secure) parts.push("Secure");
1302
+ }
1303
+ return parts.join("; ");
1304
+ }
1305
+ function isAllowedCallbackUrl(url, baseUrl) {
1306
+ if (url.startsWith("/") && !url.startsWith("//")) return true;
1307
+ try {
1308
+ const parsed = new URL(url);
1309
+ const base = new URL(baseUrl);
1310
+ const allowedHosts = [base.hostname, `www.${base.hostname}`];
1311
+ if (base.hostname.startsWith("www.")) {
1312
+ allowedHosts.push(base.hostname.slice(4));
1313
+ }
1314
+ return allowedHosts.includes(parsed.hostname);
1315
+ } catch {
1316
+ return false;
1317
+ }
1318
+ }
1319
+ var AUTH_COOKIE_NAMES = [
1320
+ "authjs.session-token",
1321
+ "__Secure-authjs.session-token",
1322
+ "authjs.callback-url",
1323
+ "__Secure-authjs.callback-url",
1324
+ "authjs.csrf-token",
1325
+ "__Secure-authjs.csrf-token",
1326
+ "authjs.pkce.code_verifier",
1327
+ "__Secure-authjs.pkce.code_verifier",
1328
+ "authjs.state",
1329
+ "__Secure-authjs.state",
1330
+ // Legacy next-auth names
1331
+ "next-auth.session-token",
1332
+ "__Secure-next-auth.session-token",
1333
+ "next-auth.callback-url",
1334
+ "__Secure-next-auth.callback-url",
1335
+ "next-auth.csrf-token",
1336
+ "__Secure-next-auth.csrf-token"
1337
+ ];
1338
+ var HOST_COOKIES = [
1339
+ "__Host-authjs.csrf-token",
1340
+ "__Host-next-auth.csrf-token"
1341
+ ];
1342
+ function buildFederatedLogoutHandler(config) {
1343
+ const { auth, domain, baseUrlFallback, extraCookies = [], onError } = config;
1344
+ return async function GET(request) {
1345
+ const session = await auth();
1346
+ const url = new URL(request.url);
1347
+ const rawCallbackUrl = url.searchParams.get("callbackUrl") || "/";
1348
+ const queryIdToken = url.searchParams.get("id_token_hint");
1349
+ const baseUrl = process.env.NEXTAUTH_URL || process.env.AUTH_URL || baseUrlFallback;
1350
+ const callbackUrl = isAllowedCallbackUrl(rawCallbackUrl, baseUrl) ? rawCallbackUrl : "/";
1351
+ const postLogoutRedirectUri = callbackUrl.startsWith("http") ? callbackUrl : `${baseUrl}${callbackUrl}`;
1352
+ const keycloakIssuer = process.env.AUTH_KEYCLOAK_ISSUER;
1353
+ if (!keycloakIssuer) {
1354
+ onError?.("Missing AUTH_KEYCLOAK_ISSUER");
1355
+ return Response.redirect(
1356
+ new URL(
1357
+ "/api/auth/signout?callbackUrl=" + encodeURIComponent(callbackUrl),
1358
+ request.url
1359
+ ).toString()
1360
+ );
1361
+ }
1362
+ const refreshToken = session?.refreshToken;
1363
+ if (refreshToken) {
1364
+ try {
1365
+ const revokeUrl = `${keycloakIssuer}/protocol/openid-connect/revoke`;
1366
+ const clientId2 = process.env.AUTH_KEYCLOAK_ID;
1367
+ const clientSecret = process.env.AUTH_KEYCLOAK_SECRET;
1368
+ await fetch(revokeUrl, {
1369
+ method: "POST",
1370
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1371
+ body: new URLSearchParams({
1372
+ token: refreshToken,
1373
+ token_type_hint: "refresh_token",
1374
+ ...clientId2 && { client_id: clientId2 },
1375
+ ...clientSecret && { client_secret: clientSecret }
1376
+ })
1377
+ });
1378
+ } catch (err) {
1379
+ onError?.("Token revocation failed", err);
1380
+ }
1381
+ }
1382
+ const keycloakLogoutUrl = new URL(
1383
+ `${keycloakIssuer}/protocol/openid-connect/logout`
1384
+ );
1385
+ keycloakLogoutUrl.searchParams.set(
1386
+ "post_logout_redirect_uri",
1387
+ postLogoutRedirectUri
1388
+ );
1389
+ const clientId = process.env.AUTH_KEYCLOAK_ID;
1390
+ if (clientId) keycloakLogoutUrl.searchParams.set("client_id", clientId);
1391
+ const idToken = session?.idToken || queryIdToken;
1392
+ if (idToken) keycloakLogoutUrl.searchParams.set("id_token_hint", idToken);
1393
+ const escapedUrl = keycloakLogoutUrl.toString().replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1394
+ const html = `<!DOCTYPE html>
1395
+ <html><head>
1396
+ <meta charset="utf-8">
1397
+ <meta http-equiv="refresh" content="0;url=${escapedUrl}">
1398
+ <title>Signing out...</title>
1399
+ <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>
1400
+ </head><body>
1401
+ <p>Signing out&hellip;</p>
1402
+ <script>window.location.replace(${JSON.stringify(keycloakLogoutUrl.toString()).replace(/</g, "\\u003c")});</script>
1403
+ </body></html>`;
1404
+ const response = new Response(html, {
1405
+ status: 200,
1406
+ headers: {
1407
+ "Content-Type": "text/html; charset=utf-8",
1408
+ "Cache-Control": "no-store, no-cache, must-revalidate",
1409
+ Pragma: "no-cache"
1410
+ }
1411
+ });
1412
+ const isProduction = process.env.NODE_ENV === "production";
1413
+ const isSecure = isProduction;
1414
+ const allCookieNames = [...AUTH_COOKIE_NAMES, ...extraCookies];
1415
+ for (const name of allCookieNames) {
1416
+ const needsSecure = isSecure || name.startsWith("__Secure-");
1417
+ response.headers.append(
1418
+ "Set-Cookie",
1419
+ expireCookie(name, { secure: needsSecure })
1420
+ );
1421
+ if (isProduction) {
1422
+ response.headers.append(
1423
+ "Set-Cookie",
1424
+ expireCookie(name, { domain, secure: needsSecure })
1425
+ );
1426
+ }
1427
+ }
1428
+ for (const name of HOST_COOKIES) {
1429
+ response.headers.append(
1430
+ "Set-Cookie",
1431
+ expireCookie(name, { hostPrefix: true })
1432
+ );
1433
+ }
1434
+ return response;
1435
+ };
1436
+ }
1437
+
1438
+ // src/auth/lazy-rate-limit-store.ts
1439
+ function createLazyRateLimitStore(getRedis, options = {}) {
1440
+ const { keyPrefix = "rl:" } = options;
1441
+ let store;
1442
+ let initialized = false;
1443
+ return function getRateLimitStore() {
1444
+ if (initialized) return store;
1445
+ initialized = true;
1446
+ const redis = getRedis();
1447
+ if (!redis) return void 0;
1448
+ store = createRedisRateLimitStore(redis, { keyPrefix });
1449
+ return store;
1450
+ };
1451
+ }
1452
+
1216
1453
  // src/env.ts
1217
1454
  function getRequiredEnv(key) {
1218
1455
  const value = process.env[key];
@@ -1322,9 +1559,11 @@ export {
1322
1559
  StandardAuditActions,
1323
1560
  StandardRateLimitPresets,
1324
1561
  WrapperPresets,
1562
+ addRateLimitHeaders,
1325
1563
  buildAllowlist,
1326
1564
  buildAuthCookies,
1327
1565
  buildErrorBody,
1566
+ buildFederatedLogoutHandler,
1328
1567
  buildKeycloakCallbacks,
1329
1568
  buildPagination,
1330
1569
  buildRateLimitHeaders,
@@ -1342,6 +1581,7 @@ export {
1342
1581
  createAuditLogger,
1343
1582
  createBetaClient,
1344
1583
  createFeatureFlags,
1584
+ createLazyRateLimitStore,
1345
1585
  createMemoryRateLimitStore,
1346
1586
  createRedisRateLimitStore,
1347
1587
  createSafeTextSchema,
@@ -1356,6 +1596,7 @@ export {
1356
1596
  extractClientIp,
1357
1597
  fetchBetaSettings,
1358
1598
  getBoolEnv,
1599
+ getClientIp,
1359
1600
  getCorrelationId,
1360
1601
  getEndSessionEndpoint,
1361
1602
  getEnvSummary,
@@ -1373,15 +1614,18 @@ export {
1373
1614
  isTokenExpired,
1374
1615
  isValidBearerToken,
1375
1616
  parseKeycloakRoles,
1617
+ rateLimitResponse,
1376
1618
  refreshKeycloakToken,
1377
1619
  resetRateLimitForKey,
1378
1620
  resolveIdentifier,
1379
1621
  resolveRateLimitIdentifier,
1622
+ safeValidate,
1380
1623
  sanitizeApiError,
1381
1624
  storeBetaCode,
1382
1625
  stripHtml,
1383
1626
  validateBetaCode,
1384
1627
  validateEnvVars,
1628
+ verifyCronAuth,
1385
1629
  zodErrorResponse
1386
1630
  };
1387
1631
  //# sourceMappingURL=auth.mjs.map