import { R as RateLimitStore, a as RateLimitRule, b as RateLimitOptions } from './env-BJWliXSO.js';
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-BJWliXSO.js';
export { c as constantTimeEqual, b as containsHtml, a as containsUrls, e as escapeHtml, h as getCorrelationId, g as sanitizeApiError, s as stripHtml, d as stripNullBytes, f as stripNullBytesDeep } from './security-DMUNYqQI.js';
import 'zod';

/**
 * Redis Rate Limit Store
 *
 * Production-ready `RateLimitStore` implementation using Redis sorted sets
 * for accurate sliding window rate limiting. Works with ioredis or any
 * Redis client that supports the required commands.
 *
 * @example
 * ```typescript
 * import Redis from 'ioredis'
 * import {
 *   createRedisRateLimitStore,
 *   checkRateLimit,
 *   CommonRateLimits,
 * } from '@digilogiclabs/platform-core/auth'
 *
 * const redis = new Redis(process.env.REDIS_URL)
 * const store = createRedisRateLimitStore(redis, { keyPrefix: 'myapp:' })
 *
 * const result = await checkRateLimit('api-call', 'user:123', CommonRateLimits.apiGeneral, { store })
 * ```
 */

/**
 * Minimal Redis client interface.
 *
 * Compatible with ioredis, node-redis v4, and @upstash/redis.
 * Only the commands used by the rate limiter are required.
 */
interface RedisRateLimitClient {
    zremrangebyscore(key: string, min: number | string, max: number | string): Promise<number>;
    zadd(key: string, score: number, member: string): Promise<number>;
    zcard(key: string): Promise<number>;
    expire(key: string, seconds: number): Promise<number>;
    get(key: string): Promise<string | null>;
    ttl(key: string): Promise<number>;
    setex(key: string, seconds: number, value: string): Promise<string>;
    del(...keys: string[]): Promise<number>;
}
interface RedisRateLimitStoreOptions {
    /** Prefix for all Redis keys (e.g., 'myapp:') */
    keyPrefix?: string;
}
/**
 * Create a Redis-backed rate limit store using sorted sets.
 *
 * Uses the sliding window algorithm:
 * - Each request adds a timestamped entry to a sorted set
 * - Old entries (outside the window) are pruned on each check
 * - The set cardinality gives the request count
 *
 * Blocks use simple key-value with TTL.
 */
declare function createRedisRateLimitStore(redis: RedisRateLimitClient, options?: RedisRateLimitStoreOptions): RateLimitStore;

/**
 * Next.js API Route Helpers
 *
 * Shared utilities for Next.js API routes that all apps need.
 * These wrap platform-core's framework-agnostic primitives into
 * Next.js-specific patterns (NextResponse, NextRequest).
 *
 * @example
 * ```typescript
 * import {
 *   enforceRateLimit,
 *   zodErrorResponse,
 *   errorResponse,
 *   extractBearerToken,
 *   isValidBearerToken,
 * } from '@digilogiclabs/platform-core/auth'
 *
 * export async function POST(request: NextRequest) {
 *   const rl = await enforceRateLimit(request, 'submit', CommonRateLimits.publicForm)
 *   if (rl) return rl
 *
 *   const parsed = schema.safeParse(await request.json())
 *   if (!parsed.success) return zodErrorResponse(parsed.error)
 *
 *   try { ... }
 *   catch (error) { return errorResponse(error) }
 * }
 * ```
 */

/**
 * Enforce rate limiting on a Next.js API request.
 *
 * Returns a 429 NextResponse if the limit is exceeded, or `null` if allowed.
 * Apps use this at the top of route handlers for early rejection.
 *
 * @example
 * ```typescript
 * const rateLimited = await enforceRateLimit(request, 'sync', CommonRateLimits.adminAction)
 * if (rateLimited) return rateLimited
 * ```
 */
declare function enforceRateLimit(request: {
    headers: {
        get(name: string): string | null;
    };
}, operation: string, rule: RateLimitRule, options?: {
    /** Override identifier (default: extracted from x-forwarded-for) */
    identifier?: string;
    /** User ID for per-user limiting */
    userId?: string;
    /** Rate limit store and options */
    rateLimitOptions?: RateLimitOptions;
}): Promise<Response | null>;
/**
 * Convert any error into a structured JSON response.
 *
 * Uses platform-core's `classifyError()` to handle ApiError,
 * Zod errors, PostgreSQL errors, and generic errors consistently.
 *
 * @example
 * ```typescript
 * catch (error) {
 *   return errorResponse(error)
 * }
 * ```
 */
