@warlock.js/seal 4.15.0 → 5.0.0

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 CHANGED
@@ -1,30 +1,47 @@
1
- # Changelog — @warlock.js/seal
2
-
3
- All notable changes to `@warlock.js/seal` are documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
-
1
+ # Changelog — @warlock.js/seal
2
+
3
+ All notable changes to `@warlock.js/seal` are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
+
7
+ ## 5.0.0 - 2026-08-25
8
+
9
+ ### Changed
10
+
11
+ - IP validation no longer imports Node's `net` module, so the same IPv4 and IPv6 rules can run in browser bundles.
12
+ - Optional validators now skip value rules only for absent values; present empty values such as `""` are validated instead of passing through with the wrong output type.
13
+
14
+ ## 4.16.0 - 2026-08-18
15
+
16
+ ### Security
17
+
18
+ - **Renamed `safeHtmlMutator`/`.safeHtml()` to `stripTagsMutator`/`.stripTags()`.** The old name implied XSS safety it never provided — the implementation is a naive `<[^>]*>` regex, not an HTML parser, and can be defeated by malformed/nested markup or content re-introduced later in a pipeline. A developer building `v.string().safeHtml()` for user-supplied rich text was liable to treat the output as pre-sanitized and skip further output encoding, opening a stored/reflected XSS path. The new names and doc comments make clear this is tag-stripping only; use a real parser-based sanitizer (DOMPurify / sanitize-html) for untrusted rich text. `safeHtmlMutator` and `.safeHtml()` remain as deprecated aliases (same behavior, `@deprecated` JSDoc pointing at the new names) so existing callers do not break.
19
+
20
+ ### Dependencies
21
+
22
+ - Bumped `@mongez/supportive-is` to `^2.1.4` (no breaking changes) and `@mongez/reinforcements` to `^4.0.1`. The reinforcements major makes `Random.string/nanoid/id/token/uuid` CSPRNG-backed (WebCrypto) and removes `Random.seed()` support — audited this package's source and tests for `Random.seed(` and for seeded/reproducible use of `Random.*`; none found, so no code changes were needed.
23
+
7
24
  ## 4.12.0
8
25
 
9
26
  ### Changed
10
27
 
11
28
  - Declares its own test runner and pins it to an exact version (`vitest@4.1.10`). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
12
29
 
13
- ## 4.9.2
14
-
15
- ### Fixed
16
-
17
- - `v.literal("")` could never pass. Every validator is required by default and `required` rejects anything the empty-value check calls empty — which includes `""` — so a schema demanding an exact empty string reported "is required" for a field that was present. A literal set containing an empty value now uses `present` (the key must exist) instead of `required`, leaving the literal set to judge the value. Only the empty string was affected; `v.literal(0)` and `v.literal(false)` always worked
18
- - `v.literal("").optional()` silently disabled the literal check rather than fixing it, accepting `""`, `null` **and** a missing key alike. The literal rule now runs on empty values (`requiresValue: false`) while treating absence as the required/present rule's question, so `.optional()` means optional again and a present value must still match
19
- - a **failed** validation no longer returns the input it rejected. `object` returned the raw input — including the unknown keys it had just complained about — while `discriminatedUnion` returned `undefined`; the same call shape had two contracts. Validating an outbound DTO to keep internal fields out of a response, then reading `data` without branching on `isValid`, shipped every field the schema existed to exclude. `data` is now `undefined` whenever `isValid` is `false`
20
- - `v.number().toFixed(n)` could never produce a valid result — the mutator returned `Number(value).toFixed(n)`, a *string*, which the validator's own `number` type rule then rejected. It now yields a number (`3.14159` → `3.14`), so the method works where it lives. No working code can have depended on the old output, since every such validation failed; for a fixed-point *string*, format at the presentation edge rather than asking a number schema to emit one
21
-
22
- ## 4.2.11
23
-
24
- ### Changed
25
-
26
- - Bumped `@mongez/reinforcements` to 3.3.0
27
-
28
- ## 4.1.15
29
-
30
- - Baseline — per-package changelog tracking starts at this version.
30
+ ## 4.9.2
31
+
32
+ ### Fixed
33
+
34
+ - `v.literal("")` could never pass. Every validator is required by default and `required` rejects anything the empty-value check calls empty — which includes `""` — so a schema demanding an exact empty string reported "is required" for a field that was present. A literal set containing an empty value now uses `present` (the key must exist) instead of `required`, leaving the literal set to judge the value. Only the empty string was affected; `v.literal(0)` and `v.literal(false)` always worked
35
+ - `v.literal("").optional()` silently disabled the literal check rather than fixing it, accepting `""`, `null` **and** a missing key alike. The literal rule now runs on empty values (`requiresValue: false`) while treating absence as the required/present rule's question, so `.optional()` means optional again and a present value must still match
36
+ - a **failed** validation no longer returns the input it rejected. `object` returned the raw input — including the unknown keys it had just complained about — while `discriminatedUnion` returned `undefined`; the same call shape had two contracts. Validating an outbound DTO to keep internal fields out of a response, then reading `data` without branching on `isValid`, shipped every field the schema existed to exclude. `data` is now `undefined` whenever `isValid` is `false`
37
+ - `v.number().toFixed(n)` could never produce a valid result — the mutator returned `Number(value).toFixed(n)`, a *string*, which the validator's own `number` type rule then rejected. It now yields a number (`3.14159` → `3.14`), so the method works where it lives. No working code can have depended on the old output, since every such validation failed; for a fixed-point *string*, format at the presentation edge rather than asking a number schema to emit one
38
+
39
+ ## 4.2.11
40
+
41
+ ### Changed
42
+
43
+ - Bumped `@mongez/reinforcements` to 3.3.0
44
+
45
+ ## 4.1.15
46
+
47
+ - Baseline — per-package changelog tracking starts at this version.
package/cjs/index.cjs CHANGED
@@ -30,7 +30,6 @@ let _mongez_reinforcements = require("@mongez/reinforcements");
30
30
  let _mongez_supportive_is = require("@mongez/supportive-is");
