/** * Enrollment flow helpers — a tiny, pure state machine for registering a new * factor: `started → challenged → confirmed` (or `failed`). It produces the * {@link EnrollmentRecord} a server should persist, without prescribing HOW you * store it, and stays adapter-based: YOU generate the challenge material (a TOTP * secret, a WebAuthn creation challenge, recovery-code hashes) and YOU verify the * user's proof — these helpers only shepherd the steps and the shape of the data. */ /** The steps a factor enrollment moves through. */ type EnrollmentStatus = "started" | "challenged" | "confirmed" | "failed"; /** A snapshot of an in-progress (or finished) enrollment. Serializable. */ interface EnrollmentState { factor: Factor; status: EnrollmentStatus; /** Challenge material shown to the user (e.g. a TOTP secret / QR, WebAuthn options). Opaque to this library. */ challenge?: C; /** ms epoch the enrollment began. */ startedAt: number; /** ms epoch of the last transition. */ updatedAt: number; } /** The data a server should persist once a factor is confirmed. Storage is yours. */ interface EnrollmentRecord { factorId: string; type: FactorType; /** Secret/credential material to persist (confirmed TOTP secret, credential id, recovery hashes…). */ secret?: unknown; /** ms epoch the factor became active. */ enrolledAt: number; } interface EnrollmentClockOptions { /** Injectable clock (ms epoch). Defaults to `Date.now`. */ now?: () => number; } interface CompleteEnrollmentOptions extends EnrollmentClockOptions { /** Material to persist in the record. Defaults to the state's `challenge`. */ secret?: unknown; } /** Start enrolling a factor. Returns a `started` state. */ declare function beginEnrollment(factor: Factor, opts?: EnrollmentClockOptions): EnrollmentState; /** * Attach the challenge material you generated (e.g. a fresh TOTP secret) and move * the enrollment to `challenged`, ready for the user to prove possession. */ declare function challengeEnrollment(state: EnrollmentState, challenge: C, opts?: EnrollmentClockOptions): EnrollmentState; /** * Confirm (or reject) a challenged enrollment. Pass the boolean result of * verifying the user's proof against the challenge (you own that check). On * success you get a `confirmed` state and the {@link EnrollmentRecord} to store; * on failure a `failed` state and no record. */ declare function completeEnrollment(state: EnrollmentState, verified: boolean, opts?: CompleteEnrollmentOptions): { state: EnrollmentState; record?: EnrollmentRecord; }; /** * Step-up authentication — decide whether a sensitive action needs a FRESH * factor challenge on top of the user's current session. Pure and injectable: * it reads the session's verified factors (and their timestamps) and compares * them against a per-action {@link StepUpPolicy}. It never mutates the session. */ /** Why a step-up is being required. */ type StepUpReason = "insufficient-aal" | "insufficient-factors" | "missing-type" | "stale"; /** Requirements a sensitive action places on the CURRENT session. */ interface StepUpPolicy { /** Minimum assurance level the action needs (session below ⇒ step-up). */ minAAL?: AalLevel; /** Minimum distinct verified factors the action needs. */ minFactors?: number; /** Factor types that must have been verified for this action. */ requireTypes?: FactorType[]; /** Max age (ms) of the most recent factor verification; older ⇒ step-up. */ maxAgeMs?: number; /** Injectable clock (ms epoch). Defaults to `Date.now`. */ now?: () => number; } /** The step-up decision for an action. */ interface StepUpDecision { /** True when a fresh factor challenge is needed before proceeding. */ required: boolean; /** All unmet requirements (empty when not required). */ reasons: StepUpReason[]; /** The session's current assurance level. */ currentAAL: AalLevel; /** ms since the most recent verification (undefined when nothing verified). */ ageMs?: number; } /** * Decide whether `session` must step up to satisfy `policy`. Returns * `{ required, reasons, currentAAL, ageMs }` — if `required` is true, prompt for * a fresh factor, then `markVerified` it on the session and re-check. */ declare function requireStepUp(session: MfaSession, policy?: StepUpPolicy): StepUpDecision; /** * Lockout / rate-limit policy — a PURE evaluator for failed-attempt lockout with * optional exponential backoff and a rolling window. No timers, no storage: you * keep the small {@link LockoutStateData} per identity (user, IP, device) and * feed it a clock. `evaluateLockout` reads; `recordFailure` / `recordSuccess` * return the next state to persist. */ /** Tunables for the lockout policy. All optional. */ interface LockoutPolicy { /** Failed attempts allowed before lockout. Default 5. */ maxAttempts?: number; /** Base cooldown (ms) once locked. Default 30_000. */ cooldownMs?: number; /** Multiplier applied per prior lockout (exponential backoff). Default 1 (flat). */ backoffFactor?: number; /** Upper bound (ms) for a backed-off cooldown. */ maxCooldownMs?: number; /** * Rolling window (ms): a run of failures is forgotten if this long has passed * since the last one (while not currently locked). Unset ⇒ failures never age. */ windowMs?: number; } /** The per-identity counter you persist between attempts. */ interface LockoutStateData { /** Consecutive failed attempts in the current run. */ fails: number; /** ms epoch of the most recent failure. */ lastFailAt?: number; /** How many times this identity has crossed into lockout (drives backoff). */ lockCount?: number; } /** The verdict for an attempt. */ interface LockoutDecision { /** True when an attempt is permitted right now. */ allowed: boolean; /** ms until the caller may retry (0 when allowed). */ retryAfter: number; /** Attempts left before lockout (0 when locked). */ remaining: number; } /** A fresh, clean lockout state. */ declare function initialLockoutState(): LockoutStateData; /** * Evaluate whether an attempt is allowed for the given state — pure, given `now` * (ms epoch, defaults to `Date.now()`). Returns `{ allowed, retryAfter, remaining }`. */ declare function evaluateLockout(state: LockoutStateData, policy?: LockoutPolicy, now?: number): LockoutDecision; /** * Record one failed attempt and return the next state to persist. Rolls a new * run after a served cooldown (keeping `lockCount` for backoff) or after the * rolling window elapses, and bumps `lockCount` on the crossing into lockout. */ declare function recordFailure(state: LockoutStateData, policy?: LockoutPolicy, now?: number): LockoutStateData; /** Record a successful attempt — clears the run and any backoff. */ declare function recordSuccess(): LockoutStateData; /** * Trusted-device tokens — issue and verify a signed "remember this device" token * so a known device can skip MFA for a window. Uses Web Crypto HMAC-SHA256 with * an INJECTABLE secret (never a hard-coded key), a self-describing base64url * payload, and an expiry baked into the signed claims. Zero deps · isomorphic * (Node 20+ / edge / browser — anywhere `globalThis.crypto.subtle` exists). */ /** Claims carried by a trusted-device token. */ interface TrustedDeviceClaims { /** Subject — the user this device is trusted for. */ sub: string; /** Stable device identifier (a fingerprint or random id you persist). */ device: string; /** Issued-at (ms epoch). */ iat: number; /** Expiry (ms epoch). */ exp: number; /** Optional scope, e.g. which factor types this trust stands in for. */ scope?: string[]; } type Secret = string | Uint8Array | ArrayBuffer; interface IssueTrustedDeviceOptions { /** The user this device is trusted for. */ sub: string; /** Stable device identifier. */ device: string; /** HMAC secret — inject it; never hard-code. */ secret: Secret; /** Lifetime of the token in ms. */ ttlMs: number; /** Optional scope claim. */ scope?: string[]; /** Injectable clock (ms epoch). Defaults to `Date.now`. */ now?: () => number; } interface VerifyTrustedDeviceOptions { /** Injectable clock (ms epoch). Defaults to `Date.now`. */ now?: () => number; /** If set, the token's `sub` must equal this. */ sub?: string; /** If set, the token's `device` must equal this. */ device?: string; } /** * Issue a signed trusted-device token: `base64url(claims).base64url(HMAC)`. * The device may present it to skip MFA until `iat + ttlMs`. */ declare function issueTrustedDevice(opts: IssueTrustedDeviceOptions): Promise; /** * Verify a trusted-device token against `secret`. Returns the {@link * TrustedDeviceClaims} when the signature is valid, the token is unexpired, and * any `sub`/`device` expectations match — otherwise `null` (never throws). */ declare function verifyTrustedDevice(token: string, secret: Secret, opts?: VerifyTrustedDeviceOptions): Promise; /** * Recovery-code factor — a self-contained (no `@lacspace/otp` dependency) * recovery/backup code factor with the generate → verify → consume lifecycle the * package models factors around. Codes are single-use one-time secrets: show the * plaintext ONCE at generation, persist only the salted SHA-256 hashes, and * remove a hash when its code is consumed. Recovery codes act as a `possession` * factor. Zero deps · isomorphic (Web Crypto). */ /** Recovery codes stand in as a possession factor. */ declare const recoveryCodeFactorType: FactorType; /** Output of {@link generateRecoveryCodes}. */ interface RecoveryCodeSet { /** Plaintext codes — display ONCE, then discard. */ codes: string[]; /** Salted hashes to persist (never store the plaintext). */ hashes: string[]; } interface GenerateRecoveryCodesOptions { /** How many codes to mint. Default 10. */ count?: number; /** Characters per code (excluding group separators). Default 10. */ length?: number; /** Group size for readability, e.g. 5 ⇒ "abcde-fghij". 0 disables grouping. Default 5. */ groupSize?: number; /** Alphabet to draw from. Default Crockford-ish base32 without ambiguous chars. */ alphabet?: string; /** Injectable RNG returning `n` random bytes. Defaults to Web Crypto. */ random?: (n: number) => Uint8Array; } /** Result of {@link consumeRecoveryCode}. */ interface ConsumeRecoveryResult { /** True when the code matched and was consumed. */ consumed: boolean; /** Index of the matched hash (or -1). */ index: number; /** Remaining hashes to persist (matched one removed). */ remaining: string[]; } /** * Generate a fresh set of recovery codes. Returns the plaintext `codes` (show * once) and the salted `hashes` to persist. */ declare function generateRecoveryCodes(opts?: GenerateRecoveryCodesOptions): Promise; /** * Verify a recovery code against stored hashes. Returns the matched index (mark * or remove it — codes are single-use) or -1 if none match. */ declare function verifyRecoveryCode(code: string, hashes: string[]): Promise; /** * Verify AND consume a recovery code in one step. Returns whether it matched, the * matched index, and the `remaining` hashes (matched one removed) to persist. */ declare function consumeRecoveryCode(code: string, hashes: string[]): Promise; /** * @lacspace/mfa * Orchestrate multi-factor auth — combine password, TOTP and passkeys into * 2FA / 3FA step-up flows with NIST-style assurance levels. * * This is the conductor, not the crypto: pair it with @lacspace/password, * @lacspace/otp and @lacspace/webauthn to verify each factor. * * Zero dependencies (bar @lacspace/otp) · isomorphic · fully typed. */ /** The three NIST factor categories. */ type FactorType = "knowledge" | "possession" | "inherence"; interface Factor { /** Your identifier for this factor, e.g. "password", "totp", "passkey-1". */ id: string; type: FactorType; } /** Authenticator Assurance Levels (NIST SP 800-63B, simplified). */ declare const AAL: { readonly AAL1: 1; readonly AAL2: 2; readonly AAL3: 3; }; type AalLevel = 1 | 2 | 3; /** * Assurance level for a set of verified factor types: * - AAL1: a single factor (usually knowledge) * - AAL2: two distinct factor types * - AAL3: two+ types including a hardware-bound inherence/possession (e.g. passkey) */ declare function assuranceLevel(types: FactorType[]): AalLevel; interface MfaPolicy { /** Minimum number of distinct factors that must be verified. Default 2. */ minFactors?: number; /** Minimum assurance level required. */ minAAL?: AalLevel; /** Factor types that must all be present. */ requiredTypes?: FactorType[]; } interface MfaConfig { /** Factors the user has registered. */ factors: Factor[]; policy?: MfaPolicy; /** If set, a verified factor "expires" after this many ms (step-up windows). */ factorTtlMs?: number; } interface MfaState { satisfied: boolean; verifiedFactors: string[]; aal: AalLevel; /** How many more distinct factors are needed. */ needFactors: number; /** Factor types still required by the policy. */ needTypes: FactorType[]; } /** Serialized form of a session — persist across requests (signed cookie / store). */ interface MfaSessionData { verified: { id: string; type: FactorType; at: number; }[]; } /** A step-up session tracking which factors a user has cleared. */ declare class MfaSession { private config; private verified; private readonly policy; private readonly ttl?; constructor(config: MfaConfig); private now; /** Live entries, dropping any that have passed the factor TTL. */ private live; /** Mark a registered factor as verified. Returns the updated state. */ markVerified(factorId: string): MfaState; get verifiedFactors(): string[]; get aal(): AalLevel; state(): MfaState; get satisfied(): boolean; /** Serialize the verified factors (persist this between requests). */ toJSON(): MfaSessionData; /** Restore a session from {@link toJSON} output plus the user's factor config. */ static fromJSON(config: MfaConfig, data: MfaSessionData): MfaSession; } declare function mfaSession(config: MfaConfig): MfaSession; /** Verify a TOTP factor (thin wrapper over @lacspace/otp). Returns true if valid. */ declare function verifyTotpFactor(token: string, secret: string, opts?: { window?: number; digits?: number; period?: number; }): Promise; /** Verify a knowledge factor: a password against its stored hash (@lacspace/password). */ declare function verifyPasswordFactor(password: string, storedHash: string): Promise; /** * Verify a recovery/backup code against stored hashes (@lacspace/otp). * Returns the matched index (remove/mark it used) or -1. Treat as a possession factor. */ declare function verifyBackupCodeFactor(code: string, hashes: string[]): Promise; /** * Verify a passkey factor. Pass the boolean result of your * `@lacspace/webauthn` `verifyAuthentication(...)` call — this normalizes it as * an `inherence`/`possession` factor for the session. Kept decoupled so you own * the WebAuthn ceremony (challenge store, credential lookup). */ declare function verifyPasskeyFactor(webauthnVerified: boolean): boolean; export { AAL, type AalLevel, type CompleteEnrollmentOptions, type ConsumeRecoveryResult, type EnrollmentClockOptions, type EnrollmentRecord, type EnrollmentState, type EnrollmentStatus, type Factor, type FactorType, type GenerateRecoveryCodesOptions, type IssueTrustedDeviceOptions, type LockoutDecision, type LockoutPolicy, type LockoutStateData, type MfaConfig, type MfaPolicy, MfaSession, type MfaSessionData, type MfaState, type RecoveryCodeSet, type StepUpDecision, type StepUpPolicy, type StepUpReason, type TrustedDeviceClaims, type VerifyTrustedDeviceOptions, assuranceLevel, beginEnrollment, challengeEnrollment, completeEnrollment, consumeRecoveryCode, evaluateLockout, generateRecoveryCodes, initialLockoutState, issueTrustedDevice, mfaSession, recordFailure, recordSuccess, recoveryCodeFactorType, requireStepUp, verifyBackupCodeFactor, verifyPasskeyFactor, verifyPasswordFactor, verifyRecoveryCode, verifyTotpFactor, verifyTrustedDevice };