anyvali 0.3.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 (204) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +370 -0
  3. package/VERSION +1 -0
  4. package/dist/errors.d.ts +6 -0
  5. package/dist/errors.d.ts.map +1 -0
  6. package/dist/errors.js +12 -0
  7. package/dist/errors.js.map +1 -0
  8. package/dist/format/validators.d.ts +2 -0
  9. package/dist/format/validators.d.ts.map +1 -0
  10. package/dist/format/validators.js +57 -0
  11. package/dist/format/validators.js.map +1 -0
  12. package/dist/forms/index.d.ts +57 -0
  13. package/dist/forms/index.d.ts.map +1 -0
  14. package/dist/forms/index.js +586 -0
  15. package/dist/forms/index.js.map +1 -0
  16. package/dist/index.d.ts +93 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +156 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/infer.d.ts +8 -0
  21. package/dist/infer.d.ts.map +1 -0
  22. package/dist/infer.js +2 -0
  23. package/dist/infer.js.map +1 -0
  24. package/dist/interchange/document.d.ts +5 -0
  25. package/dist/interchange/document.d.ts.map +1 -0
  26. package/dist/interchange/document.js +12 -0
  27. package/dist/interchange/document.js.map +1 -0
  28. package/dist/interchange/exporter.d.ts +7 -0
  29. package/dist/interchange/exporter.d.ts.map +1 -0
  30. package/dist/interchange/exporter.js +7 -0
  31. package/dist/interchange/exporter.js.map +1 -0
  32. package/dist/interchange/importer.d.ts +4 -0
  33. package/dist/interchange/importer.d.ts.map +1 -0
  34. package/dist/interchange/importer.js +229 -0
  35. package/dist/interchange/importer.js.map +1 -0
  36. package/dist/issue-codes.d.ts +19 -0
  37. package/dist/issue-codes.d.ts.map +1 -0
  38. package/dist/issue-codes.js +18 -0
  39. package/dist/issue-codes.js.map +1 -0
  40. package/dist/parse/coerce.d.ts +16 -0
  41. package/dist/parse/coerce.d.ts.map +1 -0
  42. package/dist/parse/coerce.js +115 -0
  43. package/dist/parse/coerce.js.map +1 -0
  44. package/dist/parse/defaults.d.ts +7 -0
  45. package/dist/parse/defaults.d.ts.map +1 -0
  46. package/dist/parse/defaults.js +12 -0
  47. package/dist/parse/defaults.js.map +1 -0
  48. package/dist/parse/parser.d.ts +11 -0
  49. package/dist/parse/parser.d.ts.map +1 -0
  50. package/dist/parse/parser.js +13 -0
  51. package/dist/parse/parser.js.map +1 -0
  52. package/dist/schemas/any.d.ts +7 -0
  53. package/dist/schemas/any.d.ts.map +1 -0
  54. package/dist/schemas/any.js +12 -0
  55. package/dist/schemas/any.js.map +1 -0
  56. package/dist/schemas/array.d.ts +13 -0
  57. package/dist/schemas/array.d.ts.map +1 -0
  58. package/dist/schemas/array.js +73 -0
  59. package/dist/schemas/array.js.map +1 -0
  60. package/dist/schemas/base.d.ts +37 -0
  61. package/dist/schemas/base.d.ts.map +1 -0
  62. package/dist/schemas/base.js +285 -0
  63. package/dist/schemas/base.js.map +1 -0
  64. package/dist/schemas/bool.d.ts +8 -0
  65. package/dist/schemas/bool.d.ts.map +1 -0
  66. package/dist/schemas/bool.js +27 -0
  67. package/dist/schemas/bool.js.map +1 -0
  68. package/dist/schemas/enum.d.ts +9 -0
  69. package/dist/schemas/enum.d.ts.map +1 -0
  70. package/dist/schemas/enum.js +31 -0
  71. package/dist/schemas/enum.js.map +1 -0
  72. package/dist/schemas/index.d.ts +21 -0
  73. package/dist/schemas/index.d.ts.map +1 -0
  74. package/dist/schemas/index.js +21 -0
  75. package/dist/schemas/index.js.map +1 -0
  76. package/dist/schemas/int.d.ts +32 -0
  77. package/dist/schemas/int.d.ts.map +1 -0
  78. package/dist/schemas/int.js +108 -0
  79. package/dist/schemas/int.js.map +1 -0
  80. package/dist/schemas/intersection.d.ts +16 -0
  81. package/dist/schemas/intersection.d.ts.map +1 -0
  82. package/dist/schemas/intersection.js +58 -0
  83. package/dist/schemas/intersection.js.map +1 -0
  84. package/dist/schemas/literal.d.ts +11 -0
  85. package/dist/schemas/literal.d.ts.map +1 -0
  86. package/dist/schemas/literal.js +28 -0
  87. package/dist/schemas/literal.js.map +1 -0
  88. package/dist/schemas/never.d.ts +7 -0
  89. package/dist/schemas/never.d.ts.map +1 -0
  90. package/dist/schemas/never.js +19 -0
  91. package/dist/schemas/never.js.map +1 -0
  92. package/dist/schemas/null.d.ts +7 -0
  93. package/dist/schemas/null.d.ts.map +1 -0
  94. package/dist/schemas/null.js +24 -0
  95. package/dist/schemas/null.js.map +1 -0
  96. package/dist/schemas/nullable.d.ts +10 -0
  97. package/dist/schemas/nullable.d.ts.map +1 -0
  98. package/dist/schemas/nullable.js +29 -0
  99. package/dist/schemas/nullable.js.map +1 -0
  100. package/dist/schemas/number.d.ts +27 -0
  101. package/dist/schemas/number.d.ts.map +1 -0
  102. package/dist/schemas/number.js +134 -0
  103. package/dist/schemas/number.js.map +1 -0
  104. package/dist/schemas/object.d.ts +28 -0
  105. package/dist/schemas/object.d.ts.map +1 -0
  106. package/dist/schemas/object.js +153 -0
  107. package/dist/schemas/object.js.map +1 -0
  108. package/dist/schemas/optional.d.ts +11 -0
  109. package/dist/schemas/optional.d.ts.map +1 -0
  110. package/dist/schemas/optional.js +39 -0
  111. package/dist/schemas/optional.js.map +1 -0
  112. package/dist/schemas/record.d.ts +9 -0
  113. package/dist/schemas/record.d.ts.map +1 -0
  114. package/dist/schemas/record.js +45 -0
  115. package/dist/schemas/record.js.map +1 -0
  116. package/dist/schemas/ref.d.ts +10 -0
  117. package/dist/schemas/ref.d.ts.map +1 -0
  118. package/dist/schemas/ref.js +30 -0
  119. package/dist/schemas/ref.js.map +1 -0
  120. package/dist/schemas/string.d.ts +29 -0
  121. package/dist/schemas/string.d.ts.map +1 -0
  122. package/dist/schemas/string.js +181 -0
  123. package/dist/schemas/string.js.map +1 -0
  124. package/dist/schemas/tuple.d.ts +14 -0
  125. package/dist/schemas/tuple.d.ts.map +1 -0
  126. package/dist/schemas/tuple.js +59 -0
  127. package/dist/schemas/tuple.js.map +1 -0
  128. package/dist/schemas/union.d.ts +9 -0
  129. package/dist/schemas/union.d.ts.map +1 -0
  130. package/dist/schemas/union.js +45 -0
  131. package/dist/schemas/union.js.map +1 -0
  132. package/dist/schemas/unknown.d.ts +7 -0
  133. package/dist/schemas/unknown.d.ts.map +1 -0
  134. package/dist/schemas/unknown.js +12 -0
  135. package/dist/schemas/unknown.js.map +1 -0
  136. package/dist/types.d.ts +132 -0
  137. package/dist/types.d.ts.map +1 -0
  138. package/dist/types.js +3 -0
  139. package/dist/types.js.map +1 -0
  140. package/dist/util.d.ts +6 -0
  141. package/dist/util.d.ts.map +1 -0
  142. package/dist/util.js +12 -0
  143. package/dist/util.js.map +1 -0
  144. package/package.json +41 -0
  145. package/sdk/js/CHANGELOG.md +13 -0
  146. package/src/errors.ts +17 -0
  147. package/src/format/validators.ts +71 -0
  148. package/src/forms/index.ts +789 -0
  149. package/src/index.ts +285 -0
  150. package/src/infer.ts +12 -0
  151. package/src/interchange/document.ts +18 -0
  152. package/src/interchange/exporter.ts +12 -0
  153. package/src/interchange/importer.ts +285 -0
  154. package/src/issue-codes.ts +19 -0
  155. package/src/parse/coerce.ts +133 -0
  156. package/src/parse/defaults.ts +15 -0
  157. package/src/parse/parser.ts +19 -0
  158. package/src/schemas/any.ts +14 -0
  159. package/src/schemas/array.ts +83 -0
  160. package/src/schemas/base.ts +322 -0
  161. package/src/schemas/bool.ts +30 -0
  162. package/src/schemas/enum.ts +37 -0
  163. package/src/schemas/index.ts +30 -0
  164. package/src/schemas/int.ts +129 -0
  165. package/src/schemas/intersection.ts +81 -0
  166. package/src/schemas/literal.ts +34 -0
  167. package/src/schemas/never.ts +21 -0
  168. package/src/schemas/null.ts +26 -0
  169. package/src/schemas/nullable.ts +36 -0
  170. package/src/schemas/number.ts +151 -0
  171. package/src/schemas/object.ts +203 -0
  172. package/src/schemas/optional.ts +49 -0
  173. package/src/schemas/record.ts +55 -0
  174. package/src/schemas/ref.ts +35 -0
  175. package/src/schemas/string.ts +192 -0
  176. package/src/schemas/tuple.ts +74 -0
  177. package/src/schemas/union.ts +53 -0
  178. package/src/schemas/unknown.ts +14 -0
  179. package/src/types.ts +239 -0
  180. package/src/util.ts +9 -0
  181. package/tests/conformance/runner.test.ts +28 -0
  182. package/tests/conformance/runner.ts +137 -0
  183. package/tests/forms.test.ts +146 -0
  184. package/tests/unit/coerce.test.ts +136 -0
  185. package/tests/unit/collections.test.ts +99 -0
  186. package/tests/unit/composition.test.ts +80 -0
  187. package/tests/unit/date-format.test.ts +18 -0
  188. package/tests/unit/default-mutation.test.ts +32 -0
  189. package/tests/unit/defaults.test.ts +49 -0
  190. package/tests/unit/errors.test.ts +53 -0
  191. package/tests/unit/export.test.ts +270 -0
  192. package/tests/unit/inference.test.ts +306 -0
  193. package/tests/unit/interchange.test.ts +191 -0
  194. package/tests/unit/number.test.ts +195 -0
  195. package/tests/unit/object.test.ts +208 -0
  196. package/tests/unit/parser.test.ts +151 -0
  197. package/tests/unit/primitives.test.ts +111 -0
  198. package/tests/unit/security-recursion.test.ts +105 -0
  199. package/tests/unit/security.test.ts +945 -0
  200. package/tests/unit/shared-ref-falsepos.test.ts +33 -0
  201. package/tests/unit/string-pattern-redos.test.ts +46 -0
  202. package/tests/unit/string.test.ts +147 -0
  203. package/tsconfig.json +21 -0
  204. package/vitest.config.ts +7 -0
