ata-validator 1.2.1 → 1.3.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/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
4
4
 
5
+ ## 1.3.0 - 2026-08-02
6
+
7
+ ### Added
8
+
9
+ - `useDefaults` and `assertFormat` validator options, both defaulting to `true` so existing behavior is unchanged. `useDefaults: false` stops missing properties being filled in from their `default` before validation, which matters because a `default` that does not satisfy its own subschema currently makes an otherwise valid instance fail. `assertFormat: false` treats `format` as an annotation rather than an assertion, the Draft 2020-12 reading when the format-assertion vocabulary is not in use. With both off, ata passes every case in the suite's `format` and `default` files.
10
+
11
+ ### Fixed
12
+
13
+ - A `$ref` that could not be resolved validated everything instead of failing. The JS compiler emitted no check at all for an unresolved reference, so a typo in a `$ref`, or a reference into a schema that was never registered, silently turned off every constraint behind it rather than reporting an error. Both compiler paths now decline to compile such a schema and it validates on the interpreted engine, which reports `ATA5001` naming the reference it could not resolve.
14
+ - References that resolve relative to an enclosing base URI are no longer compiled by the JS paths, which match registry entries by exact key or path suffix and have no notion of a base. Nested `$id` scopes, relative references, and documents registered under a URI different from the `$id` they declare now route to the interpreted engine, which tracks the base properly. Schemas that reference a flat registry of ids, the common `$ref: 'shared#'` shape, still take the compiled fast path.
15
+ - A schema supplied through the `schemas` option as a URI-keyed record was registered only under the `$id` it declared, so references to the URI it was registered under could not resolve. It is now addressable by both.
16
+ - Official draft 2020-12 remote-reference suite: 30 of 31 cases, up from 21. Pure-JS configuration on the full suite: 1189 of 1190, up from 1188. The native configuration stays at 1190 of 1190.
17
+
18
+ ## 1.2.2 - 2026-07-26
19
+
20
+ ### Fixed
21
+
22
+ - The `uri` format was too permissive: it only checked for a scheme prefix, so a string like `https: not a url` passed. It now also rejects any whitespace or control character, so values that carry a scheme but are not URIs are caught. Real URIs, including `mailto:` and `urn:` forms and hyphenated hosts, still pass. `uri-reference` now rejects the same characters and accepts the empty string (a valid same-document reference), which the codegen path had wrongly rejected.
23
+ - `date-time` now accepts lowercase `t` and `z` separators per RFC 3339, matching the interpreted engine. `duration` now rejects a trailing `T` with no time component (`PT`, `P1DT`), which are not valid ISO 8601 durations.
24
+ - These format checks are emitted in two places, the JS compiler and the interpreted engine, and had drifted apart on the cases above. A new differential test (`tests/test_format_engine_parity.js`) runs every built-in format through both engines over a shared corpus and fails on any disagreement, so the two stay in lockstep.
25
+
5
26
  ## 1.2.1 - 2026-07-18
6
27
 
7
28
  ### Fixed
