Files
documenso/packages/lib/utils/zod.ts
T

87 lines
2.6 KiB
TypeScript

import { z } from 'zod';
/**
* RFC 5322 compliant email regex.
*
* This is more permissive than Zod's built-in `.email()` validator which rejects
* valid international characters (e.g. "Søren@gmail.com").
*
* Compiled once at module level to avoid re-compilation on every validation call.
*/
const EMAIL_REGEX =
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~\u{0080}-\u{FFFF}-]+@[a-zA-Z0-9\u{0080}-\u{FFFF}](?:[a-zA-Z0-9\u{0080}-\u{FFFF}-]{0,61}[a-zA-Z0-9\u{0080}-\u{FFFF}])?(?:\.[a-zA-Z0-9\u{0080}-\u{FFFF}](?:[a-zA-Z0-9\u{0080}-\u{FFFF}-]{0,61}[a-zA-Z0-9\u{0080}-\u{FFFF}])?)*$/u;
const DEFAULT_EMAIL_MESSAGE = 'Invalid email address';
/**
* Code point ranges for control and invisible/formatting characters that render
* as empty or break the UI (e.g. NUL, zero-width spaces, bidi overrides, BOM).
*/
const INVALID_TEXT_CODE_POINT_RANGES: [number, number][] = [
[0x0000, 0x001f],
[0x007f, 0x009f],
[0x00ad, 0x00ad],
[0x034f, 0x034f],
[0x061c, 0x061c],
[0x180e, 0x180e],
[0x200b, 0x200f],
[0x2028, 0x202e],
[0x2060, 0x206f],
[0xfeff, 0xfeff],
[0xd800, 0xdfff],
];
/**
* The same characters expressed as literal "\\uXXXX" text, which can be stored
* verbatim (e.g. the 6 characters `\`, `u`, `0`, `0`, `0`, `0`) and still break
* rendering downstream. This regex is pure ASCII so it is safe to inline.
*/
const INVALID_TEXT_ESCAPE_SEQUENCE_REGEX =
/\\u(?:00[0-1][0-9a-f]|007f|00[89][0-9a-f]|00ad|034f|061c|180e|200[b-f]|202[8-e]|206[0-9a-f]|feff)/iu;
export const hasInvalidTextCharacters = (value: string) => {
if (INVALID_TEXT_ESCAPE_SEQUENCE_REGEX.test(value)) {
return true;
}
for (const char of value) {
const codePoint = char.codePointAt(0);
if (codePoint === undefined) {
continue;
}
const isInvalid = INVALID_TEXT_CODE_POINT_RANGES.some(([start, end]) => codePoint >= start && codePoint <= end);
if (isInvalid) {
return true;
}
}
return false;
};
/**
* Creates a Zod email schema using an RFC 5322 compliant regex.
*
* Supports international characters in the local part and domain
* (e.g. "Søren@gmail.com", "user@dömain.com").
*
* Returns a standard `ZodString` so all string methods are chainable:
* `.min()`, `.max()`, `.trim()`, `.toLowerCase()`, `.optional()`, `.nullish()`, etc.
*
* @example
* ```ts
* zEmail()
* zEmail().min(1).max(254)
* zEmail().trim().toLowerCase()
* zEmail('Email is invalid')
* zEmail({ message: 'Email is invalid' })
* ```
*/
export const zEmail = (options?: string | { message?: string }) => {
const message = typeof options === 'string' ? options : (options?.message ?? DEFAULT_EMAIL_MESSAGE);
return z.string().regex(EMAIL_REGEX, { message });
};