declare function errorResponse(error: unknown, options?: {
    isDevelopment?: boolean;
}): Response;
/**
 * Convert a Zod validation error into a user-friendly 400 response.
 *
 * Extracts the first issue and returns a clear error message
 * with the field path (e.g., "email: Invalid email").
 *
 * @example
 * ```typescript
 * const parsed = schema.safeParse(await request.json())
 * if (!parsed.success) return zodErrorResponse(parsed.error)
 * ```
 */
declare function zodErrorResponse(error: {
    issues: Array<{
        path: PropertyKey[];
        message: string;
    }>;
}): Response;
/**
 * Extract a bearer token from the Authorization header.
 *
 * @returns The token string, or null if not present/malformed.
 */
declare function extractBearerToken(request: {
    headers: {
        get(name: string): string | null;
    };
}): string | null;
/**
 * Check if a request's bearer token matches a secret.
 * Uses timing-safe comparison to prevent timing attacks.
 *
 * @example
 * ```typescript
 * if (!isValidBearerToken(request, process.env.ADMIN_SECRET)) {
 *   return new Response('Unauthorized', { status: 401 })
 * }
 * ```
 */
declare function isValidBearerToken(request: {
    headers: {
        get(name: string): string | null;
    };
}, secret: string | undefined): boolean;
/**
 * Verify cron/sync endpoint authentication using a bearer token.
 *
 * Extracts the Bearer token from the Authorization header and compares
 * it against `CRON_SECRET` (or a custom secret) using timing-safe comparison.
 * Returns a 401/500 Response on failure, or `null` on success.
 *
 * @param request - Incoming request (must have headers.get)
 * @param secret - Custom secret to compare against. Defaults to `process.env.CRON_SECRET`.
 * @returns `null` if authorized, or a JSON Response (401/500) if not.
 *
 * @example
 * ```typescript
 * export async function POST(request: NextRequest) {
 *   const authError = verifyCronAuth(request)
 *   if (authError) return authError
 *   // ... handle cron job
 * }
 * ```
 */
declare function verifyCronAuth(request: {
    headers: {
        get(name: string): string | null;
    };
}, secret?: string): Response | null;
/**
 * Extract the client IP address from a request, checking proxy headers.
 *
 * Checks in order:
 * 1. `cf-connecting-ip` (Cloudflare)
 * 2. `x-real-ip` (Nginx)
 * 3. `x-forwarded-for` (first IP from comma-separated list)
 * 4. Falls back to `'unknown'`
 *
 * @example
 * ```typescript
 * const ip = getClientIp(request)
 * await auditLog({ action: 'login', ip })
 * ```
 */
declare function getClientIp(request: {
    headers: {
        get(name: string): string | null;
    };
}): string;
/** Shape expected by rate limit response helpers. */
interface RateLimitResult {
    limit: number;
    remaining: number;
    resetMs: number;
}
/**
 * Create a 429 "Too Many Requests" response with RFC-compliant rate limit headers.
 *
 * @example
 * ```typescript
 * if (!result.allowed) {
 *   return rateLimitResponse(result)
 * }
 * ```
 */
declare function rateLimitResponse(result: RateLimitResult): Response;
/**
 * Add rate limit headers to an existing response.
 *
 * Use this to attach rate limit info to successful responses so clients
 * can see their remaining quota.
 *
 * @example
 * ```typescript
 * const response = new Response(JSON.stringify(data), { status: 200 })
 * return addRateLimitHeaders(response, result)
 * ```
 */
declare function addRateLimitHeaders(response: Response, result: RateLimitResult): Response;
/**
 * Safe Zod validation — returns { success, data?, errors? } without throwing.
 * Useful for API routes where you want to return validation errors as JSON.
 *
 * Uses structural typing for the schema parameter so platform-core
 * doesn't depend on Zod directly.
 *
 * @example
 * ```typescript
 * const result = safeValidate(MySchema, await request.json())
 * if (!result.success) {
 *   return new Response(JSON.stringify({ errors: result.errors }), { status: 400 })
 * }
 * const data = result.data // fully typed
 * ```
 */
