// Shared helpers for the 6-digit email codes used by sign-up verification and // password reset. Both flows store a salted hash keyed by email, so the code // itself only ever lives in the outgoing mail. import { createHash, 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"); export const hashCode = (email: string, code: string): string => createHash("sha256").update(`${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`, });