@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 CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RateLimitStore, a as RateLimitRule, b as RateLimitOptions } from './env-DHPZR3Lv.mjs';
2
- export { al as AllowlistConfig, A as ApiError, d as ApiErrorCode, f as ApiErrorCodeType, h as ApiPaginatedResponse, Q as ApiSecurityConfig, V as ApiSecurityContext, g as ApiSuccessResponse, aD as AuditRequest, H as AuthCookiesConfig, N as AuthMethod, aL as BetaClientConfig, C as CommonApiErrors, ar as CommonRateLimits, ac as DateRangeInput, a6 as DateRangeSchema, ag as DeploymentStage, aa as EmailInput, $ as EmailSchema, E as EnvValidationConfig, p as EnvValidationResult, ai as FlagDefinition, aj as FlagDefinitions, ah as FlagValue, r as KEYCLOAK_DEFAULT_ROLES, F as KeycloakCallbacksConfig, K as KeycloakConfig, G as KeycloakJwtFields, q as KeycloakTokenSet, ae as LoginInput, a8 as LoginSchema, ay as OpsAuditActor, aA as OpsAuditEvent, aC as OpsAuditLoggerOptions, aB as OpsAuditRecord, az as OpsAuditResource, ab as PaginationInput, a5 as PaginationSchema, a0 as PasswordSchema, a3 as PersonNameSchema, a2 as PhoneSchema, aq as RateLimitCheckResult, P as RateLimitPreset, J as RedirectCallbackConfig, ak as ResolvedFlags, O as RouteAuditConfig, ad as SearchQueryInput, a7 as SearchQuerySchema, U as SecuritySession, af as SignupInput, a9 as SignupSchema, a1 as SlugSchema, aF as StandardAuditActionType, aE as StandardAuditActions, S as StandardRateLimitPresets, T as TokenRefreshResult, _ as WrapperPresets, ao as buildAllowlist, I as buildAuthCookies, Z as buildErrorBody, M as buildKeycloakCallbacks, e as buildPagination, Y as buildRateLimitHeaders, aw as buildRateLimitResponseHeaders, L as buildRedirectCallback, y as buildTokenRefreshParams, n as checkEnvVars, at as checkRateLimit, c as classifyError, aR as clearStoredBetaCode, aJ as createAuditActor, aK as createAuditLogger, aM as createBetaClient, an as createFeatureFlags, as as createMemoryRateLimitStore, a4 as createSafeTextSchema, am as detectStage, aG as extractAuditIp, aI as extractAuditRequestId, aH as extractAuditUserAgent, X as extractClientIp, aN as fetchBetaSettings, l as getBoolEnv, B as getEndSessionEndpoint, o as getEnvSummary, m as getIntEnv, k as getOptionalEnv, au as getRateLimitStatus, j as getRequiredEnv, aQ as getStoredBetaCode, z as getTokenEndpoint, w as hasAllRoles, u as hasAnyRole, t as hasRole, ap as isAllowlisted, i as isApiError, x as isTokenExpired, s as parseKeycloakRoles, D as refreshKeycloakToken, av as resetRateLimitForKey, ax as resolveIdentifier, W as resolveRateLimitIdentifier, aP as storeBetaCode, aO as validateBetaCode, v as validateEnvVars } from './env-DHPZR3Lv.mjs';
1
+ import { R as RateLimitStore, a as RateLimitRule, b as RateLimitOptions } from './env-CYKVNpLl.mjs';
2
+ export { al as AllowlistConfig, A as ApiError, d as ApiErrorCode, f as ApiErrorCodeType, h as ApiPaginatedResponse, Q as ApiSecurityConfig, V as ApiSecurityContext, g as ApiSuccessResponse, aD as AuditRequest, H as AuthCookiesConfig, N as AuthMethod, aL as BetaClientConfig, C as CommonApiErrors, ar as CommonRateLimits, ac as DateRangeInput, a6 as DateRangeSchema, ag as DeploymentStage, aa as EmailInput, $ as EmailSchema, E as EnvValidationConfig, p as EnvValidationResult, ai as FlagDefinition, aj as FlagDefinitions, ah as FlagValue, r as KEYCLOAK_DEFAULT_ROLES, F as KeycloakCallbacksConfig, K as KeycloakConfig, G as KeycloakJwtFields, q as KeycloakTokenSet, ae as LoginInput, a8 as LoginSchema, ay as OpsAuditActor, aA as OpsAuditEvent, aC as OpsAuditLoggerOptions, aB as OpsAuditRecord, az as OpsAuditResource, ab as PaginationInput, a5 as PaginationSchema, a0 as PasswordSchema, a3 as PersonNameSchema, a2 as PhoneSchema, aq as RateLimitCheckResult, P as RateLimitPreset, J as RedirectCallbackConfig, ak as ResolvedFlags, O as RouteAuditConfig, ad as SearchQueryInput, a7 as SearchQuerySchema, U as SecuritySession, af as SignupInput, a9 as SignupSchema, a1 as SlugSchema, aF as StandardAuditActionType, aE as StandardAuditActions, S as StandardRateLimitPresets, T as TokenRefreshResult, _ as WrapperPresets, ao as buildAllowlist, I as buildAuthCookies, Z as buildErrorBody, M as buildKeycloakCallbacks, e as buildPagination, Y as buildRateLimitHeaders, aw as buildRateLimitResponseHeaders, L as buildRedirectCallback, y as buildTokenRefreshParams, n as checkEnvVars, at as checkRateLimit, c as classifyError, aR as clearStoredBetaCode, aJ as createAuditActor, aK as createAuditLogger, aM as createBetaClient, an as createFeatureFlags, as as createMemoryRateLimitStore, a4 as createSafeTextSchema, am as detectStage, aG as extractAuditIp, aI as extractAuditRequestId, aH as extractAuditUserAgent, X as extractClientIp, aN as fetchBetaSettings, l as getBoolEnv, B as getEndSessionEndpoint, o as getEnvSummary, m as getIntEnv, k as getOptionalEnv, au as getRateLimitStatus, j as getRequiredEnv, aQ as getStoredBetaCode, z as getTokenEndpoint, w as hasAllRoles, u as hasAnyRole, t as hasRole, ap as isAllowlisted, i as isApiError, x as isTokenExpired, s as parseKeycloakRoles, D as refreshKeycloakToken, av as resetRateLimitForKey, ax as resolveIdentifier, W as resolveRateLimitIdentifier, aP as storeBetaCode, aO as validateBetaCode, v as validateEnvVars } from './env-CYKVNpLl.mjs';
3
3
  export { c as constantTimeEqual, b as containsHtml, a as containsUrls, e as escapeHtml, g as getCorrelationId, d as sanitizeApiError, s as stripHtml } from './security-BvLXaQkv.mjs';
