kanun 1.0.3 → 1.0.5

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/index.cjs CHANGED
@@ -27,6 +27,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
 
28
28
  //#endregion
29
29
  let node_async_hooks = require("node:async_hooks");
30
+ let node_url = require("node:url");
31
+ let node_net = require("node:net");
30
32
  let dayjs_plugin_customParseFormat_js = require("dayjs/plugin/customParseFormat.js");
31
33
  dayjs_plugin_customParseFormat_js = __toESM(dayjs_plugin_customParseFormat_js);
32
34
  let dayjs = require("dayjs");
@@ -49,10 +51,12 @@ function getValidatorContext() {
49
51
  * @returns
50
52
  */
51
53
  function useValidatorContext(context = {}) {
52
- const nextContext = {
53
- ...getValidatorContext(),
54
- ...context
55
- };
54
+ const currentContext = validatorContextStorage.getStore();
55
+ if (currentContext) {
56
+ Object.assign(currentContext, context);
57
+ return currentContext;
58
+ }
59
+ const nextContext = { ...context };
56
60
  validatorContextStorage.enterWith(nextContext);
57
61
  return nextContext;
58
62
  }
@@ -101,10 +105,12 @@ var en_default = {
101
105
  declined: "The :attribute must be declined.",
102
106
  declined_if: "The :attribute must be declined when :other is :value.",
103
107
  different: "The :attribute and :other must be different.",
108
+ distinct: "The :attribute field has a duplicate value.",
104
109
  digits: "The :attribute must be :digits digits.",
105
110
  digits_between: "The :attribute must be between :min and :max digits.",
106
111
  email: "The :attribute must be a valid email address.",
107
112
  ends_with: "The :attribute must end with one of the following: :values.",
113
+ filled: "The :attribute field must have a value.",
108
114
  exists: "The selected :attribute is invalid.",
109
115
  gt: {
110
116
  number: "The :attribute must be greater than :value.",
@@ -122,6 +128,9 @@ var en_default = {
122
128
  in: "The :attribute must be one of the following :values.",
123
129
  includes: "The :attribute must include one of the following values: :values.",
124
130
  integer: "The :attribute must be an integer.",
131
+ ip: "The :attribute must be a valid IP address.",
132
+ ipv4: "The :attribute must be a valid IPv4 address.",
133
+ ipv6: "The :attribute must be a valid IPv6 address.",
125
134
  json: "The :attribute must be a valid JSON string.",
126
135
  lt: {
127
136
  number: "The :attribute must be less than :value.",
@@ -147,6 +156,7 @@ var en_default = {
147
156
  array: "The :attribute must have at least :min items.",
148
157
  object: "The :attribute must have at least :min items."
149
158
  },
159
+ mac_address: "The :attribute must be a valid MAC address.",
150
160
  not_in: "The selected :attribute is invalid.",
151
161
  not_regex: "The :attribute format is invalid.",
152
162
  not_includes: "The :attribute must not include any of the following values: :values.",
@@ -165,6 +175,10 @@ var en_default = {
165
175
  upper_cases: "The :attribute must contain at least :amount uppercase letters."
166
176
  },
167
177
  present: "The :attribute field must be present.",
178
+ presentsame: "The :attribute field must be present.",
179
+ prohibited: "The :attribute field is prohibited.",
180
+ prohibited_unless: "The :attribute field is prohibited unless :other is in :values.",
181
+ prohibits: "The :attribute field prohibits :values from being present.",
168
182
  regex: "The :attribute format is invalid.",
169
183
  required: "The :attribute field is required.",
170
184
  required_if: "The :attribute field is required when :other is :value.",
@@ -182,8 +196,10 @@ var en_default = {
182
196
  object: "The :attribute must contain :size items."
183
197
  },
184
198
  string: "The :attribute must be a string.",
199
+ timezone: "The :attribute must be a valid timezone.",
185
200
  unique: "The :attribute has already been taken.",
186
- url: "The :attribute must have a valid URL format."
201
+ url: "The :attribute must have a valid URL format.",
202
+ multiple_of: "The :attribute must be a multiple of :value."
187
203
  };
188
204
 
189
205
  //#endregion
@@ -652,6 +668,12 @@ const replaceAttributes = {
652
668
  };
653
669
  return message.replace(/:other|:value/gi, (matched) => values[matched]);
654
670
  },
671
+ replaceProhibitedUnless: function(payload) {
672
+ return this.replaceRequiredUnless(payload);
673
+ },
674
+ replaceProhibits: function({ message, parameters, getDisplayableAttribute }) {
675
+ return message.replace(":values", parameters.map((attribute) => getDisplayableAttribute(attribute)).join(", "));
676
+ },
655
677
  replaceRequiredUnless: function({ message, parameters, getDisplayableAttribute }) {
656
678
  const [other] = parameters;
657
679
  const values = {
@@ -666,40 +688,14 @@ const replaceAttributes = {
666
688
  replaceSize: function({ message, parameters }) {
667
689
  return message.replace(":size", parameters[0]);
668
690
  },
691
+ replaceMultipleOf: function({ message, parameters }) {
692
+ return message.replace(":value", parameters[0]);
693
+ },
669
694
  replaceUnique: function({ message, parameters, data }) {
670
695
  return message.replace(":value", data[parameters[1]]);
671
696
  }
672
697
  };
673
698
 
674
- //#endregion
675
- //#region src/Rules/closureValidationRule.ts
676
- var ClosureValidationRule = class extends IRuleContract {
677
- /**
678
- * The callback that validates the attribute
679
- */
680
- callback;
681
- /**
682
- * Indicates if the validation callback failed.
683
- */
684
- failed = false;
685
- constructor(callback) {
686
- super();
687
- this.callback = callback;
688
- }
689
- /**
690
- * Determine if the validation rule passes.
691
- */
692
- passes(value, attribute) {
693
- this.failed = false;
694
- const result = this.callback(value, (message) => {
695
- this.failed = true;
696
- this.message = message;
697
- }, attribute);
698
- if (result instanceof Promise) return result.then(() => !this.failed);
699
- return !this.failed;
700
- }
701
- };
702
-
703
699
  //#endregion
704
700
  //#region src/validators/validationData.ts
705
701
  const validationData = {
@@ -740,6 +736,35 @@ const validationData = {
740
736
  }
741
737
  };
742
738
 
739
+ //#endregion
740
+ //#region src/Rules/closureValidationRule.ts
741
+ var ClosureValidationRule = class extends IRuleContract {
742
+ /**
743
+ * The callback that validates the attribute
744
+ */
745
+ callback;
746
+ /**
747
+ * Indicates if the validation callback failed.
748
+ */
749
+ failed = false;
750
+ constructor(callback) {
751
+ super();
752
+ this.callback = callback;
753
+ }
754
+ /**
755
+ * Determine if the validation rule passes.
756
+ */
757
+ passes(value, attribute) {
758
+ this.failed = false;
759
+ const result = this.callback(value, (message) => {
760
+ this.failed = true;
761
+ this.message = message;
762
+ }, attribute);
763
+ if (result instanceof Promise) return result.then(() => !this.failed);
764
+ return !this.failed;
765
+ }
766
+ };
767
+
743
768
  //#endregion
744
769
  //#region src/validators/validationRuleParser.ts
745
770
  const validationRuleParser = {
@@ -987,7 +1012,7 @@ var validateAttributes = class {
987
1012
  */
988
1013
  validateDeclinedIf(value, parameters) {
989
1014
  this.requireParameterCount(2, parameters, "declined_if");
990
- const other = deepFind(this.data, parameters[0]);
1015
+ const other = this.getAttributeValue(parameters[0]);
991
1016
  if (!other) return true;
992
1017
  if (parameters.slice(1).indexOf(other) !== -1) return this.validateDeclined(value);
993
1018
  return true;
@@ -997,7 +1022,7 @@ var validateAttributes = class {
997
1022
  */
998
1023
  validateDifferent(value, parameters) {
999
1024
  this.requireParameterCount(1, parameters, "different");
1000
- const other = deepFind(this.data, parameters[0]);
1025
+ const other = this.getAttributeValue(parameters[0]);
1001
1026
  if (!sameType(value, other)) return true;
1002
1027
  if (value !== null && typeof value === "object") return !deepEqual(value, other);
1003
1028
  return value !== other;
@@ -1032,13 +1057,23 @@ var validateAttributes = class {
1032
1057
  /**
1033
1058
  * Validate that an attribute is a valid email address.
1034
1059
  */
1035
- validateEmail(value) {
1060
+ validateEmail(value, parameters = []) {
1036
1061
  if (typeof value !== "string") return false;
1037
- /**
1038
- * Max allowed length for a top-level-domain is 24 characters.
1039
- * reference to list of top-level-domains: https://data.iana.org/TLD/tlds-alpha-by-domain.txt
1040
- */
1041
- return value.toLowerCase().match(/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,24})+$/) !== null;
1062
+ const normalized = parameters.map((parameter) => parameter.replace(/[{}]/g, "").toLowerCase());
1063
+ if (normalized.length === 0 || normalized.includes("filter") || normalized.includes("rfc")) {
1064
+ if (!this.isEmailByFilter(value)) return false;
1065
+ }
1066
+ if (normalized.includes("strict") && !this.isStrictEmail(value)) return false;
1067
+ if (normalized.includes("dns") && !this.hasResolvableEmailDomain(value)) return false;
1068
+ if (normalized.includes("spoof") && !this.isSpoofSafeEmail(value)) return false;
1069
+ return true;
1070
+ }
1071
+ /**
1072
+ * Validate that a present attribute is not empty.
1073
+ */
1074
+ validateFilled(value) {
1075
+ if (typeof value === "undefined") return true;
1076
+ return this.validateRequired(value);
1042
1077
  }
1043
1078
  /**
1044
1079
  * Validate the attribute ends with a given substring.
@@ -1087,6 +1122,7 @@ var validateAttributes = class {
1087
1122
  */
1088
1123
  validateRequired(value) {
1089
1124
  if (value === null || typeof value === "undefined") return false;
1125
+ else if (typeof Blob !== "undefined" && value instanceof Blob) return true;
1090
1126
  else if (typeof value === "string" && value.trim() === "") return false;
1091
1127
  else if (Array.isArray(value) && value.length < 1) return false;
1092
1128
  else if (typeof value === "object" && Object.keys(value).length < 1) return false;
@@ -1097,7 +1133,7 @@ var validateAttributes = class {
1097
1133
  */
1098
1134
  validateRequiredIf(value, parameters) {
1099
1135
  this.requireParameterCount(2, parameters, "required_if");
1100
- const other = deepFind(this.data, parameters[0]);
1136
+ const other = this.getAttributeValue(parameters[0]);
1101
1137
  if (typeof other === "undefined") return true;
1102
1138
  if (this.parseDependentRuleParameters(other, parameters).indexOf(other) !== -1) return this.validateRequired(value);
1103
1139
  return true;
@@ -1107,7 +1143,7 @@ var validateAttributes = class {
1107
1143
  */
1108
1144
  validateRequiredUnless(value, parameters) {
1109
1145
  this.requireParameterCount(2, parameters, "required_unless");
1110
- let other = deepFind(this.data, parameters[0]);
1146
+ let other = this.getAttributeValue(parameters[0]);
1111
1147
  other = typeof other === "undefined" ? null : other;
1112
1148
  if (this.parseDependentRuleParameters(other, parameters).indexOf(other) === -1) return this.validateRequired(value);
1113
1149
  return true;
@@ -1144,14 +1180,14 @@ var validateAttributes = class {
1144
1180
  * Determine if any of the given attributes fail the required test.
1145
1181
  */
1146
1182
  anyFailingRequired(attributes) {
1147
- for (let i = 0; i < attributes.length; i++) if (!this.validateRequired(deepFind(this.data, attributes[i]))) return true;
1183
+ for (let i = 0; i < attributes.length; i++) if (!this.validateRequired(this.getAttributeValue(attributes[i]))) return true;
1148
1184
  return false;
1149
1185
  }
1150
1186
  /**
1151
1187
  * Determine if all of the given attributes fail the required test.
1152
1188
  */
1153
1189
  allFailingRequired(attributes) {
1154
- for (let i = 0; i < attributes.length; i++) if (this.validateRequired(deepFind(this.data, attributes[i]))) return false;
1190
+ for (let i = 0; i < attributes.length; i++) if (this.validateRequired(this.getAttributeValue(attributes[i]))) return false;
1155
1191
  return true;
1156
1192
  }
1157
1193
  /**
@@ -1193,7 +1229,13 @@ var validateAttributes = class {
1193
1229
  * Validate that an attribute exists even if not filled.
1194
1230
  */
1195
1231
  validatePresent(value, parameters, attribute) {
1196
- return typeof deepFind(this.data, attribute) !== "undefined";
1232
+ return typeof this.getAttributeValue(attribute) !== "undefined";
1233
+ }
1234
+ /**
1235
+ * Alias of the present rule kept for compatibility with requested rule naming.
1236
+ */
1237
+ validatePresentsame(value, parameters, attribute) {
1238
+ return this.validatePresent(value, parameters, attribute);
1197
1239
  }
1198
1240
  /**
1199
1241
  * Validate that an attribute is an integer.
@@ -1215,12 +1257,103 @@ var validateAttributes = class {
1215
1257
  return true;
1216
1258
  }
1217
1259
  /**
1260
+ * Validate that an attribute is prohibited.
1261
+ */
1262
+ validateProhibited(value) {
1263
+ return !this.validateRequired(value);
1264
+ }
1265
+ /**
1266
+ * Validate that an attribute is prohibited unless another field matches one of the given values.
1267
+ */
1268
+ validateProhibitedUnless(value, parameters) {
1269
+ this.requireParameterCount(2, parameters, "prohibited_unless");
1270
+ let other = this.getAttributeValue(parameters[0]);
1271
+ other = typeof other === "undefined" ? null : other;
1272
+ if (this.parseDependentRuleParameters(other, parameters).indexOf(other) !== -1) return true;
1273
+ return this.validateProhibited(value);
1274
+ }
1275
+ /**
1276
+ * Validate that present attributes prohibit other attributes from being present.
1277
+ */
1278
+ validateProhibits(value, parameters) {
1279
+ this.requireParameterCount(1, parameters, "prohibits");
1280
+ if (!this.validateRequired(value)) return true;
1281
+ return parameters.every((attribute) => this.validateProhibited(this.getAttributeValue(attribute)));
1282
+ }
1283
+ /**
1284
+ * Validate that an attribute is a valid IP address.
1285
+ */
1286
+ validateIp(value) {
1287
+ return typeof value === "string" && (0, node_net.isIP)(value) !== 0;
1288
+ }
1289
+ /**
1290
+ * Validate that an attribute is a valid IPv4 address.
1291
+ */
1292
+ validateIpv4(value) {
1293
+ return typeof value === "string" && (0, node_net.isIP)(value) === 4;
1294
+ }
1295
+ /**
1296
+ * Validate that an attribute is a valid IPv6 address.
1297
+ */
1298
+ validateIpv6(value) {
1299
+ return typeof value === "string" && (0, node_net.isIP)(value) === 6;
1300
+ }
1301
+ /**
1302
+ * Validate that an attribute is a valid MAC address.
1303
+ */
1304
+ validateMacAddress(value) {
1305
+ return typeof value === "string" && /^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/i.test(value);
1306
+ }
1307
+ /**
1308
+ * Validate that an attribute is a valid timezone identifier.
1309
+ */
1310
+ validateTimezone(value) {
1311
+ if (typeof value !== "string" || value.length === 0) return false;
1312
+ if (typeof Intl.supportedValuesOf === "function" && Intl.supportedValuesOf("timeZone").includes(value)) return true;
1313
+ try {
1314
+ new Intl.DateTimeFormat("en-US", { timeZone: value });
1315
+ return true;
1316
+ } catch {
1317
+ return false;
1318
+ }
1319
+ }
1320
+ /**
1321
+ * Validate that an attribute is a multiple of the given value.
1322
+ */
1323
+ validateMultipleOf(value, parameters) {
1324
+ this.requireParameterCount(1, parameters, "multiple_of");
1325
+ const numericValue = Number(value);
1326
+ const divisor = Number(parameters[0]);
1327
+ if (!Number.isFinite(numericValue) || !Number.isFinite(divisor) || divisor === 0) return false;
1328
+ const scale = 10 ** Math.max(this.getDecimalPlaces(value), this.getDecimalPlaces(parameters[0]));
1329
+ const scaledValue = Math.round(numericValue * scale);
1330
+ const scaledDivisor = Math.round(divisor * scale);
1331
+ return scaledDivisor !== 0 && scaledValue % scaledDivisor === 0;
1332
+ }
1333
+ /**
1334
+ * Validate that values matched by a wildcard attribute are distinct.
1335
+ */
1336
+ validateDistinct(value, parameters, attribute, primaryAttribute = attribute) {
1337
+ if (Array.isArray(value)) return new Set(value.map((entry) => this.normalizeDistinctValue(entry, parameters))).size === value.length;
1338
+ if (primaryAttribute.indexOf("*") === -1) return true;
1339
+ const strict = parameters.includes("strict");
1340
+ const values = this.getDistinctValues(primaryAttribute);
1341
+ const current = this.normalizeDistinctValue(value, parameters);
1342
+ let matches = 0;
1343
+ for (const candidate of values) {
1344
+ const normalized = this.normalizeDistinctValue(candidate, parameters);
1345
+ if (strict ? normalized === current : normalized == current) matches += 1;
1346
+ if (matches > 1) return false;
1347
+ }
1348
+ return true;
1349
+ }
1350
+ /**
1218
1351
  * Validate that an attribute is greater than another attribute.
1219
1352
  */
1220
1353
  validateGt(value, parameters, attribute) {
1221
1354
  this.requireParameterCount(1, parameters, "gt");
1222
1355
  if (typeof value !== "number" && typeof value !== "string" && typeof value !== "object") throw "The field under validation must be a number, string, array or object";
1223
- const compartedToValue = deepFind(this.data, parameters[0]) || parameters[0];
1356
+ const compartedToValue = this.getAttributeValue(parameters[0]) || parameters[0];
1224
1357
  if (!Array.isArray(compartedToValue) && isNaN(compartedToValue) === false) return getSize(value, validationRuleParser.hasRule(attribute, getNumericRules(), this.rules)) > compartedToValue;
1225
1358
  if (sameType(value, compartedToValue) === false) throw "The fields under validation must be of the same type";
1226
1359
  return getSize(value) > getSize(compartedToValue);
@@ -1231,7 +1364,7 @@ var validateAttributes = class {
1231
1364
  validateGte(value, parameters, attribute) {
1232
1365
  this.requireParameterCount(1, parameters, "gte");
1233
1366
  if (typeof value !== "number" && typeof value !== "string" && typeof value !== "object") throw "The field under validation must be a number, string, array or object";
1234
- const compartedToValue = deepFind(this.data, parameters[0]) || parameters[0];
1367
+ const compartedToValue = this.getAttributeValue(parameters[0]) || parameters[0];
1235
1368
  if (!Array.isArray(compartedToValue) && isNaN(compartedToValue) === false) return getSize(value, validationRuleParser.hasRule(attribute, getNumericRules(), this.rules)) >= compartedToValue;
1236
1369
  if (sameType(value, compartedToValue) === false) throw "The fields under validation must be of the same type";
1237
1370
  return getSize(value) >= getSize(compartedToValue);
@@ -1242,7 +1375,7 @@ var validateAttributes = class {
1242
1375
  validateLt(value, parameters, attribute) {
1243
1376
  this.requireParameterCount(1, parameters, "lt");
1244
1377
  if (typeof value !== "number" && typeof value !== "string" && typeof value !== "object") throw "The field under validation must be a number, string, array or object";
1245
- const compartedToValue = deepFind(this.data, parameters[0]) || parameters[0];
1378
+ const compartedToValue = this.getAttributeValue(parameters[0]) || parameters[0];
1246
1379
  if (!Array.isArray(compartedToValue) && isNaN(compartedToValue) === false) return getSize(value, validationRuleParser.hasRule(attribute, getNumericRules(), this.rules)) < compartedToValue;
1247
1380
  if (sameType(value, compartedToValue) === false) throw "The fields under validation must be of the same type";
1248
1381
  return getSize(value) < getSize(compartedToValue);
@@ -1253,7 +1386,7 @@ var validateAttributes = class {
1253
1386
  validateLte(value, parameters, attribute) {
1254
1387
  this.requireParameterCount(1, parameters, "lte");
1255
1388
  if (typeof value !== "number" && typeof value !== "string" && typeof value !== "object") throw "The field under validation must be a number, string, array or object";
1256
- const compartedToValue = deepFind(this.data, parameters[0]) || parameters[0];
1389
+ const compartedToValue = this.getAttributeValue(parameters[0]) || parameters[0];
1257
1390
  if (!Array.isArray(compartedToValue) && isNaN(compartedToValue) === false) return getSize(value, validationRuleParser.hasRule(attribute, getNumericRules(), this.rules)) <= compartedToValue;
1258
1391
  if (sameType(value, compartedToValue) === false) throw "The fields under validation must be of the same type";
1259
1392
  return getSize(value) <= getSize(compartedToValue);
@@ -1311,7 +1444,7 @@ var validateAttributes = class {
1311
1444
  compareDates(value, parameter, operator, rule) {
1312
1445
  value = toDate(value);
1313
1446
  if (!value) throw `Validation rule ${rule} requires the field under valation to be a date.`;
1314
- const compartedToValue = toDate(deepFind(this.data, parameter) || parameter);
1447
+ const compartedToValue = toDate(this.getAttributeValue(parameter) || parameter);
1315
1448
  if (!compartedToValue) throw `Validation rule ${rule} requires the parameter to be a date.`;
1316
1449
  return compare(value.getTime(), compartedToValue.getTime(), operator);
1317
1450
  }
@@ -1323,6 +1456,10 @@ var validateAttributes = class {
1323
1456
  }
1324
1457
  /**
1325
1458
  * Prepare the values for validation
1459
+ *
1460
+ * @param other The value to compare against
1461
+ * @param parameters The parameters for the validation rule
1462
+ * @returns The prepared values for validation
1326
1463
  */
1327
1464
  parseDependentRuleParameters(other, parameters) {
1328
1465
  let values = parameters.slice(1);
@@ -1331,6 +1468,55 @@ var validateAttributes = class {
1331
1468
  if (typeof other === "boolean") values = convertValuesToBoolean(values);
1332
1469
  return values;
1333
1470
  }
1471
+ getAttributeValue(attribute) {
1472
+ const dataValue = deepFind(this.data, attribute);
1473
+ if (typeof dataValue !== "undefined") return dataValue;
1474
+ return deepFind(this.context.requestFiles ?? {}, attribute);
1475
+ }
1476
+ getDistinctValues(primaryAttribute) {
1477
+ const gathered = validationData.initializeAndGatherData(primaryAttribute, this.data);
1478
+ const pattern = new RegExp(`^${primaryAttribute.replace(/\./g, "\\.").replace(/\*/g, "[^.]+")}$`);
1479
+ return Object.keys(gathered).filter((attribute) => pattern.test(attribute)).map((attribute) => deepFind(this.data, attribute)).filter((value) => typeof value !== "undefined");
1480
+ }
1481
+ normalizeDistinctValue(value, parameters) {
1482
+ if (parameters.includes("ignore_case") && typeof value === "string") return value.toLowerCase();
1483
+ return value;
1484
+ }
1485
+ isEmailByFilter(value) {
1486
+ return value.toLowerCase().match(/^[^\s@]+@[^\s@]+\.[^\s@]{2,24}$/) !== null;
1487
+ }
1488
+ isStrictEmail(value) {
1489
+ if (!this.isEmailByFilter(value)) return false;
1490
+ const [localPart, domain] = value.split("@");
1491
+ if (!localPart || !domain || localPart.length > 64 || domain.length > 255) return false;
1492
+ if (localPart.startsWith(".") || localPart.endsWith(".") || localPart.includes("..")) return false;
1493
+ return domain.split(".").every((label) => {
1494
+ return label.length > 0 && label.length <= 63 && /^[a-z0-9-]+$/i.test(label) && !label.startsWith("-") && !label.endsWith("-");
1495
+ });
1496
+ }
1497
+ hasResolvableEmailDomain(value) {
1498
+ if (!this.isStrictEmail(value)) return false;
1499
+ const domain = value.split("@")[1];
1500
+ const asciiDomain = (0, node_url.domainToASCII)(domain);
1501
+ return asciiDomain.length > 0 && asciiDomain.includes(".");
1502
+ }
1503
+ isSpoofSafeEmail(value) {
1504
+ if (!this.isEmailByFilter(value)) return false;
1505
+ const [localPart, domain] = value.split("@");
1506
+ return (0, node_url.domainToASCII)(domain).length > 0 && !this.hasAsciiControlCharacters(value) && localPart.normalize("NFKC") === localPart && domain.normalize("NFKC") === domain;
1507
+ }
1508
+ hasAsciiControlCharacters(value) {
1509
+ for (let index = 0; index < value.length; index++) {
1510
+ const codePoint = value.charCodeAt(index);
1511
+ if (codePoint <= 31 || codePoint === 127) return true;
1512
+ }
1513
+ return false;
1514
+ }
1515
+ getDecimalPlaces(value) {
1516
+ const stringValue = String(value).toLowerCase();
1517
+ if (!stringValue.includes(".")) return 0;
1518
+ return stringValue.split(".")[1]?.length ?? 0;
1519
+ }
1334
1520
  };
1335
1521
 
1336
1522
  //#endregion
@@ -1399,6 +1585,7 @@ const implicitRues = [
1399
1585
  "declined_if",
1400
1586
  "filled",
1401
1587
  "present",
1588
+ "presentsame",
1402
1589
  "required",
1403
1590
  "required_if",
1404
1591
  "required_unless",
@@ -2027,6 +2214,7 @@ var replaceAttributePayload = class {
2027
2214
  //#endregion
2028
2215
  //#region src/BaseValidator.ts
2029
2216
  var BaseValidator = class BaseValidator {
2217
+ excludedAttributes = /* @__PURE__ */ new Set();
2030
2218
  /**
2031
2219
  * The lang used to return error messages
2032
2220
  */
@@ -2171,6 +2359,9 @@ var BaseValidator = class BaseValidator {
2171
2359
  errors() {
2172
2360
  return this.messages;
2173
2361
  }
2362
+ getExcludedAttributes() {
2363
+ return [...this.excludedAttributes];
2364
+ }
2174
2365
  /**
2175
2366
  * Clear the error messages for the given keys.
2176
2367
  * If no keys are provided, all error messages will be cleared.
@@ -2287,6 +2478,7 @@ var BaseValidator = class BaseValidator {
2287
2478
  runAllValidations() {
2288
2479
  this.messages = new ErrorBag();
2289
2480
  this.validateAttributes = new validateAttributes(this.data, this.rules, this.getContext());
2481
+ this.excludedAttributes.clear();
2290
2482
  for (const property in this.rules) if (this.runValidation(property) === false) break;
2291
2483
  }
2292
2484
  /**
@@ -2295,6 +2487,7 @@ var BaseValidator = class BaseValidator {
2295
2487
  async runAllValidationsAsync() {
2296
2488
  this.messages = new ErrorBag();
2297
2489
  this.validateAttributes = new validateAttributes(this.data, this.rules, this.getContext());
2490
+ this.excludedAttributes.clear();
2298
2491
  for (const property in this.rules) if (await this.runValidationAsync(property) === false) break;
2299
2492
  }
2300
2493
  /**
@@ -2317,26 +2510,39 @@ var BaseValidator = class BaseValidator {
2317
2510
  * Run validation rules for the specified property and stop validation if needed
2318
2511
  */
2319
2512
  runValidation(property) {
2320
- if (Object.prototype.hasOwnProperty.call(this.rules, property) && Array.isArray(this.rules[property])) for (let i = 0; i < this.rules[property].length; i++) {
2321
- this.validateAttribute(property, this.rules[property][i]);
2322
- if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) return false;
2323
- if (this.shouldStopValidating(property)) break;
2513
+ if (Object.prototype.hasOwnProperty.call(this.rules, property) && Array.isArray(this.rules[property])) {
2514
+ if (validationRuleParser.hasRule(property, ["exclude"], this.rules)) {
2515
+ this.excludedAttributes.add(property);
2516
+ return;
2517
+ }
2518
+ for (let i = 0; i < this.rules[property].length; i++) {
2519
+ this.validateAttribute(property, this.rules[property][i]);
2520
+ if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) return false;
2521
+ if (this.shouldStopValidating(property)) break;
2522
+ }
2324
2523
  }
2325
2524
  }
2326
2525
  /**
2327
2526
  * Run validation rules for the specified property asynchronously and stop validation if needed
2328
2527
  */
2329
2528
  async runValidationAsync(property) {
2330
- if (Object.prototype.hasOwnProperty.call(this.rules, property) && Array.isArray(this.rules[property])) for (let i = 0; i < this.rules[property].length; i++) {
2331
- await this.validateAttribute(property, this.rules[property][i]);
2332
- if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) return false;
2333
- if (this.shouldStopValidating(property)) break;
2529
+ if (Object.prototype.hasOwnProperty.call(this.rules, property) && Array.isArray(this.rules[property])) {
2530
+ if (validationRuleParser.hasRule(property, ["exclude"], this.rules)) {
2531
+ this.excludedAttributes.add(property);
2532
+ return;
2533
+ }
2534
+ for (let i = 0; i < this.rules[property].length; i++) {
2535
+ await this.validateAttribute(property, this.rules[property][i]);
2536
+ if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) return false;
2537
+ if (this.shouldStopValidating(property)) break;
2538
+ }
2334
2539
  }
2335
2540
  }
2336
2541
  /**
2337
2542
  * Check if we should stop further validations on a given attribute.
2338
2543
  */
2339
2544
  shouldStopValidating(attribute) {
2545
+ if (this.excludedAttributes.has(attribute)) return true;
2340
2546
  return this.messages.has(attribute) && validationRuleParser.hasRule(attribute, ["bail"], this.rules);
2341
2547
  }
2342
2548
  /**
@@ -2361,7 +2567,7 @@ var BaseValidator = class BaseValidator {
2361
2567
  const method = `validate${buildValidationMethodName(rule)}`;
2362
2568
  if (rule !== "" && typeof this.validateAttributes[method] === "undefined") throw `Rule ${rule} is not valid`;
2363
2569
  if (!validatable) return;
2364
- const validation = this.validateAttributes[method](value, parameters, attribute);
2570
+ const validation = this.validateAttributes[method](value, parameters, attribute, this.getPrimaryAttribute(attribute));
2365
2571
  if (validation instanceof Promise) return validation.then((result) => {
2366
2572
  if (!result) this.addFailure(attribute, rule, value, parameters);
2367
2573
  });
@@ -3126,8 +3332,10 @@ var Validator = class Validator {
3126
3332
  */
3127
3333
  validatedData() {
3128
3334
  const validKeys = Object.keys(this.rules);
3335
+ const excluded = new Set(this.instance?.getExcludedAttributes() ?? []);
3129
3336
  const clean = {};
3130
3337
  for (const key of validKeys) {
3338
+ if (excluded.has(key)) continue;
3131
3339
  const value = deepFind(this.data, key);
3132
3340
  const resolvedValue = typeof value !== "undefined" ? value : deepFind(this.getContext().requestFiles ?? {}, key);
3133
3341
  if (typeof resolvedValue !== "undefined") deepSet(clean, key, resolvedValue);
@@ -3138,7 +3346,8 @@ var Validator = class Validator {
3138
3346
  * Return all validated input.
3139
3347
  */
3140
3348
  validated() {
3141
- return Object.fromEntries(Object.entries(this.data).filter(([key]) => key in this.rules));
3349
+ const excluded = new Set(this.instance?.getExcludedAttributes() ?? []);
3350
+ return Object.fromEntries(Object.entries(this.data).filter(([key]) => key in this.rules && !excluded.has(key)));
3142
3351
  }
3143
3352
  /**
3144
3353
  * Return a portion of validated input
package/dist/index.d.ts CHANGED
@@ -127,8 +127,8 @@ type LiteralUnion<T extends U, U = string> = T | (U & Record<never, never>);
127
127
  type PluginValidationRuleNameMap = ValidationRuleAutocompleteMap & CustomValidationRuleNameMap;
128
128
  type CustomParamableValidationRuleName = Extract<{ [K in keyof PluginValidationRuleNameMap]: PluginValidationRuleNameMap[K] extends 'paramable' ? K : never }[keyof PluginValidationRuleNameMap], string>;
129
129
  type CustomPlainRuleName = Extract<{ [K in keyof PluginValidationRuleNameMap]: PluginValidationRuleNameMap[K] extends 'plain' ? K : never }[keyof PluginValidationRuleNameMap], string>;
130
- type ParamableValidationRuleName = 'accepted_if' | 'after' | 'after_or_equal' | 'before' | 'before_or_equal' | 'between' | 'date_equals' | 'datetime' | 'declined_if' | 'digits_between' | 'different' | 'exists' | 'ends_with' | 'gt' | 'gte' | 'in' | 'includes' | 'lt' | 'lte' | 'max' | 'min' | 'not_in' | 'not_includes' | 'required_if' | 'required_unless' | 'required_with' | 'required_with_all' | 'required_without' | 'required_without_all' | 'same' | 'size' | 'starts_with' | 'unique' | CustomParamableValidationRuleName;
131
- type PlainRuleName = 'accepted' | 'alpha' | 'alpha_dash' | 'alpha_num' | 'array' | 'array_unique' | 'bail' | 'boolean' | 'confirmed' | 'date' | 'declined' | 'digits' | 'email' | 'integer' | 'json' | 'not_regex' | 'nullable' | 'numeric' | 'object' | 'present' | 'regex' | 'required' | 'sometimes' | 'string' | 'url' | 'hex' | 'uuid' | CustomPlainRuleName;
130
+ type ParamableValidationRuleName = 'accepted_if' | 'after' | 'after_or_equal' | 'before' | 'before_or_equal' | 'between' | 'date_equals' | 'datetime' | 'declined_if' | 'digits_between' | 'different' | 'email' | 'exists' | 'ends_with' | 'gt' | 'gte' | 'in' | 'includes' | 'lt' | 'lte' | 'max' | 'min' | 'not_in' | 'not_includes' | 'multiple_of' | 'prohibited_unless' | 'prohibits' | 'required_if' | 'required_unless' | 'required_with' | 'required_with_all' | 'required_without' | 'required_without_all' | 'same' | 'size' | 'starts_with' | 'unique' | CustomParamableValidationRuleName;
131
+ type PlainRuleName = 'accepted' | 'alpha' | 'alpha_dash' | 'alpha_num' | 'array' | 'array_unique' | 'bail' | 'boolean' | 'confirmed' | 'date' | 'declined' | 'distinct' | 'digits' | 'email' | 'exclude' | 'filled' | 'ip' | 'ipv4' | 'ipv6' | 'integer' | 'json' | 'mac_address' | 'not_regex' | 'nullable' | 'numeric' | 'object' | 'present' | 'presentsame' | 'prohibited' | 'regex' | 'required' | 'sometimes' | 'string' | 'timezone' | 'url' | 'hex' | 'uuid' | CustomPlainRuleName;
132
132
  type ValidationRuleName = ParamableValidationRuleName | PlainRuleName;
133
133
  type MethodRules = Regex | In | NotIn | RequiredIf;
134
134
  type ParamableRuleString = `${ParamableValidationRuleName}:${string}`;
@@ -308,10 +308,13 @@ interface ReplaceAttributeInterface {
308
308
  replaceGte: (payload: replaceAttributePayload) => string;
309
309
  replaceLte: (payload: replaceAttributePayload) => string;
310
310
  replaceRequiredIf: (payload: replaceAttributePayload) => string;
311
+ replaceProhibitedUnless: (payload: replaceAttributePayload) => string;
312
+ replaceProhibits: (payload: replaceAttributePayload) => string;
311
313
  replaceStartsWith: (payload: replaceAttributePayload) => string;
312
314
  replaceRequiredUnless: (payload: replaceAttributePayload) => string;
313
315
  replaceSame: (payload: replaceAttributePayload) => string;
314
316
  replaceSize: (payload: replaceAttributePayload) => string;
317
+ replaceMultipleOf: (payload: replaceAttributePayload) => string;
315
318
  replaceUnique: (payload: replaceAttributePayload) => string;
316
319
  }
317
320
  type ValidationCallback = (value: any, fail: (message: string) => void, attribute: string) => void;
@@ -402,6 +405,7 @@ declare function getValidationSize(value: any, hasNumericRule?: boolean): number
402
405
  //#endregion
403
406
  //#region src/BaseValidator.d.ts
404
407
  declare class BaseValidator<D extends GenericObject = GenericObject> {
408
+ private excludedAttributes;
405
409
  /**
406
410
  * The lang used to return error messages
407
411
  */
@@ -497,6 +501,7 @@ declare class BaseValidator<D extends GenericObject = GenericObject> {
497
501
  * @returns
498
502
  */
499
503
  errors(): ErrorBag;
504
+ getExcludedAttributes(): string[];
500
505
  /**
501
506
  * Clear the error messages for the given keys.
502
507
  * If no keys are provided, all error messages will be cleared.
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { domainToASCII } from "node:url";
3
+ import { isIP } from "node:net";
2
4
  import customParseFormat from "dayjs/plugin/customParseFormat.js";
3
5
  import dayjs from "dayjs";
4
6
 
@@ -19,10 +21,12 @@ function getValidatorContext() {
19
21
  * @returns
20
22
  */
21
23
  function useValidatorContext(context = {}) {
22
- const nextContext = {
23
- ...getValidatorContext(),
24
- ...context
25
- };
24
+ const currentContext = validatorContextStorage.getStore();
25
+ if (currentContext) {
26
+ Object.assign(currentContext, context);
27
+ return currentContext;
28
+ }
29
+ const nextContext = { ...context };
26
30
  validatorContextStorage.enterWith(nextContext);
27
31
  return nextContext;
28
32
  }
@@ -71,10 +75,12 @@ var en_default = {
71
75
  declined: "The :attribute must be declined.",
72
76
  declined_if: "The :attribute must be declined when :other is :value.",
73
77
  different: "The :attribute and :other must be different.",
78
+ distinct: "The :attribute field has a duplicate value.",
74
79
  digits: "The :attribute must be :digits digits.",
75
80
  digits_between: "The :attribute must be between :min and :max digits.",
76
81
  email: "The :attribute must be a valid email address.",
77
82
  ends_with: "The :attribute must end with one of the following: :values.",
83
+ filled: "The :attribute field must have a value.",
78
84
  exists: "The selected :attribute is invalid.",
79
85
  gt: {
80
86
  number: "The :attribute must be greater than :value.",
@@ -92,6 +98,9 @@ var en_default = {
92
98
  in: "The :attribute must be one of the following :values.",
93
99
  includes: "The :attribute must include one of the following values: :values.",
94
100
  integer: "The :attribute must be an integer.",
101
+ ip: "The :attribute must be a valid IP address.",
102
+ ipv4: "The :attribute must be a valid IPv4 address.",
103
+ ipv6: "The :attribute must be a valid IPv6 address.",
95
104
  json: "The :attribute must be a valid JSON string.",
96
105
  lt: {
97
106
  number: "The :attribute must be less than :value.",
@@ -117,6 +126,7 @@ var en_default = {
117
126
  array: "The :attribute must have at least :min items.",
118
127
  object: "The :attribute must have at least :min items."
119
128
  },
129
+ mac_address: "The :attribute must be a valid MAC address.",
120
130
  not_in: "The selected :attribute is invalid.",
121
131
  not_regex: "The :attribute format is invalid.",
122
132
  not_includes: "The :attribute must not include any of the following values: :values.",
@@ -135,6 +145,10 @@ var en_default = {
135
145
  upper_cases: "The :attribute must contain at least :amount uppercase letters."
136
146
  },
137
147
  present: "The :attribute field must be present.",
148
+ presentsame: "The :attribute field must be present.",
149
+ prohibited: "The :attribute field is prohibited.",
150
+ prohibited_unless: "The :attribute field is prohibited unless :other is in :values.",
151
+ prohibits: "The :attribute field prohibits :values from being present.",
138
152
  regex: "The :attribute format is invalid.",
139
153
  required: "The :attribute field is required.",
140
154
  required_if: "The :attribute field is required when :other is :value.",
@@ -152,8 +166,10 @@ var en_default = {
152
166
  object: "The :attribute must contain :size items."
153
167
  },
154
168
  string: "The :attribute must be a string.",
169
+ timezone: "The :attribute must be a valid timezone.",
155
170
  unique: "The :attribute has already been taken.",
156
- url: "The :attribute must have a valid URL format."
171
+ url: "The :attribute must have a valid URL format.",
172
+ multiple_of: "The :attribute must be a multiple of :value."
157
173
  };
158
174
 
159
175
  //#endregion
@@ -622,6 +638,12 @@ const replaceAttributes = {
622
638
  };
623
639
  return message.replace(/:other|:value/gi, (matched) => values[matched]);
624
640
  },
641
+ replaceProhibitedUnless: function(payload) {
642
+ return this.replaceRequiredUnless(payload);
643
+ },
644
+ replaceProhibits: function({ message, parameters, getDisplayableAttribute }) {
645
+ return message.replace(":values", parameters.map((attribute) => getDisplayableAttribute(attribute)).join(", "));
646
+ },
625
647
  replaceRequiredUnless: function({ message, parameters, getDisplayableAttribute }) {
626
648
  const [other] = parameters;
627
649
  const values = {
@@ -636,40 +658,14 @@ const replaceAttributes = {
636
658
  replaceSize: function({ message, parameters }) {
637
659
  return message.replace(":size", parameters[0]);
638
660
  },
661
+ replaceMultipleOf: function({ message, parameters }) {
662
+ return message.replace(":value", parameters[0]);
663
+ },
639
664
  replaceUnique: function({ message, parameters, data }) {
640
665
  return message.replace(":value", data[parameters[1]]);
641
666
  }
642
667
  };
643
668
 
644
- //#endregion
645
- //#region src/Rules/closureValidationRule.ts
646
- var ClosureValidationRule = class extends IRuleContract {
647
- /**
648
- * The callback that validates the attribute
649
- */
650
- callback;
651
- /**
652
- * Indicates if the validation callback failed.
653
- */
654
- failed = false;
655
- constructor(callback) {
656
- super();
657
- this.callback = callback;
658
- }
659
- /**
660
- * Determine if the validation rule passes.
661
- */
662
- passes(value, attribute) {
663
- this.failed = false;
664
- const result = this.callback(value, (message) => {
665
- this.failed = true;
666
- this.message = message;
667
- }, attribute);
668
- if (result instanceof Promise) return result.then(() => !this.failed);
669
- return !this.failed;
670
- }
671
- };
672
-
673
669
  //#endregion
674
670
  //#region src/validators/validationData.ts
675
671
  const validationData = {
@@ -710,6 +706,35 @@ const validationData = {
710
706
  }
711
707
  };
712
708
 
709
+ //#endregion
710
+ //#region src/Rules/closureValidationRule.ts
711
+ var ClosureValidationRule = class extends IRuleContract {
712
+ /**
713
+ * The callback that validates the attribute
714
+ */
715
+ callback;
716
+ /**
717
+ * Indicates if the validation callback failed.
718
+ */
719
+ failed = false;
720
+ constructor(callback) {
721
+ super();
722
+ this.callback = callback;
723
+ }
724
+ /**
725
+ * Determine if the validation rule passes.
726
+ */
727
+ passes(value, attribute) {
728
+ this.failed = false;
729
+ const result = this.callback(value, (message) => {
730
+ this.failed = true;
731
+ this.message = message;
732
+ }, attribute);
733
+ if (result instanceof Promise) return result.then(() => !this.failed);
734
+ return !this.failed;
735
+ }
736
+ };
737
+
713
738
  //#endregion
714
739
  //#region src/validators/validationRuleParser.ts
715
740
  const validationRuleParser = {
@@ -957,7 +982,7 @@ var validateAttributes = class {
957
982
  */
958
983
  validateDeclinedIf(value, parameters) {
959
984
  this.requireParameterCount(2, parameters, "declined_if");
960
- const other = deepFind(this.data, parameters[0]);
985
+ const other = this.getAttributeValue(parameters[0]);
961
986
  if (!other) return true;
962
987
  if (parameters.slice(1).indexOf(other) !== -1) return this.validateDeclined(value);
963
988
  return true;
@@ -967,7 +992,7 @@ var validateAttributes = class {
967
992
  */
968
993
  validateDifferent(value, parameters) {
969
994
  this.requireParameterCount(1, parameters, "different");
970
- const other = deepFind(this.data, parameters[0]);
995
+ const other = this.getAttributeValue(parameters[0]);
971
996
  if (!sameType(value, other)) return true;
972
997
  if (value !== null && typeof value === "object") return !deepEqual(value, other);
973
998
  return value !== other;
@@ -1002,13 +1027,23 @@ var validateAttributes = class {
1002
1027
  /**
1003
1028
  * Validate that an attribute is a valid email address.
1004
1029
  */
1005
- validateEmail(value) {
1030
+ validateEmail(value, parameters = []) {
1006
1031
  if (typeof value !== "string") return false;
1007
- /**
1008
- * Max allowed length for a top-level-domain is 24 characters.
1009
- * reference to list of top-level-domains: https://data.iana.org/TLD/tlds-alpha-by-domain.txt
1010
- */
1011
- return value.toLowerCase().match(/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,24})+$/) !== null;
1032
+ const normalized = parameters.map((parameter) => parameter.replace(/[{}]/g, "").toLowerCase());
1033
+ if (normalized.length === 0 || normalized.includes("filter") || normalized.includes("rfc")) {
1034
+ if (!this.isEmailByFilter(value)) return false;
1035
+ }
1036
+ if (normalized.includes("strict") && !this.isStrictEmail(value)) return false;
1037
+ if (normalized.includes("dns") && !this.hasResolvableEmailDomain(value)) return false;
1038
+ if (normalized.includes("spoof") && !this.isSpoofSafeEmail(value)) return false;
1039
+ return true;
1040
+ }
1041
+ /**
1042
+ * Validate that a present attribute is not empty.
1043
+ */
1044
+ validateFilled(value) {
1045
+ if (typeof value === "undefined") return true;
1046
+ return this.validateRequired(value);
1012
1047
  }
1013
1048
  /**
1014
1049
  * Validate the attribute ends with a given substring.
@@ -1057,6 +1092,7 @@ var validateAttributes = class {
1057
1092
  */
1058
1093
  validateRequired(value) {
1059
1094
  if (value === null || typeof value === "undefined") return false;
1095
+ else if (typeof Blob !== "undefined" && value instanceof Blob) return true;
1060
1096
  else if (typeof value === "string" && value.trim() === "") return false;
1061
1097
  else if (Array.isArray(value) && value.length < 1) return false;
1062
1098
  else if (typeof value === "object" && Object.keys(value).length < 1) return false;
@@ -1067,7 +1103,7 @@ var validateAttributes = class {
1067
1103
  */
1068
1104
  validateRequiredIf(value, parameters) {
1069
1105
  this.requireParameterCount(2, parameters, "required_if");
1070
- const other = deepFind(this.data, parameters[0]);
1106
+ const other = this.getAttributeValue(parameters[0]);
1071
1107
  if (typeof other === "undefined") return true;
1072
1108
  if (this.parseDependentRuleParameters(other, parameters).indexOf(other) !== -1) return this.validateRequired(value);
1073
1109
  return true;
@@ -1077,7 +1113,7 @@ var validateAttributes = class {
1077
1113
  */
1078
1114
  validateRequiredUnless(value, parameters) {
1079
1115
  this.requireParameterCount(2, parameters, "required_unless");
1080
- let other = deepFind(this.data, parameters[0]);
1116
+ let other = this.getAttributeValue(parameters[0]);
1081
1117
  other = typeof other === "undefined" ? null : other;
1082
1118
  if (this.parseDependentRuleParameters(other, parameters).indexOf(other) === -1) return this.validateRequired(value);
1083
1119
  return true;
@@ -1114,14 +1150,14 @@ var validateAttributes = class {
1114
1150
  * Determine if any of the given attributes fail the required test.
1115
1151
  */
1116
1152
  anyFailingRequired(attributes) {
1117
- for (let i = 0; i < attributes.length; i++) if (!this.validateRequired(deepFind(this.data, attributes[i]))) return true;
1153
+ for (let i = 0; i < attributes.length; i++) if (!this.validateRequired(this.getAttributeValue(attributes[i]))) return true;
1118
1154
  return false;
1119
1155
  }
1120
1156
  /**
1121
1157
  * Determine if all of the given attributes fail the required test.
1122
1158
  */
1123
1159
  allFailingRequired(attributes) {
1124
- for (let i = 0; i < attributes.length; i++) if (this.validateRequired(deepFind(this.data, attributes[i]))) return false;
1160
+ for (let i = 0; i < attributes.length; i++) if (this.validateRequired(this.getAttributeValue(attributes[i]))) return false;
1125
1161
  return true;
1126
1162
  }
1127
1163
  /**
@@ -1163,7 +1199,13 @@ var validateAttributes = class {
1163
1199
  * Validate that an attribute exists even if not filled.
1164
1200
  */
1165
1201
  validatePresent(value, parameters, attribute) {
1166
- return typeof deepFind(this.data, attribute) !== "undefined";
1202
+ return typeof this.getAttributeValue(attribute) !== "undefined";
1203
+ }
1204
+ /**
1205
+ * Alias of the present rule kept for compatibility with requested rule naming.
1206
+ */
1207
+ validatePresentsame(value, parameters, attribute) {
1208
+ return this.validatePresent(value, parameters, attribute);
1167
1209
  }
1168
1210
  /**
1169
1211
  * Validate that an attribute is an integer.
@@ -1185,12 +1227,103 @@ var validateAttributes = class {
1185
1227
  return true;
1186
1228
  }
1187
1229
  /**
1230
+ * Validate that an attribute is prohibited.
1231
+ */
1232
+ validateProhibited(value) {
1233
+ return !this.validateRequired(value);
1234
+ }
1235
+ /**
1236
+ * Validate that an attribute is prohibited unless another field matches one of the given values.
1237
+ */
1238
+ validateProhibitedUnless(value, parameters) {
1239
+ this.requireParameterCount(2, parameters, "prohibited_unless");
1240
+ let other = this.getAttributeValue(parameters[0]);
1241
+ other = typeof other === "undefined" ? null : other;
1242
+ if (this.parseDependentRuleParameters(other, parameters).indexOf(other) !== -1) return true;
1243
+ return this.validateProhibited(value);
1244
+ }
1245
+ /**
1246
+ * Validate that present attributes prohibit other attributes from being present.
1247
+ */
1248
+ validateProhibits(value, parameters) {
1249
+ this.requireParameterCount(1, parameters, "prohibits");
1250
+ if (!this.validateRequired(value)) return true;
1251
+ return parameters.every((attribute) => this.validateProhibited(this.getAttributeValue(attribute)));
1252
+ }
1253
+ /**
1254
+ * Validate that an attribute is a valid IP address.
1255
+ */
1256
+ validateIp(value) {
1257
+ return typeof value === "string" && isIP(value) !== 0;
1258
+ }
1259
+ /**
1260
+ * Validate that an attribute is a valid IPv4 address.
1261
+ */
1262
+ validateIpv4(value) {
1263
+ return typeof value === "string" && isIP(value) === 4;
1264
+ }
1265
+ /**
1266
+ * Validate that an attribute is a valid IPv6 address.
1267
+ */
1268
+ validateIpv6(value) {
1269
+ return typeof value === "string" && isIP(value) === 6;
1270
+ }
1271
+ /**
1272
+ * Validate that an attribute is a valid MAC address.
1273
+ */
1274
+ validateMacAddress(value) {
1275
+ return typeof value === "string" && /^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/i.test(value);
1276
+ }
1277
+ /**
1278
+ * Validate that an attribute is a valid timezone identifier.
1279
+ */
1280
+ validateTimezone(value) {
1281
+ if (typeof value !== "string" || value.length === 0) return false;
1282
+ if (typeof Intl.supportedValuesOf === "function" && Intl.supportedValuesOf("timeZone").includes(value)) return true;
1283
+ try {
1284
+ new Intl.DateTimeFormat("en-US", { timeZone: value });
1285
+ return true;
1286
+ } catch {
1287
+ return false;
1288
+ }
1289
+ }
1290
+ /**
1291
+ * Validate that an attribute is a multiple of the given value.
1292
+ */
1293
+ validateMultipleOf(value, parameters) {
1294
+ this.requireParameterCount(1, parameters, "multiple_of");
1295
+ const numericValue = Number(value);
1296
+ const divisor = Number(parameters[0]);
1297
+ if (!Number.isFinite(numericValue) || !Number.isFinite(divisor) || divisor === 0) return false;
1298
+ const scale = 10 ** Math.max(this.getDecimalPlaces(value), this.getDecimalPlaces(parameters[0]));
1299
+ const scaledValue = Math.round(numericValue * scale);
1300
+ const scaledDivisor = Math.round(divisor * scale);
1301
+ return scaledDivisor !== 0 && scaledValue % scaledDivisor === 0;
1302
+ }
1303
+ /**
1304
+ * Validate that values matched by a wildcard attribute are distinct.
1305
+ */
1306
+ validateDistinct(value, parameters, attribute, primaryAttribute = attribute) {
1307
+ if (Array.isArray(value)) return new Set(value.map((entry) => this.normalizeDistinctValue(entry, parameters))).size === value.length;
1308
+ if (primaryAttribute.indexOf("*") === -1) return true;
1309
+ const strict = parameters.includes("strict");
1310
+ const values = this.getDistinctValues(primaryAttribute);
1311
+ const current = this.normalizeDistinctValue(value, parameters);
1312
+ let matches = 0;
1313
+ for (const candidate of values) {
1314
+ const normalized = this.normalizeDistinctValue(candidate, parameters);
1315
+ if (strict ? normalized === current : normalized == current) matches += 1;
1316
+ if (matches > 1) return false;
1317
+ }
1318
+ return true;
1319
+ }
1320
+ /**
1188
1321
  * Validate that an attribute is greater than another attribute.
1189
1322
  */
1190
1323
  validateGt(value, parameters, attribute) {
1191
1324
  this.requireParameterCount(1, parameters, "gt");
1192
1325
  if (typeof value !== "number" && typeof value !== "string" && typeof value !== "object") throw "The field under validation must be a number, string, array or object";
1193
- const compartedToValue = deepFind(this.data, parameters[0]) || parameters[0];
1326
+ const compartedToValue = this.getAttributeValue(parameters[0]) || parameters[0];
1194
1327
  if (!Array.isArray(compartedToValue) && isNaN(compartedToValue) === false) return getSize(value, validationRuleParser.hasRule(attribute, getNumericRules(), this.rules)) > compartedToValue;
1195
1328
  if (sameType(value, compartedToValue) === false) throw "The fields under validation must be of the same type";
1196
1329
  return getSize(value) > getSize(compartedToValue);
@@ -1201,7 +1334,7 @@ var validateAttributes = class {
1201
1334
  validateGte(value, parameters, attribute) {
1202
1335
  this.requireParameterCount(1, parameters, "gte");
1203
1336
  if (typeof value !== "number" && typeof value !== "string" && typeof value !== "object") throw "The field under validation must be a number, string, array or object";
1204
- const compartedToValue = deepFind(this.data, parameters[0]) || parameters[0];
1337
+ const compartedToValue = this.getAttributeValue(parameters[0]) || parameters[0];
1205
1338
  if (!Array.isArray(compartedToValue) && isNaN(compartedToValue) === false) return getSize(value, validationRuleParser.hasRule(attribute, getNumericRules(), this.rules)) >= compartedToValue;
1206
1339
  if (sameType(value, compartedToValue) === false) throw "The fields under validation must be of the same type";
1207
1340
  return getSize(value) >= getSize(compartedToValue);
@@ -1212,7 +1345,7 @@ var validateAttributes = class {
1212
1345
  validateLt(value, parameters, attribute) {
1213
1346
  this.requireParameterCount(1, parameters, "lt");
1214
1347
  if (typeof value !== "number" && typeof value !== "string" && typeof value !== "object") throw "The field under validation must be a number, string, array or object";
1215
- const compartedToValue = deepFind(this.data, parameters[0]) || parameters[0];
1348
+ const compartedToValue = this.getAttributeValue(parameters[0]) || parameters[0];
1216
1349
  if (!Array.isArray(compartedToValue) && isNaN(compartedToValue) === false) return getSize(value, validationRuleParser.hasRule(attribute, getNumericRules(), this.rules)) < compartedToValue;
1217
1350
  if (sameType(value, compartedToValue) === false) throw "The fields under validation must be of the same type";
1218
1351
  return getSize(value) < getSize(compartedToValue);
@@ -1223,7 +1356,7 @@ var validateAttributes = class {
1223
1356
  validateLte(value, parameters, attribute) {
1224
1357
  this.requireParameterCount(1, parameters, "lte");
1225
1358
  if (typeof value !== "number" && typeof value !== "string" && typeof value !== "object") throw "The field under validation must be a number, string, array or object";
1226
- const compartedToValue = deepFind(this.data, parameters[0]) || parameters[0];
1359
+ const compartedToValue = this.getAttributeValue(parameters[0]) || parameters[0];
1227
1360
  if (!Array.isArray(compartedToValue) && isNaN(compartedToValue) === false) return getSize(value, validationRuleParser.hasRule(attribute, getNumericRules(), this.rules)) <= compartedToValue;
1228
1361
  if (sameType(value, compartedToValue) === false) throw "The fields under validation must be of the same type";
1229
1362
  return getSize(value) <= getSize(compartedToValue);
@@ -1281,7 +1414,7 @@ var validateAttributes = class {
1281
1414
  compareDates(value, parameter, operator, rule) {
1282
1415
  value = toDate(value);
1283
1416
  if (!value) throw `Validation rule ${rule} requires the field under valation to be a date.`;
1284
- const compartedToValue = toDate(deepFind(this.data, parameter) || parameter);
1417
+ const compartedToValue = toDate(this.getAttributeValue(parameter) || parameter);
1285
1418
  if (!compartedToValue) throw `Validation rule ${rule} requires the parameter to be a date.`;
1286
1419
  return compare(value.getTime(), compartedToValue.getTime(), operator);
1287
1420
  }
@@ -1293,6 +1426,10 @@ var validateAttributes = class {
1293
1426
  }
1294
1427
  /**
1295
1428
  * Prepare the values for validation
1429
+ *
1430
+ * @param other The value to compare against
1431
+ * @param parameters The parameters for the validation rule
1432
+ * @returns The prepared values for validation
1296
1433
  */
1297
1434
  parseDependentRuleParameters(other, parameters) {
1298
1435
  let values = parameters.slice(1);
@@ -1301,6 +1438,55 @@ var validateAttributes = class {
1301
1438
  if (typeof other === "boolean") values = convertValuesToBoolean(values);
1302
1439
  return values;
1303
1440
  }
1441
+ getAttributeValue(attribute) {
1442
+ const dataValue = deepFind(this.data, attribute);
1443
+ if (typeof dataValue !== "undefined") return dataValue;
1444
+ return deepFind(this.context.requestFiles ?? {}, attribute);
1445
+ }
1446
+ getDistinctValues(primaryAttribute) {
1447
+ const gathered = validationData.initializeAndGatherData(primaryAttribute, this.data);
1448
+ const pattern = new RegExp(`^${primaryAttribute.replace(/\./g, "\\.").replace(/\*/g, "[^.]+")}$`);
1449
+ return Object.keys(gathered).filter((attribute) => pattern.test(attribute)).map((attribute) => deepFind(this.data, attribute)).filter((value) => typeof value !== "undefined");
1450
+ }
1451
+ normalizeDistinctValue(value, parameters) {
1452
+ if (parameters.includes("ignore_case") && typeof value === "string") return value.toLowerCase();
1453
+ return value;
1454
+ }
1455
+ isEmailByFilter(value) {
1456
+ return value.toLowerCase().match(/^[^\s@]+@[^\s@]+\.[^\s@]{2,24}$/) !== null;
1457
+ }
1458
+ isStrictEmail(value) {
1459
+ if (!this.isEmailByFilter(value)) return false;
1460
+ const [localPart, domain] = value.split("@");
1461
+ if (!localPart || !domain || localPart.length > 64 || domain.length > 255) return false;
1462
+ if (localPart.startsWith(".") || localPart.endsWith(".") || localPart.includes("..")) return false;
1463
+ return domain.split(".").every((label) => {
1464
+ return label.length > 0 && label.length <= 63 && /^[a-z0-9-]+$/i.test(label) && !label.startsWith("-") && !label.endsWith("-");
1465
+ });
1466
+ }
1467
+ hasResolvableEmailDomain(value) {
1468
+ if (!this.isStrictEmail(value)) return false;
1469
+ const domain = value.split("@")[1];
1470
+ const asciiDomain = domainToASCII(domain);
1471
+ return asciiDomain.length > 0 && asciiDomain.includes(".");
1472
+ }
1473
+ isSpoofSafeEmail(value) {
1474
+ if (!this.isEmailByFilter(value)) return false;
1475
+ const [localPart, domain] = value.split("@");
1476
+ return domainToASCII(domain).length > 0 && !this.hasAsciiControlCharacters(value) && localPart.normalize("NFKC") === localPart && domain.normalize("NFKC") === domain;
1477
+ }
1478
+ hasAsciiControlCharacters(value) {
1479
+ for (let index = 0; index < value.length; index++) {
1480
+ const codePoint = value.charCodeAt(index);
1481
+ if (codePoint <= 31 || codePoint === 127) return true;
1482
+ }
1483
+ return false;
1484
+ }
1485
+ getDecimalPlaces(value) {
1486
+ const stringValue = String(value).toLowerCase();
1487
+ if (!stringValue.includes(".")) return 0;
1488
+ return stringValue.split(".")[1]?.length ?? 0;
1489
+ }
1304
1490
  };
1305
1491
 
1306
1492
  //#endregion
@@ -1369,6 +1555,7 @@ const implicitRues = [
1369
1555
  "declined_if",
1370
1556
  "filled",
1371
1557
  "present",
1558
+ "presentsame",
1372
1559
  "required",
1373
1560
  "required_if",
1374
1561
  "required_unless",
@@ -1997,6 +2184,7 @@ var replaceAttributePayload = class {
1997
2184
  //#endregion
1998
2185
  //#region src/BaseValidator.ts
1999
2186
  var BaseValidator = class BaseValidator {
2187
+ excludedAttributes = /* @__PURE__ */ new Set();
2000
2188
  /**
2001
2189
  * The lang used to return error messages
2002
2190
  */
@@ -2141,6 +2329,9 @@ var BaseValidator = class BaseValidator {
2141
2329
  errors() {
2142
2330
  return this.messages;
2143
2331
  }
2332
+ getExcludedAttributes() {
2333
+ return [...this.excludedAttributes];
2334
+ }
2144
2335
  /**
2145
2336
  * Clear the error messages for the given keys.
2146
2337
  * If no keys are provided, all error messages will be cleared.
@@ -2257,6 +2448,7 @@ var BaseValidator = class BaseValidator {
2257
2448
  runAllValidations() {
2258
2449
  this.messages = new ErrorBag();
2259
2450
  this.validateAttributes = new validateAttributes(this.data, this.rules, this.getContext());
2451
+ this.excludedAttributes.clear();
2260
2452
  for (const property in this.rules) if (this.runValidation(property) === false) break;
2261
2453
  }
2262
2454
  /**
@@ -2265,6 +2457,7 @@ var BaseValidator = class BaseValidator {
2265
2457
  async runAllValidationsAsync() {
2266
2458
  this.messages = new ErrorBag();
2267
2459
  this.validateAttributes = new validateAttributes(this.data, this.rules, this.getContext());
2460
+ this.excludedAttributes.clear();
2268
2461
  for (const property in this.rules) if (await this.runValidationAsync(property) === false) break;
2269
2462
  }
2270
2463
  /**
@@ -2287,26 +2480,39 @@ var BaseValidator = class BaseValidator {
2287
2480
  * Run validation rules for the specified property and stop validation if needed
2288
2481
  */
2289
2482
  runValidation(property) {
2290
- if (Object.prototype.hasOwnProperty.call(this.rules, property) && Array.isArray(this.rules[property])) for (let i = 0; i < this.rules[property].length; i++) {
2291
- this.validateAttribute(property, this.rules[property][i]);
2292
- if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) return false;
2293
- if (this.shouldStopValidating(property)) break;
2483
+ if (Object.prototype.hasOwnProperty.call(this.rules, property) && Array.isArray(this.rules[property])) {
2484
+ if (validationRuleParser.hasRule(property, ["exclude"], this.rules)) {
2485
+ this.excludedAttributes.add(property);
2486
+ return;
2487
+ }
2488
+ for (let i = 0; i < this.rules[property].length; i++) {
2489
+ this.validateAttribute(property, this.rules[property][i]);
2490
+ if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) return false;
2491
+ if (this.shouldStopValidating(property)) break;
2492
+ }
2294
2493
  }
2295
2494
  }
2296
2495
  /**
2297
2496
  * Run validation rules for the specified property asynchronously and stop validation if needed
2298
2497
  */
2299
2498
  async runValidationAsync(property) {
2300
- if (Object.prototype.hasOwnProperty.call(this.rules, property) && Array.isArray(this.rules[property])) for (let i = 0; i < this.rules[property].length; i++) {
2301
- await this.validateAttribute(property, this.rules[property][i]);
2302
- if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) return false;
2303
- if (this.shouldStopValidating(property)) break;
2499
+ if (Object.prototype.hasOwnProperty.call(this.rules, property) && Array.isArray(this.rules[property])) {
2500
+ if (validationRuleParser.hasRule(property, ["exclude"], this.rules)) {
2501
+ this.excludedAttributes.add(property);
2502
+ return;
2503
+ }
2504
+ for (let i = 0; i < this.rules[property].length; i++) {
2505
+ await this.validateAttribute(property, this.rules[property][i]);
2506
+ if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) return false;
2507
+ if (this.shouldStopValidating(property)) break;
2508
+ }
2304
2509
  }
2305
2510
  }
2306
2511
  /**
2307
2512
  * Check if we should stop further validations on a given attribute.
2308
2513
  */
2309
2514
  shouldStopValidating(attribute) {
2515
+ if (this.excludedAttributes.has(attribute)) return true;
2310
2516
  return this.messages.has(attribute) && validationRuleParser.hasRule(attribute, ["bail"], this.rules);
2311
2517
  }
2312
2518
  /**
@@ -2331,7 +2537,7 @@ var BaseValidator = class BaseValidator {
2331
2537
  const method = `validate${buildValidationMethodName(rule)}`;
2332
2538
  if (rule !== "" && typeof this.validateAttributes[method] === "undefined") throw `Rule ${rule} is not valid`;
2333
2539
  if (!validatable) return;
2334
- const validation = this.validateAttributes[method](value, parameters, attribute);
2540
+ const validation = this.validateAttributes[method](value, parameters, attribute, this.getPrimaryAttribute(attribute));
2335
2541
  if (validation instanceof Promise) return validation.then((result) => {
2336
2542
  if (!result) this.addFailure(attribute, rule, value, parameters);
2337
2543
  });
@@ -3096,8 +3302,10 @@ var Validator = class Validator {
3096
3302
  */
3097
3303
  validatedData() {
3098
3304
  const validKeys = Object.keys(this.rules);
3305
+ const excluded = new Set(this.instance?.getExcludedAttributes() ?? []);
3099
3306
  const clean = {};
3100
3307
  for (const key of validKeys) {
3308
+ if (excluded.has(key)) continue;
3101
3309
  const value = deepFind(this.data, key);
3102
3310
  const resolvedValue = typeof value !== "undefined" ? value : deepFind(this.getContext().requestFiles ?? {}, key);
3103
3311
  if (typeof resolvedValue !== "undefined") deepSet(clean, key, resolvedValue);
@@ -3108,7 +3316,8 @@ var Validator = class Validator {
3108
3316
  * Return all validated input.
3109
3317
  */
3110
3318
  validated() {
3111
- return Object.fromEntries(Object.entries(this.data).filter(([key]) => key in this.rules));
3319
+ const excluded = new Set(this.instance?.getExcludedAttributes() ?? []);
3320
+ return Object.fromEntries(Object.entries(this.data).filter(([key]) => key in this.rules && !excluded.has(key)));
3112
3321
  }
3113
3322
  /**
3114
3323
  * Return a portion of validated input
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kanun",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Framework-agnostic TypeScript-first validation library.",
5
5
  "type": "module",
6
6
  "files": [