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,285 +1,285 @@
1
- import type {
2
- AnyValiDocument,
3
- SchemaNode,
4
- } from "../types.js";
5
- import { BaseSchema } from "../schemas/base.js";
6
- import { StringSchema } from "../schemas/string.js";
7
- import {
8
- NumberSchema,
9
- Float32Schema,
10
- Float64Schema,
11
- } from "../schemas/number.js";
12
- import {
13
- IntSchema,
14
- Int8Schema,
15
- Int16Schema,
16
- Int32Schema,
17
- Int64Schema,
18
- Uint8Schema,
19
- Uint16Schema,
20
- Uint32Schema,
21
- Uint64Schema,
22
- } from "../schemas/int.js";
23
- import { BoolSchema } from "../schemas/bool.js";
24
- import { NullSchema } from "../schemas/null.js";
25
- import { AnySchema } from "../schemas/any.js";
26
- import { UnknownSchema } from "../schemas/unknown.js";
27
- import { NeverSchema } from "../schemas/never.js";
28
- import { LiteralSchema } from "../schemas/literal.js";
29
- import { EnumSchema } from "../schemas/enum.js";
30
- import { ArraySchema } from "../schemas/array.js";
31
- import { TupleSchema } from "../schemas/tuple.js";
32
- import { ObjectSchema } from "../schemas/object.js";
33
- import { RecordSchema } from "../schemas/record.js";
34
- import { UnionSchema } from "../schemas/union.js";
35
- import { IntersectionSchema } from "../schemas/intersection.js";
36
- import { OptionalSchema } from "../schemas/optional.js";
37
- import { NullableSchema } from "../schemas/nullable.js";
38
- import { RefSchema } from "../schemas/ref.js";
39
- import { normalizeCoercionConfig } from "../parse/coerce.js";
40
- import type { StringFormat, UnknownKeyMode } from "../types.js";
41
-
42
- /**
43
- * Import an AnyValiDocument back into a live Schema.
44
- */
45
- /**
46
- * Maximum schema-document nesting depth accepted by importSchema. Bounds the
47
- * recursive importNode walk so an untrusted, deeply nested document cannot
48
- * exhaust the call stack (DoS). Throws a controlled error instead of a
49
- * RangeError stack overflow.
50
- */
51
- const MAX_IMPORT_DEPTH = 512;
52
-
53
- export function importSchema(doc: AnyValiDocument): BaseSchema {
54
- const definitions = doc.definitions ?? {};
55
- const resolvedDefs = new Map<string, BaseSchema>();
56
-
57
- function importNode(node: any, depth: number = 0): BaseSchema {
58
- if (depth > MAX_IMPORT_DEPTH) {
59
- throw new Error(
60
- `Schema document too deeply nested (max depth ${MAX_IMPORT_DEPTH} exceeded)`
61
- );
62
- }
63
- const d = depth + 1;
64
- let schema: BaseSchema;
65
-
66
- switch (node.kind) {
67
- case "string": {
68
- let s = new StringSchema();
69
- if (node.minLength !== undefined) s = s.minLength(node.minLength);
70
- if (node.maxLength !== undefined) s = s.maxLength(node.maxLength);
71
- if (node.pattern !== undefined) s = s.pattern(node.pattern);
72
- if (node.startsWith !== undefined) s = s.startsWith(node.startsWith);
73
- if (node.endsWith !== undefined) s = s.endsWith(node.endsWith);
74
- if (node.includes !== undefined) s = s.includes(node.includes);
75
- if (node.format !== undefined)
76
- s = s.format(node.format as StringFormat);
77
- schema = s;
78
- break;
79
- }
80
-
81
- case "number":
82
- case "float64": {
83
- let s =
84
- node.kind === "float64" ? new Float64Schema() : new NumberSchema();
85
- schema = applyNumericConstraints(s, node);
86
- break;
87
- }
88
-
89
- case "float32": {
90
- schema = applyNumericConstraints(new Float32Schema(), node);
91
- break;
92
- }
93
-
94
- case "int":
95
- case "int64": {
96
- schema = applyNumericConstraints(
97
- node.kind === "int64" ? new Int64Schema() : new IntSchema(),
98
- node
99
- );
100
- break;
101
- }
102
-
103
- case "int8":
104
- schema = applyNumericConstraints(new Int8Schema(), node);
105
- break;
106
- case "int16":
107
- schema = applyNumericConstraints(new Int16Schema(), node);
108
- break;
109
- case "int32":
110
- schema = applyNumericConstraints(new Int32Schema(), node);
111
- break;
112
- case "uint8":
113
- schema = applyNumericConstraints(new Uint8Schema(), node);
114
- break;
115
- case "uint16":
116
- schema = applyNumericConstraints(new Uint16Schema(), node);
117
- break;
118
- case "uint32":
119
- schema = applyNumericConstraints(new Uint32Schema(), node);
120
- break;
121
- case "uint64":
122
- schema = applyNumericConstraints(new Uint64Schema(), node);
123
- break;
124
-
125
- case "bool":
126
- schema = new BoolSchema();
127
- break;
128
-
129
- case "null":
130
- schema = new NullSchema();
131
- break;
132
-
133
- case "any":
134
- schema = new AnySchema();
135
- break;
136
-
137
- case "unknown":
138
- schema = new UnknownSchema();
139
- break;
140
-
141
- case "never":
142
- schema = new NeverSchema();
143
- break;
144
-
145
- case "literal": {
146
- schema = new LiteralSchema(node.value);
147
- break;
148
- }
149
-
150
- case "enum": {
151
- schema = new EnumSchema(node.values);
152
- break;
153
- }
154
-
155
- case "array": {
156
- let s = new ArraySchema(importNode(node.items, d));
157
- if (node.minItems !== undefined) s = s.minItems(node.minItems);
158
- if (node.maxItems !== undefined) s = s.maxItems(node.maxItems);
159
- schema = s;
160
- break;
161
- }
162
-
163
- case "tuple": {
164
- // Corpus uses "elements", our export uses "items"
165
- const elements = node.elements ?? node.items;
166
- schema = new TupleSchema(
167
- elements.map((i: any) => importNode(i, d))
168
- );
169
- break;
170
- }
171
-
172
- case "object": {
173
- const shape: Record<string, BaseSchema> = Object.create(null);
174
- const requiredSet = new Set<string>(node.required ?? []);
175
- for (const [key, propNode] of Object.entries(
176
- node.properties ?? {}
177
- )) {
178
- let propSchema = importNode(propNode, d);
179
- if (!requiredSet.has(key)) {
180
- propSchema = new OptionalSchema(propSchema);
181
- }
182
- // Use defineProperty to safely handle __proto__ and other special keys
183
- Object.defineProperty(shape, key, {
184
- value: propSchema,
185
- writable: true,
186
- enumerable: true,
187
- configurable: true,
188
- });
189
- }
190
- Object.setPrototypeOf(shape, Object.prototype);
191
- schema = new ObjectSchema(shape, {
192
- unknownKeys:
193
- (node.unknownKeys as UnknownKeyMode) ?? "strip",
194
- });
195
- break;
196
- }
197
-
198
- case "record": {
199
- // Corpus uses "values", our export uses "valueSchema"
200
- const valueNode = node.values ?? node.valueSchema;
201
- schema = new RecordSchema(importNode(valueNode, d));
202
- break;
203
- }
204
-
205
- case "union": {
206
- schema = new UnionSchema(
207
- node.variants.map((v: any) => importNode(v, d))
208
- );
209
- break;
210
- }
211
-
212
- case "intersection": {
213
- schema = new IntersectionSchema(
214
- node.allOf.map((s: any) => importNode(s, d))
215
- );
216
- break;
217
- }
218
-
219
- case "optional": {
220
- // Corpus uses "schema", our export uses "inner"
221
- const innerNode = node.schema ?? node.inner;
222
- schema = new OptionalSchema(importNode(innerNode, d));
223
- break;
224
- }
225
-
226
- case "nullable": {
227
- // Corpus uses "schema", our export uses "inner"
228
- const innerNode = node.schema ?? node.inner;
229
- schema = new NullableSchema(importNode(innerNode, d));
230
- break;
231
- }
232
-
233
- case "ref": {
234
- const refPath = node.ref as string;
235
- const defName = refPath.replace("#/definitions/", "");
236
- schema = new RefSchema(refPath, () => {
237
- if (resolvedDefs.has(defName)) {
238
- return resolvedDefs.get(defName)!;
239
- }
240
- const defNode = definitions[defName];
241
- if (!defNode) {
242
- throw new Error(`Unresolved definition: ${defName}`);
243
- }
244
- const resolved = importNode(defNode);
245
- resolvedDefs.set(defName, resolved);
246
- return resolved;
247
- });
248
- break;
249
- }
250
-
251
- default:
252
- throw new Error(`Unsupported schema kind: ${node.kind}`);
253
- }
254
-
255
- // Apply default
256
- if (node.default !== undefined) {
257
- schema = schema.default(node.default as any);
258
- }
259
-
260
- // Apply coercion config - handle both string and object formats
261
- if (node.coerce !== undefined) {
262
- const config = normalizeCoercionConfig(node.coerce);
263
- schema = schema.coerce(config);
264
- }
265
-
266
- return schema;
267
- }
268
-
269
- return importNode(doc.root);
270
- }
271
-
272
- function applyNumericConstraints<T extends NumberSchema>(
273
- schema: T,
274
- node: any
275
- ): T {
276
- let s = schema;
277
- if (node.min !== undefined) s = s.min(node.min) as T;
278
- if (node.max !== undefined) s = s.max(node.max) as T;
279
- if (node.exclusiveMin !== undefined)
280
- s = s.exclusiveMin(node.exclusiveMin) as T;
281
- if (node.exclusiveMax !== undefined)
282
- s = s.exclusiveMax(node.exclusiveMax) as T;
283
- if (node.multipleOf !== undefined) s = s.multipleOf(node.multipleOf) as T;
284
- return s;
285
- }
1
+ import type {
2
+ AnyValiDocument,
3
+ SchemaNode,
4
+ } from "../types.js";
5
+ import { BaseSchema } from "../schemas/base.js";
6
+ import { StringSchema } from "../schemas/string.js";
7
+ import {
8
+ NumberSchema,
9
+ Float32Schema,
10
+ Float64Schema,
11
+ } from "../schemas/number.js";
12
+ import {
13
+ IntSchema,
14
+ Int8Schema,
15
+ Int16Schema,
16
+ Int32Schema,
17
+ Int64Schema,
18
+ Uint8Schema,
19
+ Uint16Schema,
20
+ Uint32Schema,
21
+ Uint64Schema,
22
+ } from "../schemas/int.js";
23
+ import { BoolSchema } from "../schemas/bool.js";
24
+ import { NullSchema } from "../schemas/null.js";
25
+ import { AnySchema } from "../schemas/any.js";
26
+ import { UnknownSchema } from "../schemas/unknown.js";
27
+ import { NeverSchema } from "../schemas/never.js";
28
+ import { LiteralSchema } from "../schemas/literal.js";
29
+ import { EnumSchema } from "../schemas/enum.js";
30
+ import { ArraySchema } from "../schemas/array.js";
31
+ import { TupleSchema } from "../schemas/tuple.js";
32
+ import { ObjectSchema } from "../schemas/object.js";
33
+ import { RecordSchema } from "../schemas/record.js";
34
+ import { UnionSchema } from "../schemas/union.js";
35
+ import { IntersectionSchema } from "../schemas/intersection.js";
36
+ import { OptionalSchema } from "../schemas/optional.js";
37
+ import { NullableSchema } from "../schemas/nullable.js";
38
+ import { RefSchema } from "../schemas/ref.js";
39
+ import { normalizeCoercionConfig } from "../parse/coerce.js";
40
+ import type { StringFormat, UnknownKeyMode } from "../types.js";
41
+
42
+ /**
43
+ * Import an AnyValiDocument back into a live Schema.
44
+ */
45
+ /**
46
+ * Maximum schema-document nesting depth accepted by importSchema. Bounds the
47
+ * recursive importNode walk so an untrusted, deeply nested document cannot
48
+ * exhaust the call stack (DoS). Throws a controlled error instead of a
49
+ * RangeError stack overflow.
50
+ */
51
+ const MAX_IMPORT_DEPTH = 512;
52
+
53
+ export function importSchema(doc: AnyValiDocument): BaseSchema {
54
+ const definitions = doc.definitions ?? {};
55
+ const resolvedDefs = new Map<string, BaseSchema>();
56
+
57
+ function importNode(node: any, depth: number = 0): BaseSchema {
58
+ if (depth > MAX_IMPORT_DEPTH) {
59
+ throw new Error(
60
+ `Schema document too deeply nested (max depth ${MAX_IMPORT_DEPTH} exceeded)`
61
+ );
62
+ }
63
+ const d = depth + 1;
64
+ let schema: BaseSchema;
65
+
66
+ switch (node.kind) {
67
+ case "string": {
68
+ let s = new StringSchema();
69
+ if (node.minLength !== undefined) s = s.minLength(node.minLength);
70
+ if (node.maxLength !== undefined) s = s.maxLength(node.maxLength);
71
+ if (node.pattern !== undefined) s = s.pattern(node.pattern);
72
+ if (node.startsWith !== undefined) s = s.startsWith(node.startsWith);
73
+ if (node.endsWith !== undefined) s = s.endsWith(node.endsWith);
74
+ if (node.includes !== undefined) s = s.includes(node.includes);
75
+ if (node.format !== undefined)
76
+ s = s.format(node.format as StringFormat);
77
+ schema = s;
78
+ break;
79
+ }
80
+
81
+ case "number":
82
+ case "float64": {
83
+ let s =
84
+ node.kind === "float64" ? new Float64Schema() : new NumberSchema();
85
+ schema = applyNumericConstraints(s, node);
86
+ break;
87
+ }
88
+
89
+ case "float32": {
90
+ schema = applyNumericConstraints(new Float32Schema(), node);
91
+ break;
92
+ }
93
+
94
+ case "int":
95
+ case "int64": {
96
+ schema = applyNumericConstraints(
97
+ node.kind === "int64" ? new Int64Schema() : new IntSchema(),
98
+ node
99
+ );
100
+ break;
101
+ }
102
+
103
+ case "int8":
104
+ schema = applyNumericConstraints(new Int8Schema(), node);
105
+ break;
106
+ case "int16":
107
+ schema = applyNumericConstraints(new Int16Schema(), node);
108
+ break;
109
+ case "int32":
110
+ schema = applyNumericConstraints(new Int32Schema(), node);
111
+ break;
112
+ case "uint8":
113
+ schema = applyNumericConstraints(new Uint8Schema(), node);
114
+ break;
115
+ case "uint16":
116
+ schema = applyNumericConstraints(new Uint16Schema(), node);
117
+ break;
118
+ case "uint32":
119
+ schema = applyNumericConstraints(new Uint32Schema(), node);
120
+ break;
121
+ case "uint64":
122
+ schema = applyNumericConstraints(new Uint64Schema(), node);
123
+ break;
124
+
125
+ case "bool":
126
+ schema = new BoolSchema();
127
+ break;
128
+
129
+ case "null":
130
+ schema = new NullSchema();
131
+ break;
132
+
133
+ case "any":
134
+ schema = new AnySchema();
135
+ break;
136
+
137
+ case "unknown":
138
+ schema = new UnknownSchema();
139
+ break;
140
+
141
+ case "never":
142
+ schema = new NeverSchema();
143
+ break;
144
+
145
+ case "literal": {
146
+ schema = new LiteralSchema(node.value);
147
+ break;
148
+ }
149
+
150
+ case "enum": {
151
+ schema = new EnumSchema(node.values);
152
+ break;
153
+ }
154
+
155
+ case "array": {
156
+ let s = new ArraySchema(importNode(node.items, d));
157
+ if (node.minItems !== undefined) s = s.minItems(node.minItems);
158
+ if (node.maxItems !== undefined) s = s.maxItems(node.maxItems);
159
+ schema = s;
160
+ break;
161
+ }
162
+
163
+ case "tuple": {
164
+ // Corpus uses "elements", our export uses "items"
165
+ const elements = node.elements ?? node.items;
166
+ schema = new TupleSchema(
167
+ elements.map((i: any) => importNode(i, d))
168
+ );
169
+ break;
170
+ }
171
+
172
+ case "object": {
173
+ const shape: Record<string, BaseSchema> = Object.create(null);
174
+ const requiredSet = new Set<string>(node.required ?? []);
175
+ for (const [key, propNode] of Object.entries(
176
+ node.properties ?? {}
177
+ )) {
178
+ let propSchema = importNode(propNode, d);
179
+ if (!requiredSet.has(key)) {
180
+ propSchema = new OptionalSchema(propSchema);
181
+ }
182
+ // Use defineProperty to safely handle __proto__ and other special keys
183
+ Object.defineProperty(shape, key, {
184
+ value: propSchema,
185
+ writable: true,
186
+ enumerable: true,
187
+ configurable: true,
188
+ });
189
+ }
190
+ Object.setPrototypeOf(shape, Object.prototype);
191
+ schema = new ObjectSchema(shape, {
192
+ unknownKeys:
193
+ (node.unknownKeys as UnknownKeyMode) ?? "strip",
194
+ });
195
+ break;
196
+ }
197
+
198
+ case "record": {
199
+ // Corpus uses "values", our export uses "valueSchema"
200
+ const valueNode = node.values ?? node.valueSchema;
201
+ schema = new RecordSchema(importNode(valueNode, d));
202
+ break;
203
+ }
204
+
205
+ case "union": {
206
+ schema = new UnionSchema(
207
+ node.variants.map((v: any) => importNode(v, d))
208
+ );
209
+ break;
210
+ }
211
+
212
+ case "intersection": {
213
+ schema = new IntersectionSchema(
214
+ node.allOf.map((s: any) => importNode(s, d))
215
+ );
216
+ break;
217
+ }
218
+
219
+ case "optional": {
220
+ // Corpus uses "schema", our export uses "inner"
221
+ const innerNode = node.schema ?? node.inner;
222
+ schema = new OptionalSchema(importNode(innerNode, d));
223
+ break;
224
+ }
225
+
226
+ case "nullable": {
227
+ // Corpus uses "schema", our export uses "inner"
228
+ const innerNode = node.schema ?? node.inner;
229
+ schema = new NullableSchema(importNode(innerNode, d));
230
+ break;
231
+ }
232
+
233
+ case "ref": {
234
+ const refPath = node.ref as string;
235
+ const defName = refPath.replace("#/definitions/", "");
236
+ schema = new RefSchema(refPath, () => {
237
+ if (resolvedDefs.has(defName)) {
238
+ return resolvedDefs.get(defName)!;
239
+ }
240
+ const defNode = definitions[defName];
241
+ if (!defNode) {
242
+ throw new Error(`Unresolved definition: ${defName}`);
243
+ }
244
+ const resolved = importNode(defNode);
245
+ resolvedDefs.set(defName, resolved);
246
+ return resolved;
247
+ });
248
+ break;
249
+ }
250
+
251
+ default:
252
+ throw new Error(`Unsupported schema kind: ${node.kind}`);
253
+ }
254
+
255
+ // Apply default
256
+ if (node.default !== undefined) {
257
+ schema = schema.default(node.default as any);
258
+ }
259
+
260
+ // Apply coercion config - handle both string and object formats
261
+ if (node.coerce !== undefined) {
262
+ const config = normalizeCoercionConfig(node.coerce);
263
+ schema = schema.coerce(config);
264
+ }
265
+
266
+ return schema;
267
+ }
268
+
269
+ return importNode(doc.root);
270
+ }
271
+
272
+ function applyNumericConstraints<T extends NumberSchema>(
273
+ schema: T,
274
+ node: any
275
+ ): T {
276
+ let s = schema;
277
+ if (node.min !== undefined) s = s.min(node.min) as T;
278
+ if (node.max !== undefined) s = s.max(node.max) as T;
279
+ if (node.exclusiveMin !== undefined)
280
+ s = s.exclusiveMin(node.exclusiveMin) as T;
281
+ if (node.exclusiveMax !== undefined)
282
+ s = s.exclusiveMax(node.exclusiveMax) as T;
283
+ if (node.multipleOf !== undefined) s = s.multipleOf(node.multipleOf) as T;
284
+ return s;
285
+ }
@@ -1,19 +1,19 @@
1
- export const ISSUE_CODES = {
2
- INVALID_TYPE: "invalid_type",
3
- REQUIRED: "required",
4
- UNKNOWN_KEY: "unknown_key",
5
- TOO_SMALL: "too_small",
6
- TOO_LARGE: "too_large",
7
- INVALID_STRING: "invalid_string",
8
- INVALID_NUMBER: "invalid_number",
9
- INVALID_LITERAL: "invalid_literal",
10
- INVALID_UNION: "invalid_union",
11
- CUSTOM_VALIDATION_NOT_PORTABLE: "custom_validation_not_portable",
12
- UNSUPPORTED_EXTENSION: "unsupported_extension",
13
- UNSUPPORTED_SCHEMA_KIND: "unsupported_schema_kind",
14
- COERCION_FAILED: "coercion_failed",
15
- DEFAULT_INVALID: "default_invalid",
16
- TOO_DEEP: "too_deep",
17
- } as const;
18
-
19
- export type IssueCode = (typeof ISSUE_CODES)[keyof typeof ISSUE_CODES];
1
+ export const ISSUE_CODES = {
2
+ INVALID_TYPE: "invalid_type",
3
+ REQUIRED: "required",
4
+ UNKNOWN_KEY: "unknown_key",
5
+ TOO_SMALL: "too_small",
6
+ TOO_LARGE: "too_large",
7
+ INVALID_STRING: "invalid_string",
8
+ INVALID_NUMBER: "invalid_number",
9
+ INVALID_LITERAL: "invalid_literal",
10
+ INVALID_UNION: "invalid_union",
11
+ CUSTOM_VALIDATION_NOT_PORTABLE: "custom_validation_not_portable",
12
+ UNSUPPORTED_EXTENSION: "unsupported_extension",
13
+ UNSUPPORTED_SCHEMA_KIND: "unsupported_schema_kind",
14
+ COERCION_FAILED: "coercion_failed",
15
+ DEFAULT_INVALID: "default_invalid",
16
+ TOO_DEEP: "too_deep",
17
+ } as const;
18
+
19
+ export type IssueCode = (typeof ISSUE_CODES)[keyof typeof ISSUE_CODES];
@@ -1,5 +1,10 @@
1
1
  import type { CoercionConfig } from "../types.js";
2
2
 
3
+ // Decimal floating-point grammar: optional sign, digits with optional
4
+ // fraction (or bare fraction), optional decimal exponent. Excludes hex/octal/
5
+ // binary literals, Infinity and NaN that JS `Number()` would otherwise accept.
6
+ const DECIMAL_FLOAT_RE = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
7
+
3
8
  export type CoercionResult =
4
9
  | { success: true; value: unknown }
5
10
  | { success: false; message: string };
@@ -101,6 +106,16 @@ export function applyCoercion(
101
106
  message: `Cannot coerce empty string to ${targetType}`,
102
107
  };
103
108
  }
109
+ // Spec 5.1: parse as DECIMAL floating-point. JS `Number()` also accepts
110
+ // hex (0x), octal (0o) and binary (0b) literals, which would let
111
+ // "0x10" slip through as 16 and bypass the decimal-only contract.
112
+ // Restrict to a decimal float grammar before parsing.
113
+ if (!DECIMAL_FLOAT_RE.test(trimmed)) {
114
+ return {
115
+ success: false,
116
+ message: `Cannot coerce "${value}" to ${targetType}`,
117
+ };
118
+ }
104
119
  const num = Number(trimmed);
105
120
  if (!Number.isFinite(num)) {
106
121
  return {