import { bM as ChallengeDefinition, eg as PluginContext, bq as AuthHooks, br as AuthInput, bw as AuthUser, bL as Challenge, bv as AuthStrategy } from '../_dts-chunks/auth-service.d-B0csjDLa.d.ts'; export { D as DEFAULT_PREVIEW_TTL_SECONDS, P as PreviewTokenScope, a as PreviewVerifyResult, S as SignPreviewTokenOptions, p as previewTokenCovers, s as signPreviewToken, v as verifyPreviewToken } from '../_dts-chunks/preview-token.d-BMsKq2in.d.ts'; import '@nextlyhq/adapter-drizzle'; import '@nextlyhq/adapter-drizzle/types'; import 'react'; import '../_dts-chunks/nextly-error.d-WlStqaV9.d.ts'; import '../_dts-chunks/error-codes.d-CbwkO1ux.d.ts'; import '../_dts-chunks/media.d-DtIw8UQM.d.ts'; import 'zod'; import '../_dts-chunks/storage.d-CEowrt6p.d.ts'; import 'drizzle-orm'; /** * Convert a string secret to a Uint8Array key for jose. * Uses the raw bytes of the secret (UTF-8 encoded). */ declare function secretToKey(secret: string): Uint8Array; /** * Sign a JWT access token. * * @param claims - The payload claims (from buildClaims) * @param secret - The NEXTLY_SECRET string * @param ttlSeconds - Token TTL in seconds (default 900 = 15 minutes) * @returns Signed JWT string */ declare function signAccessToken(claims: Record, secret: string, ttlSeconds?: number): Promise; interface NextlyJwtPayload { sub: string; iat: number; exp: number; jti: string; email: string; name: string; image: string | null; roleIds: string[]; [key: string]: unknown; } interface BuildClaimsInput { userId: string; email: string; name: string; image: string | null; roleIds: string[]; customFields?: Record; } /** * Build JWT claims from user data. * Does NOT set iat/exp/jti -- those are set by the signing function. */ declare function buildClaims(input: BuildClaimsInput): Record; type VerifyResult = { valid: true; payload: NextlyJwtPayload; } | { valid: false; reason: "expired" | "invalid" | "malformed"; }; /** * Verify and decode a JWT access token. * * @param token - The JWT string from the cookie * @param secret - The NEXTLY_SECRET string * @returns VerifyResult with payload on success or reason on failure */ declare function verifyAccessToken(token: string, secret: string): Promise; interface SessionUser { id: string; email: string; name: string; image: string | null; roleIds: string[]; /** Custom user fields from user_ext table */ [key: string]: unknown; } interface AuthContext { userId: string; userName: string; userEmail: string; permissions: Map; roles: string[]; authMethod: "session" | "api-key"; } interface RefreshTokenRecord { id: string; userId: string; tokenHash: string; userAgent: string | null; ipAddress: string | null; expiresAt: Date; createdAt: Date; } type GetSessionResult = { authenticated: true; user: SessionUser; } | { authenticated: false; reason: "no_token" | "expired" | "invalid"; }; /** * Extract and verify the session from a request. * No database hit -- purely stateless JWT verification. */ declare function getSession(request: Request, secret: string): Promise; /** * Check if a session user has a specific role. */ declare function hasRole(user: SessionUser, roleSlug: string): boolean; /** * Check if a session user has any of the specified roles. */ declare function hasAnyRole(user: SessionUser, roleSlugs: string[]): boolean; /** * Check if a session user has all of the specified roles. */ declare function hasAllRoles(user: SessionUser, roleSlugs: string[]): boolean; /** * Generate a new opaque refresh token (64 bytes, hex-encoded = 128 chars). */ declare function generateRefreshToken(): string; /** * Hash a refresh token using SHA-256 for database storage. * We use SHA-256 (not bcrypt) because refresh tokens have high entropy (64 bytes). */ declare function hashRefreshToken(token: string): string; /** * Require a valid session. Returns the session user or throws * `NextlyError.authRequired()`. */ declare function requireAuth(request: Request, secret: string): Promise; /** * Require a specific role. Throws `NextlyError.forbidden()` if missing. * * Public message comes from the factory ("You don't have permission to * perform this action."). The required role identity is recorded in * logContext only; the wire response never includes it (spec §13.7). */ declare function requireRole(user: SessionUser, roleSlug: string): void; /** * Require any of the specified roles. Throws `NextlyError.forbidden()` if * none match. The role list lives in logContext per spec §13.7; * the wire response never includes it. */ declare function requireAnyRole(user: SessionUser, roleSlugs: string[]): void; /** * Require all of the specified roles. Throws `NextlyError.forbidden()` if * any are missing. The role list lives in logContext per spec §13.7. */ declare function requireAllRoles(user: SessionUser, roleSlugs: string[]): void; interface ErrorResponse { success: false; statusCode: number; message: string; error: string; data: null; headers?: Record; } /** * Create a JSON error response object. */ declare function createErrorResponse(statusCode: number, message: string, error: string): ErrorResponse; /** * Create a JSON Response from an ErrorResponse. */ declare function createJsonErrorResponse(errResp: ErrorResponse): Response; /** * Check if a value is an ErrorResponse. */ declare function isErrorResponse(value: unknown): value is ErrorResponse; /** * Check a single permission against an AuthContext. * For session auth: delegates to RBAC service. * For API key auth: checks pre-resolved permissions map. */ declare function checkPermission(context: AuthContext, action: string, resource: string, deps: { checkUserPermission?: (userId: string, action: string, resource: string) => Promise; }): Promise; /** * Attempt API key authentication from the Authorization header. * Returns null if no Bearer token present (not an error -- caller should try session auth). * Returns ErrorResponse if token is present but invalid/expired/rate-limited. * Returns AuthContext on success. */ declare function authenticateApiKey(request: Request, deps: { validateApiKey: (rawKey: string) => Promise<{ valid: boolean; userId?: string; permissions?: Map; roles?: string[]; error?: string; retryAfter?: number; }>; }): Promise; /** * Check if a user has the super-admin role. * * Resolves the user's full role set (direct + inherited) and checks * if any role has the `super-admin` slug. Results are cached in-memory * for 60 seconds. * * @param userId - The user ID to check * @returns `true` if the user has the super-admin role * * @example * ```typescript * if (await isSuperAdmin(userId)) { * // Bypass all access checks * } * ``` */ declare function isSuperAdmin(userId: string, executor?: unknown): Promise; type CollectionOperation = "create" | "read" | "update" | "delete" | "publish" | "unpublish"; interface CollectionAccessDeps { /** Check a user's permission via RBAC service */ checkUserPermission?: (userId: string, action: string, resource: string) => Promise; /** Evaluate code-defined access function from defineCollection */ evaluateCodeAccess?: (collectionSlug: string, operation: CollectionOperation, context: AuthContext) => Promise; } /** * Check if the auth context has access to a collection operation. * Priority: super-admin bypass > code-defined access > database RBAC. */ declare function checkCollectionAccess(context: AuthContext, collectionSlug: string, operation: CollectionOperation, deps: CollectionAccessDeps): Promise; declare function hashPassword(plain: string, saltRounds?: number): Promise; declare function verifyPassword(plain: string, hash: string): Promise; type PasswordStrengthResult = { ok: true; errors?: undefined; } | { ok: false; errors: string[]; }; declare function validatePasswordStrength(password: string): PasswordStrengthResult; /** * Single entry point for security-sensitive event recording. Callers * pass a structured event; the writer persists it to the `audit_log` * table via the database adapter. * * Behaviour contract: * * - **Never throws.** Auth handlers must not fail-open or fail-closed * because the audit table is unreachable. A DB failure logs a * structured warning via `getNextlyLogger()` and the request * continues. * - **Append-only by application convention.** This writer never offers an * update path, but the application holds two privileges an operator * hardening the table must allow for: a column-scoped UPDATE that erases a * deleted account's request identifiers, and DELETE, which retention needs * to prune rows past their window. A blanket UPDATE revoke stops * `eraseActorPersonalData`, which runs inside the user-deletion * transaction, and so stops account deletion outright; revoking DELETE * stops retention silently, since a pass must never fail the request that * offered it. See the dialect schema definitions for the exact grants. * - **Metadata is opaque JSON.** The `metadata` field stays generic * so we can extend coverage without a migration each time. Callers * pass dialect-portable JSON-serialisable values only. * * Hash-chained tamper-evidence (each row signs (prev_hash, this_row)) * is intentionally deferred — under concurrent auth events the chain * needs a lock-around-write that complicates the hot path. Operators * who need cryptographic integrity right now should rely on DB-level * * @module domains/audit/audit-log-writer * @since 1.0.0 */ type AuditEventKind = "csrf-failed" | "login-failed" | "login-succeeded" | "password-changed" | "role-assigned" | "role-revoked" | "user-deleted"; interface AuditEvent { kind: AuditEventKind; /** The user performing the action; null when unauthenticated (failed login, failed CSRF). */ actorUserId?: string | null; /** The user being acted on; null when not account-scoped. */ targetUserId?: string | null; ipAddress?: string | null; userAgent?: string | null; /** JSON-serialisable details. Goes into the dialect's JSON column. */ metadata?: Record; } interface AuditLogWriter { write(event: AuditEvent): Promise; } /** * @experimental Registry of challenge definitions a plugin can resolve (e.g. TOTP). * Keyed by challenge id; duplicate ids are a registration error (D71). */ declare class ChallengeRegistry { #private; add(def: ChallengeDefinition): void; has(id: string): boolean; resolve(id: string, args: { userId: string; response: Record; }, ctx: PluginContext): Promise<{ ok: true; } | { ok: false; reason?: string; }>; } /** * @experimental Registry that collects plugin-contributed {@link AuthHooks} and * runs each phase in registration order. Modify-style phases thread their value * through every hook; `afterAuthenticate` short-circuits the moment a hook * returns a `{ challenge }`; observe-style phases just fan out (D71). */ declare class AuthHookRegistry { #private; add(hooks: AuthHooks): void; /** True when no hooks are registered — lets the handler take the legacy fast path. */ get isEmpty(): boolean; runBeforeLogin(input: AuthInput, ctx: PluginContext): Promise; runAfterAuthenticate(user: AuthUser, ctx: PluginContext): Promise; runAfterLogin(user: AuthUser, ctx: PluginContext): Promise; runCustomizeClaims(claims: Record, user: AuthUser, ctx: PluginContext): Promise>; runDetermineUser(request: Request, ctx: PluginContext): Promise; runBeforeRegister(data: Record, ctx: PluginContext): Promise>; runAfterRegister(user: AuthUser, ctx: PluginContext): Promise; runBeforeLogout(user: AuthUser | null, ctx: PluginContext): Promise; runAfterLogout(ctx: PluginContext): Promise; } /** * A provider button rendered on the login screen (D57). Clicking it starts the * named auth strategy. */ interface AuthUiProvider { strategy: string; label: string; icon?: string; component?: string; } /** * The aggregated, public auth-page UI contract (D57). Served pre-auth to the * login screen so it can render provider buttons, the right challenge view for a * `{ status: "challenge" }` login response, and any injected form slots. * * Slots are arrays so multiple plugins can compose (e.g. two plugins each adding * something after the form). */ interface AuthUiMeta { providers: AuthUiProvider[]; /** challengeType → component path (last plugin wins on a collision). */ challengeViews: Record; slots: { beforeForm: string[]; afterForm: string[]; branding: string[]; }; } /** * Combined dependency interface for all auth handlers. * Defined as a standalone interface (not multi-extends) to avoid TS2320 conflicts * where the same method name has different return types across handler deps. * The route handler builds this from the DI container services and config. */ interface AuthRouterDeps { secret: string; isProduction: boolean; accessTokenTTL: number; refreshTokenTTL: number; maxLoginAttempts: number; lockoutDurationSeconds: number; loginStallTimeMs: number; requireEmailVerification: boolean; /** * Spec §13.2 opt-in flag. When false (the spec default), email-conflict * registrations silent-success with a generic message. When true, the * handler returns 409 DUPLICATE so the user knows the email is in use * (trades enumeration risk for UX). */ revealRegistrationConflict: boolean; /** * Dev-only auto-login config (config.admin.devAutoLogin). Hard-blocked * at runtime when isProduction is true. When set, the session handler * issues a real session for the named user on the first /admin visit * if no valid session is present. See packages/nextly/src/auth/handlers/session.ts. */ devAutoLogin: false | { email: string; password?: string; }; allowedOrigins: string[]; /** * When true, client-IP resolution honors `X-Forwarded-For` (filtered * through `trustedProxyIps`). When false (default), proxy headers are * ignored. */ trustProxy: boolean; /** CIDR list of proxy IPs (from TRUSTED_PROXY_IPS). */ trustedProxyIps: string[]; /** * Per-IP rate limit on auth write endpoints. `requestsPerHour: 0` * disables the envelope. Single shared bucket across login / register * / forgot-password / reset-password per IP. */ authRateLimit: { requestsPerHour: number; windowMs: number; }; /** * Writer for security-sensitive auth events. Handlers call * `auditLog.write(...)` on failed CSRF, failed login, password change, * role mutation, and user delete. Writer is fail-safe; any DB error * logs a warning and the request continues. */ auditLog: AuditLogWriter; /** Ordered auth strategies (config opt-in first, built-in `password` last). */ authStrategies: AuthStrategy[]; /** Auth-flow hook registry (beforeLogin/afterAuthenticate/customizeClaims/…). */ authHooks: AuthHookRegistry; /** Challenge registry for multi-step auth (e.g. 2FA). */ challengeRegistry: ChallengeRegistry; /** Base plugin context handed to strategies/hooks/challenges. */ pluginCtx: PluginContext; /** Pending-auth token TTL in seconds (default 300). */ challengeTokenTTL: number; /** Max challenge-resolve attempts before failing (default 5). */ maxChallengeAttempts: number; /** Aggregated auth-page UI (D57), served pre-auth from GET /auth/ui. */ authUi: AuthUiMeta; findUserByEmail: (email: string) => Promise<{ id: string; email: string; name: string; image: string | null; passwordHash: string; emailVerified: Date | null; isActive: boolean; mustChangePassword: boolean | null; failedLoginAttempts: number; lockedUntil: Date | null; } | null>; findUserById: (userId: string) => Promise<{ id: string; email: string; name: string; image: string | null; isActive: boolean; mustChangePassword: boolean | null; } | null>; incrementFailedAttempts: (userId: string) => Promise; lockAccount: (userId: string, lockedUntil: Date) => Promise; resetFailedAttempts: (userId: string) => Promise; fetchRoleIds: (userId: string) => Promise; fetchCustomFields: (userId: string) => Promise>; storeRefreshToken: (record: { id: string; userId: string; tokenHash: string; userAgent: string | null; ipAddress: string | null; expiresAt: Date; }) => Promise; findRefreshTokenByHash: (tokenHash: string) => Promise<{ id: string; userId: string; expiresAt: Date; userAgent: string | null; ipAddress: string | null; } | null>; deleteRefreshToken: (id: string) => Promise; deleteRefreshTokenByHash: (tokenHash: string) => Promise; deleteAllRefreshTokensForUser: (userId: string) => Promise; getUserCount: () => Promise; createSuperAdmin: (data: { email: string; name: string; password: string; }) => Promise<{ id: string; email: string; name: string; }>; seedPermissions: () => Promise; registerUser: (data: { email: string; password: string; name: string; }) => Promise<{ id: string; email: string; name: string | null; }>; generatePasswordResetToken: (email: string, redirectPath?: string) => Promise<{ token?: string; }>; resetPasswordWithToken: (token: string, newPassword: string) => Promise<{ email: string; }>; acceptInvite: (token: string, newPassword: string) => Promise<{ userId: string; }>; setInitialPassword: (userId: string, newPassword: string) => Promise<{ userId: string; }>; changePassword: (userId: string, currentPassword: string, newPassword: string) => Promise<{ success: boolean; error?: string; }>; verifyEmail: (token: string) => Promise<{ success: boolean; error?: string; email?: string; }>; resendVerificationEmail: (email: string) => Promise<{ success: boolean; error?: string; }>; } /** * Route an auth request to the appropriate handler. * Returns null if the path doesn't match any auth route (caller handles 404). * * @param request - The incoming HTTP request * @param authPath - The path after the auth prefix (e.g., "login", "setup-status") * @param deps - Injected service dependencies */ declare function routeAuthRequest(request: Request, authPath: string, deps: AuthRouterDeps): Promise; export { authenticateApiKey, buildClaims, checkCollectionAccess, checkPermission, createErrorResponse, createJsonErrorResponse, generateRefreshToken, getSession, hasAllRoles, hasAnyRole, hasRole, hashPassword, hashRefreshToken, isErrorResponse, isSuperAdmin, requireAllRoles, requireAnyRole, requireAuth, requireRole, routeAuthRequest, secretToKey, signAccessToken, validatePasswordStrength, verifyAccessToken, verifyPassword }; export type { AuthContext, AuthRouterDeps, BuildClaimsInput, ErrorResponse, GetSessionResult, NextlyJwtPayload, PasswordStrengthResult, RefreshTokenRecord, SessionUser, VerifyResult };