@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.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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
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…</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
|