ata-validator 1.31.3 → 1.32.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.
package/index.d.ts CHANGED
@@ -497,6 +497,16 @@ export interface Validator<T = unknown> {
497
497
  /** Parse JSON and validate against the schema. Returns the parsed value and the validation result. Works without the native addon. */
498
498
  validateAndParse(jsonString: string | Buffer): ValidateAndParseResult<T>;
499
499
 
500
+ /**
501
+ * Validate, then return a copy holding only the properties the schema declares, with defaults
502
+ * filled. The input is not modified. Throws an error named `AtaValidationError`, with `errors`,
503
+ * when the value is invalid, and a `TypeError` where the kept key set cannot be proven from the
504
+ * schema (a $ref it cannot inline, patternProperties, an object that allows unknown keys) or the
505
+ * validator rewrites input (`coerceTypes`, custom keywords). Same result as the `parse` export of
506
+ * a module built with `ata build`.
507
+ */
508
+ parse(data: unknown): T;
509
+
500
510
  /** Ultra-fast buffer validation via native addon */
501
511
  isValid(input: Buffer | Uint8Array | string): boolean;
502
512
 
package/index.js CHANGED
@@ -2895,6 +2895,59 @@ Validator.prototype._extendValidate = function (resolve) {
2895
2895
  // closure, where wrapping the five entry points cost a closure per entry point,
2896
2896
  // the wrappers themselves and an accessor, most of what building a wrapped
2897
2897
  // validator took.
2898
+ // parse(data): validate, then return a copy holding only what the schema
2899
+ // declares, the way the parse() export of an ahead-of-time module does, with
2900
+ // the same emitter behind both. Building the copy from the schema's own key
2901
+ // list costs less than finding and deleting unknown keys, and leaves the
2902
+ // caller's object alone. Where the key set cannot be proven (a $ref it cannot
2903
+ // inline, patternProperties, an open object) the method declines with an
2904
+ // error instead of guessing, as the module ships no parse() there; so it does
2905
+ // under options that rewrite input, whose answers a copy would not reproduce.
2906
+ Validator.prototype.parse = function (data) {
2907
+ let fn = this._parseFn;
2908
+ if (fn === undefined) {
2909
+ fn = _buildParse(this);
2910
+ Object.defineProperty(this, '_parseFn', { value: fn, writable: true, configurable: true, enumerable: false });
2911
+ }
2912
+ return fn(data);
2913
+ };
2914
+
2915
+ function _buildParse(self) {
2916
+ const decline = (why) => () => {
2917
+ throw new TypeError(`parse() is not available for this validator: ${why}. Use validate() with removeAdditional instead.`);
2918
+ };
2919
+ const o = self._options;
2920
+ if (o.coerceTypes || o.removeAdditional === 'all' || self._usesKeywords || self._verdictTail !== null || self._validateTail !== null) {
2921
+ return decline('its options or keywords rewrite or extend what the schema checks');
2922
+ }
2923
+ const { cloneExprFor } = require('./lib/clone-emit');
2924
+ const expr = cloneExprFor(self._schemaObj);
2925
+ if (!expr) return decline('the set of keys to keep cannot be proven from the schema');
2926
+ let copy;
2927
+ try {
2928
+ // eslint-disable-next-line no-new-func
2929
+ copy = new Function('data', 'return ' + expr);
2930
+ } catch {
2931
+ return decline('code generation is not allowed here');
2932
+ }
2933
+ self._ensureCompiled();
2934
+ const verdict = self._jsFn;
2935
+ if (typeof verdict !== 'function') return decline('the schema has no generated verdict function');
2936
+ return (data) => {
2937
+ if (!verdict(data)) {
2938
+ const e = new Error('validation failed');
2939
+ e.name = 'AtaValidationError';
2940
+ let target = data;
2941
+ if (self._mutatesInput) {
2942
+ try { target = structuredClone(data); } catch { target = data; }
2943
+ }
2944
+ e.errors = self.validate(target).errors;
2945
+ throw e;
2946
+ }
2947
+ return copy(data);
2948
+ };
2949
+ }
2950
+
2898
2951
  Validator.prototype._extendChecks = function (resolve) {
2899
2952
  if (typeof resolve !== 'function') throw new TypeError('_extendChecks expects a function');
2900
2953
  this._extendValidate(resolve);
package/lib/aot-impl.js CHANGED
@@ -189,277 +189,7 @@ function emitFormatDecls(closures, mode, declKW) {
189
189
  return { decls: out, exportsSetFormats: false };
190
190
  }
191
191
 
192
- // Emit an expression that rebuilds `access` from the properties the schema
193
- // declares, dropping everything else. This is the sanitising counterpart to
194
- // removeAdditional: instead of enumerating the input and deleting the keys
195
- // that do not belong, it reads only the keys that do. That is O(schema)
196
- // rather than O(input), it leaves the caller's object untouched, and it keeps
197
- // one hidden class instead of walking the object through a transition per
198
- // delete.
199
- //
200
- // Returns null when the clone cannot be proven exact, and the module then
201
- // simply has no parse(). Declining is recoverable; a sanitiser that quietly
202
- // drops a property the schema allows is not.
203
- // Resolve a local JSON pointer ('#', '#/$defs/x', draft-7 '#/definitions/x',
204
- // any '#/...' path) against the schema root. Returns null for anything else:
205
- // external documents, anchors, URNs. Those stay with the runtime engines.
206
- function resolveLocalPointer(root, ref) {
207
- if (ref === '#') return root;
208
- if (typeof ref !== 'string' || !ref.startsWith('#/')) return null;
209
- let cur = root;
210
- for (const raw of ref.slice(2).split('/')) {
211
- let seg = raw.replace(/~1/g, '/').replace(/~0/g, '~');
212
- if (cur === null || typeof cur !== 'object') return null;
213
- if (!Object.prototype.hasOwnProperty.call(cur, seg)) {
214
- try { seg = decodeURIComponent(seg); } catch (_) { return null; }
215
- if (!Object.prototype.hasOwnProperty.call(cur, seg)) return null;
216
- }
217
- cur = cur[seg];
218
- }
219
- return cur;
220
- }
221
-
222
- // Inline local, acyclic $refs before the clone proof runs, so a generated
223
- // schema ($defs + $ref, the shape every schema generator emits) can still get
224
- // a parse(). Only a node that is exactly a $ref plus annotations is replaced;
225
- // a $ref with a constraining sibling, a cycle, an unresolvable target or an
226
- // external reference is left in place, and emitClone declines it as before.
227
- // The walk never mutates its input and never descends into data positions
228
- // (default, const, enum, examples), where an object holding a "$ref" key is
229
- // a value, not a schema.
230
- const _INLINE_ANNOTATIONS = new Set(['$ref', 'title', 'description', '$comment', 'examples', 'deprecated', 'readOnly', 'writeOnly', 'default']);
231
- const _INLINE_DATA_KEYS = new Set(['default', 'const', 'enum', 'examples']);
232
- function inlineRefsForClone(root) {
233
- const state = { budget: 512 };
234
- // `inRef` marks content brought in from behind a reference. The runtime's
235
- // useDefaults fills a default written next to the $ref, and does not fill
236
- // defaults written inside the referenced definition, so the inlined copy
237
- // keeps the first and drops the second. parse() must stay exactly as
238
- // generous as validate(); a parse that fills more is a disagreement, not a
239
- // feature.
240
- const walk = (node, active, inRef) => {
241
- if (!node || typeof node !== 'object') return node;
242
- if (Array.isArray(node)) return node.map((n) => walk(n, active, inRef));
243
- let cur = node;
244
- let entered = false;
245
- let siblingDefault;
246
- while (cur && typeof cur === 'object' && !Array.isArray(cur) && typeof cur.$ref === 'string') {
247
- for (const k of Object.keys(cur)) if (!_INLINE_ANNOTATIONS.has(k)) return node;
248
- if (active.has(cur.$ref) || state.budget-- <= 0) return node;
249
- const target = resolveLocalPointer(root, cur.$ref);
250
- if (!target || typeof target !== 'object' || Array.isArray(target)) return node;
251
- if (!entered && !inRef && cur.default !== undefined) siblingDefault = cur.default;
252
- active = new Set(active);
253
- active.add(cur.$ref);
254
- cur = target;
255
- entered = true;
256
- }
257
- const strip = inRef || entered;
258
- const out = {};
259
- for (const k of Object.keys(cur)) {
260
- if (strip && k === 'default') continue;
261
- const v = cur[k];
262
- if (_INLINE_DATA_KEYS.has(k) || k === '$defs' || k === 'definitions') { out[k] = v; continue; }
263
- out[k] = walk(v, active, strip);
264
- }
265
- if (siblingDefault !== undefined) out.default = siblingDefault;
266
- return out;
267
- };
268
- return walk(root, new Set(), false);
269
- }
270
-
271
- function emitClone(node, access, depth) {
272
- if (!node || typeof node !== 'object') return null;
273
- if (depth > 12) return null;
274
- // Only plain object nodes are cloneable, and only when the schema names
275
- // every property it keeps. A $ref means the set of allowed keys is not
276
- // this node's to decide. In-place applicators (allOf, anyOf, oneOf,
277
- // if/then/else) are admitted under the same proof the unevaluated* error
278
- // generators use: every property name they contribute must already be
279
- // declared in this node's own `properties`, so they constrain values but
280
- // never widen the key set. `not` never widens it. `unevaluatedProperties`
281
- // is admitted only as `false` under that proof, where it is exactly
282
- // `additionalProperties: false`.
283
- if (node.$ref || node.patternProperties || node.additionalProperties === true) {
284
- return null;
285
- }
286
- // additionalProperties as a schema is the record shape (z.record and every
287
- // generator's map type): the key set is open by declaration, so keeping
288
- // every key is exact, and every undeclared value is rebuilt against that
289
- // one schema. That is a proof, not a guess, so it is admitted. What stays
290
- // out: applicators next to a record (a branch could constrain some keys'
291
- // values beyond the record schema, and the rebuild would ignore it), and
292
- // `additionalProperties: true`, where the value is unconstrained and this
293
- // pass only copies what it can name. An open object with no
294
- // `additionalProperties` at all also stays out, for the original reason:
295
- // validate() accepts its unknown keys, so a parse() that strips them would
296
- // silently drop allowed data, and one that keeps them raw would not be a
297
- // sanitiser.
298
- const apSchema = (node.additionalProperties && typeof node.additionalProperties === 'object')
299
- ? node.additionalProperties
300
- : null;
301
- if (apSchema) {
302
- if (node.unevaluatedProperties !== undefined) return null;
303
- if (node.allOf || node.anyOf || node.oneOf || node.if || node.then || node.else ||
304
- node.dependentSchemas !== undefined || node.dependencies !== undefined ||
305
- node.$dynamicRef !== undefined || node.$recursiveRef !== undefined) return null;
306
- }
307
- if (node.unevaluatedProperties !== undefined && node.unevaluatedProperties !== false) return null;
308
- if (node.type !== 'object') return null;
309
- if (!node.properties && !apSchema) return null;
310
- const keys = node.properties ? Object.keys(node.properties) : [];
311
- if (keys.length === 0 && !apSchema) return null;
312
- if (node.allOf || node.anyOf || node.oneOf || node.if || node.then || node.else ||
313
- node.unevaluatedProperties === false) {
314
- if (node.$dynamicRef !== undefined || node.$recursiveRef !== undefined ||
315
- node.dependentSchemas !== undefined || node.dependencies !== undefined) return null;
316
- const names = new Set();
317
- const state = { prefix: 0 };
318
- for (const k of ['allOf', 'anyOf', 'oneOf']) {
319
- if (node[k] !== undefined) {
320
- if (!Array.isArray(node[k])) return null;
321
- for (const b of node[k]) if (!unevalContributions(b, 'props', names, state, 0)) return null;
322
- }
323
- }
324
- for (const k of ['if', 'then', 'else']) {
325
- if (node[k] !== undefined && !unevalContributions(node[k], 'props', names, state, 0)) return null;
326
- }
327
- for (const n of names) if (!Object.prototype.hasOwnProperty.call(node.properties, n)) return null;
328
- }
329
- const required = new Set(Array.isArray(node.required) ? node.required : []);
330
-
331
- const fixed = [];
332
- const conditional = [];
333
- for (const key of keys) {
334
- const prop = node.properties[key];
335
- if (!prop || typeof prop !== 'object') return null;
336
- if (prop.$ref) return null;
337
- // A property that is required and carries a default is the one place the
338
- // runtime and a raw-input validation disagree: the runtime fills the
339
- // default before checking `required`, so `{}` validates there and would
340
- // throw here. Declining keeps parse() exactly as strict as validate().
341
- if (required.has(key) && prop.default !== undefined) return null;
342
- // A default the property's own schema rejects would make parse() hand
343
- // back a document validate() refuses: the runtime fills defaults before
344
- // validating, so it catches the bad default, and a raw-input check here
345
- // never sees it. Probe the default at emit time and decline when it does
346
- // not hold, or when the subschema cannot be compiled to check it.
347
- if (prop.default !== undefined) {
348
- let probe = null;
349
- try { probe = compileToJSCodegen(prop, null, null); } catch (_) { probe = null; }
350
- if (!probe || !probe(prop.default)) return null;
351
- }
352
- const read = `${access}[${JSON.stringify(key)}]`;
353
- const value = emitValueExpr(prop, read, depth);
354
- if (value === null) return null;
355
- // Computed key syntax: a plain `"__proto__":` in an object literal is
356
- // the prototype slot, not a property; `["__proto__"]:` is a property.
357
- if (required.has(key)) fixed.push(`[${JSON.stringify(key)}]: ${value}`);
358
- else conditional.push({ key, value, dflt: prop.default });
359
- }
360
- if (fixed.length === 0 && conditional.length === 0 && !apSchema) return null;
361
-
362
- const tmp = `_c${depth}`;
363
- // The record loop: every key of the input that is not a declared property
364
- // is kept, its value rebuilt against the additionalProperties schema. The
365
- // input is already validated, so every such value satisfies that schema;
366
- // the rebuild only strips inside it. Defaults under a record value are
367
- // dropped before emission because the runtime's useDefaults does not fill
368
- // them there, and parse() must equal validate().data, not improve on it.
369
- let recordLoop = '';
370
- if (apSchema) {
371
- const kVar = `_k${depth}`;
372
- const vVar = `_v${depth}`;
373
- const valueExpr = emitValueExpr(stripDefaultsDeep(apSchema), vVar, depth);
374
- if (valueExpr === null) return null;
375
- let skip = '';
376
- let declSet = '';
377
- if (keys.length > 8) {
378
- declSet = `const _d${depth} = new Set(${JSON.stringify(keys)}); `;
379
- skip = `if (_d${depth}.has(${kVar})) continue; `;
380
- } else if (keys.length > 0) {
381
- skip = `if (${keys.map((k) => `${kVar} === ${JSON.stringify(k)}`).join(' || ')}) continue; `;
382
- }
383
- // The key comes from the input, so it can be the string "__proto__" (an
384
- // own key only JSON.parse writes); assignment would hit the prototype
385
- // setter, so that one key goes through defineProperty.
386
- recordLoop = ` ${declSet}for (const ${kVar} of Object.keys(${access})) { ${skip}const ${vVar} = ${access}[${kVar}]; if (${kVar} === "__proto__") Object.defineProperty(${tmp}, ${kVar}, { value: ${valueExpr}, writable: true, enumerable: true, configurable: true }); else ${tmp}[${kVar}] = ${valueExpr}; }`;
387
- }
388
-
389
- const literal = `{ ${fixed.join(', ')} }`;
390
- if (conditional.length === 0 && !recordLoop) return literal;
391
- // Optional properties are added only when present, so the result never
392
- // gains a key the input did not have.
393
- const adds = conditional
394
- .map(({ key, value, dflt }) => {
395
- // Object.hasOwn, not `in`: `in` also answers for what the instance
396
- // inherits, so a property named `constructor` would copy the inherited
397
- // function into the output that validate().data does not carry. And a
398
- // key named `__proto__` cannot be written by assignment, which hits
399
- // the prototype setter; defineProperty writes an own key.
400
- const assign = key === '__proto__'
401
- ? `Object.defineProperty(${tmp}, ${JSON.stringify(key)}, { value: ${value}, writable: true, enumerable: true, configurable: true });`
402
- : `${tmp}[${JSON.stringify(key)}] = ${value};`;
403
- const set = `if (Object.hasOwn(${access}, ${JSON.stringify(key)})) ${assign}`;
404
- // The default is emitted as a literal inside the call, so an object or
405
- // array default is a fresh value on every parse, never shared state.
406
- if (dflt === undefined) return set;
407
- const setDflt = key === '__proto__'
408
- ? `Object.defineProperty(${tmp}, ${JSON.stringify(key)}, { value: ${JSON.stringify(dflt)}, writable: true, enumerable: true, configurable: true });`
409
- : `${tmp}[${JSON.stringify(key)}] = ${JSON.stringify(dflt)};`;
410
- return `${set} else ${setDflt}`;
411
- })
412
- .join(' ');
413
- return `(function(){ const ${tmp} = ${literal}; ${adds}${recordLoop} return ${tmp}; })()`;
414
- }
415
-
416
- // The value dispatch shared by declared properties and the record loop. A
417
- // value is copyable only when the emitter can name everything that may live
418
- // under it: a primitive holds nothing, an object with declared properties or
419
- // a record schema is rebuilt the same way, an array of primitives has
420
- // nowhere to hide a key. A $ref, a composition or an untyped node could all
421
- // carry keys this code cannot see, and copying the reference would smuggle
422
- // them into a value that claims to be sanitised, so the caller declines the
423
- // whole clone instead.
424
- // Drop every schema-position `default` in a subtree, leaving data positions
425
- // (const, enum, examples) untouched. Used on a record's value schema, where
426
- // the runtime's useDefaults fills nothing.
427
- function stripDefaultsDeep(node) {
428
- if (!node || typeof node !== 'object') return node;
429
- if (Array.isArray(node)) return node.map(stripDefaultsDeep);
430
- const out = {};
431
- for (const k of Object.keys(node)) {
432
- if (k === 'default') continue;
433
- out[k] = _INLINE_DATA_KEYS.has(k) ? node[k] : stripDefaultsDeep(node[k]);
434
- }
435
- return out;
436
- }
437
-
438
- const _CLONE_PRIMITIVE = new Set(['string', 'number', 'integer', 'boolean', 'null']);
439
- function emitValueExpr(prop, read, depth) {
440
- if (!prop || typeof prop !== 'object') return null;
441
- if (prop.$ref) return null;
442
- const types = Array.isArray(prop.type) ? prop.type : [prop.type];
443
- if (types.every((t) => _CLONE_PRIMITIVE.has(t))) return read;
444
- const isRecordable = (s) => s.properties ||
445
- (s.additionalProperties && typeof s.additionalProperties === 'object');
446
- if (prop.type === 'object' && isRecordable(prop)) {
447
- return emitClone(prop, read, depth + 1);
448
- }
449
- if (prop.type === 'array' && prop.items && typeof prop.items === 'object' &&
450
- !Array.isArray(prop.items)) {
451
- const it = prop.items;
452
- const itemTypes = Array.isArray(it.type) ? it.type : [it.type];
453
- if (itemTypes.every((t) => _CLONE_PRIMITIVE.has(t))) return read;
454
- if (it.type === 'object' && isRecordable(it)) {
455
- const el = '_e' + depth;
456
- const inner = emitClone(it, el, depth + 1);
457
- if (!inner) return null;
458
- return `${read}.map((${el}) => (${inner}))`;
459
- }
460
- }
461
- return null;
462
- }
192
+ const { emitClone, inlineRefsForClone } = require('./clone-emit');
463
193
 
464
194
  function toStandaloneModule(validator, opts) {
465
195
  assertEmittable(validator, 'toStandaloneModule');
@@ -0,0 +1,302 @@
1
+ 'use strict';
2
+
3
+ // The copy behind parse(): shared by the parse() export of an ahead-of-time
4
+ // module (lib/aot-impl.js) and the runtime's Validator#parse (index.js). It
5
+ // lives apart from the module emitter so the runtime can use it without
6
+ // pulling the emitter, and the safe-regex source it embeds, into a browser
7
+ // bundle.
8
+
9
+ const { compileToJSCodegen, unevalContributions } = require('./js-compiler');
10
+
11
+ // Emit an expression that rebuilds `access` from the properties the schema
12
+ // declares, dropping everything else. This is the sanitising counterpart to
13
+ // removeAdditional: instead of enumerating the input and deleting the keys
14
+ // that do not belong, it reads only the keys that do. That is O(schema)
15
+ // rather than O(input), it leaves the caller's object untouched, and it keeps
16
+ // one hidden class instead of walking the object through a transition per
17
+ // delete.
18
+ //
19
+ // Returns null when the clone cannot be proven exact, and the module then
20
+ // simply has no parse(). Declining is recoverable; a sanitiser that quietly
21
+ // drops a property the schema allows is not.
22
+ // Resolve a local JSON pointer ('#', '#/$defs/x', draft-7 '#/definitions/x',
23
+ // any '#/...' path) against the schema root. Returns null for anything else:
24
+ // external documents, anchors, URNs. Those stay with the runtime engines.
25
+ function resolveLocalPointer(root, ref) {
26
+ if (ref === '#') return root;
27
+ if (typeof ref !== 'string' || !ref.startsWith('#/')) return null;
28
+ let cur = root;
29
+ for (const raw of ref.slice(2).split('/')) {
30
+ let seg = raw.replace(/~1/g, '/').replace(/~0/g, '~');
31
+ if (cur === null || typeof cur !== 'object') return null;
32
+ if (!Object.prototype.hasOwnProperty.call(cur, seg)) {
33
+ try { seg = decodeURIComponent(seg); } catch (_) { return null; }
34
+ if (!Object.prototype.hasOwnProperty.call(cur, seg)) return null;
35
+ }
36
+ cur = cur[seg];
37
+ }
38
+ return cur;
39
+ }
40
+
41
+ // Inline local, acyclic $refs before the clone proof runs, so a generated
42
+ // schema ($defs + $ref, the shape every schema generator emits) can still get
43
+ // a parse(). Only a node that is exactly a $ref plus annotations is replaced;
44
+ // a $ref with a constraining sibling, a cycle, an unresolvable target or an
45
+ // external reference is left in place, and emitClone declines it as before.
46
+ // The walk never mutates its input and never descends into data positions
47
+ // (default, const, enum, examples), where an object holding a "$ref" key is
48
+ // a value, not a schema.
49
+ const _INLINE_ANNOTATIONS = new Set(['$ref', 'title', 'description', '$comment', 'examples', 'deprecated', 'readOnly', 'writeOnly', 'default']);
50
+ const _INLINE_DATA_KEYS = new Set(['default', 'const', 'enum', 'examples']);
51
+ function inlineRefsForClone(root) {
52
+ const state = { budget: 512 };
53
+ // `inRef` marks content brought in from behind a reference. The runtime's
54
+ // useDefaults fills a default written next to the $ref, and does not fill
55
+ // defaults written inside the referenced definition, so the inlined copy
56
+ // keeps the first and drops the second. parse() must stay exactly as
57
+ // generous as validate(); a parse that fills more is a disagreement, not a
58
+ // feature.
59
+ const walk = (node, active, inRef) => {
60
+ if (!node || typeof node !== 'object') return node;
61
+ if (Array.isArray(node)) return node.map((n) => walk(n, active, inRef));
62
+ let cur = node;
63
+ let entered = false;
64
+ let siblingDefault;
65
+ while (cur && typeof cur === 'object' && !Array.isArray(cur) && typeof cur.$ref === 'string') {
66
+ for (const k of Object.keys(cur)) if (!_INLINE_ANNOTATIONS.has(k)) return node;
67
+ if (active.has(cur.$ref) || state.budget-- <= 0) return node;
68
+ const target = resolveLocalPointer(root, cur.$ref);
69
+ if (!target || typeof target !== 'object' || Array.isArray(target)) return node;
70
+ if (!entered && !inRef && cur.default !== undefined) siblingDefault = cur.default;
71
+ active = new Set(active);
72
+ active.add(cur.$ref);
73
+ cur = target;
74
+ entered = true;
75
+ }
76
+ const strip = inRef || entered;
77
+ const out = {};
78
+ for (const k of Object.keys(cur)) {
79
+ if (strip && k === 'default') continue;
80
+ const v = cur[k];
81
+ if (_INLINE_DATA_KEYS.has(k) || k === '$defs' || k === 'definitions') { out[k] = v; continue; }
82
+ out[k] = walk(v, active, strip);
83
+ }
84
+ if (siblingDefault !== undefined) out.default = siblingDefault;
85
+ return out;
86
+ };
87
+ return walk(root, new Set(), false);
88
+ }
89
+
90
+ const PROTO_KEY_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '__proto__']);
91
+
92
+ function emitClone(node, access, depth) {
93
+ if (!node || typeof node !== 'object') return null;
94
+ if (depth > 12) return null;
95
+ // Only plain object nodes are cloneable, and only when the schema names
96
+ // every property it keeps. A $ref means the set of allowed keys is not
97
+ // this node's to decide. In-place applicators (allOf, anyOf, oneOf,
98
+ // if/then/else) are admitted under the same proof the unevaluated* error
99
+ // generators use: every property name they contribute must already be
100
+ // declared in this node's own `properties`, so they constrain values but
101
+ // never widen the key set. `not` never widens it. `unevaluatedProperties`
102
+ // is admitted only as `false` under that proof, where it is exactly
103
+ // `additionalProperties: false`.
104
+ if (node.$ref || node.patternProperties || node.additionalProperties === true) {
105
+ return null;
106
+ }
107
+ // additionalProperties as a schema is the record shape (z.record and every
108
+ // generator's map type): the key set is open by declaration, so keeping
109
+ // every key is exact, and every undeclared value is rebuilt against that
110
+ // one schema. That is a proof, not a guess, so it is admitted. What stays
111
+ // out: applicators next to a record (a branch could constrain some keys'
112
+ // values beyond the record schema, and the rebuild would ignore it), and
113
+ // `additionalProperties: true`, where the value is unconstrained and this
114
+ // pass only copies what it can name. An open object with no
115
+ // `additionalProperties` at all also stays out, for the original reason:
116
+ // validate() accepts its unknown keys, so a parse() that strips them would
117
+ // silently drop allowed data, and one that keeps them raw would not be a
118
+ // sanitiser.
119
+ const apSchema = (node.additionalProperties && typeof node.additionalProperties === 'object')
120
+ ? node.additionalProperties
121
+ : null;
122
+ if (apSchema) {
123
+ if (node.unevaluatedProperties !== undefined) return null;
124
+ if (node.allOf || node.anyOf || node.oneOf || node.if || node.then || node.else ||
125
+ node.dependentSchemas !== undefined || node.dependencies !== undefined ||
126
+ node.$dynamicRef !== undefined || node.$recursiveRef !== undefined) return null;
127
+ }
128
+ if (node.unevaluatedProperties !== undefined && node.unevaluatedProperties !== false) return null;
129
+ if (node.type !== 'object') return null;
130
+ if (!node.properties && !apSchema) return null;
131
+ const keys = node.properties ? Object.keys(node.properties) : [];
132
+ if (keys.length === 0 && !apSchema) return null;
133
+ if (node.allOf || node.anyOf || node.oneOf || node.if || node.then || node.else ||
134
+ node.unevaluatedProperties === false) {
135
+ if (node.$dynamicRef !== undefined || node.$recursiveRef !== undefined ||
136
+ node.dependentSchemas !== undefined || node.dependencies !== undefined) return null;
137
+ const names = new Set();
138
+ const state = { prefix: 0 };
139
+ for (const k of ['allOf', 'anyOf', 'oneOf']) {
140
+ if (node[k] !== undefined) {
141
+ if (!Array.isArray(node[k])) return null;
142
+ for (const b of node[k]) if (!unevalContributions(b, 'props', names, state, 0)) return null;
143
+ }
144
+ }
145
+ for (const k of ['if', 'then', 'else']) {
146
+ if (node[k] !== undefined && !unevalContributions(node[k], 'props', names, state, 0)) return null;
147
+ }
148
+ for (const n of names) if (!Object.prototype.hasOwnProperty.call(node.properties, n)) return null;
149
+ }
150
+ const required = new Set(Array.isArray(node.required) ? node.required : []);
151
+
152
+ const fixed = [];
153
+ const conditional = [];
154
+ for (const key of keys) {
155
+ const prop = node.properties[key];
156
+ if (!prop || typeof prop !== 'object') return null;
157
+ if (prop.$ref) return null;
158
+ // A property that is required and carries a default is the one place the
159
+ // runtime and a raw-input validation disagree: the runtime fills the
160
+ // default before checking `required`, so `{}` validates there and would
161
+ // throw here. Declining keeps parse() exactly as strict as validate().
162
+ if (required.has(key) && prop.default !== undefined) return null;
163
+ // A default the property's own schema rejects would make parse() hand
164
+ // back a document validate() refuses: the runtime fills defaults before
165
+ // validating, so it catches the bad default, and a raw-input check here
166
+ // never sees it. Probe the default at emit time and decline when it does
167
+ // not hold, or when the subschema cannot be compiled to check it.
168
+ if (prop.default !== undefined) {
169
+ let probe = null;
170
+ try { probe = compileToJSCodegen(prop, null, null); } catch (_) { probe = null; }
171
+ if (!probe || !probe(prop.default)) return null;
172
+ }
173
+ const read = `${access}[${JSON.stringify(key)}]`;
174
+ const value = emitValueExpr(prop, read, depth);
175
+ if (value === null) return null;
176
+ // A plain `"__proto__":` in an object literal is the prototype slot, not
177
+ // a property; `["__proto__"]:` is a property. Only that key is written as
178
+ // a computed one: a literal with computed keys cannot use the engine's
179
+ // literal boilerplate and is built key by key, which made the copy cost
180
+ // about as much as the validation in front of it.
181
+ if (required.has(key)) fixed.push(key === '__proto__' ? `[${JSON.stringify(key)}]: ${value}` : `${JSON.stringify(key)}: ${value}`);
182
+ else conditional.push({ key, value, dflt: prop.default });
183
+ }
184
+ if (fixed.length === 0 && conditional.length === 0 && !apSchema) return null;
185
+
186
+ const tmp = `_c${depth}`;
187
+ // The record loop: every key of the input that is not a declared property
188
+ // is kept, its value rebuilt against the additionalProperties schema. The
189
+ // input is already validated, so every such value satisfies that schema;
190
+ // the rebuild only strips inside it. Defaults under a record value are
191
+ // dropped before emission because the runtime's useDefaults does not fill
192
+ // them there, and parse() must equal validate().data, not improve on it.
193
+ let recordLoop = '';
194
+ if (apSchema) {
195
+ const kVar = `_k${depth}`;
196
+ const vVar = `_v${depth}`;
197
+ const valueExpr = emitValueExpr(stripDefaultsDeep(apSchema), vVar, depth);
198
+ if (valueExpr === null) return null;
199
+ let skip = '';
200
+ let declSet = '';
201
+ if (keys.length > 8) {
202
+ declSet = `const _d${depth} = new Set(${JSON.stringify(keys)}); `;
203
+ skip = `if (_d${depth}.has(${kVar})) continue; `;
204
+ } else if (keys.length > 0) {
205
+ skip = `if (${keys.map((k) => `${kVar} === ${JSON.stringify(k)}`).join(' || ')}) continue; `;
206
+ }
207
+ // The key comes from the input, so it can be the string "__proto__" (an
208
+ // own key only JSON.parse writes); assignment would hit the prototype
209
+ // setter, so that one key goes through defineProperty.
210
+ recordLoop = ` ${declSet}for (const ${kVar} of Object.keys(${access})) { ${skip}const ${vVar} = ${access}[${kVar}]; if (${kVar} === "__proto__") Object.defineProperty(${tmp}, ${kVar}, { value: ${valueExpr}, writable: true, enumerable: true, configurable: true }); else ${tmp}[${kVar}] = ${valueExpr}; }`;
211
+ }
212
+
213
+ const literal = `{ ${fixed.join(', ')} }`;
214
+ if (conditional.length === 0 && !recordLoop) return literal;
215
+ // Optional properties are added only when present, so the result never
216
+ // gains a key the input did not have.
217
+ const adds = conditional
218
+ .map(({ key, value, dflt }) => {
219
+ // Object.hasOwn, not `in`: `in` also answers for what the instance
220
+ // inherits, so a property named `constructor` would copy the inherited
221
+ // function into the output that validate().data does not carry. And a
222
+ // key named `__proto__` cannot be written by assignment, which hits
223
+ // the prototype setter; defineProperty writes an own key.
224
+ const assign = key === '__proto__'
225
+ ? `Object.defineProperty(${tmp}, ${JSON.stringify(key)}, { value: ${value}, writable: true, enumerable: true, configurable: true });`
226
+ : `${tmp}[${JSON.stringify(key)}] = ${value};`;
227
+ // The same own-property test the generators emit, since Object.hasOwn
228
+ // is not inlined: `in`, then the prototype, then hasOwnProperty only
229
+ // for an object that is not an ordinary one.
230
+ const k = JSON.stringify(key);
231
+ const own = PROTO_KEY_NAMES.has(key)
232
+ ? `Object.prototype.hasOwnProperty.call(${access}, ${k})`
233
+ : `(${k} in ${access} && (${access}.__proto__ === Object.prototype || Object.prototype.hasOwnProperty.call(${access}, ${k})))`;
234
+ const set = `if (${own}) ${assign}`;
235
+ // The default is emitted as a literal inside the call, so an object or
236
+ // array default is a fresh value on every parse, never shared state.
237
+ if (dflt === undefined) return set;
238
+ const setDflt = key === '__proto__'
239
+ ? `Object.defineProperty(${tmp}, ${JSON.stringify(key)}, { value: ${JSON.stringify(dflt)}, writable: true, enumerable: true, configurable: true });`
240
+ : `${tmp}[${JSON.stringify(key)}] = ${JSON.stringify(dflt)};`;
241
+ return `${set} else ${setDflt}`;
242
+ })
243
+ .join(' ');
244
+ return `(function(){ const ${tmp} = ${literal}; ${adds}${recordLoop} return ${tmp}; })()`;
245
+ }
246
+
247
+ // The value dispatch shared by declared properties and the record loop. A
248
+ // value is copyable only when the emitter can name everything that may live
249
+ // under it: a primitive holds nothing, an object with declared properties or
250
+ // a record schema is rebuilt the same way, an array of primitives has
251
+ // nowhere to hide a key. A $ref, a composition or an untyped node could all
252
+ // carry keys this code cannot see, and copying the reference would smuggle
253
+ // them into a value that claims to be sanitised, so the caller declines the
254
+ // whole clone instead.
255
+ // Drop every schema-position `default` in a subtree, leaving data positions
256
+ // (const, enum, examples) untouched. Used on a record's value schema, where
257
+ // the runtime's useDefaults fills nothing.
258
+ function stripDefaultsDeep(node) {
259
+ if (!node || typeof node !== 'object') return node;
260
+ if (Array.isArray(node)) return node.map(stripDefaultsDeep);
261
+ const out = {};
262
+ for (const k of Object.keys(node)) {
263
+ if (k === 'default') continue;
264
+ out[k] = _INLINE_DATA_KEYS.has(k) ? node[k] : stripDefaultsDeep(node[k]);
265
+ }
266
+ return out;
267
+ }
268
+
269
+ const _CLONE_PRIMITIVE = new Set(['string', 'number', 'integer', 'boolean', 'null']);
270
+ function emitValueExpr(prop, read, depth) {
271
+ if (!prop || typeof prop !== 'object') return null;
272
+ if (prop.$ref) return null;
273
+ const types = Array.isArray(prop.type) ? prop.type : [prop.type];
274
+ if (types.every((t) => _CLONE_PRIMITIVE.has(t))) return read;
275
+ const isRecordable = (s) => s.properties ||
276
+ (s.additionalProperties && typeof s.additionalProperties === 'object');
277
+ if (prop.type === 'object' && isRecordable(prop)) {
278
+ return emitClone(prop, read, depth + 1);
279
+ }
280
+ if (prop.type === 'array' && prop.items && typeof prop.items === 'object' &&
281
+ !Array.isArray(prop.items)) {
282
+ const it = prop.items;
283
+ const itemTypes = Array.isArray(it.type) ? it.type : [it.type];
284
+ if (itemTypes.every((t) => _CLONE_PRIMITIVE.has(t))) return read;
285
+ if (it.type === 'object' && isRecordable(it)) {
286
+ const el = '_e' + depth;
287
+ const inner = emitClone(it, el, depth + 1);
288
+ if (!inner) return null;
289
+ return `${read}.map((${el}) => (${inner}))`;
290
+ }
291
+ }
292
+ return null;
293
+ }
294
+
295
+ // The copy as an expression over `data`, or null where the kept key set is
296
+ // not provable.
297
+ function cloneExprFor(schema) {
298
+ if (!schema || typeof schema !== 'object') return null;
299
+ return emitClone(inlineRefsForClone(schema), 'data', 0);
300
+ }
301
+
302
+ module.exports = { cloneExprFor, emitClone, inlineRefsForClone, stripDefaultsDeep, resolveLocalPointer };
@@ -230,7 +230,7 @@ function cpAtMost(s, max) {
230
230
  function typeMatches(type, d) {
231
231
  switch (type) {
232
232
  case 'string': return typeof d === 'string';
233
- case 'number': return typeof d === 'number' && isFinite(d);
233
+ case 'number': return Number.isFinite(d);
234
234
  case 'integer': return typeof d === 'number' && Number.isInteger(d);
235
235
  case 'boolean': return typeof d === 'boolean';
236
236
  case 'null': return d === null;
@@ -529,7 +529,7 @@ function evalLeafV(P, data) {
529
529
  if (!found) return false;
530
530
  }
531
531
  if (P.hasConst && !deepEqual(P.const, data)) return false;
532
- if (P.hasNumber && typeof data === 'number' && isFinite(data)) {
532
+ if (P.hasNumber && Number.isFinite(data)) {
533
533
  if (P.minimum !== undefined && !(data >= P.minimum)) return false;
534
534
  if (P.maximum !== undefined && !(data <= P.maximum)) return false;
535
535
  if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) return false;
@@ -646,7 +646,7 @@ function evalLeaf(P, data, errors, instancePath, schemaPath) {
646
646
  if (errors !== NOERRORS) errors.push(err('const', 'const', instancePath, schemaPath + '/const', { allowedValue: P.const }, 'must be equal to constant'));
647
647
  valid = false;
648
648
  }
649
- if (P.hasNumber && typeof data === 'number' && isFinite(data)) {
649
+ if (P.hasNumber && Number.isFinite(data)) {
650
650
  if (P.minimum !== undefined && !(data >= P.minimum)) { errors === NOERRORS || errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: P.minimum }, `must be >= ${P.minimum}`)); valid = false; }
651
651
  if (P.maximum !== undefined && !(data <= P.maximum)) { errors === NOERRORS || errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: P.maximum }, `must be <= ${P.maximum}`)); valid = false; }
652
652
  if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) { errors === NOERRORS || errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: P.exclusiveMinimum }, `must be > ${P.exclusiveMinimum}`)); valid = false; }