declare function safeValidate<T>(schema: {
    safeParse: (data: unknown) => {
        success: boolean;
        data?: T;
        error?: {
            issues: Array<{
                path: (string | number)[];
                message: string;
            }>;
        };
    };
}, data: unknown): {
    success: true;
    data: T;
} | {
    success: false;
    errors: Array<{
        field: string;
        message: string;
    }>;
};

/**
 * Federated Logout Handler for Keycloak + Auth.js
 *
 * Builds a Next.js route handler that:
 * 1. Revokes the refresh token with Keycloak
 * 2. Returns an HTML page that clears all auth cookies
 * 3. Auto-redirects to Keycloak's OIDC logout endpoint
 *
 * Uses HTML 200 (not 307 redirect) because browsers may not reliably
 * process Set-Cookie headers on redirect responses through CDN proxies.
 *
 * Edge-runtime compatible.
 *
 * @example
 * ```typescript
 * // app/api/auth/federated-logout/route.ts
 * import { buildFederatedLogoutHandler } from '@digilogiclabs/platform-core/auth'
 * import { auth } from '@/auth'
 *
 * export const dynamic = 'force-dynamic'
 *
 * export const GET = buildFederatedLogoutHandler({
 *   auth,
 *   domain: '.digilogiclabs.com',
 *   baseUrlFallback: 'https://digilogiclabs.com',
 * })
 * ```
 */
interface FederatedLogoutConfig {
    /**
     * Auth.js `auth()` function — used to get the session's idToken.
     * Must return an object with an optional `idToken` field.
     */
    auth: () => Promise<{
        idToken?: string;
    } | null>;
    /**
     * Production cookie domain (e.g. '.digilogiclabs.com').
     * Used to clear cookies scoped to the root domain.
     */
    domain: string;
    /**
     * Fallback base URL when NEXTAUTH_URL/AUTH_URL are not set.
     * e.g. 'https://digilogiclabs.com'
     */
    baseUrlFallback: string;
    /**
     * Additional cookie names to clear beyond the standard Auth.js set.
     * e.g. ['admin-session']
     */
    extraCookies?: string[];
    /**
     * Optional error reporter. Called on missing issuer and token revocation failures.
     * If not provided, errors are silently ignored.
     */
    onError?: (message: string, error?: unknown) => void;
    /**
     * Number of chunk suffixes to enumerate when clearing session-token cookies.
     *
     * Auth.js splits encrypted JWE session tokens across `.0`, `.1`, `.2` ...
     * suffixed cookies when the value exceeds the ~4 KB per-cookie limit.
     * Keycloak access + id + refresh tokens reliably push past this threshold,
     * so the unsuffixed name alone does NOT clear the session in production —
     * the chunks survive and Auth.js reassembles the session on the next
     * `/api/auth/session` call, causing silent re-auth after logout.
     *
     * Default: `3` (covers `.0`/`.1`/`.2` — 12 KB ceiling, well above any
     * realistic JWE). Raise only if your tokens are unusually large; each
     * additional chunk adds 4 Set-Cookie headers (host + domain scopes for
     * both `authjs` and `next-auth` session-token names), and the total
     * response headers must stay under Cloudflare's ~16 KB HTTP/2 limit.
     */
    chunkCount?: number;
    /**
     * Whether to also clear legacy `next-auth.*` cookie names in addition to
     * the `authjs.*` names. Defaults to `true` for compatibility with apps
     * that migrated from NextAuth v4 → Auth.js v5 (their sessions may still
     * be under the old names). Set to `false` on apps that never ran
     * pre-Auth.js-v5 to slim the Set-Cookie response.
     *
     * Default: `true`
     */
    includeLegacyNextAuthCookies?: boolean;
}
/**
 * Build a Next.js route handler for federated logout with Keycloak.
 *
 * The returned handler:
 * - Validates the callbackUrl against an allowlist (open redirect protection)
 * - Revokes the Keycloak refresh token
 * - Clears all Auth.js cookies (both standard and __Secure- prefixed)
 * - Returns an HTML page that auto-redirects to Keycloak's OIDC logout
 */
declare function buildFederatedLogoutHandler(config: FederatedLogoutConfig): (request: Request) => Promise<Response>;