4
4
  import 'zod';
5
5
 
@@ -172,5 +172,219 @@ declare function isValidBearerToken(request: {
172
172
  get(name: string): string | null;
173
173
  };
174
174
  }, secret: string | undefined): boolean;
175
+ /**
176
+ * Verify cron/sync endpoint authentication using a bearer token.
177
+ *
178
+ * Extracts the Bearer token from the Authorization header and compares
179
+ * it against `CRON_SECRET` (or a custom secret) using timing-safe comparison.
180
+ * Returns a 401/500 Response on failure, or `null` on success.
181
+ *
182
+ * @param request - Incoming request (must have headers.get)
183
+ * @param secret - Custom secret to compare against. Defaults to `process.env.CRON_SECRET`.
184
+ * @returns `null` if authorized, or a JSON Response (401/500) if not.
185
+ *
186
+ * @example
187
+ * ```typescript
188
+ * export async function POST(request: NextRequest) {
189
+ * const authError = verifyCronAuth(request)
190
+ * if (authError) return authError
191
+ * // ... handle cron job
192
+ * }
193
+ * ```
194
+ */
195
+ declare function verifyCronAuth(request: {
196
+ headers: {
197
+ get(name: string): string | null;
198
+ };
199
+ }, secret?: string): Response | null;
200
+ /**
201
+ * Extract the client IP address from a request, checking proxy headers.
202
+ *
203
+ * Checks in order:
204
+ * 1. `cf-connecting-ip` (Cloudflare)
205
+ * 2. `x-real-ip` (Nginx)
206
+ * 3. `x-forwarded-for` (first IP from comma-separated list)
207
+ * 4. Falls back to `'unknown'`
208
+ *
209
+ * @example
210
+ * ```typescript
211
+ * const ip = getClientIp(request)
212
+ * await auditLog({ action: 'login', ip })
213
+ * ```
214
+ */
215
+ declare function getClientIp(request: {
216
+ headers: {
217
+ get(name: string): string | null;
218
+ };
219
+ }): string;
220
+ /** Shape expected by rate limit response helpers. */
221
+ interface RateLimitResult {
222
+ limit: number;
223
+ remaining: number;
224
+ resetMs: number;
225
+ }
226
+ /**
227
+ * Create a 429 "Too Many Requests" response with RFC-compliant rate limit headers.
228
+ *
229
+ * @example
230
+ * ```typescript
231
+ * if (!result.allowed) {
232
+ * return rateLimitResponse(result)
233
+ * }
234
+ * ```
235
+ */
236
+ declare function rateLimitResponse(result: RateLimitResult): Response;
237
+ /**
238
+ * Add rate limit headers to an existing response.
239
+ *
240
+ * Use this to attach rate limit info to successful responses so clients
241
+ * can see their remaining quota.
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * const response = new Response(JSON.stringify(data), { status: 200 })
246
+ * return addRateLimitHeaders(response, result)
247
+ * ```
248
+ */
249
+ declare function addRateLimitHeaders(response: Response, result: RateLimitResult): Response;
250
+ /**
251
+ * Safe Zod validation — returns { success, data?, errors? } without throwing.
252
+ * Useful for API routes where you want to return validation errors as JSON.
253
+ *
254
+ * Uses structural typing for the schema parameter so platform-core
255
+ * doesn't depend on Zod directly.
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * const result = safeValidate(MySchema, await request.json())
260
+ * if (!result.success) {
261
+ * return new Response(JSON.stringify({ errors: result.errors }), { status: 400 })
262
+ * }
263
+ * const data = result.data // fully typed
264
+ * ```
265
+ */
266
+ declare function safeValidate<T>(schema: {
267
+ safeParse: (data: unknown) => {
268
+ success: boolean;
269
+ data?: T;
270
+ error?: {
271
+ issues: Array<{
272
+ path: (string | number)[];
273
+ message: string;
274
+ }>;
275
+ };
276
+ };
277
+ }, data: unknown): {
278
+ success: true;
279
+ data: T;
280
+ } | {
281
+ success: false;
282
+ errors: Array<{
283
+ field: string;
284
+ message: string;
285
+ }>;
286
+ };
287
+
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;
175
389
 