@@ -0,0 +1,151 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parse, safeParse } from "../../src/parse/parser.js";
3
+ import { applyDefault } from "../../src/parse/defaults.js";
4
+ import { ABSENT } from "../../src/schemas/base.js";
5
+ import {
6
+ createDocument,
7
+ ANYVALI_VERSION,
8
+ SCHEMA_VERSION,
9
+ } from "../../src/interchange/document.js";
10
+ import { StringSchema } from "../../src/schemas/string.js";
11
+ import { IntSchema } from "../../src/schemas/int.js";
12
+ import { importSchema } from "../../src/interchange/importer.js";
13
+
14
+ describe("parse/parser.ts - standalone parse", () => {
15
+ it("parse returns validated value", () => {
16
+ const s = new StringSchema();
17
+ expect(parse(s, "hello")).toBe("hello");
18
+ });
19
+
20
+ it("parse throws on invalid input", () => {
21
+ const s = new StringSchema();
22
+ expect(() => parse(s, 42)).toThrow();
23
+ });
24
+
25
+ it("safeParse returns success result", () => {
26
+ const s = new StringSchema();
27
+ const result = safeParse(s, "hi");
28
+ expect(result.success).toBe(true);
29
+ if (result.success) expect(result.data).toBe("hi");
30
+ });
31
+
32
+ it("safeParse returns failure result", () => {
33
+ const s = new StringSchema();
34
+ const result = safeParse(s, 123);
35
+ expect(result.success).toBe(false);
36
+ if (!result.success) expect(result.issues.length).toBeGreaterThan(0);
37
+ });
38
+ });
39
+
40
+ describe("parse/defaults.ts - applyDefault", () => {
41
+ it("returns default when input is undefined", () => {
42
+ expect(applyDefault(undefined, "fallback")).toBe("fallback");
43
+ });
44
+
45
+ it("returns default when input is ABSENT", () => {
46
+ expect(applyDefault(ABSENT, "fallback")).toBe("fallback");
47
+ });
48
+
49
+ it("returns input when it has a value", () => {
50
+ expect(applyDefault("hello", "fallback")).toBe("hello");
51
+ });
52
+
53
+ it("returns input when default is ABSENT", () => {
54
+ expect(applyDefault(undefined, ABSENT)).toBe(undefined);
55
+ });
56
+
57
+ it("returns input (even falsy) when present", () => {
58
+ expect(applyDefault(0, "fallback")).toBe(0);
59
+ expect(applyDefault("", "fallback")).toBe("");
60
+ expect(applyDefault(null, "fallback")).toBe(null);
61
+ expect(applyDefault(false, "fallback")).toBe(false);
62
+ });
63
+ });
64
+
65
+ describe("interchange/document.ts", () => {
66
+ it("exports constants", () => {
67
+ expect(ANYVALI_VERSION).toBe("1.0");
68
+ expect(SCHEMA_VERSION).toBe("1");
69
+ });
70
+
71
+ it("createDocument creates a valid document", () => {
72
+ const root = { kind: "string" as const };
73
+ const doc = createDocument(root as any);
74
+ expect(doc.anyvaliVersion).toBe("1.0");
75
+ expect(doc.schemaVersion).toBe("1");
76
+ expect(doc.root).toBe(root);
77
+ expect(doc.definitions).toEqual({});
78
+ expect(doc.extensions).toEqual({});
79
+ });
80
+
81
+ it("createDocument with definitions and extensions", () => {
82
+ const root = { kind: "ref" as const, ref: "#/definitions/Foo" };
83
+ const defs = { Foo: { kind: "string" as const } };
84
+ const exts = { myExt: { foo: "bar" } };
85
+ const doc = createDocument(root as any, defs as any, exts);
86
+ expect(doc.definitions).toBe(defs);
87
+ expect(doc.extensions).toBe(exts);
88
+ });
89
+ });
90
+
91
+ describe("importer.ts edge cases", () => {
92
+ it("throws on unsupported schema kind", () => {
93
+ const doc = {
94
+ anyvaliVersion: "1.0",
95
+ schemaVersion: "1",
96
+ root: { kind: "foobar" },
97
+ definitions: {},
98
+ extensions: {},
99
+ };
100
+ expect(() => importSchema(doc as any)).toThrow("Unsupported schema kind: foobar");
101
+ });
102
+
103
+ it("throws on unresolved ref definition", () => {
104
+ const doc = {
105
+ anyvaliVersion: "1.0",
106
+ schemaVersion: "1",
107
+ root: { kind: "ref", ref: "#/definitions/Missing" },
108
+ definitions: {},
109
+ extensions: {},
110
+ };
111
+ const imported = importSchema(doc as any);
112
+ // The ref is lazy, so it only throws when we try to parse
113
+ expect(() => imported.parse("anything")).toThrow("Unresolved definition: Missing");
114
+ });
115
+
116
+ it("imports ref that resolves to a definition", () => {
117
+ const doc = {
118
+ anyvaliVersion: "1.0",
119
+ schemaVersion: "1",
120
+ root: { kind: "ref", ref: "#/definitions/Name" },
121
+ definitions: { Name: { kind: "string" } },
122
+ extensions: {},
123
+ };
124
+ const imported = importSchema(doc as any);
125
+ expect(imported.parse("hello")).toBe("hello");
126
+ });
127
+
128
+ it("imports with coerce as string format", () => {
129
+ const doc = {
130
+ anyvaliVersion: "1.0",
131
+ schemaVersion: "1",
132
+ root: { kind: "int", coerce: "string->int" },
133
+ definitions: {},
134
+ extensions: {},
135
+ };
136
+ const imported = importSchema(doc as any);
137
+ expect(imported.parse("42")).toBe(42);
138
+ });
139
+
140
+ it("imports with coerce as array format", () => {
141
+ const doc = {
142
+ anyvaliVersion: "1.0",
143
+ schemaVersion: "1",
144
+ root: { kind: "string", coerce: ["trim", "lower"] },
145
+ definitions: {},
146
+ extensions: {},
147
+ };
148
+ const imported = importSchema(doc as any);
149
+ expect(imported.parse(" HELLO ")).toBe("hello");
150
+ });
151
+ });
@@ -0,0 +1,111 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ bool,
4
+ null_,
5
+ any,
6
+ unknown,
7
+ never,
8
+ literal,
9
+ enum_,
10
+ } from "../../src/index.js";
11
+
12
+ describe("BoolSchema", () => {
13
+ it("accepts booleans", () => {
14
+ expect(bool().parse(true)).toBe(true);
15
+ expect(bool().parse(false)).toBe(false);
16
+ });
17
+
18
+ it("rejects non-booleans", () => {
19
+ expect(bool().safeParse("true").success).toBe(false);
20
+ expect(bool().safeParse(1).success).toBe(false);
21
+ expect(bool().safeParse(null).success).toBe(false);
22
+ });
23
+
24
+ it("coerces string to bool", () => {
25
+ const s = bool().coerce({ from: "string" });
26
+ expect(s.parse("true")).toBe(true);
27
+ expect(s.parse("false")).toBe(false);
28
+ expect(s.parse("1")).toBe(true);
29
+ expect(s.parse("0")).toBe(false);
30
+ });
31
+
32
+ it("coercion failure for invalid string", () => {
33
+ const s = bool().coerce({ from: "string" });
34
+ const result = s.safeParse("maybe");
35
+ expect(result.success).toBe(false);
36
+ if (!result.success) expect(result.issues[0].code).toBe("coercion_failed");
37
+ });
38
+ });
39
+
40
+ describe("NullSchema", () => {
41
+ it("accepts null", () => {
42
+ expect(null_().parse(null)).toBe(null);
43
+ });
44
+
45
+ it("rejects non-null", () => {
46
+ expect(null_().safeParse(undefined).success).toBe(false);
47
+ expect(null_().safeParse(0).success).toBe(false);
48
+ expect(null_().safeParse("").success).toBe(false);
49
+ });
50
+ });
51
+
52
+ describe("AnySchema", () => {
53
+ it("accepts anything", () => {
54
+ expect(any().parse(42)).toBe(42);
55
+ expect(any().parse("hello")).toBe("hello");
56
+ expect(any().parse(null)).toBe(null);
57
+ expect(any().parse(undefined)).toBe(undefined);
58
+ });
59
+ });
60
+
61
+ describe("UnknownSchema", () => {
62
+ it("accepts anything", () => {
63
+ expect(unknown().parse(42)).toBe(42);
64
+ expect(unknown().parse("hello")).toBe("hello");
65
+ });
66
+ });
67
+
68
+ describe("NeverSchema", () => {
69
+ it("rejects everything", () => {
70
+ expect(never().safeParse(42).success).toBe(false);
71
+ expect(never().safeParse(null).success).toBe(false);
72
+ expect(never().safeParse(undefined).success).toBe(false);
73
+ });
74
+ });
75
+
76
+ describe("LiteralSchema", () => {
77
+ it("accepts matching literal", () => {
78
+ expect(literal("hello").parse("hello")).toBe("hello");
79
+ expect(literal(42).parse(42)).toBe(42);
80
+ expect(literal(true).parse(true)).toBe(true);
81
+ expect(literal(null).parse(null)).toBe(null);
82
+ });
83
+
84
+ it("rejects non-matching values", () => {
85
+ expect(literal("hello").safeParse("world").success).toBe(false);
86
+ expect(literal(42).safeParse(43).success).toBe(false);
87
+ if (!literal(42).safeParse(43).success) {
88
+ expect((literal(42).safeParse(43) as any).issues[0].code).toBe("invalid_literal");
89
+ }
90
+ });
91
+ });
92
+
93
+ describe("EnumSchema", () => {
94
+ it("accepts values in the enum", () => {
95
+ const s = enum_(["a", "b", "c"]);
96
+ expect(s.parse("a")).toBe("a");
97
+ expect(s.parse("b")).toBe("b");
98
+ });
99
+
100
+ it("rejects values not in the enum", () => {
101
+ const s = enum_(["a", "b", "c"]);
102
+ expect(s.safeParse("d").success).toBe(false);
103
+ expect(s.safeParse(1).success).toBe(false);
104
+ });
105
+
106
+ it("works with numeric enums", () => {
107
+ const s = enum_([1, 2, 3]);
108
+ expect(s.parse(1)).toBe(1);
109
+ expect(s.safeParse(4).success).toBe(false);
110
+ });
111
+ });
@@ -0,0 +1,105 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { importSchema } from "../../src/index.js";
3
+ import type { AnyValiDocument } from "../../src/types.js";
4
+
5
+ // Recursive schema: Node = object({ value: int, next: optional(ref Node) }).
6
+ function recursiveSchemaDoc(): AnyValiDocument {
7
+ return {
8
+ anyvaliVersion: "1.0",
9
+ schemaVersion: "1",
10
+ root: { kind: "ref", ref: "#/definitions/Node" } as any,
11
+ definitions: {
12
+ Node: {
13
+ kind: "object",
14
+ properties: {
15
+ next: { kind: "ref", ref: "#/definitions/Node" },
16
+ },
17
+ required: [],
18
+ unknownKeys: "strip",
19
+ } as any,
20
+ },
21
+ extensions: {},
22
+ } as AnyValiDocument;
23
+ }
24
+
25
+ // Build deeply nested data: { next: { next: { ... } } }.
26
+ function deepData(depth: number): unknown {
27
+ let cur: any = {};
28
+ for (let i = 0; i < depth; i++) cur = { next: cur };
29
+ return cur;
30
+ }
31
+
32
+ // Build a deeply nested schema document: array(array(array(...))).
33
+ function deepArraySchemaDoc(depth: number): AnyValiDocument {
34
+ let node: any = { kind: "int" };
35
+ for (let i = 0; i < depth; i++) node = { kind: "array", items: node };
36
+ return {
37
+ anyvaliVersion: "1.0",
38
+ schemaVersion: "1",
39
+ root: node,
40
+ definitions: {},
41
+ extensions: {},
42
+ } as AnyValiDocument;
43
+ }
44
+
45
+ describe("Recursion DoS (AVV-002 / AVV-003)", () => {
46
+ it("safeParse must not throw on deeply nested recursive-schema input", () => {
47
+ const schema = importSchema(recursiveSchemaDoc());
48
+ const data = deepData(100_000);
49
+ // safeParse contract: never throws, always returns a result.
50
+ let result: ReturnType<typeof schema.safeParse>;
51
+ expect(() => {
52
+ result = schema.safeParse(data);
53
+ }).not.toThrow();
54
+ // Bounded: extremely deep input is rejected, not accepted blindly.
55
+ expect(result!.success).toBe(false);
56
+ });
57
+
58
+ it("depth guard is not bypassed by a union in the recursion path", () => {
59
+ // Node = object({ next: union(null, ref Node) }) -- the recursive ref is
60
+ // reached *through* a union variant. The union must propagate depth so the
61
+ // guard still bounds recursion.
62
+ const doc: AnyValiDocument = {
63
+ anyvaliVersion: "1.0",
64
+ schemaVersion: "1",
65
+ root: { kind: "ref", ref: "#/definitions/Node" } as any,
66
+ definitions: {
67
+ Node: {
68
+ kind: "object",
69
+ properties: {
70
+ next: {
71
+ kind: "union",
72
+ variants: [{ kind: "null" }, { kind: "ref", ref: "#/definitions/Node" }],
73
+ },
74
+ },
75
+ required: ["next"],
76
+ unknownKeys: "strip",
77
+ } as any,
78
+ },
79
+ extensions: {},
80
+ } as AnyValiDocument;
81
+ const schema = importSchema(doc);
82
+
83
+ let cur: any = { next: null };
84
+ for (let i = 0; i < 100_000; i++) cur = { next: cur };
85
+
86
+ let result: ReturnType<typeof schema.safeParse>;
87
+ expect(() => {
88
+ result = schema.safeParse(cur);
89
+ }).not.toThrow();
90
+ expect(result!.success).toBe(false);
91
+ });
92
+
93
+ it("importSchema must not stack-overflow on a deeply nested schema doc", () => {
94
+ const doc = deepArraySchemaDoc(100_000);
95
+ // Should fail in a controlled way, not throw RangeError (stack overflow).
96
+ let err: unknown;
97
+ try {
98
+ importSchema(doc);
99
+ } catch (e) {
100
+ err = e;
101
+ }
102
+ expect(err).toBeInstanceOf(Error);
103
+ expect((err as Error) instanceof RangeError).toBe(false);
104
+ });
105
+ });