/** * @typedef {object} BackupCodesOptions * @property {number} [length=10] * Total number of characters per code (excluding group separators). * 10 chars from a 32-symbol alphabet = 50 bits of entropy per code — * comfortably beyond brute-force even with weak server-side hashing. * @property {number} [groups=2] * How many dash-separated groups to visually split the code into * for readability. Set to `1` to disable grouping. * @property {string} [alphabet] * Override the character set. The default (Crockford Base32) skips * the ambiguous `0/O/1/I/L` glyphs. */ /** * Generate a batch of one-time recovery codes. * * Codes are formatted like `ABCD-1234-EF` — dash-separated for easy * transcription from a paper printout, uppercase only, no ambiguous * characters. * * The caller is responsible for **hashing** the codes before storage * (bcrypt / argon2 / a strong HMAC keyed with a server secret — do * NOT store them raw). See the README for a worked example. * * @param {number} [n=10] How many codes to generate. * @param {BackupCodesOptions} [options] * @returns {string[]} */ declare function backupCodes(n?: number, options?: BackupCodesOptions$1): string[]; /** * Normalize a user-supplied code to the format `backupCodes` returns * so timing-safe compare works. Strips whitespace, uppercases, and * removes dashes. * * @param {string} input * @returns {string} */ declare function normalizeBackupCode(input: string): string; /** * Timing-safe compare between a user-supplied code and a candidate. * Both sides are normalized first (whitespace / dashes / case). * * Use this to check the user's input against every unused stored code * *without* short-circuiting on mismatch length, so an attacker can't * distinguish "wrong format" from "wrong value" from timing. * * @param {string} candidate What the user submitted. * @param {string} stored One of the codes you saved at enrollment. * @returns {boolean} */ declare function compareBackupCode(candidate: string, stored: string): boolean; /** * Timing-safe scan across a list of stored codes. Returns the index of * the first matching entry, or `null` when nothing matches. **Every** * entry is compared even after a match, so an attacker can't distinguish * "wrong code" from "wrong slot" through timing. * * const idx = verifyBackupCode(userInput, user.backupCodes) * if (idx === null) return res.status(401).end() * await db.markBackupCodeUsed(userId, idx) // single-use * * Store the codes **hashed** — bcrypt / argon2 / a strong keyed HMAC. * This helper hands off the compare to {@link compareBackupCode}, so * if your `storedList` is a list of plain strings the raw input is * matched against them directly; wire it into your hash routine * yourself when you're storing digests. * * @param {string} candidate User-supplied code (any case / spacing). * @param {string[]} storedList Your saved codes (in the order you * want indices reported). * @returns {number | null} Zero-based index of the match, or null. */ declare function verifyBackupCode(candidate: string, storedList: string[]): number | null; /** * Ready-made shapes for the most common backup-code conventions. * Spread into `backupCodes` options to pick one, override individual * fields to tweak. * * backupCodes(10, backupPresets.numeric) * backupCodes(10, { ...backupPresets.long, groups: 3 }) * * @type {Readonly>} */ declare const backupPresets: Readonly>; type BackupCodesOptions$1 = { /** * Total number of characters per code (excluding group separators). * 10 chars from a 32-symbol alphabet = 50 bits of entropy per code — * comfortably beyond brute-force even with weak server-side hashing. */ length?: number | undefined; /** * How many dash-separated groups to visually split the code into * for readability. Set to `1` to disable grouping. */ groups?: number | undefined; /** * Override the character set. The default (Crockford Base32) skips * the ambiguous `0/O/1/I/L` glyphs. */ alphabet?: string | undefined; }; /** * @typedef {'base32' | 'base32padded' | 'hex' | 'raw'} SecretEncoding */ /** * @typedef {object} SecretOptions * @property {number} [bytes=20] * Number of random bytes to generate. Default matches RFC 4226's * "recommended minimum" — 20 bytes = 160 bits, the size of a * SHA-1 output. Use 32 for SHA-256, 64 for SHA-512. * @property {SecretEncoding} [encoding='base32'] * How to encode the returned string. Google Authenticator and every * other TOTP app expects `base32` (RFC 4648, no padding). */ /** * Generate a cryptographically random OTP secret. * * The default (20 bytes, base32, no padding) is what every mainstream * TOTP app understands — the string is what you render in a QR / * paste on the enrollment screen. * * @param {SecretOptions} [options] * @returns {string} */ declare function generateSecret(options?: SecretOptions$1): string; /** * Decode any of the accepted secret encodings into a Buffer for HMAC * use. Accepts base32 (with or without padding, case-insensitive, * spaces stripped — matches how users paste), hex, and raw Buffers / * Uint8Arrays. Never trusts the caller — throws on malformed input. * * **Auto-detection ambiguity.** When `encoding` is omitted, a string is * probed as base32 first, then hex. Some strings are valid under BOTH * alphabets (e.g. `'abcdef'` — only `a-f`), and auto-detect will read * them as base32, producing the wrong key bytes for a caller who meant * hex. If you store secrets hex-encoded, pass `encoding: 'hex'` (or * `'base32'`) explicitly to remove the guesswork. * * @param {string | Buffer | Uint8Array} secret * @param {{ encoding?: 'base32' | 'hex' }} [options] * Force the input encoding instead of auto-detecting. Recommended * whenever the secret is not a base32 enrollment string. * @returns {Buffer} */ declare function decodeSecret(secret: string | Buffer | Uint8Array, options?: { encoding?: "base32" | "hex"; }): Buffer; type SecretEncoding$1 = "base32" | "base32padded" | "hex" | "raw"; type SecretOptions$1 = { /** * Number of random bytes to generate. Default matches RFC 4226's * "recommended minimum" — 20 bytes = 160 bits, the size of a * SHA-1 output. Use 32 for SHA-256, 64 for SHA-512. */ bytes?: number | undefined; /** * How to encode the returned string. Google Authenticator and every * other TOTP app expects `base32` (RFC 4648, no padding). */ encoding?: SecretEncoding$1 | undefined; }; /** * @typedef {'totp' | 'hotp'} ProvisioningType */ /** * @typedef {object} ProvisioningOptions * @property {string} label * Account identifier — typically the user's email or username. * Rendered in the Authenticator app's list. * @property {string} secret * Base32-encoded secret. Do NOT pass the raw Buffer. * @property {string} [issuer] * Your app name. Shows above the account label in the app UI and * is duplicated into the label per the Google Authenticator * Key URI Format recommendation. * @property {ProvisioningType} [type='totp'] * @property {6 | 7 | 8 | 9 | 10} [digits=6] * @property {number} [period=30] TOTP only. * @property {number} [counter] HOTP only — required for hotp type. * @property {'SHA1' | 'SHA256' | 'SHA512'} [algorithm='SHA1'] */ /** * Build an `otpauth://` provisioning URI — the string you render as a * QR code on the enrollment screen. Compatible with Google * Authenticator, Authy, 1Password, Bitwarden, Yubico Authenticator, * Aegis, and every other mainstream 2FA app. * * The format is documented at: * https://github.com/google/google-authenticator/wiki/Key-Uri-Format * * @param {ProvisioningOptions} options * @returns {string} */ declare function provisioningUri(options: ProvisioningOptions$1): string; /** * @typedef {object} ParsedProvisioning * @property {'totp' | 'hotp'} type * @property {string} label The account identifier — the * "Issuer:" prefix (if any) is stripped. * @property {string} secret Base32, unpadded — pass straight into `totp` / `hotp`. * @property {string | undefined} issuer * @property {6 | 7 | 8 | undefined} digits * @property {number | undefined} period TOTP only. * @property {number | undefined} counter HOTP only. * @property {'SHA1' | 'SHA224' | 'SHA256' | 'SHA384' | 'SHA512' | undefined} algorithm */ /** * Parse an `otpauth://` provisioning URI back into its parts — the * inverse of {@link provisioningUri}. Handy for migration flows where * you decode a QR the user scanned from another app. * * Returns `null` for anything that isn't a well-formed provisioning * URI. Never throws on malformed input. * * const info = parseProvisioningUri(qrPayload) * if (!info) return res.status(400).end('invalid QR') * await db.users.upsert(userId, { secret: info.secret }) * * @param {unknown} input * @returns {ParsedProvisioning | null} */ declare function parseProvisioningUri(input: unknown): ParsedProvisioning | null; type ProvisioningType$1 = "totp" | "hotp"; type ProvisioningOptions$1 = { /** * Account identifier — typically the user's email or username. * Rendered in the Authenticator app's list. */ label: string; /** * Base32-encoded secret. Do NOT pass the raw Buffer. */ secret: string; /** * Your app name. Shows above the account label in the app UI and * is duplicated into the label per the Google Authenticator * Key URI Format recommendation. */ issuer?: string | undefined; type?: ProvisioningType$1 | undefined; digits?: 6 | 7 | 8 | 9 | 10 | undefined; /** * TOTP only. */ period?: number | undefined; /** * HOTP only — required for hotp type. */ counter?: number | undefined; algorithm?: "SHA1" | "SHA256" | "SHA512" | undefined; }; type ParsedProvisioning = { type: "totp" | "hotp"; /** * The account identifier — the * "Issuer:" prefix (if any) is stripped. */ label: string; /** * Base32, unpadded — pass straight into `totp` / `hotp`. */ secret: string; issuer: string | undefined; digits: 6 | 7 | 8 | undefined; /** * TOTP only. */ period: number | undefined; /** * HOTP only. */ counter: number | undefined; algorithm: "SHA1" | "SHA224" | "SHA256" | "SHA384" | "SHA512" | undefined; }; /** * RFC 4226 HOTP — HMAC-based one-time password. * * @param {string | Buffer | Uint8Array} secret * @param {number} counter * @param {HotpOptions} [options] * @returns {string} Zero-padded N-digit code. */ declare function hotp(secret: string | Buffer | Uint8Array, counter: number, options?: HotpOptions$1): string; /** * Verify a counter-based OTP, returning the *matched counter* on * success (so the caller can advance their stored value) or `null` * when nothing in the drift window matched. * * The compare is timing-safe. Every candidate counter in the window * is checked even after a match — the constant-time property does * not extend across the loop, but the input is derived from a * pre-computed hash, not the user's guess, so this is safe. * * @param {unknown} code User-supplied candidate (string of digits). * @param {string | Buffer | Uint8Array} secret * @param {number} counter Current stored counter. * @param {HotpVerifyOptions} [options] * @returns {number | null} Matched counter (advance to `matched + 1`) * or `null` on no match. */ declare function verifyHotp(code: unknown, secret: string | Buffer | Uint8Array, counter: number, options?: HotpVerifyOptions$1): number | null; /** * @typedef {object} ResyncOptions * @property {number} [startCounter=0] * Where to start scanning. Almost always the last known-good counter * from your database. * @property {number} [maxLookAhead=500] * How far ahead of `startCounter` we scan. RFC 4226 §7.4 does not * specify a bound; production deployments use 100–1000 depending on * how often tokens might drift. * @property {6 | 7 | 8 | 9 | 10} [digits=6] * @property {OtpAlgorithm} [algorithm='SHA1'] */ /** * RFC 4226 §7.4 counter resynchronisation. Given two consecutive OTPs * the user typed off a hardware token that drifted, find the counter * value that makes both codes match — code #1 at some counter `N` * and code #2 at exactly `N+1`. Returns the *next* counter to store * (`N + 2`) on success, or `null` when the pair is not consistent. * * The scan is bounded by `maxLookAhead`; requests further off than * that fail rather than hanging. * * const nextCounter = resynchronize(secret, ['847362', '128394'], { * startCounter: userRow.hotpCounter, * }) * if (nextCounter === null) return res.status(400).end('resync failed') * await db.users.update(userId, { hotpCounter: nextCounter }) * * @param {string | Buffer | Uint8Array} secret * @param {[string, string]} codes Two consecutive user-entered codes. * @param {ResyncOptions} [options] * @returns {number | null} */ declare function resynchronize(secret: string | Buffer | Uint8Array, codes: [string, string], options?: ResyncOptions): number | null; type OtpAlgorithm$1 = "SHA1" | "SHA224" | "SHA256" | "SHA384" | "SHA512"; type HotpOptions$1 = { /** * Length of the emitted code. 6 is the universal default — Google * Authenticator, Microsoft Authenticator, Yubico, and every other * mainstream app agree on 6. Twilio Authy accepts 7. Aegis / 2FAS / * FreeOTP / 1Password / Bitwarden accept 6-10. Values above 10 * would emit non-uniform digits and are refused. */ digits?: 6 | 7 | 8 | 9 | 10 | undefined; /** * HMAC algorithm. **`SHA1` is the only value that works everywhere** — * Google Authenticator and Microsoft Authenticator only accept SHA-1. * `SHA256` and `SHA512` are supported by Twilio Authy (SHA-256 only), * Aegis, 2FAS, FreeOTP, 1Password, Bitwarden, and Yubico * Authenticator. Stick with SHA-1 for public-facing enrollment; * SHA-256/512 only when you control the client too. */ algorithm?: OtpAlgorithm$1 | undefined; }; type HotpVerifyOptions$1 = { digits?: 6 | 7 | 8 | 9 | 10 | undefined; algorithm?: OtpAlgorithm$1 | undefined; /** * Counter drift tolerance — accept codes in the range * `[counter, counter + window]`. HOTP always looks *ahead* (never * behind) because used counters can never be replayed. Set to 0 for * strict single-counter verify. */ window?: number | undefined; }; type ResyncOptions = { /** * Where to start scanning. Almost always the last known-good counter * from your database. */ startCounter?: number | undefined; /** * How far ahead of `startCounter` we scan. RFC 4226 §7.4 does not * specify a bound; production deployments use 100–1000 depending on * how often tokens might drift. */ maxLookAhead?: number | undefined; digits?: 6 | 7 | 8 | 9 | 10 | undefined; algorithm?: OtpAlgorithm$1 | undefined; }; /** * Current TOTP for the given secret. * * @param {string | Buffer | Uint8Array} secret * @param {TotpOptions} [options] * @returns {string} */ declare function totp(secret: string | Buffer | Uint8Array, options?: TotpOptions$1): string; /** * Seconds remaining before the current TOTP code rolls over. Handy for * the countdown ring most 2FA screens show. * * @param {number} [period=30] * @param {number} [timestamp] ms since epoch, default Date.now(). * @param {number} [t0=0] Epoch offset in seconds (RFC 6238 "T0"). * Pass the same value used at enrollment so * the countdown lines up with `totp`. * @returns {number} Whole seconds in `(0, period]`. */ declare function remainingSeconds(period?: number, timestamp?: number, t0?: number): number; /** * Verify a TOTP code with configurable drift tolerance. * * Returns `true` on success (with optional silent replay guard) or * `false` on any failure. Never throws for user-input problems — * a wrong code is a normal auth-outcome, not an error. * * @param {unknown} code * @param {string | Buffer | Uint8Array} secret * @param {TotpVerifyOptions} [options] * @returns {Promise} */ declare function verifyTotp(code: unknown, secret: string | Buffer | Uint8Array, options?: TotpVerifyOptions$1): Promise; type TotpOptions$1 = { digits?: 6 | 7 | 8 | 9 | 10 | undefined; algorithm?: OtpAlgorithm$1 | undefined; /** * Seconds per code. RFC 6238 default is 30. */ period?: number | undefined; /** * Override "now" in ms since epoch. * Useful for testing; production * code should leave it undefined. */ timestamp?: number | undefined; /** * Epoch offset in seconds — RFC 6238 * calls this "T0". Almost every * deployment leaves it at 0 (Unix * epoch); a handful of legacy SecurID * migrations use a custom start. */ t0?: number | undefined; }; type ReplayGuard$1 = { /** * Any store shaped like the `@exortek/security` rate-limit stores — * memory / Redis / custom all satisfy this duck type. The guard uses * the store's **atomic** `incr` (Redis `INCR`) as a compare-and-set so * two concurrent requests carrying the same code can't both pass — a * `get`-then-`set` pair would leave a TOCTOU window open. */ store: { incr: (key: string, ttlMs: number) => Promise<{ count: number; }>; }; /** * Caller-provided namespace (typically the user id). We compose the * real store key as `otp:used::` — the counter alone * would collide across users. */ key: string; }; type TotpVerifyOptions$1 = { digits?: 6 | 7 | 8 | 9 | 10 | undefined; algorithm?: OtpAlgorithm$1 | undefined; period?: number | undefined; /** * Skew tolerance in periods. `window: 1` accepts `T-1`, `T`, and * `T+1` — the same tolerance Google Authenticator applies internally. * `window: 0` is strict; `window: 2+` gets progressively less * defensive against brute-force. */ window?: number | undefined; /** * Override "now" (ms since epoch). */ timestamp?: number | undefined; /** * Epoch offset in seconds. Match the value * used at enrollment. */ t0?: number | undefined; /** * Opt-in replay defence: after a successful verify we mark that * specific counter as "used" for the remaining validity of the * window, so a stolen code can't be reused inside its slop period. * Requires an async store. */ replay?: ReplayGuard$1 | undefined; }; /** * @typedef {object} EnrollOptions * @property {string} label * Account identifier — usually the user's email or username. * Rendered in the Authenticator app. * @property {string} [issuer] * Your app name. Shows above the account label in the app UI. * @property {'totp' | 'hotp'} [type='totp'] * @property {6 | 7 | 8 | 9 | 10} [digits=6] * @property {number} [period=30] TOTP only. * @property {number} [counter=0] HOTP only — starting counter. * @property {import('./hotp.js').OtpAlgorithm} [algorithm='SHA1'] * @property {import('./secret.js').SecretOptions} [secretOptions] * Overrides for the underlying `generateSecret` call — bytes, encoding. * @property {number} [backupCodeCount=10] * Set to `0` to skip generating backup codes. * @property {import('./backup.js').BackupCodesOptions} [backupCodeOptions] * Passed straight to `backupCodes` — shape, alphabet, groups. */ /** * @typedef {object} EnrollmentBundle * @property {string} secret Base32-encoded secret to save. * @property {string} uri `otpauth://` URI — render as QR. * @property {string[]} backupCodes One-time recovery codes. Empty * array when `backupCodeCount: 0`. */ /** * One-call enrollment — mint a secret, build the provisioning URI, and * generate backup codes in a single step. * * const { secret, uri, backupCodes } = enroll({ * label: 'alice@example.com', * issuer: 'MyApp', * }) * // Save `secret` and hashed(backupCodes) server-side. * // Render `uri` as a QR on the enrollment page. * * The bundle is composed from `generateSecret` + `provisioningUri` + * `backupCodes` — everything each helper accepts is passed through so * you can still tune individual pieces (algorithm, period, backup code * format, etc.) without dropping down to primitives. * * @param {EnrollOptions} options * @returns {EnrollmentBundle} */ declare function enroll(options: EnrollOptions): EnrollmentBundle; type EnrollOptions = { /** * Account identifier — usually the user's email or username. * Rendered in the Authenticator app. */ label: string; /** * Your app name. Shows above the account label in the app UI. */ issuer?: string | undefined; type?: "totp" | "hotp" | undefined; digits?: 6 | 7 | 8 | 9 | 10 | undefined; /** * TOTP only. */ period?: number | undefined; /** * HOTP only — starting counter. */ counter?: number | undefined; algorithm?: OtpAlgorithm$1 | undefined; /** * Overrides for the underlying `generateSecret` call — bytes, encoding. */ secretOptions?: SecretOptions$1 | undefined; /** * Set to `0` to skip generating backup codes. */ backupCodeCount?: number | undefined; /** * Passed straight to `backupCodes` — shape, alphabet, groups. */ backupCodeOptions?: BackupCodesOptions$1 | undefined; }; type EnrollmentBundle = { /** * Base32-encoded secret to save. */ secret: string; /** * `otpauth://` URI — render as QR. */ uri: string; /** * One-time recovery codes. Empty * array when `backupCodeCount: 0`. */ backupCodes: string[]; }; /** * Shared base error class — the single error structure behind every * `@exortek/*` package's `errors.js`. * * Every package keeps its own class identity with a one-liner subclass; * codes stay per-package frozen maps, status mapping is declared as a * static field: * * import { BaseError } from '@exortek/shared/errors'; * * export const ErrorCode = Object.freeze({ * INVALID_ARGUMENT: 'INVALID_ARGUMENT', * INVALID_TOKEN: 'INVALID_TOKEN', * }); * * export class JwtError extends BaseError { * static statuses = { INVALID_ARGUMENT: 400, INVALID_TOKEN: 401 }; * static defaultStatus = 500; * } * * Instances carry a stable machine-readable `code` (branch on this, * never on the message), an optional HTTP `status`, an optional * `details` object, and the standard `cause` chain. */ declare class BaseError extends Error { /** * Optional `code → HTTP status` map declared on the subclass. When * absent the instance carries no `status` at all — for HTTP-agnostic * packages like `@exortek/crypto`. * * @type {Record | undefined} */ static statuses: Record | undefined; /** * Fallback status for codes missing from `statuses`. * * @type {number} */ static defaultStatus: number; /** * @param {string} code Stable machine-readable code; branch on this. * @param {string} message Human-readable diagnostic. Free-form; may * change across versions. * @param {{ cause?: unknown, status?: number, details?: Record }} [options] */ constructor(code: string, message: string, options?: { cause?: unknown; status?: number; details?: Record; }); /** @type {string} */ code: string; /** @type {number | undefined} */ status: number | undefined; /** @type {Record | undefined} */ details: Record | undefined; } declare const ErrorCode: Readonly<{ INVALID_ARGUMENT: "INVALID_ARGUMENT"; INVALID_SECRET: "INVALID_SECRET"; UNSUPPORTED_ALGORITHM: "UNSUPPORTED_ALGORITHM"; }>; /** * Every recoverable failure raised by this package. Carries a stable * `code` (from {@link ErrorCode}) and a `status` — the HTTP response * status a middleware layer would use when translating the error. */ declare class OtpError extends BaseError { static statuses: { INVALID_ARGUMENT: number; UNSUPPORTED_ALGORITHM: number; INVALID_SECRET: number; }; } type OtpAlgorithm = OtpAlgorithm$1; type HotpOptions = HotpOptions$1; type HotpVerifyOptions = HotpVerifyOptions$1; type TotpOptions = TotpOptions$1; type TotpVerifyOptions = TotpVerifyOptions$1; type ReplayGuard = ReplayGuard$1; type ProvisioningOptions = ProvisioningOptions$1; type ProvisioningType = ProvisioningType$1; type SecretOptions = SecretOptions$1; type SecretEncoding = SecretEncoding$1; type BackupCodesOptions = BackupCodesOptions$1; export { ErrorCode, OtpError, backupCodes, backupPresets, compareBackupCode, decodeSecret, enroll, generateSecret, hotp, normalizeBackupCode, parseProvisioningUri, provisioningUri, remainingSeconds, resynchronize, totp, verifyBackupCode, verifyHotp, verifyTotp }; export type { BackupCodesOptions, HotpOptions, HotpVerifyOptions, OtpAlgorithm, ProvisioningOptions, ProvisioningType, ReplayGuard, SecretEncoding, SecretOptions, TotpOptions, TotpVerifyOptions };