176
- export { RateLimitOptions, RateLimitRule, RateLimitStore, type RedisRateLimitClient, type RedisRateLimitStoreOptions, createRedisRateLimitStore, enforceRateLimit, errorResponse, extractBearerToken, isValidBearerToken, zodErrorResponse };
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
@@ -1,5 +1,5 @@
1
- import { R as RateLimitStore, a as RateLimitRule, b as RateLimitOptions } from './env-DHPZR3Lv.js';
2
- export { al as AllowlistConfig, A as ApiError, d as ApiErrorCode, f as ApiErrorCodeType, h as ApiPaginatedResponse, Q as ApiSecurityConfig, V as ApiSecurityContext, g as ApiSuccessResponse, aD as AuditRequest, H as AuthCookiesConfig, N as AuthMethod, aL as BetaClientConfig, C as CommonApiErrors, ar as CommonRateLimits, ac as DateRangeInput, a6 as DateRangeSchema, ag as DeploymentStage, aa as EmailInput, $ as EmailSchema, E as EnvValidationConfig, p as EnvValidationResult, ai as FlagDefinition, aj as FlagDefinitions, ah as FlagValue, r as KEYCLOAK_DEFAULT_ROLES, F as KeycloakCallbacksConfig, K as KeycloakConfig, G as KeycloakJwtFields, q as KeycloakTokenSet, ae as LoginInput, a8 as LoginSchema, ay as OpsAuditActor, aA as OpsAuditEvent, aC as OpsAuditLoggerOptions, aB as OpsAuditRecord, az as OpsAuditResource, ab as PaginationInput, a5 as PaginationSchema, a0 as PasswordSchema, a3 as PersonNameSchema, a2 as PhoneSchema, aq as RateLimitCheckResult, P as RateLimitPreset, J as RedirectCallbackConfig, ak as ResolvedFlags, O as RouteAuditConfig, ad as SearchQueryInput, a7 as SearchQuerySchema, U as SecuritySession, af as SignupInput, a9 as SignupSchema, a1 as SlugSchema, aF as StandardAuditActionType, aE as StandardAuditActions, S as StandardRateLimitPresets, T as TokenRefreshResult, _ as WrapperPresets, ao as buildAllowlist, I as buildAuthCookies, Z as buildErrorBody, M as buildKeycloakCallbacks, e as buildPagination, Y as buildRateLimitHeaders, aw as buildRateLimitResponseHeaders, L as buildRedirectCallback, y as buildTokenRefreshParams, n as checkEnvVars, at as checkRateLimit, c as classifyError, aR as clearStoredBetaCode, aJ as createAuditActor, aK as createAuditLogger, aM as createBetaClient, an as createFeatureFlags, as as createMemoryRateLimitStore, a4 as createSafeTextSchema, am as detectStage, aG as extractAuditIp, aI as extractAuditRequestId, aH as extractAuditUserAgent, X as extractClientIp, aN as fetchBetaSettings, l as getBoolEnv, B as getEndSessionEndpoint, o as getEnvSummary, m as getIntEnv, k as getOptionalEnv, au as getRateLimitStatus, j as getRequiredEnv, aQ as getStoredBetaCode, z as getTokenEndpoint, w as hasAllRoles, u as hasAnyRole, t as hasRole, ap as isAllowlisted, i as isApiError, x as isTokenExpired, s as parseKeycloakRoles, D as refreshKeycloakToken, av as resetRateLimitForKey, ax as resolveIdentifier, W as resolveRateLimitIdentifier, aP as storeBetaCode, aO as validateBetaCode, v as validateEnvVars } from './env-DHPZR3Lv.js';
1
+ import { R as RateLimitStore, a as RateLimitRule, b as RateLimitOptions } from './env-CYKVNpLl.js';
2
+ export { al as AllowlistConfig, A as ApiError, d as ApiErrorCode, f as ApiErrorCodeType, h as ApiPaginatedResponse, Q as ApiSecurityConfig, V as ApiSecurityContext, g as ApiSuccessResponse, aD as AuditRequest, H as AuthCookiesConfig, N as AuthMethod, aL as BetaClientConfig, C as CommonApiErrors, ar as CommonRateLimits, ac as DateRangeInput, a6 as DateRangeSchema, ag as DeploymentStage, aa as EmailInput, $ as EmailSchema, E as EnvValidationConfig, p as EnvValidationResult, ai as FlagDefinition, aj as FlagDefinitions, ah as FlagValue, r as KEYCLOAK_DEFAULT_ROLES, F as KeycloakCallbacksConfig, K as KeycloakConfig, G as KeycloakJwtFields, q as KeycloakTokenSet, ae as LoginInput, a8 as LoginSchema, ay as OpsAuditActor, aA as OpsAuditEvent, aC as OpsAuditLoggerOptions, aB as OpsAuditRecord, az as OpsAuditResource, ab as PaginationInput, a5 as PaginationSchema, a0 as PasswordSchema, a3 as PersonNameSchema, a2 as PhoneSchema, aq as RateLimitCheckResult, P as RateLimitPreset, J as RedirectCallbackConfig, ak as ResolvedFlags, O as RouteAuditConfig, ad as SearchQueryInput, a7 as SearchQuerySchema, U as SecuritySession, af as SignupInput, a9 as SignupSchema, a1 as SlugSchema, aF as StandardAuditActionType, aE as StandardAuditActions, S as StandardRateLimitPresets, T as TokenRefreshResult, _ as WrapperPresets, ao as buildAllowlist, I as buildAuthCookies, Z as buildErrorBody, M as buildKeycloakCallbacks, e as buildPagination, Y as buildRateLimitHeaders, aw as buildRateLimitResponseHeaders, L as buildRedirectCallback, y as buildTokenRefreshParams, n as checkEnvVars, at as checkRateLimit, c as classifyError, aR as clearStoredBetaCode, aJ as createAuditActor, aK as createAuditLogger, aM as createBetaClient, an as createFeatureFlags, as as createMemoryRateLimitStore, a4 as createSafeTextSchema, am as detectStage, aG as extractAuditIp, aI as extractAuditRequestId, aH as extractAuditUserAgent, X as extractClientIp, aN as fetchBetaSettings, l as getBoolEnv, B as getEndSessionEndpoint, o as getEnvSummary, m as getIntEnv, k as getOptionalEnv, au as getRateLimitStatus, j as getRequiredEnv, aQ as getStoredBetaCode, z as getTokenEndpoint, w as hasAllRoles, u as hasAnyRole, t as hasRole, ap as isAllowlisted, i as isApiError, x as isTokenExpired, s as parseKeycloakRoles, D as refreshKeycloakToken, av as resetRateLimitForKey, ax as resolveIdentifier, W as resolveRateLimitIdentifier, aP as storeBetaCode, aO as validateBetaCode, v as validateEnvVars } from './env-CYKVNpLl.js';
3
3
  export { c as constantTimeEqual, b as containsHtml, a as containsUrls, e as escapeHtml, g as getCorrelationId, d as sanitizeApiError, s as stripHtml } from './security-BvLXaQkv.js';
