anyvali 0.3.1 → 0.3.4

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 (40) hide show
  1. package/CHANGELOG.md +67 -44
  2. package/README.md +370 -370
  3. package/dist/parse/coerce.d.ts.map +1 -1
  4. package/dist/parse/coerce.js +14 -0
  5. package/dist/parse/coerce.js.map +1 -1
  6. package/dist/schemas/number.d.ts.map +1 -1
  7. package/dist/schemas/number.js +15 -0
  8. package/dist/schemas/number.js.map +1 -1
  9. package/dist/schemas/optional.d.ts.map +1 -1
  10. package/dist/schemas/optional.js +4 -3
  11. package/dist/schemas/optional.js.map +1 -1
  12. package/package.json +40 -40
  13. package/sdk/js/CHANGELOG.md +13 -13
  14. package/src/format/validators.ts +71 -71
  15. package/src/index.ts +285 -285
  16. package/src/infer.ts +12 -12
  17. package/src/interchange/importer.ts +285 -285
  18. package/src/issue-codes.ts +19 -19
  19. package/src/parse/coerce.ts +15 -0
  20. package/src/schemas/base.ts +322 -322
  21. package/src/schemas/intersection.ts +81 -81
  22. package/src/schemas/number.ts +17 -0
  23. package/src/schemas/object.ts +203 -203
  24. package/src/schemas/optional.ts +4 -3
  25. package/src/schemas/record.ts +55 -55
  26. package/src/schemas/string.ts +192 -192
  27. package/src/schemas/union.ts +53 -53
  28. package/src/types.ts +239 -239
  29. package/tests/unit/collections.test.ts +99 -99
  30. package/tests/unit/date-format.test.ts +18 -18
  31. package/tests/unit/default-mutation.test.ts +32 -32
  32. package/tests/unit/defaults.test.ts +70 -1
  33. package/tests/unit/inference.test.ts +306 -306
  34. package/tests/unit/interchange.test.ts +191 -191
  35. package/tests/unit/object.test.ts +208 -208
  36. package/tests/unit/security-recursion.test.ts +105 -105
  37. package/tests/unit/security.test.ts +1067 -945
  38. package/tests/unit/shared-ref-falsepos.test.ts +33 -33
  39. package/tests/unit/string-pattern-redos.test.ts +46 -46
  40. package/tests/unit/string.test.ts +147 -147
