@blamejs/core 0.12.60 → 0.12.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -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.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.
12
+
13
+ - v0.12.61 (2026-05-26) — **`b.jsonPath` — JSONPath query (RFC 9535).** A full RFC 9535 JSONPath query evaluator, complementing the framework's JSONPath guards (which screen path strings). b.jsonPath.query(doc, path) compiles a path and returns the matched node values; b.jsonPath.paths returns their normalized locations. The complete surface is implemented: name / wildcard / index / slice selectors, descendant segments (..), filter selectors (?) with comparison and logical operators, relative (@) and absolute ($) embedded queries, and the five standard functions length / count / match / search / value — with the spec's well-typedness rules enforced at compile time so a malformed or ill-typed query is rejected rather than silently mis-evaluated. Descendant walks are node-capped to bound work on hostile input. Verified against all 703 cases of the official jsonpath-compliance-test-suite. **Added:** *`b.jsonPath.query(doc, path)` / `b.jsonPath.paths(doc, path)`* — `query` returns the array of node values selected by an RFC 9535 JSONPath; `paths` returns the normalized-path string of each match (e.g. `$['a'][1]['p']`). Supports every selector (name, wildcard `*`, index incl. negative, slice `start:end:step` incl. negative step, comma-separated selections), child and descendant (`..`) segments, and filter expressions with `==` / `!=` / `<` / `<=` / `>` / `>=`, `&&` / `||` / `!`, existence tests, and the standard functions. The well-typedness rules are checked when the path is compiled — a non-singular query used as a comparison operand, an ill-typed function argument, or a value-typed function used as a test all throw `json-path/invalid`. Pairs with `b.guardJsonPath`, which screens operator-supplied path strings before they reach the evaluator.
14
+
11
15
  - v0.12.60 (2026-05-25) — **`b.jsonMergePatch` — JSON Merge Patch (RFC 7396).** The simpler companion to JSON Patch: b.jsonMergePatch.merge applies an RFC 7396 merge patch (application/merge-patch+json), the partial-document PATCH body. A member present in the patch replaces or — for nested objects — merges into the target, a member whose value is null removes that key, and a patch that is itself an array, scalar, or null replaces the target wholesale. The inputs are never mutated (the merge runs on a deep copy) and member keys are written as literal own properties, so a "__proto__" member cannot reach any prototype. Verified against every RFC 7396 Appendix A test case. **Added:** *`b.jsonMergePatch.merge(target, patch)`* — Applies a JSON Merge Patch to a target document and returns the result without mutating either input. When the patch is an object it overlays the target (a null member deletes the key, a nested object merges recursively, any other value replaces); when the patch is an array, scalar, or null it replaces the whole target. Member keys are set via `Object.defineProperty`, so a `"__proto__"` member becomes a literal own key rather than altering a prototype. Pairs with `b.jsonPatch` — merge patch reads like the resource you want, JSON Patch expresses precise operations (array reordering, literal-null values).
12
16
 
13
17
  - v0.12.58 (2026-05-25) — **`b.jsonPointer` (RFC 6901) + `b.jsonPatch` (RFC 6902) — JSON Pointer + Patch.** Two related JSON primitives. b.jsonPointer.get references a value within a JSON document by RFC 6901 path (/foo/0/bar), handling the ~1 / ~0 escapes and array indices, and refusing pointers that do not resolve. b.jsonPatch.apply applies an RFC 6902 patch — add / remove / replace / move / copy / test — the standard HTTP PATCH payload (application/json-patch+json). It is atomic: operations run against a deep copy, so a failure at any step (an out-of-range index, a missing source, or a failed test) throws and leaves the input document untouched, and test compares values structurally. Verified against the official json-patch/json-patch-tests conformance suite (every enabled result and error case) plus the RFC 6901 §5 pointer examples. **Added:** *`b.jsonPointer.get(doc, pointer)` / `b.jsonPointer.parse(pointer)`* — Resolve an RFC 6901 JSON Pointer against a document — walking object keys and array indices, decoding `~1` → `/` and `~0` → `~`, and returning the whole document for the empty pointer. Throws `json-pointer/not-found` for a missing key, an out-of-range or non-numeric (leading-zero) array index, or descent into a primitive, and `json-pointer/bad-pointer` for a non-`/`-prefixed pointer. `parse` exposes the decoded reference tokens. · *`b.jsonPatch.apply(doc, operations)`* — Apply an RFC 6902 patch and return the result. Supports `add` (insert / append with `-` for arrays, set for objects, whole-document for `""`), `remove`, `replace` (overwrite an existing location), `move`, `copy`, and `test` (structural equality). Atomic — the patch runs on a deep copy, so the input `doc` is never mutated and any failure (unknown op, missing `path` / `value` / `from`, bad index, failed `test`, or moving a location into its own child) throws a typed error. Paths are RFC 6901 pointers resolved through `b.jsonPointer`; suitable for HTTP PATCH endpoints.
package/README.md CHANGED
@@ -99,6 +99,8 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
99
99
  - **Signed webhooks + API encryption** — SLH-DSA-SHAKE-256f default; ML-DSA-65 opt-in; ECIES API encryption (`b.webhook`, `b.crypto`)
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
+ - **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
+ - **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
102
104
  - **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)
103
105
  - **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
104
106
  - **Structured Fields** — full RFC 9651 codec (`b.structuredFields.parse` / `serialize`): Items / Lists / Dictionaries, Inner Lists, Parameters, and every bare-item type (Integer / Decimal / String / Token / Byte Sequence / Boolean / Date / Display String) with strict grammar + range enforcement — the parser behind Content-Digest, Client Hints, and HTTP Message Signatures
package/index.js CHANGED
@@ -401,6 +401,8 @@ var linkHeader = require("./lib/link-header");
401
401
  var jsonPointer = require("./lib/json-pointer");
402
402
  var jsonPatch = require("./lib/json-patch");
403
403
  var jsonMergePatch = require("./lib/json-merge-patch");
404
+ var jsonPath = require("./lib/json-path");
405
+ var jtd = require("./lib/jtd");
404
406
  var standardWebhooks = require("./lib/standard-webhooks");
405
407
  var lro = require("./lib/lro");
406
408
  var jsonApi = require("./lib/jsonapi");