4
4
  import 'zod';
5
5
 
@@ -172,5 +172,219 @@ declare function isValidBearerToken(request: {
172
172
  get(name: string): string | null;
173
173
  };
174
174
  }, secret: string | undefined): boolean;
175
+ /**
176
+ * Verify cron/sync endpoint authentication using a bearer token.
177
+ *
178
+ * Extracts the Bearer token from the Authorization header and compares
179
+ * it against `CRON_SECRET` (or a custom secret) using timing-safe comparison.
180
+ * Returns a 401/500 Response on failure, or `null` on success.
181
+ *
182
+ * @param request - Incoming request (must have headers.get)
183
+ * @param secret - Custom secret to compare against. Defaults to `process.env.CRON_SECRET`.
184
+ * @returns `null` if authorized, or a JSON Response (401/500) if not.
185
+ *
186
+ * @example
187
+ * ```typescript
188
+ * export async function POST(request: NextRequest) {
189
+ * const authError = verifyCronAuth(request)
190
+ * if (authError) return authError
191
+ * // ... handle cron job
192
+ * }
193
+ * ```
194
+ */
195
+ declare function verifyCronAuth(request: {
196
+ headers: {
197
+ get(name: string): string | null;
198
+ };
199
+ }, secret?: string): Response | null;
200
+ /**
201
+ * Extract the client IP address from a request, checking proxy headers.
202
+ *
203
+ * Checks in order:
204
+ * 1. `cf-connecting-ip` (Cloudflare)
205
+ * 2. `x-real-ip` (Nginx)
206
+ * 3. `x-forwarded-for` (first IP from comma-separated list)
207
+ * 4. Falls back to `'unknown'`
208
+ *
209
+ * @example
210
+ * ```typescript
211
+ * const ip = getClientIp(request)
212
+ * await auditLog({ action: 'login', ip })
213
+ * ```
214
+ */
215
+ declare function getClientIp(request: {
216
+ headers: {
217
+ get(name: string): string | null;
218
+ };
219
+ }): string;
220
+ /** Shape expected by rate limit response helpers. */
221
+ interface RateLimitResult {
222
+ limit: number;
223
+ remaining: number;
224
+ resetMs: number;
225
+ }
226
+ /**
227
+ * Create a 429 "Too Many Requests" response with RFC-compliant rate limit headers.
228
+ *
229
+ * @example
230
+ * ```typescript
231
+ * if (!result.allowed) {
232
+ * return rateLimitResponse(result)
233
+ * }
234
+ * ```
235
+ */
236
+ declare function rateLimitResponse(result: RateLimitResult): Response;
237
+ /**
238
+ * Add rate limit headers to an existing response.
239
+ *
240
+ * Use this to attach rate limit info to successful responses so clients
241
+ * can see their remaining quota.
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * const response = new Response(JSON.stringify(data), { status: 200 })
246
+ * return addRateLimitHeaders(response, result)
247
+ * ```
248
+ */
249
+ declare function addRateLimitHeaders(response: Response, result: RateLimitResult): Response;
250
+ /**
251
+ * Safe Zod validation — returns { success, data?, errors? } without throwing.
252
+ * Useful for API routes where you want to return validation errors as JSON.
253
+ *
254
+ * Uses structural typing for the schema parameter so platform-core
255
+ * doesn't depend on Zod directly.
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * const result = safeValidate(MySchema, await request.json())
260
+ * if (!result.success) {
261
+ * return new Response(JSON.stringify({ errors: result.errors }), { status: 400 })
262
+ * }
263
+ * const data = result.data // fully typed
264
+ * ```
265
+ */
266
+ declare function safeValidate<T>(schema: {
267
+ safeParse: (data: unknown) => {
268
+ success: boolean;
269
+ data?: T;
270
+ error?: {
271
+ issues: Array<{
272
+ path: (string | number)[];
273
+ message: string;
274
+ }>;
275
+ };
276
+ };
277
+ }, data: unknown): {
278
+ success: true;
279
+ data: T;
280
+ } | {
281
+ success: false;
282
+ errors: Array<{
283
+ field: string;
284
+ message: string;
285
+ }>;
286
+ };
287
+
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;
175
389
 
176
- export { RateLimitOptions, RateLimitRule, RateLimitStore, type RedisRateLimitClient, type RedisRateLimitStoreOptions, createRedisRateLimitStore, enforceRateLimit, errorResponse, extractBearerToken, isValidBearerToken, zodErrorResponse };
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 };