@@ -1,81 +1,81 @@
1
- import type { ParseContext, SchemaNode } from "../types.js";
2
- import { BaseSchema } from "./base.js";
3
-
4
- /** Convert a union of types to an intersection. */
5
- type UnionToIntersection<U> = (
6
- U extends any ? (x: U) => void : never
7
- ) extends (x: infer I) => void
8
- ? I
9
- : never;
10
-
11
- /** Flatten an intersection into a clean single-level type. */
12
- type Prettify<T> = { [K in keyof T]: T[K] } & {};
13
-
14
- export class IntersectionSchema<
15
- T extends BaseSchema<any, any>[] = BaseSchema[],
16
- > extends BaseSchema<
17
- unknown,
18
- Prettify<UnionToIntersection<T[number]["_output"]>>
19
- > {
20
- private _schemas: T;
21
-
22
- constructor(schemas: [...T]) {
23
- super();
24
- this._schemas = schemas as T;
25
- }
26
-
27
- _validate(input: unknown, ctx: ParseContext): unknown {
28
- let result: unknown = input;
29
- let anyFailed = false;
30
-
31
- for (const schema of this._schemas) {
32
- const innerCtx: ParseContext = {
33
- path: [...ctx.path],
34
- issues: [],
35
- // Propagate recursion depth and circular-reference tracking so the
36
- // depth guard is not reset (and bypassed) at each intersection member.
37
- definitions: ctx.definitions,
38
- seen: ctx.seen,
39
- depth: ctx.depth,
40
- };
41
- const validated = schema._runPipeline(input, innerCtx);
42
-
43
- if (innerCtx.issues.length > 0) {
44
- ctx.issues.push(...innerCtx.issues);
45
- anyFailed = true;
46
- } else {
47
- // Merge object results
48
- if (
49
- typeof result === "object" &&
50
- result !== null &&
51
- typeof validated === "object" &&
52
- validated !== null &&
53
- !Array.isArray(result) &&
54
- !Array.isArray(validated)
55
- ) {
56
- result = {
57
- ...(result as Record<string, unknown>),
58
- ...(validated as Record<string, unknown>),
59
- };
60
- } else {
61
- result = validated;
62
- }
63
- }
64
- }
65
-
66
- if (anyFailed) {
67
- return undefined;
68
- }
69
-
70
- return result;
71
- }
72
-
73
- _toNode(): SchemaNode {
74
- const node = {
75
- kind: "intersection" as const,
76
- allOf: this._schemas.map((s) => s._toNode()),
77
- };
78
- this._addDefault(node as unknown as SchemaNode);
79
- return node as unknown as SchemaNode;
80
- }
81
- }
1
+ import type { ParseContext, SchemaNode } from "../types.js";
2
+ import { BaseSchema } from "./base.js";
3
+
4
+ /** Convert a union of types to an intersection. */
5
+ type UnionToIntersection<U> = (
6
+ U extends any ? (x: U) => void : never
7
+ ) extends (x: infer I) => void
8
+ ? I
9
+ : never;
10
+
11
+ /** Flatten an intersection into a clean single-level type. */
12
+ type Prettify<T> = { [K in keyof T]: T[K] } & {};
13
+
14
+ export class IntersectionSchema<
15
+ T extends BaseSchema<any, any>[] = BaseSchema[],
16
+ > extends BaseSchema<
17
+ unknown,
18
+ Prettify<UnionToIntersection<T[number]["_output"]>>
19
+ > {
20
+ private _schemas: T;
21
+
22
+ constructor(schemas: [...T]) {
23
+ super();
24
+ this._schemas = schemas as T;
25
+ }
26
+
27
+ _validate(input: unknown, ctx: ParseContext): unknown {
28
+ let result: unknown = input;
29
+ let anyFailed = false;
30
+
31
+ for (const schema of this._schemas) {
32
+ const innerCtx: ParseContext = {
33
+ path: [...ctx.path],
34
+ issues: [],
35
+ // Propagate recursion depth and circular-reference tracking so the
36
+ // depth guard is not reset (and bypassed) at each intersection member.
37
+ definitions: ctx.definitions,
38
+ seen: ctx.seen,
39
+ depth: ctx.depth,
40
+ };
41
+ const validated = schema._runPipeline(input, innerCtx);
42
+
43
+ if (innerCtx.issues.length > 0) {
44
+ ctx.issues.push(...innerCtx.issues);
45
+ anyFailed = true;
46
+ } else {
47
+ // Merge object results
48
+ if (
49
+ typeof result === "object" &&
50
+ result !== null &&
51
+ typeof validated === "object" &&
52
+ validated !== null &&
53
+ !Array.isArray(result) &&
54
+ !Array.isArray(validated)
55
+ ) {
56
+ result = {
57
+ ...(result as Record<string, unknown>),
58
+ ...(validated as Record<string, unknown>),
59
+ };
60
+ } else {
61
+ result = validated;
62
+ }
63
+ }
64
+ }
65
+
66
+ if (anyFailed) {
67
+ return undefined;
68
+ }
69
+
70
+ return result;
71
+ }
72
+
73
+ _toNode(): SchemaNode {
74
+ const node = {
75
+ kind: "intersection" as const,
76
+ allOf: this._schemas.map((s) => s._toNode()),
77
+ };
78
+ this._addDefault(node as unknown as SchemaNode);
79
+ return node as unknown as SchemaNode;
80
+ }
81
+ }
@@ -3,6 +3,9 @@ import { BaseSchema } from "./base.js";
3
3
  import { ISSUE_CODES } from "../issue-codes.js";
4
4
  import { describeType } from "../util.js";
5
5
 
