@zudojs/validation 0.1.0 → 1.0.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.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/dist/validationComposer/index.d.ts +3 -1
  4. package/dist/validationComposer/index.js +3 -1
  5. package/dist/validationComposer/validationComposer.combinators.d.ts +21 -0
  6. package/dist/validationComposer/validationComposer.combinators.js +78 -0
  7. package/dist/validationComposer/validationComposer.core.d.ts +10 -24
  8. package/dist/validationComposer/validationComposer.core.js +2 -139
  9. package/dist/validationComposer/validationComposer.wrappers.d.ts +38 -0
  10. package/dist/validationComposer/validationComposer.wrappers.js +96 -0
  11. package/dist/validationConstraints/collection/index.d.ts +8 -0
  12. package/dist/validationConstraints/collection/index.js +8 -0
  13. package/dist/validationConstraints/{validationConstraints.array.d.ts → collection/validationConstraints.array.d.ts} +1 -1
  14. package/dist/validationConstraints/{validationConstraints.array.js → collection/validationConstraints.array.js} +23 -3
  15. package/dist/validationConstraints/{validationConstraints.collection.d.ts → collection/validationConstraints.collection.d.ts} +1 -1
  16. package/dist/validationConstraints/{validationConstraints.collection.js → collection/validationConstraints.collection.js} +1 -1
  17. package/dist/validationConstraints/index.d.ts +3 -8
  18. package/dist/validationConstraints/index.js +6 -16
  19. package/dist/validationConstraints/scalar/index.d.ts +9 -0
  20. package/dist/validationConstraints/scalar/index.js +9 -0
  21. package/dist/validationConstraints/scalar/validationConstraints.date.d.ts +13 -0
  22. package/dist/validationConstraints/scalar/validationConstraints.date.js +45 -0
  23. package/dist/validationConstraints/{validationConstraints.number.d.ts → scalar/validationConstraints.number.d.ts} +1 -1
  24. package/dist/validationConstraints/{validationConstraints.number.js → scalar/validationConstraints.number.js} +12 -1
  25. package/dist/validationConstraints/{validationConstraints.string.d.ts → scalar/validationConstraints.string.d.ts} +18 -3
  26. package/dist/validationConstraints/{validationConstraints.string.js → scalar/validationConstraints.string.js} +49 -13
  27. package/dist/validationConstraints/structure/index.d.ts +10 -0
  28. package/dist/validationConstraints/structure/index.js +10 -0
  29. package/dist/validationConstraints/{validationConstraints.circular.d.ts → structure/validationConstraints.circular.d.ts} +8 -4
  30. package/dist/validationConstraints/structure/validationConstraints.circular.js +55 -0
  31. package/dist/validationConstraints/structure/validationConstraints.depth.d.ts +37 -0
  32. package/dist/validationConstraints/structure/validationConstraints.depth.js +58 -0
  33. package/dist/validationConstraints/structure/validationConstraints.size.d.ts +31 -0
  34. package/dist/validationConstraints/structure/validationConstraints.size.js +101 -0
  35. package/dist/validationConstraints/structure/validationConstraints.traverse.d.ts +57 -0
  36. package/dist/validationConstraints/structure/validationConstraints.traverse.js +118 -0
  37. package/dist/validationConstraints/validationConstraints.base.d.ts +18 -3
  38. package/dist/validationConstraints/validationConstraints.base.js +50 -25
  39. package/dist/validationErrors/validationError.base.js +28 -1
  40. package/dist/validationFactory/validationFactory.core.d.ts +9 -8
  41. package/dist/validationFactory/validationFactory.core.js +11 -9
  42. package/dist/validationNormalizer/validationNormalizer.core.js +2 -4
  43. package/dist/validationNormalizer/validationNormalizer.helpers.d.ts +36 -3
  44. package/dist/validationNormalizer/validationNormalizer.helpers.js +48 -7
  45. package/dist/validationParser/index.d.ts +2 -1
  46. package/dist/validationParser/index.js +2 -1
  47. package/dist/validationParser/validationParser.collection.d.ts +28 -0
  48. package/dist/validationParser/validationParser.collection.js +98 -0
  49. package/dist/validationParser/validationParser.core.d.ts +0 -12
  50. package/dist/validationParser/validationParser.core.js +0 -71
  51. package/dist/validationRegistry/validationRegistry.core.d.ts +8 -0
  52. package/dist/validationRegistry/validationRegistry.core.js +15 -1
  53. package/dist/validationResult/validationResult.type.d.ts +8 -1
  54. package/dist/validationResult/validationResult.type.js +20 -5
  55. package/dist/validationTransformer/index.d.ts +2 -1
  56. package/dist/validationTransformer/index.js +2 -1
  57. package/dist/validationTransformer/validationTransformer.core.d.ts +0 -15
  58. package/dist/validationTransformer/validationTransformer.core.js +5 -56
  59. package/dist/validationTransformer/validationTransformer.helpers.d.ts +23 -0
  60. package/dist/validationTransformer/validationTransformer.helpers.js +67 -0
  61. package/package.json +24 -13
  62. package/dist/.tsbuildinfo +0 -1
  63. package/dist/index.d.ts.map +0 -1
  64. package/dist/index.js.map +0 -1
  65. package/dist/validationComposer/index.d.ts.map +0 -1
  66. package/dist/validationComposer/index.js.map +0 -1
  67. package/dist/validationComposer/validationComposer.core.d.ts.map +0 -1
  68. package/dist/validationComposer/validationComposer.core.js.map +0 -1
  69. package/dist/validationConstraints/index.d.ts.map +0 -1
  70. package/dist/validationConstraints/index.js.map +0 -1
  71. package/dist/validationConstraints/validationConstraints.array.d.ts.map +0 -1
  72. package/dist/validationConstraints/validationConstraints.array.js.map +0 -1
  73. package/dist/validationConstraints/validationConstraints.base.d.ts.map +0 -1
  74. package/dist/validationConstraints/validationConstraints.base.js.map +0 -1
  75. package/dist/validationConstraints/validationConstraints.circular.d.ts.map +0 -1
  76. package/dist/validationConstraints/validationConstraints.circular.js +0 -73
  77. package/dist/validationConstraints/validationConstraints.circular.js.map +0 -1
  78. package/dist/validationConstraints/validationConstraints.collection.d.ts.map +0 -1
  79. package/dist/validationConstraints/validationConstraints.collection.js.map +0 -1
  80. package/dist/validationConstraints/validationConstraints.date.d.ts +0 -14
  81. package/dist/validationConstraints/validationConstraints.date.d.ts.map +0 -1
  82. package/dist/validationConstraints/validationConstraints.date.js +0 -30
  83. package/dist/validationConstraints/validationConstraints.date.js.map +0 -1
  84. package/dist/validationConstraints/validationConstraints.depth.d.ts +0 -20
  85. package/dist/validationConstraints/validationConstraints.depth.d.ts.map +0 -1
  86. package/dist/validationConstraints/validationConstraints.depth.js +0 -70
  87. package/dist/validationConstraints/validationConstraints.depth.js.map +0 -1
  88. package/dist/validationConstraints/validationConstraints.number.d.ts.map +0 -1
  89. package/dist/validationConstraints/validationConstraints.number.js.map +0 -1
  90. package/dist/validationConstraints/validationConstraints.size.d.ts +0 -20
  91. package/dist/validationConstraints/validationConstraints.size.d.ts.map +0 -1
  92. package/dist/validationConstraints/validationConstraints.size.js +0 -82
  93. package/dist/validationConstraints/validationConstraints.size.js.map +0 -1
  94. package/dist/validationConstraints/validationConstraints.string.d.ts.map +0 -1
  95. package/dist/validationConstraints/validationConstraints.string.js.map +0 -1
  96. package/dist/validationErrors/index.d.ts.map +0 -1
  97. package/dist/validationErrors/index.js.map +0 -1
  98. package/dist/validationErrors/validationError.base.d.ts.map +0 -1
  99. package/dist/validationErrors/validationError.base.js.map +0 -1
  100. package/dist/validationErrors/validationError.types.d.ts.map +0 -1
  101. package/dist/validationErrors/validationError.types.js.map +0 -1
  102. package/dist/validationFactory/index.d.ts.map +0 -1
  103. package/dist/validationFactory/index.js.map +0 -1
  104. package/dist/validationFactory/validationFactory.core.d.ts.map +0 -1
  105. package/dist/validationFactory/validationFactory.core.js.map +0 -1
  106. package/dist/validationNormalizer/index.d.ts.map +0 -1
  107. package/dist/validationNormalizer/index.js.map +0 -1
  108. package/dist/validationNormalizer/validationNormalizer.core.d.ts.map +0 -1
  109. package/dist/validationNormalizer/validationNormalizer.core.js.map +0 -1
  110. package/dist/validationNormalizer/validationNormalizer.helpers.d.ts.map +0 -1
  111. package/dist/validationNormalizer/validationNormalizer.helpers.js.map +0 -1
  112. package/dist/validationParser/index.d.ts.map +0 -1
  113. package/dist/validationParser/index.js.map +0 -1
  114. package/dist/validationParser/validationParser.core.d.ts.map +0 -1
  115. package/dist/validationParser/validationParser.core.js.map +0 -1
  116. package/dist/validationRegistry/index.d.ts.map +0 -1
  117. package/dist/validationRegistry/index.js.map +0 -1
  118. package/dist/validationRegistry/validationRegistry.core.d.ts.map +0 -1
  119. package/dist/validationRegistry/validationRegistry.core.js.map +0 -1
  120. package/dist/validationRegistry/validationRegistry.readonly.d.ts.map +0 -1
  121. package/dist/validationRegistry/validationRegistry.readonly.js.map +0 -1
  122. package/dist/validationResult/index.d.ts.map +0 -1
  123. package/dist/validationResult/index.js.map +0 -1
  124. package/dist/validationResult/validationResult.type.d.ts.map +0 -1
  125. package/dist/validationResult/validationResult.type.js.map +0 -1
  126. package/dist/validationSchema/index.d.ts.map +0 -1
  127. package/dist/validationSchema/index.js.map +0 -1
  128. package/dist/validationSchema/validationSchema.core.d.ts.map +0 -1
  129. package/dist/validationSchema/validationSchema.core.js.map +0 -1
  130. package/dist/validationTransformer/index.d.ts.map +0 -1
  131. package/dist/validationTransformer/index.js.map +0 -1
  132. package/dist/validationTransformer/validationTransformer.core.d.ts.map +0 -1
  133. package/dist/validationTransformer/validationTransformer.core.js.map +0 -1
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @zudojs/validation — Size checking.
3
+ *
4
+ * Estimates serialized payload size to prevent memory exhaustion
5
+ * from excessively large payloads.
6
+ */
7
+ import { SerializationPayloadTooLargeError } from "@zudojs/errors";
8
+ import { MAX_MEASURABLE_DEPTH } from "./validationConstraints.depth.js";
9
+ import { TraversalLimitError, traverse, } from "./validationConstraints.traverse.js";
10
+ /** Bytes charged for a single node, excluding its children. */
11
+ function chargeFor(value) {
12
+ if (value === null || value === undefined)
13
+ return 4;
14
+ switch (typeof value) {
15
+ case "string":
16
+ return value.length * 2 + 2;
17
+ case "number":
18
+ // JSON emits up to 21 characters for a double; charge the worst case
19
+ // rather than a flat 8, which under-counted every numeric field.
20
+ return 21;
21
+ case "boolean":
22
+ return 5;
23
+ case "bigint":
24
+ return value.toString().length + 2;
25
+ case "symbol":
26
+ case "function":
27
+ return 0;
28
+ }
29
+ if (ArrayBuffer.isView(value))
30
+ return value.byteLength;
31
+ if (value instanceof Date)
32
+ return 26;
33
+ if (value instanceof RegExp)
34
+ return value.source.length + 4;
35
+ if (Array.isArray(value))
36
+ return 2 + Math.max(0, value.length - 1);
37
+ if (value instanceof Map)
38
+ return 2 + Math.max(0, value.size * 3 - 1);
39
+ if (value instanceof Set)
40
+ return 2 + Math.max(0, value.size - 1);
41
+ const keys = Object.keys(value);
42
+ return (2 +
43
+ keys.reduce((total, key) => total + key.length + 4, 0) +
44
+ Math.max(0, keys.length - 1));
45
+ }
46
+ /**
47
+ * Estimate the byte size of a value as JSON without allocating a string.
48
+ *
49
+ * A value referenced from several places is charged once per occurrence, the
50
+ * way a serializer expands it. Counting it once let a compact payload built
51
+ * from shared subtrees estimate at a few hundred bytes while serializing to
52
+ * hundreds of megabytes.
53
+ *
54
+ * @param value - The value to measure.
55
+ * @param maxBytes - Stop counting past this budget; the budget is returned.
56
+ * @returns The estimated serialized size in bytes.
57
+ */
58
+ export function estimateSerializedSize(value, maxBytes = Number.POSITIVE_INFINITY) {
59
+ try {
60
+ return traverse(value, {
61
+ maxDepth: MAX_MEASURABLE_DEPTH,
62
+ maxCost: maxBytes,
63
+ charge: chargeFor,
64
+ }).cost;
65
+ }
66
+ catch (error) {
67
+ if (error instanceof TraversalLimitError) {
68
+ return error.halt === "budget" ? error.observed : maxBytes;
69
+ }
70
+ throw error;
71
+ }
72
+ }
73
+ /**
74
+ * Assert that a value's estimated serialized size is within limits.
75
+ *
76
+ * Aborts as soon as the budget is passed, so an oversized payload is rejected
77
+ * without first being measured in full.
78
+ *
79
+ * @param value - The value to check.
80
+ * @param maxSize - Maximum permitted size in bytes.
81
+ * @throws {SerializationPayloadTooLargeError} when the estimate exceeds maxSize.
82
+ */
83
+ export function assertSizeWithinLimit(value, maxSize) {
84
+ try {
85
+ traverse(value, {
86
+ maxDepth: MAX_MEASURABLE_DEPTH,
87
+ maxCost: maxSize,
88
+ charge: chargeFor,
89
+ });
90
+ }
91
+ catch (error) {
92
+ if (error instanceof TraversalLimitError && error.halt === "budget") {
93
+ throw new SerializationPayloadTooLargeError(error.observed, maxSize);
94
+ }
95
+ if (error instanceof TraversalLimitError && error.halt === "depth") {
96
+ throw new SerializationPayloadTooLargeError(error.observed, maxSize);
97
+ }
98
+ throw error;
99
+ }
100
+ }
101
+ //# sourceMappingURL=validationConstraints.size.js.map
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @zudojs/validation — Bounded object-graph traversal.
3
+ *
4
+ * The depth, size and circular-reference guards all walk the same graph and
5
+ * all previously got the same three details wrong.
6
+ *
7
+ * The visited set must track the current *path*, not everything ever seen:
8
+ * marking on descent and unmarking on ascent detects a genuine cycle while
9
+ * still counting a shared subtree once per occurrence, the way a serializer
10
+ * would expand it.
11
+ *
12
+ * The walk must be bounded, and abort the moment a bound is passed. A guard
13
+ * that measures the whole input before comparing it to a limit is not a
14
+ * guard — it does the expensive work regardless.
15
+ *
16
+ * And the walk must be iterative. A recursive walker overflows the stack on
17
+ * exactly the deeply nested input the depth guard exists to reject, so the
18
+ * check would fail inside itself before it could report anything.
19
+ */
20
+ /** Why a traversal stopped early. */
21
+ export type TraversalHalt = "depth" | "budget" | "cycle";
22
+ /** Signals that a traversal hit one of its bounds. */
23
+ export declare class TraversalLimitError extends Error {
24
+ readonly halt: TraversalHalt;
25
+ readonly path: string;
26
+ readonly observed: number;
27
+ constructor(halt: TraversalHalt, path: string, observed: number);
28
+ }
29
+ /** What the caller wants from each node. */
30
+ export interface TraversalVisitor {
31
+ /** Maximum nesting depth to descend before halting. */
32
+ readonly maxDepth: number;
33
+ /** Cumulative budget; halts once `charge` totals more than this. */
34
+ readonly maxCost?: number;
35
+ /** Whether a cycle should halt the walk rather than be skipped. */
36
+ readonly failOnCycle?: boolean;
37
+ /** Cost contributed by a single node, excluding its children. */
38
+ charge?(value: unknown): number;
39
+ }
40
+ /** What a completed traversal observed. */
41
+ export interface TraversalReport {
42
+ /** Deepest nesting level reached. */
43
+ readonly depth: number;
44
+ /** Total cost accumulated across every visited node. */
45
+ readonly cost: number;
46
+ }
47
+ /**
48
+ * Walk a value graph within explicit depth and cost bounds.
49
+ *
50
+ * @param root - The value to walk.
51
+ * @param visitor - Bounds and per-node cost.
52
+ * @param rootPath - Label for the root node in error paths.
53
+ * @returns What the traversal observed, when it completed within bounds.
54
+ * @throws {TraversalLimitError} as soon as a bound is exceeded.
55
+ */
56
+ export declare function traverse(root: unknown, visitor: TraversalVisitor, rootPath?: string): TraversalReport;
57
+ //# sourceMappingURL=validationConstraints.traverse.d.ts.map
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @zudojs/validation — Bounded object-graph traversal.
3
+ *
4
+ * The depth, size and circular-reference guards all walk the same graph and
5
+ * all previously got the same three details wrong.
6
+ *
7
+ * The visited set must track the current *path*, not everything ever seen:
8
+ * marking on descent and unmarking on ascent detects a genuine cycle while
9
+ * still counting a shared subtree once per occurrence, the way a serializer
10
+ * would expand it.
11
+ *
12
+ * The walk must be bounded, and abort the moment a bound is passed. A guard
13
+ * that measures the whole input before comparing it to a limit is not a
14
+ * guard — it does the expensive work regardless.
15
+ *
16
+ * And the walk must be iterative. A recursive walker overflows the stack on
17
+ * exactly the deeply nested input the depth guard exists to reject, so the
18
+ * check would fail inside itself before it could report anything.
19
+ */
20
+ /** Signals that a traversal hit one of its bounds. */
21
+ export class TraversalLimitError extends Error {
22
+ halt;
23
+ path;
24
+ observed;
25
+ constructor(halt, path, observed) {
26
+ super(`Traversal halted (${halt}) at ${path}`);
27
+ this.halt = halt;
28
+ this.path = path;
29
+ this.observed = observed;
30
+ this.name = "TraversalLimitError";
31
+ }
32
+ }
33
+ /** Whether a value has children worth descending into. */
34
+ function isContainer(value) {
35
+ return (typeof value === "object" && value !== null && !ArrayBuffer.isView(value));
36
+ }
37
+ /** The child values of a container, as [pathSegment, value] pairs. */
38
+ function childrenOf(value) {
39
+ if (Array.isArray(value)) {
40
+ return value.map((child, index) => [`[${index}]`, child]);
41
+ }
42
+ if (value instanceof Map) {
43
+ const children = [];
44
+ let index = 0;
45
+ for (const [key, entry] of value) {
46
+ children.push([`.key(${index})`, key], [`[${String(key)}]`, entry]);
47
+ index++;
48
+ }
49
+ return children;
50
+ }
51
+ if (value instanceof Set) {
52
+ return [...value].map((entry, index) => [`.item(${index})`, entry]);
53
+ }
54
+ if (value instanceof Date || value instanceof RegExp)
55
+ return [];
56
+ const record = value;
57
+ return Object.keys(record).map((key) => [`.${key}`, record[key]]);
58
+ }
59
+ /**
60
+ * Walk a value graph within explicit depth and cost bounds.
61
+ *
62
+ * @param root - The value to walk.
63
+ * @param visitor - Bounds and per-node cost.
64
+ * @param rootPath - Label for the root node in error paths.
65
+ * @returns What the traversal observed, when it completed within bounds.
66
+ * @throws {TraversalLimitError} as soon as a bound is exceeded.
67
+ */
68
+ export function traverse(root, visitor, rootPath = "root") {
69
+ const maxCost = visitor.maxCost ?? Number.POSITIVE_INFINITY;
70
+ const onPath = new Set();
71
+ const stack = [{ value: root, depth: 0, path: rootPath }];
72
+ let cost = 0;
73
+ let deepest = 0;
74
+ while (stack.length > 0) {
75
+ const frame = stack.pop();
76
+ if (frame.leave) {
77
+ onPath.delete(frame.leave);
78
+ continue;
79
+ }
80
+ const { value, depth, path } = frame;
81
+ cost += visitor.charge?.(value) ?? 0;
82
+ if (cost > maxCost)
83
+ throw new TraversalLimitError("budget", path, cost);
84
+ if (depth > deepest)
85
+ deepest = depth;
86
+ if (!isContainer(value))
87
+ continue;
88
+ // A container occupies the level below the one it sits at, so an empty
89
+ // `{}` at depth d reaches d + 1 — the same level at which the depth
90
+ // guard below refuses to descend into it. Reporting only leaf depths
91
+ // made `getSerializationDepth({})` 0 while `assertDepthWithinLimit({},
92
+ // 0)` threw.
93
+ if (depth + 1 > deepest)
94
+ deepest = depth + 1;
95
+ if (onPath.has(value)) {
96
+ if (visitor.failOnCycle) {
97
+ throw new TraversalLimitError("cycle", path, depth);
98
+ }
99
+ continue;
100
+ }
101
+ if (depth >= visitor.maxDepth) {
102
+ throw new TraversalLimitError("depth", path, depth + 1);
103
+ }
104
+ onPath.add(value);
105
+ stack.push({ value: undefined, depth, path, leave: value });
106
+ const children = childrenOf(value);
107
+ for (let index = children.length - 1; index >= 0; index--) {
108
+ const [segment, child] = children[index];
109
+ stack.push({
110
+ value: child,
111
+ depth: depth + 1,
112
+ path: `${path}${segment}`,
113
+ });
114
+ }
115
+ }
116
+ return { depth: deepest, cost };
117
+ }
118
+ //# sourceMappingURL=validationConstraints.traverse.js.map
@@ -7,21 +7,36 @@ export interface ValidationConstraint<T> {
7
7
  readonly validate: (value: T) => boolean;
8
8
  readonly message: string;
9
9
  readonly code: string;
10
+ /**
11
+ * Narrows an arbitrary value to the type `validate` expects.
12
+ *
13
+ * Constraints are frequently handed values straight off a trust boundary,
14
+ * where the declared type parameter guarantees nothing. Without a guard,
15
+ * `everyItem(...)` called on a number throws a raw `TypeError` out of the
16
+ * validator, turning a 400 into a 500.
17
+ */
18
+ readonly guard?: (value: unknown) => value is T;
10
19
  }
