UNPKG

otplib

Version:

TypeScript-first library for TOTP and HOTP with multi-runtime and plugin support

1 lines 254 kB
{"version":3,"sources":["../src/index.ts","../../core/src/errors.ts","../../core/src/utils.ts","../../core/src/crypto-context.ts","../../uri/src/generate.ts","../../hotp/src/class.ts","../../hotp/src/index.ts","../../totp/src/class.ts","../../totp/src/index.ts","../../../node_modules/.pnpm/@scure+base@2.0.0/node_modules/@scure/base/index.ts","../../plugin-base32-scure/src/index.ts","../../../node_modules/.pnpm/@noble+hashes@2.0.1/node_modules/@noble/hashes/src/utils.ts","../../../node_modules/.pnpm/@noble+hashes@2.0.1/node_modules/@noble/hashes/src/hmac.ts","../../../node_modules/.pnpm/@noble+hashes@2.0.1/node_modules/@noble/hashes/src/_md.ts","../../../node_modules/.pnpm/@noble+hashes@2.0.1/node_modules/@noble/hashes/src/legacy.ts","../../../node_modules/.pnpm/@noble+hashes@2.0.1/node_modules/@noble/hashes/src/_u64.ts","../../../node_modules/.pnpm/@noble+hashes@2.0.1/node_modules/@noble/hashes/src/sha2.ts","../../plugin-crypto-noble/src/index.ts","../src/defaults.ts","../src/functional.ts","../src/class.ts"],"sourcesContent":["export type {\n OTPAuthOptions,\n TOTPOptions,\n OTPGenerateOptions as OTPFunctionalOptions,\n OTPVerifyOptions as OTPVerifyFunctionalOptions,\n} from \"./types.js\";\n\nexport {\n generateSecret,\n generateURI,\n generate,\n generateSync,\n verify,\n verifySync,\n type OTPStrategy,\n} from \"./functional.js\";\n\nexport {\n OTP,\n type OTPClassOptions,\n type OTPGenerateOptions,\n type OTPVerifyOptions,\n type OTPURIGenerateOptions,\n} from \"./class.js\";\n\nexport type {\n Base32Plugin,\n CryptoPlugin,\n HashAlgorithm,\n OTPResult,\n OTPGuardrails,\n OTPGuardrailsConfig,\n} from \"@otplib/core\";\nexport type { VerifyResult } from \"@otplib/totp\";\n\nexport { HOTP } from \"@otplib/hotp\";\nexport { TOTP } from \"@otplib/totp\";\n\nexport { createGuardrails, stringToBytes, wrapResult, wrapResultAsync } from \"@otplib/core\";\n\n// Default Plugins\nexport { NobleCryptoPlugin } from \"@otplib/plugin-crypto-noble\";\nexport { ScureBase32Plugin } from \"@otplib/plugin-base32-scure\";\n","/**\n * Options for OTPError construction\n */\nexport type OTPErrorOptions = {\n /**\n * The underlying error that caused this error.\n * Useful for error chaining and debugging.\n */\n cause?: unknown;\n};\n\n/**\n * Base error class for all otplib errors\n *\n * Supports ES2022 error chaining via the `cause` property.\n *\n * @example\n * ```typescript\n * try {\n * // ... operation that throws\n * } catch (error) {\n * throw new OTPError('Operation failed', { cause: error });\n * }\n * ```\n */\nexport class OTPError extends Error {\n constructor(message: string, options?: OTPErrorOptions) {\n super(message, options);\n this.name = \"OTPError\";\n }\n}\n\n/**\n * Error thrown when secret validation fails\n */\nexport class SecretError extends OTPError {\n constructor(message: string) {\n super(message);\n this.name = \"SecretError\";\n }\n}\n\n/**\n * Error thrown when secret is too short (< 128 bits)\n */\nexport class SecretTooShortError extends SecretError {\n constructor(minBytes: number, actualBytes: number) {\n super(\n `Secret must be at least ${minBytes} bytes (${minBytes * 8} bits), got ${actualBytes} bytes`,\n );\n this.name = \"SecretTooShortError\";\n }\n}\n\n/**\n * Error thrown when secret is unreasonably large (> 64 bytes)\n */\nexport class SecretTooLongError extends SecretError {\n constructor(maxBytes: number, actualBytes: number) {\n super(`Secret must not exceed ${maxBytes} bytes, got ${actualBytes} bytes`);\n this.name = \"SecretTooLongError\";\n }\n}\n\n/**\n * Error thrown when counter is invalid\n */\nexport class CounterError extends OTPError {\n constructor(message: string) {\n super(message);\n this.name = \"CounterError\";\n }\n}\n\n/**\n * Error thrown when counter is negative\n */\nexport class CounterNegativeError extends CounterError {\n constructor() {\n super(\"Counter must be non-negative\");\n this.name = \"CounterNegativeError\";\n }\n}\n\n/**\n * Error thrown when counter exceeds maximum value (2^53 - 1 for safe integer)\n */\nexport class CounterOverflowError extends CounterError {\n constructor() {\n super(\"Counter exceeds maximum safe integer value\");\n this.name = \"CounterOverflowError\";\n }\n}\n\n/**\n * Error thrown when counter is not an integer\n */\nexport class CounterNotIntegerError extends CounterError {\n constructor() {\n super(\"Counter must be a finite integer\");\n this.name = \"CounterNotIntegerError\";\n }\n}\n\n/**\n * Error thrown when time is invalid\n */\nexport class TimeError extends OTPError {\n constructor(message: string) {\n super(message);\n this.name = \"TimeError\";\n }\n}\n\n/**\n * Error thrown when time is negative\n */\nexport class TimeNegativeError extends TimeError {\n constructor() {\n super(\"Time must be non-negative\");\n this.name = \"TimeNegativeError\";\n }\n}\n\n/**\n * Error thrown when time is not a finite number\n */\nexport class TimeNotFiniteError extends TimeError {\n constructor() {\n super(\"Time must be a finite number\");\n this.name = \"TimeNotFiniteError\";\n }\n}\n\n/**\n * Error thrown when period is invalid\n */\nexport class PeriodError extends OTPError {\n constructor(message: string) {\n super(message);\n this.name = \"PeriodError\";\n }\n}\n\n/**\n * Error thrown when period is too small\n */\nexport class PeriodTooSmallError extends PeriodError {\n constructor(minPeriod: number) {\n super(`Period must be at least ${minPeriod} second(s)`);\n this.name = \"PeriodTooSmallError\";\n }\n}\n\n/**\n * Error thrown when period is too large\n */\nexport class PeriodTooLargeError extends PeriodError {\n constructor(maxPeriod: number) {\n super(`Period must not exceed ${maxPeriod} seconds`);\n this.name = \"PeriodTooLargeError\";\n }\n}\n\n/**\n * Error thrown when digits value is invalid\n */\nexport class DigitsError extends OTPError {\n constructor(message: string) {\n super(message);\n this.name = \"DigitsError\";\n }\n}\n\n/**\n * Error thrown when hash algorithm is invalid\n */\nexport class AlgorithmError extends OTPError {\n constructor(message: string) {\n super(message);\n this.name = \"AlgorithmError\";\n }\n}\n\n/**\n * Error thrown when token is invalid\n */\nexport class TokenError extends OTPError {\n constructor(message: string) {\n super(message);\n this.name = \"TokenError\";\n }\n}\n\n/**\n * Error thrown when token has incorrect length\n */\nexport class TokenLengthError extends TokenError {\n constructor(expected: number, actual: number) {\n super(`Token must be ${expected} digits, got ${actual}`);\n this.name = \"TokenLengthError\";\n }\n}\n\n/**\n * Error thrown when token contains non-digit characters\n */\nexport class TokenFormatError extends TokenError {\n constructor() {\n super(\"Token must contain only digits\");\n this.name = \"TokenFormatError\";\n }\n}\n\n/**\n * Error thrown when crypto operation fails\n */\nexport class CryptoError extends OTPError {\n constructor(message: string, options?: OTPErrorOptions) {\n super(message, options);\n this.name = \"CryptoError\";\n }\n}\n\n/**\n * Error thrown when HMAC computation fails\n *\n * The original error from the crypto plugin is available via `cause`.\n *\n * @example\n * ```typescript\n * try {\n * await cryptoContext.hmac('sha1', key, data);\n * } catch (error) {\n * if (error instanceof HMACError) {\n * console.log('HMAC failed:', error.message);\n * console.log('Original error:', error.cause);\n * }\n * }\n * ```\n */\nexport class HMACError extends CryptoError {\n constructor(message: string, options?: OTPErrorOptions) {\n super(`HMAC computation failed: ${message}`, options);\n this.name = \"HMACError\";\n }\n}\n\n/**\n * Error thrown when random byte generation fails\n *\n * The original error from the crypto plugin is available via `cause`.\n */\nexport class RandomBytesError extends CryptoError {\n constructor(message: string, options?: OTPErrorOptions) {\n super(`Random byte generation failed: ${message}`, options);\n this.name = \"RandomBytesError\";\n }\n}\n\n/**\n * Error thrown when Base32 operation fails\n */\nexport class Base32Error extends OTPError {\n constructor(message: string, options?: OTPErrorOptions) {\n super(message, options);\n this.name = \"Base32Error\";\n }\n}\n\n/**\n * Error thrown when Base32 encoding fails\n *\n * The original error from the Base32 plugin is available via `cause`.\n *\n * @example\n * ```typescript\n * try {\n * base32Context.encode(data);\n * } catch (error) {\n * if (error instanceof Base32EncodeError) {\n * console.log('Encoding failed:', error.message);\n * console.log('Original error:', error.cause);\n * }\n * }\n * ```\n */\nexport class Base32EncodeError extends Base32Error {\n constructor(message: string, options?: OTPErrorOptions) {\n super(`Base32 encoding failed: ${message}`, options);\n this.name = \"Base32EncodeError\";\n }\n}\n\n/**\n * Error thrown when Base32 decoding fails\n *\n * The original error from the Base32 plugin is available via `cause`.\n *\n * @example\n * ```typescript\n * try {\n * base32Context.decode(invalidString);\n * } catch (error) {\n * if (error instanceof Base32DecodeError) {\n * console.log('Decoding failed:', error.message);\n * console.log('Original error:', error.cause);\n * }\n * }\n * ```\n */\nexport class Base32DecodeError extends Base32Error {\n constructor(message: string, options?: OTPErrorOptions) {\n super(`Base32 decoding failed: ${message}`, options);\n this.name = \"Base32DecodeError\";\n }\n}\n\n/**\n * Error thrown when counter tolerance is invalid\n */\nexport class CounterToleranceError extends OTPError {\n constructor(message: string) {\n super(message);\n this.name = \"CounterToleranceError\";\n }\n}\n\n/**\n * Error thrown when counter tolerance is too large\n */\nexport class CounterToleranceTooLargeError extends CounterToleranceError {\n constructor(maxWindow: number, totalChecks: number) {\n super(\n `Counter tolerance validation failed: total checks (${totalChecks}) exceeds MAX_WINDOW (${maxWindow})`,\n );\n this.name = \"CounterToleranceTooLargeError\";\n }\n}\n\n/**\n * Error thrown when counter tolerance contains negative values\n */\nexport class CounterToleranceNegativeError extends CounterToleranceError {\n constructor() {\n super(\"Counter tolerance cannot contain negative values\");\n this.name = \"CounterToleranceNegativeError\";\n }\n}\n\n/**\n * Error thrown when epoch tolerance is invalid\n */\nexport class EpochToleranceError extends OTPError {\n constructor(message: string) {\n super(message);\n this.name = \"EpochToleranceError\";\n }\n}\n\n/**\n * Error thrown when epoch tolerance contains negative values\n */\nexport class EpochToleranceNegativeError extends EpochToleranceError {\n constructor() {\n super(\"Epoch tolerance cannot contain negative values\");\n this.name = \"EpochToleranceNegativeError\";\n }\n}\n\n/**\n * Error thrown when epoch tolerance is too large\n */\nexport class EpochToleranceTooLargeError extends EpochToleranceError {\n constructor(maxTolerance: number, actualValue: number) {\n super(\n `Epoch tolerance must not exceed ${maxTolerance} seconds, got ${actualValue}. ` +\n `Large tolerances can cause performance issues.`,\n );\n this.name = \"EpochToleranceTooLargeError\";\n }\n}\n\n/**\n * Error thrown when a required plugin is missing\n */\nexport class PluginError extends OTPError {\n constructor(message: string) {\n super(message);\n this.name = \"PluginError\";\n }\n}\n\n/**\n * Error thrown when crypto plugin is not configured\n */\nexport class CryptoPluginMissingError extends PluginError {\n constructor() {\n super(\"Crypto plugin is required.\");\n this.name = \"CryptoPluginMissingError\";\n }\n}\n\n/**\n * Error thrown when Base32 plugin is not configured\n */\nexport class Base32PluginMissingError extends PluginError {\n constructor() {\n super(\"Base32 plugin is required.\");\n this.name = \"Base32PluginMissingError\";\n }\n}\n\n/**\n * Error thrown when required configuration is missing\n */\nexport class ConfigurationError extends OTPError {\n constructor(message: string) {\n super(message);\n this.name = \"ConfigurationError\";\n }\n}\n\n/**\n * Error thrown when secret is not configured\n */\nexport class SecretMissingError extends ConfigurationError {\n constructor() {\n super(\n \"Secret is required. \" +\n \"Use generateSecret() to create one, or provide via { secret: 'YOUR_BASE32_SECRET' }\",\n );\n this.name = \"SecretMissingError\";\n }\n}\n\n/**\n * Error thrown when label is not configured (required for URI generation)\n */\nexport class LabelMissingError extends ConfigurationError {\n constructor() {\n super(\"Label is required for URI generation. Example: { label: 'user@example.com' }\");\n this.name = \"LabelMissingError\";\n }\n}\n\n/**\n * Error thrown when issuer is not configured (required for URI generation)\n */\nexport class IssuerMissingError extends ConfigurationError {\n constructor() {\n super(\"Issuer is required for URI generation. Example: { issuer: 'MyApp' }\");\n this.name = \"IssuerMissingError\";\n }\n}\n\n/**\n * Error thrown when secret must be a Base32 string but is provided as bytes\n */\nexport class SecretTypeError extends ConfigurationError {\n constructor() {\n super(\n \"Class API requires secret to be a Base32 string, not Uint8Array. \" +\n \"Use generateSecret() or provide a Base32-encoded string.\",\n );\n this.name = \"SecretTypeError\";\n }\n}\n\n/**\n * Error thrown when afterTimeStep parameter is invalid\n */\nexport class AfterTimeStepError extends OTPError {\n constructor(message: string) {\n super(message);\n this.name = \"AfterTimeStepError\";\n }\n}\n\n/**\n * Error thrown when afterTimeStep is negative\n */\nexport class AfterTimeStepNegativeError extends AfterTimeStepError {\n constructor() {\n super(\"afterTimeStep must be >= 0\");\n this.name = \"AfterTimeStepNegativeError\";\n }\n}\n\n/**\n * Error thrown when afterTimeStep is not an integer\n */\nexport class AfterTimeStepNotIntegerError extends AfterTimeStepError {\n constructor() {\n super(\"Invalid afterTimeStep: non-integer value\");\n this.name = \"AfterTimeStepNotIntegerError\";\n }\n}\n\n/**\n * Error thrown when afterTimeStep exceeds the verification range\n */\nexport class AfterTimeStepRangeExceededError extends AfterTimeStepError {\n constructor() {\n super(\"Invalid afterTimeStep: cannot be greater than current time step plus window\");\n this.name = \"AfterTimeStepRangeExceededError\";\n }\n}\n","import {\n OTPError,\n SecretTooShortError,\n SecretTooLongError,\n CounterNegativeError,\n CounterOverflowError,\n CounterNotIntegerError,\n TimeNegativeError,\n TimeNotFiniteError,\n PeriodTooSmallError,\n PeriodTooLargeError,\n TokenLengthError,\n TokenFormatError,\n CounterToleranceError,\n CounterToleranceTooLargeError,\n CounterToleranceNegativeError,\n EpochToleranceError,\n EpochToleranceNegativeError,\n EpochToleranceTooLargeError,\n CryptoPluginMissingError,\n Base32PluginMissingError,\n ConfigurationError,\n SecretMissingError,\n LabelMissingError,\n IssuerMissingError,\n SecretTypeError,\n} from \"./errors.js\";\n\nimport type {\n HashAlgorithm,\n SecretOptions,\n OTPResultOk,\n OTPResultError,\n OTPResult,\n} from \"./types.js\";\n\n/**\n * Singleton TextEncoder instance to avoid repeated allocations\n */\nconst textEncoder = new TextEncoder();\n\n/**\n * Singleton TextDecoder instance to avoid repeated allocations\n */\nconst textDecoder = new TextDecoder();\n\n/**\n * Minimum secret length in bytes (128 bits as per RFC 4226)\n */\nexport const MIN_SECRET_BYTES = 16;\n\n/**\n * Maximum secret length in bytes (512 bits)\n *\n * The 64-byte maximum is not part of the RFCs.\n * This is to prevent excessive memory usage in HMAC operations.\n */\nexport const MAX_SECRET_BYTES = 64;\n\n/**\n * Recommended secret length in bytes (160 bits as per RFC 4226)\n */\nexport const RECOMMENDED_SECRET_BYTES = 20;\n\n/**\n * Minimum period in seconds\n */\nexport const MIN_PERIOD = 1;\n\n/**\n * Maximum period in seconds (1 hour)\n */\nexport const MAX_PERIOD = 3600;\n\n/**\n * Default period in seconds (30 seconds as per RFC 6238)\n */\nexport const DEFAULT_PERIOD = 30;\n\n/**\n * Maximum safe integer for counter (2^53 - 1)\n */\nexport const MAX_COUNTER = Number.MAX_SAFE_INTEGER;\n\n/**\n * Maximum verification window size\n *\n * Limits the number of HMAC computations during verification to prevent DoS attacks.\n * A window of 99 means up to 99 HMAC computations (total checks including current counter).\n * Odd number to cater for equal distribution of time drift + current.\n *\n * For TOTP: window=1 is typically sufficient (allows +-30 seconds clock drift)\n * For HOTP: window=10-50 handles reasonable counter desynchronization\n */\nexport const MAX_WINDOW = 99;\n\n/**\n * Configurable guardrails for OTP validation\n *\n * Allows overriding default safety limits for non-standard production requirements.\n * Use with caution - custom guardrails can weaken security.\n */\nexport type OTPGuardrailsConfig = {\n MIN_SECRET_BYTES: number;\n MAX_SECRET_BYTES: number;\n MIN_PERIOD: number;\n MAX_PERIOD: number;\n MAX_COUNTER: number;\n MAX_WINDOW: number;\n};\n\n/**\n * Module-private symbol to track guardrail override status\n *\n * This symbol is used as a property key to store whether guardrails contain custom values.\n * Being module-private and a symbol ensures:\n * - Cannot be accessed outside this module (not exported)\n * - Cannot be recreated (each Symbol() call is unique)\n * - Hidden from normal enumeration (Object.keys, JSON.stringify, for-in)\n * - Minimal memory overhead (~1 byte per object)\n * - No garbage collection concerns\n *\n * @internal\n */\nconst OVERRIDE_SYMBOL = Symbol(\"otplib.guardrails.override\");\n\n/**\n * Complete guardrails configuration\n *\n * This represents the final, immutable configuration used by validation functions.\n * Internally tracks whether any values were overridden from RFC recommendations,\n * enabling security auditing and compliance monitoring without exposing implementation\n * details in the public API.\n *\n * The override status is stored using a module-private Symbol that cannot be accessed\n * or recreated outside this module, providing true encapsulation.\n *\n * @see {@link OTPGuardrailsConfig} for the base configuration structure\n * @see {@link createGuardrails} for creating guardrails instances\n * @see {@link hasGuardrailOverrides} to check if guardrails were customized\n */\nexport type OTPGuardrails = Readonly<OTPGuardrailsConfig> & {\n [OVERRIDE_SYMBOL]?: boolean;\n};\n\n/**\n * Validate guardrail numeric field\n */\nfunction assertGuardrailSafeInteger(\n name: string,\n value: unknown,\n min: number,\n): asserts value is number {\n if (typeof value !== \"number\" || !Number.isSafeInteger(value)) {\n throw new ConfigurationError(`Guardrail '${name}' must be a safe integer`);\n }\n if (value < min) {\n throw new ConfigurationError(`Guardrail '${name}' must be >= ${min}`);\n }\n}\n\n/**\n * Default guardrails matching RFC recommendations\n *\n * Frozen to ensure immutability. Used as default parameter for validation functions.\n * For custom guardrails, use the createGuardrails() factory function.\n */\nconst DEFAULT_GUARDRAILS: OTPGuardrails = Object.freeze({\n MIN_SECRET_BYTES,\n MAX_SECRET_BYTES,\n MIN_PERIOD,\n MAX_PERIOD,\n MAX_COUNTER,\n MAX_WINDOW,\n [OVERRIDE_SYMBOL]: false,\n});\n\n/**\n * Create guardrails configuration object\n *\n * Factory function that merges custom guardrails with defaults and returns\n * an immutable (frozen) object. Validates custom guardrails to ensure they\n * maintain basic safety invariants.\n *\n * When called without arguments or with `undefined`, returns the default guardrails\n * singleton (optimized to avoid unnecessary allocations). When called with custom\n * values, creates a new frozen object and internally marks it as overridden.\n *\n * @param custom - Optional partial guardrails to override defaults\n * @returns Frozen guardrails object\n * @throws {ConfigurationError} If custom guardrails violate safety invariants\n *\n * @example Basic usage\n * ```ts\n * import { createGuardrails, hasGuardrailOverrides } from '@otplib/core'\n *\n * // Returns default singleton (no overrides)\n * const defaults = createGuardrails();\n * hasGuardrailOverrides(defaults); // false\n *\n * // Creates new object with overrides\n * const custom = createGuardrails({\n * MIN_SECRET_BYTES: 8,\n * MAX_WINDOW: 200\n * });\n * hasGuardrailOverrides(custom); // true\n * ```\n *\n * @example Monitoring custom guardrails\n * ```ts\n * import { createGuardrails, hasGuardrailOverrides } from '@otplib/core';\n *\n * const guardrails = createGuardrails({ MAX_WINDOW: 20 });\n *\n * if (hasGuardrailOverrides(guardrails)) {\n * logger.warn('Non-default guardrails in use', { guardrails });\n * }\n * ```\n *\n * @see {@link hasGuardrailOverrides} to check if guardrails were customized\n */\nexport function createGuardrails(custom?: Partial<OTPGuardrailsConfig>): OTPGuardrails {\n if (!custom) {\n return DEFAULT_GUARDRAILS;\n }\n\n if (custom.MIN_SECRET_BYTES !== undefined) {\n assertGuardrailSafeInteger(\"MIN_SECRET_BYTES\", custom.MIN_SECRET_BYTES, 1);\n }\n\n if (custom.MAX_SECRET_BYTES !== undefined) {\n assertGuardrailSafeInteger(\"MAX_SECRET_BYTES\", custom.MAX_SECRET_BYTES, 1);\n }\n\n if (custom.MIN_PERIOD !== undefined) {\n assertGuardrailSafeInteger(\"MIN_PERIOD\", custom.MIN_PERIOD, 1);\n }\n\n if (custom.MAX_PERIOD !== undefined) {\n assertGuardrailSafeInteger(\"MAX_PERIOD\", custom.MAX_PERIOD, 1);\n }\n\n if (custom.MAX_COUNTER !== undefined) {\n assertGuardrailSafeInteger(\"MAX_COUNTER\", custom.MAX_COUNTER, 0);\n }\n\n if (custom.MAX_WINDOW !== undefined) {\n assertGuardrailSafeInteger(\"MAX_WINDOW\", custom.MAX_WINDOW, 1);\n }\n\n const merged = {\n ...DEFAULT_GUARDRAILS,\n ...custom,\n };\n\n if (merged.MIN_SECRET_BYTES > merged.MAX_SECRET_BYTES) {\n throw new ConfigurationError(\"Guardrail 'MIN_SECRET_BYTES' must be <= 'MAX_SECRET_BYTES'\");\n }\n\n if (merged.MIN_PERIOD > merged.MAX_PERIOD) {\n throw new ConfigurationError(\"Guardrail 'MIN_PERIOD' must be <= 'MAX_PERIOD'\");\n }\n\n return Object.freeze({\n ...merged,\n [OVERRIDE_SYMBOL]: true,\n });\n}\n\n/**\n * Check if guardrails contain custom overrides\n *\n * Returns `true` if the guardrails object was created with custom values,\n * `false` if using RFC-recommended defaults. Useful for security auditing,\n * compliance monitoring, and development warnings.\n *\n * This function accesses a module-private Symbol property that cannot be\n * accessed or modified outside this module, ensuring reliable detection.\n *\n * @param guardrails - The guardrails object to check\n * @returns `true` if guardrails were customized, `false` if using defaults\n *\n * @example Security monitoring\n * ```ts\n * import { createGuardrails, hasGuardrailOverrides } from '@otplib/core';\n *\n * const guardrails = createGuardrails({ MAX_WINDOW: 20 });\n *\n * if (hasGuardrailOverrides(guardrails)) {\n * console.warn('Custom guardrails detected:', guardrails);\n * // Log to security audit system\n * }\n * ```\n *\n * @example Compliance check\n * ```ts\n * function validateGuardrails(guardrails: OTPGuardrails) {\n * if (hasGuardrailOverrides(guardrails)) {\n * throw new Error('Custom guardrails not allowed in production');\n * }\n * }\n * ```\n */\nexport function hasGuardrailOverrides(guardrails: OTPGuardrails): boolean {\n return guardrails[OVERRIDE_SYMBOL] ?? false;\n}\n\n/**\n * Validate secret key\n *\n * @param secret - The secret to validate\n * @param guardrails - Validation guardrails (defaults to RFC recommendations)\n * @throws {SecretTooShortError} If secret is too short\n * @throws {SecretTooLongError} If secret is too long\n */\nexport function validateSecret(\n secret: Uint8Array,\n guardrails: OTPGuardrails = DEFAULT_GUARDRAILS,\n): void {\n if (secret.length < guardrails.MIN_SECRET_BYTES) {\n throw new SecretTooShortError(guardrails.MIN_SECRET_BYTES, secret.length);\n }\n\n if (secret.length > guardrails.MAX_SECRET_BYTES) {\n throw new SecretTooLongError(guardrails.MAX_SECRET_BYTES, secret.length);\n }\n}\n\n/**\n * Validate counter value\n *\n * @param counter - The counter to validate\n * @param guardrails - Validation guardrails (defaults to RFC recommendations)\n * @throws {CounterNegativeError} If counter is negative\n * @throws {CounterOverflowError} If counter exceeds safe integer\n */\nexport function validateCounter(\n counter: number | bigint,\n guardrails: OTPGuardrails = DEFAULT_GUARDRAILS,\n): void {\n if (typeof counter === \"number\") {\n if (!Number.isFinite(counter) || !Number.isInteger(counter)) {\n throw new CounterNotIntegerError();\n }\n\n if (!Number.isSafeInteger(counter)) {\n // Numbers outside the safe integer range cannot be reliably converted to bigint\n throw new CounterOverflowError();\n }\n }\n\n const value = typeof counter === \"bigint\" ? counter : BigInt(counter);\n\n if (value < 0n) {\n throw new CounterNegativeError();\n }\n\n if (value > BigInt(guardrails.MAX_COUNTER)) {\n throw new CounterOverflowError();\n }\n}\n\n/**\n * Validate time value\n *\n * @param time - The time in seconds to validate\n * @throws {TimeNegativeError} If time is negative\n */\nexport function validateTime(time: number): void {\n if (!Number.isFinite(time)) {\n throw new TimeNotFiniteError();\n }\n\n if (time < 0) {\n throw new TimeNegativeError();\n }\n}\n\n/**\n * Validate period value\n *\n * @param period - The period in seconds to validate\n * @param guardrails - Validation guardrails (defaults to RFC recommendations)\n * @throws {PeriodTooSmallError} If period is too small\n * @throws {PeriodTooLargeError} If period is too large\n */\nexport function validatePeriod(\n period: number,\n guardrails: OTPGuardrails = DEFAULT_GUARDRAILS,\n): void {\n if (!Number.isInteger(period) || period < guardrails.MIN_PERIOD) {\n throw new PeriodTooSmallError(guardrails.MIN_PERIOD);\n }\n\n if (period > guardrails.MAX_PERIOD) {\n throw new PeriodTooLargeError(guardrails.MAX_PERIOD);\n }\n}\n\n/**\n * Validate token\n *\n * @param token - The token string to validate\n * @param digits - Expected number of digits\n * @throws {TokenLengthError} If token has incorrect length\n * @throws {TokenFormatError} If token contains non-digit characters\n */\nexport function validateToken(token: string, digits: number): void {\n if (token.length !== digits) {\n throw new TokenLengthError(digits, token.length);\n }\n\n if (!/^\\d+$/.test(token)) {\n throw new TokenFormatError();\n }\n}\n\n/**\n * Validate counter tolerance for HOTP verification\n *\n * Prevents DoS attacks by limiting the number of counter values checked.\n *\n * @param counterTolerance - Counter tolerance specification (number or array of offsets)\n * @param guardrails - Validation guardrails (defaults to RFC recommendations)\n * @throws {CounterToleranceTooLargeError} If tolerance size exceeds MAX_WINDOW\n *\n * @example\n * ```ts\n * validateCounterTolerance(1); // OK: [0, 1] = 2 checks\n * validateCounterTolerance(98); // OK: [0, 98] = 99 checks\n * validateCounterTolerance(99); // Throws: exceeds MAX_WINDOW\n * validateCounterTolerance([0, 1]); // OK: 2 offsets\n * ```\n */\nexport function validateCounterTolerance(\n counterTolerance: number | [number, number],\n guardrails: OTPGuardrails = DEFAULT_GUARDRAILS,\n): void {\n const [past, future] = normalizeCounterTolerance(counterTolerance);\n\n if (!Number.isSafeInteger(past) || !Number.isSafeInteger(future)) {\n throw new CounterToleranceError(\"Counter tolerance values must be safe integers\");\n }\n\n if (past < 0 || future < 0) {\n throw new CounterToleranceNegativeError();\n }\n\n const totalChecks = past + future + 1;\n\n if (totalChecks > guardrails.MAX_WINDOW) {\n throw new CounterToleranceTooLargeError(guardrails.MAX_WINDOW, totalChecks);\n }\n}\n\n/**\n * Validate epoch tolerance for TOTP verification\n *\n * Prevents DoS attacks by limiting the time range checked.\n * Also validates that tolerance values are non-negative.\n *\n * @param epochTolerance - Epoch tolerance specification (number or tuple [past, future])\n * @param period - The TOTP period in seconds (default: 30). Used to calculate max tolerance.\n * @param guardrails - Validation guardrails (defaults to RFC recommendations)\n * @throws {EpochToleranceNegativeError} If tolerance contains negative values\n * @throws {EpochToleranceTooLargeError} If tolerance exceeds MAX_WINDOW periods\n *\n * @example\n * ```ts\n * validateEpochTolerance(30); // OK: 30 seconds symmetric\n * validateEpochTolerance([5, 0]); // OK: 5 seconds past only\n * validateEpochTolerance([-5, 0]); // Throws: negative values not allowed\n * validateEpochTolerance(1471); // Throws with default guardrails (30s period)\n * validateEpochTolerance(2940, 60); // OK with 60s period\n * ```\n */\nexport function validateEpochTolerance(\n epochTolerance: number | [number, number],\n period: number = DEFAULT_PERIOD,\n guardrails: OTPGuardrails = DEFAULT_GUARDRAILS,\n): void {\n const [pastTolerance, futureTolerance] = Array.isArray(epochTolerance)\n ? epochTolerance\n : [epochTolerance, epochTolerance];\n\n if (!Number.isSafeInteger(pastTolerance) || !Number.isSafeInteger(futureTolerance)) {\n throw new EpochToleranceError(\"Epoch tolerance values must be safe integers\");\n }\n\n // Check for negative values\n if (pastTolerance < 0 || futureTolerance < 0) {\n throw new EpochToleranceNegativeError();\n }\n\n // MAX_WINDOW checks means at most MAX_WINDOW - 1 periods of tolerance\n // (the current time step always consumes one check).\n const maxToleranceSeconds = (guardrails.MAX_WINDOW - 1) * period;\n const totalToleranceSeconds = pastTolerance + futureTolerance;\n\n if (totalToleranceSeconds > maxToleranceSeconds) {\n throw new EpochToleranceTooLargeError(maxToleranceSeconds, totalToleranceSeconds);\n }\n}\n\n/**\n * Convert counter to 8-byte big-endian array\n *\n * Per RFC 4226 Section 5.1, the counter value is represented as an 8-byte\n * big-endian (network byte order) unsigned integer.\n *\n * @see {@link https://tools.ietf.org/html/rfc4226#section-5.1 | RFC 4226 Section 5.1 - Symbol Descriptions}\n *\n * @param value - The counter value to convert\n * @returns 8-byte big-endian array\n */\nexport function counterToBytes(value: number | bigint): Uint8Array {\n const bigintValue = typeof value === \"bigint\" ? value : BigInt(value);\n const buffer = new ArrayBuffer(8);\n const view = new DataView(buffer);\n\n view.setBigUint64(0, bigintValue, false);\n\n return new Uint8Array(buffer);\n}\n\n/**\n * Perform Dynamic Truncation as per RFC 4226 Section 5.3\n *\n * The algorithm:\n * 1. Take the low-order 4 bits of the last byte as offset\n * 2. Extract 4 bytes starting at offset\n * 3. Mask the most significant bit to get a 31-bit unsigned integer\n *\n * This ensures consistent extraction across different HMAC output sizes\n * while producing a value that fits in a signed 32-bit integer.\n *\n * @see {@link https://tools.ietf.org/html/rfc4226#section-5.3 | RFC 4226 Section 5.3 - Generating an HOTP Value}\n *\n * @param hmacResult - HMAC result (at least 20 bytes for SHA-1)\n * @returns Truncated 31-bit unsigned integer\n */\nexport function dynamicTruncate(hmacResult: Uint8Array): number {\n const offset = hmacResult[hmacResult.length - 1] & 0x0f;\n\n const binary =\n ((hmacResult[offset] & 0x7f) << 24) |\n (hmacResult[offset + 1] << 16) |\n (hmacResult[offset + 2] << 8) |\n hmacResult[offset + 3];\n\n return binary;\n}\n\n/**\n * Convert truncated integer to OTP string with specified digits\n *\n * Computes: Snum mod 10^Digit (RFC 4226 Section 5.3)\n *\n * The result is zero-padded to ensure consistent length,\n * as required for proper token comparison.\n *\n * @see {@link https://tools.ietf.org/html/rfc4226#section-5.3 | RFC 4226 Section 5.3 - Generating an HOTP Value}\n *\n * @param value - The truncated integer value (Snum)\n * @param digits - Number of digits for the OTP (Digit, typically 6-8)\n * @returns OTP string with leading zeros if necessary\n */\nexport function truncateDigits(value: number, digits: number): string {\n const maxOtp = 10 ** digits;\n const otp = value % maxOtp;\n return otp.toString().padStart(digits, \"0\");\n}\n\n/**\n * Validate that two byte arrays have equal length\n *\n * Useful as a preliminary check before performing byte-by-byte comparisons.\n *\n * @param a - First byte array\n * @param b - Second byte array\n * @returns true if arrays have equal length, false otherwise\n */\nexport function validateByteLengthEqual(a: Uint8Array, b: Uint8Array): boolean {\n return a.length === b.length;\n}\n\n/**\n * Constant-time comparison to prevent timing attacks\n *\n * This implements a timing-safe equality check as recommended in\n * RFC 4226 Section 7.2 for token validation to prevent\n * timing side-channel attacks.\n *\n * @see {@link https://tools.ietf.org/html/rfc4226#section-7.2 | RFC 4226 Section 7.2 - Validation and Verification}\n *\n * @param a - First value to compare\n * @param b - Second value to compare\n * @returns true if values are equal, false otherwise\n */\nexport function constantTimeEqual(a: string | Uint8Array, b: string | Uint8Array): boolean {\n const bufA = stringToBytes(a);\n const bufB = stringToBytes(b);\n\n if (!validateByteLengthEqual(bufA, bufB)) {\n return false;\n }\n\n let result = 0;\n for (let i = 0; i < bufA.length; i++) {\n result |= bufA[i] ^ bufB[i];\n }\n\n return result === 0;\n}\n\n/**\n * Get HMAC digest size in bytes for a given algorithm\n *\n * @param algorithm - The hash algorithm\n * @returns Digest size in bytes\n */\nexport function getDigestSize(algorithm: HashAlgorithm): number {\n switch (algorithm) {\n case \"sha1\":\n return 20;\n case \"sha256\":\n return 32;\n case \"sha512\":\n return 64;\n }\n}\n\n/**\n * Convert a string or Uint8Array to Uint8Array\n *\n * This utility function normalizes input to Uint8Array, converting strings\n * using UTF-8 encoding. Uint8Array inputs are returned as-is.\n *\n * Use this to convert raw secret strings (passphrases) to Uint8Array\n * before passing them to generation or verification functions.\n *\n * @param value - The value to convert (string or Uint8Array)\n * @returns The value as a Uint8Array (UTF-8 encoded for strings)\n *\n * @example\n * ```ts\n * import { stringToBytes } from '@otplib/core'\n *\n * const bytes1 = stringToBytes('1234567890123456')\n * // Returns: Uint8Array([49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54])\n *\n * const bytes2 = stringToBytes(new Uint8Array([1, 2, 3]))\n * // Returns: Uint8Array([1, 2, 3]) - returned as-is\n * ```\n */\nexport function stringToBytes(value: string | Uint8Array): Uint8Array {\n return typeof value === \"string\" ? textEncoder.encode(value) : value;\n}\n\n/**\n * Convert bytes to UTF-8 string\n *\n * Uses TextDecoder for proper UTF-8 handling.\n *\n * @param bytes - Uint8Array to convert\n * @returns UTF-8 string\n *\n * @example\n * ```ts\n * const str = bytesToString(new Uint8Array([104, 101, 108, 108, 111]));\n * // str === \"hello\"\n * ```\n */\nexport function bytesToString(bytes: Uint8Array): string {\n return textDecoder.decode(bytes);\n}\n\n/**\n * Normalize secret input to Uint8Array\n *\n * Accepts either a Base32-encoded string or Uint8Array and returns Uint8Array.\n * If a Base32Plugin is provided, string secrets will be automatically decoded.\n *\n * **Note**: By default, strings are assumed to be Base32 encoded.\n * If you have a raw string secret (e.g. a passphrase), you must convert it\n * to a Uint8Array using {@link stringToBytes} before calling this function.\n *\n * @param secret - The secret to normalize (Base32 string or Uint8Array)\n * @param base32 - Optional Base32Plugin to decode string secrets\n * @returns The secret as Uint8Array\n * @throws {Error} If secret is a string but no Base32Plugin is provided\n *\n * @example\n * ```ts\n * import { normalizeSecret } from '@otplib/core'\n * import { ScureBase32Plugin } from '@otplib/plugin-base32-scure'\n *\n * const base32 = new ScureBase32Plugin()\n *\n * // Uint8Array - returned as-is\n * const secret1 = normalizeSecret(new Uint8Array([1, 2, 3]))\n *\n * // Base32 string - automatically decoded\n * const secret2 = normalizeSecret('JBSWY3DPEHPK3PXP', base32)\n * ```\n */\nexport function normalizeSecret(\n secret: string | Uint8Array,\n base32?: { decode: (str: string) => Uint8Array },\n): Uint8Array {\n if (typeof secret === \"string\") {\n requireBase32Plugin(base32);\n return base32.decode(secret);\n }\n return secret;\n}\n\n/**\n * Generate a random Base32-encoded secret\n *\n * Creates a cryptographically secure random secret suitable for OTP generation.\n * The default length of 20 bytes (160 bits) matches RFC 4226 recommendations\n * and provides good security margin.\n *\n * @param options - Secret generation options\n * @returns Base32-encoded secret string (without padding for Google Authenticator compatibility)\n *\n * @example\n * ```ts\n * import { generateSecret } from '@otplib/core';\n * import { NodeCryptoPlugin } from '@otplib/plugin-crypto-node';\n * import { ScureBase32Plugin } from '@otplib/plugin-base32-scure';\n *\n * const secret = generateSecret({\n * crypto: new NodeCryptoPlugin(),\n * base32: new ScureBase32Plugin(),\n * });\n * // Returns: 'JBSWY3DPEHPK3PXP...' (32 characters)\n * ```\n *\n * @example Custom length\n * ```ts\n * const secret = generateSecret({\n * crypto: new NodeCryptoPlugin(),\n * base32: new ScureBase32Plugin(),\n * length: 32, // 256 bits for SHA-256\n * });\n * ```\n */\nexport function generateSecret(options: SecretOptions): string {\n const { crypto, base32, length = RECOMMENDED_SECRET_BYTES } = options;\n\n requireCryptoPlugin(crypto);\n requireBase32Plugin(base32);\n\n const randomBytes = crypto.randomBytes(length);\n return base32.encode(randomBytes, { padding: false });\n}\n\n/**\n * Normalize counter tolerance to [past, future] tuple\n *\n * Converts a number or tuple counter tolerance specification into a [past, future] tuple\n * - Number: creates look-ahead only tolerance [0, tolerance] (default for security)\n * - Tuple: uses the tuple as-is (past, future)\n *\n * The default behavior (number → look-ahead only) improves security by preventing\n * replay attacks. HOTP counters should only move forward in normal operation.\n *\n * @param counterTolerance - Counter tolerance specification (number or tuple [past, future])\n * @returns Tuple [past, future] representing counters to check\n *\n * @example\n * ```ts\n * normalizeCounterTolerance(0) // [0, 0]\n * normalizeCounterTolerance(5) // [0, 5] - look-ahead only (secure default)\n * normalizeCounterTolerance([10, 5]) // [10, 5] - explicit past/future\n * normalizeCounterTolerance([5, 5]) // [5, 5] - explicit symmetric (use with caution)\n * ```\n */\nexport function normalizeCounterTolerance(\n counterTolerance: number | [number, number] = 0,\n): [number, number] {\n return Array.isArray(counterTolerance) ? counterTolerance : [0, counterTolerance];\n}\n\n/**\n * Normalize epoch tolerance to [past, future] tuple\n *\n * Converts a number or tuple epoch tolerance specification into a [past, future] tuple\n * - Number: creates symmetric tolerance [tolerance, tolerance]\n * - Tuple: uses the tuple as-is\n *\n * @param epochTolerance - Epoch tolerance specification (number or tuple [past, future])\n * @returns Tuple [pastTolerance, futureTolerance] in seconds\n *\n * @example\n * ```ts\n * normalizeEpochTolerance(0) // [0, 0]\n * normalizeEpochTolerance(30) // [30, 30]\n * normalizeEpochTolerance([5, 0]) // [5, 0]\n * normalizeEpochTolerance([10, 5]) // [10, 5]\n * ```\n */\nexport function normalizeEpochTolerance(\n epochTolerance: number | [number, number] = 0,\n): [number, number] {\n return Array.isArray(epochTolerance) ? epochTolerance : [epochTolerance, epochTolerance];\n}\n\n/**\n * Require crypto plugin to be configured\n *\n * @param crypto - The crypto plugin\n * @throws {CryptoPluginMissingError} If crypto plugin is not set\n */\nexport function requireCryptoPlugin<T>(crypto: T | undefined): asserts crypto is T {\n if (!crypto) {\n throw new CryptoPluginMissingError();\n }\n}\n\n/**\n * Require Base32 plugin to be configured\n *\n * @param base32 - The Base32 plugin\n * @throws {Base32PluginMissingError} If Base32 plugin is not set\n */\nexport function requireBase32Plugin<T>(base32: T | undefined): asserts base32 is T {\n if (!base32) {\n throw new Base32PluginMissingError();\n }\n}\n\n/**\n * Require secret to be configured\n *\n * @param secret - The secret value\n * @throws {SecretMissingError} If secret is not set\n */\nexport function requireSecret<T>(secret: T | undefined): asserts secret is T {\n if (!secret) {\n throw new SecretMissingError();\n }\n}\n\n/**\n * Require label to be configured (for URI generation)\n *\n * @param label - The label value\n * @throws {LabelMissingError} If label is not set\n */\nexport function requireLabel(label: string | undefined): asserts label is string {\n if (!label) {\n throw new LabelMissingError();\n }\n}\n\n/**\n * Require issuer to be configured (for URI generation)\n *\n * @param issuer - The issuer value\n * @throws {IssuerMissingError} If issuer is not set\n */\nexport function requireIssuer(issuer: string | undefined): asserts issuer is string {\n if (!issuer) {\n throw new IssuerMissingError();\n }\n}\n\n/**\n * Require secret to be a Base32 string (for URI generation)\n *\n * @param secret - The secret value\n * @throws {SecretTypeError} If secret is not a string\n */\nexport function requireBase32String(secret: string | Uint8Array): asserts secret is string {\n if (typeof secret !== \"string\") {\n throw new SecretTypeError();\n }\n}\n\n/**\n * Create a success result\n * @internal\n */\nfunction ok<T>(value: T): OTPResultOk<T> {\n return { ok: true, value };\n}\n\n/**\n * Create a failure result\n * @internal\n */\nfunction err<E>(error: E): OTPResultError<E> {\n return { ok: false, error };\n}\n\n/**\n * Wrap a synchronous function to return OTPResult instead of throwing\n *\n * Preserves the original OTPError subclass so users can access\n * specific error information via instanceof checks.\n *\n * @internal\n */\nexport function wrapResult<T, Args extends unknown[]>(\n fn: (...args: Args) => T,\n): (...args: Args) => OTPResult<T, OTPError> {\n return (...args: Args): OTPResult<T, OTPError> => {\n try {\n return ok(fn(...args));\n } catch (error) {\n return err(error as OTPError);\n }\n };\n}\n\n/**\n * Wrap an async function to return OTPResult instead of throwing\n *\n * Preserves the original OTPError subclass so users can access\n * specific error information via instanceof checks.\n *\n * @internal\n */\nexport function wrapResultAsync<T, Args extends unknown[]>(\n fn: (...args: Args) => Promise<T>,\n): (...args: Args) => Promise<OTPResult<T, OTPError>> {\n return async (...args: Args): Promise<OTPResult<T, OTPError>> => {\n try {\n return ok(await fn(...args));\n } catch (error) {\n return err(error as OTPError);\n }\n };\n}\n","import { HMACError, RandomBytesError } from \"./errors.js\";\n\nimport type { CryptoPlugin, HashAlgorithm } from \"./types.js\";\n\n/**\n * CryptoContext provides a unified interface for crypto operations\n * using a pluggable crypto backend\n */\nexport class CryptoContext {\n /**\n * Create a new CryptoContext with the given crypto plugin\n *\n * @param crypto - The crypto plugin to use\n */\n constructor(private readonly crypto: CryptoPlugin) {}\n\n /**\n * Get the underlying crypto plugin\n */\n get plugin(): CryptoPlugin {\n return this.crypto;\n }\n\n /**\n * Compute HMAC using the configured crypto plugin\n *\n * @param algorithm - The hash algorithm to use\n * @param key - The secret key as a byte array\n * @param data - The data to authenticate as a byte array\n * @returns HMAC digest as a byte array\n * @throws {HMACError} If HMAC computation fails\n */\n async hmac(algorithm: HashAlgorithm, key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {\n try {\n const result = this.crypto.hmac(algorithm, key, data);\n return result instanceof Promise ? await result : result;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new HMACError(message, { cause: error });\n }\n }\n\n /**\n * Synchronous HMAC computation\n *\n * @param algorithm - The hash algorithm to use\n * @param key - The secret key as a byte array\n * @param data - The data to authenticate as a byte array\n * @returns HMAC digest as a byte array\n * @throws {HMACError} If HMAC computation fails or if crypto plugin doesn't support sync operations\n */\n hmacSync(algorithm: HashAlgorithm, key: Uint8Array, data: Uint8Array): Uint8Array {\n try {\n const result = this.crypto.hmac(algorithm, key, data);\n if (result instanceof Promise) {\n throw new HMACError(\"Crypto plugin does not support synchronous HMAC operations\");\n }\n return result;\n } catch (error) {\n if (error instanceof HMACError) {\n throw error;\n }\n const message = error instanceof Error ? error.message : String(error);\n throw new HMACError(message, { cause: error });\n }\n }\n\n /**\n * Generate cryptographically secure random bytes\n *\n * @param length - Number of random bytes to generate\n * @returns Random bytes\n * @throws {RandomBytesError} If random byte generation fails\n */\n randomBytes(length: number): Uint8Array {\n try {\n return this.crypto.randomBytes(length);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new RandomBytesError(message, { cause: error });\n }\n }\n}\n\n/**\n * Create a CryptoContext from a crypto plugin\n *\n * @param crypto - The crypto plugin to use\n * @returns A new CryptoContext instance\n */\nexport function createCryptoContext(crypto: CryptoPlugin): CryptoContext {\n return new CryptoContext(crypto);\n}\n","import type { OTPAuthURI } from \"./types.js\";\nimport type { HashAlgorithm, Digits } from \"@otplib/core\";\n\n/**\n * Base options for URI generation\n */\nexport type URIOptions = {\n /**\n * Service/provider name (e.g., 'ACME Co', 'GitHub', 'AWS')\n */\n issuer?: string;\n\n /**\n * Account identifier (e.g., email, username)\n */\n label?: string;\n\n /**\n * Base32-encoded secret key\n */\n secret: string;\n\n /**\n * Hash algorithm (default: 'sha1')\n * Note: Google Authenticator only supports sha1\n */\n algorithm?: HashAlgorithm;\n\n /**\n * Number of digits (default: 6)\n * Google Authenticator supports 6 or 8, RFC also allows 7\n */\n digits?: Digits;\n\n /**\n * Time step in seconds for TOTP (default: 30)\n */\n period?: number;\n\n /**\n * Counter value for HOTP\n */\n counter?: number;\n};\n\n/**\n * TOTP-specific URI options\n */\nexport type TOTPURIOptions = URIOptions & {\n period?: number;\n counter?: never;\n};\n\n/**\n * HOTP-specific URI options\n */\nexport type HOTPURIOptions = URIOptions & {\n period?: never;\n counter?: number;\n};\n\n/**\n * Generate an otpauth:// URI\n *\n * @param uri - The URI components\n * @returns The otpauth:// URI string\n *\n * @example\n * ```ts\n * import { generate } from '@otplib/uri';\n * import { base32 } from '@otplib/plugin-base32-scure';\n *\n * const secret = base32.encode(new Uint8Array([1, 2, 3, 4, 5]));\n *\n * const uri = generate({\n * type: 'totp',\n * label: 'ACME:john@example.com',\n * params: {\n * secret,\n * issuer: 'ACME',\n * algorithm: 'sha1',\n * digits: 6,\n * },\n * });\n * // Returns: 'otpauth://totp/ACME:john%40example.com?secret=...'\n * ```\n */\nexport function generate(uri: OTPAuthURI): string {\n const { type, label, params } = uri;\n\n // Encode label parts while preserving ':' as the issuer/account separator\n const encodedLabel = label\n .split(\":\")\n .map((part) => encodeURIComponent(part))\n .join(\":\");\n\n let result = `otpauth://${type}/${encodedLabel}?`;\n\n const queryParams: string[] = [];\n\n if (params.secret) {\n queryParams.push(`secret=${encodeURIComponent(params.secret)}`);\n }\n\n if (params.issuer) {\n queryParams.push(`issuer=${encodeURIComponent(params.issuer)}`);\n }\n\n if (params.algorithm && params.algorithm !== \"sha1\") {\n queryParams.push(`algorithm=${params.algorithm.toUpperCase()}`);\n }\n\n if (params.digits && params.digits !== 6) {\n queryParams.push(`digits=${params.digits}`);\n }\n\n if (type === \"hotp\" && params.counter !== undefined) {\n queryParams.push(`counter=${params.counter}`);\n }\n\n if (type === \"totp\" && params.period !== undefined && params.period !== 30) {\n queryParams.push(`period=${params.p