package/index.d.ts CHANGED
@@ -317,6 +317,19 @@ export type ValidateAndParseResult<T = unknown> =
317
317
  export interface ValidatorOptions {
318
318
  coerceTypes?: boolean;
319
319
  removeAdditional?: boolean;
320
+ /**
321
+ * When true (default), missing properties are filled in from their `default`
322
+ * before validation. Set to false to leave the instance untouched, which is
323
+ * what the specification describes: `default` is an annotation and never
324
+ * affects whether an instance is valid.
325
+ */
326
+ useDefaults?: boolean;
327
+ /**
328
+ * When true (default), `format` is asserted and a value that does not match
329
+ * its format fails. Set to false to treat `format` as an annotation, the
330
+ * Draft 2020-12 behavior when the format-assertion vocabulary is not in use.
331
+ */
332
+ assertFormat?: boolean;
320
333
  schemas?: Record<string, object> | object[];
321
334
  /**
322
335
  * Custom format checkers. Keys are format names referenced from `format` in
package/index.js CHANGED
@@ -9,7 +9,7 @@ const {
9
9
  compileToJSCodegenWithErrors,
10
10
  compileToJSCombined,
11
11
  } = require("./lib/js-compiler");
12
- const { normalizeDraft7, normalizeNullable } = require("./lib/draft7");
12
+ const { normalizeDraft7, normalizeNullable, stripFormatAssertions } = require("./lib/draft7");
13
13
  const { classify } = require("./lib/shape-classifier");
14
14
  const { buildTier0Plan, tier0Validate } = require("./lib/tier0");
15
15
 
@@ -250,11 +250,13 @@ function buildPreprocessCodegen(schema, options) {
250
250
  }
251
251
 
252
252
  // defaults: inline per property
253
- for (const [key, prop] of Object.entries(props)) {
254
- if (prop && typeof prop === 'object' && prop.default !== undefined) {
255
- const k = JSON.stringify(key);
256
- const def = JSON.stringify(prop.default);
257
- lines.push(`if(!(${k} in d))d[${k}]=${def}`);
253
+ if (options.useDefaults !== false) {
254
+ for (const [key, prop] of Object.entries(props)) {
255
+ if (prop && typeof prop === 'object' && prop.default !== undefined) {
256
+ const k = JSON.stringify(key);
257
+ const def = JSON.stringify(prop.default);
258
+ lines.push(`if(!(${k} in d))d[${k}]=${def}`);
259
+ }
258
260
  }
259
261
  }
260
262
 
@@ -435,7 +437,11 @@ function buildSchemaMap(schemas) {
435
437
  } else {
436
438
  for (const [key, s] of Object.entries(schemas)) {
437
439
  const normalized = _normalizeCallerSchema(s)
438
- map.set(normalized.$id || key, normalized)
440
+ // A retrieved document is addressable both by the URI it was registered
441
+ // under and by the $id it declares. Registering only the $id makes
442
+ // references to the retrieval URI unresolvable.
443
+ map.set(key, normalized)
444
+ if (normalized.$id && normalized.$id !== key) map.set(normalized.$id, normalized)
439
445
  }
440
446
  }
441
447
  return map
@@ -537,10 +543,18 @@ class Validator {
537
543
  // When schema is a string, JSON.parse already produces a fresh object.
538
544
  // When schema is an object, normalization runs on a clone so the caller's
539
545
  // object is never touched.
540
- const schemaObj = typeof schema === "string"
546
+ let schemaObj = typeof schema === "string"
541
547
  ? JSON.parse(schema)
542
548
  : _normalizeCallerSchema(schema);
543
549
 
550
+ // assertFormat: false makes `format` annotation-only. Strip it on a clone
551
+ // so the caller's schema keeps the keyword.
552
+ if (options.assertFormat === false) {
553
+ schemaObj = stripFormatAssertions(
554
+ schemaObj === schema ? _deepCloneWithSymbols(schemaObj) : schemaObj,
555
+ );
556
+ }
557
+
544
558
  this._schemaStr = null; // lazy: computed on first use
545
559
  this._schemaObj = schemaObj;
546
560
  this._options = options;
@@ -722,7 +736,7 @@ class Validator {
722
736
  const preprocessSchema = resolveSchemaForPreprocess(schemaObj, this._schemaMap);
723
737
  let preprocess = buildPreprocessCodegen(preprocessSchema, options);
724
738
  if (!preprocess) {
725
- const applyDefaults = buildDefaultsApplier(preprocessSchema);
739
+ const applyDefaults = options.useDefaults === false ? null : buildDefaultsApplier(preprocessSchema);
726
740
  const applyCoerce = options.coerceTypes ? buildCoercer(preprocessSchema) : null;
727
741
  const applyRemove = options.removeAdditional
728
742
  ? buildRemover(preprocessSchema)
package/lib/draft7.js CHANGED
@@ -126,4 +126,46 @@ function _normalizeNullable(schema) {
126
126
  }
127
127
  }
128
128
 
129
- module.exports = { isDraft7, normalizeDraft7, normalizeNullable }
129
+ // Draft 2020-12 treats `format` as an annotation unless the assertion
130
+ // vocabulary is in use. Removing the keyword is exactly that reading, and it
131
+ // keeps every engine in agreement without each one growing its own switch.
132
+ // Walks schema-bearing keywords only, so a property or definition named
133
+ // "format" is untouched.
134
+ function stripFormatAssertions(schema) {
135
+ if (typeof schema !== 'object' || schema === null) return schema
136
+ _stripFormat(schema, new Set())
137
+ return schema
138
+ }
139
+
140
+ function _stripFormat(schema, seen) {
141
+ if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) return
142
+ if (seen.has(schema)) return
143
+ seen.add(schema)
144
+
145
+ if (typeof schema.format === 'string') delete schema.format
146
+
147
+ const objSubs = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
148
+ for (const key of objSubs) {
149
+ if (schema[key] && typeof schema[key] === 'object' && !Array.isArray(schema[key])) {
150
+ for (const v of Object.values(schema[key])) _stripFormat(v, seen)
151
+ }
152
+ }
153
+ const arrSubs = ['allOf', 'anyOf', 'oneOf', 'prefixItems']
154
+ for (const key of arrSubs) {
155
+ if (Array.isArray(schema[key])) {
156
+ for (const s of schema[key]) _stripFormat(s, seen)
157
+ }
158
+ }
159
+ const singleSubs = ['items', 'additionalItems', 'contains', 'not', 'if', 'then', 'else',
160
+ 'additionalProperties', 'propertyNames', 'unevaluatedItems',
161
+ 'unevaluatedProperties', 'contentSchema']
162
+ for (const key of singleSubs) {
163
+ if (Array.isArray(schema[key])) {
164
+ for (const s of schema[key]) _stripFormat(s, seen)
165
+ } else {
166
+ _stripFormat(schema[key], seen)
167
+ }
168
+ }
169
+ }
170
+
171
+ module.exports = { isDraft7, normalizeDraft7, normalizeNullable, stripFormatAssertions }
@@ -225,8 +225,8 @@ const FORMAT_CHECKS = {
225
225
  time: (s) => /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d+)?([Zz]|[+-]\d{2}:\d{2})?$/.test(s),
226
226
  duration: (s) => /^P(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/.test(s) && s !== 'P' && !s.endsWith('T'),
227
227
  uuid: (s) => s.length === 36 && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s),
228
- uri: (s) => /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(s),
229
- 'uri-reference': (s) => s === '' || !/\s/.test(s),
228
+ uri: (s) => /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(s) && !/[\s\u0000-\u001f\u007f]/.test(s),
229
+ 'uri-reference': (s) => !/[\s\u0000-\u001f\u007f]/.test(s),
230
230
  ipv4: (s) => { const p = s.split('.'); return p.length === 4 && p.every((n) => { const v = +n; return n !== '' && v >= 0 && v <= 255 && String(v) === n; }); },
231
231
  ipv6: (s) => s !== '' && /^[0-9a-fA-F:.]+$/.test(s) && s.split(':').length >= 3 && s.split(':').length <= 8,
232
232
  hostname: (s) => s.length > 0 && s.length <= 253 && /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(s),
@@ -107,6 +107,8 @@ function compileToJS(schema, defs, schemaMap) {
107
107
  if (!hasDynamic && hasNestedIdScope(schema)) return null
108
108
  }
109
109
 
110
+ if (!defs && needsBaseTracking(schema, schemaMap, new Set())) return null
111
+
110
112
  // Collect $defs early so sub-schemas can resolve $ref
111
113
  const rootDefs = defs || collectDefs(schema)
112
114
 
@@ -611,27 +613,23 @@ function resolveRef(ref, defs, schemaMap) {
611
613
  if (entry) return (d) => { const fn = entry.fn; return fn ? fn(d) : true }
612
614
  }
613
615
  }
616
+ // A referenced schema that cannot itself be compiled must propagate the bail.
617
+ // Substituting an always-true function here would make the reference vacuous
618
+ // and silently drop every constraint behind it.
614
619
  // 2. Cross-schema ref (exact match)
615
620
  if (schemaMap && schemaMap.has(ref)) {
616
- const resolved = schemaMap.get(ref)
617
- const fn = compileToJS(resolved, null, schemaMap)
618
- return fn || (() => true)
621
+ return compileToJS(schemaMap.get(ref), null, schemaMap)
619
622
  }
620
623
  // 3. Cross-schema ref with JSON pointer fragment ("<id>#/<path>")
621
624
  if (schemaMap && ref.includes('#')) {
622
625
  const r = resolveCrossSchemaRef(ref, schemaMap)
623
- if (r) {
624
- const fn = compileToJS(r.schema, null, schemaMap)
625
- return fn || (() => true)
626
- }
626
+ if (r) return compileToJS(r.schema, null, schemaMap)
627
627
  }
628
628
  // 4. Cross-schema ref (relative URI resolution, no fragment)
629
629
  if (schemaMap && !ref.includes('://') && !ref.startsWith('#')) {
630
630
  for (const [id] of schemaMap) {
631
631
  if (id.endsWith('/' + ref)) {
632
- const resolved = schemaMap.get(id)
633
- const fn = compileToJS(resolved, null, schemaMap)
634
- return fn || (() => true)
632
+ return compileToJS(schemaMap.get(id), null, schemaMap)
635
633
  }
636
634
  }
637
635
  }
@@ -933,6 +931,169 @@ function hasAdditionalPropertiesSchema(schema) {
933
931
  return false
934
932
  }
935
933
 
934
+ // Schema-bearing keywords, so the walk below never mistakes a property named
935
+ // "$ref" for an actual reference.
936
+ const SUBSCHEMA_MAPS = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
937
+ const SUBSCHEMA_LISTS = ['allOf', 'anyOf', 'oneOf', 'prefixItems']
938
+ const SUBSCHEMA_SINGLES = ['items', 'additionalItems', 'contains', 'not', 'if', 'then', 'else',
939
+ 'additionalProperties', 'propertyNames', 'unevaluatedItems', 'unevaluatedProperties', 'contentSchema']
940
+
941
+ // genCode emits nothing for a $ref it cannot resolve, which makes the reference
942
+ // silently vacuous — a typo in a $ref would turn validation off rather than
943
+ // fail. Codegen bails instead, so the schema routes to the interpreted engine,
944
+ // which reports the unresolved reference as an error. Mirrors genCode's own
945
+ // resolution rules; anything genCode cannot resolve is treated as unresolvable.
946
+ function refResolves(ref, rootDefs, anchors, schemaMap) {
947
+ if (ref === '#') return true
948
+ const local = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
949
+ if (local) return !!(rootDefs && rootDefs[local[1]])
950
+ if (ref.startsWith('#') && !ref.startsWith('#/')) {
951
+ const entry = rootDefs && rootDefs[ref]
952
+ return !!((entry && entry.raw) || (anchors && anchors[ref]))
953
+ }
954
+ if (ref.startsWith('#')) return false
955
+ if (!schemaMap) return false
956
+ if (schemaMap.has(ref)) return true
957
+ if (!ref.includes('://')) {
958
+ for (const [id] of schemaMap) if (id.endsWith('/' + ref)) return true
959
+ }
960
+ if (ref.includes('#')) return !!resolveCrossSchemaRef(ref, schemaMap)
961
+ return false
962
+ }
963
+
964
+ // Resolve a $ref that crosses into another document, mirroring genCode's rules.
965
+ function crossDocTarget(ref, schemaMap) {
966
+ if (!schemaMap || ref.startsWith('#')) return null
967
+ if (schemaMap.has(ref)) return schemaMap.get(ref)
968
+ if (!ref.includes('://')) {
969
+ for (const [id, s] of schemaMap) if (id.endsWith('/' + ref)) return s
970
+ }
971
+ if (ref.includes('#')) {
972
+ const r = resolveCrossSchemaRef(ref, schemaMap)
973
+ if (r) return r.schema
974
+ }
975
+ return null
976
+ }
977
+
978
+ // True when a subtree contains a fragment-only reference ("#", "#/$defs/x",
979
+ // "#anchor"). Such a reference is relative to the document that holds it.
980
+ function subtreeHasLocalRef(node, seen) {
981
+ if (typeof node !== 'object' || node === null || Array.isArray(node)) return false
982
+ if (seen.has(node)) return false
983
+ seen.add(node)
984
+ if (typeof node.$ref === 'string' && node.$ref.startsWith('#')) return true
985
+ for (const key of SUBSCHEMA_MAPS) {
986
+ const group = node[key]
987
+ if (group && typeof group === 'object' && !Array.isArray(group)) {
988
+ for (const sub of Object.values(group)) if (subtreeHasLocalRef(sub, seen)) return true
989
+ }
990
+ }
991
+ for (const key of SUBSCHEMA_LISTS) {
992
+ if (Array.isArray(node[key])) {
993
+ for (const sub of node[key]) if (subtreeHasLocalRef(sub, seen)) return true
994
+ }
995
+ }
996
+ for (const key of SUBSCHEMA_SINGLES) {
997
+ if (Array.isArray(node[key])) {
998
+ for (const sub of node[key]) if (subtreeHasLocalRef(sub, seen)) return true
999
+ } else if (subtreeHasLocalRef(node[key], seen)) return true
1000
+ }
1001
+ return false
1002
+ }
1003
+
1004
+ // Neither JS path tracks the base URI a reference is resolved against: they
1005
+ // match registry keys by exact string or by path suffix. That is fine for a
1006
+ // flat registry of absolute ids, but wrong as soon as resolution depends on an
1007
+ // enclosing scope. These shapes go to the interpreted engine, which resolves
1008
+ // against a real base.
1009
+ function needsBaseTracking(schema, schemaMap, seen) {
1010
+ if (!schemaMap || schemaMap.size === 0) return false
1011
+ // A nested $id opens a new scope that later relative references resolve
1012
+ // against.
1013
+ if (hasNestedIdScope(schema)) return true
1014
+ return refsAreScopeSensitive(schema, schemaMap, seen)
1015
+ }
1016
+
1017
+ function refsAreScopeSensitive(node, schemaMap, seen) {
1018
+ if (typeof node !== 'object' || node === null || Array.isArray(node)) return false
1019
+ if (seen.has(node)) return false
1020
+ seen.add(node)
1021
+
1022
+ const ref = node.$ref
1023
+ if (typeof ref === 'string' && !ref.startsWith('#')) {
1024
+ const key = ref.split('#')[0]
1025
+ // A key the registry holds verbatim needs no base to resolve — this is the
1026
+ // flat-registry case ($ref: 'shared#'). Anything else that is not absolute
1027
+ // only resolves by walking up from an enclosing base.
1028
+ if (!schemaMap.has(key) && !ref.includes('://')) return true
1029
+ const target = crossDocTarget(ref, schemaMap)
1030
+ if (target && typeof target === 'object') {
1031
+ // Registered under a key that differs from the $id it declares, so its
1032
+ // own references resolve against a different base than its retrieval URI.
1033
+ if (typeof target.$id === 'string' && target.$id !== key) return true
1034
+ if (subtreeHasLocalRef(target, new Set()) && refsAreScopeSensitive(target, schemaMap, seen)) return true
1035
+ }
1036
+ }
1037
+
1038
+ for (const key of SUBSCHEMA_MAPS) {
1039
+ const group = node[key]
1040
+ if (group && typeof group === 'object' && !Array.isArray(group)) {
1041
+ for (const sub of Object.values(group)) if (refsAreScopeSensitive(sub, schemaMap, seen)) return true
1042
+ }
1043
+ }
1044
+ for (const key of SUBSCHEMA_LISTS) {
1045
+ if (Array.isArray(node[key])) {
1046
+ for (const sub of node[key]) if (refsAreScopeSensitive(sub, schemaMap, seen)) return true
1047
+ }
1048
+ }
1049
+ for (const key of SUBSCHEMA_SINGLES) {
1050
+ if (Array.isArray(node[key])) {
1051
+ for (const sub of node[key]) if (refsAreScopeSensitive(sub, schemaMap, seen)) return true
1052
+ } else if (refsAreScopeSensitive(node[key], schemaMap, seen)) return true
1053
+ }
1054
+ return false
1055
+ }
1056
+
1057
+ function hasUnresolvableRef(node, rootDefs, anchors, schemaMap, seen) {
1058
+ if (typeof node !== 'object' || node === null || Array.isArray(node)) return false
1059
+ if (seen.has(node)) return false
1060
+ seen.add(node)
1061
+
1062
+ if (typeof node.$ref === 'string') {
1063
+ if (!refResolves(node.$ref, rootDefs, anchors, schemaMap)) return true
1064
+ // Following a reference into another document changes the base URI, which
1065
+ // codegen does not track: it would resolve the target's own fragment-only
1066
+ // references against the entry schema instead. Bail so the interpreted
1067
+ // engine, which does track the base, handles it.
1068
+ const target = crossDocTarget(node.$ref, schemaMap)
1069
+ if (target && subtreeHasLocalRef(target, new Set())) return true
1070
+ }
1071
+
1072
+ for (const key of SUBSCHEMA_MAPS) {
1073
+ const group = node[key]
1074
+ if (group && typeof group === 'object' && !Array.isArray(group)) {
1075
+ for (const sub of Object.values(group)) {
1076
+ if (hasUnresolvableRef(sub, rootDefs, anchors, schemaMap, seen)) return true
1077
+ }
1078
+ }
1079
+ }
1080
+ for (const key of SUBSCHEMA_LISTS) {
1081
+ if (Array.isArray(node[key])) {
1082
+ for (const sub of node[key]) {
1083
+ if (hasUnresolvableRef(sub, rootDefs, anchors, schemaMap, seen)) return true
1084
+ }
1085
+ }
1086
+ }
1087
+ for (const key of SUBSCHEMA_SINGLES) {
1088
+ if (Array.isArray(node[key])) {
1089
+ for (const sub of node[key]) {
1090
+ if (hasUnresolvableRef(sub, rootDefs, anchors, schemaMap, seen)) return true
1091
+ }
1092
+ } else if (hasUnresolvableRef(node[key], rootDefs, anchors, schemaMap, seen)) return true
1093
+ }
1094
+ return false
1095
+ }
1096
+
936
1097
  // --- Codegen mode: generates a single Function (NOT CSP-safe) ---
937
1098
  // This matches ajv's approach: one monolithic function, V8 JIT fully inlines it
938
1099
  function compileToJSCodegen(schema, schemaMap, userFormats) {
@@ -998,6 +1159,9 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
998
1159
  }
999
1160
  }
1000
1161
 
1162
+ if (hasUnresolvableRef(schema, rootDefs, anchors, schemaMap, new Set())) return null
1163
+ if (needsBaseTracking(schema, schemaMap, new Set())) return null
1164
+
1001
1165
  const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema, userFormats: userFormats || null }
1002
1166
  const lines = []
1003
1167
  genCode(schema, 'd', lines, ctx)
@@ -2438,20 +2602,20 @@ const FORMAT_CODEGEN = {
2438
2602
  ? `if(${v}.length!==36||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${v}))return false`
2439
2603
  : `if(typeof ${v}==='string'&&(${v}.length!==36||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${v})))return false`,
2440
2604
  'date-time': (v, isStr) => isStr
2441
- ? `if(!/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$/.test(${v})||isNaN(Date.parse(${v})))return false`
2442
- : `if(typeof ${v}==='string'&&(!/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$/.test(${v})||isNaN(Date.parse(${v}))))return false`,
2605
+ ? `if(!/^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?([Zz]|[+-]\\d{2}:\\d{2})$/.test(${v})||isNaN(Date.parse(${v})))return false`
2606
+ : `if(typeof ${v}==='string'&&(!/^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?([Zz]|[+-]\\d{2}:\\d{2})$/.test(${v})||isNaN(Date.parse(${v}))))return false`,
2443
2607
  time: (v, isStr) => isStr
2444
2608
  ? `if(!/^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})?$/.test(${v}))return false`
2445
2609
  : `if(typeof ${v}==='string'&&!/^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})?$/.test(${v}))return false`,
2446
2610
  duration: (v, isStr) => isStr
2447
- ? `if(!/^P(?:\\d+Y)?(?:\\d+M)?(?:\\d+W)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?$/.test(${v})||${v}==='P')return false`
2448
- : `if(typeof ${v}==='string'&&(!/^P(?:\\d+Y)?(?:\\d+M)?(?:\\d+W)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?$/.test(${v})||${v}==='P'))return false`,
2611
+ ? `if(!/^P(?:\\d+Y)?(?:\\d+M)?(?:\\d+W)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?$/.test(${v})||${v}==='P'||${v}.endsWith('T'))return false`
2612
+ : `if(typeof ${v}==='string'&&(!/^P(?:\\d+Y)?(?:\\d+M)?(?:\\d+W)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?$/.test(${v})||${v}==='P'||${v}.endsWith('T')))return false`,
2449
2613
  uri: (v, isStr) => isStr
2450
- ? `if(!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(${v}))return false`
2451
- : `if(typeof ${v}==='string'&&!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(${v}))return false`,
2614
+ ? `if(!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(${v})||/[\\s\\u0000-\\u001f\\u007f]/.test(${v}))return false`
2615
+ : `if(typeof ${v}==='string'&&(!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(${v})||/[\\s\\u0000-\\u001f\\u007f]/.test(${v})))return false`,
2452
2616
  'uri-reference': (v, isStr) => isStr
2453
- ? `if(${v}===''||/\\s/.test(${v}))return false`
2454
- : `if(typeof ${v}==='string'&&(${v}===''||/\\s/.test(${v})))return false`,
2617
+ ? `if(/[\\s\\u0000-\\u001f\\u007f]/.test(${v}))return false`
2618
+ : `if(typeof ${v}==='string'&&/[\\s\\u0000-\\u001f\\u007f]/.test(${v}))return false`,
2455
2619
  ipv4: (v, isStr) => isStr
2456
2620
  ? `{const _p=${v}.split('.');if(_p.length!==4||!_p.every(function(n){var x=+n;return x>=0&&x<=255&&String(x)===n}))return false}`
2457
2621
  : `if(typeof ${v}==='string'){const _p=${v}.split('.');if(_p.length!==4||!_p.every(function(n){var x=+n;return x>=0&&x<=255&&String(x)===n}))return false}`,
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.2.1';
10
+ module.exports = '1.3.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -44,7 +44,7 @@
44
44
  "release:check": "node scripts/regen-safe-regex-source.js && node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_source_sync.js && node tests/test_version_sync.js",
45
45
  "build": "cmake-js build --target ata",
46
46
  "rebuild": "cmake-js rebuild --target ata",
47
- "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_defs_pointer_alias.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_order.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
47
+ "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_format_engine_parity.js && node tests/test_defs_pointer_alias.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_order.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
48
48
  "bench:size": "node benchmark/bench_aot_size.mjs",
49
49
  "test:suite": "node tests/run_suite.js",
50
50
  "test:compat": "node tests/test_compat.js",
@@ -112,12 +112,12 @@
112
112
  "LICENSE"
113
113
  ],
114
114
  "optionalDependencies": {
115
- "@ata-validator/native-darwin-arm64": "1.2.1",
116
- "@ata-validator/native-linux-x64-gnu": "1.2.1",
117
- "@ata-validator/native-linux-arm64-gnu": "1.2.1",
118
- "@ata-validator/native-linux-x64-musl": "1.2.1",
119
- "@ata-validator/native-linux-arm64-musl": "1.2.1",
120
- "@ata-validator/native-win32-x64": "1.2.1"
115
+ "@ata-validator/native-darwin-arm64": "1.3.0",
116
+ "@ata-validator/native-linux-x64-gnu": "1.3.0",
117
+ "@ata-validator/native-linux-arm64-gnu": "1.3.0",
118
+ "@ata-validator/native-linux-x64-musl": "1.3.0",
119
+ "@ata-validator/native-linux-arm64-musl": "1.3.0",
120
+ "@ata-validator/native-win32-x64": "1.3.0"
121
121
  },
122
122
  "peerDependencies": {
123
123
  "yaml": "^2.0.0"