@@ -937,7 +937,7 @@ class Interpreter {
937
937
  }
938
938
 
939
939
  // numbers
940
- if (P.hasNumber && typeof data === 'number' && isFinite(data)) {
940
+ if (P.hasNumber && Number.isFinite(data)) {
941
941
  if (P.minimum !== undefined && !(data >= P.minimum)) { errors === NOERRORS || errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: P.minimum }, `must be >= ${P.minimum}`)); valid = false; }
942
942
  if (P.maximum !== undefined && !(data <= P.maximum)) { errors === NOERRORS || errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: P.maximum }, `must be <= ${P.maximum}`)); valid = false; }
943
943
  if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) { errors === NOERRORS || errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: P.exclusiveMinimum }, `must be > ${P.exclusiveMinimum}`)); valid = false; }
@@ -489,24 +489,24 @@ function compileToJS(schema, defs, schemaMap) {
489
489
  // answer differently for it.
490
490
  if (schema.minimum !== undefined) {
491
491
  const min = schema.minimum
492
- checks.push((d) => typeof d !== 'number' || !isFinite(d) || d >= min)
492
+ checks.push((d) => !Number.isFinite(d) || d >= min)
493
493
  }
494
494
  if (schema.maximum !== undefined) {
495
495
  const max = schema.maximum
496
- checks.push((d) => typeof d !== 'number' || !isFinite(d) || d <= max)
496
+ checks.push((d) => !Number.isFinite(d) || d <= max)
497
497
  }
498
498
  if (schema.exclusiveMinimum !== undefined) {
499
499
  const min = schema.exclusiveMinimum
500
- checks.push((d) => typeof d !== 'number' || !isFinite(d) || d > min)
500
+ checks.push((d) => !Number.isFinite(d) || d > min)
501
501
  }
502
502
  if (schema.exclusiveMaximum !== undefined) {
503
503
  const max = schema.exclusiveMaximum
504
- checks.push((d) => typeof d !== 'number' || !isFinite(d) || d < max)
504
+ checks.push((d) => !Number.isFinite(d) || d < max)
505
505
  }
506
506
  if (schema.multipleOf !== undefined) {
507
507
  const div = schema.multipleOf
508
508
  checks.push((d) => {
509
- if (typeof d !== 'number' || !isFinite(d)) return true
509
+ if (!Number.isFinite(d)) return true
510
510
  if (div === 0) return false
511
511
  const q = d / div
512
512
  return Number.isInteger(q) || Math.abs(q - Math.round(q)) < 1e-9
@@ -850,7 +850,7 @@ function multipleOfBad(v, m) {
850
850
 
851
851
  const TYPE_CHECKS = {
852
852
  string: (d) => typeof d === 'string',
853
- number: (d) => typeof d === 'number' && isFinite(d),
853
+ number: (d) => Number.isFinite(d),
854
854
  integer: (d) => Number.isInteger(d),
855
855
  boolean: (d) => typeof d === 'boolean',
856
856
  null: (d) => d === null,
@@ -918,8 +918,15 @@ const PROTO_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '_
918
918
  // and reading the errors of a product document went from 3.9 to 5.8 us. The
919
919
  // helper only runs once `in` has said yes, and on an ordinary object it
920
920
  // answers from the prototype without looking at the key.
921
- const OWN_HELPERS = 'const _hop=Object.prototype.hasOwnProperty;function _h(o,k){return _hop.call(o,k)}' +
922
- 'function _ok(o,k){return o.__proto__===Object.prototype||_hop.call(o,k)}' +
921
+ //
922
+ // `__proto__` is read through the accessor on Object.prototype, which Deno
923
+ // removes. There every read came back undefined, every object took the per-key
924
+ // hasOwnProperty path, and a ten-field check went from 2.5 to 40 ns. Where the
925
+ // accessor is missing the prototype comes from Object.getPrototypeOf instead,
926
+ // chosen by a constant the engine folds, so Node pays nothing for the branch.
927
+ const OWN_HELPERS = 'const _PN=({}).__proto__===Object.prototype;' +
928
+ 'const _hop=Object.prototype.hasOwnProperty;function _h(o,k){return _hop.call(o,k)}' +
929
+ 'function _ok(o,k){return (_PN?o.__proto__:Object.getPrototypeOf(o))===Object.prototype||_hop.call(o,k)}' +
923
930
  'function _hall(o,ks){for(let i=0;i<ks.length;i++)if(!_hop.call(o,ks[i]))return false;return true}'
924
931
  //
925
932
  // Better still, the prototype is read once per object: a node that asks about
@@ -928,6 +935,11 @@ const OWN_HELPERS = 'const _hop=Object.prototype.hasOwnProperty;function _h(o,k)
928
935
  // helper reading `__proto__` for each key was megamorphic, since it saw every
929
936
  // object shape in the document, and cost the error path about a microsecond
930
937
  // on a product document.
938
+ // Whether `v`, already known to be an object, has Object.prototype as its
939
+ // prototype, read without depending on the __proto__ accessor. Needs OWN_HELPERS.
940
+ function protoIs (v) {
941
+ return `((_PN?${v}.__proto__:Object.getPrototypeOf(${v}))===Object.prototype)`
942
+ }
931
943
  function ownKeyExpr (ctx, v, key) {
932
944
  hoistOnce(ctx, '_ownHoisted', OWN_HELPERS)
933
945
  const k = JSON.stringify(key)
@@ -957,7 +969,8 @@ function withPlain (schema, v, lines, ctx, run) {
957
969
  if (!ctx.plainOf) ctx.plainOf = new Map()
958
970
  const name = '_pk' + ctx.varCounter++
959
971
  const at = lines.length
960
- lines.push(`const ${name}=typeof ${v}==='object'&&${v}!==null&&${v}.__proto__===Object.prototype`)
972
+ const decl = `const ${name}=typeof ${v}==='object'&&${v}!==null&&${protoIs(v)}`
973
+ lines.push(decl)
961
974
  const prev = ctx.plainOf.get(v)
962
975
  ctx.plainOf.set(v, name)
963
976
  try {
@@ -970,14 +983,15 @@ function withPlain (schema, v, lines, ctx, run) {
970
983
  const re = new RegExp('\\b' + name + '\\b')
971
984
  let used = false
972
985
  for (let i = at + 1; i < lines.length && !used; i++) if (re.test(lines[i])) used = true
973
- if (!used && lines[at] === `const ${name}=typeof ${v}==='object'&&${v}!==null&&${v}.__proto__===Object.prototype`) lines.splice(at, 1)
986
+ if (!used && lines[at] === decl) lines.splice(at, 1)
974
987
  }
975
988
  }
976
989
  // The same test at run time, for the closure compiler.
977
990
  const _hop = Object.prototype.hasOwnProperty
991
+ const _PN = ({}).__proto__ === Object.prototype
978
992
  function hasOwnKey (d, key) {
979
993
  if (PROTO_NAMES.has(key)) return _hop.call(d, key)
980
- return key in d && (d.__proto__ === Object.prototype || _hop.call(d, key))
994
+ return key in d && ((_PN ? d.__proto__ : Object.getPrototypeOf(d)) === Object.prototype || _hop.call(d, key))
981
995
  }
982
996
 
983
997
  const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'toString', 'valueOf',
@@ -2005,7 +2019,7 @@ function tryGenCombined(schema, access, ctx) {
2005
2019
  }
2006
2020
 
2007
2021
  if (t === 'number') {
2008
- const conds = [`typeof _v!=='number'||!isFinite(_v)`]
2022
+ const conds = [`!Number.isFinite(_v)`]
2009
2023
  if (schema.minimum !== undefined) conds.push(`_v<${schema.minimum}`)
2010
2024
  if (schema.maximum !== undefined) conds.push(`_v>${schema.maximum}`)
2011
2025
  if (schema.exclusiveMinimum !== undefined) conds.push(`_v<=${schema.exclusiveMinimum}`)
@@ -2190,7 +2204,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2190
2204
  case 'object': lines.push(`if(typeof ${v}!=='object'||${v}===null||Array.isArray(${v}))return false`); break
2191
2205
  case 'array': lines.push(`if(!Array.isArray(${v}))return false`); break
2192
2206
  case 'string': lines.push(`if(typeof ${v}!=='string')return false`); break
2193
- case 'number': lines.push(`if(typeof ${v}!=='number'||!isFinite(${v}))return false`); break
2207
+ case 'number': lines.push(`if(!Number.isFinite(${v}))return false`); break
2194
2208
  case 'integer': lines.push(`if(!Number.isInteger(${v}))return false`); break
2195
2209
  case 'boolean': lines.push(`if(typeof ${v}!=='boolean')return false`); break
2196
2210
  case 'null': lines.push(`if(${v}!==null)return false`); break
@@ -2201,7 +2215,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2201
2215
  case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
2202
2216
  case 'array': return `Array.isArray(${v})`
2203
2217
  case 'string': return `typeof ${v}==='string'`
2204
- case 'number': return `(typeof ${v}==='number'&&isFinite(${v}))`
2218
+ case 'number': return `Number.isFinite(${v})`
2205
2219
  case 'integer': return `Number.isInteger(${v})`
2206
2220
  case 'boolean': return `typeof ${v}==='boolean'`
2207
2221
  case 'null': return `${v}===null`
@@ -2270,7 +2284,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2270
2284
  // Read directly: the object type is already established here, and a
2271
2285
  // flag declared for this alone costs more than it saves. The list is
2272
2286
  // built only on that path, which an ordinary object never takes.
2273
- lines.push(`if(${v}.__proto__!==Object.prototype&&!_hall(${v},${JSON.stringify(typed)}))return false`)
2287
+ lines.push(`if(!${protoIs(v)}&&!_hall(${v},${JSON.stringify(typed)}))return false`)
2274
2288
  }
2275
2289
  } else if (schema.required && schema.required.length > 0) {
2276
2290
  const checks = schema.required.map(key => `!${ownKeyExpr(ctx, v, key)}`)
@@ -2308,7 +2322,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2308
2322
  // non-finite value through: it carries no type, an empty schema accepts it,
2309
2323
  // and the interpreted engine reads it the same way. With `type: number` the
2310
2324
  // type check has already turned it away, so the guard is only needed here.
2311
- const numGuard = isNum ? '' : `typeof ${v}==='number'&&isFinite(${v})&&`
2325
+ const numGuard = isNum ? '' : `Number.isFinite(${v})&&`
2312
2326
  if (schema.minimum !== undefined) lines.push(`if(${numGuard}${v}<${schema.minimum})return false`)
2313
2327
  if (schema.maximum !== undefined) lines.push(`if(${numGuard}${v}>${schema.maximum})return false`)
2314
2328
  if (schema.exclusiveMinimum !== undefined) lines.push(`if(${numGuard}${v}<=${schema.exclusiveMinimum})return false`)
@@ -3973,7 +3987,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3973
3987
  case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
3974
3988
  case 'array': return `Array.isArray(${v})`
3975
3989
  case 'string': return `typeof ${v}==='string'`
3976
- case 'number': return `(typeof ${v}==='number'&&isFinite(${v}))`
3990
+ case 'number': return `Number.isFinite(${v})`
3977
3991
  case 'integer': return `Number.isInteger(${v})`
3978
3992
  case 'boolean': return `typeof ${v}==='boolean'`
3979
3993
  case 'null': return `${v}===null`
@@ -4710,7 +4724,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4710
4724
  case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
4711
4725
  case 'array': return `Array.isArray(${v})`
4712
4726
  case 'string': return `typeof ${v}==='string'`
4713
- case 'number': return `(typeof ${v}==='number'&&isFinite(${v}))`
4727
+ case 'number': return `Number.isFinite(${v})`
4714
4728
  case 'integer': return `Number.isInteger(${v})`
4715
4729
  case 'boolean': return `typeof ${v}==='boolean'`
4716
4730
  case 'null': return `${v}===null`
@@ -4906,6 +4920,21 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4906
4920
  const allowed = propKeys.map(k => JSON.stringify(k)).join(',')
4907
4921
  body = `const _a${ci}=new Set([${allowed}]);let _x${ci}=0;for(const _f${ci} in ${v}){if(!_a${ci}.has(_f${ci})){_x${ci}=1;break}}if(_x${ci}){${detail(`!_a${ci}.has(_k${ci}[_i])`)}}`
4908
4922
  }
4923
+ // When every declared property is required, a clean object has exactly the
4924
+ // declared keys: if the enumerated count equals the declared count and
4925
+ // every declared name is there, the enumerated set is the declared set and
4926
+ // nothing is extra. That is the verdict generator's count, and it skips
4927
+ // comparing each key against every name, which kept this function over
4928
+ // twice the verdict's cost on an accepted document. Anything else, a
4929
+ // different count or a missing name, falls through to the scan, which
4930
+ // names the extra keys. A name Object.prototype carries answers `in`
4931
+ // through the prototype without being enumerated, so those stay on the
4932
+ // scan.
4933
+ const req = new Set(Array.isArray(schema.required) ? schema.required : [])
4934
+ if (propKeys.length > 0 && propKeys.every((k) => req.has(k) && !PROTO_NAMES.has(k))) {
4935
+ const present = propKeys.map((k) => `${JSON.stringify(k)} in ${v}`).join('&&')
4936
+ body = `let _n${ci}=0;for(const _f${ci} in ${v})_n${ci}++;if(_n${ci}!==${propKeys.length}||!(${present})){${body}}`
4937
+ }
4909
4938
  lines.push(isObj
4910
4939
  ? `{${body}}`
4911
4940
  : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body}}`)
@@ -182,7 +182,7 @@ function install(deps) {
182
182
  // no type bit, and an empty schema accepts it. So the numeric keywords
183
183
  // must not apply to it either, or `{ minimum: 3 }` rejects a value that
184
184
  // `{}` accepts and the compiled engine admits.
185
- if (run !== null) checks.push((data) => typeof data !== 'number' || !isFinite(data) || run(data));
185
+ if (run !== null) checks.push((data) => !Number.isFinite(data) || run(data));
186
186
  }
187
187
  if (P.hasString) {
188
188
  const strs = [];
@@ -28,7 +28,7 @@ const HELPERS = 'const _hop=Object.prototype.hasOwnProperty;';
28
28
  const PROTO_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '__proto__']);
29
29
 
30
30
  function isPrimitive(x) {
31
- return x === null || typeof x === 'string' || typeof x === 'boolean' || (typeof x === 'number' && isFinite(x));
31
+ return x === null || typeof x === 'string' || typeof x === 'boolean' || Number.isFinite(x);
32
32
  }
33
33
 
34
34
  function lit(x) {
@@ -58,7 +58,7 @@ function local(ctx) {
58
58
  function typeTest(mask, v) {
59
59
  const parts = [];
60
60
  if (mask & T_STRING) parts.push(`typeof ${v}==='string'`);
61
- if (mask & T_NUMBER) parts.push(`(typeof ${v}==='number'&&isFinite(${v}))`);
61
+ if (mask & T_NUMBER) parts.push(`Number.isFinite(${v})`);
62
62
  else if (mask & T_INTEGER) parts.push(`Number.isInteger(${v})`);
63
63
  if (mask & T_BOOLEAN) parts.push(`typeof ${v}==='boolean'`);
64
64
  if (mask & T_NULL) parts.push(`${v}===null`);
@@ -114,7 +114,7 @@ function emitLeaf(ctx, P, v, known) {
114
114
  if (P.exclusiveMinimum !== undefined) n += `if(${v}<=${lit(P.exclusiveMinimum)})return false;`;
115
115
  if (P.exclusiveMaximum !== undefined) n += `if(${v}>=${lit(P.exclusiveMaximum)})return false;`;
116
116
  if (P.multipleOf !== undefined) n += `if(!${arg(ctx, multipleOfOk)}(${v},${lit(P.multipleOf)}))return false;`;
117
- out += guarded(known, K_NUM, `typeof ${v}==='number'&&isFinite(${v})`, n);
117
+ out += guarded(known, K_NUM, `Number.isFinite(${v})`, n);
118
118
  }
119
119
  if (P.hasString) {
120
120
  let s = '';
package/lib/tier0.js CHANGED
@@ -46,6 +46,8 @@ const F_MULT = 16;
46
46
  // itself; see ownKeyExpr in js-compiler.js.
47
47
  const PROTO_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '__proto__']);
48
48
  const _hop = Object.prototype.hasOwnProperty;
49
+ // Deno removes the __proto__ accessor; read the prototype another way there.
50
+ const _PN = ({}).__proto__ === Object.prototype;
49
51
 
50
52
  function primConstraint(key, propSchema) {
51
53
  const t = propSchema.type;
@@ -137,7 +139,7 @@ function checkPrimitive(c, v) {
137
139
  if ((f & F_MULT) && !_multipleOk(v, c.multipleOf)) return false;
138
140
  }
139
141
  } else if (m === T_NUMBER) {
140
- if (typeof v !== 'number' || !isFinite(v)) return false;
142
+ if (!Number.isFinite(v)) return false;
141
143
  const f = c.numFlags;
142
144
  if (f !== 0) {
143
145
  if ((f & F_MIN) && v < c.min) return false;
@@ -169,7 +171,7 @@ function tier0ValidateObject(plan, data) {
169
171
  let v = data[c.key];
170
172
  // Presence is an own-property question: a value the object inherits is
171
173
  // treated as absent, as the interpreted engine treats it.
172
- if (v !== undefined && (c.protoName || data.__proto__ !== Object.prototype) && !_hop.call(data, c.key)) v = undefined;
174
+ if (v !== undefined && (c.protoName || (_PN ? data.__proto__ : Object.getPrototypeOf(data)) !== Object.prototype) && !_hop.call(data, c.key)) v = undefined;
173
175
  if (v === undefined) {
174
176
  if (reqMask & (1 << i)) return false;
175
177
  continue;
@@ -194,7 +196,7 @@ function tier0ValidateObject(plan, data) {
194
196
  if ((f & F_MULT) && !_multipleOk(v, c.multipleOf)) return false;
195
197
  }
196
198
  } else if (m === T_NUMBER) {
197
- if (typeof v !== 'number' || !isFinite(v)) return false;
199
+ if (!Number.isFinite(v)) return false;
198
200
  const f = c.numFlags;
199
201
  if (f !== 0) {
200
202
  if ((f & F_MIN) && v < c.min) 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.31.3';
10
+ module.exports = '1.32.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.31.3",
3
+ "version": "1.32.1",
4
4
  "description": "JSON Schema validation that compiles for speed and still runs where code generation is blocked. Compiled and interpreted engines answer identically at 100% of the official suite. TypeScript inference, Standard Schema V1, and a build step that emits dependency-free modules.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -51,7 +51,7 @@
51
51
  "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",
52
52
  "build": "cmake-js build --target ata",
53
53
  "rebuild": "cmake-js rebuild --target ata",
54
- "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_validate_json_scanner_cost.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.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_native_loaded.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_format_mode.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_engine_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_uri_fast_path.js && node tests/test_extend_verdict.js && node tests/test_ipv4_regex.js && node tests/test_ipv6_fast_path.js && node tests/test_nested_recursion_decline.js && node tests/test_generator_error_content.js && node tests/test_inherited_key_presence.js && node tests/test_plan_source.js && node tests/test_fused_remove_additional.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_deferred_def_scope.js && node tests/test_node_scoped_ctx_flags.js && node tests/test_fast_slot_exhaustion.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_additional_props_combined_cost.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_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_single_pass_errors.js && node tests/test_lazy_json_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_defaults_own_keys.js && node tests/test_plan_compiler.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_enrich_received.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_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.js && node tests/test_schema_hash.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.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",
54
+ "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_validate_json_scanner_cost.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.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_native_loaded.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_format_mode.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_engine_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_uri_fast_path.js && node tests/test_extend_verdict.js && node tests/test_ipv4_regex.js && node tests/test_ipv6_fast_path.js && node tests/test_nested_recursion_decline.js && node tests/test_generator_error_content.js && node tests/test_inherited_key_presence.js && node tests/test_proto_accessor.js && node tests/test_plan_source.js && node tests/test_fused_remove_additional.js && node tests/test_runtime_parse.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_deferred_def_scope.js && node tests/test_node_scoped_ctx_flags.js && node tests/test_fast_slot_exhaustion.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_additional_props_combined_cost.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_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_single_pass_errors.js && node tests/test_lazy_json_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_defaults_own_keys.js && node tests/test_plan_compiler.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_enrich_received.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_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.js && node tests/test_schema_hash.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.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",
55
55
  "bench:size": "node benchmark/bench_aot_size.mjs",
56
56
  "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
57
57
  "test:compat": "node tests/test_compat.js",
@@ -121,13 +121,13 @@
121
121
  "LICENSE"
122
122
  ],
123
123
  "optionalDependencies": {
124
- "@ata-validator/native-darwin-arm64": "1.31.3",
125
- "@ata-validator/native-darwin-x64": "1.31.3",
126
- "@ata-validator/native-linux-arm64-gnu": "1.31.3",
127
- "@ata-validator/native-linux-arm64-musl": "1.31.3",
128
- "@ata-validator/native-linux-x64-gnu": "1.31.3",
129
- "@ata-validator/native-linux-x64-musl": "1.31.3",
130
- "@ata-validator/native-win32-x64": "1.31.3"
124
+ "@ata-validator/native-darwin-arm64": "1.32.1",
125
+ "@ata-validator/native-darwin-x64": "1.32.1",
126
+ "@ata-validator/native-linux-arm64-gnu": "1.32.1",
127
+ "@ata-validator/native-linux-arm64-musl": "1.32.1",
128
+ "@ata-validator/native-linux-x64-gnu": "1.32.1",
129
+ "@ata-validator/native-linux-x64-musl": "1.32.1",
130
+ "@ata-validator/native-win32-x64": "1.32.1"
131
131
  },
132
132
  "peerDependencies": {
133
133
  "yaml": "^2.0.0"