6
+ /** Largest finite magnitude representable in IEEE 754 binary32. */
7
+ const FLOAT32_MAX = 3.4028234663852886e38;
8
+
6
9
  export class NumberSchema extends BaseSchema<number, number> {
7
10
  protected _kind: SchemaKind;
8
11
  protected _min?: number;
@@ -62,6 +65,20 @@ export class NumberSchema extends BaseSchema<number, number> {
62
65
  return undefined;
63
66
  }
64
67
 
68
+ // float32 MUST reject values outside the binary32 representable range
69
+ // (spec 1.4). Without this, float32 silently accepts any float64 value,
70
+ // defeating the narrowing guarantee.
71
+ if (this._kind === "float32" && input !== 0 && Math.abs(input) > FLOAT32_MAX) {
72
+ ctx.issues.push({
73
+ code: ISSUE_CODES.TOO_LARGE,
74
+ message: `Value ${input} is outside the float32 range`,
75
+ path: [...ctx.path],
76
+ expected: "float32",
77
+ received: String(input),
78
+ });
79
+ return undefined;
80
+ }
81
+
65
82
  this._validateConstraints(input, ctx);
66
83
  return input;
67
84
  }
@@ -1,203 +1,203 @@
1
- import type { ParseContext, SchemaNode, UnknownKeyMode } from "../types.js";
2
- import { BaseSchema, ABSENT } from "./base.js";
3
- import { ISSUE_CODES } from "../issue-codes.js";
4
- import { describeType } from "../util.js";
5
- import type { OptionalSchema } from "./optional.js";
6
-
7
- /** Flatten an intersection into a clean single-level type. */
8
- type Prettify<T> = { [K in keyof T]: T[K] } & {};
9
-
10
- /** Map a shape record to its inferred output type, separating required from optional fields. */
11
- export type InferShape<
12
- T extends Record<string, BaseSchema<any, any>>,
13
- > = Prettify<
14
- {
15
- [K in keyof T as T[K] extends OptionalSchema<any> ? never : K]: T[K]["_output"];
16
- } & {
17
- [K in keyof T as T[K] extends OptionalSchema<any>
18
- ? K
19
- : never]?: T[K]["_output"];
20
- }
21
- >;
22
-
23
- interface PropertyDef {
24
- schema: BaseSchema;
25
- required: boolean;
26
- }
27
-
28
- export class ObjectSchema<
29
- TShape extends Record<string, BaseSchema<any, any>> = Record<
30
- string,
31
- BaseSchema
32
- >,
33
- > extends BaseSchema<Record<string, unknown>, InferShape<TShape>> {
34
- private _properties: Map<string, PropertyDef>;
35
- private _unknownKeys: UnknownKeyMode;
36
- private _unknownKeysExplicit: boolean;
37
-
38
- constructor(
39
- shape: TShape,
40
- options?: { unknownKeys?: UnknownKeyMode },
41
- ) {
42
- super();
43
- this._properties = new Map();
44
- this._unknownKeys = options?.unknownKeys ?? "strip";
45
- this._unknownKeysExplicit = options?.unknownKeys !== undefined;
46
-
47
- for (const [key, schema] of Object.entries(shape)) {
48
- // Check if the schema is an OptionalSchema wrapper
49
- const isOptional = (schema as any)._isOptionalWrapper === true;
50
- this._properties.set(key, {
51
- schema,
52
- required: !isOptional,
53
- });
54
- }
55
- }
56
-
57
- unknownKeys(mode: UnknownKeyMode): ObjectSchema<TShape> {
58
- const clone = this._clone();
59
- clone._unknownKeys = mode;
60
- clone._unknownKeysExplicit = true;
61
- return clone;
62
- }
63
-
64
- private _effectiveUnknownKeys(): UnknownKeyMode {
65
- return this._unknownKeysExplicit ? this._unknownKeys : "strip";
66
- }
67
-
68
- private _exportUnknownKeys(): UnknownKeyMode {
69
- return this._unknownKeysExplicit ? this._unknownKeys : "strip";
70
- }
71
-
72
- _validate(input: unknown, ctx: ParseContext): unknown {
73
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
74
- ctx.issues.push({
75
- code: ISSUE_CODES.INVALID_TYPE,
76
- message: `Expected object, received ${describeType(input)}`,
77
- path: [...ctx.path],
78
- expected: "object",
79
- received: describeType(input),
80
- });
81
- return undefined;
82
- }
83
-
84
- // Circular reference detection
85
- if (!ctx.seen) ctx.seen = new WeakSet();
86
- if (ctx.seen.has(input as object)) {
87
- ctx.issues.push({
88
- code: ISSUE_CODES.INVALID_TYPE,
89
- message: "Circular reference detected",
90
- path: [...ctx.path],
91
- expected: "object",
92
- received: "circular",
93
- });
94
- return undefined;
95
- }
96
- ctx.seen.add(input as object);
97
-
98
- const obj = input as Record<string, unknown>;
99
- const result: Record<string, unknown> = Object.create(null);
100
- const inputKeys = new Set(Object.keys(obj));
101
-
102
- // Detect __proto__ via hasOwnProperty (Object.keys skips it)
103
- if (Object.prototype.hasOwnProperty.call(obj, "__proto__")) {
104
- inputKeys.add("__proto__");
105
- }
106
-
107
- // Validate declared properties
108
- for (const [key, prop] of this._properties) {
109
- ctx.path.push(key);
110
- const hasKey = Object.prototype.hasOwnProperty.call(obj, key);
111
- inputKeys.delete(key);
112
-
113
- if (!hasKey) {
114
- // Check if required
115
- if (prop.required && prop.schema._defaultValue === ABSENT) {
116
- const expectedKind = prop.schema._toNode().kind;
117
- ctx.issues.push({
118
- code: ISSUE_CODES.REQUIRED,
119
- message: `Required property "${key}" is missing`,
120
- path: [...ctx.path],
121
- expected: expectedKind,
122
- received: "undefined",
123
- });
124
- ctx.path.pop();
125
- continue;
126
- }
127
- }
128
-
129
- const rawValue = hasKey ? obj[key] : undefined;
130
- const val = prop.schema._runPipeline(rawValue, ctx);
131
-
132
- // Only include in result if value is not undefined or it was explicitly present
133
- if (val !== undefined || hasKey || prop.schema._defaultValue !== ABSENT) {
134
- Object.defineProperty(result, key, {
135
- value: val,
136
- writable: true,
137
- enumerable: true,
138
- configurable: true,
139
- });
140
- }
141
-
142
- ctx.path.pop();
143
- }
144
-
145
- // Handle unknown keys
146
- for (const key of inputKeys) {
147
- switch (this._effectiveUnknownKeys()) {
148
- case "reject":
149
- ctx.issues.push({
150
- code: ISSUE_CODES.UNKNOWN_KEY,
151
- message: `Unknown key "${key}"`,
152
- path: [...ctx.path, key],
153
- expected: "undefined",
154
- received: key,
155
- });
156
- break;
157
- case "allow":
158
- Object.defineProperty(result, key, {
159
- value: obj[key],
160
- writable: true,
161
- enumerable: true,
162
- configurable: true,
163
- });
164
- break;
165
- case "strip":
166
- // Just ignore it
167
- break;
168
- }
169
- }
170
-
171
- // Remove from the ancestor set now that this subtree is fully processed.
172
- // The guard tracks the current ancestor chain (true cycles), not every
173
- // object ever seen — otherwise a shared/repeated non-circular reference in
174
- // sibling positions would be falsely rejected as circular.
175
- ctx.seen.delete(input as object);
176
-
177
- // Restore normal prototype so result behaves like a standard object
178
- // while preventing __proto__ pollution via Object.create(null) above
179
- Object.setPrototypeOf(result, Object.prototype);
180
- return result;
181
- }
182
-
183
- _toNode(): SchemaNode {
184
- const properties: Record<string, SchemaNode> = {};
185
- const required: string[] = [];
186
-
187
- for (const [key, prop] of this._properties) {
188
- properties[key] = prop.schema._toNode();
189
- if (prop.required) {
190
- required.push(key);
191
- }
192
- }
193
-
194
- const node = {
195
- kind: "object" as const,
196
- properties,
197
- required,
198
- unknownKeys: this._exportUnknownKeys(),
199
- };
200
- this._addDefault(node as unknown as SchemaNode);
201
- return node as unknown as SchemaNode;
202
- }
203
- }
1
+ import type { ParseContext, SchemaNode, UnknownKeyMode } from "../types.js";
2
+ import { BaseSchema, ABSENT } from "./base.js";
3
+ import { ISSUE_CODES } from "../issue-codes.js";
4
+ import { describeType } from "../util.js";
5
+ import type { OptionalSchema } from "./optional.js";
6
+
7
+ /** Flatten an intersection into a clean single-level type. */
8
+ type Prettify<T> = { [K in keyof T]: T[K] } & {};
9
+
10
+ /** Map a shape record to its inferred output type, separating required from optional fields. */
11
+ export type InferShape<
12
+ T extends Record<string, BaseSchema<any, any>>,
13
+ > = Prettify<
14
+ {
15
+ [K in keyof T as T[K] extends OptionalSchema<any> ? never : K]: T[K]["_output"];
16
+ } & {
17
+ [K in keyof T as T[K] extends OptionalSchema<any>
18
+ ? K
19
+ : never]?: T[K]["_output"];
20
+ }
21
+ >;
22
+
23
+ interface PropertyDef {
24
+ schema: BaseSchema;
25
+ required: boolean;
26
+ }
27
+
28
+ export class ObjectSchema<
29
+ TShape extends Record<string, BaseSchema<any, any>> = Record<
30
+ string,
31
+ BaseSchema
32
+ >,
33
+ > extends BaseSchema<Record<string, unknown>, InferShape<TShape>> {
34
+ private _properties: Map<string, PropertyDef>;
35
+ private _unknownKeys: UnknownKeyMode;
36
+ private _unknownKeysExplicit: boolean;
37
+
38
+ constructor(
39
+ shape: TShape,
40
+ options?: { unknownKeys?: UnknownKeyMode },
41
+ ) {
42
+ super();
43
+ this._properties = new Map();
44
+ this._unknownKeys = options?.unknownKeys ?? "strip";
45
+ this._unknownKeysExplicit = options?.unknownKeys !== undefined;
46
+
47
+ for (const [key, schema] of Object.entries(shape)) {
48
+ // Check if the schema is an OptionalSchema wrapper
49
+ const isOptional = (schema as any)._isOptionalWrapper === true;
50
+ this._properties.set(key, {
51
+ schema,
52
+ required: !isOptional,
53
+ });
54
+ }
55
+ }
56
+
57
+ unknownKeys(mode: UnknownKeyMode): ObjectSchema<TShape> {
58
+ const clone = this._clone();
59
+ clone._unknownKeys = mode;
60
+ clone._unknownKeysExplicit = true;
61
+ return clone;
62
+ }
63
+
64
+ private _effectiveUnknownKeys(): UnknownKeyMode {
65
+ return this._unknownKeysExplicit ? this._unknownKeys : "strip";
66
+ }
67
+
68
+ private _exportUnknownKeys(): UnknownKeyMode {
69
+ return this._unknownKeysExplicit ? this._unknownKeys : "strip";
70
+ }
71
+
72
+ _validate(input: unknown, ctx: ParseContext): unknown {
73
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
74
+ ctx.issues.push({
75
+ code: ISSUE_CODES.INVALID_TYPE,
76
+ message: `Expected object, received ${describeType(input)}`,
77
+ path: [...ctx.path],
78
+ expected: "object",
79
+ received: describeType(input),
80
+ });
81
+ return undefined;
82
+ }
83
+
84
+ // Circular reference detection
85
+ if (!ctx.seen) ctx.seen = new WeakSet();
86
+ if (ctx.seen.has(input as object)) {
87
+ ctx.issues.push({
88
+ code: ISSUE_CODES.INVALID_TYPE,
89
+ message: "Circular reference detected",
90
+ path: [...ctx.path],
91
+ expected: "object",
92
+ received: "circular",
93
+ });
94
+ return undefined;
95
+ }
96
+ ctx.seen.add(input as object);
97
+
98
+ const obj = input as Record<string, unknown>;
99
+ const result: Record<string, unknown> = Object.create(null);
100
+ const inputKeys = new Set(Object.keys(obj));
101
+
102
+ // Detect __proto__ via hasOwnProperty (Object.keys skips it)
103
+ if (Object.prototype.hasOwnProperty.call(obj, "__proto__")) {
104
+ inputKeys.add("__proto__");
105
+ }
106
+
107
+ // Validate declared properties
108
+ for (const [key, prop] of this._properties) {
109
+ ctx.path.push(key);
110
+ const hasKey = Object.prototype.hasOwnProperty.call(obj, key);
111
+ inputKeys.delete(key);
112
+
113
+ if (!hasKey) {
114
+ // Check if required
115
+ if (prop.required && prop.schema._defaultValue === ABSENT) {
116
+ const expectedKind = prop.schema._toNode().kind;
117
+ ctx.issues.push({
118
+ code: ISSUE_CODES.REQUIRED,
119
+ message: `Required property "${key}" is missing`,
120
+ path: [...ctx.path],
121
+ expected: expectedKind,
122
+ received: "undefined",
123
+ });
124
+ ctx.path.pop();
125
+ continue;
126
+ }
127
+ }
128
+
129
+ const rawValue = hasKey ? obj[key] : undefined;
130
+ const val = prop.schema._runPipeline(rawValue, ctx);
131
+
132
+ // Only include in result if value is not undefined or it was explicitly present
133
+ if (val !== undefined || hasKey || prop.schema._defaultValue !== ABSENT) {
134
+ Object.defineProperty(result, key, {
135
+ value: val,
136
+ writable: true,
137
+ enumerable: true,
138
+ configurable: true,
139
+ });
140
+ }
141
+
142
+ ctx.path.pop();
143
+ }
144
+
145
+ // Handle unknown keys
146
+ for (const key of inputKeys) {
147
+ switch (this._effectiveUnknownKeys()) {
148
+ case "reject":
149
+ ctx.issues.push({
150
+ code: ISSUE_CODES.UNKNOWN_KEY,
151
+ message: `Unknown key "${key}"`,
152
+ path: [...ctx.path, key],
153
+ expected: "undefined",
154
+ received: key,
155
+ });
156
+ break;
157
+ case "allow":
158
+ Object.defineProperty(result, key, {
159
+ value: obj[key],
160
+ writable: true,
161
+ enumerable: true,
162
+ configurable: true,
163
+ });
164
+ break;
165
+ case "strip":
166
+ // Just ignore it
167
+ break;
168
+ }
169
+ }
170
+
171
+ // Remove from the ancestor set now that this subtree is fully processed.
172
+ // The guard tracks the current ancestor chain (true cycles), not every
173
+ // object ever seen — otherwise a shared/repeated non-circular reference in
174
+ // sibling positions would be falsely rejected as circular.
175
+ ctx.seen.delete(input as object);
176
+
177
+ // Restore normal prototype so result behaves like a standard object
178
+ // while preventing __proto__ pollution via Object.create(null) above
179
+ Object.setPrototypeOf(result, Object.prototype);
180
+ return result;
181
+ }
182
+
183
+ _toNode(): SchemaNode {
184
+ const properties: Record<string, SchemaNode> = {};
185
+ const required: string[] = [];
186
+
187
+ for (const [key, prop] of this._properties) {
188
+ properties[key] = prop.schema._toNode();
189
+ if (prop.required) {
190
+ required.push(key);
191
+ }
192
+ }
193
+
194
+ const node = {
195
+ kind: "object" as const,
196
+ properties,
197
+ required,
198
+ unknownKeys: this._exportUnknownKeys(),
199
+ };
200
+ this._addDefault(node as unknown as SchemaNode);
201
+ return node as unknown as SchemaNode;
202
+ }
203
+ }
@@ -25,9 +25,10 @@ export class OptionalSchema<
25
25
  _runPipeline(input: unknown, ctx: ParseContext): unknown {
26
26
  const isAbsent = input === undefined || input === ABSENT;
27
27
 
28
- // If absent and we have a default from inner, apply it
29
- if (isAbsent && this._inner._defaultValue !== ABSENT) {
30
- return this._inner._runPipeline(input, ctx);
28
+ // If absent and we have a default on this wrapper, use the normal
29
+ // BaseSchema default path so validation and cloning still run.
30
+ if (isAbsent && this._defaultValue !== ABSENT) {
31
+ return super._runPipeline(input, ctx);
31
32
  }
32
33
 
33
34
  if (isAbsent) {