// Shared helpers for the 6-digit email codes used by sign-up verification and // password reset. Both flows store a peppered HMAC-SHA256 hash keyed by email, // so the code itself only ever lives in the outgoing mail. The HMAC uses // AUTH_JWT_SECRET as a pepper: a DB dump alone cannot recover codes without it. import { createHmac, randomInt, timingSafeEqual } from "crypto"; export const CODE_TTL_MINUTES = 15; // A 6-digit code is only 1,000,000 wide, so the attempt cap is what actually // makes it safe to email. Both code tables carry an `attempts` column. export const MAX_CODE_ATTEMPTS = 5; export const generateCode = (): string => String(randomInt(0, 1_000_000)).padStart(6, "0"); // Reuse the existing required secret as a pepper. No new env var and no // schema change (the codes table has no salt column). const pepper = (): string => { const value = process.env.AUTH_JWT_SECRET; if (!value) throw new Error("Missing AUTH_JWT_SECRET."); return value; }; export const hashCode = (email: string, code: string): string => createHmac("sha256", pepper()) .update(`waseel-otp:${email}:${code}`) .digest("hex"); export const codeMatches = ( storedHash: string, email: string, code: string, ): boolean => { const expected = Buffer.from(storedHash, "hex"); const actual = Buffer.from(hashCode(email, code), "hex"); return expected.length === actual.length && timingSafeEqual(expected, actual); }; // Gmail and iOS Mail only offer a one-tap "copy code" / keyboard suggestion // when the code leads the subject line and the body opens with a recognised // phrasing. Keep both formats intact when editing this copy. export const verificationEmail = (code: string) => ({ subject: `${code} is your Waseel verification code`, text: `${code} is your Waseel verification code.\n\n` + `Welcome to Waseel! Enter this code in the app to finish signing up.\n` + `It expires in ${CODE_TTL_MINUTES} minutes.\n`, }); export const resetEmail = (code: string) => ({ subject: `${code} is your Waseel password reset code`, text: `${code} is your Waseel password reset code.\n\n` + `We received a request to reset your Waseel password. Enter this code in ` + `the app to choose a new one.\n` + `It expires in ${CODE_TTL_MINUTES} minutes. If you didn't ask for this, ` + `you can ignore this email.\n`, });