@blamejs/core 0.12.63 → 0.12.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +2 -0
- package/index.js +4 -0
- package/lib/base32.js +154 -0
- package/lib/json-schema.js +740 -0
- package/lib/totp.js +10 -31
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.12.x
|
|
10
10
|
|
|
11
|
+
- v0.12.65 (2026-05-26) — **`b.base32` — RFC 4648 Base32 encode / decode.** Encode and decode RFC 4648 Base32 — the case-insensitive alphabet behind TOTP / 2FA secrets, DNSSEC NSEC3 hashes, and human-transcribable identifiers. Both RFC 4648 variants are supported: the standard alphabet (the default) and the extended-hex alphabet. b.base32.encode pads to an 8-character boundary by default (pass padding: false for the bare form TOTP key URIs use); b.base32.decode is strict by default but accepts the real-world shapes humans produce — lower-case, embedded spaces and dashes, missing padding — under loose: true. Verified against the RFC 4648 §10 test vectors for both alphabets. The TOTP primitive now composes this codec instead of carrying its own Base32 implementation. **Added:** *`b.base32.encode` / `b.base32.decode`* — RFC 4648 Base32 codec. `encode(buf, opts)` takes a Buffer or Uint8Array and returns a Base32 string, padded to an 8-character boundary unless `padding: false`; `decode(str, opts)` returns a Buffer. The `variant` option selects the standard (`"rfc4648"`, default) or extended-hex (`"rfc4648-hex"`) alphabet. Decoding is strict by default — any character outside the alphabet throws `Base32Error` — and `loose: true` up-cases the input and ignores embedded spaces, dashes, and missing padding, which is how copied TOTP keys and hand-typed codes arrive. **Changed:** *TOTP composes `b.base32`* — `b.auth.totp` now encodes and decodes its secrets through `b.base32` rather than a private Base32 implementation. Behavior is unchanged — secrets are still emitted unpadded and parsed leniently (case-insensitive, ignoring spaces and dashes).
|
|
12
|
+
|
|
13
|
+
- v0.12.64 (2026-05-25) — **`b.jsonSchema` — JSON Schema 2020-12 validation.** Validate JSON against a JSON Schema 2020-12 document — the dialect OpenAPI 3.1 adopted and the most widely implemented schema language. b.jsonSchema.compile(schema) returns a reusable validator; b.jsonSchema.validate(schema, instance) compiles and runs in one call, returning { valid, errors } where each error names the failing instance location, keyword, and schema path. The full 2020-12 vocabulary is supported: every applicator (allOf / anyOf / oneOf / not / if-then-else, properties / patternProperties / additionalProperties / prefixItems / items / contains), the annotation-aware unevaluatedProperties / unevaluatedItems, every assertion keyword, and reference resolution ($ref / $anchor / $dynamicRef / $dynamicAnchor / $defs / $id base URIs). format is an annotation by default (opt in to assertion with assertFormat). External references resolve through an operator-supplied schema map — never a network fetch. Verified against the official JSON-Schema-Test-Suite (1292 of 1295 draft2020-12 cases; the remainder need the bundled dialect metaschema or $vocabulary selection, both opt-in). This is the standards-track counterpart to the fluent b.safeSchema builder and the portable b.jtd. **Added:** *`b.jsonSchema` — JSON Schema 2020-12* — `compile(schema, opts)` returns `{ validate, isValid }`; `validate(schema, instance, opts)` and `isValid(schema, instance, opts)` compile and run in one call. `validate` returns `{ valid, errors }`, each error a `{ instancePath, keyword, schemaPath, message }`. The full 2020-12 vocabulary is implemented — applicators, annotation-aware `unevaluatedProperties` / `unevaluatedItems`, every assertion keyword, and `$ref` / `$anchor` / `$dynamicRef` / `$dynamicAnchor` / `$defs` / `$id` resolution. `format` is an annotation by default (`assertFormat: true` to assert). External references resolve through `opts.schemas` (a URI→schema map), never a network fetch. Reach for it when the schema is an existing JSON Schema document (an API contract, OpenAPI component, or config schema); `b.safeSchema` remains the fluent in-process builder and `b.jtd` the portable codegen-friendly option. Validating a schema document against the dialect metaschema requires supplying that metaschema via `opts.schemas`, and `$vocabulary`-based keyword selection is not honored (every standard keyword always asserts).
|
|
14
|
+
|
|
11
15
|
- v0.12.63 (2026-05-25) — **`b.cloudEvents` gains the JSON event format, batch, and the HTTP binding.** b.cloudEvents grows beyond wrap / parse into a full CloudEvents 1.0.2 surface. b.cloudEvents.validate / isValid check an envelope against the spec without throwing (the non-throwing companion to parse). toJSON / fromJSON serialize and parse the JSON event format, and toJSONBatch / fromJSONBatch handle the JSON batch format; untrusted bodies parse through the framework's bounded, prototype-pollution-safe JSON reader. The new http.* binding speaks both content modes the spec defines — binary mode spreads context attributes across percent-encoded ce-* headers with the data in the body, structured mode carries the whole event as application/cloudevents+json — plus the batch mode, and http.decode auto-detects the incoming mode from Content-Type exactly as a conformant receiver does. Verified against the spec's normative example events. **Added:** *`b.cloudEvents` JSON event format, batch, and HTTP binding* — `validate` / `isValid` report spec violations without throwing (the non-throwing companion to `parse`). `toJSON` / `fromJSON` and `toJSONBatch` / `fromJSONBatch` serialize and parse the JSON event and batch formats over the existing envelope shape. `http.encodeBinary` / `http.encodeStructured` / `http.encodeBatch` render the three HTTP content modes — binary spreads attributes across percent-encoded `ce-*` headers, structured and batched carry the event(s) as `application/cloudevents+json` / `application/cloudevents-batch+json` — and `http.decode` parses a request back into an envelope (or array) by auto-detecting the mode from `Content-Type`. `b.jtd` or `b.safeSchema` still validate the event's `data` payload. **Fixed:** *`b.csp.build` accepts `fenced-frame-src` and `webrtc`* — The CSP3 `fenced-frame-src` directive — which the default security-headers policy emits to block `<fencedframe>` embeds — was missing from the builder's recognized-directive set, so the default policy could not round-trip through `b.csp.build` (it threw `csp/unknown-directive`). Both `fenced-frame-src` and the CSP3 `webrtc` directive are now recognized.
|
|
12
16
|
|
|
13
17
|
- v0.12.62 (2026-05-26) — **`b.jtd` — JSON Type Definition validation (RFC 8927).** Validate JSON against a JSON Type Definition schema (RFC 8927) — a small, portable, cross-implementation schema language, the interop-friendly companion to the framework's fluent b.safeSchema builder. b.jtd.validate(schema, instance) returns an array of { instancePath, schemaPath } errors (empty = valid); b.jtd.isValid is the boolean form. All eight schema forms are supported — empty, type, enum, elements, properties (with optional / additional properties and nullable), values, discriminator (with mapping), and ref (with definitions) — including the integer-range and RFC 3339 timestamp types. A malformed schema is rejected at compile time with jtd/bad-schema rather than silently mis-validating. Verified against the official json-typedef-spec suites: all 316 validation cases and all 49 invalid-schema cases. **Added:** *`b.jtd.validate(schema, instance)` / `b.jtd.isValid(schema, instance)`* — `validate` returns the RFC 8927 error list — each `{ instancePath, schemaPath }` naming the offending value and the broken schema rule — and `isValid` is the boolean convenience form. Supports every JTD form and type: the numeric types enforce their exact ranges (int8 … uint32, float32 / float64), `timestamp` requires an RFC 3339 date-time, `properties` honours `optionalProperties` / `additionalProperties` / `nullable`, and `discriminator` selects a `mapping` schema by a tag property. The schema is checked for well-formedness before validation, so unknown keywords, multiple forms, bad refs, or a discriminator over a non-properties mapping all throw `jtd/bad-schema`. Use JTD for schemas you share across implementations or generate code from; use `b.safeSchema` for in-process fluent validation.
|
package/README.md
CHANGED
|
@@ -100,6 +100,8 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
100
100
|
- **HPKE / HTTP signatures** — RFC 9180 HPKE with ML-KEM-1024 + HKDF-SHA3-512 + ChaCha20-Poly1305 (`b.crypto.hpke`); RFC 9421 HTTP Message Signatures with derived components and ed25519 / ML-DSA-65 (`b.crypto.httpSig`); RFC 9530 Content-Digest / Repr-Digest body-integrity fields (SHA-256 / SHA-512, legacy algorithms refused — `b.contentDigest`) to sign the digest rather than the whole body
|
|
101
101
|
- **Link header** — RFC 8288 Web Linking codec (`b.linkHeader.parse` / `serialize`): parse and build `Link: <uri>; rel="next"` relations, the standard REST pagination mechanism; quote-aware (a comma inside a quoted parameter never splits the list)
|
|
102
102
|
- **JSON Type Definition** — RFC 8927 validation (`b.jtd.validate` / `isValid`): portable, cross-implementation schema validation (all eight forms — type / enum / elements / properties / values / discriminator / ref / empty), returning instancePath / schemaPath errors; validated against the official 316-case suite. Interop companion to the fluent `b.safeSchema` builder
|
|
103
|
+
- **JSON Schema 2020-12** — the OpenAPI 3.1 dialect (`b.jsonSchema.compile` / `validate` / `isValid`): full vocabulary including every applicator, annotation-aware `unevaluatedProperties` / `unevaluatedItems`, and `$ref` / `$dynamicRef` / `$anchor` / `$id` resolution (external refs via an operator-supplied schema map, never a network fetch); `format` is an annotation unless `assertFormat` is set; returns located `{ valid, errors }`. Validated against the official JSON-Schema-Test-Suite. Standards-track counterpart to `b.safeSchema` and `b.jtd`
|
|
104
|
+
- **Base32** — RFC 4648 codec (`b.base32.encode` / `decode`): standard + extended-hex alphabets, padded or bare, strict or lenient decode (case-insensitive, ignoring spaces / dashes for copied TOTP keys); validated against the RFC 4648 §10 vectors. The codec behind `b.auth.totp` secrets
|
|
103
105
|
- **JSONPath** — full RFC 9535 query evaluator (`b.jsonPath.query` / `paths`): name / wildcard / index / slice / descendant selectors, `?filter` expressions, and the five standard functions, with compile-time well-typedness checks (validated against the official 703-case compliance suite); complements the JSONPath guards
|
|
104
106
|
- **JSON Pointer / Patch** — RFC 6901 `b.jsonPointer.get` (reference a value by `/foo/0/bar`) + RFC 6902 `b.jsonPatch.apply` (atomic add / remove / replace / move / copy / test for HTTP PATCH; the input document is never mutated, structural `test` comparison) + RFC 7396 `b.jsonMergePatch.merge` (the `merge-patch+json` partial-document format; both PATCH formats are prototype-pollution-safe)
|
|
105
107
|
- **Canonical JSON** — RFC 8785 JSON Canonicalization Scheme (`b.canonicalJson.stringifyJcs`): the deterministic, sorted-key byte form to hash or sign (custom credentials, receipts, deterministic request signing); UTF-16 key ordering + ECMAScript number formatting, with a lenient `stringify` variant for Buffers / Dates / BigInts
|
package/index.js
CHANGED
|
@@ -403,6 +403,8 @@ var jsonPatch = require("./lib/json-patch");
|
|
|
403
403
|
var jsonMergePatch = require("./lib/json-merge-patch");
|
|
404
404
|
var jsonPath = require("./lib/json-path");
|
|
405
405
|
var jtd = require("./lib/jtd");
|
|
406
|
+
var jsonSchema = require("./lib/json-schema");
|
|
407
|
+
var base32 = require("./lib/base32");
|
|
406
408
|
var standardWebhooks = require("./lib/standard-webhooks");
|
|
407
409
|
var lro = require("./lib/lro");
|
|
408
410
|
var jsonApi = require("./lib/jsonapi");
|
|
@@ -427,6 +429,8 @@ module.exports = {
|
|
|
427
429
|
jsonMergePatch: jsonMergePatch,
|
|
428
430
|
jsonPath: jsonPath,
|
|
429
431
|
jtd: jtd,
|
|
432
|
+
jsonSchema: jsonSchema,
|
|
433
|
+
base32: base32,
|
|
430
434
|
standardWebhooks: standardWebhooks,
|
|
431
435
|
lro: lro,
|
|
432
436
|
jsonApi: jsonApi,
|
package/lib/base32.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.base32
|
|
4
|
+
* @nav Data
|
|
5
|
+
* @title Base32
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Encode and decode <a href="https://www.rfc-editor.org/rfc/rfc4648">RFC
|
|
9
|
+
* 4648</a> Base32 — the case-insensitive, digit-light alphabet used for
|
|
10
|
+
* TOTP / 2FA secrets, DNSSEC NSEC3 hashes, and human-transcribable
|
|
11
|
+
* identifiers. Both RFC 4648 variants are supported: the standard
|
|
12
|
+
* alphabet (<code>variant: "rfc4648"</code>, default) and the
|
|
13
|
+
* extended-hex alphabet (<code>variant: "rfc4648-hex"</code>, which sorts
|
|
14
|
+
* in the same order as the underlying bytes).
|
|
15
|
+
*
|
|
16
|
+
* <code>encode</code> pads to an 8-character boundary with
|
|
17
|
+
* <code>=</code> by default (pass <code>padding: false</code> for the
|
|
18
|
+
* bare form TOTP key URIs use). <code>decode</code> is strict by default
|
|
19
|
+
* — it rejects any character outside the alphabet — but
|
|
20
|
+
* <code>loose: true</code> accepts the real-world shapes humans produce:
|
|
21
|
+
* lower-case input, embedded spaces and dashes, and missing padding.
|
|
22
|
+
*
|
|
23
|
+
* @card
|
|
24
|
+
* RFC 4648 Base32 encode / decode (standard + extended-hex alphabets,
|
|
25
|
+
* padded or bare, strict or lenient) — the codec behind TOTP secrets and
|
|
26
|
+
* transcribable identifiers.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
var { defineClass } = require("./framework-error");
|
|
30
|
+
|
|
31
|
+
var Base32Error = defineClass("Base32Error", { alwaysPermanent: true });
|
|
32
|
+
|
|
33
|
+
var ALPHABETS = {
|
|
34
|
+
"rfc4648": "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",
|
|
35
|
+
"rfc4648-hex": "0123456789ABCDEFGHIJKLMNOPQRSTUV",
|
|
36
|
+
};
|
|
37
|
+
// Reverse lookups per variant: char-code → 5-bit value.
|
|
38
|
+
var LOOKUPS = {};
|
|
39
|
+
Object.keys(ALPHABETS).forEach(function (v) {
|
|
40
|
+
var map = {};
|
|
41
|
+
for (var i = 0; i < ALPHABETS[v].length; i++) map[ALPHABETS[v].charAt(i)] = i;
|
|
42
|
+
LOOKUPS[v] = map;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
var GROUP = 8; // allow:raw-byte-literal — Base32 emits 8 chars per 5 input bytes (RFC 4648 §6)
|
|
46
|
+
var BITS = 5; // 5 bits per Base32 symbol
|
|
47
|
+
|
|
48
|
+
function _alphabet(variant) {
|
|
49
|
+
var a = ALPHABETS[variant || "rfc4648"];
|
|
50
|
+
if (!a) throw new Base32Error("base32/bad-variant", "base32: variant must be 'rfc4648' or 'rfc4648-hex'");
|
|
51
|
+
return a;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @primitive b.base32.encode
|
|
56
|
+
* @signature b.base32.encode(input, opts?)
|
|
57
|
+
* @since 0.12.65
|
|
58
|
+
* @status stable
|
|
59
|
+
* @related b.base32.decode
|
|
60
|
+
*
|
|
61
|
+
* Encode a Buffer (or Uint8Array) to an RFC 4648 Base32 string. Output is
|
|
62
|
+
* padded to an 8-character boundary with <code>=</code> unless
|
|
63
|
+
* <code>padding: false</code>. The empty input encodes to the empty string.
|
|
64
|
+
*
|
|
65
|
+
* @opts
|
|
66
|
+
* variant: "rfc4648" | "rfc4648-hex", // default: "rfc4648"
|
|
67
|
+
* padding: boolean, // default: true
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* b.base32.encode(Buffer.from("foobar"));
|
|
71
|
+
* // → "MFRGGZDFMZTWQ===="
|
|
72
|
+
*/
|
|
73
|
+
function encode(input, opts) {
|
|
74
|
+
opts = opts || {};
|
|
75
|
+
var buf;
|
|
76
|
+
if (Buffer.isBuffer(input)) buf = input;
|
|
77
|
+
else if (input instanceof Uint8Array) buf = Buffer.from(input);
|
|
78
|
+
else throw new Base32Error("base32/bad-input", "base32.encode: input must be a Buffer or Uint8Array");
|
|
79
|
+
var alphabet = _alphabet(opts.variant);
|
|
80
|
+
var pad = opts.padding !== false;
|
|
81
|
+
|
|
82
|
+
var out = "";
|
|
83
|
+
var value = 0, bits = 0;
|
|
84
|
+
for (var i = 0; i < buf.length; i++) {
|
|
85
|
+
value = (value << 8) | buf[i]; // allow:raw-byte-literal — shift in one input byte
|
|
86
|
+
bits += 8; // allow:raw-byte-literal — eight bits per input byte
|
|
87
|
+
while (bits >= BITS) {
|
|
88
|
+
out += alphabet.charAt((value >>> (bits - BITS)) & 31); // allow:raw-byte-literal — low 5 bits mask (2^5 - 1)
|
|
89
|
+
bits -= BITS;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (bits > 0) out += alphabet.charAt((value << (BITS - bits)) & 31); // allow:raw-byte-literal — final partial group, low 5 bits
|
|
93
|
+
if (pad) while (out.length % GROUP !== 0) out += "=";
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @primitive b.base32.decode
|
|
99
|
+
* @signature b.base32.decode(str, opts?)
|
|
100
|
+
* @since 0.12.65
|
|
101
|
+
* @status stable
|
|
102
|
+
* @related b.base32.encode
|
|
103
|
+
*
|
|
104
|
+
* Decode an RFC 4648 Base32 string to a Buffer. Strict by default: any
|
|
105
|
+
* character outside the variant's alphabet (other than trailing
|
|
106
|
+
* <code>=</code> padding) throws <code>Base32Error</code>. With
|
|
107
|
+
* <code>loose: true</code> the decoder up-cases the input and ignores
|
|
108
|
+
* embedded spaces and dashes (and missing padding) — the shapes TOTP keys
|
|
109
|
+
* and hand-typed codes take.
|
|
110
|
+
*
|
|
111
|
+
* @opts
|
|
112
|
+
* variant: "rfc4648" | "rfc4648-hex", // default: "rfc4648"
|
|
113
|
+
* loose: boolean, // default: false
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* b.base32.decode("MFRGGZDFMZTWQ====").toString();
|
|
117
|
+
* // → "foobar"
|
|
118
|
+
*/
|
|
119
|
+
function decode(str, opts) {
|
|
120
|
+
opts = opts || {};
|
|
121
|
+
if (typeof str !== "string") throw new Base32Error("base32/bad-input", "base32.decode: input must be a string");
|
|
122
|
+
_alphabet(opts.variant);
|
|
123
|
+
var lookup = LOOKUPS[opts.variant || "rfc4648"];
|
|
124
|
+
var loose = opts.loose === true;
|
|
125
|
+
|
|
126
|
+
var bytes = [];
|
|
127
|
+
var value = 0, bits = 0;
|
|
128
|
+
var inPad = false; // once "=" padding starts, only more "=" may follow
|
|
129
|
+
for (var i = 0; i < str.length; i++) {
|
|
130
|
+
var ch = str.charAt(i);
|
|
131
|
+
if (ch === "=") { inPad = true; continue; } // trailing padding
|
|
132
|
+
if (loose && (ch === " " || ch === "-")) continue; // ignore separators
|
|
133
|
+
// A data character after padding is malformed in either mode — the "="
|
|
134
|
+
// run must be trailing (rejects "M=Y======" / "MZXW=6YTB").
|
|
135
|
+
if (inPad) throw new Base32Error("base32/bad-char", "base32.decode: data character '" + ch + "' after padding at index " + i);
|
|
136
|
+
if (loose) ch = ch.toUpperCase();
|
|
137
|
+
var idx = lookup[ch];
|
|
138
|
+
if (idx === undefined) throw new Base32Error("base32/bad-char", "base32.decode: invalid Base32 character '" + str.charAt(i) + "' at index " + i);
|
|
139
|
+
value = (value << BITS) | idx;
|
|
140
|
+
bits += BITS;
|
|
141
|
+
if (bits >= 8) { // allow:raw-byte-literal — emit a full output byte
|
|
142
|
+
bytes.push((value >>> (bits - 8)) & 0xff); // allow:raw-byte-literal — eight-bit output byte mask
|
|
143
|
+
bits -= 8; // allow:raw-byte-literal — consumed eight bits
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return Buffer.from(bytes);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
encode: encode,
|
|
151
|
+
decode: decode,
|
|
152
|
+
ALPHABETS: ALPHABETS,
|
|
153
|
+
Base32Error: Base32Error,
|
|
154
|
+
};
|
|
@@ -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
|
+
}
|
package/lib/totp.js
CHANGED
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
*/
|
|
62
62
|
var nodeCrypto = require("node:crypto");
|
|
63
63
|
var C = require("./constants");
|
|
64
|
+
var base32 = require("./base32");
|
|
64
65
|
var { generateBytes, generateToken, timingSafeEqual } = require("./crypto");
|
|
65
66
|
var { AuthError } = require("./framework-error");
|
|
66
67
|
|
|
@@ -84,45 +85,23 @@ var DEFAULT_SECRET_BYTES = C.BYTES.bytes(128);
|
|
|
84
85
|
var MIN_SECRET_BYTES = 20;
|
|
85
86
|
// HOTP counter is an 8-byte big-endian field per RFC 4226 §5.1.
|
|
86
87
|
var HOTP_COUNTER_BYTES = C.BYTES.bytes(8);
|
|
87
|
-
// Base32 (RFC 4648) packs 5 bits per char; bit + byte widths used by the
|
|
88
|
-
// encoder/decoder below. Routed through C.BYTES so every byte literal in
|
|
89
|
-
// the file lives behind the same helper.
|
|
90
|
-
var BITS_PER_BYTE = C.BYTES.bytes(8);
|
|
91
88
|
|
|
92
89
|
// ---- Base32 (RFC 4648, no padding — TOTP convention) ----
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
// Composes b.base32: secrets are emitted unpadded and parsed leniently
|
|
91
|
+
// (case-insensitive, ignoring the spaces / dashes humans add when copying
|
|
92
|
+
// a key). An invalid character is surfaced as the TOTP-specific error code.
|
|
95
93
|
|
|
96
94
|
function _base32Encode(buf) {
|
|
97
|
-
|
|
98
|
-
for (var i = 0; i < buf.length; i++) {
|
|
99
|
-
bits += buf[i].toString(2).padStart(BITS_PER_BYTE, "0");
|
|
100
|
-
}
|
|
101
|
-
var out = "";
|
|
102
|
-
for (var j = 0; j < bits.length; j += 5) {
|
|
103
|
-
var chunk = bits.substring(j, j + 5).padEnd(5, "0");
|
|
104
|
-
out += BASE32[parseInt(chunk, 2)];
|
|
105
|
-
}
|
|
106
|
-
return out;
|
|
95
|
+
return base32.encode(buf, { padding: false });
|
|
107
96
|
}
|
|
108
97
|
|
|
109
98
|
function _base32Decode(str) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (idx === -1) {
|
|
116
|
-
throw new AuthError("auth-totp/bad-secret",
|
|
117
|
-
"secret contains invalid base32 character: '" + str[i] + "'");
|
|
118
|
-
}
|
|
119
|
-
bits += idx.toString(2).padStart(5, "0");
|
|
120
|
-
}
|
|
121
|
-
var bytes = [];
|
|
122
|
-
for (var j = 0; j + BITS_PER_BYTE <= bits.length; j += BITS_PER_BYTE) {
|
|
123
|
-
bytes.push(parseInt(bits.substring(j, j + BITS_PER_BYTE), 2));
|
|
99
|
+
try {
|
|
100
|
+
return base32.decode(str, { loose: true });
|
|
101
|
+
} catch (e) {
|
|
102
|
+
throw new AuthError("auth-totp/bad-secret",
|
|
103
|
+
"secret contains invalid base32 character" + (e && e.message ? ": " + e.message : ""));
|
|
124
104
|
}
|
|
125
|
-
return Buffer.from(bytes);
|
|
126
105
|
}
|
|
127
106
|
|
|
128
107
|
// ---- Core HOTP (RFC 4226 §5.3) ----
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:9f4770af-2b1a-425d-be19-619f1b638c9e",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-26T08:53:06.102Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.12.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.12.65",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.65",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.12.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.12.65",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.12.
|
|
57
|
+
"ref": "@blamejs/core@0.12.65",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|