export interface AuthUser { username: string; passwordHash: string; roles?: string[] | undefined; } export interface AuthSession { token: string; username: string; expiresAt: number; } interface UserAuthConfig { sessionTtlMs?: number | undefined; maxSessions?: number | undefined; /** Maximum number of per-account lock state entries. Excess entries are evicted by staleness. Default: 10000. */ maxAccountLocks?: number | undefined; users?: AuthUser[] | undefined; bootstrapFilePath: string; bootstrapCredentialPath: string; /** Scrypt cost params for hashing new passwords. Existing hashes carry their own params. Default: Node defaults. */ scryptParams?: ScryptParams | undefined; /** Injectable clock for testing. Defaults to Date.now. */ nowFn?: (() => number) | undefined; } /** * Scrypt cost parameters. N must be a power of 2. Increasing N multiplies * memory usage by 128*N*r bytes and CPU time proportionally. * Safe minimums: N>=16384, r>=8, p>=1. */ export interface ScryptParams { /** CPU/memory cost factor, must be a power of 2. Default: 16384. */ readonly N: number; /** Block size. Default: 8. */ readonly r: number; /** Parallelization factor. Default: 1. */ readonly p: number; } /** * Per-account login failure state for escalating lockout. * Independent of IP-based throttling. */ interface AccountLockState { failures: number; lockedUntil: number; } /** * Result of authenticate(), includes lock state so callers can * return Retry-After without leaking whether the username exists. * * The union is strict: `.user` is ONLY present (and non-undefined) on the * `ok:true` branch, so TypeScript catches any caller that forgets to check `.ok` * before accessing the user. `.usedBootstrapCredential` is set on the `ok:true` * branch so callers can decide whether to retire the bootstrap credential file. */ export type AuthenticateResult = { readonly ok: true; readonly user: AuthUser; readonly usedBootstrapCredential: boolean; readonly lockedUntilMs?: undefined; } | { readonly ok: false; readonly user?: undefined; readonly usedBootstrapCredential?: undefined; readonly lockedUntilMs?: number; }; export interface AuthUserRecord { readonly username: string; readonly roles: readonly string[]; } export interface AuthSessionRecord { readonly tokenFingerprint: string; readonly username: string; readonly expiresAt: number; } export interface LocalAuthSnapshot { readonly userStorePath: string; readonly bootstrapCredentialPath: string; readonly persisted: boolean; readonly bootstrapCredentialPresent: boolean; readonly userCount: number; readonly sessionCount: number; readonly users: readonly AuthUserRecord[]; readonly sessions: readonly AuthSessionRecord[]; } export declare class UserAuthManager { private users; private sessions; /** Per-username failure counters for account-level lockout (independent of IP throttling). */ private accountLocks; private sessionTtlMs; private readonly maxSessions; private readonly maxAccountLocks; private readonly userStorePath; private readonly bootstrapCredentialPath; private readonly persistUsers; private readonly scryptParams; /** Injectable clock, defaults to Date.now for production. */ private readonly nowFn; constructor(config: UserAuthConfig); static hashPassword(password: string, params?: ScryptParams): string; /** * Authenticate username/password with per-account lockout. * - Does NOT leak whether a username exists (same generic error path for unknown users). * - Records failures against the username bucket (not IP) with escalating backoff. * - Returns lockedUntilMs when the account is temporarily locked. * - Returns usedBootstrapCredential=true when the matched credential originated from * the bootstrap credential file, so callers can defer retirement until a non-bootstrap * login succeeds. */ authenticate(username: string, password: string): AuthenticateResult; /** * Record a login failure for the given username and apply escalating backoff. * * Thresholds are anchored ABOVE the per-IP login budget (default 5/min) so * the first account lock cannot fire before the IP limiter has already * throttled further attempts. This preserves the 401-then-429-by-IP contract * for in-budget attempts. * * 1-5 failures: no lock (IP budget exhaustion fires at attempt 6+) * 6-9 failures: 30-second lock * 10-19 failures: 5-minute lock * 20+ failures: 30-minute lock * * Note: failures also increment during an active lock window (when the account * is already locked and another attempt arrives). This means a lock can expire * with a failure count already in a higher tier, causing the next lock after * expiry to jump directly to that tier. This is intentional: repeated attempts * during a lock are themselves failures and escalate the penalty schedule. */ private _recordLoginFailure; /** * Evict account lock entries when the map is at capacity. * Prefers entries with expired locks + no recent failures (stale entries). * Falls back to the entry with the oldest/soonest-expired lock. * Preserves no-username-enumeration: eviction policy is time-based, not * user-existence-based. */ private _evictAccountLockIfNeeded; /** * Expose account lock state for testing. * Returns a defensive copy so callers cannot mutate internal state. * Use the injected nowFn (via UserAuthManager constructor) to advance time in tests. */ getAccountLockState(username: string): AccountLockState | undefined; getUser(username: string): AuthUserRecord | null; createSession(username: string): AuthSession; validateSession(token: string): AuthSession | null; revokeSession(token: string): boolean; revokeSessionsForUser(username: string): number; listUsers(): AuthUserRecord[]; listSessions(): AuthSessionRecord[]; addUser(username: string, password: string, roles?: readonly string[]): AuthUserRecord; deleteUser(username: string): boolean; rotatePassword(username: string, nextPassword: string): void; inspect(): LocalAuthSnapshot; clearBootstrapCredentialFile(): boolean; getBootstrapCredentialPath(): string; private pruneExpiredSessions; private persist; } export {}; //# sourceMappingURL=user-auth.d.ts.map