@digilogiclabs/platform-core 1.10.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 +103 -1
- package/dist/auth.d.ts +103 -1
- package/dist/auth.js +178 -8
- package/dist/auth.js.map +1 -1
- package/dist/auth.mjs +176 -8
- package/dist/auth.mjs.map +1 -1
- package/dist/migrate.js +0 -0
- package/package.json +11 -11
package/dist/auth.d.mts
CHANGED
|
@@ -285,4 +285,106 @@ declare function safeValidate<T>(schema: {
|
|
|
285
285
|
}>;
|
|
286
286
|
};
|
|
287
287
|
|
|
288
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Federated Logout Handler for Keycloak + Auth.js
|
|
290
|
+
*
|
|
291
|
+
* Builds a Next.js route handler that:
|
|
292
|
+
* 1. Revokes the refresh token with Keycloak
|
|
293
|
+
* 2. Returns an HTML page that clears all auth cookies
|
|
294
|
+
* 3. Auto-redirects to Keycloak's OIDC logout endpoint
|
|
295
|
+
*
|
|
296
|
+
* Uses HTML 200 (not 307 redirect) because browsers may not reliably
|
|
297
|
+
* process Set-Cookie headers on redirect responses through CDN proxies.
|
|
298
|
+
*
|
|
299
|
+
* Edge-runtime compatible.
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```typescript
|
|
303
|
+
* // app/api/auth/federated-logout/route.ts
|
|
304
|
+
* import { buildFederatedLogoutHandler } from '@digilogiclabs/platform-core/auth'
|
|
305
|
+
* import { auth } from '@/auth'
|
|
306
|
+
*
|
|
307
|
+
* export const dynamic = 'force-dynamic'
|
|
308
|
+
*
|
|
309
|
+
* export const GET = buildFederatedLogoutHandler({
|
|
310
|
+
* auth,
|
|
311
|
+
* domain: '.digilogiclabs.com',
|
|
312
|
+
* baseUrlFallback: 'https://digilogiclabs.com',
|
|
313
|
+
* })
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
interface FederatedLogoutConfig {
|
|
317
|
+
/**
|
|
318
|
+
* Auth.js `auth()` function — used to get the session's idToken.
|
|
319
|
+
* Must return an object with an optional `idToken` field.
|
|
320
|
+
*/
|
|
321
|
+
auth: () => Promise<{
|
|
322
|
+
idToken?: string;
|
|
323
|
+
} | null>;
|
|
324
|
+
/**
|
|
325
|
+
* Production cookie domain (e.g. '.digilogiclabs.com').
|
|
326
|
+
* Used to clear cookies scoped to the root domain.
|
|
327
|
+
*/
|
|
328
|
+
domain: string;
|
|
329
|
+
/**
|
|
330
|
+
* Fallback base URL when NEXTAUTH_URL/AUTH_URL are not set.
|
|
331
|
+
* e.g. 'https://digilogiclabs.com'
|
|
332
|
+
*/
|
|
333
|
+
baseUrlFallback: string;
|
|
334
|
+
/**
|
|
335
|
+
* Additional cookie names to clear beyond the standard Auth.js set.
|
|
336
|
+
* e.g. ['admin-session']
|
|
337
|
+
*/
|
|
338
|
+
extraCookies?: string[];
|
|
339
|
+
/**
|
|
340
|
+
* Optional error reporter. Called on missing issuer and token revocation failures.
|
|
341
|
+
* If not provided, errors are silently ignored.
|
|
342
|
+
*/
|
|
343
|
+
onError?: (message: string, error?: unknown) => void;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Build a Next.js route handler for federated logout with Keycloak.
|
|
347
|
+
*
|
|
348
|
+
* The returned handler:
|
|
349
|
+
* - Validates the callbackUrl against an allowlist (open redirect protection)
|
|
350
|
+
* - Revokes the Keycloak refresh token
|
|
351
|
+
* - Clears all Auth.js cookies (both standard and __Secure- prefixed)
|
|
352
|
+
* - Returns an HTML page that auto-redirects to Keycloak's OIDC logout
|
|
353
|
+
*/
|
|
354
|
+
declare function buildFederatedLogoutHandler(config: FederatedLogoutConfig): (request: Request) => Promise<Response>;
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Lazy Rate Limit Store
|
|
358
|
+
*
|
|
359
|
+
* Creates a singleton rate limit store backed by Redis with automatic
|
|
360
|
+
* fallback to in-memory when Redis is unavailable.
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* ```typescript
|
|
364
|
+
* // lib/rate-limit-store.ts
|
|
365
|
+
* import { createLazyRateLimitStore } from '@digilogiclabs/platform-core/auth'
|
|
366
|
+
* import { getRedisClient } from './redis'
|
|
367
|
+
*
|
|
368
|
+
* export const getRateLimitStore = createLazyRateLimitStore(getRedisClient)
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
371
|
+
|
|
372
|
+
interface LazyRateLimitStoreOptions {
|
|
373
|
+
/** Redis key prefix for rate limit keys (default: 'rl:') */
|
|
374
|
+
keyPrefix?: string;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Create a lazy singleton rate limit store factory.
|
|
378
|
+
*
|
|
379
|
+
* Returns a getter function that:
|
|
380
|
+
* - On first call, attempts to get a Redis client and create a Redis-backed store
|
|
381
|
+
* - If Redis is unavailable, returns undefined (enforceRateLimit falls back to in-memory)
|
|
382
|
+
* - Caches the result for subsequent calls
|
|
383
|
+
*
|
|
384
|
+
* @param getRedis - Function that returns a Redis client (or null/undefined if unavailable)
|
|
385
|
+
* @param options - Optional configuration
|
|
386
|
+
* @returns A getter function that returns the RateLimitStore or undefined
|
|
387
|
+
*/
|
|
388
|
+
declare function createLazyRateLimitStore(getRedis: () => RedisRateLimitClient | null | undefined, options?: LazyRateLimitStoreOptions): () => RateLimitStore | undefined;
|
|
389
|
+
|
|
390
|
+
export { type FederatedLogoutConfig, type LazyRateLimitStoreOptions, RateLimitOptions, type RateLimitResult, RateLimitRule, RateLimitStore, type RedisRateLimitClient, type RedisRateLimitStoreOptions, addRateLimitHeaders, buildFederatedLogoutHandler, createLazyRateLimitStore, createRedisRateLimitStore, enforceRateLimit, errorResponse, extractBearerToken, getClientIp, isValidBearerToken, rateLimitResponse, safeValidate, verifyCronAuth, zodErrorResponse };
|
package/dist/auth.d.ts
CHANGED
|
@@ -285,4 +285,106 @@ declare function safeValidate<T>(schema: {
|
|
|
285
285
|
}>;
|
|
286
286
|
};
|
|
287
287
|
|
|
288
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Federated Logout Handler for Keycloak + Auth.js
|
|
290
|
+
*
|
|
291
|
+
* Builds a Next.js route handler that:
|
|
292
|
+
* 1. Revokes the refresh token with Keycloak
|
|
293
|
+
* 2. Returns an HTML page that clears all auth cookies
|
|
294
|
+
* 3. Auto-redirects to Keycloak's OIDC logout endpoint
|
|
295
|
+
*
|
|
296
|
+
* Uses HTML 200 (not 307 redirect) because browsers may not reliably
|
|
297
|
+
* process Set-Cookie headers on redirect responses through CDN proxies.
|
|
298
|
+
*
|
|
299
|
+
* Edge-runtime compatible.
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```typescript
|
|
303
|
+
* // app/api/auth/federated-logout/route.ts
|
|
304
|
+
* import { buildFederatedLogoutHandler } from '@digilogiclabs/platform-core/auth'
|
|
305
|
+
* import { auth } from '@/auth'
|
|
306
|
+
*
|
|
307
|
+
* export const dynamic = 'force-dynamic'
|
|
308
|
+
*
|
|
309
|
+
* export const GET = buildFederatedLogoutHandler({
|
|
310
|
+
* auth,
|
|
311
|
+
* domain: '.digilogiclabs.com',
|
|
312
|
+
* baseUrlFallback: 'https://digilogiclabs.com',
|
|
313
|
+
* })
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
interface FederatedLogoutConfig {
|
|
317
|
+
/**
|
|
318
|
+
* Auth.js `auth()` function — used to get the session's idToken.
|
|
319
|
+
* Must return an object with an optional `idToken` field.
|
|
320
|
+
*/
|
|
321
|
+
auth: () => Promise<{
|
|
322
|
+
idToken?: string;
|
|
323
|
+
} | null>;
|
|
324
|
+
/**
|
|
325
|
+
* Production cookie domain (e.g. '.digilogiclabs.com').
|
|
326
|
+
* Used to clear cookies scoped to the root domain.
|
|
327
|
+
*/
|
|
328
|
+
domain: string;
|
|
329
|
+
/**
|
|
330
|
+
* Fallback base URL when NEXTAUTH_URL/AUTH_URL are not set.
|
|
331
|
+
* e.g. 'https://digilogiclabs.com'
|
|
332
|
+
*/
|
|
333
|
+
baseUrlFallback: string;
|
|
334
|
+
/**
|
|
335
|
+
* Additional cookie names to clear beyond the standard Auth.js set.
|
|
336
|
+
* e.g. ['admin-session']
|
|
337
|
+
*/
|
|
338
|
+
extraCookies?: string[];
|
|
339
|
+
/**
|
|
340
|
+
* Optional error reporter. Called on missing issuer and token revocation failures.
|
|
341
|
+
* If not provided, errors are silently ignored.
|
|
342
|
+
*/
|
|
343
|
+
onError?: (message: string, error?: unknown) => void;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Build a Next.js route handler for federated logout with Keycloak.
|
|
347
|
+
*
|
|
348
|
+
* The returned handler:
|
|
349
|
+
* - Validates the callbackUrl against an allowlist (open redirect protection)
|
|
350
|
+
* - Revokes the Keycloak refresh token
|
|
351
|
+
* - Clears all Auth.js cookies (both standard and __Secure- prefixed)
|
|
352
|
+
* - Returns an HTML page that auto-redirects to Keycloak's OIDC logout
|
|
353
|
+
*/
|
|
354
|
+
declare function buildFederatedLogoutHandler(config: FederatedLogoutConfig): (request: Request) => Promise<Response>;
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Lazy Rate Limit Store
|
|
358
|
+
*
|
|
359
|
+
* Creates a singleton rate limit store backed by Redis with automatic
|
|
360
|
+
* fallback to in-memory when Redis is unavailable.
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* ```typescript
|
|
364
|
+
* // lib/rate-limit-store.ts
|
|
365
|
+
* import { createLazyRateLimitStore } from '@digilogiclabs/platform-core/auth'
|
|
366
|
+
* import { getRedisClient } from './redis'
|
|
367
|
+
*
|
|
368
|
+
* export const getRateLimitStore = createLazyRateLimitStore(getRedisClient)
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
371
|
+
|
|
372
|
+
interface LazyRateLimitStoreOptions {
|
|
373
|
+
/** Redis key prefix for rate limit keys (default: 'rl:') */
|
|
374
|
+
keyPrefix?: string;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Create a lazy singleton rate limit store factory.
|
|
378
|
+
*
|
|
379
|
+
* Returns a getter function that:
|
|
380
|
+
* - On first call, attempts to get a Redis client and create a Redis-backed store
|
|
381
|
+
* - If Redis is unavailable, returns undefined (enforceRateLimit falls back to in-memory)
|
|
382
|
+
* - Caches the result for subsequent calls
|
|
383
|
+
*
|
|
384
|
+
* @param getRedis - Function that returns a Redis client (or null/undefined if unavailable)
|
|
385
|
+
* @param options - Optional configuration
|
|
386
|
+
* @returns A getter function that returns the RateLimitStore or undefined
|
|
387
|
+
*/
|
|
388
|
+
declare function createLazyRateLimitStore(getRedis: () => RedisRateLimitClient | null | undefined, options?: LazyRateLimitStoreOptions): () => RateLimitStore | undefined;
|
|
389
|
+
|
|
390
|
+
export { type FederatedLogoutConfig, type LazyRateLimitStoreOptions, RateLimitOptions, type RateLimitResult, RateLimitRule, RateLimitStore, type RedisRateLimitClient, type RedisRateLimitStoreOptions, addRateLimitHeaders, buildFederatedLogoutHandler, createLazyRateLimitStore, createRedisRateLimitStore, enforceRateLimit, errorResponse, extractBearerToken, getClientIp, isValidBearerToken, rateLimitResponse, safeValidate, verifyCronAuth, zodErrorResponse };
|
package/dist/auth.js
CHANGED
|
@@ -42,6 +42,7 @@ __export(auth_exports, {
|
|
|
42
42
|
buildAllowlist: () => buildAllowlist,
|
|
43
43
|
buildAuthCookies: () => buildAuthCookies,
|
|
44
44
|
buildErrorBody: () => buildErrorBody,
|
|
45
|
+
buildFederatedLogoutHandler: () => buildFederatedLogoutHandler,
|
|
45
46
|
buildKeycloakCallbacks: () => buildKeycloakCallbacks,
|
|
46
47
|
buildPagination: () => buildPagination,
|
|
47
48
|
buildRateLimitHeaders: () => buildRateLimitHeaders,
|
|
@@ -59,6 +60,7 @@ __export(auth_exports, {
|
|
|
59
60
|
createAuditLogger: () => createAuditLogger,
|
|
60
61
|
createBetaClient: () => createBetaClient,
|
|
61
62
|
createFeatureFlags: () => createFeatureFlags,
|
|
63
|
+
createLazyRateLimitStore: () => createLazyRateLimitStore,
|
|
62
64
|
createMemoryRateLimitStore: () => createMemoryRateLimitStore,
|
|
63
65
|
createRedisRateLimitStore: () => createRedisRateLimitStore,
|
|
64
66
|
createSafeTextSchema: () => createSafeTextSchema,
|
|
@@ -1223,10 +1225,10 @@ function isValidBearerToken(request, secret) {
|
|
|
1223
1225
|
function verifyCronAuth(request, secret) {
|
|
1224
1226
|
const authHeader = request.headers.get("authorization");
|
|
1225
1227
|
if (!authHeader?.startsWith("Bearer ")) {
|
|
1226
|
-
return new Response(
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
);
|
|
1228
|
+
return new Response(JSON.stringify({ error: "Missing authorization" }), {
|
|
1229
|
+
status: 401,
|
|
1230
|
+
headers: { "Content-Type": "application/json" }
|
|
1231
|
+
});
|
|
1230
1232
|
}
|
|
1231
1233
|
const token = authHeader.slice(7);
|
|
1232
1234
|
const expectedSecret = secret ?? process.env.CRON_SECRET;
|
|
@@ -1238,10 +1240,10 @@ function verifyCronAuth(request, secret) {
|
|
|
1238
1240
|
);
|
|
1239
1241
|
}
|
|
1240
1242
|
if (!constantTimeEqual(token, expectedSecret)) {
|
|
1241
|
-
return new Response(
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
);
|
|
1243
|
+
return new Response(JSON.stringify({ error: "Invalid authorization" }), {
|
|
1244
|
+
status: 401,
|
|
1245
|
+
headers: { "Content-Type": "application/json" }
|
|
1246
|
+
});
|
|
1245
1247
|
}
|
|
1246
1248
|
return null;
|
|
1247
1249
|
}
|
|
@@ -1393,6 +1395,172 @@ function clearStoredBetaCode(config = {}) {
|
|
|
1393
1395
|
}
|
|
1394
1396
|
}
|
|
1395
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
|
+
|
|
1396
1564
|
// src/env.ts
|
|
1397
1565
|
function getRequiredEnv(key) {
|
|
1398
1566
|
const value = process.env[key];
|
|
@@ -1507,6 +1675,7 @@ function getEnvSummary(keys) {
|
|
|
1507
1675
|
buildAllowlist,
|
|
1508
1676
|
buildAuthCookies,
|
|
1509
1677
|
buildErrorBody,
|
|
1678
|
+
buildFederatedLogoutHandler,
|
|
1510
1679
|
buildKeycloakCallbacks,
|
|
1511
1680
|
buildPagination,
|
|
1512
1681
|
buildRateLimitHeaders,
|
|
@@ -1524,6 +1693,7 @@ function getEnvSummary(keys) {
|
|
|
1524
1693
|
createAuditLogger,
|
|
1525
1694
|
createBetaClient,
|
|
1526
1695
|
createFeatureFlags,
|
|
1696
|
+
createLazyRateLimitStore,
|
|
1527
1697
|
createMemoryRateLimitStore,
|
|
1528
1698
|
createRedisRateLimitStore,
|
|
1529
1699
|
createSafeTextSchema,
|