/**
 * Lazy Rate Limit Store
 *
 * Creates a singleton rate limit store backed by Redis with automatic
 * fallback to in-memory when Redis is unavailable.
 *
 * @example
 * ```typescript
 * // lib/rate-limit-store.ts
 * import { createLazyRateLimitStore } from '@digilogiclabs/platform-core/auth'
 * import { getRedisClient } from './redis'
 *
 * export const getRateLimitStore = createLazyRateLimitStore(getRedisClient)
 * ```
 */

interface LazyRateLimitStoreOptions {
    /** Redis key prefix for rate limit keys (default: 'rl:') */
    keyPrefix?: string;
}
/**
 * Create a lazy singleton rate limit store factory.
 *
 * Returns a getter function that:
 * - On first call, attempts to get a Redis client and create a Redis-backed store
 * - If Redis is unavailable, returns undefined (enforceRateLimit falls back to in-memory)
 * - Caches the result for subsequent calls
 *
 * @param getRedis - Function that returns a Redis client (or null/undefined if unavailable)
 * @param options - Optional configuration
 * @returns A getter function that returns the RateLimitStore or undefined
 */
declare function createLazyRateLimitStore(getRedis: () => RedisRateLimitClient | null | undefined, options?: LazyRateLimitStoreOptions): () => RateLimitStore | undefined;

/**
 * Composable Secure API Handler Factory
 *
 * A generic, dependency-injected version of DLL's `withAuthenticatedApi` pattern.
 * Apps provide their own auth, rate limiting, audit, and logger implementations
 * via `createSecureHandlerFactory()`, then use the returned wrappers.
 *
 * This avoids coupling to any specific auth library (Auth.js, Keycloak, etc.)
 * while providing consistent security middleware across all apps.
 *
 * @example
 * ```typescript
 * // lib/api-security.ts — app-level setup (once per app)
 * import { createSecureHandlerFactory } from '@digilogiclabs/platform-core/auth'
 * import { auth } from '@/auth'
 *
 * export const { withPublicApi, withAuthenticatedApi, withAdminApi } =
 *   createSecureHandlerFactory({
 *     getSession: () => auth(),
 *     isAdmin: (session) => session?.user?.roles?.includes('admin') ?? false,
 *     rateLimiter: { enforce: enforceRateLimit, presets: MY_PRESETS },
 *     classifyError,
 *   })
 *
 * // app/api/items/route.ts — per-route usage
 * export const POST = withAuthenticatedApi({
 *   rateLimit: 'mutation',
 *   validate: CreateItemSchema,
 * }, async (request, { session, validated }) => {
 *   return Response.json({ created: true })
 * })
 * ```
 */
