…password reset (S2)
One OTP infra, three uses (PLAN_WEB_SIGNUP S2):
- accounts.ts: 6-digit codes (crypto.randomInt), sha256-at-rest, 10 min TTL,
5-attempt burn, purpose-scoped (verify|login|reset), 60s send-cooldown
applied uniformly to unknown emails so nothing enumerates accounts.
- resetPasswordWithOtp fills the missing forgot-password hole: atomic
code-consume + scrypt install, sessionVersion bump (all old sessions die),
and the code doubles as ownership proof so the account levels to verified.
- mailer.ts: generic sendMail + otpEmail template; the verification mail now
leads with the code and keeps the 24h link as fallback.
- server.ts: POST /api/auth/email/code | /api/auth/email/login |
/api/auth/password/reset (pre-gate, IP rate-bucketed) and session-authed
POST /api/auth/verify/confirm; register + verify/resend mails carry codes.
- login page: three-mode form (password / code / reset) with send-code flow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Part of PLAN_WEB_SIGNUP_v1.0 — milestone S2. Stacked on #297 (S1); merge that first — GitHub will retarget this to
main.What
One OTP infrastructure, three uses:
POST /api/auth/verify/confirm.POST /api/auth/email/code+POST /api/auth/email/login. Same trust root as an emailed reset link, so no new assumptions (plan §7 D4 verdict).POST /api/auth/password/resetconsumes a reset code and installs the new scrypt hash atomically, bumpssessionVersion(kills every outstanding session), and marks the account verified (the code just proved ownership).Hardening
{ok:true}for existing and unknown emails alike.Login page
Three-mode form (password / code / reset) with a send-code flow; new modes toggle
requiredflags so hidden inputs never block validity. Covered by the LOGIN_HTML syntax guard from S1.Tests
npm test: 1305 pass / 0 fail. New: 7 OTP cases (round-trip, purpose enforcement, uniform cooldown, attempt burn, expiry, reset semantics, malformed input) + 2 mail-compose cases.🤖 Generated with Claude Code