export function toCents(price: string | number): number { const n = typeof price === 'string' ? parseFloat(price) : price; if (Number.isNaN(n) || n < 0) { throw new Error('invalid price'); } return Math.round(n * 100); } export function formatPrice(cents: number): string { return `$${(cents / 100).toFixed(2)}`; } // RFC 5321 caps an address at 254 characters; reject anything longer up front so // validation cost stays bounded regardless of what a client posts. const MAX_EMAIL_LENGTH = 254; // Both patterns are anchored single character classes with no overlapping // alternatives, so they match in linear time. Splitting on '@' and '.' in code // rather than in one combined pattern avoids the ambiguous (and backtracking) // `[^\s@]+\.[^\s@]+` domain match. const LOCAL_PART_RE = /^[^\s@]+$/; const DOMAIN_LABEL_RE = /^[^\s@.]+$/; export function isValidEmail(email: string): boolean { const trimmed = email.trim(); if (trimmed.length === 0 || trimmed.length > MAX_EMAIL_LENGTH) { return false; } const at = trimmed.indexOf('@'); if (at === -1 || at !== trimmed.lastIndexOf('@')) { return false; } if (!LOCAL_PART_RE.test(trimmed.slice(0, at))) { return false; } const labels = trimmed.slice(at + 1).split('.'); return labels.length >= 2 && labels.every((label) => DOMAIN_LABEL_RE.test(label)); } export const MARKETING_CONSENT_TEXT = 'I want to receive occasional emails about new one-of-a-kind items from Redefined Designs. I can unsubscribe at any time.';