/** Minimal session shape — apps extend this with their own session type */
interface SecureSession {
    user?: {
        id?: string;
        email?: string | null;
        name?: string | null;
        roles?: string[];
        [key: string]: unknown;
    };
    [key: string]: unknown;
}
/** Result from a rate limit check */
interface SecureRateLimitResult {
    allowed: boolean;
    limit: number;
    remaining: number;
    current?: number;
    resetMs?: number;
}
/** Minimal logger interface */
interface SecureLogger {
    info(message: string, meta?: Record<string, unknown>): void;
    warn(message: string, meta?: Record<string, unknown>): void;
    error(message: string, meta?: Record<string, unknown>): void;
}
/** Configuration for the secure handler factory */
interface SecureHandlerFactoryConfig<TSession extends SecureSession = SecureSession, TPresetKey extends string = string> {
    /** Get the current authenticated session (e.g. from Auth.js) */
    getSession: () => Promise<TSession | null>;
    /** Check if a session has admin privileges */
    isAdmin: (session: TSession | null) => boolean;
    /** Check if a session has any of the required roles */
    hasAnyRole?: (session: TSession | null, roles: string[]) => boolean;
    /** Rate limiting integration */
    rateLimiter?: {
        /** Check and enforce rate limit — return response if blocked, null if allowed */
        enforce: (request: Request, operation: string, preset: TPresetKey, userId?: string) => Promise<Response | null>;
        /** Default preset for public APIs */
        publicDefault?: TPresetKey;
        /** Default preset for authenticated APIs */
        authDefault?: TPresetKey;
        /** Default preset for admin APIs */
        adminDefault?: TPresetKey;
    };
    /** Optional: legacy admin token secret (Bearer token auth) */
    adminSecret?: string;
    /** Optional: audit logging */
    auditLog?: (entry: {
        actor: {
            id: string;
            type: string;
            email?: string;
        };
        action: string;
        resource?: {
            type: string;
            id: string;
        };
        outcome: "success" | "failure" | "blocked";
        reason?: string;
        metadata?: Record<string, unknown>;
    }) => Promise<void>;
    /** Optional: create a request-scoped logger */
    createLogger?: (request: Request, operation: string) => SecureLogger;
    /**
     * Optional: create a timed audit helper for the handler.
     * Called after auth + validation, before handler execution.
     * The returned object is placed on context.timedAudit so handlers
     * can call `timedAudit.success(metadata)` / `timedAudit.failure(reason, metadata)`.
     */
    createTimedAudit?: (auditConfig: {
        action: string;
        resource?: {
            type: string;
            id?: string;
        };
        actor: {
            id: string;
            type: string;
            email?: string;
        };
    }, request: Request) => SecureTimedAudit;
    /** Optional: error classifier (defaults to platform-core classifyError) */
    classifyError?: (error: unknown, isDev: boolean) => {
        status: number;
        body: {
            error: string;
            code?: string;
        };
    };
    /** Is the app running in development mode (default: NODE_ENV check) */
    isDevelopment?: boolean;
    /**
     * Auto-strip NUL bytes (U+0000) from query params and JSON body before
     * they reach `validate.safeParse` and the handler's validated context.
     *
     * Why: Postgres rejects NUL bytes in text columns with an unhelpful
     * `invalid byte sequence` → HTTP 500 `DATABASE_ERROR`. Since 1.17.3 apps
     * have had `stripNullBytes` / `stripNullBytesDeep` utilities and have
     * been inlining them at every route that forwards user input to the DB.
     * This factory option moves the strip to a single place — every
     * schema-validated route gets it for free.
     *
     * Scope: only applies to routes that use `validate` (we sanitize `input`
     * right before `safeParse`). Routes that parse `request.url` or
     * `request.json()` themselves still need to call `stripNullBytes` /
     * `stripNullBytesDeep` manually.
     *
     * @default true
     */
    sanitizeNullBytes?: boolean;
}
/** Timed audit helper placed on context for handler-level audit control */
interface SecureTimedAudit {
    success: (metadata?: Record<string, unknown>) => Promise<void>;
    failure: (reason: string, metadata?: Record<string, unknown>) => Promise<void>;
}
/** Authentication method — how the request was authenticated */
type SecureAuthMethod = "session" | "legacy_token" | "none";
/**
 * Object-style audit configuration (used by DLL and other apps with
 * structured audit enums). This is an alternative to the flat string form.
 */
interface SecureAuditConfig {
    /** The audit action (string or enum value) */
    action: string;
    /** Resource type being acted upon */
    resource?: string;
    /** Extract resource ID from request/params for audit (called after validation) */
    getResourceId?: (request: Request, params: Record<string, string>, validated: unknown) => string | undefined;
}
/** Per-route config */
interface SecureRouteConfig<TValidated = unknown, TPresetKey extends string = string> {
    /** Rate limit preset key */
    rateLimit?: TPresetKey;
    /** Skip rate limiting */
    skipRateLimit?: boolean;
    /** Zod schema for input validation (POST/PUT body, GET query params) */
    validate?: {
        safeParse: (data: unknown) => {
            success: boolean;
            data?: TValidated;
            error?: {
                issues: Array<{
                    path: PropertyKey[];
                    message: string;
                }>;
                flatten?: () => {
                    fieldErrors: Record<string, string[]>;
                };
            };
        };
    };
    /**
     * Audit configuration — accepts either:
     * - A string (action name, with optional `auditResource` for resource type)
     * - An object `{ action, resource?, getResourceId? }` for structured audit enums
     */
    audit?: string | SecureAuditConfig;
    /** Audit resource type (used when `audit` is a string) */
    auditResource?: string;
    /** Extract resource ID from request/params for audit (called after validation).
     *  Used when `audit` is a string. When `audit` is an object, use its `getResourceId` instead. */
    getResourceId?: (request: Request, params: Record<string, string>, validated: unknown) => string | undefined;
    /** Custom operation name for logging */
    operation?: string;
}
/** Context passed to route handlers */
interface SecureRouteContext<TSession extends SecureSession = SecureSession, TValidated = unknown, TParams = Record<string, string>> {
    /** Authenticated session (null for public APIs) */
    session: TSession | null;
    /** Whether request was authenticated via legacy token */
    isLegacyToken: boolean;
    /** How the request was authenticated */
    authMethod: SecureAuthMethod;
    /** Whether the user has admin privileges */
    isAdmin: boolean;
    /** Validated input data */
    validated: TValidated;
    /** Request-scoped logger */
    logger: SecureLogger;
    /** Request correlation ID */
    requestId: string;
    /** Resolved route params */
    params: TParams;
    /** Timed audit helper (present when factory has createTimedAudit and route has audit config) */
    timedAudit?: SecureTimedAudit;
}
/** Route handler function */
type SecureRouteHandler<TSession extends SecureSession = SecureSession, TValidated = unknown, TParams = Record<string, string>> = (request: Request, context: SecureRouteContext<TSession, TValidated, TParams>) => Promise<Response>;
/**
 * Create a set of composable API security wrappers for your app.
 *
 * Returns `withPublicApi`, `withAuthenticatedApi`, `withAdminApi`,
 * and `createSecureHandler` for custom auth requirements.
 */
