@blamejs/core 0.12.62 → 0.12.64

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.
@@ -0,0 +1,740 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.jsonSchema
4
+ * @nav Data
5
+ * @title JSON Schema
6
+ *
7
+ * @intro
8
+ * Validate JSON against a <a href="https://json-schema.org/">JSON Schema</a>
9
+ * 2020-12 document — the dialect <a href="https://www.openapis.org/">OpenAPI
10
+ * 3.1</a> adopted and the most widely implemented schema language. This is
11
+ * the standards-track counterpart to the fluent <code>b.safeSchema</code>
12
+ * builder (in-process, ergonomic) and the portable <code>b.jtd</code>
13
+ * (small, codegen-friendly): reach for <code>b.jsonSchema</code> when the
14
+ * schema is an existing JSON Schema document — an API contract, a config
15
+ * schema, an OpenAPI component.
16
+ *
17
+ * <code>compile(schema, opts)</code> returns a reusable validator;
18
+ * <code>validate(schema, instance, opts)</code> compiles and runs in one
19
+ * call, returning <code>{ valid, errors }</code> where each error names the
20
+ * failing instance location, the schema keyword, and a message. The full
21
+ * 2020-12 vocabulary is supported — every applicator
22
+ * (<code>allOf</code> / <code>anyOf</code> / <code>oneOf</code> /
23
+ * <code>not</code> / <code>if</code>-<code>then</code>-<code>else</code>,
24
+ * <code>properties</code> / <code>patternProperties</code> /
25
+ * <code>additionalProperties</code> / <code>prefixItems</code> /
26
+ * <code>items</code> / <code>contains</code>), the annotation-aware
27
+ * <code>unevaluatedProperties</code> / <code>unevaluatedItems</code>, every
28
+ * assertion keyword, and reference resolution
29
+ * (<code>$ref</code> / <code>$anchor</code> / <code>$dynamicRef</code> /
30
+ * <code>$dynamicAnchor</code> / <code>$defs</code> / <code>$id</code> base
31
+ * URIs). <code>format</code> is an annotation by default (opt in to
32
+ * assertion with <code>assertFormat: true</code>). External references
33
+ * resolve through an operator-supplied schema map (<code>opts.schemas</code>)
34
+ * — never a network fetch.
35
+ *
36
+ * Two advanced behaviors are opt-in rather than built in: validating a
37
+ * schema <em>document</em> against the dialect metaschema works only if you
38
+ * supply that metaschema via <code>opts.schemas</code> (it is not bundled),
39
+ * and <code>$vocabulary</code>-based keyword selection is not honored —
40
+ * every standard keyword always asserts.
41
+ *
42
+ * @card
43
+ * JSON Schema 2020-12 validation (the OpenAPI 3.1 dialect) — full
44
+ * vocabulary including <code>$dynamicRef</code> and annotation-aware
45
+ * <code>unevaluated*</code>, returning located <code>{ valid, errors }</code>.
46
+ * The standards-track companion to <code>b.safeSchema</code> and
47
+ * <code>b.jtd</code>.
48
+ */
49
+
50
+ var numericBounds = require("./numeric-bounds");
51
+ var rfc3339 = require("./rfc3339");
52
+ var { defineClass } = require("./framework-error");
53
+
54
+ var JsonSchemaError = defineClass("JsonSchemaError", { alwaysPermanent: true });
55
+
56
+ var DIALECT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
57
+ var MAX_REF_DEPTH = 10000; // allow:raw-byte-literal — recursion-depth cap (count, not a byte size)
58
+ var DEFAULT_MAX_ERRORS = 100; // error-collection cap
59
+
60
+ function _typeOf(v) {
61
+ if (v === null) return "null";
62
+ if (Array.isArray(v)) return "array";
63
+ if (typeof v === "number") return "number";
64
+ if (typeof v === "boolean") return "boolean";
65
+ if (typeof v === "string") return "string";
66
+ if (typeof v === "object") return "object";
67
+ return "unknown";
68
+ }
69
+ function _isObject(v) { return v !== null && typeof v === "object" && !Array.isArray(v); }
70
+ function _isInteger(v) { return typeof v === "number" && isFinite(v) && Math.floor(v) === v; }
71
+
72
+ // Deep equality for enum / const / uniqueItems (JSON value semantics).
73
+ function _deepEqual(a, b) {
74
+ if (a === b) return true;
75
+ var ta = _typeOf(a), tb = _typeOf(b);
76
+ if (ta !== tb) return false;
77
+ if (ta === "number") return a === b;
78
+ if (ta === "array") {
79
+ if (a.length !== b.length) return false;
80
+ for (var i = 0; i < a.length; i++) if (!_deepEqual(a[i], b[i])) return false;
81
+ return true;
82
+ }
83
+ if (ta === "object") {
84
+ var ka = Object.keys(a), kb = Object.keys(b);
85
+ if (ka.length !== kb.length) return false;
86
+ for (var j = 0; j < ka.length; j++) {
87
+ if (!Object.prototype.hasOwnProperty.call(b, ka[j])) return false;
88
+ if (!_deepEqual(a[ka[j]], b[ka[j]])) return false;
89
+ }
90
+ return true;
91
+ }
92
+ return false;
93
+ }
94
+
95
+ // --- URI helpers (RFC 3986 resolution via WHATWG URL where possible) ---
96
+
97
+ function _resolveUri(ref, base) {
98
+ if (!base) {
99
+ // No base — keep absolute as-is; bare fragments stay as "#...".
100
+ if (ref.indexOf("#") === 0) return ref;
101
+ return ref;
102
+ }
103
+ if (ref === "") return base;
104
+ // RFC 3986 relative→absolute resolution of a schema $id/$ref (operator-
105
+ // trusted schema text, not request data); safeUrl.parse intentionally
106
+ // rejects the relative refs and non-http schemes schemas legitimately use.
107
+ try { return new URL(ref, base).href; } // allow:raw-new-url — schema $id/$ref URI resolution, not request-data URL handling
108
+ catch (_e) {
109
+ // Relative resolution against a non-URL base (e.g. "urn:..." or a
110
+ // bare name). Fall back to fragment-aware concatenation.
111
+ if (ref.indexOf("#") === 0) {
112
+ var hashIdx = base.indexOf("#");
113
+ return (hashIdx === -1 ? base : base.slice(0, hashIdx)) + ref;
114
+ }
115
+ return ref;
116
+ }
117
+ }
118
+ function _splitFragment(uri) {
119
+ var i = uri.indexOf("#");
120
+ if (i === -1) return { base: uri, fragment: null };
121
+ return { base: uri.slice(0, i), fragment: uri.slice(i + 1) };
122
+ }
123
+ // Decode a JSON Pointer reference token (~1 → /, ~0 → ~) per RFC 6901.
124
+ function _unescapePointerToken(t) { return t.replace(/~1/g, "/").replace(/~0/g, "~"); }
125
+
126
+ // --- registry: indexes every subschema by canonical URI + anchors ---
127
+
128
+ function _Registry() { this.schemas = {}; this.dynamicAnchors = {}; this.baseByNode = new Map(); }
129
+
130
+ _Registry.prototype.add = function (schema, baseUri) {
131
+ this._walk(schema, baseUri || "", "");
132
+ // A document retrieved from URI X is addressable by X even when its own
133
+ // $id is a different (canonical) URI — register the retrieval URI too.
134
+ if (baseUri && (_isObject(schema) || typeof schema === "boolean")) {
135
+ if (!Object.prototype.hasOwnProperty.call(this.schemas, baseUri)) this.schemas[baseUri] = schema;
136
+ if (!Object.prototype.hasOwnProperty.call(this.schemas, baseUri + "#")) this.schemas[baseUri + "#"] = schema;
137
+ }
138
+ };
139
+
140
+ // Walk a schema document, registering $id base changes, $anchor and
141
+ // $dynamicAnchor names, and indexing every subschema by its base URI +
142
+ // JSON-pointer fragment.
143
+ _Registry.prototype._walk = function (node, baseUri, pointer) {
144
+ if (!_isObject(node) && typeof node !== "boolean") return;
145
+ if (typeof node === "boolean") { this.schemas[baseUri + "#" + pointer] = node; return; }
146
+
147
+ var thisBase = baseUri;
148
+ if (typeof node.$id === "string") {
149
+ thisBase = _resolveUri(node.$id, baseUri);
150
+ var sf = _splitFragment(thisBase);
151
+ thisBase = sf.base + (sf.fragment ? "#" + sf.fragment : "");
152
+ // Canonicalize: $id without fragment becomes the new base.
153
+ if (!sf.fragment) {
154
+ this.schemas[thisBase] = node;
155
+ this.schemas[thisBase + "#"] = node;
156
+ }
157
+ pointer = ""; // pointer is now relative to the new base
158
+ }
159
+ // Index this node by base#pointer + record its canonical base so the
160
+ // validator uses it directly (a $ref to a node with its own relative $id
161
+ // must NOT re-resolve that $id against the URI used to reach it).
162
+ this.schemas[thisBase + "#" + pointer] = node;
163
+ if (pointer === "") this.schemas[thisBase] = node;
164
+ this.baseByNode.set(node, thisBase);
165
+
166
+ if (typeof node.$anchor === "string") {
167
+ this.schemas[thisBase + "#" + node.$anchor] = node;
168
+ }
169
+ if (typeof node.$dynamicAnchor === "string") {
170
+ this.schemas[thisBase + "#" + node.$dynamicAnchor] = node;
171
+ if (!this.dynamicAnchors[node.$dynamicAnchor]) this.dynamicAnchors[node.$dynamicAnchor] = [];
172
+ this.dynamicAnchors[node.$dynamicAnchor].push({ uri: thisBase, schema: node });
173
+ }
174
+
175
+ // Recurse. Keywords whose values are schemas vs maps-of-schemas vs
176
+ // arrays-of-schemas are walked with the right shape.
177
+ var self = this;
178
+ function child(key, sub, ptr) { self._walk(sub, thisBase, ptr); }
179
+ SCHEMA_KEYWORDS.forEach(function (k) {
180
+ if (node[k] !== undefined) child(k, node[k], pointer + "/" + k);
181
+ });
182
+ SCHEMA_MAP_KEYWORDS.forEach(function (k) {
183
+ if (_isObject(node[k])) {
184
+ Object.keys(node[k]).forEach(function (sk) {
185
+ child(k, node[k][sk], pointer + "/" + k + "/" + _escPtr(sk));
186
+ });
187
+ }
188
+ });
189
+ SCHEMA_ARRAY_KEYWORDS.forEach(function (k) {
190
+ if (Array.isArray(node[k])) {
191
+ node[k].forEach(function (sub, idx) { child(k, sub, pointer + "/" + k + "/" + idx); });
192
+ }
193
+ });
194
+ };
195
+
196
+ _Registry.prototype.resolve = function (uri) {
197
+ if (Object.prototype.hasOwnProperty.call(this.schemas, uri)) return this.schemas[uri];
198
+ var sf = _splitFragment(uri);
199
+ // Try base with empty fragment.
200
+ if (sf.fragment === null) {
201
+ if (Object.prototype.hasOwnProperty.call(this.schemas, sf.base + "#")) return this.schemas[sf.base + "#"];
202
+ return undefined;
203
+ }
204
+ // JSON-pointer fragment: resolve against the registered base document.
205
+ if (sf.fragment === "" || sf.fragment.charAt(0) === "/") {
206
+ var doc = this.schemas[sf.base] !== undefined ? this.schemas[sf.base] : this.schemas[sf.base + "#"];
207
+ if (doc === undefined) return undefined;
208
+ return _pointerInto(doc, sf.fragment);
209
+ }
210
+ // Plain-name anchor.
211
+ return this.schemas[sf.base + "#" + sf.fragment];
212
+ };
213
+
214
+ function _escPtr(s) { return s.replace(/~/g, "~0").replace(/\//g, "~1"); }
215
+
216
+ function _pointerInto(doc, fragment) {
217
+ if (fragment === "" ) return doc;
218
+ var parts = fragment.split("/");
219
+ parts.shift(); // leading ""
220
+ var cur = doc;
221
+ for (var i = 0; i < parts.length; i++) {
222
+ var tok = _unescapePointerToken(decodeURIComponent(parts[i]));
223
+ if (cur === null || typeof cur !== "object") return undefined;
224
+ if (Array.isArray(cur)) {
225
+ var idx = Number(tok);
226
+ if (!_isInteger(idx) || idx < 0 || idx >= cur.length) return undefined;
227
+ cur = cur[idx];
228
+ } else {
229
+ if (!Object.prototype.hasOwnProperty.call(cur, tok)) return undefined;
230
+ cur = cur[tok];
231
+ }
232
+ }
233
+ return cur;
234
+ }
235
+
236
+ // Keyword classification for the registry walker.
237
+ var SCHEMA_KEYWORDS = ["additionalProperties", "propertyNames", "items",
238
+ "contains", "not", "if", "then", "else", "unevaluatedItems",
239
+ "unevaluatedProperties"];
240
+ var SCHEMA_MAP_KEYWORDS = ["$defs", "definitions", "properties",
241
+ "patternProperties", "dependentSchemas"];
242
+ var SCHEMA_ARRAY_KEYWORDS = ["allOf", "anyOf", "oneOf", "prefixItems"];
243
+
244
+ module.exports = _buildModule();
245
+
246
+ function _buildModule() {
247
+ return {
248
+ DIALECT: DIALECT_2020_12,
249
+ JsonSchemaError: JsonSchemaError,
250
+ compile: compile,
251
+ validate: validate,
252
+ isValid: isValid,
253
+ };
254
+ }
255
+
256
+ /**
257
+ * @primitive b.jsonSchema.compile
258
+ * @signature b.jsonSchema.compile(schema, opts?)
259
+ * @since 0.12.64
260
+ * @status stable
261
+ * @related b.jsonSchema.validate, b.safeSchema, b.jtd
262
+ *
263
+ * Compile a JSON Schema 2020-12 document into a reusable validator. The
264
+ * returned object has <code>validate(instance)</code> →
265
+ * <code>{ valid, errors }</code> and <code>isValid(instance)</code> →
266
+ * boolean. Compiling once and validating many instances avoids re-indexing
267
+ * the schema's references on every call.
268
+ *
269
+ * @opts
270
+ * schemas: object, // map of external $id/URI → schema, for $ref
271
+ * assertFormat: boolean, // default: false (format is an annotation)
272
+ * maxErrors: number, // default: 100 — stop collecting past this
273
+ *
274
+ * @example
275
+ * var v = b.jsonSchema.compile({ type: "object",
276
+ * properties: { n: { type: "integer" } }, required: ["n"] });
277
+ * v.validate({ n: 1 }).valid; // → true
278
+ */
279
+ function compile(schema, opts) {
280
+ opts = opts || {};
281
+ if (!_isObject(schema) && typeof schema !== "boolean") {
282
+ throw new JsonSchemaError("json-schema/bad-schema", "jsonSchema.compile: schema must be an object or boolean");
283
+ }
284
+ var registry = new _Registry();
285
+ // Register operator-supplied external schemas first (so $id collisions
286
+ // prefer the root document registered last).
287
+ if (_isObject(opts.schemas)) {
288
+ Object.keys(opts.schemas).forEach(function (uri) {
289
+ registry.add(opts.schemas[uri], uri);
290
+ });
291
+ }
292
+ var rootBase = (_isObject(schema) && typeof schema.$id === "string") ? _resolveUri(schema.$id, "") : "";
293
+ registry.add(schema, rootBase);
294
+
295
+ var assertFormat = opts.assertFormat === true;
296
+ var maxErrors = numericBounds.isPositiveFiniteInt(opts.maxErrors) ? opts.maxErrors : DEFAULT_MAX_ERRORS;
297
+
298
+ function _run(instance) {
299
+ var ctx = {
300
+ registry: registry, assertFormat: assertFormat, maxErrors: maxErrors,
301
+ errors: [], depth: 0, dynamicScope: [],
302
+ };
303
+ _validate(schema, instance, "", "#", rootBase, ctx);
304
+ return { valid: ctx.errors.length === 0, errors: ctx.errors };
305
+ }
306
+ return {
307
+ validate: _run,
308
+ isValid: function (instance) { return _run(instance).valid; },
309
+ };
310
+ }
311
+
312
+ /**
313
+ * @primitive b.jsonSchema.validate
314
+ * @signature b.jsonSchema.validate(schema, instance, opts?)
315
+ * @since 0.12.64
316
+ * @status stable
317
+ * @related b.jsonSchema.compile, b.jsonSchema.isValid
318
+ *
319
+ * Compile <code>schema</code> and validate <code>instance</code> in one
320
+ * call, returning <code>{ valid, errors }</code>. Each error is
321
+ * <code>{ instancePath, keyword, schemaPath, message }</code>. For repeated
322
+ * validation against the same schema, use <code>compile</code> instead.
323
+ *
324
+ * @opts
325
+ * schemas: object, // map of external $id/URI → schema, for $ref
326
+ * assertFormat: boolean, // default: false (format is an annotation)
327
+ * maxErrors: number, // default: 100 — stop collecting past this
328
+ *
329
+ * @example
330
+ * b.jsonSchema.validate({ type: "string", minLength: 2 }, "hi").valid;
331
+ * // → true
332
+ */
333
+ function validate(schema, instance, opts) {
334
+ return compile(schema, opts).validate(instance);
335
+ }
336
+
337
+ /**
338
+ * @primitive b.jsonSchema.isValid
339
+ * @signature b.jsonSchema.isValid(schema, instance, opts?)
340
+ * @since 0.12.64
341
+ * @status stable
342
+ * @related b.jsonSchema.validate
343
+ *
344
+ * Boolean convenience form of <code>validate</code>.
345
+ *
346
+ * @opts
347
+ * schemas: object, // map of external $id/URI → schema, for $ref
348
+ * assertFormat: boolean, // default: false (format is an annotation)
349
+ * maxErrors: number, // default: 100 — stop collecting past this
350
+ *
351
+ * @example
352
+ * b.jsonSchema.isValid({ type: "integer" }, 3); // → true
353
+ */
354
+ function isValid(schema, instance, opts) {
355
+ return compile(schema, opts).validate(instance).valid;
356
+ }
357
+
358
+ // ============================================================
359
+ // Core evaluation. Returns { evaluatedProps: {name:true}, evaluatedItems:
360
+ // {index:true} } describing the annotations produced for unevaluated*.
361
+ // Errors are pushed onto ctx.errors. A subschema "fails" iff it pushed at
362
+ // least one error during its own evaluation (tracked via error-count
363
+ // snapshot at each applicator boundary).
364
+ // ============================================================
365
+
366
+ function _err(ctx, instancePath, keyword, schemaPath, message) {
367
+ if (ctx.errors.length < ctx.maxErrors) {
368
+ ctx.errors.push({ instancePath: instancePath, keyword: keyword, schemaPath: schemaPath, message: message });
369
+ }
370
+ }
371
+
372
+ // Validate `instance` against `schema`. Annotations (evaluated props/items)
373
+ // are returned so callers (objects/arrays with unevaluated*) can consult
374
+ // them. `silent` runs validation without recording errors (used by
375
+ // applicators that only need the boolean + annotations, e.g. anyOf/oneOf
376
+ // branches, if).
377
+ function _validate(schema, instance, instancePath, schemaPath, baseUri, ctx, silent) {
378
+ var ann = { evaluatedProps: {}, evaluatedItems: {} };
379
+ if (schema === true) return ann;
380
+ if (schema === false) {
381
+ if (!silent) _err(ctx, instancePath, "false", schemaPath, "schema is false — no value is valid");
382
+ ann.failed = true;
383
+ return ann;
384
+ }
385
+ if (!_isObject(schema)) return ann;
386
+
387
+ if (ctx.depth++ > MAX_REF_DEPTH) throw new JsonSchemaError("json-schema/ref-loop", "jsonSchema: reference depth exceeded (cyclic $ref?)");
388
+ // The effective base for this subschema. The registry already computed
389
+ // each walked node's canonical base (its $id resolved against its lexical
390
+ // parent), so prefer that — re-resolving $id against the URI we arrived
391
+ // by would double a relative $id. Fall back to live resolution for nodes
392
+ // the registry didn't index (defensive).
393
+ var effectiveBase = ctx.registry.baseByNode.has(schema)
394
+ ? ctx.registry.baseByNode.get(schema)
395
+ : (typeof schema.$id === "string" ? _resolveUri(schema.$id, baseUri) : baseUri);
396
+ // Push the effective base onto the dynamic scope so $dynamicRef can find
397
+ // the outermost frame carrying a matching $dynamicAnchor.
398
+ ctx.dynamicScope.push(effectiveBase);
399
+ try {
400
+ return _validateBody(schema, instance, instancePath, schemaPath, effectiveBase, ctx, silent, ann);
401
+ } finally { ctx.depth--; ctx.dynamicScope.pop(); }
402
+ }
403
+
404
+ function _validateBody(schema, instance, instancePath, schemaPath, baseUri, ctx, silent, ann) {
405
+ // baseUri already reflects this subschema's $id (resolved in _validate).
406
+ var type = _typeOf(instance);
407
+ var startErrors = ctx.errors.length;
408
+ function fail() { return ctx.errors.length > startErrors; }
409
+ function emit(kw, msg) { if (!silent) _err(ctx, instancePath, kw, schemaPath + "/" + kw, msg); ann.failed = true; }
410
+
411
+ // ---- $ref / $dynamicRef (in-place applicators) ----
412
+ if (typeof schema.$ref === "string") {
413
+ var refUri = _resolveUri(schema.$ref, baseUri);
414
+ var target = ctx.registry.resolve(refUri);
415
+ if (target === undefined) target = ctx.registry.resolve(_splitFragment(refUri).base + "#" + (_splitFragment(refUri).fragment || ""));
416
+ if (target === undefined) {
417
+ emit("$ref", "cannot resolve $ref '" + schema.$ref + "'");
418
+ } else {
419
+ var refBase = _splitFragment(refUri).base || baseUri;
420
+ var refAnn = _validate(target, instance, instancePath, schemaPath + "/$ref", refBase, ctx, silent);
421
+ _mergeAnn(ann, refAnn);
422
+ if (refAnn.failed) ann.failed = true; // the child emits its own errors (when not silent); propagate pass/fail
423
+ }
424
+ }
425
+ if (typeof schema.$dynamicRef === "string") {
426
+ _applyDynamicRef(schema.$dynamicRef, instance, instancePath, schemaPath, baseUri, ctx, silent, ann);
427
+ }
428
+
429
+ // ---- assertions ----
430
+ if (schema.type !== undefined && !_typeMatches(schema.type, instance, type)) {
431
+ emit("type", "value is " + type + ", expected " + (Array.isArray(schema.type) ? schema.type.join("/") : schema.type));
432
+ }
433
+ if (schema.enum !== undefined) {
434
+ var inEnum = false;
435
+ for (var ei = 0; ei < schema.enum.length; ei++) { if (_deepEqual(instance, schema.enum[ei])) { inEnum = true; break; } }
436
+ if (!inEnum) emit("enum", "value is not one of the enum values");
437
+ }
438
+ if (Object.prototype.hasOwnProperty.call(schema, "const")) {
439
+ if (!_deepEqual(instance, schema.const)) emit("const", "value does not equal const");
440
+ }
441
+
442
+ if (type === "number") _checkNumber(schema, instance, emit);
443
+ if (type === "string") _checkString(schema, instance, ctx, emit);
444
+ if (type === "array") _checkArray(schema, instance, instancePath, schemaPath, baseUri, ctx, silent, ann, emit);
445
+ if (type === "object") _checkObject(schema, instance, instancePath, schemaPath, baseUri, ctx, silent, ann, emit);
446
+
447
+ // ---- in-place applicators (apply regardless of type) ----
448
+ _applyLogical(schema, instance, instancePath, schemaPath, baseUri, ctx, silent, ann, emit);
449
+
450
+ // ---- format (annotation by default; assertion when enabled) ----
451
+ if (typeof schema.format === "string" && ctx.assertFormat) {
452
+ if (!_checkFormat(schema.format, instance, type)) emit("format", "value does not match format '" + schema.format + "'");
453
+ }
454
+
455
+ // ---- unevaluatedProperties / unevaluatedItems (consume annotations) ----
456
+ if (type === "object" && schema.unevaluatedProperties !== undefined) {
457
+ Object.keys(instance).forEach(function (key) {
458
+ if (ann.evaluatedProps[key]) return;
459
+ var sub = _validate(schema.unevaluatedProperties, instance[key], instancePath + "/" + _escPtr(key), schemaPath + "/unevaluatedProperties", baseUri, ctx, silent);
460
+ if (!sub.failed) ann.evaluatedProps[key] = true;
461
+ else emit("unevaluatedProperties", "unevaluated property '" + key + "' is invalid");
462
+ });
463
+ }
464
+ if (type === "array" && schema.unevaluatedItems !== undefined) {
465
+ for (var ui = 0; ui < instance.length; ui++) {
466
+ if (ann.evaluatedItems[ui]) continue;
467
+ var subi = _validate(schema.unevaluatedItems, instance[ui], instancePath + "/" + ui, schemaPath + "/unevaluatedItems", baseUri, ctx, silent);
468
+ if (!subi.failed) ann.evaluatedItems[ui] = true;
469
+ else emit("unevaluatedItems", "unevaluated item at index " + ui + " is invalid");
470
+ }
471
+ }
472
+
473
+ if (fail()) ann.failed = true;
474
+ return ann;
475
+ }
476
+
477
+ function _typeMatches(typeKw, instance, actual) {
478
+ var list = Array.isArray(typeKw) ? typeKw : [typeKw];
479
+ for (var i = 0; i < list.length; i++) {
480
+ var t = list[i];
481
+ if (t === actual) return true;
482
+ if (t === "integer" && actual === "number" && _isInteger(instance)) return true;
483
+ }
484
+ return false;
485
+ }
486
+
487
+ function _checkNumber(schema, n, emit) {
488
+ if (typeof schema.multipleOf === "number") {
489
+ var q = n / schema.multipleOf;
490
+ if (!isFinite(q) || Math.abs(q - Math.round(q)) > 1e-9 * Math.max(1, Math.abs(q))) { // allow:raw-time-literal — float tolerance for multipleOf
491
+ // Exact check for integers; tolerance only bridges float error.
492
+ if (n % schema.multipleOf !== 0) emit("multipleOf", "value is not a multiple of " + schema.multipleOf);
493
+ }
494
+ }
495
+ if (typeof schema.maximum === "number" && n > schema.maximum) emit("maximum", "value > maximum " + schema.maximum);
496
+ if (typeof schema.exclusiveMaximum === "number" && n >= schema.exclusiveMaximum) emit("exclusiveMaximum", "value >= exclusiveMaximum " + schema.exclusiveMaximum);
497
+ if (typeof schema.minimum === "number" && n < schema.minimum) emit("minimum", "value < minimum " + schema.minimum);
498
+ if (typeof schema.exclusiveMinimum === "number" && n <= schema.exclusiveMinimum) emit("exclusiveMinimum", "value <= exclusiveMinimum " + schema.exclusiveMinimum);
499
+ }
500
+
501
+ function _strLen(s) {
502
+ // Code-point length (not UTF-16 units) per JSON Schema string length.
503
+ var n = 0;
504
+ for (var i = 0; i < s.length; i++) { n++; var c = s.charCodeAt(i); if (c >= 0xD800 && c <= 0xDBFF) i++; }
505
+ return n;
506
+ }
507
+ function _checkString(schema, s, ctx, emit) {
508
+ if (typeof schema.maxLength === "number" && _strLen(s) > schema.maxLength) emit("maxLength", "string longer than maxLength " + schema.maxLength);
509
+ if (typeof schema.minLength === "number" && _strLen(s) < schema.minLength) emit("minLength", "string shorter than minLength " + schema.minLength);
510
+ if (typeof schema.pattern === "string") {
511
+ var re = _compileRegex(schema.pattern, ctx);
512
+ if (re && !re.test(s)) emit("pattern", "string does not match pattern");
513
+ }
514
+ }
515
+
516
+ var _regexCache = {};
517
+ function _compileRegex(pattern, ctx) {
518
+ if (Object.prototype.hasOwnProperty.call(_regexCache, pattern)) return _regexCache[pattern];
519
+ var re = null;
520
+ try { re = new RegExp(pattern, "u"); } // allow:dynamic-regex — JSON Schema pattern is part of the (operator-trusted) schema, not instance data
521
+ catch (_e) {
522
+ try { re = new RegExp(pattern); } catch (_e2) { re = null; }
523
+ }
524
+ _regexCache[pattern] = re;
525
+ return re;
526
+ }
527
+
528
+ function _checkArray(schema, arr, instancePath, schemaPath, baseUri, ctx, silent, ann, emit) {
529
+ if (typeof schema.maxItems === "number" && arr.length > schema.maxItems) emit("maxItems", "array longer than maxItems " + schema.maxItems);
530
+ if (typeof schema.minItems === "number" && arr.length < schema.minItems) emit("minItems", "array shorter than minItems " + schema.minItems);
531
+ if (schema.uniqueItems === true) {
532
+ for (var a = 0; a < arr.length; a++) for (var bI = a + 1; bI < arr.length; bI++) {
533
+ if (_deepEqual(arr[a], arr[bI])) { emit("uniqueItems", "array items are not unique (indices " + a + ", " + bI + ")"); a = arr.length; break; }
534
+ }
535
+ }
536
+ var prefixLen = 0;
537
+ if (Array.isArray(schema.prefixItems)) {
538
+ prefixLen = schema.prefixItems.length;
539
+ for (var pi = 0; pi < prefixLen && pi < arr.length; pi++) {
540
+ var ps = _validate(schema.prefixItems[pi], arr[pi], instancePath + "/" + pi, schemaPath + "/prefixItems/" + pi, baseUri, ctx, silent);
541
+ if (!ps.failed) ann.evaluatedItems[pi] = true; else emit("prefixItems", "item " + pi + " does not match prefixItems schema");
542
+ }
543
+ }
544
+ if (schema.items !== undefined) {
545
+ for (var ii = prefixLen; ii < arr.length; ii++) {
546
+ var is = _validate(schema.items, arr[ii], instancePath + "/" + ii, schemaPath + "/items", baseUri, ctx, silent);
547
+ if (!is.failed) ann.evaluatedItems[ii] = true; else emit("items", "item " + ii + " does not match items schema");
548
+ }
549
+ }
550
+ if (schema.contains !== undefined) {
551
+ var matched = 0;
552
+ for (var ci = 0; ci < arr.length; ci++) {
553
+ var cs = _validate(schema.contains, arr[ci], instancePath + "/" + ci, schemaPath + "/contains", baseUri, ctx, true);
554
+ if (!cs.failed) { matched++; ann.evaluatedItems[ci] = true; }
555
+ }
556
+ var minC = typeof schema.minContains === "number" ? schema.minContains : 1;
557
+ if (matched < minC) emit("contains", "array has " + matched + " matching items, need at least " + minC);
558
+ if (typeof schema.maxContains === "number" && matched > schema.maxContains) emit("maxContains", "array has " + matched + " matching items, more than maxContains " + schema.maxContains);
559
+ }
560
+ }
561
+
562
+ function _checkObject(schema, obj, instancePath, schemaPath, baseUri, ctx, silent, ann, emit) {
563
+ var keys = Object.keys(obj);
564
+ if (typeof schema.maxProperties === "number" && keys.length > schema.maxProperties) emit("maxProperties", "object has more than maxProperties " + schema.maxProperties);
565
+ if (typeof schema.minProperties === "number" && keys.length < schema.minProperties) emit("minProperties", "object has fewer than minProperties " + schema.minProperties);
566
+ if (Array.isArray(schema.required)) {
567
+ schema.required.forEach(function (rk) {
568
+ if (!Object.prototype.hasOwnProperty.call(obj, rk)) emit("required", "missing required property '" + rk + "'");
569
+ });
570
+ }
571
+ if (_isObject(schema.dependentRequired)) {
572
+ Object.keys(schema.dependentRequired).forEach(function (dk) {
573
+ if (Object.prototype.hasOwnProperty.call(obj, dk) && Array.isArray(schema.dependentRequired[dk])) {
574
+ schema.dependentRequired[dk].forEach(function (req) {
575
+ if (!Object.prototype.hasOwnProperty.call(obj, req)) emit("dependentRequired", "property '" + dk + "' requires '" + req + "'");
576
+ });
577
+ }
578
+ });
579
+ }
580
+ if (_isObject(schema.properties)) {
581
+ keys.forEach(function (k) {
582
+ if (Object.prototype.hasOwnProperty.call(schema.properties, k)) {
583
+ var ps = _validate(schema.properties[k], obj[k], instancePath + "/" + _escPtr(k), schemaPath + "/properties/" + _escPtr(k), baseUri, ctx, silent);
584
+ if (!ps.failed) ann.evaluatedProps[k] = true; else ann.failed = true;
585
+ }
586
+ });
587
+ }
588
+ if (_isObject(schema.patternProperties)) {
589
+ Object.keys(schema.patternProperties).forEach(function (pat) {
590
+ var re = _compileRegex(pat, ctx);
591
+ if (!re) return;
592
+ keys.forEach(function (k) {
593
+ if (re.test(k)) {
594
+ var ps = _validate(schema.patternProperties[pat], obj[k], instancePath + "/" + _escPtr(k), schemaPath + "/patternProperties/" + _escPtr(pat), baseUri, ctx, silent);
595
+ if (!ps.failed) ann.evaluatedProps[k] = true; else ann.failed = true;
596
+ }
597
+ });
598
+ });
599
+ }
600
+ if (schema.additionalProperties !== undefined) {
601
+ keys.forEach(function (k) {
602
+ if (ann.evaluatedProps[k]) return;
603
+ // additionalProperties applies to keys not in properties and not
604
+ // matched by patternProperties (regardless of those passing).
605
+ if (_isObject(schema.properties) && Object.prototype.hasOwnProperty.call(schema.properties, k)) return;
606
+ if (_patternMatches(schema.patternProperties, k, ctx)) return;
607
+ var ps = _validate(schema.additionalProperties, obj[k], instancePath + "/" + _escPtr(k), schemaPath + "/additionalProperties", baseUri, ctx, silent);
608
+ if (!ps.failed) ann.evaluatedProps[k] = true; else ann.failed = true;
609
+ });
610
+ }
611
+ if (schema.propertyNames !== undefined) {
612
+ keys.forEach(function (k) {
613
+ var ps = _validate(schema.propertyNames, k, instancePath + "/" + _escPtr(k), schemaPath + "/propertyNames", baseUri, ctx, silent);
614
+ if (ps.failed) emit("propertyNames", "property name '" + k + "' is invalid");
615
+ });
616
+ }
617
+ if (_isObject(schema.dependentSchemas)) {
618
+ Object.keys(schema.dependentSchemas).forEach(function (dk) {
619
+ if (Object.prototype.hasOwnProperty.call(obj, dk)) {
620
+ var ds = _validate(schema.dependentSchemas[dk], obj, instancePath, schemaPath + "/dependentSchemas/" + _escPtr(dk), baseUri, ctx, silent);
621
+ _mergeAnn(ann, ds);
622
+ if (ds.failed) ann.failed = true;
623
+ }
624
+ });
625
+ }
626
+ }
627
+
628
+ function _patternMatches(patternProperties, key, ctx) {
629
+ if (!_isObject(patternProperties)) return false;
630
+ var pats = Object.keys(patternProperties);
631
+ for (var i = 0; i < pats.length; i++) {
632
+ var re = _compileRegex(pats[i], ctx);
633
+ if (re && re.test(key)) return true;
634
+ }
635
+ return false;
636
+ }
637
+
638
+ function _applyLogical(schema, instance, instancePath, schemaPath, baseUri, ctx, silent, ann, emit) {
639
+ if (Array.isArray(schema.allOf)) {
640
+ schema.allOf.forEach(function (sub, i) {
641
+ var r = _validate(sub, instance, instancePath, schemaPath + "/allOf/" + i, baseUri, ctx, silent);
642
+ _mergeAnn(ann, r);
643
+ if (r.failed) emit("allOf", "value does not match allOf[" + i + "]");
644
+ });
645
+ }
646
+ if (Array.isArray(schema.anyOf)) {
647
+ var anyMatched = false;
648
+ schema.anyOf.forEach(function (sub, i) {
649
+ var r = _validate(sub, instance, instancePath, schemaPath + "/anyOf/" + i, baseUri, ctx, true);
650
+ if (!r.failed) { anyMatched = true; _mergeAnn(ann, r); }
651
+ });
652
+ if (!anyMatched) emit("anyOf", "value does not match any anyOf subschema");
653
+ }
654
+ if (Array.isArray(schema.oneOf)) {
655
+ var matchCount = 0;
656
+ schema.oneOf.forEach(function (sub, i) {
657
+ var r = _validate(sub, instance, instancePath, schemaPath + "/oneOf/" + i, baseUri, ctx, true);
658
+ if (!r.failed) { matchCount++; _mergeAnn(ann, r); }
659
+ });
660
+ if (matchCount !== 1) emit("oneOf", "value matches " + matchCount + " oneOf subschemas, expected exactly 1");
661
+ }
662
+ if (schema.not !== undefined) {
663
+ var rn = _validate(schema.not, instance, instancePath, schemaPath + "/not", baseUri, ctx, true);
664
+ if (!rn.failed) emit("not", "value must not match the 'not' subschema");
665
+ }
666
+ if (schema.if !== undefined) {
667
+ var ri = _validate(schema.if, instance, instancePath, schemaPath + "/if", baseUri, ctx, true);
668
+ if (!ri.failed) {
669
+ _mergeAnn(ann, ri); // if's annotations apply only when 'if' validates
670
+ if (schema.then !== undefined) {
671
+ var rt = _validate(schema.then, instance, instancePath, schemaPath + "/then", baseUri, ctx, silent);
672
+ _mergeAnn(ann, rt);
673
+ if (rt.failed) emit("then", "value matches 'if' but not 'then'");
674
+ }
675
+ } else if (schema.else !== undefined) {
676
+ var re2 = _validate(schema.else, instance, instancePath, schemaPath + "/else", baseUri, ctx, silent);
677
+ _mergeAnn(ann, re2);
678
+ if (re2.failed) emit("else", "value does not match 'if' nor 'else'");
679
+ }
680
+ }
681
+ }
682
+
683
+ function _applyDynamicRef(dref, instance, instancePath, schemaPath, baseUri, ctx, silent, ann) {
684
+ var refUri = _resolveUri(dref, baseUri);
685
+ var sf = _splitFragment(refUri);
686
+ var anchorName = sf.fragment;
687
+ // Resolve lexically first (exactly like $ref).
688
+ var target = ctx.registry.resolve(refUri);
689
+ var targetBase = sf.base || baseUri;
690
+ // Dynamic scope resolution applies ONLY when the fragment is a plain-name
691
+ // anchor AND the lexically-resolved target itself carries a matching
692
+ // $dynamicAnchor. Otherwise $dynamicRef behaves like a normal $ref (so a
693
+ // plain $anchor of the same name, or a non-matching/absent $dynamicAnchor,
694
+ // is left as the lexical target).
695
+ var isPlainName = anchorName && anchorName.charAt(0) !== "/" && anchorName !== "";
696
+ if (isPlainName && _isObject(target) && target.$dynamicAnchor === anchorName) {
697
+ for (var i = 0; i < ctx.dynamicScope.length; i++) {
698
+ var frameBase = ctx.dynamicScope[i];
699
+ var cand = ctx.registry.schemas[frameBase + "#" + anchorName];
700
+ if (_isObject(cand) && cand.$dynamicAnchor === anchorName) { target = cand; targetBase = frameBase; break; }
701
+ }
702
+ }
703
+ if (target === undefined) { if (!silent) _err(ctx, instancePath, "$dynamicRef", schemaPath + "/$dynamicRef", "cannot resolve $dynamicRef '" + dref + "'"); ann.failed = true; return; }
704
+ var r = _validate(target, instance, instancePath, schemaPath + "/$dynamicRef", targetBase, ctx, silent);
705
+ _mergeAnn(ann, r);
706
+ if (r.failed) ann.failed = true;
707
+ }
708
+
709
+ function _mergeAnn(into, from) {
710
+ if (!from) return;
711
+ if (from.evaluatedProps) Object.keys(from.evaluatedProps).forEach(function (k) { into.evaluatedProps[k] = true; });
712
+ if (from.evaluatedItems) Object.keys(from.evaluatedItems).forEach(function (k) { into.evaluatedItems[k] = true; });
713
+ }
714
+
715
+ // --- format assertions (opt-in) ---
716
+ function _checkFormat(format, value, type) {
717
+ if (type !== "string") return true; // format only asserts on strings
718
+ switch (format) {
719
+ case "date-time": return rfc3339.isValidDateTime(value);
720
+ // RFC 3339 full-date: shape + real field ranges (reuse the strict
721
+ // date-time validator by anchoring a midnight UTC time).
722
+ case "date": return /^\d{4}-\d{2}-\d{2}$/.test(value) && rfc3339.isValidDateTime(value + "T00:00:00Z"); // allow:regex-no-length-cap — fixed-width date shape
723
+ // RFC 3339 full-time: a mandatory offset + valid ranges, obtained by
724
+ // anchoring an epoch date (rejects "12:00:00" and "25:61:61Z").
725
+ case "time": return rfc3339.isValidDateTime("1970-01-01T" + value);
726
+ // Single "@", non-empty local + domain, no whitespace. The class
727
+ // excludes "@", so the split point is unique — the match is linear.
728
+ case "email": return /^[^@\s]+@[^@\s]+$/.test(value); // allow:regex-no-length-cap — linear (no overlapping quantifiers)
729
+ case "uri": case "iri": {
730
+ if (/\s/.test(value)) return false; // raw whitespace is not a valid URI
731
+ if (/%(?![0-9A-Fa-f]{2})/.test(value)) return false; // malformed percent-escape
732
+ if (!/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) return false; // absolute URI requires a scheme // allow:regex-no-length-cap — linear scheme prefix
733
+ try { new URL(value); return true; } catch (_e) { return false; } // allow:raw-new-url — string-shape check, no fetch / SSRF surface
734
+ }
735
+ case "uuid": return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(value); // allow:regex-no-length-cap — fixed-width UUID
736
+ case "ipv4": return /^(\d{1,3}\.){3}\d{1,3}$/.test(value) && value.split(".").every(function (o) { return Number(o) <= 255; }); // allow:regex-no-length-cap — bounded dotted-quad
737
+ case "regex": try { new RegExp(value); return true; } catch (_e2) { return false; } // allow:dynamic-regex — format:"regex" validates the string IS a regex
738
+ default: return true; // unknown formats are valid (annotation semantics)
739
+ }
740
+ }