@azure-net/node-kit 0.0.0-stage → 0.1.1
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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +301 -2
- package/VALIDATION.md +866 -0
- package/dist/delivery/http/ResponseBuilder.d.ts +44 -0
- package/dist/delivery/http/ResponseBuilder.js +135 -0
- package/dist/delivery/http/index.d.ts +1 -0
- package/dist/delivery/http/index.js +1 -0
- package/dist/delivery/index.d.ts +1 -0
- package/dist/delivery/index.js +1 -0
- package/dist/domain/Ensure.d.ts +2 -0
- package/dist/domain/Ensure.js +7 -0
- package/dist/domain/EnsureError.d.ts +4 -0
- package/dist/domain/EnsureError.js +7 -0
- package/dist/domain/index.d.ts +2 -0
- package/dist/domain/index.js +2 -0
- package/dist/infrastructure/background/BackgroundTask.d.ts +12 -0
- package/dist/infrastructure/background/BackgroundTask.js +41 -0
- package/dist/infrastructure/background/index.d.ts +1 -0
- package/dist/infrastructure/background/index.js +1 -0
- package/dist/infrastructure/environment/Env.d.ts +19 -0
- package/dist/infrastructure/environment/Env.js +33 -0
- package/dist/infrastructure/environment/index.d.ts +1 -0
- package/dist/infrastructure/environment/index.js +1 -0
- package/dist/infrastructure/index.d.ts +2 -0
- package/dist/infrastructure/index.js +2 -0
- package/dist/tools/DateHelper.d.ts +13 -0
- package/dist/tools/DateHelper.js +77 -0
- package/dist/tools/DurationHelper.d.ts +6 -0
- package/dist/tools/DurationHelper.js +31 -0
- package/dist/tools/EnumHelper.d.ts +26 -0
- package/dist/tools/EnumHelper.js +40 -0
- package/dist/tools/ErrorHelper.d.ts +3 -0
- package/dist/tools/ErrorHelper.js +31 -0
- package/dist/tools/NumberHelper.d.ts +19 -0
- package/dist/tools/NumberHelper.js +49 -0
- package/dist/tools/ObjectHelper.d.ts +28 -0
- package/dist/tools/ObjectHelper.js +90 -0
- package/dist/tools/PromiseHelper.d.ts +3 -0
- package/dist/tools/PromiseHelper.js +7 -0
- package/dist/tools/StringHelper.d.ts +75 -0
- package/dist/tools/StringHelper.js +127 -0
- package/dist/tools/UidHelper.d.ts +56 -0
- package/dist/tools/UidHelper.js +132 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +9 -0
- package/dist/validation/Engine.d.ts +6 -0
- package/dist/validation/Engine.js +406 -0
- package/dist/validation/Result.d.ts +31 -0
- package/dist/validation/Result.js +264 -0
- package/dist/validation/Symbols.d.ts +1 -0
- package/dist/validation/Symbols.js +1 -0
- package/dist/validation/Types.d.ts +105 -0
- package/dist/validation/Types.js +1 -0
- package/dist/validation/Validator.d.ts +26 -0
- package/dist/validation/Validator.js +95 -0
- package/dist/validation/index.d.ts +5 -0
- package/dist/validation/index.js +5 -0
- package/dist/validation/messages/En.d.ts +3 -0
- package/dist/validation/messages/En.js +118 -0
- package/dist/validation/messages/Ru.d.ts +3 -0
- package/dist/validation/messages/Ru.js +118 -0
- package/dist/validation/messages/index.d.ts +7 -0
- package/dist/validation/messages/index.js +7 -0
- package/dist/validation/rules/CustomRules.d.ts +8 -0
- package/dist/validation/rules/CustomRules.js +64 -0
- package/dist/validation/rules/Factory.d.ts +4 -0
- package/dist/validation/rules/Factory.js +21 -0
- package/dist/validation/rules/StringRules.d.ts +105 -0
- package/dist/validation/rules/StringRules.js +481 -0
- package/dist/validation/rules/StructureRules.d.ts +30 -0
- package/dist/validation/rules/StructureRules.js +145 -0
- package/dist/validation/rules/ValueRules.d.ts +82 -0
- package/dist/validation/rules/ValueRules.js +365 -0
- package/dist/validation/rules/index.d.ts +151 -0
- package/dist/validation/rules/index.js +19 -0
- package/package.json +77 -3
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { sizeOption, testRule } from "./Factory.js";
|
|
3
|
+
function checkOptions(options) {
|
|
4
|
+
if (!options || typeof options !== "object" || Array.isArray(options))
|
|
5
|
+
throw new TypeError("Options must be an object.");
|
|
6
|
+
for (const key of ["code", "messageKey", "label"]) {
|
|
7
|
+
if (options[key] !== undefined && typeof options[key] !== "string")
|
|
8
|
+
throw new TypeError(`${key} must be a string.`);
|
|
9
|
+
}
|
|
10
|
+
if (options.message !== undefined &&
|
|
11
|
+
typeof options.message !== "string" &&
|
|
12
|
+
typeof options.message !== "function") {
|
|
13
|
+
throw new TypeError("message must be a string or function.");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function format(code, predicate, options = {}, params = {}) {
|
|
17
|
+
checkOptions(options);
|
|
18
|
+
return testRule(code, (value) => typeof value === "string" && predicate(value), options, params);
|
|
19
|
+
}
|
|
20
|
+
// Full matches use an absolute end assertion: JavaScript's $ also accepts a final newline.
|
|
21
|
+
function full(source, flags = "") {
|
|
22
|
+
return new RegExp(`^(?:${source})(?![\\s\\S])`, flags);
|
|
23
|
+
}
|
|
24
|
+
// Fixed, non-global patterns are shared; neither input data nor match results are cached.
|
|
25
|
+
const fixed = {
|
|
26
|
+
hostnameLabel: full("[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", "i"),
|
|
27
|
+
emailAtom: full("[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+"),
|
|
28
|
+
base64: full("[A-Za-z0-9+/]*={0,2}"),
|
|
29
|
+
base64url: full("[A-Za-z0-9_-]*"),
|
|
30
|
+
prefix: full("0|[1-9][0-9]{0,2}"),
|
|
31
|
+
date: full("[0-9]{4}-[0-9]{2}-[0-9]{2}"),
|
|
32
|
+
time: full("([0-9]{2}):([0-9]{2})(?::([0-9]{2})(?:\\.([0-9]+))?)?"),
|
|
33
|
+
nanoid: full("[A-Za-z0-9_-]+"),
|
|
34
|
+
hashHex: full("[0-9a-f]+", "i"),
|
|
35
|
+
creditCard: full("[0-9](?:[ -]?[0-9]){11,18}"),
|
|
36
|
+
iban: full("[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}"),
|
|
37
|
+
weeks: full("P[0-9]+W"),
|
|
38
|
+
duration: full("P([0-9]+Y)?([0-9]+M)?([0-9]+D)?(?:T([0-9]+H)?([0-9]+M)?([0-9]+(?:[.,][0-9]+)?S)?)?"),
|
|
39
|
+
};
|
|
40
|
+
function pattern(code, source, options, flags = "") {
|
|
41
|
+
const regex = full(source, flags);
|
|
42
|
+
return format(code, (value) => regex.test(value), options);
|
|
43
|
+
}
|
|
44
|
+
function snapshotRegex(regex) {
|
|
45
|
+
if (!(regex instanceof RegExp))
|
|
46
|
+
throw new TypeError("Expected a RegExp.");
|
|
47
|
+
const copy = new RegExp(regex.source, regex.flags);
|
|
48
|
+
return (value) => {
|
|
49
|
+
copy.lastIndex = 0;
|
|
50
|
+
return copy.test(value);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function textArgument(value) {
|
|
54
|
+
if (typeof value !== "string")
|
|
55
|
+
throw new TypeError("Expected a string.");
|
|
56
|
+
}
|
|
57
|
+
function lengthRule(code, bound, options, compare) {
|
|
58
|
+
checkOptions(options);
|
|
59
|
+
sizeOption(bound, code);
|
|
60
|
+
const unit = options.unit === undefined ? "codePoints" : options.unit;
|
|
61
|
+
if (unit !== "codePoints" && unit !== "codeUnits")
|
|
62
|
+
throw new TypeError("Invalid length unit.");
|
|
63
|
+
return testRule(code, (value) => {
|
|
64
|
+
if (typeof value === "string")
|
|
65
|
+
return compare(unit === "codePoints" ? Array.from(value).length : value.length);
|
|
66
|
+
if (Array.isArray(value))
|
|
67
|
+
return compare(value.length);
|
|
68
|
+
if (value instanceof Map || value instanceof Set)
|
|
69
|
+
return compare(value.size);
|
|
70
|
+
return false;
|
|
71
|
+
}, options, { [code]: bound, unit });
|
|
72
|
+
}
|
|
73
|
+
function validHostname(value) {
|
|
74
|
+
const name = value.endsWith(".") ? value.slice(0, -1) : value;
|
|
75
|
+
return (name.length > 0 &&
|
|
76
|
+
name.length <= 253 &&
|
|
77
|
+
name
|
|
78
|
+
.split(".")
|
|
79
|
+
.every((label) => label.length <= 63 && fixed.hostnameLabel.test(label)));
|
|
80
|
+
}
|
|
81
|
+
function urlRule(code, options, httpOnly) {
|
|
82
|
+
checkOptions(options);
|
|
83
|
+
const protocol = options.protocol === undefined
|
|
84
|
+
? undefined
|
|
85
|
+
: snapshotRegex(options.protocol);
|
|
86
|
+
const hostname = options.hostname === undefined
|
|
87
|
+
? undefined
|
|
88
|
+
: snapshotRegex(options.hostname);
|
|
89
|
+
return format(code, (value) => {
|
|
90
|
+
// WHATWG URL silently removes whitespace and repairs backslashes; do not normalize input.
|
|
91
|
+
if (/[\s\u0000-\u001f\u007f\\]/u.test(value))
|
|
92
|
+
return false;
|
|
93
|
+
try {
|
|
94
|
+
const url = new URL(value);
|
|
95
|
+
if (httpOnly &&
|
|
96
|
+
(!/^https?:\/\/[^/?#]/i.test(value) ||
|
|
97
|
+
!["http:", "https:"].includes(url.protocol) ||
|
|
98
|
+
!url.hostname))
|
|
99
|
+
return false;
|
|
100
|
+
return ((!protocol || protocol(url.protocol.slice(0, -1))) &&
|
|
101
|
+
(!hostname || hostname(url.hostname)));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}, options, {
|
|
107
|
+
protocol: options.protocol?.toString(),
|
|
108
|
+
hostname: options.hostname?.toString(),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function canonicalBase64(value, url) {
|
|
112
|
+
if (!(url ? fixed.base64url : fixed.base64).test(value))
|
|
113
|
+
return false;
|
|
114
|
+
const encoding = url ? "base64url" : "base64";
|
|
115
|
+
return Buffer.from(value, encoding).toString(encoding) === value;
|
|
116
|
+
}
|
|
117
|
+
function ip(value, version) {
|
|
118
|
+
// Zone identifiers describe an interface, not the address itself.
|
|
119
|
+
if (value.includes("%"))
|
|
120
|
+
return false;
|
|
121
|
+
const actual = isIP(value);
|
|
122
|
+
return version === undefined ? actual !== 0 : actual === version;
|
|
123
|
+
}
|
|
124
|
+
function cidr(value, version) {
|
|
125
|
+
const parts = value.split("/");
|
|
126
|
+
if (parts.length !== 2)
|
|
127
|
+
return false;
|
|
128
|
+
const [address, prefix] = parts;
|
|
129
|
+
return (ip(address, version) &&
|
|
130
|
+
fixed.prefix.test(prefix) &&
|
|
131
|
+
Number(prefix) <= (isIP(address) === 4 ? 32 : 128));
|
|
132
|
+
}
|
|
133
|
+
function validDate(value) {
|
|
134
|
+
if (!fixed.date.test(value))
|
|
135
|
+
return false;
|
|
136
|
+
const year = Number(value.slice(0, 4)), month = Number(value.slice(5, 7)), day = Number(value.slice(8));
|
|
137
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
138
|
+
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
139
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= days[month - 1];
|
|
140
|
+
}
|
|
141
|
+
function precisionOption(options) {
|
|
142
|
+
checkOptions(options);
|
|
143
|
+
const precision = options.precision ?? null;
|
|
144
|
+
if (precision !== null &&
|
|
145
|
+
(!Number.isSafeInteger(precision) || precision < -1))
|
|
146
|
+
throw new RangeError("precision must be -1, null, or a non-negative safe integer.");
|
|
147
|
+
return precision;
|
|
148
|
+
}
|
|
149
|
+
function validTime(value, precision, requireSeconds) {
|
|
150
|
+
const match = fixed.time.exec(value);
|
|
151
|
+
if (!match ||
|
|
152
|
+
Number(match[1]) > 23 ||
|
|
153
|
+
Number(match[2]) > 59 ||
|
|
154
|
+
(match[3] !== undefined && Number(match[3]) > 59))
|
|
155
|
+
return false;
|
|
156
|
+
if (precision === -1)
|
|
157
|
+
return match[3] === undefined;
|
|
158
|
+
if (requireSeconds && match[3] === undefined)
|
|
159
|
+
return false;
|
|
160
|
+
if (precision === null)
|
|
161
|
+
return true;
|
|
162
|
+
return match[3] !== undefined && (match[4]?.length ?? 0) === precision;
|
|
163
|
+
}
|
|
164
|
+
const hashBytes = {
|
|
165
|
+
md5: 16,
|
|
166
|
+
sha1: 20,
|
|
167
|
+
sha256: 32,
|
|
168
|
+
sha384: 48,
|
|
169
|
+
sha512: 64,
|
|
170
|
+
};
|
|
171
|
+
let currencies;
|
|
172
|
+
/**
|
|
173
|
+
* Noncoercive format checks. Format scope was reviewed against Zod 4.6.7's
|
|
174
|
+
* src/v4/core/{regexes,schemas}.ts (MIT, Colin McDonnell and contributors).
|
|
175
|
+
* Implementations below are independent; no Zod runtime dependency is required.
|
|
176
|
+
*/
|
|
177
|
+
export const stringRules = {
|
|
178
|
+
string(options = {}) {
|
|
179
|
+
checkOptions(options);
|
|
180
|
+
return testRule("string", (value) => typeof value === "string", options, {}, true);
|
|
181
|
+
},
|
|
182
|
+
/** Strings count Unicode code points by default, arrays count elements, maps/sets count entries. */
|
|
183
|
+
length(length, options = {}) {
|
|
184
|
+
return lengthRule("length", length, options, (size) => size === length);
|
|
185
|
+
},
|
|
186
|
+
minLength(minLength, options = {}) {
|
|
187
|
+
return lengthRule("minLength", minLength, options, (size) => size >= minLength);
|
|
188
|
+
},
|
|
189
|
+
maxLength(maxLength, options = {}) {
|
|
190
|
+
return lengthRule("maxLength", maxLength, options, (size) => size <= maxLength);
|
|
191
|
+
},
|
|
192
|
+
/** State-safe, not ReDoS-safe: callers are responsible for their pattern's complexity. */
|
|
193
|
+
matches(regex, options = {}) {
|
|
194
|
+
const test = snapshotRegex(regex);
|
|
195
|
+
return format("matches", test, options, {
|
|
196
|
+
pattern: regex.source,
|
|
197
|
+
flags: regex.flags,
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
includes(substring, options = {}) {
|
|
201
|
+
textArgument(substring);
|
|
202
|
+
return format("includes", (value) => value.includes(substring), options, {
|
|
203
|
+
substring,
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
startsWith(prefix, options = {}) {
|
|
207
|
+
textArgument(prefix);
|
|
208
|
+
return format("startsWith", (value) => value.startsWith(prefix), options, {
|
|
209
|
+
prefix,
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
endsWith(suffix, options = {}) {
|
|
213
|
+
textArgument(suffix);
|
|
214
|
+
return format("endsWith", (value) => value.endsWith(suffix), options, {
|
|
215
|
+
suffix,
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
lowercase(options = {}) {
|
|
219
|
+
return format("lowercase", (value) => value === value.toLowerCase(), options);
|
|
220
|
+
},
|
|
221
|
+
uppercase(options = {}) {
|
|
222
|
+
return format("uppercase", (value) => value === value.toUpperCase(), options);
|
|
223
|
+
},
|
|
224
|
+
/** ASCII dot-atom mailbox with a dotted DNS domain; not all RFC 5322 mailboxes. */
|
|
225
|
+
email(options = {}) {
|
|
226
|
+
return format("email", (value) => {
|
|
227
|
+
if (value.length > 254)
|
|
228
|
+
return false;
|
|
229
|
+
const parts = value.split("@");
|
|
230
|
+
if (parts.length !== 2)
|
|
231
|
+
return false;
|
|
232
|
+
const [local, domain] = parts;
|
|
233
|
+
return (local.length <= 64 &&
|
|
234
|
+
local.split(".").every((atom) => fixed.emailAtom.test(atom)) &&
|
|
235
|
+
domain.includes(".") &&
|
|
236
|
+
!domain.endsWith(".") &&
|
|
237
|
+
validHostname(domain) &&
|
|
238
|
+
!isIP(domain) &&
|
|
239
|
+
/[a-z]/i.test(domain.split(".").at(-1)));
|
|
240
|
+
}, options);
|
|
241
|
+
},
|
|
242
|
+
/** RFC 9562 versions 1-8 and, without a version filter, Nil/Max UUIDs. */
|
|
243
|
+
uuid(options = {}) {
|
|
244
|
+
checkOptions(options);
|
|
245
|
+
const version = options.version;
|
|
246
|
+
if (version !== undefined &&
|
|
247
|
+
(!Number.isInteger(version) || version < 1 || version > 8))
|
|
248
|
+
throw new RangeError("UUID version must be 1 through 8.");
|
|
249
|
+
const normal = full(`[0-9a-f]{8}-[0-9a-f]{4}-${version ?? "[1-8]"}[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}`, "i");
|
|
250
|
+
return format("uuid", (value) => normal.test(value) ||
|
|
251
|
+
(version === undefined &&
|
|
252
|
+
(value === "00000000-0000-0000-0000-000000000000" ||
|
|
253
|
+
value.toLowerCase() === "ffffffff-ffff-ffff-ffff-ffffffffffff")), options, { version: version ?? "any" });
|
|
254
|
+
},
|
|
255
|
+
guid(options = {}) {
|
|
256
|
+
return pattern("guid", "[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}", options, "i");
|
|
257
|
+
},
|
|
258
|
+
/** Absolute WHATWG URLs, including non-HTTP schemes; not a safe-URL/security policy. */
|
|
259
|
+
url(options = {}) {
|
|
260
|
+
return urlRule("url", options, false);
|
|
261
|
+
},
|
|
262
|
+
httpUrl(options = {}) {
|
|
263
|
+
return urlRule("httpUrl", options, true);
|
|
264
|
+
},
|
|
265
|
+
/** ASCII DNS labels, optionally terminated by a root dot; use punycode for IDNs. */
|
|
266
|
+
hostname(options = {}) {
|
|
267
|
+
return format("hostname", validHostname, options);
|
|
268
|
+
},
|
|
269
|
+
/** International '+' notation with 2-15 digits; does not verify allocation or reachability. */
|
|
270
|
+
e164(options = {}) {
|
|
271
|
+
return pattern("e164", "\\+[1-9][0-9]{1,14}", options);
|
|
272
|
+
},
|
|
273
|
+
/** Common emoji sequences, not a versioned Unicode RGI membership check. */
|
|
274
|
+
emoji(options = {}) {
|
|
275
|
+
const base = "\\p{Extended_Pictographic}\\uFE0F?\\p{Emoji_Modifier}?";
|
|
276
|
+
return pattern("emoji", `(?:\\p{Regional_Indicator}{2}|[0-9#*]\\uFE0F?\\u20E3|${base}(?:\\u200D${base})*(?:[\\u{E0020}-\\u{E007E}]+\\u{E007F})?)+`, options, "u");
|
|
277
|
+
},
|
|
278
|
+
/** Canonical padded RFC 4648 encoding (empty input is valid). */
|
|
279
|
+
base64(options = {}) {
|
|
280
|
+
return format("base64", (value) => canonicalBase64(value, false), options);
|
|
281
|
+
},
|
|
282
|
+
/** Canonical unpadded RFC 4648 URL encoding (empty input is valid). */
|
|
283
|
+
base64url(options = {}) {
|
|
284
|
+
return format("base64url", (value) => canonicalBase64(value, true), options);
|
|
285
|
+
},
|
|
286
|
+
hex(options = {}) {
|
|
287
|
+
return pattern("hex", "[0-9a-f]*", options, "i");
|
|
288
|
+
},
|
|
289
|
+
/** Compact signed JWT syntax only: no signature, claim, expiry, or algorithm-security verification. */
|
|
290
|
+
jwt(options = {}) {
|
|
291
|
+
checkOptions(options);
|
|
292
|
+
const algorithm = options.algorithm;
|
|
293
|
+
if (algorithm !== undefined &&
|
|
294
|
+
(typeof algorithm !== "string" || !algorithm || algorithm === "none"))
|
|
295
|
+
throw new TypeError("algorithm must name a signing algorithm.");
|
|
296
|
+
return format("jwt", (value) => {
|
|
297
|
+
const parts = value.split(".");
|
|
298
|
+
if (parts.length !== 3 ||
|
|
299
|
+
!parts.every((part) => part.length > 0 && canonicalBase64(part, true)))
|
|
300
|
+
return false;
|
|
301
|
+
try {
|
|
302
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
303
|
+
const header = JSON.parse(decoder.decode(Buffer.from(parts[0], "base64url")));
|
|
304
|
+
const payload = JSON.parse(decoder.decode(Buffer.from(parts[1], "base64url")));
|
|
305
|
+
if (!header ||
|
|
306
|
+
typeof header !== "object" ||
|
|
307
|
+
Array.isArray(header) ||
|
|
308
|
+
!payload ||
|
|
309
|
+
typeof payload !== "object" ||
|
|
310
|
+
Array.isArray(payload))
|
|
311
|
+
return false;
|
|
312
|
+
const h = header;
|
|
313
|
+
return (typeof h.alg === "string" &&
|
|
314
|
+
h.alg.length > 0 &&
|
|
315
|
+
h.alg !== "none" &&
|
|
316
|
+
(algorithm === undefined || h.alg === algorithm) &&
|
|
317
|
+
(h.typ === undefined ||
|
|
318
|
+
(typeof h.typ === "string" && h.typ.toUpperCase() === "JWT")) &&
|
|
319
|
+
h.b64 !== false);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
}, options, { algorithm: algorithm ?? "any" });
|
|
325
|
+
},
|
|
326
|
+
nanoid(options = {}) {
|
|
327
|
+
checkOptions(options);
|
|
328
|
+
const length = options.length === undefined ? 21 : options.length;
|
|
329
|
+
sizeOption(length, "length");
|
|
330
|
+
if (!length)
|
|
331
|
+
throw new RangeError("Nano ID length must be positive.");
|
|
332
|
+
return format("nanoid", (value) => value.length === length && fixed.nanoid.test(value), options, { length });
|
|
333
|
+
},
|
|
334
|
+
/** Canonical 25-character CUID v1 shape, not proof of provenance. */
|
|
335
|
+
cuid(options = {}) {
|
|
336
|
+
return pattern("cuid", "c[0-9a-z]{24}", options);
|
|
337
|
+
},
|
|
338
|
+
/** CUID2 allows custom lengths: lowercase leading letter followed by lowercase alphanumerics. */
|
|
339
|
+
cuid2(options = {}) {
|
|
340
|
+
return pattern("cuid2", "[a-z][a-z0-9]*", options);
|
|
341
|
+
},
|
|
342
|
+
ulid(options = {}) {
|
|
343
|
+
return pattern("ulid", "[0-7][0-9A-HJKMNP-TV-Z]{25}", options, "i");
|
|
344
|
+
},
|
|
345
|
+
/** Zod-compatible 20-character base32hex shape, case-insensitive; no padding-bit or provenance check. */
|
|
346
|
+
xid(options = {}) {
|
|
347
|
+
return pattern("xid", "[0-9a-v]{20}", options, "i");
|
|
348
|
+
},
|
|
349
|
+
/** Zod-compatible 27-character base62 shape; no decoded 160-bit range or timestamp check. */
|
|
350
|
+
ksuid(options = {}) {
|
|
351
|
+
return pattern("ksuid", "[A-Za-z0-9]{27}", options);
|
|
352
|
+
},
|
|
353
|
+
ip(options = {}) {
|
|
354
|
+
return format("ip", (value) => ip(value), options);
|
|
355
|
+
},
|
|
356
|
+
ipv4(options = {}) {
|
|
357
|
+
return format("ipv4", (value) => ip(value, 4), options);
|
|
358
|
+
},
|
|
359
|
+
ipv6(options = {}) {
|
|
360
|
+
return format("ipv6", (value) => ip(value, 6), options);
|
|
361
|
+
},
|
|
362
|
+
/** CIDR address/prefix notation; host bits need not be zero. */
|
|
363
|
+
cidr(options = {}) {
|
|
364
|
+
return format("cidr", (value) => cidr(value), options);
|
|
365
|
+
},
|
|
366
|
+
cidrv4(options = {}) {
|
|
367
|
+
return format("cidrv4", (value) => cidr(value, 4), options);
|
|
368
|
+
},
|
|
369
|
+
cidrv6(options = {}) {
|
|
370
|
+
return format("cidrv6", (value) => cidr(value, 6), options);
|
|
371
|
+
},
|
|
372
|
+
mac(options = {}) {
|
|
373
|
+
checkOptions(options);
|
|
374
|
+
const delimiter = options.delimiter === undefined ? ":" : options.delimiter;
|
|
375
|
+
if (![":", "-", ""].includes(delimiter))
|
|
376
|
+
throw new TypeError("Invalid MAC delimiter.");
|
|
377
|
+
const regex = full(Array(6).fill("[0-9a-f]{2}").join(delimiter), "i");
|
|
378
|
+
return format("mac", (value) => regex.test(value), options, { delimiter });
|
|
379
|
+
},
|
|
380
|
+
/** 12-19 digit PAN with optional single space/hyphen separators and Luhn checksum, not issuer verification. */
|
|
381
|
+
creditCard(options = {}) {
|
|
382
|
+
return format("creditCard", (value) => {
|
|
383
|
+
if (!fixed.creditCard.test(value))
|
|
384
|
+
return false;
|
|
385
|
+
const digits = value.replace(/[ -]/g, "");
|
|
386
|
+
if (/^0+$/.test(digits))
|
|
387
|
+
return false;
|
|
388
|
+
let sum = 0;
|
|
389
|
+
for (let index = digits.length - 1, double = false; index >= 0; index--, double = !double) {
|
|
390
|
+
let digit = Number(digits[index]);
|
|
391
|
+
if (double) {
|
|
392
|
+
digit *= 2;
|
|
393
|
+
if (digit > 9)
|
|
394
|
+
digit -= 9;
|
|
395
|
+
}
|
|
396
|
+
sum += digit;
|
|
397
|
+
}
|
|
398
|
+
return sum % 10 === 0;
|
|
399
|
+
}, options);
|
|
400
|
+
},
|
|
401
|
+
/** Electronic IBAN shape and MOD-97 checksum; no country-specific BBAN or bank registry validation. */
|
|
402
|
+
iban(options = {}) {
|
|
403
|
+
return format("iban", (value) => {
|
|
404
|
+
if (!fixed.iban.test(value))
|
|
405
|
+
return false;
|
|
406
|
+
const check = Number(value.slice(2, 4));
|
|
407
|
+
if (check < 2 || check > 98)
|
|
408
|
+
return false;
|
|
409
|
+
let remainder = 0;
|
|
410
|
+
for (const char of value.slice(4) + value.slice(0, 4)) {
|
|
411
|
+
const numeric = char >= "A" ? char.charCodeAt(0) - 55 : Number(char);
|
|
412
|
+
remainder = (remainder * (numeric >= 10 ? 100 : 10) + numeric) % 97;
|
|
413
|
+
}
|
|
414
|
+
return remainder === 1;
|
|
415
|
+
}, options);
|
|
416
|
+
},
|
|
417
|
+
/** Uppercase currencies supported by the host's ICU data, not a live ISO 4217 registry. */
|
|
418
|
+
currencyCode(options = {}) {
|
|
419
|
+
currencies ??= new Set(Intl.supportedValuesOf("currency"));
|
|
420
|
+
return format("currencyCode", (value) => currencies.has(value), options);
|
|
421
|
+
},
|
|
422
|
+
hash(algorithm, options = {}) {
|
|
423
|
+
checkOptions(options);
|
|
424
|
+
if (!Object.hasOwn(hashBytes, algorithm))
|
|
425
|
+
throw new TypeError("Unsupported hash algorithm.");
|
|
426
|
+
const bytes = hashBytes[algorithm], encoding = options.encoding === undefined ? "hex" : options.encoding;
|
|
427
|
+
if (!["hex", "base64", "base64url"].includes(encoding))
|
|
428
|
+
throw new TypeError("Unsupported hash encoding.");
|
|
429
|
+
return format("hash", (value) => encoding === "hex"
|
|
430
|
+
? value.length === bytes * 2 && fixed.hashHex.test(value)
|
|
431
|
+
: canonicalBase64(value, encoding === "base64url") &&
|
|
432
|
+
Buffer.from(value, encoding).length === bytes, options, { algorithm, encoding });
|
|
433
|
+
},
|
|
434
|
+
/** Gregorian calendar YYYY-MM-DD, including astronomical year 0000. */
|
|
435
|
+
isoDate(options = {}) {
|
|
436
|
+
return format("isoDate", validDate, options);
|
|
437
|
+
},
|
|
438
|
+
/** Local HH:mm[:ss[.fraction]]; no leap seconds or 24:00. precision -1 means minutes. */
|
|
439
|
+
isoTime(options = {}) {
|
|
440
|
+
const precision = precisionOption(options);
|
|
441
|
+
return format("isoTime", (value) => validTime(value, precision, false), options, { precision });
|
|
442
|
+
},
|
|
443
|
+
/** UTC by default; explicit opt-ins for offsets and local time. Zoned forms require seconds unless precision=-1. */
|
|
444
|
+
isoDateTime(options = {}) {
|
|
445
|
+
const precision = precisionOption(options);
|
|
446
|
+
for (const key of ["offset", "local"]) {
|
|
447
|
+
if (options[key] !== undefined && typeof options[key] !== "boolean")
|
|
448
|
+
throw new TypeError(`${key} must be boolean.`);
|
|
449
|
+
}
|
|
450
|
+
const offset = options.offset ?? false, local = options.local ?? false;
|
|
451
|
+
return format("isoDateTime", (value) => {
|
|
452
|
+
if (value[10] !== "T" || !validDate(value.slice(0, 10)))
|
|
453
|
+
return false;
|
|
454
|
+
let time = value.slice(11), zoned = false;
|
|
455
|
+
if (time.endsWith("Z")) {
|
|
456
|
+
time = time.slice(0, -1);
|
|
457
|
+
zoned = true;
|
|
458
|
+
}
|
|
459
|
+
else if (/[+-]/.test(time)) {
|
|
460
|
+
if (!offset ||
|
|
461
|
+
!/[+-](?:[01][0-9]|2[0-3]):[0-5][0-9]$(?![\s\S])/.test(time))
|
|
462
|
+
return false;
|
|
463
|
+
time = time.slice(0, -6);
|
|
464
|
+
zoned = true;
|
|
465
|
+
}
|
|
466
|
+
return (zoned || local) && validTime(time, precision, zoned);
|
|
467
|
+
}, options, { precision, offset, local });
|
|
468
|
+
},
|
|
469
|
+
/** ISO 8601-1 subset: weeks alone, or ordered Y/M/D and H/M/S; fractions only on seconds. */
|
|
470
|
+
isoDuration(options = {}) {
|
|
471
|
+
return format("isoDuration", (value) => {
|
|
472
|
+
if (fixed.weeks.test(value))
|
|
473
|
+
return true;
|
|
474
|
+
const match = fixed.duration.exec(value);
|
|
475
|
+
return (!!match &&
|
|
476
|
+
match.slice(1).some((part) => part !== undefined) &&
|
|
477
|
+
(!value.includes("T") ||
|
|
478
|
+
match.slice(4).some((part) => part !== undefined)));
|
|
479
|
+
}, options);
|
|
480
|
+
},
|
|
481
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AnyRule, BuiltinRule, FieldRule, RuleOptions, ValidationRules } from "../Types.js";
|
|
2
|
+
import { validatorDefinition } from "../Symbols.js";
|
|
3
|
+
import type { Validator } from "../Validator.js";
|
|
4
|
+
export declare const structureRules: {
|
|
5
|
+
optional: () => BuiltinRule<undefined>;
|
|
6
|
+
nullable: () => BuiltinRule<null>;
|
|
7
|
+
nullish: () => BuiltinRule<null | undefined>;
|
|
8
|
+
object<T extends object, TRoot = unknown, C = unknown>(shape: ValidationRules<T, TRoot, C>, options?: RuleOptions & {
|
|
9
|
+
unknownKeys?: "reject" | "allow";
|
|
10
|
+
}): BuiltinRule<T, C, TRoot>;
|
|
11
|
+
array<T, TRoot = unknown, C = unknown>(items: readonly FieldRule<T, T[], TRoot, C>[], options?: RuleOptions): BuiltinRule<T[], C, TRoot>;
|
|
12
|
+
tuple<T extends readonly unknown[], TRoot = unknown, C = unknown>(items: { readonly [K in keyof T]: readonly FieldRule<T[K], T, TRoot, C>[]; }, options?: RuleOptions): BuiltinRule<T, C, TRoot>;
|
|
13
|
+
record<T, TRoot = unknown, C = unknown>(items: readonly FieldRule<T, Record<string, T>, TRoot, C>[], options?: RuleOptions, keys?: readonly FieldRule<string, Record<string, T>, TRoot, C>[]): BuiltinRule<Record<string, T>, C, TRoot>;
|
|
14
|
+
map<K, V, TRoot = unknown, C = unknown>(keys: readonly FieldRule<K, Map<K, V>, TRoot, C>[], items: readonly FieldRule<V, Map<K, V>, TRoot, C>[], options?: RuleOptions): BuiltinRule<Map<K, V>, C, TRoot>;
|
|
15
|
+
set<T, TRoot = unknown, C = unknown>(items: readonly FieldRule<T, Set<T>, TRoot, C>[], options?: RuleOptions): BuiltinRule<Set<T>, C, TRoot>;
|
|
16
|
+
union<T, D = unknown, TRoot = unknown, C = unknown>(alternatives: readonly (readonly FieldRule<T, D, TRoot, C>[])[], options?: RuleOptions): BuiltinRule<T, C, TRoot>;
|
|
17
|
+
discriminatedUnion<T, D = unknown, TRoot = unknown, C = unknown>(key: string, variants: Readonly<Record<string, readonly FieldRule<T, D, TRoot, C>[]>>, options?: RuleOptions): BuiltinRule<T, C, TRoot>;
|
|
18
|
+
intersection<A extends object, B extends object, TRoot = unknown, C = unknown>(left: ValidationRules<A, TRoot, C>, right: ValidationRules<B, TRoot, C>, options?: RuleOptions): BuiltinRule<A & B, C, TRoot>;
|
|
19
|
+
validator<T extends object, C>(validator: (new () => Validator<T, C>) & {
|
|
20
|
+
[validatorDefinition](): Readonly<Record<string, readonly AnyRule[]>>;
|
|
21
|
+
readonly configuration: {
|
|
22
|
+
readonly unknownKeys?: "reject" | "allow";
|
|
23
|
+
};
|
|
24
|
+
}, options?: RuleOptions): BuiltinRule<T, C>;
|
|
25
|
+
lazy<T, D = unknown, TRoot = unknown, C = unknown>(resolve: () => readonly FieldRule<T, D, TRoot, C>[], options?: RuleOptions): BuiltinRule<T, C, TRoot>;
|
|
26
|
+
unique(options?: RuleOptions): BuiltinRule<never>;
|
|
27
|
+
uniqueBy<T>(key: keyof T, options?: RuleOptions): BuiltinRule<never>;
|
|
28
|
+
sameAs(key: string, options?: RuleOptions): BuiltinRule<never>;
|
|
29
|
+
notSameAs(key: string, options?: RuleOptions): BuiltinRule<never>;
|
|
30
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { descriptor, testRule } from "./Factory.js";
|
|
2
|
+
import { validatorDefinition } from "../Symbols.js";
|
|
3
|
+
const list = (items) => Object.freeze([...items]);
|
|
4
|
+
function checkKeys(shape) {
|
|
5
|
+
if (Object.getOwnPropertySymbols(shape).length)
|
|
6
|
+
throw new TypeError("Validation schemas support string and numeric keys, not symbols.");
|
|
7
|
+
}
|
|
8
|
+
function structure(kind, options = {}, extra = {}) {
|
|
9
|
+
return descriptor({
|
|
10
|
+
kind,
|
|
11
|
+
code: `type.${kind}`,
|
|
12
|
+
structural: true,
|
|
13
|
+
options,
|
|
14
|
+
params: {},
|
|
15
|
+
...extra,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export const structureRules = {
|
|
19
|
+
optional: () => structure("optional"),
|
|
20
|
+
nullable: () => structure("nullable"),
|
|
21
|
+
nullish: () => structure("optional", {}, { params: { nullable: true } }),
|
|
22
|
+
object(shape, options = {}) {
|
|
23
|
+
checkKeys(shape);
|
|
24
|
+
const snapshot = Object.fromEntries(Object.entries(shape).map(([key, rules]) => [
|
|
25
|
+
key,
|
|
26
|
+
list(rules),
|
|
27
|
+
]));
|
|
28
|
+
return structure("object", options, {
|
|
29
|
+
shape: Object.freeze(snapshot),
|
|
30
|
+
unknownKeys: options.unknownKeys,
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
array(items, options = {}) {
|
|
34
|
+
return structure("array", options, { items: list(items) });
|
|
35
|
+
},
|
|
36
|
+
tuple(items, options = {}) {
|
|
37
|
+
return structure("tuple", options, {
|
|
38
|
+
tuple: Object.freeze(items.map(list)),
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
record(items, options = {}, keys) {
|
|
42
|
+
return structure("record", options, {
|
|
43
|
+
items: list(items),
|
|
44
|
+
keys: keys && list(keys),
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
map(keys, items, options = {}) {
|
|
48
|
+
return structure("map", options, { keys: list(keys), items: list(items) });
|
|
49
|
+
},
|
|
50
|
+
set(items, options = {}) {
|
|
51
|
+
return structure("set", options, { items: list(items) });
|
|
52
|
+
},
|
|
53
|
+
union(alternatives, options = {}) {
|
|
54
|
+
if (alternatives.length < 2)
|
|
55
|
+
throw new TypeError("A union requires at least two alternatives.");
|
|
56
|
+
return structure("union", options, {
|
|
57
|
+
alternatives: Object.freeze(alternatives.map(list)),
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
discriminatedUnion(key, variants, options = {}) {
|
|
61
|
+
if (!Object.keys(variants).length)
|
|
62
|
+
throw new TypeError("A discriminated union requires variants.");
|
|
63
|
+
return structure("union", options, {
|
|
64
|
+
discriminator: key,
|
|
65
|
+
variants: Object.freeze(Object.fromEntries(Object.entries(variants).map(([key, value]) => [key, list(value)]))),
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
intersection(left, right, options = {}) {
|
|
69
|
+
checkKeys(left);
|
|
70
|
+
checkKeys(right);
|
|
71
|
+
const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
72
|
+
const a = left;
|
|
73
|
+
const b = right;
|
|
74
|
+
return structure("object", options, {
|
|
75
|
+
shape: Object.freeze(Object.fromEntries([...keys].map((key) => {
|
|
76
|
+
const first = Object.hasOwn(a, key) ? a[key] : undefined;
|
|
77
|
+
const second = Object.hasOwn(b, key) ? b[key] : undefined;
|
|
78
|
+
return [
|
|
79
|
+
key,
|
|
80
|
+
first && second
|
|
81
|
+
? list([
|
|
82
|
+
structure("intersection", options, {
|
|
83
|
+
alternatives: [list(first), list(second)],
|
|
84
|
+
}),
|
|
85
|
+
])
|
|
86
|
+
: list(first ?? second),
|
|
87
|
+
];
|
|
88
|
+
}))),
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
validator(validator, options = {}) {
|
|
92
|
+
let cached;
|
|
93
|
+
return structure("validator", options, {
|
|
94
|
+
unknownKeys: validator.configuration.unknownKeys ?? "reject",
|
|
95
|
+
resolve: () => (cached ??= [
|
|
96
|
+
structure("object", options, {
|
|
97
|
+
shape: validator[validatorDefinition](),
|
|
98
|
+
}),
|
|
99
|
+
]),
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
lazy(resolve, options = {}) {
|
|
103
|
+
let cached;
|
|
104
|
+
return structure("lazy", options, {
|
|
105
|
+
resolve: () => (cached ??= list(resolve())),
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
unique(options = {}) {
|
|
109
|
+
return testRule("array.unique", (value) => Array.isArray(value) && new Set(value).size === value.length, options);
|
|
110
|
+
},
|
|
111
|
+
uniqueBy(key, options = {}) {
|
|
112
|
+
return testRule("array.unique", (value) => {
|
|
113
|
+
if (!Array.isArray(value))
|
|
114
|
+
return false;
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
for (const item of value) {
|
|
117
|
+
const selected = item != null && Object.hasOwn(item, key) ? item[key] : undefined;
|
|
118
|
+
if (seen.has(selected))
|
|
119
|
+
return false;
|
|
120
|
+
seen.add(selected);
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}, options, { key: String(key) });
|
|
124
|
+
},
|
|
125
|
+
sameAs(key, options = {}) {
|
|
126
|
+
return testRule("comparison.sameAs", (value, { data }) => {
|
|
127
|
+
const property = data !== null && typeof data === "object"
|
|
128
|
+
? Object.getOwnPropertyDescriptor(data, key)
|
|
129
|
+
: undefined;
|
|
130
|
+
return (!!property &&
|
|
131
|
+
Object.hasOwn(property, "value") &&
|
|
132
|
+
Object.is(value, property.value));
|
|
133
|
+
}, options, { field: key });
|
|
134
|
+
},
|
|
135
|
+
notSameAs(key, options = {}) {
|
|
136
|
+
return testRule("comparison.notSameAs", (value, { data }) => {
|
|
137
|
+
const property = data !== null && typeof data === "object"
|
|
138
|
+
? Object.getOwnPropertyDescriptor(data, key)
|
|
139
|
+
: undefined;
|
|
140
|
+
return (!!property &&
|
|
141
|
+
Object.hasOwn(property, "value") &&
|
|
142
|
+
!Object.is(value, property.value));
|
|
143
|
+
}, options, { field: key });
|
|
144
|
+
},
|
|
145
|
+
};
|