31
31
  let dayjs = require("dayjs");
32
32
  dayjs = __toESM(dayjs, 1);
33
- let net = require("net");
34
33
 
35
34
  //#region ../seal/src/standard-schema/json-schema.ts
36
35
  /**
@@ -935,9 +934,10 @@ var BaseValidator = class {
935
934
  let isValid = true;
936
935
  const isFirstErrorOnly = context.configurations?.firstErrorOnly ?? true;
937
936
  const isEmpty = isEmptyValue(valueForRules);
937
+ const skipEmptyRules = this.isOptional ? valueForRules === void 0 : isEmpty;
938
938
  const rulesToRun = this.requiredRule ? [this.requiredRule, ...this.rules] : this.rules;
939
939
  for (const rule of rulesToRun) {
940
- if ((rule.requiresValue ?? true) && isEmpty) continue;
940
+ if ((rule.requiresValue ?? true) && skipEmptyRules) continue;
941
941
  this.setRuleAttributesList(rule);
942
942
  const result = await rule.validate(mutatedData, context);
943
943
  if (result.isValid === false) {
@@ -1354,10 +1354,24 @@ const stringifyMutator = async (value) => {
1354
1354
  const trimMutator = async (value, context) => {
1355
1355
  return (0, _mongez_reinforcements.trim)(value?.toString(), context?.options?.needle ?? " ");
1356
1356
  };
1357
- /** Remove HTML tags (safe HTML) */
1358
- const safeHtmlMutator = async (value) => {
1357
+ /**
1358
+ * Strip HTML tags with a naive `<...>` regex.
1359
+ *
1360
+ * This is a **tag stripper, not an XSS sanitizer**: it is not a parser, so
1361
+ * malformed/nested markup, attribute payloads, or content re-introduced
1362
+ * later in a pipeline can defeat it. Do not rely on this alone to make
1363
+ * untrusted rich text safe to render — use a real parser-based sanitizer
1364
+ * (e.g. DOMPurify / sanitize-html) for that.
1365
+ */
1366
+ const stripTagsMutator = async (value) => {
1359
1367
  return value?.toString().replace(/<[^>]*>?/gm, "");
1360
1368
  };
1369
+ /**
1370
+ * @deprecated Renamed to {@link stripTagsMutator} — the old name implied
1371
+ * XSS safety this tag-stripping regex does not provide. Kept as an alias
1372
+ * for backward compatibility; switch to `stripTagsMutator`.
1373
+ */
1374
+ const safeHtmlMutator = stripTagsMutator;
1361
1375
  /** HTML escape */
1362
1376
  const htmlEscapeMutator = async (value) => {
1363
1377
  return value?.toString().replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
@@ -1755,13 +1769,38 @@ const nanoidRule = {
1755
1769
  //#endregion
1756
1770
  //#region ../seal/src/rules/string/ip.ts
1757
1771
  /**
1772
+ * Pure, dependency-free re-implementation of Node's `net.isIP`/`isIPv4`/`isIPv6`.
1773
+ * Mirrors the exact regex Node uses internally (lib/internal/net.js) so that
1774
+ * behavior stays identical while remaining runnable in the browser (no `net` import).
1775
+ */
1776
+ const v4Seg = "(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])";
1777
+ const v4Str = `(${v4Seg}[.]){3}${v4Seg}`;
1778
+ const IPv4Reg = new RegExp(`^${v4Str}$`);
1779
+ const v6Seg = "(?:[0-9a-fA-F]{1,4})";
1780
+ const IPv6Reg = new RegExp(`^((?:${v6Seg}:){7}(?:${v6Seg}|:)|(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|(?:${v6Seg}:){5}(?::${v4Str}|(:${v6Seg}){1,2}|:)|(?:${v6Seg}:){4}(?:(:${v6Seg}){0,1}:${v4Str}|(:${v6Seg}){1,3}|:)|(?:${v6Seg}:){3}(?:(:${v6Seg}){0,2}:${v4Str}|(:${v6Seg}){1,4}|:)|(?:${v6Seg}:){2}(?:(:${v6Seg}){0,3}:${v4Str}|(:${v6Seg}){1,5}|:)|(?:${v6Seg}:){1}(?:(:${v6Seg}){0,4}:${v4Str}|(:${v6Seg}){1,6}|:)|(?::((?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:)))(%[0-9a-zA-Z-.:]{1,})?\$`);
1781
+ function isIPv4(value) {
1782
+ return IPv4Reg.test(value);
1783
+ }
1784
+ function isIPv6(value) {
1785
+ return IPv6Reg.test(value);
1786
+ }
1787
+ /**
1788
+ * Returns 4 or 6 when `value` is a valid IPv4/IPv6 address, otherwise 0.
1789
+ * Signature mirrors Node's `net.isIP` for drop-in behavior parity.
1790
+ */
1791
+ function isIP(value) {
1792
+ if (isIPv4(value)) return 4;
1793
+ if (isIPv6(value)) return 6;
1794
+ return 0;
1795
+ }
1796
+ /**
1758
1797
  * IP rule - validates IP address (v4 or v6)
1759
1798
  */
1760
1799
  const ipRule = {
1761
1800
  name: "ip",
1762
1801
  defaultErrorMessage: "The :input must be a valid IP address",
1763
1802
  async validate(value, context) {
1764
- if ((0, net.isIP)(value)) return VALID_RULE;
1803
+ if (isIP(value)) return VALID_RULE;
1765
1804
  return invalidRule(this, context);
1766
1805
  }
1767
1806
  };
@@ -1772,7 +1811,7 @@ const ip4Rule = {
1772
1811
  name: "ip4",
1773
1812
  defaultErrorMessage: "The :input must be a valid IPv4 address",
1774
1813
  async validate(value, context) {
1775
- if ((0, net.isIP)(value) === 4) return VALID_RULE;
1814
+ if (isIP(value) === 4) return VALID_RULE;
1776
1815
  return invalidRule(this, context);
1777
1816
  }
1778
1817
  };
@@ -1783,7 +1822,7 @@ const ip6Rule = {
1783
1822
  name: "ip6",
1784
1823
  defaultErrorMessage: "The :input must be a valid IPv6 address",
1785
1824
  async validate(value, context) {
1786
- if ((0, net.isIP)(value) === 6) return VALID_RULE;
1825
+ if (isIP(value) === 6) return VALID_RULE;
1787
1826
  return invalidRule(this, context);
1788
1827
  }
1789
1828
  };
@@ -7985,9 +8024,25 @@ var StringValidator = class extends PrimitiveValidator {
7985
8024
  char
7986
8025
  });
7987
8026
  }
7988
- /** Remove HTML tags (safe HTML) */
8027
+ /**
8028
+ * Strip HTML tags with a naive `<...>` regex.
8029
+ *
8030
+ * This is a **tag stripper, not an XSS sanitizer**: it is not a parser,
8031
+ * so malformed/nested markup or content re-introduced later in a
8032
+ * pipeline can defeat it. Do not rely on this alone to make untrusted
8033
+ * rich text safe to render — use a real parser-based sanitizer (e.g.
8034
+ * DOMPurify / sanitize-html) for that.
8035
+ */
8036
+ stripTags() {
8037
+ return this.addMutator(stripTagsMutator);
8038
+ }
8039
+ /**
8040
+ * @deprecated Renamed to {@link StringValidator.stripTags} — the old
8041
+ * name implied XSS safety this tag-stripping regex does not provide.
8042
+ * Kept as an alias for backward compatibility; switch to `.stripTags()`.
8043
+ */
7989
8044
  safeHtml() {
7990
- return this.addMutator(safeHtmlMutator);
8045
+ return this.stripTags();
7991
8046
  }
7992
8047
  /** HTML escape special characters */
7993
8048
  htmlEscape() {
@@ -8781,6 +8836,9 @@ exports.ip6Rule = ip6Rule;
8781
8836
  exports.ipRule = ipRule;
8782
8837
  exports.isCreditCardRule = isCreditCardRule;
8783
8838
  exports.isDateValue = isDateValue;
8839
+ exports.isIP = isIP;
8840
+ exports.isIPv4 = isIPv4;
8841
+ exports.isIPv6 = isIPv6;
8784
8842
  exports.isNumericRule = isNumericRule;
8785
8843
  exports.jsonMutator = jsonMutator;
8786
8844
  exports.kebabCaseMutator = kebabCaseMutator;
@@ -8900,6 +8958,7 @@ exports.startsWithRule = startsWithRule;
8900
8958
  exports.stringMutator = stringMutator;
8901
8959
  exports.stringRule = stringRule;
8902
8960
  exports.stringifyMutator = stringifyMutator;
8961
+ exports.stripTagsMutator = stripTagsMutator;
8903
8962
  exports.stripUnknownMutator = stripUnknownMutator;
8904
8963
  exports.strongPasswordRule = strongPasswordRule;
8905
8964
  exports.titleCaseMutator = titleCaseMutator;