@@ -423,6 +425,8 @@ module.exports = {
423
425
  jsonPointer: jsonPointer,
424
426
  jsonPatch: jsonPatch,
425
427
  jsonMergePatch: jsonMergePatch,
428
+ jsonPath: jsonPath,
429
+ jtd: jtd,
426
430
  standardWebhooks: standardWebhooks,
427
431
  lro: lro,
428
432
  jsonApi: jsonApi,
@@ -0,0 +1,638 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.jsonPath
4
+ * @nav Data
5
+ * @title JSONPath
6
+ *
7
+ * @intro
8
+ * Query a JSON value with JSONPath (RFC 9535) — the standardized path
9
+ * language for selecting nodes from a document
10
+ * (<code>$.store.book[?@.price &lt; 10].title</code>). This is the
11
+ * query evaluator that complements the framework's JSONPath
12
+ * <em>guards</em> (<code>b.guardJsonPath</code>, which screen
13
+ * operator-supplied path strings): <code>query</code> compiles a path
14
+ * and returns the matched values, <code>paths</code> returns their
15
+ * normalized locations.
16
+ *
17
+ * The full RFC 9535 surface is implemented — name / wildcard / index /
18
+ * slice selectors, descendant segments (<code>..</code>), filter
19
+ * selectors (<code>?</code>) with comparison and logical operators and
20
+ * relative (<code>@</code>) / absolute (<code>$</code>) queries, and
21
+ * the five standard functions <code>length</code>, <code>count</code>,
22
+ * <code>match</code>, <code>search</code>, and <code>value</code> — with
23
+ * the spec's well-typedness rules enforced at compile time (a
24
+ * malformed or ill-typed query is rejected, not silently mis-evaluated).
25
+ *
26
+ * @card
27
+ * JSONPath query (RFC 9535) — select nodes from a JSON document with
28
+ * the standard path language: name / wildcard / index / slice /
29
+ * descendant selectors, <code>?filter</code> expressions, and the five
30
+ * standard functions, with compile-time well-typedness checks.
31
+ */
32
+
33
+ var { defineClass } = require("./framework-error");
34
+
35
+ var JsonPathError = defineClass("JsonPathError", { alwaysPermanent: true });
36
+
37
+ var MAX_DESCEND_NODES = 1000000; // allow:raw-byte-literal — DoS ceiling on nodes visited by a descendant walk
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Parser — recursive descent over the RFC 9535 ABNF.
41
+ // ---------------------------------------------------------------------------
42
+
43
+ function _isBlank(c) { return c === " " || c === "\t" || c === "\n" || c === "\r"; }
44
+ function _isDigit(c) { return c >= "0" && c <= "9"; }
45
+ // member-name-shorthand name-first: ALPHA / "_" / non-ASCII.
46
+ function _isNameFirst(c) { var cc = c.charCodeAt(0); return (c >= "A" && c <= "Z") || (c >= "a" && c <= "z") || c === "_" || cc >= 0x80; }
47
+ function _isNameChar(c) { return _isNameFirst(c) || _isDigit(c); }
48
+
49
+ function _Parser(s) { this.s = s; this.i = 0; }
50
+ _Parser.prototype.err = function (msg) { throw new JsonPathError("json-path/invalid", "jsonPath: " + msg + " at index " + this.i); };
51
+ _Parser.prototype.peek = function () { return this.i < this.s.length ? this.s.charAt(this.i) : ""; };
52
+ _Parser.prototype.eat = function (c) { if (this.peek() !== c) this.err("expected '" + c + "'"); this.i += 1; };
53
+ _Parser.prototype.skipBlank = function () { while (_isBlank(this.peek())) this.i += 1; };
54
+
55
+ _Parser.prototype.parseQuery = function () {
56
+ if (this.peek() !== "$") this.err("query must start with '$'");
57
+ this.i += 1;
58
+ var segments = this.parseSegments();
59
+ if (this.i !== this.s.length) this.err("trailing characters");
60
+ return { type: "root", segments: segments };
61
+ };
62
+
63
+ _Parser.prototype.parseSegments = function () {
64
+ var segs = [];
65
+ for (;;) {
66
+ var save = this.i;
67
+ this.skipBlank();
68
+ var c = this.peek();
69
+ if (c === "[") { segs.push({ kind: "child", selectors: this.parseBracket() }); }
70
+ else if (c === ".") {
71
+ if (this.s.charAt(this.i + 1) === ".") { this.i += 2; segs.push(this.parseDescendant()); }
72
+ else { this.i += 1; segs.push({ kind: "child", selectors: [this.parseShorthand()] }); }
73
+ } else { this.i = save; break; }
74
+ }
75
+ return segs;
76
+ };
77
+
78
+ _Parser.prototype.parseDescendant = function () {
79
+ var c = this.peek();
80
+ if (c === "[") return { kind: "descendant", selectors: this.parseBracket() };
81
+ if (c === "*") { this.i += 1; return { kind: "descendant", selectors: [{ type: "wildcard" }] }; }
82
+ return { kind: "descendant", selectors: [this.parseShorthand()] };
83
+ };
84
+
85
+ _Parser.prototype.parseShorthand = function () {
86
+ var c = this.peek();
87
+ if (c === "*") { this.i += 1; return { type: "wildcard" }; }
88
+ if (!_isNameFirst(c)) this.err("invalid member name");
89
+ var start = this.i; this.i += 1;
90
+ while (_isNameChar(this.peek())) this.i += 1;
91
+ return { type: "name", name: this.s.slice(start, this.i) };
92
+ };
93
+
94
+ _Parser.prototype.parseBracket = function () {
95
+ this.eat("[");
96
+ var selectors = [];
97
+ for (;;) {
98
+ this.skipBlank();
99
+ selectors.push(this.parseSelector());
100
+ this.skipBlank();
101
+ var c = this.peek();
102
+ if (c === ",") { this.i += 1; continue; }
103
+ if (c === "]") { this.i += 1; break; }
104
+ this.err("expected ',' or ']' in selection");
105
+ }
106
+ return selectors;
107
+ };
108
+
109
+ _Parser.prototype.parseSelector = function () {
110
+ var c = this.peek();
111
+ if (c === "*") { this.i += 1; return { type: "wildcard" }; }
112
+ if (c === "'" || c === "\"") return { type: "name", name: this.parseStringLiteral() };
113
+ if (c === "?") { this.i += 1; this.skipBlank(); return { type: "filter", expr: this.parseLogicalOr() }; }
114
+ // index or slice
115
+ if (c === ":" || c === "-" || _isDigit(c)) return this.parseIndexOrSlice();
116
+ this.err("invalid selector");
117
+ };
118
+
119
+ _Parser.prototype.parseIntToken = function () {
120
+ var start = this.i;
121
+ if (this.peek() === "-") this.i += 1;
122
+ if (!_isDigit(this.peek())) this.err("expected integer");
123
+ if (this.peek() === "0") {
124
+ this.i += 1;
125
+ if (_isDigit(this.peek())) this.err("leading zero in integer");
126
+ } else {
127
+ while (_isDigit(this.peek())) this.i += 1;
128
+ }
129
+ var txt = this.s.slice(start, this.i);
130
+ if (txt === "-0") this.err("negative zero");
131
+ var n = Number(txt);
132
+ if (!Number.isSafeInteger(n)) this.err("integer out of range");
133
+ return n;
134
+ };
135
+
136
+ _Parser.prototype.parseIndexOrSlice = function () {
137
+ // Detect slice by looking for ':' before ',' or ']' (whitespace-aware).
138
+ var isSlice = false, j = this.i;
139
+ while (j < this.s.length) {
140
+ var ch = this.s.charAt(j);
141
+ if (ch === ":") { isSlice = true; break; }
142
+ if (ch === "," || ch === "]") break;
143
+ if (_isBlank(ch)) { j += 1; continue; }
144
+ if (ch === "-" || _isDigit(ch)) { j += 1; continue; }
145
+ break;
146
+ }
147
+ if (!isSlice) { return { type: "index", index: this.parseIntToken() }; }
148
+ // slice: [start] ":" [end] [ ":" [step] ]
149
+ var start = null, end = null, step = null;
150
+ this.skipBlank();
151
+ if (this.peek() === "-" || _isDigit(this.peek())) start = this.parseIntToken();
152
+ this.skipBlank(); this.eat(":"); this.skipBlank();
153
+ if (this.peek() === "-" || _isDigit(this.peek())) end = this.parseIntToken();
154
+ this.skipBlank();
155
+ if (this.peek() === ":") {
156
+ this.i += 1; this.skipBlank();
157
+ if (this.peek() === "-" || _isDigit(this.peek())) step = this.parseIntToken();
158
+ }
159
+ return { type: "slice", start: start, end: end, step: step };
160
+ };
161
+
162
+ _Parser.prototype.parseStringLiteral = function () {
163
+ var quote = this.peek(); this.i += 1;
164
+ var out = "";
165
+ for (;;) {
166
+ if (this.i >= this.s.length) this.err("unterminated string");
167
+ var c = this.s.charAt(this.i); this.i += 1;
168
+ if (c === quote) return out;
169
+ if (c === "\\") {
170
+ var e = this.s.charAt(this.i); this.i += 1;
171
+ if (e === "n") out += "\n";
172
+ else if (e === "t") out += "\t";
173
+ else if (e === "r") out += "\r";
174
+ else if (e === "b") out += "\b";
175
+ else if (e === "f") out += "\f";
176
+ else if (e === "/") out += "/";
177
+ else if (e === "\\") out += "\\";
178
+ else if (e === quote) out += quote;
179
+ else if (e === "u") {
180
+ var hex = this.s.substr(this.i, 4);
181
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) this.err("invalid \\u escape");
182
+ var cp = parseInt(hex, 16); this.i += 4; // allow:raw-byte-literal — base-16 radix for \uXXXX
183
+ // Surrogate pair handling.
184
+ if (cp >= 0xD800 && cp <= 0xDBFF && this.s.substr(this.i, 2) === "\\u") {
185
+ var hex2 = this.s.substr(this.i + 2, 4);
186
+ if (/^[0-9a-fA-F]{4}$/.test(hex2)) {
187
+ var lo = parseInt(hex2, 16); // allow:raw-byte-literal — base-16 radix for \uXXXX low surrogate
188
+ if (lo >= 0xDC00 && lo <= 0xDFFF) { out += String.fromCharCode(cp, lo); this.i += 6; continue; }
189
+ }
190
+ this.err("invalid surrogate pair");
191
+ }
192
+ if (cp >= 0xD800 && cp <= 0xDFFF) this.err("lone surrogate in string");
193
+ out += String.fromCharCode(cp);
194
+ } else this.err("invalid escape");
195
+ } else {
196
+ var cc = c.charCodeAt(0);
197
+ if (cc <= 0x1f) this.err("unescaped control character in string");
198
+ // RFC 9535: a literal of the SAME quote must be escaped; the other
199
+ // quote is allowed literally (handled by the quote check above).
200
+ out += c;
201
+ }
202
+ }
203
+ };
204
+
205
+ // --- filter expression grammar ---
206
+ // Each node carries a `vtype`: "value" (ValueType), "logical" (LogicalType),
207
+ // or "nodes" (NodesType) for well-typedness checks.
208
+
209
+ _Parser.prototype.parseLogicalOr = function () {
210
+ var left = this.parseLogicalAnd();
211
+ for (;;) {
212
+ this.skipBlank();
213
+ if (this.s.substr(this.i, 2) === "||") { this.i += 2; this.skipBlank(); var r = this.parseLogicalAnd(); left = { type: "or", a: left, b: r, vtype: "logical" }; }
214
+ else break;
215
+ }
216
+ return left;
217
+ };
218
+ _Parser.prototype.parseLogicalAnd = function () {
219
+ var left = this.parseBasic();
220
+ for (;;) {
221
+ this.skipBlank();
222
+ if (this.s.substr(this.i, 2) === "&&") { this.i += 2; this.skipBlank(); var r = this.parseBasic(); left = { type: "and", a: left, b: r, vtype: "logical" }; }
223
+ else break;
224
+ }
225
+ return left;
226
+ };
227
+ _Parser.prototype.parseBasic = function () {
228
+ this.skipBlank();
229
+ if (this.peek() === "!") { this.i += 1; this.skipBlank(); var inner = this.parseBasic(); this._requireTestable(inner); return { type: "not", e: inner, vtype: "logical" }; }
230
+ if (this.peek() === "(") {
231
+ this.i += 1; this.skipBlank(); var e = this.parseLogicalOr(); this.skipBlank(); this.eat(")");
232
+ return e;
233
+ }
234
+ // comparison or test
235
+ var first = this.parseComparableOrQuery();
236
+ this.skipBlank();
237
+ var op = this._peekCompareOp();
238
+ if (op) {
239
+ this.i += op.length; this.skipBlank();
240
+ var second = this.parseComparableOrQuery();
241
+ this._requireComparable(first); this._requireComparable(second);
242
+ return { type: "compare", op: op, a: first, b: second, vtype: "logical" };
243
+ }
244
+ // test-expr: a query (existence) or a LogicalType function
245
+ this._requireTestable(first);
246
+ return first;
247
+ };
248
+
249
+ _Parser.prototype._peekCompareOp = function () {
250
+ var two = this.s.substr(this.i, 2);
251
+ if (two === "==" || two === "!=" || two === "<=" || two === ">=") return two;
252
+ var one = this.peek();
253
+ if (one === "<" || one === ">") return one;
254
+ return null;
255
+ };
256
+
257
+ // A comparable: literal / singular-query / function-expr. A query here may
258
+ // be non-singular (NodesType) when used as a test; the caller checks type.
259
+ _Parser.prototype.parseComparableOrQuery = function () {
260
+ var c = this.peek();
261
+ if (c === "'" || c === "\"") return { type: "lit", value: this.parseStringLiteral(), vtype: "value" };
262
+ if (c === "$" || c === "@") return this.parseFilterQuery();
263
+ if (_isDigit(c) || c === "-") return { type: "lit", value: this.parseNumber(), vtype: "value" };
264
+ if (this.s.substr(this.i, 4) === "true") { this.i += 4; return { type: "lit", value: true, vtype: "value" }; }
265
+ if (this.s.substr(this.i, 5) === "false") { this.i += 5; return { type: "lit", value: false, vtype: "value" }; }
266
+ if (this.s.substr(this.i, 4) === "null") { this.i += 4; return { type: "lit", value: null, vtype: "value" }; }
267
+ // function call
268
+ if (/^[a-z]/.test(c)) return this.parseFunction();
269
+ this.err("expected a comparable or query in filter");
270
+ };
271
+
272
+ _Parser.prototype.parseNumber = function () {
273
+ var start = this.i;
274
+ if (this.peek() === "-") this.i += 1;
275
+ if (this.peek() === "0") this.i += 1;
276
+ else { if (!_isDigit(this.peek())) this.err("invalid number"); while (_isDigit(this.peek())) this.i += 1; }
277
+ if (this.peek() === ".") { this.i += 1; if (!_isDigit(this.peek())) this.err("invalid fraction"); while (_isDigit(this.peek())) this.i += 1; }
278
+ if (this.peek() === "e" || this.peek() === "E") {
279
+ this.i += 1; if (this.peek() === "+" || this.peek() === "-") this.i += 1;
280
+ if (!_isDigit(this.peek())) this.err("invalid exponent"); while (_isDigit(this.peek())) this.i += 1;
281
+ }
282
+ return Number(this.s.slice(start, this.i));
283
+ };
284
+
285
+ _Parser.prototype.parseFilterQuery = function () {
286
+ var rootChar = this.peek(); this.i += 1; // $ or @
287
+ var segments = this.parseSegments();
288
+ var singular = segments.every(function (seg) {
289
+ return seg.kind === "child" && seg.selectors.length === 1 &&
290
+ (seg.selectors[0].type === "name" || seg.selectors[0].type === "index");
291
+ });
292
+ return { type: "query", root: rootChar, segments: segments, singular: singular, vtype: singular ? "value" : "nodes" };
293
+ };
294
+
295
+ var FUNCTIONS = {
296
+ length: { params: ["value"], ret: "value" },
297
+ count: { params: ["nodes"], ret: "value" },
298
+ value: { params: ["nodes"], ret: "value" },
299
+ match: { params: ["value", "value"], ret: "logical" },
300
+ search: { params: ["value", "value"], ret: "logical" },
301
+ };
302
+
303
+ _Parser.prototype.parseFunction = function () {
304
+ var start = this.i;
305
+ while (/[a-z]/.test(this.peek()) || this.peek() === "_" || _isDigit(this.peek())) this.i += 1;
306
+ var name = this.s.slice(start, this.i);
307
+ if (!Object.prototype.hasOwnProperty.call(FUNCTIONS, name)) this.err("unknown function '" + name + "'");
308
+ var spec = FUNCTIONS[name];
309
+ this.eat("(");
310
+ var args = [];
311
+ this.skipBlank();
312
+ if (this.peek() !== ")") {
313
+ for (;;) {
314
+ this.skipBlank();
315
+ args.push(this.parseFunctionArg());
316
+ this.skipBlank();
317
+ if (this.peek() === ",") { this.i += 1; continue; }
318
+ break;
319
+ }
320
+ }
321
+ this.skipBlank(); this.eat(")");
322
+ if (args.length !== spec.params.length) this.err("function '" + name + "' expects " + spec.params.length + " argument(s)");
323
+ for (var k = 0; k < args.length; k++) {
324
+ if (!_argMatches(spec.params[k], args[k])) this.err("function '" + name + "' argument " + (k + 1) + " type mismatch");
325
+ }
326
+ return { type: "func", name: name, args: args, vtype: spec.ret };
327
+ };
328
+
329
+ _Parser.prototype.parseFunctionArg = function () {
330
+ var c = this.peek();
331
+ if (c === "'" || c === "\"") return { type: "lit", value: this.parseStringLiteral(), vtype: "value" };
332
+ if (c === "$" || c === "@") return this.parseFilterQuery();
333
+ if (_isDigit(c) || c === "-") return { type: "lit", value: this.parseNumber(), vtype: "value" };
334
+ if (this.s.substr(this.i, 4) === "true") { this.i += 4; return { type: "lit", value: true, vtype: "value" }; }
335
+ if (this.s.substr(this.i, 5) === "false") { this.i += 5; return { type: "lit", value: false, vtype: "value" }; }
336
+ if (this.s.substr(this.i, 4) === "null") { this.i += 4; return { type: "lit", value: null, vtype: "value" }; }
337
+ if (/^[a-z]/.test(c)) return this.parseFunction();
338
+ if (c === "!" || c === "(") return this.parseLogicalOr(); // logical arg (none of the std funcs take it, caught by type check)
339
+ this.err("invalid function argument");
340
+ };
341
+
342
+ // A function parameter of declared type accepts: value←value-typed arg
343
+ // (literal / singular query / value-returning function); nodes←any query
344
+ // or nodes-returning function; logical←logical expr or logical function.
345
+ function _argMatches(param, arg) {
346
+ if (param === "value") return arg.vtype === "value" || (arg.type === "query" && arg.singular);
347
+ if (param === "nodes") return arg.type === "query" || (arg.type === "func" && arg.vtype === "nodes");
348
+ if (param === "logical") return arg.vtype === "logical";
349
+ return false;
350
+ }
351
+
352
+ _Parser.prototype._requireComparable = function (node) {
353
+ // Comparables must be ValueType: a literal, a SINGULAR query, or a
354
+ // value-returning function. A non-singular query or logical/nodes
355
+ // function is ill-typed.
356
+ if (node.type === "lit") return;
357
+ if (node.type === "query") { if (!node.singular) this.err("non-singular query is not comparable"); return; }
358
+ if (node.type === "func") { if (node.vtype !== "value") this.err("function '" + node.name + "' is not comparable (not ValueType)"); return; }
359
+ this.err("operand is not comparable");
360
+ };
361
+ _Parser.prototype._requireTestable = function (node) {
362
+ // A test-expr is a query (existence) or a LogicalType function; a
363
+ // ValueType function (length/count/value) is NOT a valid test.
364
+ if (node.type === "query") return;
365
+ if (node.type === "func") { if (node.vtype !== "logical") this.err("function '" + node.name + "' is not a valid test (not LogicalType)"); return; }
366
+ if (node.vtype === "logical") return;
367
+ this.err("expression is not a valid test");
368
+ };
369
+
370
+ function _parse(path) {
371
+ if (typeof path !== "string") throw new JsonPathError("json-path/bad-arg", "jsonPath: path must be a string");
372
+ return new _Parser(path).parseQuery();
373
+ }
374
+
375
+ // ---------------------------------------------------------------------------
376
+ // Evaluator — produces a nodelist of { value, path: [tokens] }.
377
+ // ---------------------------------------------------------------------------
378
+
379
+ function _isObject(v) { return v !== null && typeof v === "object" && !Array.isArray(v); }
380
+
381
+ function _applySelector(sel, node, root, out) {
382
+ var v = node.value;
383
+ if (sel.type === "name") {
384
+ if (_isObject(v) && Object.prototype.hasOwnProperty.call(v, sel.name)) out.push({ value: v[sel.name], path: node.path.concat(sel.name) });
385
+ } else if (sel.type === "wildcard") {
386
+ if (Array.isArray(v)) { for (var i = 0; i < v.length; i++) out.push({ value: v[i], path: node.path.concat(i) }); }
387
+ else if (_isObject(v)) { Object.keys(v).forEach(function (k) { out.push({ value: v[k], path: node.path.concat(k) }); }); }
388
+ } else if (sel.type === "index") {
389
+ if (Array.isArray(v)) { var idx = sel.index < 0 ? v.length + sel.index : sel.index; if (idx >= 0 && idx < v.length) out.push({ value: v[idx], path: node.path.concat(idx) }); }
390
+ } else if (sel.type === "slice") {
391
+ if (Array.isArray(v)) _applySlice(sel, v, node, out);
392
+ } else if (sel.type === "filter") {
393
+ var items = Array.isArray(v) ? v.map(function (e, i) { return { value: e, path: node.path.concat(i) }; })
394
+ : _isObject(v) ? Object.keys(v).map(function (k) { return { value: v[k], path: node.path.concat(k) }; }) : [];
395
+ items.forEach(function (it) { if (_truthy(_evalLogical(sel.expr, it, root))) out.push(it); });
396
+ }
397
+ }
398
+
399
+ function _applySlice(sel, arr, node, out) {
400
+ var len = arr.length;
401
+ var step = sel.step === null ? 1 : sel.step;
402
+ if (step === 0) return;
403
+ var lower, upper, start, end;
404
+ if (step > 0) {
405
+ start = sel.start === null ? 0 : sel.start;
406
+ end = sel.end === null ? len : sel.end;
407
+ lower = start < 0 ? Math.max(len + start, 0) : Math.min(start, len);
408
+ upper = end < 0 ? Math.max(len + end, 0) : Math.min(end, len);
409
+ for (var i = lower; i < upper; i += step) out.push({ value: arr[i], path: node.path.concat(i) });
410
+ } else {
411
+ start = sel.start === null ? len - 1 : sel.start;
412
+ end = sel.end === null ? -len - 1 : sel.end;
413
+ lower = start < 0 ? Math.max(len + start, -1) : Math.min(start, len - 1);
414
+ upper = end < 0 ? Math.max(len + end, -1) : Math.min(end, len - 1);
415
+ for (var j = lower; j > upper; j += step) out.push({ value: arr[j], path: node.path.concat(j) });
416
+ }
417
+ }
418
+
419
+ function _descend(node, acc, budget) {
420
+ acc.push(node);
421
+ if (budget.n++ > MAX_DESCEND_NODES) throw new JsonPathError("json-path/too-large", "jsonPath: descendant walk exceeded the node cap");
422
+ var v = node.value;
423
+ if (Array.isArray(v)) { for (var i = 0; i < v.length; i++) _descend({ value: v[i], path: node.path.concat(i) }, acc, budget); }
424
+ else if (_isObject(v)) { Object.keys(v).forEach(function (k) { _descend({ value: v[k], path: node.path.concat(k) }, acc, budget); }); }
425
+ }
426
+
427
+ function _evalSegments(segments, root) {
428
+ var nodes = [{ value: root, path: [] }];
429
+ for (var s = 0; s < segments.length; s++) {
430
+ var seg = segments[s];
431
+ var next = [];
432
+ var base = nodes;
433
+ if (seg.kind === "descendant") {
434
+ var acc = []; var budget = { n: 0 };
435
+ nodes.forEach(function (nd) { _descend(nd, acc, budget); });
436
+ base = acc;
437
+ }
438
+ base.forEach(function (nd) { seg.selectors.forEach(function (sel) { _applySelector(sel, nd, root, next); }); });
439
+ nodes = next;
440
+ }
441
+ return nodes;
442
+ }
443
+
444
+ // --- filter evaluation ---
445
+ var NOTHING = { __nothing: true }; // the "Nothing" value (missing node)
446
+
447
+ function _singularValue(q, current, root) {
448
+ var node = q.root === "$" ? root : current.value;
449
+ for (var i = 0; i < q.segments.length; i++) {
450
+ var sel = q.segments[i].selectors[0];
451
+ if (sel.type === "name") {
452
+ if (!_isObject(node) || !Object.prototype.hasOwnProperty.call(node, sel.name)) return NOTHING;
453
+ node = node[sel.name];
454
+ } else { // index
455
+ if (!Array.isArray(node)) return NOTHING;
456
+ var idx = sel.index < 0 ? node.length + sel.index : sel.index;
457
+ if (idx < 0 || idx >= node.length) return NOTHING;
458
+ node = node[idx];
459
+ }
460
+ }
461
+ return node;
462
+ }
463
+
464
+ function _queryNodes(q, current, root) {
465
+ var startVal = q.root === "$" ? root : current.value;
466
+ return _evalSegments(q.segments, startVal);
467
+ }
468
+
469
+ function _evalComparable(node, current, root) {
470
+ if (node.type === "lit") return node.value;
471
+ if (node.type === "query") return _singularValue(node, current, root);
472
+ if (node.type === "func") return _evalFunctionValue(node, current, root);
473
+ return NOTHING;
474
+ }
475
+
476
+ function _deepEqual(a, b) {
477
+ if (a === b) return true;
478
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return a === b;
479
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
480
+ if (Array.isArray(a)) { if (a.length !== b.length) return false; for (var i = 0; i < a.length; i++) if (!_deepEqual(a[i], b[i])) return false; return true; }
481
+ var ka = Object.keys(a), kb = Object.keys(b);
482
+ if (ka.length !== kb.length) return false;
483
+ for (var j = 0; j < ka.length; j++) { if (!Object.prototype.hasOwnProperty.call(b, ka[j]) || !_deepEqual(a[ka[j]], b[ka[j]])) return false; }
484
+ return true;
485
+ }
486
+
487
+ function _equals(a, b) {
488
+ var aN = a === NOTHING, bN = b === NOTHING;
489
+ if (aN || bN) return aN && bN; // Nothing equals only Nothing
490
+ return _deepEqual(a, b);
491
+ }
492
+ // Strict ordering "a < b": defined only for two numbers or two strings
493
+ // (RFC 9535 §2.3.5.2.2); anything else (incl. Nothing) is not <.
494
+ function _less(a, b) {
495
+ if (a === NOTHING || b === NOTHING) return false;
496
+ var comparable = (typeof a === "number" && typeof b === "number") || (typeof a === "string" && typeof b === "string");
497
+ if (!comparable) return false;
498
+ return a < b;
499
+ }
500
+ function _compare(op, a, b) {
501
+ switch (op) {
502
+ case "==": return _equals(a, b);
503
+ case "!=": return !_equals(a, b);
504
+ case "<": return _less(a, b);
505
+ case ">": return _less(b, a);
506
+ case "<=": return _less(a, b) || _equals(a, b); // §2.3.5.2.2: <= is < OR ==
507
+ case ">=": return _less(b, a) || _equals(a, b);
508
+ default: return false;
509
+ }
510
+ }
511
+
512
+ function _truthy(x) { return x === true; }
513
+
514
+ function _evalLogical(node, current, root) {
515
+ switch (node.type) {
516
+ case "or": return _evalLogical(node.a, current, root) || _evalLogical(node.b, current, root);
517
+ case "and": return _evalLogical(node.a, current, root) && _evalLogical(node.b, current, root);
518
+ case "not": return !_evalLogical(node.e, current, root);
519
+ case "compare": return _compare(node.op, _evalComparable(node.a, current, root), _evalComparable(node.b, current, root));
520
+ case "query": return _queryNodes(node, current, root).length > 0; // existence test
521
+ case "func": return _evalFunctionLogical(node, current, root);
522
+ default: return false;
523
+ }
524
+ }
525
+
526
+ // --- standard functions ---
527
+ function _funcArgValue(arg, current, root) {
528
+ if (arg.type === "lit") return arg.value;
529
+ if (arg.type === "query") return _singularValue(arg, current, root); // ValueType from singular query
530
+ if (arg.type === "func") return _evalFunctionValue(arg, current, root);
531
+ return NOTHING;
532
+ }
533
+ function _funcArgNodes(arg, current, root) {
534
+ if (arg.type === "query") return _queryNodes(arg, current, root);
535
+ if (arg.type === "func") { var v = _evalFunctionValue(arg, current, root); return v === NOTHING ? [] : [{ value: v, path: [] }]; }
536
+ return [];
537
+ }
538
+
539
+ function _evalFunctionValue(node, current, root) {
540
+ if (node.name === "length") {
541
+ var v = _funcArgValue(node.args[0], current, root);
542
+ if (v === NOTHING) return NOTHING; // length(Nothing) = Nothing (the sentinel is itself an object)
543
+ if (typeof v === "string") return Array.from(v).length;
544
+ if (Array.isArray(v)) return v.length;
545
+ if (_isObject(v)) return Object.keys(v).length;
546
+ return NOTHING;
547
+ }
548
+ if (node.name === "count") return _funcArgNodes(node.args[0], current, root).length;
549
+ if (node.name === "value") { var ns = _funcArgNodes(node.args[0], current, root); return ns.length === 1 ? ns[0].value : NOTHING; }
550
+ return NOTHING;
551
+ }
552
+
553
+ function _iRegexpToJs(pattern, anchored) {
554
+ // I-Regexp (RFC 9485) is close to a JS regex subset. Translate the one
555
+ // systematic difference — "." must not match line separators — and
556
+ // (for match) anchor the whole input.
557
+ var translated = pattern.replace(/(\\.)|(\[(?:\\.|[^\]\\])*\])|\./g, function (m, esc, cls) {
558
+ if (esc) return esc;
559
+ if (cls) return cls;
560
+ return "[^\\n\\r]";
561
+ });
562
+ // The pattern is the I-Regexp argument of an RFC 9535 match()/search()
563
+ // filter — translated (not raw) and used only as a boolean test.
564
+ return new RegExp(anchored ? "^(?:" + translated + ")$" : translated, "su"); // allow:dynamic-regex — translated I-Regexp from a match()/search() filter argument
565
+ }
566
+ function _evalFunctionLogical(node, current, root) {
567
+ var input = _funcArgValue(node.args[0], current, root);
568
+ var pat = _funcArgValue(node.args[1], current, root);
569
+ if (typeof input !== "string" || typeof pat !== "string") return false;
570
+ var re;
571
+ try { re = _iRegexpToJs(pat, node.name === "match"); } catch (_e) { return false; }
572
+ return re.test(input);
573
+ }
574
+
575
+ /**
576
+ * @primitive b.jsonPath.query
577
+ * @signature b.jsonPath.query(doc, path)
578
+ * @since 0.12.61
579
+ * @status stable
580
+ * @compliance soc2
581
+ * @related b.jsonPath.paths, b.guardJsonPath.gate
582
+ *
583
+ * Evaluate an RFC 9535 JSONPath query against a JSON value and return the
584
+ * array of matched node values (the nodelist, in document order). The
585
+ * full path language is supported — name / wildcard / index / slice
586
+ * selectors, descendant segments (<code>..</code>), and filter
587
+ * selectors (<code>?</code>) with comparisons, <code>&&</code> /
588
+ * <code>||</code> / <code>!</code>, relative (<code>@</code>) and
589
+ * absolute (<code>$</code>) queries, and the functions
590
+ * <code>length</code> / <code>count</code> / <code>match</code> /
591
+ * <code>search</code> / <code>value</code>. A malformed or ill-typed
592
+ * query throws <code>json-path/invalid</code>.
593
+ *
594
+ * @example
595
+ * b.jsonPath.query({ a: [{ p: 1 }, { p: 9 }] }, "$.a[?@.p > 5].p");
596
+ * // → [9]
597
+ */
598
+ function query(doc, path) {
599
+ var ast = _parse(path);
600
+ return _evalSegments(ast.segments, doc).map(function (n) { return n.value; });
601
+ }
602
+
603
+ function _normalizedPath(tokens) {
604
+ var out = "$";
605
+ for (var i = 0; i < tokens.length; i++) {
606
+ var t = tokens[i];
607
+ if (typeof t === "number") out += "[" + t + "]";
608
+ else out += "['" + String(t).replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "']";
609
+ }
610
+ return out;
611
+ }
612
+
613
+ /**
614
+ * @primitive b.jsonPath.paths
615
+ * @signature b.jsonPath.paths(doc, path)
616
+ * @since 0.12.61
617
+ * @status stable
618
+ * @related b.jsonPath.query
619
+ *
620
+ * Like <code>query</code>, but returns the normalized-path location of
621
+ * each match (RFC 9535 §2.7 normalized paths, e.g.
622
+ * <code>$['a'][1]['p']</code>) instead of the values — useful for
623
+ * reporting or for building a follow-up patch.
624
+ *
625
+ * @example
626
+ * b.jsonPath.paths({ a: [{ p: 1 }, { p: 9 }] }, "$.a[?@.p > 5].p");
627
+ * // → ["$['a'][1]['p']"]
628
+ */
629
+ function paths(doc, path) {
630
+ var ast = _parse(path);
631
+ return _evalSegments(ast.segments, doc).map(function (n) { return _normalizedPath(n.path); });
632
+ }
633
+
634
+ module.exports = {
635
+ query: query,
636
+ paths: paths,
637
+ JsonPathError: JsonPathError,
638
+ };
package/lib/jtd.js ADDED
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.jtd
4
+ * @nav Data
5
+ * @title JSON Type Definition
6
+ *
7
+ * @intro
8
+ * Validate a JSON value against a JSON Type Definition schema (RFC
9
+ * 8927) — a small, portable, cross-implementation schema language.
10
+ * Unlike the framework's fluent <code>b.safeSchema</code> builder, a
11
+ * JTD schema is plain JSON you can share with any JTD implementation
12
+ * (and generate code from), which makes it the right choice for
13
+ * interop contracts.
14
+ *
15
+ * <code>validate(schema, instance)</code> returns an array of errors,
16
+ * each a <code>{ instancePath, schemaPath }</code> pair pointing at the
17
+ * offending value and the schema rule it broke (an empty array means
18
+ * valid). All eight schema forms are supported — empty, <code>type</code>,
19
+ * <code>enum</code>, <code>elements</code>, <code>properties</code>,
20
+ * <code>values</code>, <code>discriminator</code>, and <code>ref</code>
21
+ * (with <code>definitions</code>) — and a malformed schema is rejected
22
+ * at compile time rather than mis-validating.
23
+ *
24
+ * @card
25
+ * JSON Type Definition (RFC 8927) — validate JSON against a portable,
26
+ * standardized schema (all eight forms), returning instancePath /
27
+ * schemaPath errors. The interop-friendly companion to the fluent
28
+ * <code>b.safeSchema</code> builder.
29
+ */
30
+
31
+ var { defineClass } = require("./framework-error");
32
+
33
+ var JtdError = defineClass("JtdError", { alwaysPermanent: true });
34
+
35
+ var MAX_DEPTH = 10000; // allow:raw-time-literal — recursion cap for self-referential refs
36
+
37
+ var TYPES = {
38
+ boolean: 1, string: 1, timestamp: 1, float32: 1, float64: 1,
39
+ int8: 1, uint8: 1, int16: 1, uint16: 1, int32: 1, uint32: 1,
40
+ };
41
+ var INT_RANGES = {
42
+ int8: [-128, 127], uint8: [0, 255], int16: [-32768, 32767], // allow:raw-byte-literal — RFC 8927 integer type bounds
43
+ uint16: [0, 65535], int32: [-2147483648, 2147483647], uint32: [0, 4294967295], // allow:raw-byte-literal — RFC 8927 integer type bounds
44
+ };
45
+ var FORM_KEYWORDS = ["ref", "type", "enum", "elements", "properties", "optionalProperties", "values", "discriminator"];
46
+ var SHARED_KEYWORDS = { definitions: 1, nullable: 1, metadata: 1 };
47
+
48
+ function _isPlainObject(v) { return v !== null && typeof v === "object" && !Array.isArray(v); }
49
+ function _isInteger(v) { return typeof v === "number" && isFinite(v) && Math.floor(v) === v; }
50
+
51
+ // RFC 3339 date-time (the JTD "timestamp" type).
52
+ var RFC3339 = /^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
53
+ function _validTimestamp(s) {
54
+ var m = RFC3339.exec(s);
55
+ if (!m) return false;
56
+ var mo = +m[2], d = +m[3], h = +m[4], mi = +m[5], se = +m[6];
57
+ if (mo < 1 || mo > 12 || d < 1 || d > 31 || h > 23 || mi > 59 || se > 60) return false; // allow:raw-time-literal — RFC 3339 field ranges (60 = leap second)
58
+ var days = [31, ((+m[1] % 4 === 0 && +m[1] % 100 !== 0) || +m[1] % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; // allow:raw-time-literal — days per month
59
+ if (d > days[mo - 1]) return false;
60
+ var tz = m[8];
61
+ if (tz !== "Z" && tz !== "z") { // numeric offset must be in range
62
+ if (+tz.slice(1, 3) > 23 || +tz.slice(4, 6) > 59) return false; // allow:raw-time-literal — RFC 3339 offset hour/minute ranges
63
+ }
64
+ return true;
65
+ }
66
+
67
+ // --- compile-time well-formedness (RFC 8927 section 2.2) ---
68
+ function _checkSchema(schema, root, isRoot) {
69
+ if (!_isPlainObject(schema)) throw new JtdError("jtd/bad-schema", "jtd: schema must be an object");
70
+ if (!isRoot && Object.prototype.hasOwnProperty.call(schema, "definitions")) throw new JtdError("jtd/bad-schema", "jtd: 'definitions' is allowed only at the root");
71
+ Object.keys(schema).forEach(function (k) {
72
+ if (FORM_KEYWORDS.indexOf(k) === -1 && !SHARED_KEYWORDS[k] && k !== "additionalProperties" && k !== "mapping") throw new JtdError("jtd/bad-schema", "jtd: unknown keyword '" + k + "'");
73
+ });
74
+ if (Object.prototype.hasOwnProperty.call(schema, "nullable") && typeof schema.nullable !== "boolean") throw new JtdError("jtd/bad-schema", "jtd: 'nullable' must be a boolean");
75
+ if (Object.prototype.hasOwnProperty.call(schema, "metadata") && !_isPlainObject(schema.metadata)) throw new JtdError("jtd/bad-schema", "jtd: 'metadata' must be an object");
76
+ if (Object.prototype.hasOwnProperty.call(schema, "definitions")) {
77
+ if (!_isPlainObject(schema.definitions)) throw new JtdError("jtd/bad-schema", "jtd: 'definitions' must be an object");
78
+ Object.keys(schema.definitions).forEach(function (k) { _checkSchema(schema.definitions[k], root, false); });
79
+ }
80
+ if (Object.prototype.hasOwnProperty.call(schema, "mapping") && !Object.prototype.hasOwnProperty.call(schema, "discriminator")) throw new JtdError("jtd/bad-schema", "jtd: 'mapping' is only valid with 'discriminator'");
81
+ var formSet = {};
82
+ FORM_KEYWORDS.forEach(function (k) { if (Object.prototype.hasOwnProperty.call(schema, k)) formSet[(k === "optionalProperties") ? "properties" : k] = 1; });
83
+ var formNames = Object.keys(formSet);
84
+ if (formNames.length > 1) throw new JtdError("jtd/bad-schema", "jtd: a schema may use only one form (got " + formNames.join(", ") + ")");
85
+
86
+ if ("ref" in schema) {
87
+ if (typeof schema.ref !== "string" || !root.definitions || !Object.prototype.hasOwnProperty.call(root.definitions, schema.ref)) throw new JtdError("jtd/bad-schema", "jtd: 'ref' must name a key in the root definitions");
88
+ }
89
+ if ("type" in schema && !TYPES[schema.type]) throw new JtdError("jtd/bad-schema", "jtd: unknown type '" + schema.type + "'");
90
+ if ("enum" in schema) {
91
+ if (!Array.isArray(schema.enum) || schema.enum.length === 0) throw new JtdError("jtd/bad-schema", "jtd: 'enum' must be a non-empty array");
92
+ var seen = Object.create(null);
93
+ schema.enum.forEach(function (e) { if (typeof e !== "string") throw new JtdError("jtd/bad-schema", "jtd: 'enum' values must be strings"); if (seen[e]) throw new JtdError("jtd/bad-schema", "jtd: duplicate enum value"); seen[e] = 1; });
94
+ }
95
+ if ("elements" in schema) _checkSchema(schema.elements, root, false);
96
+ if ("values" in schema) _checkSchema(schema.values, root, false);
97
+ if ("properties" in schema || "optionalProperties" in schema) {
98
+ var props = schema.properties || {}, opt = schema.optionalProperties || {};
99
+ if (!_isPlainObject(props) || !_isPlainObject(opt)) throw new JtdError("jtd/bad-schema", "jtd: properties / optionalProperties must be objects");
100
+ Object.keys(props).forEach(function (k) { if (Object.prototype.hasOwnProperty.call(opt, k)) throw new JtdError("jtd/bad-schema", "jtd: '" + k + "' in both properties and optionalProperties"); _checkSchema(props[k], root, false); });
101
+ Object.keys(opt).forEach(function (k) { _checkSchema(opt[k], root, false); });
102
+ if ("additionalProperties" in schema && typeof schema.additionalProperties !== "boolean") throw new JtdError("jtd/bad-schema", "jtd: 'additionalProperties' must be a boolean");
103
+ }
104
+ if ("discriminator" in schema) {
105
+ if (typeof schema.discriminator !== "string") throw new JtdError("jtd/bad-schema", "jtd: 'discriminator' must be a string");
106
+ if (!_isPlainObject(schema.mapping)) throw new JtdError("jtd/bad-schema", "jtd: 'discriminator' requires a 'mapping' object");
107
+ Object.keys(schema.mapping).forEach(function (k) {
108
+ var sub = schema.mapping[k];
109
+ _checkSchema(sub, root, false);
110
+ if (!_isPlainObject(sub) || (!("properties" in sub) && !("optionalProperties" in sub))) throw new JtdError("jtd/bad-schema", "jtd: discriminator mapping schemas must use the properties form");
111
+ if (sub.nullable === true) throw new JtdError("jtd/bad-schema", "jtd: discriminator mapping schemas must not be nullable");
112
+ var p = sub.properties || {}, o = sub.optionalProperties || {};
113
+ if (Object.prototype.hasOwnProperty.call(p, schema.discriminator) || Object.prototype.hasOwnProperty.call(o, schema.discriminator)) throw new JtdError("jtd/bad-schema", "jtd: discriminator tag must not appear in a mapping schema's properties");
114
+ });
115
+ }
116
+ if ("additionalProperties" in schema && !("properties" in schema) && !("optionalProperties" in schema)) throw new JtdError("jtd/bad-schema", "jtd: 'additionalProperties' requires a properties form");
117
+ }
118
+
119
+ // --- validation (RFC 8927 section 3.3) ---
120
+ function _typeOk(type, v) {
121
+ if (type === "boolean") return typeof v === "boolean";
122
+ if (type === "string") return typeof v === "string";
123
+ if (type === "timestamp") return typeof v === "string" && _validTimestamp(v);
124
+ if (type === "float32" || type === "float64") return typeof v === "number" && isFinite(v);
125
+ var range = INT_RANGES[type];
126
+ return _isInteger(v) && v >= range[0] && v <= range[1];
127
+ }
128
+
129
+ function _val(schema, inst, ip, sp, root, depth, errors, discrimTag) {
130
+ if (depth > MAX_DEPTH) throw new JtdError("jtd/too-deep", "jtd: schema recursion exceeded the depth cap");
131
+ if (schema.nullable === true && inst === null) return;
132
+
133
+ if ("ref" in schema) { _val(root.definitions[schema.ref], inst, ip, ["definitions", schema.ref], root, depth + 1, errors, undefined); return; }
134
+
135
+ if ("type" in schema) {
136
+ if (!_typeOk(schema.type, inst)) errors.push({ instancePath: ip.slice(), schemaPath: sp.concat("type") });
137
+ return;
138
+ }
139
+ if ("enum" in schema) {
140
+ if (typeof inst !== "string" || schema.enum.indexOf(inst) === -1) errors.push({ instancePath: ip.slice(), schemaPath: sp.concat("enum") });
141
+ return;
142
+ }
143
+ if ("elements" in schema) {
144
+ if (!Array.isArray(inst)) { errors.push({ instancePath: ip.slice(), schemaPath: sp.concat("elements") }); return; }
145
+ for (var i = 0; i < inst.length; i++) _val(schema.elements, inst[i], ip.concat(String(i)), sp.concat("elements"), root, depth + 1, errors, undefined);
146
+ return;
147
+ }
148
+ if ("properties" in schema || "optionalProperties" in schema) {
149
+ if (!_isPlainObject(inst)) {
150
+ errors.push({ instancePath: ip.slice(), schemaPath: sp.concat("properties" in schema ? "properties" : "optionalProperties") });
151
+ return;
152
+ }
153
+ var props = schema.properties || {}, opt = schema.optionalProperties || {};
154
+ Object.keys(props).forEach(function (k) {
155
+ if (Object.prototype.hasOwnProperty.call(inst, k)) _val(props[k], inst[k], ip.concat(k), sp.concat("properties", k), root, depth + 1, errors, undefined);
156
+ else errors.push({ instancePath: ip.slice(), schemaPath: sp.concat("properties", k) });
157
+ });
158
+ Object.keys(opt).forEach(function (k) {
159
+ if (Object.prototype.hasOwnProperty.call(inst, k)) _val(opt[k], inst[k], ip.concat(k), sp.concat("optionalProperties", k), root, depth + 1, errors, undefined);
160
+ });
161
+ if (schema.additionalProperties !== true) {
162
+ Object.keys(inst).forEach(function (k) {
163
+ if (!Object.prototype.hasOwnProperty.call(props, k) && !Object.prototype.hasOwnProperty.call(opt, k) && k !== discrimTag) {
164
+ errors.push({ instancePath: ip.concat(k), schemaPath: sp.slice() });
165
+ }
166
+ });
167
+ }
168
+ return;
169
+ }
170
+ if ("values" in schema) {
171
+ if (!_isPlainObject(inst)) { errors.push({ instancePath: ip.slice(), schemaPath: sp.concat("values") }); return; }
172
+ Object.keys(inst).forEach(function (k) { _val(schema.values, inst[k], ip.concat(k), sp.concat("values"), root, depth + 1, errors, undefined); });
173
+ return;
174
+ }
175
+ if ("discriminator" in schema) {
176
+ if (!_isPlainObject(inst)) { errors.push({ instancePath: ip.slice(), schemaPath: sp.concat("discriminator") }); return; }
177
+ var tag = schema.discriminator;
178
+ if (!Object.prototype.hasOwnProperty.call(inst, tag)) { errors.push({ instancePath: ip.slice(), schemaPath: sp.concat("discriminator") }); return; }
179
+ if (typeof inst[tag] !== "string") { errors.push({ instancePath: ip.concat(tag), schemaPath: sp.concat("discriminator") }); return; }
180
+ if (!Object.prototype.hasOwnProperty.call(schema.mapping, inst[tag])) { errors.push({ instancePath: ip.concat(tag), schemaPath: sp.concat("mapping") }); return; }
181
+ _val(schema.mapping[inst[tag]], inst, ip, sp.concat("mapping", inst[tag]), root, depth + 1, errors, tag);
182
+ return;
183
+ }
184
+ // empty form: accepts anything
185
+ }
186
+
187
+ /**
188
+ * @primitive b.jtd.validate
189
+ * @signature b.jtd.validate(schema, instance)
190
+ * @since 0.12.62
191
+ * @status stable
192
+ * @compliance soc2
193
+ * @related b.safeSchema, b.jsonPointer.get
194
+ *
195
+ * Validate a JSON value against a JSON Type Definition schema (RFC 8927)
196
+ * and return the array of validation errors — each a
197
+ * <code>{ instancePath, schemaPath }</code> pair of token arrays naming
198
+ * the offending value and the schema rule it broke. An empty array means
199
+ * the instance is valid. All eight schema forms are supported. The schema
200
+ * itself is checked for well-formedness first; a malformed schema throws
201
+ * <code>jtd/bad-schema</code> rather than silently mis-validating.
202
+ *
203
+ * @example
204
+ * b.jtd.validate({ properties: { id: { type: "uint32" } } }, { id: -1 });
205
+ * // -> [ { instancePath: ["id"], schemaPath: ["properties", "id", "type"] } ]
206
+ */
207
+ function validate(schema, instance) {
208
+ _checkSchema(schema, schema, true);
209
+ var errors = [];
210
+ _val(schema, instance, [], [], schema, 0, errors, undefined);
211
+ return errors;
212
+ }
213
+
214
+ /**
215
+ * @primitive b.jtd.isValid
216
+ * @signature b.jtd.isValid(schema, instance)
217
+ * @since 0.12.62
218
+ * @status stable
219
+ * @related b.jtd.validate
220
+ *
221
+ * Convenience boolean form of <code>validate</code> — <code>true</code>
222
+ * when the instance conforms to the JTD schema (no errors). Throws
223
+ * <code>jtd/bad-schema</code> on a malformed schema.
224
+ *
225
+ * @example
226
+ * b.jtd.isValid({ type: "string" }, "hello"); // -> true
227
+ */
228
+ function isValid(schema, instance) { return validate(schema, instance).length === 0; }
229
+
230
+ module.exports = {
231
+ validate: validate,
232
+ isValid: isValid,
233
+ JtdError: JtdError,
234
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.12.60",
3
+ "version": "0.12.62",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:1329fa76-6890-4008-be5d-6db27e71506c",
5
+ "serialNumber": "urn:uuid:f56710ee-2e4e-4c8e-af88-80f79561870e",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-26T01:30:30.570Z",
8
+ "timestamp": "2026-05-26T03:54:31.896Z",
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.60",
22
+ "bom-ref": "@blamejs/core@0.12.62",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.12.60",
25
+ "version": "0.12.62",
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.60",
29
+ "purl": "pkg:npm/%40blamejs/core@0.12.62",
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.60",
57
+ "ref": "@blamejs/core@0.12.62",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]