declare function createSecureHandlerFactory<TSession extends SecureSession = SecureSession, TPresetKey extends string = string>(factoryConfig: SecureHandlerFactoryConfig<TSession, TPresetKey>): {
    createSecureHandler: <TValidated = unknown, TParams = Record<string, string>>(routeConfig: SecureRouteConfig<TValidated, TPresetKey> & {
        requireAuth?: boolean;
        requireAdmin?: boolean;
        requireRoles?: string[];
        allowLegacyToken?: boolean;
    }, handler: SecureRouteHandler<TSession, TValidated, TParams>) => (request: Request, routeContext: {
        params: Promise<TParams>;
    }) => Promise<Response>;
    withPublicApi: <TValidated = unknown, TParams = Record<string, string>>(config: SecureRouteConfig<TValidated, TPresetKey>, handler: SecureRouteHandler<TSession, TValidated, TParams>) => (request: Request, routeContext: {
        params: Promise<TParams>;
    }) => Promise<Response>;
    withAuthenticatedApi: <TValidated = unknown, TParams = Record<string, string>>(config: SecureRouteConfig<TValidated, TPresetKey>, handler: SecureRouteHandler<TSession, TValidated, TParams>) => (request: Request, routeContext: {
        params: Promise<TParams>;
    }) => Promise<Response>;
    withAdminApi: <TValidated = unknown, TParams = Record<string, string>>(config: SecureRouteConfig<TValidated, TPresetKey>, handler: SecureRouteHandler<TSession, TValidated, TParams>) => (request: Request, routeContext: {
        params: Promise<TParams>;
    }) => Promise<Response>;
    withLegacyAdminApi: <TValidated = unknown, TParams = Record<string, string>>(config: SecureRouteConfig<TValidated, TPresetKey>, handler: SecureRouteHandler<TSession, TValidated, TParams>) => (request: Request, routeContext: {
        params: Promise<TParams>;
    }) => Promise<Response>;
};

