totp
@noy-db/on-totp·pnpm add @noy-db/on-totpAuthenticator-app second factor (RFC 6238).
What it does
generateTotpSecret() returns a random 20-byte Base32 secret (the RFC 4226
recommended minimum). totpProvisioningUri(secret, { account, issuer, digits, period, algorithm }) builds a standard otpauth://totp/ URI for
QR-code scanning into any authenticator app. generateTotpCode(secret, options) computes the current code; verifyTotp(secret, code, options)
checks a user-entered code with constant-time comparison and a
configurable clock-drift window (default ±1 step) to tolerate
authenticator/server skew. Defaults: 6 digits, 30s period, SHA1 (for
compatibility with Google Authenticator and similar apps). Zero
dependencies — HMAC via Web Crypto, validated against the official RFC
test vectors.
Usage
import { generateTotpSecret, totpProvisioningUri, verifyTotp } from '@noy-db/on-totp'
const secret = generateTotpSecret()
const uri = totpProvisioningUri(secret, { account: 'alice@acme.com', issuer: 'Acme Notes' })
// show `uri` as a QR code
const ok = await verifyTotp(secret, userEnteredCode) // constant-time, ±1 step window
⛔ Replay is the caller's to prevent. Without it, one code verifies for the whole drift
window (~90 s) — anyone who sees it can reuse it. Since 0.10.0-pre.2, verifyTotpStep returns the
step a code matched (or null), and the lastUsedStep option on both verifiers refuses that step
and every earlier one (RFC 6238 §5.2). The package stays stateless, so you persist the step —
per secret, somewhere the party presenting codes cannot rewind it:
import { verifyTotpStep } from '@noy-db/on-totp'
const step = await verifyTotpStep(secret, userEnteredCode, { lastUsedStep })
if (step === null) throw new Error('bad or reused code')
lastUsedStep = step // store it before treating the factor as passed