Files
redefined-designs/backend/src/utils.ts
T

45 lines
1.5 KiB
TypeScript
Executable File

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.';