/**
 * createEnsureUser — drift-proof choke point for resolving a Keycloak session
 * to a local `users.id` in apps that mirror Keycloak users into their own
 * `users` table.
 *
 * **Why this exists:** Keycloak-mirrored apps that match only by `keycloak_id`
 * silently accumulate duplicate rows whenever the Keycloak `sub` rotates
 * (admin reset, realm migration, sub-claim format change, logout-saga
 * resign-in storms). Each duplicate strands `creator_id` / favorites /
 * preferences / analytics behind an orphan row keyed on a stale sub. Same
 * email, different rows, invisible data.
 *
 * **The 3-layer pattern this factory enforces:**
 *
 * 1. **Hot path** — match by `keycloak_id` (one indexed read). If the cached
 *    email or displayName differs from the current Keycloak claims, fire
 *    `onClaimRefresh` so the local mirror stays aligned with Keycloak truth.
 *    A missing claim never erases an existing value (don't-erase-on-missing).
 *
 * 2. **Claim-on-login by email** — if no `keycloak_id` row exists but a row
 *    exists for the same (lower-cased) email, re-anchor that row to the
 *    current sub via `rebindKeycloakId`. The email is trusted because
 *    Keycloak only forwards verified addresses into the OIDC session. This
 *    is the rule that prevents new drift from being created.
 *
 * 3. **Insert** — brand-new user. Optional `onNewUser` hook for
 *    schema-specific backfills (e.g. linking pre-registration anonymous
 *    donations by email). `onNewUser` errors are swallowed; the user row
 *    is already created, so failure to backfill must not block sign-in.
 *
 * **Choke-point invariant:** every authenticated path that needs a `users.id`
 * MUST go through the function this factory returns. Use it in BOTH read and
 * write paths so every authenticated GET becomes a self-healing touchpoint —
 * stale `keycloak_id` sessions are re-anchored on the FIRST authenticated
 * request, not "first POST that mutates state." Read-side `if (!user) return
 * empty` early-returns silently mask drift behind 200 OK responses with
 * empty payloads.
 *
 * **Pair with a DB-level guard:**
 * ```sql
 * CREATE UNIQUE INDEX <prefix>_users_lower_email_uniq
 *   ON <schema>.users (lower(email))
 *   WHERE email IS NOT NULL;
 * ```
 * Apply ONLY after merging existing duplicates — pre-existing dups block
 * index creation. See `memory/user-identity-choke-point.md` for the merge
 * playbook + cross-app port reference.
 *
 * **Edge-runtime safe:** this module imports nothing — apps wire their own
 * Drizzle/pg queries via the four required callbacks. No transitive
 * dependency on `pg`, `stripe`, or any Node-only SDK.
 *
 * @example
 * ```ts
 * // app/src/lib/db/ensure-user.ts
 * import { createEnsureUser } from '@digilogiclabs/platform-core/auth';
 * import { db } from './drizzle';
 * import { users, donations } from './schema';
 * import { and, eq, isNull, sql } from 'drizzle-orm';
 *
 * export const ensureUser = createEnsureUser({
 *   async findByKeycloakId(keycloakId) {
 *     const [row] = await db.select({
 *       id: users.id,
 *       email: users.email,
 *       displayName: users.displayName,
 *     }).from(users).where(eq(users.keycloakId, keycloakId)).limit(1);
 *     return row ?? null;
 *   },
 *   async findByEmailLower(emailLower) {
 *     const [row] = await db.select({
 *       id: users.id,
 *       email: users.email,
 *       displayName: users.displayName,
 *     }).from(users)
 *       .where(sql`lower(${users.email}) = ${emailLower}`)
 *       .limit(1);
 *     return row ?? null;
 *   },
 *   async rebindKeycloakId(userId, keycloakId, name) {
 *     await db.update(users)
 *       .set({ keycloakId, ...(name ? { displayName: name } : {}), updatedAt: new Date() })
 *       .where(eq(users.id, userId));
 *   },
 *   async insertNewUser({ keycloakId, email, name }) {
 *     const [row] = await db.insert(users).values({
 *       keycloakId,
 *       email: email ?? undefined,
 *       displayName: name ?? undefined,
 *     }).returning({ id: users.id });
 *     return { id: row!.id };
 *   },
 *   async onClaimRefresh(userId, next) {
 *     await db.update(users)
 *       .set({
 *         ...(next.email !== undefined ? { email: next.email } : {}),
 *         ...(next.displayName !== undefined ? { displayName: next.displayName } : {}),
 *         updatedAt: new Date(),
 *       })
 *       .where(eq(users.id, userId));
 *   },
 *   async onNewUser(userId, email) {
 *     if (!email) return;
 *     await db.update(donations)
 *       .set({ userId })
 *       .where(and(sql`lower(${donations.email}) = ${email.toLowerCase()}`, isNull(donations.userId)));
 *   },
 * });
 * ```
 */
/**
 * Minimal row shape returned by lookup callbacks. `email` and `displayName`
 * are optional so apps that don't track those columns can omit them — they
 * only feed the `onClaimRefresh` drift-detection path.
 */
interface EnsureUserRow {
    id: string;
    email?: string | null;
    displayName?: string | null;
}
/**
 * Claims surfaced to `onClaimRefresh`. Only fields the factory actually
 * detected as changed are populated; unchanged fields are `undefined` so
 * the callback can do a sparse UPDATE without overwriting other columns.
 */
