@c9up/rune 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/MessagesProvider.d.ts +5 -0
- package/dist/MessagesProvider.d.ts.map +1 -1
- package/dist/MessagesProvider.js +1 -1
- package/dist/MessagesProvider.js.map +1 -1
- package/dist/Schema.d.ts +831 -35
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +2342 -140
- package/dist/Schema.js.map +1 -1
- package/dist/date.d.ts +36 -0
- package/dist/date.d.ts.map +1 -0
- package/dist/date.js +275 -0
- package/dist/date.js.map +1 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -1
- package/dist/formats.d.ts +148 -0
- package/dist/formats.d.ts.map +1 -0
- package/dist/formats.js +671 -0
- package/dist/formats.js.map +1 -0
- package/dist/index.d.ts +150 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +180 -1
- package/dist/index.js.map +1 -1
- package/dist/magic.d.ts +30 -0
- package/dist/magic.d.ts.map +1 -0
- package/dist/magic.js +154 -0
- package/dist/magic.js.map +1 -0
- package/dist/native.d.ts +18 -6
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +34 -19
- package/dist/native.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +9 -1
- package/src/MessagesProvider.ts +1 -1
- package/src/Schema.ts +3389 -177
- package/src/date.ts +320 -0
- package/src/errors.ts +11 -0
- package/src/formats.ts +776 -0
- package/src/index.ts +269 -0
- package/src/magic.ts +181 -0
- package/src/native.ts +36 -21
- package/src/types.ts +55 -0
package/dist/formats.js
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format validators backing the VineJS string/number/array rules.
|
|
3
|
+
*
|
|
4
|
+
* VineJS delegates these to `validator.js`. rune has zero runtime dependencies,
|
|
5
|
+
* so each check is implemented here. Where VineJS ships a per-locale table we
|
|
6
|
+
* cannot reasonably reproduce in full (mobile numbers, postal codes, passports),
|
|
7
|
+
* rune supports a named subset and **fails closed** on an unknown locale rather
|
|
8
|
+
* than waving the value through — an unchecked value that reports "valid" is the
|
|
9
|
+
* failure mode this package exists to prevent.
|
|
10
|
+
*/
|
|
11
|
+
const HEX_RE = /^#?(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
12
|
+
const ULID_RE = /^[0-7][0-9ABCDEFGHJKMNPQRSTVWXYZ]{25}$/i;
|
|
13
|
+
const JWT_RE = /^[\w-]+\.[\w-]+\.[\w-]*$/;
|
|
14
|
+
const IPV4_RE = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
|
|
15
|
+
const E164_RE = /^\+?[1-9]\d{6,14}$/;
|
|
16
|
+
/** Code-point scan rather than a control-character regex, which lints as suspicious. */
|
|
17
|
+
export const isAscii = (v) => [...v].every((c) => (c.codePointAt(0) ?? 0) <= 0x7f);
|
|
18
|
+
export const isHexCode = (v) => HEX_RE.test(v);
|
|
19
|
+
export const isUlid = (v) => ULID_RE.test(v);
|
|
20
|
+
export const isJwt = (v) => JWT_RE.test(v);
|
|
21
|
+
/** IPv6, including the `::` compressed form and IPv4-mapped tails. */
|
|
22
|
+
function isIpV6(v) {
|
|
23
|
+
if (!v.includes(":"))
|
|
24
|
+
return false;
|
|
25
|
+
const halves = v.split("::");
|
|
26
|
+
if (halves.length > 2)
|
|
27
|
+
return false;
|
|
28
|
+
const expand = (part) => part === "" ? [] : part.split(":");
|
|
29
|
+
const head = expand(halves[0]);
|
|
30
|
+
const tail = halves.length === 2 ? expand(halves[1]) : [];
|
|
31
|
+
const groups = [...head, ...tail];
|
|
32
|
+
// A trailing IPv4 literal occupies two groups.
|
|
33
|
+
const last = groups.at(-1);
|
|
34
|
+
const ipv4Tail = last?.includes(".") ?? false;
|
|
35
|
+
if (ipv4Tail && last !== undefined && !IPV4_RE.test(last))
|
|
36
|
+
return false;
|
|
37
|
+
const count = groups.length + (ipv4Tail ? 1 : 0);
|
|
38
|
+
if (halves.length === 1 ? count !== 8 : count >= 8)
|
|
39
|
+
return false;
|
|
40
|
+
return groups
|
|
41
|
+
.slice(0, ipv4Tail ? -1 : undefined)
|
|
42
|
+
.every((g) => /^[0-9a-f]{1,4}$/i.test(g));
|
|
43
|
+
}
|
|
44
|
+
export function isIpAddress(v, version) {
|
|
45
|
+
if (version === 4)
|
|
46
|
+
return IPV4_RE.test(v);
|
|
47
|
+
if (version === 6)
|
|
48
|
+
return isIpV6(v);
|
|
49
|
+
return IPV4_RE.test(v) || isIpV6(v);
|
|
50
|
+
}
|
|
51
|
+
/** Luhn checksum — the digits-only part of card validation. */
|
|
52
|
+
export function isCreditCard(v) {
|
|
53
|
+
const digits = v.replace(/[ -]/g, "");
|
|
54
|
+
if (!/^\d{12,19}$/.test(digits))
|
|
55
|
+
return false;
|
|
56
|
+
let sum = 0;
|
|
57
|
+
let double = false;
|
|
58
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
59
|
+
let d = digits.charCodeAt(i) - 48;
|
|
60
|
+
if (double) {
|
|
61
|
+
d *= 2;
|
|
62
|
+
if (d > 9)
|
|
63
|
+
d -= 9;
|
|
64
|
+
}
|
|
65
|
+
sum += d;
|
|
66
|
+
double = !double;
|
|
67
|
+
}
|
|
68
|
+
return sum % 10 === 0;
|
|
69
|
+
}
|
|
70
|
+
/** IBAN mod-97 check (ISO 13616), computed digit by digit to avoid BigInt. */
|
|
71
|
+
export function isIban(v) {
|
|
72
|
+
const s = v.replace(/\s/g, "").toUpperCase();
|
|
73
|
+
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{10,30}$/.test(s))
|
|
74
|
+
return false;
|
|
75
|
+
const rearranged = s.slice(4) + s.slice(0, 4);
|
|
76
|
+
let remainder = 0;
|
|
77
|
+
for (const ch of rearranged) {
|
|
78
|
+
const chunk = /\d/.test(ch) ? ch : String(ch.charCodeAt(0) - 55); // A→10 … Z→35
|
|
79
|
+
for (const digit of chunk) {
|
|
80
|
+
remainder = (remainder * 10 + (digit.charCodeAt(0) - 48)) % 97;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return remainder === 1;
|
|
84
|
+
}
|
|
85
|
+
/** `"lat,lng"` within the valid ranges. */
|
|
86
|
+
export function isCoordinates(v) {
|
|
87
|
+
const parts = v.split(",");
|
|
88
|
+
if (parts.length !== 2)
|
|
89
|
+
return false;
|
|
90
|
+
const lat = Number(parts[0].trim());
|
|
91
|
+
const lng = Number(parts[1].trim());
|
|
92
|
+
if (!Number.isFinite(lat) || !Number.isFinite(lng))
|
|
93
|
+
return false;
|
|
94
|
+
return lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Postal-code patterns. A named subset of VineJS's table — extend it here rather
|
|
98
|
+
* than at the call site, and see the module note on unknown locales.
|
|
99
|
+
*/
|
|
100
|
+
const POSTAL_CODES = {
|
|
101
|
+
AD: /^AD\d{3}$/i,
|
|
102
|
+
AT: /^\d{4}$/,
|
|
103
|
+
AU: /^\d{4}$/,
|
|
104
|
+
BE: /^\d{4}$/,
|
|
105
|
+
BG: /^\d{4}$/,
|
|
106
|
+
BR: /^\d{5}-?\d{3}$/,
|
|
107
|
+
CA: /^[ABCEGHJ-NPRSTVXY]\d[ABCEGHJ-NPRSTV-Z] ?\d[ABCEGHJ-NPRSTV-Z]\d$/i,
|
|
108
|
+
CH: /^\d{4}$/,
|
|
109
|
+
CN: /^\d{6}$/,
|
|
110
|
+
CZ: /^\d{3} ?\d{2}$/,
|
|
111
|
+
DE: /^\d{5}$/,
|
|
112
|
+
DK: /^\d{4}$/,
|
|
113
|
+
EE: /^\d{5}$/,
|
|
114
|
+
ES: /^\d{5}$/,
|
|
115
|
+
FI: /^\d{5}$/,
|
|
116
|
+
FR: /^\d{5}$/,
|
|
117
|
+
GB: /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i,
|
|
118
|
+
GR: /^\d{3} ?\d{2}$/,
|
|
119
|
+
HR: /^\d{5}$/,
|
|
120
|
+
HU: /^\d{4}$/,
|
|
121
|
+
IE: /^[A-Z]\d[\dW] ?[A-Z\d]{4}$/i,
|
|
122
|
+
IL: /^\d{5}(?:\d{2})?$/,
|
|
123
|
+
IN: /^\d{6}$/,
|
|
124
|
+
IS: /^\d{3}$/,
|
|
125
|
+
IT: /^\d{5}$/,
|
|
126
|
+
JP: /^\d{3}-?\d{4}$/,
|
|
127
|
+
KR: /^\d{5}$/,
|
|
128
|
+
LI: /^\d{4}$/,
|
|
129
|
+
LT: /^(?:LT-)?\d{5}$/i,
|
|
130
|
+
LU: /^\d{4}$/,
|
|
131
|
+
LV: /^(?:LV-)?\d{4}$/i,
|
|
132
|
+
MC: /^980\d{2}$/,
|
|
133
|
+
MT: /^[A-Z]{3} ?\d{4}$/i,
|
|
134
|
+
MX: /^\d{5}$/,
|
|
135
|
+
NL: /^\d{4} ?[A-Z]{2}$/i,
|
|
136
|
+
NO: /^\d{4}$/,
|
|
137
|
+
NZ: /^\d{4}$/,
|
|
138
|
+
PL: /^\d{2}-?\d{3}$/,
|
|
139
|
+
PT: /^\d{4}-?\d{3}$/,
|
|
140
|
+
RO: /^\d{6}$/,
|
|
141
|
+
RU: /^\d{6}$/,
|
|
142
|
+
SE: /^\d{3} ?\d{2}$/,
|
|
143
|
+
SI: /^(?:SI-)?\d{4}$/i,
|
|
144
|
+
SK: /^\d{3} ?\d{2}$/,
|
|
145
|
+
TR: /^\d{5}$/,
|
|
146
|
+
UA: /^\d{5}$/,
|
|
147
|
+
US: /^\d{5}(?:-\d{4})?$/,
|
|
148
|
+
ZA: /^\d{4}$/,
|
|
149
|
+
};
|
|
150
|
+
/** Country codes rune can check postal codes for. */
|
|
151
|
+
export const SUPPORTED_POSTAL_CODES = Object.keys(POSTAL_CODES);
|
|
152
|
+
export function isPostalCode(v, countryCode) {
|
|
153
|
+
const re = POSTAL_CODES[countryCode.toUpperCase()];
|
|
154
|
+
return re === undefined ? null : re.test(v.trim());
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Mobile numbers in E.164 form — the locale-less check. Per-locale plans live
|
|
158
|
+
* in `MOBILE_LOCALES`; see {@link isMobileForLocale}.
|
|
159
|
+
*/
|
|
160
|
+
export const isMobile = (v) => E164_RE.test(v.replace(/[ .-]/g, ""));
|
|
161
|
+
/** HTML-escape the five characters that break out of markup (VineJS `escape`). */
|
|
162
|
+
export function escapeHtml(v) {
|
|
163
|
+
return v
|
|
164
|
+
.replace(/&/g, "&")
|
|
165
|
+
.replace(/</g, "<")
|
|
166
|
+
.replace(/>/g, ">")
|
|
167
|
+
.replace(/"/g, """)
|
|
168
|
+
.replace(/'/g, "'");
|
|
169
|
+
}
|
|
170
|
+
const GMAIL_DOMAINS = new Set(["gmail.com", "googlemail.com"]);
|
|
171
|
+
export function normalizeEmail(value, options = {}) {
|
|
172
|
+
const at = value.lastIndexOf("@");
|
|
173
|
+
if (at < 1)
|
|
174
|
+
return value;
|
|
175
|
+
let local = value.slice(0, at);
|
|
176
|
+
let domain = value.slice(at + 1);
|
|
177
|
+
// The domain is case-insensitive per RFC 1035, so it is always lowercased.
|
|
178
|
+
domain = domain.toLowerCase();
|
|
179
|
+
const allLowercase = options.all_lowercase ?? options.allLowercase;
|
|
180
|
+
const removeSubaddress = options.gmail_remove_subaddress ?? options.gmailRemoveSubaddress;
|
|
181
|
+
const removeDots = options.gmail_remove_dots ?? options.gmailRemoveDots;
|
|
182
|
+
if (allLowercase !== false)
|
|
183
|
+
local = local.toLowerCase();
|
|
184
|
+
if (GMAIL_DOMAINS.has(domain)) {
|
|
185
|
+
if (removeSubaddress)
|
|
186
|
+
local = local.split("+")[0];
|
|
187
|
+
if (removeDots)
|
|
188
|
+
local = local.replace(/\./g, "");
|
|
189
|
+
}
|
|
190
|
+
else if (removeSubaddress) {
|
|
191
|
+
local = local.split("+")[0];
|
|
192
|
+
}
|
|
193
|
+
return `${local}@${domain}`;
|
|
194
|
+
}
|
|
195
|
+
export function normalizeUrl(value, options = {}) {
|
|
196
|
+
let url;
|
|
197
|
+
try {
|
|
198
|
+
url = new URL(value);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Not parseable: hand it back untouched and let `url()` report the failure.
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
if (options.forceProtocol)
|
|
205
|
+
url.protocol = `${options.forceProtocol}:`;
|
|
206
|
+
if (options.forceHttps && url.protocol === "http:")
|
|
207
|
+
url.protocol = "https:";
|
|
208
|
+
if (options.stripWWW)
|
|
209
|
+
url.hostname = url.hostname.replace(/^www\./, "");
|
|
210
|
+
if (options.stripHash)
|
|
211
|
+
url.hash = "";
|
|
212
|
+
if (options.stripAuthentication) {
|
|
213
|
+
url.username = "";
|
|
214
|
+
url.password = "";
|
|
215
|
+
}
|
|
216
|
+
if (options.removeExplicitPort &&
|
|
217
|
+
((url.protocol === "http:" && url.port === "80") ||
|
|
218
|
+
(url.protocol === "https:" && url.port === "443"))) {
|
|
219
|
+
url.port = "";
|
|
220
|
+
}
|
|
221
|
+
if (options.removeDirectoryIndex) {
|
|
222
|
+
url.pathname = url.pathname.replace(/\/index\.(?:html?|php|asp)$/i, "/");
|
|
223
|
+
}
|
|
224
|
+
for (const parameter of options.removeQueryParameters ?? []) {
|
|
225
|
+
for (const name of [...url.searchParams.keys()]) {
|
|
226
|
+
const matches = typeof parameter === "string"
|
|
227
|
+
? name === parameter
|
|
228
|
+
: parameter.test(name);
|
|
229
|
+
if (matches)
|
|
230
|
+
url.searchParams.delete(name);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (options.sortQueryParameters)
|
|
234
|
+
url.searchParams.sort();
|
|
235
|
+
let out = url.toString();
|
|
236
|
+
if (options.removeTrailingSlash) {
|
|
237
|
+
// Any path, not just the root one.
|
|
238
|
+
out = out.replace(/\/(?=(?:\?|#|$))/, "");
|
|
239
|
+
}
|
|
240
|
+
else if (options.stripTrailingSlash && url.pathname === "/") {
|
|
241
|
+
out = out.replace(/\/(?=(?:\?|#|$))/, "");
|
|
242
|
+
}
|
|
243
|
+
if (options.stripProtocol)
|
|
244
|
+
out = out.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
/** `dash-case`, `snake_case` and spaced words to `camelCase`. */
|
|
248
|
+
export function toCamelCase(v) {
|
|
249
|
+
return v
|
|
250
|
+
.trim()
|
|
251
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "")
|
|
252
|
+
.replace(/^(.)/, (c) => c.toLowerCase());
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Passport numbers. Named subset, same fail-closed contract as
|
|
256
|
+
* {@link isPostalCode}: an unknown country returns `null`.
|
|
257
|
+
*/
|
|
258
|
+
const PASSPORTS = {
|
|
259
|
+
AT: /^[A-Z]\d{7}$/i,
|
|
260
|
+
AU: /^[A-Z]\d{7}$/i,
|
|
261
|
+
BE: /^[A-Z]{2}\d{6}$/i,
|
|
262
|
+
CA: /^[A-Z]{2}\d{6}$/i,
|
|
263
|
+
CH: /^[A-Z]\d{7}$/i,
|
|
264
|
+
CZ: /^\d{8}$/,
|
|
265
|
+
DE: /^[CFGHJKLMNPRTVWXYZ0-9]{9}$/i,
|
|
266
|
+
DK: /^\d{9}$/,
|
|
267
|
+
ES: /^[A-Z]{3}\d{6}$/i,
|
|
268
|
+
FI: /^[A-Z]{2}\d{7}$/i,
|
|
269
|
+
FR: /^\d{2}[A-Z]{2}\d{5}$/i,
|
|
270
|
+
GB: /^\d{9}$/,
|
|
271
|
+
GR: /^[A-Z]{2}\d{7}$/i,
|
|
272
|
+
HU: /^[A-Z]{2}\d{6}$/i,
|
|
273
|
+
IE: /^[A-Z0-9]{2}\d{7}$/i,
|
|
274
|
+
IN: /^[A-Z]\d{7}$/i,
|
|
275
|
+
IT: /^[A-Z0-9]{2}\d{7}$/i,
|
|
276
|
+
JP: /^[A-Z]{2}\d{7}$/i,
|
|
277
|
+
KR: /^[MS]\d{8}$/i,
|
|
278
|
+
NL: /^[A-Z]{2}\d{6}[A-Z0-9]$/i,
|
|
279
|
+
NO: /^\d{8}$/,
|
|
280
|
+
PL: /^[A-Z]{2}\d{7}$/i,
|
|
281
|
+
PT: /^[A-Z]\d{6}$/i,
|
|
282
|
+
RO: /^\d{8,9}$/,
|
|
283
|
+
RU: /^\d{9}$/,
|
|
284
|
+
SE: /^\d{8}$/,
|
|
285
|
+
TR: /^[A-Z]\d{8}$/i,
|
|
286
|
+
UA: /^[A-Z]{2}\d{6}$/i,
|
|
287
|
+
US: /^\d{9}$/,
|
|
288
|
+
ZA: /^[TAMD]\d{8}$/i,
|
|
289
|
+
};
|
|
290
|
+
export const SUPPORTED_PASSPORTS = Object.keys(PASSPORTS);
|
|
291
|
+
export function isPassport(v, countryCode) {
|
|
292
|
+
const re = PASSPORTS[countryCode.toUpperCase()];
|
|
293
|
+
return re === undefined ? null : re.test(v.trim());
|
|
294
|
+
}
|
|
295
|
+
/** Build the character class for `alpha`/`alphaNumeric` from its options. */
|
|
296
|
+
export function alphaPattern(base, options = {}) {
|
|
297
|
+
let extra = "";
|
|
298
|
+
if (options.allowSpaces)
|
|
299
|
+
extra += " ";
|
|
300
|
+
if (options.allowUnderscores)
|
|
301
|
+
extra += "_";
|
|
302
|
+
if (options.allowDashes)
|
|
303
|
+
extra += "\\-";
|
|
304
|
+
return new RegExp(`^[${base}${extra}]+$`);
|
|
305
|
+
}
|
|
306
|
+
export function isUrlWithOptions(value, options = {}) {
|
|
307
|
+
// VineJS forwards validator.js options verbatim, so a transcribed Adonis
|
|
308
|
+
// validator arrives in snake_case. Both spellings are honoured, snake_case
|
|
309
|
+
// first, so neither form is silently ignored.
|
|
310
|
+
const requireProtocol = (options.require_protocol ?? options.requireProtocol) !== false;
|
|
311
|
+
const protocols = options.protocols ?? ["http", "https"];
|
|
312
|
+
const candidate = requireProtocol || /^[a-z][a-z0-9+.-]*:\/\//i.test(value)
|
|
313
|
+
? value
|
|
314
|
+
: `https://${value}`;
|
|
315
|
+
let url;
|
|
316
|
+
try {
|
|
317
|
+
url = new URL(candidate);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
if (requireProtocol && !/^[a-z][a-z0-9+.-]*:\/\//i.test(value))
|
|
323
|
+
return false;
|
|
324
|
+
if (!protocols.includes(url.protocol.replace(/:$/, "")))
|
|
325
|
+
return false;
|
|
326
|
+
if (url.hostname.length === 0)
|
|
327
|
+
return false;
|
|
328
|
+
const allowUnderscores = options.allow_underscores ?? options.allowUnderscores ?? false;
|
|
329
|
+
if (!allowUnderscores && url.hostname.includes("_"))
|
|
330
|
+
return false;
|
|
331
|
+
const requireTld = (options.require_tld ?? options.requireTld) !== false;
|
|
332
|
+
if (requireTld && !url.hostname.includes("."))
|
|
333
|
+
return false;
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Mobile numbering plans. Named subset of VineJS's `locale` table, same
|
|
338
|
+
* fail-closed contract as the postal codes: an unknown locale returns `null`.
|
|
339
|
+
*/
|
|
340
|
+
const MOBILE_LOCALES = {
|
|
341
|
+
"fr-CH": /^(?:\+41|0)7[5-9]\d{7}$/,
|
|
342
|
+
"de-CH": /^(?:\+41|0)7[5-9]\d{7}$/,
|
|
343
|
+
"it-CH": /^(?:\+41|0)7[5-9]\d{7}$/,
|
|
344
|
+
"fr-FR": /^(?:\+33|0)[67]\d{8}$/,
|
|
345
|
+
"fr-BE": /^(?:\+32|0)4[5-9]\d{7}$/,
|
|
346
|
+
"nl-BE": /^(?:\+32|0)4[5-9]\d{7}$/,
|
|
347
|
+
"en-US": /^(?:\+1)?[2-9]\d{9}$/,
|
|
348
|
+
"en-CA": /^(?:\+1)?[2-9]\d{9}$/,
|
|
349
|
+
"en-GB": /^(?:\+44|0)7\d{9}$/,
|
|
350
|
+
"en-IE": /^(?:\+353|0)8[35-9]\d{7}$/,
|
|
351
|
+
"en-AU": /^(?:\+61|0)4\d{8}$/,
|
|
352
|
+
"en-NZ": /^(?:\+64|0)2\d{7,9}$/,
|
|
353
|
+
"en-IN": /^(?:\+91|0)?[6-9]\d{9}$/,
|
|
354
|
+
"de-DE": /^(?:\+49|0)1[5-7]\d{8,9}$/,
|
|
355
|
+
"de-AT": /^(?:\+43|0)6[4-9]\d{7,10}$/,
|
|
356
|
+
"it-IT": /^(?:\+39)?3\d{8,9}$/,
|
|
357
|
+
"es-ES": /^(?:\+34)?[679]\d{8}$/,
|
|
358
|
+
"pt-PT": /^(?:\+351)?9[1236]\d{7}$/,
|
|
359
|
+
"pt-BR": /^(?:\+55)?(?:\d{2})?9?\d{8}$/,
|
|
360
|
+
"nl-NL": /^(?:\+31|0)6\d{8}$/,
|
|
361
|
+
"da-DK": /^(?:\+45)?\d{8}$/,
|
|
362
|
+
"sv-SE": /^(?:\+46|0)7[02369]\d{7}$/,
|
|
363
|
+
"nb-NO": /^(?:\+47)?[49]\d{7}$/,
|
|
364
|
+
"fi-FI": /^(?:\+358|0)4\d{5,10}$/,
|
|
365
|
+
"pl-PL": /^(?:\+48)?\d{9}$/,
|
|
366
|
+
"cs-CZ": /^(?:\+420)?[6-7]\d{8}$/,
|
|
367
|
+
"sk-SK": /^(?:\+421)?9\d{8}$/,
|
|
368
|
+
"hu-HU": /^(?:\+36|06)(?:20|30|31|50|70)\d{7}$/,
|
|
369
|
+
"ro-RO": /^(?:\+40|0)7\d{8}$/,
|
|
370
|
+
"el-GR": /^(?:\+30|0)6[89]\d{8}$/,
|
|
371
|
+
"tr-TR": /^(?:\+90|0)5\d{9}$/,
|
|
372
|
+
"ru-RU": /^(?:\+7|8)9\d{9}$/,
|
|
373
|
+
"uk-UA": /^(?:\+380|0)\d{9}$/,
|
|
374
|
+
"ja-JP": /^(?:\+81|0)[7-9]0\d{8}$/,
|
|
375
|
+
"ko-KR": /^(?:\+82|0)1[0-9]\d{7,8}$/,
|
|
376
|
+
"zh-CN": /^(?:\+86|0)?1[3-9]\d{9}$/,
|
|
377
|
+
"zh-TW": /^(?:\+886|0)9\d{8}$/,
|
|
378
|
+
"ar-AE": /^(?:\+971|0)5[0245678]\d{7}$/,
|
|
379
|
+
"ar-SA": /^(?:\+966|0)5\d{8}$/,
|
|
380
|
+
"he-IL": /^(?:\+972|0)5[0-9]\d{7}$/,
|
|
381
|
+
"en-ZA": /^(?:\+27|0)[6-8]\d{8}$/,
|
|
382
|
+
};
|
|
383
|
+
export const SUPPORTED_MOBILE_LOCALES = Object.keys(MOBILE_LOCALES);
|
|
384
|
+
export function isMobileForLocale(v, locale) {
|
|
385
|
+
const re = MOBILE_LOCALES[locale];
|
|
386
|
+
if (re === undefined)
|
|
387
|
+
return null;
|
|
388
|
+
return re.test(v.replace(/[ .-]/g, ""));
|
|
389
|
+
}
|
|
390
|
+
/** Hard bound on anything handed to the email parser (RFC 5322 line limit). */
|
|
391
|
+
const MAX_EMAIL_INPUT = 998;
|
|
392
|
+
/** Unquoted local part: dot-separated atoms of RFC 5322 atext. */
|
|
393
|
+
const ATEXT = "[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+";
|
|
394
|
+
const DOT_ATOM_RE = new RegExp(`^${ATEXT}(?:\\.${ATEXT})*$`);
|
|
395
|
+
/** Quoted local part: `"anything but bare quote/backslash, or escaped"`. */
|
|
396
|
+
const QUOTED_LOCAL_RE = /^"(?:[^"\\]|\\.)*"$/;
|
|
397
|
+
/**
|
|
398
|
+
* Split `Display Name <address@host>` into its address.
|
|
399
|
+
*
|
|
400
|
+
* Parsed rather than matched: the obvious pattern —
|
|
401
|
+
* `^\s*(?:"..."|[^<>@]*?)\s*<(.+)>\s*$` — lets `\s*` and `[^<>@]*?` both
|
|
402
|
+
* claim a space, so an input of N spaces has N ways to be split and the engine
|
|
403
|
+
* tries them all. Measured at O(n³): 8 KB of spaces blocked the event loop for
|
|
404
|
+
* 67 seconds, which turns any route validating an email into a denial of
|
|
405
|
+
* service. This walk is linear and answers the same question.
|
|
406
|
+
*
|
|
407
|
+
* Returns the address, or null when the input is not in display-name form.
|
|
408
|
+
*/
|
|
409
|
+
function displayNameAddress(input) {
|
|
410
|
+
const trimmed = input.trim();
|
|
411
|
+
if (!trimmed.endsWith(">"))
|
|
412
|
+
return null;
|
|
413
|
+
let open;
|
|
414
|
+
if (trimmed.startsWith('"')) {
|
|
415
|
+
// A quoted display name may contain anything, `<` included, so the
|
|
416
|
+
// address opens at the first `<` AFTER the closing quote.
|
|
417
|
+
const closingQuote = closingQuoteIndex(trimmed);
|
|
418
|
+
if (closingQuote === -1)
|
|
419
|
+
return null;
|
|
420
|
+
open = trimmed.indexOf("<", closingQuote + 1);
|
|
421
|
+
if (open === -1)
|
|
422
|
+
return null;
|
|
423
|
+
if (trimmed.slice(closingQuote + 1, open).trim() !== "")
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
open = trimmed.indexOf("<");
|
|
428
|
+
if (open === -1)
|
|
429
|
+
return null;
|
|
430
|
+
// An unquoted display name carries none of `<`, `>` or `@` — the same
|
|
431
|
+
// restriction the pattern expressed.
|
|
432
|
+
if (/[<>@]/.test(trimmed.slice(0, open)))
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
const address = trimmed.slice(open + 1, -1);
|
|
436
|
+
return address.length > 0 ? address : null;
|
|
437
|
+
}
|
|
438
|
+
/** Index of the quote closing the one at position 0, or -1. */
|
|
439
|
+
function closingQuoteIndex(input) {
|
|
440
|
+
for (let i = 1; i < input.length; i++) {
|
|
441
|
+
if (input[i] === "\\") {
|
|
442
|
+
i++;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (input[i] === '"')
|
|
446
|
+
return i;
|
|
447
|
+
}
|
|
448
|
+
return -1;
|
|
449
|
+
}
|
|
450
|
+
/** A single DNS label: alphanumerics and inner hyphens, 1..63 chars. */
|
|
451
|
+
function isDnsLabel(label) {
|
|
452
|
+
if (label.length === 0 || label.length > 63)
|
|
453
|
+
return false;
|
|
454
|
+
if (label.startsWith("-") || label.endsWith("-"))
|
|
455
|
+
return false;
|
|
456
|
+
return /^[a-zA-Z0-9-]+$/.test(label);
|
|
457
|
+
}
|
|
458
|
+
/** Bracketed IP domain literal — `[192.168.0.1]` or `[IPv6:::1]`. */
|
|
459
|
+
function isIpDomainLiteral(domain) {
|
|
460
|
+
if (!domain.startsWith("[") || !domain.endsWith("]"))
|
|
461
|
+
return false;
|
|
462
|
+
const inner = domain.slice(1, -1);
|
|
463
|
+
if (inner.toLowerCase().startsWith("ipv6:")) {
|
|
464
|
+
return isIpAddress(inner.slice(5), 6);
|
|
465
|
+
}
|
|
466
|
+
return isIpAddress(inner, 4);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Validate an email address.
|
|
470
|
+
*
|
|
471
|
+
* Deliberately structural rather than one giant regex: the length caps, the
|
|
472
|
+
* quoted local part and the IP-literal domain are separate rules in RFC 5321,
|
|
473
|
+
* and a single pattern that tries to express all of them is the classic source
|
|
474
|
+
* of both false accepts and false rejects.
|
|
475
|
+
*/
|
|
476
|
+
export function isEmail(value, options = {}) {
|
|
477
|
+
let candidate = value;
|
|
478
|
+
// Bound the input BEFORE any parsing. A caller may opt out of the 254-char
|
|
479
|
+
// address cap, but never out of a bound: an unbounded string reaching the
|
|
480
|
+
// parser is how a validator becomes an outage. RFC 5322 caps a whole line
|
|
481
|
+
// at 998 octets, so a display-name form has no business being longer.
|
|
482
|
+
if (value.length > MAX_EMAIL_INPUT)
|
|
483
|
+
return false;
|
|
484
|
+
if (options.allow_display_name) {
|
|
485
|
+
const address = displayNameAddress(candidate);
|
|
486
|
+
if (address !== null)
|
|
487
|
+
candidate = address;
|
|
488
|
+
}
|
|
489
|
+
else if (/[<>]/.test(candidate)) {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
if (!options.ignore_max_length && candidate.length > 254)
|
|
493
|
+
return false;
|
|
494
|
+
const at = candidate.lastIndexOf("@");
|
|
495
|
+
if (at < 1 || at === candidate.length - 1)
|
|
496
|
+
return false;
|
|
497
|
+
const local = candidate.slice(0, at);
|
|
498
|
+
const domain = candidate.slice(at + 1);
|
|
499
|
+
if (options.blacklisted_chars) {
|
|
500
|
+
for (const char of options.blacklisted_chars) {
|
|
501
|
+
if (local.includes(char))
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const quoted = QUOTED_LOCAL_RE.test(local);
|
|
506
|
+
if (!quoted && !DOT_ATOM_RE.test(local))
|
|
507
|
+
return false;
|
|
508
|
+
if (!options.ignore_max_length && local.length > 64)
|
|
509
|
+
return false;
|
|
510
|
+
if (domain.startsWith("[")) {
|
|
511
|
+
return options.allow_ip_domain === true && isIpDomainLiteral(domain);
|
|
512
|
+
}
|
|
513
|
+
const labels = domain.split(".");
|
|
514
|
+
if (options.require_tld !== false) {
|
|
515
|
+
if (labels.length < 2)
|
|
516
|
+
return false;
|
|
517
|
+
// A TLD is alphabetic and at least two characters.
|
|
518
|
+
const tld = labels[labels.length - 1];
|
|
519
|
+
if (tld.length < 2 || !/^[a-zA-Z]+$/.test(tld))
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
if (!labels.every(isDnsLabel))
|
|
523
|
+
return false;
|
|
524
|
+
if (options.domain_specific_validation &&
|
|
525
|
+
GMAIL_DOMAINS.has(domain.toLowerCase())) {
|
|
526
|
+
// Gmail: 6..30 chars, letters/digits/dots only, no leading/trailing dot,
|
|
527
|
+
// no doubled dot — and dots are ignored for the length check.
|
|
528
|
+
const username = local.split("+")[0];
|
|
529
|
+
if (!/^[a-zA-Z0-9.]+$/.test(username))
|
|
530
|
+
return false;
|
|
531
|
+
if (username.startsWith(".") || username.endsWith("."))
|
|
532
|
+
return false;
|
|
533
|
+
if (username.includes(".."))
|
|
534
|
+
return false;
|
|
535
|
+
const bare = username.replace(/\./g, "");
|
|
536
|
+
if (bare.length < 6 || bare.length > 30)
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* VAT number patterns, per country. `check` runs the country's own checksum
|
|
543
|
+
* when there is a short, well-defined one; countries without a `check` are
|
|
544
|
+
* validated on FORMAT only, and that is stated rather than implied.
|
|
545
|
+
*/
|
|
546
|
+
const VAT_RULES = {
|
|
547
|
+
// mod-97 on the 9 leading digits (the two check digits are the last two).
|
|
548
|
+
BE: {
|
|
549
|
+
pattern: /^BE0?\d{9}$/i,
|
|
550
|
+
check: (d) => mod97(d.slice(0, 8)) === Number(d.slice(8, 10)),
|
|
551
|
+
},
|
|
552
|
+
FR: { pattern: /^FR[0-9A-Z]{2}\d{9}$/i },
|
|
553
|
+
DE: { pattern: /^DE\d{9}$/i, check: (d) => germanChecksum(d) },
|
|
554
|
+
NL: { pattern: /^NL\d{9}B\d{2}$/i, check: (d) => dutchChecksum(d) },
|
|
555
|
+
IT: { pattern: /^IT\d{11}$/i, check: (d) => luhnLike(d) },
|
|
556
|
+
ES: { pattern: /^ES[0-9A-Z]\d{7}[0-9A-Z]$/i },
|
|
557
|
+
PT: { pattern: /^PT\d{9}$/i, check: (d) => mod11(d) },
|
|
558
|
+
LU: {
|
|
559
|
+
pattern: /^LU\d{8}$/i,
|
|
560
|
+
check: (d) => Number(d.slice(0, 6)) % 89 === Number(d.slice(6, 8)),
|
|
561
|
+
},
|
|
562
|
+
AT: { pattern: /^ATU\d{8}$/i },
|
|
563
|
+
DK: { pattern: /^DK\d{8}$/i },
|
|
564
|
+
FI: { pattern: /^FI\d{8}$/i },
|
|
565
|
+
SE: { pattern: /^SE\d{12}$/i },
|
|
566
|
+
IE: { pattern: /^IE(?:\d{7}[A-W]{1,2}|\d[A-Z+*]\d{5}[A-W])$/i },
|
|
567
|
+
PL: { pattern: /^PL\d{10}$/i },
|
|
568
|
+
CZ: { pattern: /^CZ\d{8,10}$/i },
|
|
569
|
+
SK: { pattern: /^SK\d{10}$/i },
|
|
570
|
+
GR: { pattern: /^(?:EL|GR)\d{9}$/i },
|
|
571
|
+
HU: { pattern: /^HU\d{8}$/i },
|
|
572
|
+
RO: { pattern: /^RO\d{2,10}$/i },
|
|
573
|
+
BG: { pattern: /^BG\d{9,10}$/i },
|
|
574
|
+
HR: { pattern: /^HR\d{11}$/i },
|
|
575
|
+
SI: { pattern: /^SI\d{8}$/i },
|
|
576
|
+
EE: { pattern: /^EE\d{9}$/i },
|
|
577
|
+
LV: { pattern: /^LV\d{11}$/i },
|
|
578
|
+
LT: { pattern: /^LT(?:\d{9}|\d{12})$/i },
|
|
579
|
+
MT: { pattern: /^MT\d{8}$/i },
|
|
580
|
+
CY: { pattern: /^CY\d{8}[A-Z]$/i },
|
|
581
|
+
GB: { pattern: /^GB(?:\d{9}|\d{12}|GD\d{3}|HA\d{3})$/i },
|
|
582
|
+
CH: {
|
|
583
|
+
pattern: /^CHE\d{9}(?:TVA|MWST|IVA)?$/i,
|
|
584
|
+
check: (d) => swissUidChecksum(d),
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
/** Countries `vat()` can check. */
|
|
588
|
+
export const SUPPORTED_VAT_COUNTRIES = Object.keys(VAT_RULES);
|
|
589
|
+
/** Plain mod-97 over a digit string. */
|
|
590
|
+
function mod97(digits) {
|
|
591
|
+
let remainder = 0;
|
|
592
|
+
for (const digit of digits) {
|
|
593
|
+
remainder = (remainder * 10 + (digit.charCodeAt(0) - 48)) % 97;
|
|
594
|
+
}
|
|
595
|
+
return 97 - remainder;
|
|
596
|
+
}
|
|
597
|
+
/** ISO 7064 mod-11 used by the Portuguese NIF. */
|
|
598
|
+
function mod11(digits) {
|
|
599
|
+
let sum = 0;
|
|
600
|
+
for (let i = 0; i < 8; i++) {
|
|
601
|
+
sum += (digits.charCodeAt(i) - 48) * (9 - i);
|
|
602
|
+
}
|
|
603
|
+
const check = 11 - (sum % 11);
|
|
604
|
+
const expected = check >= 10 ? 0 : check;
|
|
605
|
+
return expected === digits.charCodeAt(8) - 48;
|
|
606
|
+
}
|
|
607
|
+
/** German USt-IdNr. checksum (the "11-test" defined by the Bundeszentralamt). */
|
|
608
|
+
function germanChecksum(digits) {
|
|
609
|
+
let product = 10;
|
|
610
|
+
for (let i = 0; i < 8; i++) {
|
|
611
|
+
const digit = digits.charCodeAt(i) - 48;
|
|
612
|
+
let sum = (digit + product) % 10;
|
|
613
|
+
if (sum === 0)
|
|
614
|
+
sum = 10;
|
|
615
|
+
product = (2 * sum) % 11;
|
|
616
|
+
}
|
|
617
|
+
const check = 11 - product;
|
|
618
|
+
return (check === 10 ? 0 : check) === digits.charCodeAt(8) - 48;
|
|
619
|
+
}
|
|
620
|
+
/** Dutch BTW checksum: weighted 9..2 mod 11 over the first 8 digits. */
|
|
621
|
+
function dutchChecksum(digits) {
|
|
622
|
+
let sum = 0;
|
|
623
|
+
for (let i = 0; i < 8; i++) {
|
|
624
|
+
sum += (digits.charCodeAt(i) - 48) * (9 - i);
|
|
625
|
+
}
|
|
626
|
+
return sum % 11 === digits.charCodeAt(8) - 48;
|
|
627
|
+
}
|
|
628
|
+
/** Italian partita IVA: Luhn over 11 digits. */
|
|
629
|
+
function luhnLike(digits) {
|
|
630
|
+
let sum = 0;
|
|
631
|
+
for (let i = 0; i < 11; i++) {
|
|
632
|
+
let digit = digits.charCodeAt(i) - 48;
|
|
633
|
+
if (i % 2 === 1) {
|
|
634
|
+
digit *= 2;
|
|
635
|
+
if (digit > 9)
|
|
636
|
+
digit -= 9;
|
|
637
|
+
}
|
|
638
|
+
sum += digit;
|
|
639
|
+
}
|
|
640
|
+
return sum % 10 === 0;
|
|
641
|
+
}
|
|
642
|
+
/** Swiss UID (CHE): weights 5,4,3,2,7,6,5,4 mod 11. */
|
|
643
|
+
function swissUidChecksum(digits) {
|
|
644
|
+
const weights = [5, 4, 3, 2, 7, 6, 5, 4];
|
|
645
|
+
let sum = 0;
|
|
646
|
+
for (let i = 0; i < 8; i++) {
|
|
647
|
+
sum += (digits.charCodeAt(i) - 48) * weights[i];
|
|
648
|
+
}
|
|
649
|
+
const remainder = sum % 11;
|
|
650
|
+
if (remainder === 10)
|
|
651
|
+
return false;
|
|
652
|
+
const check = remainder === 0 ? 0 : 11 - remainder;
|
|
653
|
+
return check === digits.charCodeAt(8) - 48;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Validate a VAT number for one country. Returns `null` when the country has no
|
|
657
|
+
* rule, so the caller can fail LOUDLY instead of accepting the value.
|
|
658
|
+
*/
|
|
659
|
+
export function isVat(value, countryCode) {
|
|
660
|
+
const rule = VAT_RULES[countryCode.toUpperCase()];
|
|
661
|
+
if (rule === undefined)
|
|
662
|
+
return null;
|
|
663
|
+
const normalized = value.replace(/[\s.-]/g, "").toUpperCase();
|
|
664
|
+
if (!rule.pattern.test(normalized))
|
|
665
|
+
return false;
|
|
666
|
+
if (!rule.check)
|
|
667
|
+
return true;
|
|
668
|
+
const digits = normalized.replace(/[^0-9]/g, "");
|
|
669
|
+
return rule.check(digits);
|
|
670
|
+
}
|
|
671
|
+
//# sourceMappingURL=formats.js.map
|