@zudojs/validation 1.0.3 → 1.1.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/README.md +8 -0
- package/dist/validationConstraints/collection/validationConstraints.array.js +4 -3
- package/dist/validationConstraints/scalar/validationConstraints.string.js +3 -2
- package/dist/validationConstraints/structure/validationConstraints.circular.d.ts +2 -1
- package/dist/validationConstraints/structure/validationConstraints.circular.js +4 -3
- package/dist/validationConstraints/structure/validationConstraints.depth.d.ts +15 -1
- package/dist/validationConstraints/structure/validationConstraints.depth.js +16 -2
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -46,6 +46,14 @@ assertSizeWithinLimit(body, 1_000_000);
|
|
|
46
46
|
assertNoCircularReference(body);
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
A too-deep payload throws `SerializationDepthError` with `statusCode` 400
|
|
50
|
+
and `expose: true` from both `assertDepthWithinLimit` and
|
|
51
|
+
`assertNoCircularReference`: untrusted input that nests too far is a client
|
|
52
|
+
error, and the message names only the observed depth and the limit
|
|
53
|
+
(`Maximum serialization depth exceeded: 40 > 32`), never the payload.
|
|
54
|
+
Constructed directly, `SerializationDepthError` still defaults to an
|
|
55
|
+
unexposed 500, which is right for data your own server built.
|
|
56
|
+
|
|
49
57
|
## Safety Notes
|
|
50
58
|
|
|
51
59
|
- Constraint failures do **not** carry the rejected value. `ValidationError` is
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatCount } from "@zudojs/types";
|
|
1
2
|
import { createConstraint, assertNonNegativeInteger, } from "../validationConstraints.base.js";
|
|
2
3
|
/** Narrows an unknown value to a readonly array. */
|
|
3
4
|
function isArrayOf(value) {
|
|
@@ -34,7 +35,7 @@ export function minItems(minimum) {
|
|
|
34
35
|
return createConstraint((value) => value.length >= minimum, {
|
|
35
36
|
name: `min_items_${minimum}`,
|
|
36
37
|
code: "min_items",
|
|
37
|
-
message: `Value must contain at least ${minimum}
|
|
38
|
+
message: `Value must contain at least ${formatCount(minimum, "item")}.`,
|
|
38
39
|
guard: isArrayOf,
|
|
39
40
|
});
|
|
40
41
|
}
|
|
@@ -46,7 +47,7 @@ export function maxItems(maximum) {
|
|
|
46
47
|
return createConstraint((value) => value.length <= maximum, {
|
|
47
48
|
name: `max_items_${maximum}`,
|
|
48
49
|
code: "max_items",
|
|
49
|
-
message: `Value must contain at most ${maximum}
|
|
50
|
+
message: `Value must contain at most ${formatCount(maximum, "item")}.`,
|
|
50
51
|
guard: isArrayOf,
|
|
51
52
|
});
|
|
52
53
|
}
|
|
@@ -58,7 +59,7 @@ export function exactItems(length) {
|
|
|
58
59
|
return createConstraint((value) => value.length === length, {
|
|
59
60
|
name: `exact_items_${length}`,
|
|
60
61
|
code: "exact_items",
|
|
61
|
-
message: `Value must contain exactly ${length}
|
|
62
|
+
message: `Value must contain exactly ${formatCount(length, "item")}.`,
|
|
62
63
|
guard: isArrayOf,
|
|
63
64
|
});
|
|
64
65
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ValidationLength, ValidationPattern } from "@zudojs/constants";
|
|
2
|
+
import { formatCount } from "@zudojs/types";
|
|
2
3
|
import { createConstraint, assertNonNegativeInteger, } from "../validationConstraints.base.js";
|
|
3
4
|
/** Counts Unicode code points rather than UTF-16 code units. */
|
|
4
5
|
function characterLength(value) {
|
|
@@ -22,7 +23,7 @@ export function minLength(minimum) {
|
|
|
22
23
|
return createConstraint((value) => characterLength(value) >= minimum, {
|
|
23
24
|
name: `min_length_${minimum}`,
|
|
24
25
|
code: "min_length",
|
|
25
|
-
message: `Value must contain at least ${minimum}
|
|
26
|
+
message: `Value must contain at least ${formatCount(minimum, "character")}.`,
|
|
26
27
|
guard: (value) => typeof value === "string",
|
|
27
28
|
});
|
|
28
29
|
}
|
|
@@ -34,7 +35,7 @@ export function maxLength(maximum) {
|
|
|
34
35
|
return createConstraint((value) => characterLength(value) <= maximum, {
|
|
35
36
|
name: `max_length_${maximum}`,
|
|
36
37
|
code: "max_length",
|
|
37
|
-
message: `Value must contain at most ${maximum}
|
|
38
|
+
message: `Value must contain at most ${formatCount(maximum, "character")}.`,
|
|
38
39
|
guard: (value) => typeof value === "string",
|
|
39
40
|
});
|
|
40
41
|
}
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* @param maxDepth - Depth ceiling, so a deep graph cannot exhaust the stack
|
|
18
18
|
* before a cycle is reported.
|
|
19
19
|
* @throws {CircularReferenceError} on the first cycle found.
|
|
20
|
-
* @throws {SerializationDepthError} when the graph is deeper than `maxDepth
|
|
20
|
+
* @throws {SerializationDepthError} when the graph is deeper than `maxDepth`
|
|
21
|
+
* (a 400 with `expose: true`, like `assertDepthWithinLimit`).
|
|
21
22
|
* Running out of depth is not evidence of a cycle, and reporting it as one
|
|
22
23
|
* told callers a payload referenced itself when it merely nested too far.
|
|
23
24
|
*/
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* or validation to prevent stack overflows and provide clear error messages.
|
|
6
6
|
*/
|
|
7
7
|
import { CircularReferenceError, SerializationDepthError, } from "@zudojs/errors";
|
|
8
|
-
import { MAX_MEASURABLE_DEPTH } from "./validationConstraints.depth.js";
|
|
8
|
+
import { MAX_MEASURABLE_DEPTH, UNTRUSTED_DEPTH_ERROR, } from "./validationConstraints.depth.js";
|
|
9
9
|
import { TraversalLimitError, traverse, } from "./validationConstraints.traverse.js";
|
|
10
10
|
/**
|
|
11
11
|
* Detect circular references in a value graph.
|
|
@@ -20,7 +20,8 @@ import { TraversalLimitError, traverse, } from "./validationConstraints.traverse
|
|
|
20
20
|
* @param maxDepth - Depth ceiling, so a deep graph cannot exhaust the stack
|
|
21
21
|
* before a cycle is reported.
|
|
22
22
|
* @throws {CircularReferenceError} on the first cycle found.
|
|
23
|
-
* @throws {SerializationDepthError} when the graph is deeper than `maxDepth
|
|
23
|
+
* @throws {SerializationDepthError} when the graph is deeper than `maxDepth`
|
|
24
|
+
* (a 400 with `expose: true`, like `assertDepthWithinLimit`).
|
|
24
25
|
* Running out of depth is not evidence of a cycle, and reporting it as one
|
|
25
26
|
* told callers a payload referenced itself when it merely nested too far.
|
|
26
27
|
*/
|
|
@@ -33,7 +34,7 @@ export function assertNoCircularReference(value, path = "root", maxDepth = MAX_M
|
|
|
33
34
|
throw new CircularReferenceError(error.path);
|
|
34
35
|
}
|
|
35
36
|
if (error instanceof TraversalLimitError && error.halt === "depth") {
|
|
36
|
-
throw new SerializationDepthError(error.observed, maxDepth);
|
|
37
|
+
throw new SerializationDepthError(error.observed, maxDepth, UNTRUSTED_DEPTH_ERROR);
|
|
37
38
|
}
|
|
38
39
|
throw error;
|
|
39
40
|
}
|
|
@@ -11,6 +11,19 @@
|
|
|
11
11
|
* that overflows the stack on the input it was checking protects nothing.
|
|
12
12
|
*/
|
|
13
13
|
export declare const MAX_MEASURABLE_DEPTH = 512;
|
|
14
|
+
/**
|
|
15
|
+
* Error options for depth failures found by these guards.
|
|
16
|
+
*
|
|
17
|
+
* The guards exist to check UNTRUSTED input, so input that nests too deep is
|
|
18
|
+
* a client error: a 400 that may be exposed. `SerializationDepthError` alone
|
|
19
|
+
* defaults to an unexposed 500, which turned a too-deep request body into an
|
|
20
|
+
* opaque server error. The message carries only the observed depth and the
|
|
21
|
+
* limit, never the payload.
|
|
22
|
+
*/
|
|
23
|
+
export declare const UNTRUSTED_DEPTH_ERROR: Readonly<{
|
|
24
|
+
statusCode: 400;
|
|
25
|
+
expose: true;
|
|
26
|
+
}>;
|
|
14
27
|
/**
|
|
15
28
|
* Compute the maximum nesting depth of a value.
|
|
16
29
|
*
|
|
@@ -31,7 +44,8 @@ export declare function getSerializationDepth(value: unknown, limit?: number): n
|
|
|
31
44
|
*
|
|
32
45
|
* @param value - The value to check.
|
|
33
46
|
* @param maxDepth - Maximum permitted nesting depth.
|
|
34
|
-
* @throws {SerializationDepthError} when depth exceeds the limit
|
|
47
|
+
* @throws {SerializationDepthError} when depth exceeds the limit, with
|
|
48
|
+
* `statusCode` 400 and `expose: true` (see {@link UNTRUSTED_DEPTH_ERROR}).
|
|
35
49
|
*/
|
|
36
50
|
export declare function assertDepthWithinLimit(value: unknown, maxDepth: number): void;
|
|
37
51
|
//# sourceMappingURL=validationConstraints.depth.d.ts.map
|
|
@@ -13,6 +13,19 @@ import { TraversalLimitError, traverse, } from "./validationConstraints.traverse
|
|
|
13
13
|
* that overflows the stack on the input it was checking protects nothing.
|
|
14
14
|
*/
|
|
15
15
|
export const MAX_MEASURABLE_DEPTH = 512;
|
|
16
|
+
/**
|
|
17
|
+
* Error options for depth failures found by these guards.
|
|
18
|
+
*
|
|
19
|
+
* The guards exist to check UNTRUSTED input, so input that nests too deep is
|
|
20
|
+
* a client error: a 400 that may be exposed. `SerializationDepthError` alone
|
|
21
|
+
* defaults to an unexposed 500, which turned a too-deep request body into an
|
|
22
|
+
* opaque server error. The message carries only the observed depth and the
|
|
23
|
+
* limit, never the payload.
|
|
24
|
+
*/
|
|
25
|
+
export const UNTRUSTED_DEPTH_ERROR = Object.freeze({
|
|
26
|
+
statusCode: 400,
|
|
27
|
+
expose: true,
|
|
28
|
+
});
|
|
16
29
|
/**
|
|
17
30
|
* Compute the maximum nesting depth of a value.
|
|
18
31
|
*
|
|
@@ -42,7 +55,8 @@ export function getSerializationDepth(value, limit = MAX_MEASURABLE_DEPTH) {
|
|
|
42
55
|
*
|
|
43
56
|
* @param value - The value to check.
|
|
44
57
|
* @param maxDepth - Maximum permitted nesting depth.
|
|
45
|
-
* @throws {SerializationDepthError} when depth exceeds the limit
|
|
58
|
+
* @throws {SerializationDepthError} when depth exceeds the limit, with
|
|
59
|
+
* `statusCode` 400 and `expose: true` (see {@link UNTRUSTED_DEPTH_ERROR}).
|
|
46
60
|
*/
|
|
47
61
|
export function assertDepthWithinLimit(value, maxDepth) {
|
|
48
62
|
try {
|
|
@@ -50,7 +64,7 @@ export function assertDepthWithinLimit(value, maxDepth) {
|
|
|
50
64
|
}
|
|
51
65
|
catch (error) {
|
|
52
66
|
if (error instanceof TraversalLimitError && error.halt === "depth") {
|
|
53
|
-
throw new SerializationDepthError(error.observed, maxDepth);
|
|
67
|
+
throw new SerializationDepthError(error.observed, maxDepth, UNTRUSTED_DEPTH_ERROR);
|
|
54
68
|
}
|
|
55
69
|
throw error;
|
|
56
70
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/validation",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Schema validation with Zod integration, constraints, parsers, composers, circular detection, and depth/size checks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -25,13 +25,14 @@
|
|
|
25
25
|
"!dist/.tsbuildinfo"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@zudojs/constants": "1.1.
|
|
29
|
-
"@zudojs/errors": "1.
|
|
30
|
-
"
|
|
28
|
+
"@zudojs/constants": "1.1.2",
|
|
29
|
+
"@zudojs/errors": "1.3.0",
|
|
30
|
+
"@zudojs/types": "1.2.0",
|
|
31
|
+
"zod": "^4.6.5"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"typescript": "7.0.2",
|
|
34
|
-
"vitest": "^
|
|
35
|
+
"vitest": "^5.0.1"
|
|
35
36
|
},
|
|
36
37
|
"engines": {
|
|
37
38
|
"node": ">=24.0.0"
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"zod",
|
|
46
47
|
"schemas"
|
|
47
48
|
],
|
|
48
|
-
"homepage": "https://
|
|
49
|
+
"homepage": "https://zudojs.oyinlola.site/docs/packages-validation",
|
|
49
50
|
"bugs": {
|
|
50
51
|
"url": "https://github.com/oyinlola-tech/zudo/issues"
|
|
51
52
|
},
|