interface EnsureUserClaimDelta {
    /**
     * The current Keycloak `email` claim, if it differs from the cached value.
     * `null` means Keycloak explicitly cleared the email; `undefined` means
     * the email was unchanged or not provided in this call.
     */
    email?: string | null;
    /**
     * The current Keycloak `name` claim, if it differs from the cached value.
     * Only present when the caller supplied a non-empty name AND it differs
     * from the cached `displayName` — a missing claim never erases the
     * existing name.
     */
    displayName?: string;
}
interface EnsureUserConfig {
    /**
     * Lookup by `keycloak_id`. Should return the row with at least `id`, and
     * preferably `email` and `displayName` so the factory can detect claim
     * drift and call `onClaimRefresh`.
     */
    findByKeycloakId(keycloakId: string): Promise<EnsureUserRow | null>;
    /**
     * Lookup by lower-cased email. The factory passes the email already
     * lower-cased, so the callback can match with a simple
     * `lower(users.email) = $1` predicate. Only invoked when the caller
     * supplied a non-empty email.
     */
    findByEmailLower(emailLowerCase: string): Promise<EnsureUserRow | null>;
    /**
     * Re-anchor a stale row's `keycloak_id` to the current sub. Used when
     * step 2 (claim-on-login) finds a row by email whose sub no longer
     * matches. Implementations should ALSO refresh `displayName` when `name`
     * is non-empty (don't-erase-on-missing-claim) and bump `updatedAt`.
     */
    rebindKeycloakId(userId: string, keycloakId: string, name: string | null | undefined): Promise<void>;
    /**
     * Insert a brand-new user row. Must return `{ id }` of the new row.
     * Implementations should treat null/undefined `email` / `name` as
     * absent fields, not empty strings.
     */
    insertNewUser(input: {
        keycloakId: string;
        email: string | null | undefined;
        name: string | null | undefined;
    }): Promise<{
        id: string;
    }>;
    /**
     * Optional. Called from the hot path (step 1) when the cached email or
     * displayName differs from the current Keycloak claims. Apps that wire
     * this get a self-correcting local mirror — every authenticated request
     * brings the cached profile back into sync with Keycloak truth.
     *
     * **Don't-erase-on-missing-claim:** a missing claim is never reported as
     * a change. `email: null` means Keycloak explicitly cleared the email;
     * `email: undefined` means it wasn't supplied. Callbacks should use
     * `if (next.email !== undefined)` to distinguish.
     *
     * Errors thrown from this callback propagate to the caller — the user
     * is already authenticated and the row exists, so a failed refresh is
     * worth surfacing rather than swallowing.
     */
    onClaimRefresh?(userId: string, delta: EnsureUserClaimDelta): Promise<void>;
    /**
     * Optional. Called from step 3 (insert) after a brand-new row is
     * created. Used for app-specific backfills like linking pre-registration
     * anonymous donations by email. Receives the new user's id and the
     * email that was supplied at signup.
     *
     * Errors thrown from this callback are swallowed — the user row is
     * already created, so a failed backfill must not block sign-in. If you
     * need observability, log inside the callback.
     */
    onNewUser?(userId: string, email: string | null | undefined): Promise<void>;
}
/** Function returned by `createEnsureUser`. */
type EnsureUserFn = (keycloakId: string, email?: string | null, name?: string | null) => Promise<string>;
/**
 * Build the canonical `ensureUser(keycloakId, email?, name?)` helper for an
 * app that mirrors Keycloak users into its own `users` table.
 *
 * See file-level JSDoc for the full pattern, invariants, and example wiring.
 */
declare function createEnsureUser(config: EnsureUserConfig): EnsureUserFn;

export { type EnsureUserClaimDelta, type EnsureUserConfig, type EnsureUserFn, type EnsureUserRow, type FederatedLogoutConfig, type LazyRateLimitStoreOptions, RateLimitOptions, type RateLimitResult, RateLimitRule, RateLimitStore, type RedisRateLimitClient, type RedisRateLimitStoreOptions, type SecureAuditConfig, type SecureAuthMethod, type SecureHandlerFactoryConfig, type SecureLogger, type SecureRateLimitResult, type SecureRouteConfig, type SecureRouteContext, type SecureRouteHandler, type SecureSession, type SecureTimedAudit, addRateLimitHeaders, buildFederatedLogoutHandler, createEnsureUser, createLazyRateLimitStore, createRedisRateLimitStore, createSecureHandlerFactory, enforceRateLimit, errorResponse, extractBearerToken, getClientIp, isValidBearerToken, rateLimitResponse, safeValidate, verifyCronAuth, zodErrorResponse };
