stable

email-otp

Email one-time-passcode second factor with a caller-supplied transport (SMTP / SES / Postmark / any function) and constant-time verification.

@noy-db/on-email-otp · pnpm add @noy-db/on-email-otp Email OTP second factor, bring-your-own transport.

What it does

issueEmailOtp({ email, transport, ttlSeconds, digits, maxAttempts }) generates a numeric code (default 6 digits), hashes it with a random salt (SHA-256(code ⊕ salt)) into an EmailOtpRecord — the plaintext code is never stored, but treat the record as the code until it expires: a 6-digit code has a million values, so anyone holding the salt and digest recovers it in milliseconds — and calls your transport function with the code to deliver it, so any SMTP / SES / Postmark / custom sender works. Default TTL is 300 seconds (5 minutes), default max attempts 5. verifyEmailOtp(input, record) increments attempts on every call and returns { ok, reason } where reason is 'expired', 'mismatch', or 'locked' once maxAttempts is hit; the caller deletes the record on success or exhaustion.

Usage

import { issueEmailOtp, verifyEmailOtp } from '@noy-db/on-email-otp'

const { record } = await issueEmailOtp({
  email: 'alice@acme.com',
  transport: async ({ to, code }) => sendViaSES(to, `Your code: ${code}`),
})

const result = await verifyEmailOtp(userEnteredCode, record)
if (result.ok) { /* second factor satisfied */ }

See also