11
20
  /**
12
21
  * Options for creating a validation constraint.
13
22
  */
14
- export interface ConstraintOptions {
23
+ export interface ConstraintOptions<T = unknown> {
15
24
  readonly name?: string;
16
25
  readonly code?: string;
17
26
  readonly message?: string;
27
+ readonly guard?: (value: unknown) => value is T;
18
28
  }
19
29
  /**
20
30
  * Creates a reusable validation constraint.
21
31
  */
22
- export declare function createConstraint<T>(validate: (value: T) => boolean, options?: ConstraintOptions): ValidationConstraint<T>;
32
+ export declare function createConstraint<T>(validate: (value: T) => boolean, options?: ConstraintOptions<T>): ValidationConstraint<T>;
23
33
  /**
24
34
  * Executes a constraint against a value.
35
+ *
36
+ * The rejected value is deliberately *not* attached to the issue. Issues flow
37
+ * into `ValidationError`, which is exposed with a 400, so echoing the input
38
+ * would return rejected passwords, tokens and PII to the caller and write them
39
+ * to any log that serializes the error.
25
40
  */
26
41
  export declare function checkConstraint<T>(constraint: ValidationConstraint<T>, value: T, path?: readonly (string | number)[]): ValidationResult<T>;
27
42
  /**
@@ -35,7 +50,7 @@ export declare function combineConstraints<T>(...constraints: readonly Validatio
35
50
  /**
36
51
  * Creates a negated constraint.
37
52
  */
38
- export declare function not<T>(constraint: ValidationConstraint<T>, options?: ConstraintOptions): ValidationConstraint<T>;
53
+ export declare function not<T>(constraint: ValidationConstraint<T>, options?: ConstraintOptions<T>): ValidationConstraint<T>;
39
54
  /**
40
55
  * Requires a value to be defined.
41
56
  */
@@ -3,30 +3,61 @@ import { failure, success, } from "../validationResult/validationResult.type.js"
3
3
  * Creates a reusable validation constraint.
4
4
  */
5
5
  export function createConstraint(validate, options = {}) {
6
- const name = options.name ?? "custom";
7
- const code = options.code ?? "constraint_failed";
8
- const message = options.message ?? "Validation constraint failed.";
6
+ const guard = options.guard;
7
+ /**
8
+ * `validate` is public, so it is called directly as often as it is called
9
+ * through `checkConstraints`. Applying the guard here rather than only at
10
+ * the call site means no entry point can be handed a wrong-typed value and
11
+ * throw a raw `TypeError` back at a trust boundary.
12
+ */
13
+ const guarded = guard
14
+ ? (value) => guard(value) && validate(value)
15
+ : validate;
9
16
  return Object.freeze({
10
- name,
11
- validate,
12
- message,
13
- code,
17
+ name: options.name ?? "custom",
18
+ validate: guarded,
19
+ message: options.message ?? "Validation constraint failed.",
20
+ code: options.code ?? "constraint_failed",
21
+ ...(guard ? { guard } : {}),
14
22
  });
15
23
  }
16
24
  /**
17
- * Executes a constraint against a value.
25
+ * Run a constraint against a value of unknown type.
26
+ *
27
+ * Returns false rather than throwing when the value is the wrong shape, so a
28
+ * mistyped field reports as a validation failure instead of escaping as an
29
+ * unhandled error.
18
30
  */
19
- export function checkConstraint(constraint, value, path = []) {
20
- if (constraint.validate(value)) {
21
- return success(value);
31
+ function runConstraint(constraint, value) {
32
+ if (constraint.guard && !constraint.guard(value))
33
+ return false;
34
+ try {
35
+ return constraint.validate(value);
22
36
  }
23
- const validationIssue = {
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ /** Builds the issue describing a failed constraint. */
42
+ function toIssue(constraint, path) {
43
+ return {
24
44
  path: [...path],
25
45
  code: constraint.code,
26
46
  message: constraint.message,
27
- received: value,
28
47
  };
29
- return failure([validationIssue]);
48
+ }
49
+ /**
50
+ * Executes a constraint against a value.
51
+ *
52
+ * The rejected value is deliberately *not* attached to the issue. Issues flow
53
+ * into `ValidationError`, which is exposed with a 400, so echoing the input
54
+ * would return rejected passwords, tokens and PII to the caller and write them
55
+ * to any log that serializes the error.
56
+ */
57
+ export function checkConstraint(constraint, value, path = []) {
58
+ if (runConstraint(constraint, value))
59
+ return success(value);
60
+ return failure([toIssue(constraint, path)]);
30
61
  }
31
62
  /**
32
63
  * Executes multiple constraints against a value.
@@ -34,25 +65,19 @@ export function checkConstraint(constraint, value, path = []) {
34
65
  export function checkConstraints(constraints, value, path = []) {
35
66
  const issues = [];
36
67
  for (const constraint of constraints) {
37
- if (!constraint.validate(value)) {
38
- issues.push({
39
- path: [...path],
40
- code: constraint.code,
41
- message: constraint.message,
42
- received: value,
43
- });
68
+ if (!runConstraint(constraint, value)) {
69
+ issues.push(toIssue(constraint, path));
44
70
  }
45
71
  }
46
- if (issues.length > 0) {
72
+ if (issues.length > 0)
47
73
  return failure(issues);
48
- }
49
74
  return success(value);
50
75
  }
51
76
  /**
52
77
  * Combines constraints into a single constraint.
53
78
  */
54
79
  export function combineConstraints(...constraints) {
55
- return createConstraint((value) => constraints.every((constraint) => constraint.validate(value)), {
80
+ return createConstraint((value) => constraints.every((constraint) => runConstraint(constraint, value)), {
56
81
  name: constraints.map((constraint) => constraint.name).join("_and_") ||
57
82
  "combined",
58
83
  code: "combined_constraint_failed",
@@ -63,7 +88,7 @@ export function combineConstraints(...constraints) {
63
88
  * Creates a negated constraint.
64
89
  */
65
90
  export function not(constraint, options = {}) {
66
- return createConstraint((value) => !constraint.validate(value), {
91
+ return createConstraint((value) => !runConstraint(constraint, value), {
67
92
  name: options.name ?? `not_${constraint.name}`,
68
93
  code: options.code ?? "negated_constraint_failed",
69
94
  message: options.message ?? `Value must not satisfy ${constraint.name}.`,
@@ -3,6 +3,33 @@
3
3
  */
4
4
  import { formatIssues, toFieldErrors, } from "../validationResult/validationResult.type.js";
5
5
  import { BaseError, ErrorCode, ErrorCategory, ErrorSeverity, } from "@zudojs/errors";
6
+ /**
7
+ * Maps a package-specific validation code to the shared error registry code.
8
+ *
9
+ * `ValidationErrorCode` and `ErrorCode` are different enums. Casting one into
10
+ * the other put a value on `BaseError.code` that the errors package does not
11
+ * recognise, so any status mapping or registry lookup keyed on it missed.
12
+ */
13
+ function toErrorCode(code) {
14
+ switch (code) {
15
+ case ValidationErrorCode.REQUIRED:
16
+ return ErrorCode.MISSING_FIELD;
17
+ case ValidationErrorCode.INVALID_TYPE:
18
+ return ErrorCode.INVALID_FIELD;
19
+ case ValidationErrorCode.INVALID_FORMAT:
20
+ return ErrorCode.INVALID_FORMAT;
21
+ case ValidationErrorCode.INVALID_VALUE:
22
+ return ErrorCode.INVALID_VALUE;
23
+ case ValidationErrorCode.INVALID_INPUT:
24
+ return ErrorCode.INVALID_INPUT;
25
+ case ValidationErrorCode.SCHEMA_FAILED:
26
+ return ErrorCode.SCHEMA_VALIDATION;
27
+ case ValidationErrorCode.CONSTRAINT_FAILED:
28
+ case ValidationErrorCode.UNKNOWN:
29
+ default:
30
+ return ErrorCode.VALIDATION_FAILED;
31
+ }
32
+ }
6
33
  /** Error codes used by the validation package. */
7
34
  export var ValidationErrorCode;
8
35
  (function (ValidationErrorCode) {
@@ -23,7 +50,7 @@ export class ValidationError extends BaseError {
23
50
  timestamp;
24
51
  constructor(message, issues = [], options = {}) {
25
52
  super(message, {
26
- code: options.code ?? ErrorCode.VALIDATION_FAILED,
53
+ code: toErrorCode(options.code),
27
54
  category: ErrorCategory.VALIDATION,
28
55
  severity: ErrorSeverity.WARNING,
29
56
  statusCode: 400,
@@ -1,4 +1,4 @@
1
- import type { ValidationConstraint } from "../validationConstraints/index.js";
1
+ import type { ConstraintOptions, ValidationConstraint } from "../validationConstraints/index.js";
2
2
  import type { ValidationResult } from "../validationResult/validationResult.type.js";
3
3
  import type { ValidationSchema } from "../validationSchema/validationSchema.core.js";
4
4
  import { type ValidationParser, type AsyncValidationParser } from "../validationParser/validationParser.core.js";
@@ -21,11 +21,7 @@ export declare class ValidationFactory {
21
21
  normalizer<T>(normalizer: Normalizer<T>): ValidationNormalizer<T>;
22
22
  asyncNormalizer<T>(normalizer: AsyncNormalizer<T>): AsyncValidationNormalizer<T>;
23
23
  composer<T>(steps: readonly ValidationStep<T>[], options?: ValidationComposerOptions): ValidationComposer<T>;
24
- constraint<T>(validateValue: (value: T) => boolean, options?: {
25
- readonly name?: string;
26
- readonly code?: string;
27
- readonly message?: string;
28
- }): ValidationConstraint<T>;
24
+ constraint<T>(validateValue: (value: T) => boolean, options?: ConstraintOptions<T>): ValidationConstraint<T>;
29
25
  validate<T>(schema: ValidationSchema<T>, value: unknown): ValidationResult<T>;
30
26
  validateAsync<T>(schema: ValidationSchema<T>, value: unknown): Promise<ValidationResult<T>>;
31
27
  check<T>(constraints: readonly ValidationConstraint<T>[], value: T): ValidationResult<T>;
@@ -35,6 +31,11 @@ export declare class ValidationFactory {
35
31
  }
36
32
  /** Creates a new validation factory. */
37
33
  export declare function createValidationFactory(options?: ValidationFactoryOptions): ValidationFactory;
38
- /** Default validation factory instance. */
39
- export declare const validationFactory: ValidationFactory;
34
+ /**
35
+ * Create a factory that shares a registry with an existing one.
36
+ *
37
+ * Prefer this to a process-wide singleton: a shared factory means one module
38
+ * calling `registry.clear()` silently removes rules another module registered.
39
+ */
40
+ export declare function createScopedValidationFactory(parent: ValidationFactory): ValidationFactory;
40
41
  //# sourceMappingURL=validationFactory.core.d.ts.map
@@ -1,4 +1,4 @@
1
- import { checkConstraints } from "../validationConstraints/index.js";
1
+ import { checkConstraints, createConstraint, } from "../validationConstraints/index.js";
2
2
  import { validate, validateAsync, } from "../validationSchema/validationSchema.core.js";
3
3
  import { createValidationParser, createAsyncValidationParser, } from "../validationParser/validationParser.core.js";
4
4
  import { createValidationTransformer, createAsyncValidationTransformer, } from "../validationTransformer/validationTransformer.core.js";
@@ -33,12 +33,7 @@ export class ValidationFactory {
33
33
  return createValidationComposer(steps, options);
34
34
  }
35
35
  constraint(validateValue, options = {}) {
36
- return {
37
- name: options.name ?? "custom",
38
- code: options.code ?? "constraint_failed",
39
- message: options.message ?? "Validation constraint failed.",
40
- validate: validateValue,
41
- };
36
+ return createConstraint(validateValue, options);
42
37
  }
43
38
  validate(schema, value) {
44
39
  return validate(schema, value);
@@ -65,6 +60,13 @@ export class ValidationFactory {
65
60
  export function createValidationFactory(options = {}) {
66
61
  return new ValidationFactory(options);
67
62
  }
68
- /** Default validation factory instance. */
69
- export const validationFactory = createValidationFactory();
63
+ /**
64
+ * Create a factory that shares a registry with an existing one.
65
+ *
66
+ * Prefer this to a process-wide singleton: a shared factory means one module
67
+ * calling `registry.clear()` silently removes rules another module registered.
68
+ */
69
+ export function createScopedValidationFactory(parent) {
70
+ return new ValidationFactory({ registry: parent.registry });
71
+ }
70
72
  //# sourceMappingURL=validationFactory.core.js.map
@@ -17,13 +17,12 @@ export function createNormalizer(normalizer, options = {}) {
17
17
  try {
18
18
  return success(normalizer(value));
19
19
  }
20
- catch (error) {
20
+ catch {
21
21
  return failure([
22
22
  {
23
23
  path: [],
24
24
  code: "normalization_failed",
25
25
  message: options.errorMessage ?? "Value normalization failed.",
26
- received: error instanceof Error ? error.message : error,
27
26
  },
28
27
  ]);
29
28
  }
@@ -47,13 +46,12 @@ export function createAsyncNormalizer(normalizer, options = {}) {
47
46
  try {
48
47
  return success(await normalizer(value));
49
48
  }
50
- catch (error) {
49
+ catch {
51
50
  return failure([
52
51
  {
53
52
  path: [],
54
53
  code: "normalization_failed",
55
54
  message: options.errorMessage ?? "Value normalization failed.",
56
- received: error instanceof Error ? error.message : error,
57
55
  },
58
56
  ]);
59
57
  }
@@ -11,17 +11,50 @@ export declare function normalizeLowercase(value: string): string;
11
11
  export declare function normalizeUppercase(value: string): string;
12
12
  /** Normalizes Unicode text using NFC normalization. */
13
13
  export declare function normalizeUnicode(value: string): string;
14
- /** Normalizes an email address. */
14
+ /**
15
+ * Normalizes Unicode text using NFKC, folding compatibility forms.
16
+ *
17
+ * Use this wherever the result identifies something. NFC is canonical only:
18
+ * it leaves ligatures and fullwidth forms distinct from their ASCII
19
+ * spellings, so two visually identical identifiers survive as two values.
20
+ */
21
+ export declare function normalizeUnicodeCompatibility(value: string): string;
22
+ /**
23
+ * Case-folds a string for identifier comparison.
24
+ *
25
+ * `toLowerCase` is a locale-sensitive display transform. Folding through
26
+ * upper- then lower-case is the closest stable approximation available
27
+ * without ICU, and is what identifier comparison needs.
28
+ */
29
+ export declare function foldCase(value: string): string;
30
+ /**
31
+ * Normalizes an email address.
32
+ *
33
+ * Only the domain is lowercased. The local part is case-sensitive per RFC
34
+ * 5321, and folding it can merge two distinct mailboxes.
35
+ */
15
36
  export declare function normalizeEmail(value: string): string;
16
37
  /** Normalizes a URL by removing surrounding whitespace. */
17
38
  export declare function normalizeUrl(value: string): string;
18
- /** Normalizes an identifier by trimming and lowercasing it. */
39
+ /**
40
+ * Normalizes an identifier by trimming, compatibility-folding and case-folding.
41
+ *
42
+ * The output identifies an account or a resource, so it uses NFKC and a case
43
+ * fold rather than NFC and `toLowerCase`: otherwise two spellings that render
44
+ * identically normalize to two different identifiers.
45
+ */
19
46
  export declare function normalizeIdentifier(value: string): string;
20
47
  /** Removes surrounding quotes from a string. */
21
48
  export declare function normalizeQuotes(value: string): string;
22
49
  /** Removes Unicode byte-order marks from the beginning of text. */
23
50
  export declare function removeBom(value: string): string;
24
- /** Normalizes an array by applying a normalizer to every item. */
51
+ /**
52
+ * Normalizes an array by applying a normalizer to every item.
53
+ *
54
+ * The normalizer is invoked with the value only. Passing it straight to `map`
55
+ * would also hand it the index and the array, which silently overrides the
56
+ * optional second parameter of functions like `parseInt`.
57
+ */
25
58
  export declare function normalizeArray<T>(values: readonly T[], normalizer: (value: T) => T): T[];
26
59
  /** Normalizes an array asynchronously. */
27
60
  export declare function normalizeArrayAsync<T>(values: readonly T[], normalizer: (value: T) => T | Promise<T>): Promise<T[]>;