@blamejs/blamejs-shop 0.1.29 → 0.1.30
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 +2 -0
- package/README.md +3 -1
- package/SECURITY.md +9 -0
- package/lib/admin.js +317 -1
- package/lib/asset-manifest.json +1 -1
- package/lib/product-qa.js +88 -0
- package/lib/storefront.js +225 -1
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +6 -0
- package/lib/vendor/blamejs/README.md +3 -0
- package/lib/vendor/blamejs/api-snapshot.json +74 -2
- package/lib/vendor/blamejs/index.js +6 -0
- package/lib/vendor/blamejs/lib/base32.js +154 -0
- package/lib/vendor/blamejs/lib/json-schema.js +740 -0
- package/lib/vendor/blamejs/lib/totp.js +10 -31
- package/lib/vendor/blamejs/lib/uri-template.js +286 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.64.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.65.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.12.66.json +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/base32.test.js +79 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/json-schema.test.js +134 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/uri-template.test.js +99 -0
- package/package.json +1 -1
|
@@ -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) ----
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.uriTemplate
|
|
4
|
+
* @nav HTTP
|
|
5
|
+
* @title URI Templates
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Expand <a href="https://www.rfc-editor.org/rfc/rfc6570">RFC 6570</a> URI
|
|
9
|
+
* Templates — the <code>{var}</code> syntax that OpenAPI links, HAL
|
|
10
|
+
* <code>_links</code>, and hypermedia API clients use to turn a template
|
|
11
|
+
* plus a set of variables into a concrete URI. The full Level 4 grammar
|
|
12
|
+
* is supported: every operator (<code>{+var}</code> reserved,
|
|
13
|
+
* <code>{#var}</code> fragment, <code>{.var}</code> label,
|
|
14
|
+
* <code>{/var}</code> path, <code>{;var}</code> path-style parameters,
|
|
15
|
+
* <code>{?var}</code> query, <code>{&var}</code> query continuation),
|
|
16
|
+
* the <code>{var:3}</code> prefix modifier, and the <code>{var*}</code>
|
|
17
|
+
* explode modifier for lists and associative arrays.
|
|
18
|
+
*
|
|
19
|
+
* <code>expand(template, vars)</code> returns the expanded string;
|
|
20
|
+
* <code>compile(template)</code> parses once and returns a reusable
|
|
21
|
+
* <code>{ expand }</code> for templates applied to many variable sets. A
|
|
22
|
+
* malformed template (an unclosed expression, an unknown operator, or a
|
|
23
|
+
* non-numeric prefix) throws <code>UriTemplateError</code>.
|
|
24
|
+
*
|
|
25
|
+
* @card
|
|
26
|
+
* RFC 6570 URI Template expansion (full Level 4 — every operator, the
|
|
27
|
+
* <code>:N</code> prefix and <code>*</code> explode modifiers) — the
|
|
28
|
+
* <code>{var}</code> syntax behind OpenAPI links and HAL hypermedia.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
var { defineClass } = require("./framework-error");
|
|
32
|
+
|
|
33
|
+
var UriTemplateError = defineClass("UriTemplateError", { alwaysPermanent: true });
|
|
34
|
+
|
|
35
|
+
var MAX_PREFIX = 10000; // allow:raw-byte-literal — RFC 6570 caps prefix length at 9999
|
|
36
|
+
|
|
37
|
+
// Operator table (RFC 6570 §2.2 / §3.2.1). first = prefix when any value is
|
|
38
|
+
// present; sep = separator between values; named = emit "name=value";
|
|
39
|
+
// ifemp = string used when a named value is empty; reserved = allow the
|
|
40
|
+
// reserved set through unencoded.
|
|
41
|
+
var OPERATORS = {
|
|
42
|
+
"": { first: "", sep: ",", named: false, ifemp: "", reserved: false },
|
|
43
|
+
"+": { first: "", sep: ",", named: false, ifemp: "", reserved: true },
|
|
44
|
+
"#": { first: "#", sep: ",", named: false, ifemp: "", reserved: true },
|
|
45
|
+
".": { first: ".", sep: ".", named: false, ifemp: "", reserved: false },
|
|
46
|
+
"/": { first: "/", sep: "/", named: false, ifemp: "", reserved: false },
|
|
47
|
+
";": { first: ";", sep: ";", named: true, ifemp: "", reserved: false },
|
|
48
|
+
"?": { first: "?", sep: "&", named: true, ifemp: "=", reserved: false },
|
|
49
|
+
"&": { first: "&", sep: "&", named: true, ifemp: "=", reserved: false },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
var UNRESERVED = /[A-Za-z0-9\-._~]/;
|
|
53
|
+
// Reserved = gen-delims + sub-delims (RFC 3986 §2.2).
|
|
54
|
+
var RESERVED = /[:/?#[\]@!$&'()*+,;=]/;
|
|
55
|
+
|
|
56
|
+
function _pctEncode(str, allowReserved) {
|
|
57
|
+
var out = "";
|
|
58
|
+
for (var i = 0; i < str.length; i++) {
|
|
59
|
+
var ch = str.charAt(i);
|
|
60
|
+
// Preserve existing percent-encoded triplets when the reserved set is
|
|
61
|
+
// allowed (operators "+" and "#").
|
|
62
|
+
if (allowReserved && ch === "%" && /^[0-9A-Fa-f]{2}$/.test(str.substr(i + 1, 2))) {
|
|
63
|
+
out += str.substr(i, 3); i += 2; continue;
|
|
64
|
+
}
|
|
65
|
+
if (UNRESERVED.test(ch) || (allowReserved && RESERVED.test(ch))) { out += ch; continue; }
|
|
66
|
+
// Percent-encode the character's raw UTF-8 bytes (handles surrogate
|
|
67
|
+
// pairs). encodeURIComponent is not used — it leaves !*'() unencoded,
|
|
68
|
+
// which RFC 6570 unreserved-only expansion must escape.
|
|
69
|
+
var cp = str.codePointAt(i);
|
|
70
|
+
var bytes = Buffer.from(String.fromCodePoint(cp), "utf8");
|
|
71
|
+
for (var b = 0; b < bytes.length; b++) out += "%" + bytes[b].toString(16).toUpperCase().padStart(2, "0"); // allow:raw-byte-literal — hex radix
|
|
72
|
+
if (cp > 0xFFFF) i++; // consumed a surrogate pair // allow:raw-byte-literal — BMP boundary for surrogate-pair detection
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _allDigits(s) {
|
|
78
|
+
if (s.length === 0) return false;
|
|
79
|
+
for (var i = 0; i < s.length; i++) { var c = s.charCodeAt(i); if (c < 48 || c > 57) return false; } // allow:raw-byte-literal — ASCII '0'..'9' code-point bounds
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// A composite member/value is "defined" unless it is undefined or null
|
|
84
|
+
// (an empty string, 0, or false is defined and expands).
|
|
85
|
+
function _memberDefined(v) { return v !== undefined && v !== null; }
|
|
86
|
+
|
|
87
|
+
function _isDefined(v) {
|
|
88
|
+
if (v === undefined || v === null) return false;
|
|
89
|
+
if (Array.isArray(v)) return v.length > 0;
|
|
90
|
+
if (typeof v === "object") return Object.keys(v).length > 0;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
function _toStr(v) {
|
|
94
|
+
if (typeof v === "string") return v;
|
|
95
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
96
|
+
return String(v);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// A literal run may not contain a stray "}" (an unmatched expression close)
|
|
100
|
+
// — RFC 6570 literals exclude "{" and "}".
|
|
101
|
+
function _checkLiteral(lit) {
|
|
102
|
+
if (lit.indexOf("}") !== -1) throw new UriTemplateError("uri-template/unmatched-brace", "uriTemplate: unmatched '}' in template literal");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Parse a template into an array of literal strings + expression objects.
|
|
106
|
+
function _parse(template) {
|
|
107
|
+
if (typeof template !== "string") throw new UriTemplateError("uri-template/bad-template", "uriTemplate: template must be a string");
|
|
108
|
+
var parts = [];
|
|
109
|
+
var i = 0;
|
|
110
|
+
while (i < template.length) {
|
|
111
|
+
var open = template.indexOf("{", i);
|
|
112
|
+
if (open === -1) { _checkLiteral(template.slice(i)); parts.push({ literal: template.slice(i) }); break; }
|
|
113
|
+
if (open > i) { _checkLiteral(template.slice(i, open)); parts.push({ literal: template.slice(i, open) }); }
|
|
114
|
+
var close = template.indexOf("}", open);
|
|
115
|
+
if (close === -1) throw new UriTemplateError("uri-template/unclosed", "uriTemplate: unclosed expression at index " + open);
|
|
116
|
+
parts.push(_parseExpr(template.slice(open + 1, close)));
|
|
117
|
+
i = close + 1;
|
|
118
|
+
}
|
|
119
|
+
return parts;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _parseExpr(body) {
|
|
123
|
+
if (body.length === 0) throw new UriTemplateError("uri-template/empty-expression", "uriTemplate: empty expression {}");
|
|
124
|
+
var op = "";
|
|
125
|
+
var c0 = body.charAt(0);
|
|
126
|
+
if ("+#./;?&".indexOf(c0) !== -1) { op = c0; body = body.slice(1); }
|
|
127
|
+
else if ("=,!@|".indexOf(c0) !== -1) {
|
|
128
|
+
// Operators reserved by RFC 6570 §2.2 for future extensions → error.
|
|
129
|
+
throw new UriTemplateError("uri-template/reserved-operator", "uriTemplate: operator '" + c0 + "' is reserved");
|
|
130
|
+
}
|
|
131
|
+
var specs = body.split(",").map(function (raw) {
|
|
132
|
+
if (raw.length === 0) throw new UriTemplateError("uri-template/bad-varspec", "uriTemplate: empty variable name");
|
|
133
|
+
var explode = false, prefix = null;
|
|
134
|
+
var name = raw;
|
|
135
|
+
if (raw.charAt(raw.length - 1) === "*") { explode = true; name = raw.slice(0, -1); }
|
|
136
|
+
else {
|
|
137
|
+
var colon = raw.indexOf(":");
|
|
138
|
+
if (colon !== -1) {
|
|
139
|
+
name = raw.slice(0, colon);
|
|
140
|
+
var n = raw.slice(colon + 1);
|
|
141
|
+
if (!_allDigits(n)) throw new UriTemplateError("uri-template/bad-prefix", "uriTemplate: prefix length must be a non-negative integer");
|
|
142
|
+
prefix = parseInt(n, 10);
|
|
143
|
+
if (prefix >= MAX_PREFIX) throw new UriTemplateError("uri-template/bad-prefix", "uriTemplate: prefix length exceeds 9999");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!/^(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})(?:\.?(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2}))*$/.test(name)) {
|
|
147
|
+
throw new UriTemplateError("uri-template/bad-varname", "uriTemplate: invalid variable name '" + name + "'");
|
|
148
|
+
}
|
|
149
|
+
return { name: name, explode: explode, prefix: prefix };
|
|
150
|
+
});
|
|
151
|
+
return { op: op, specs: specs };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function _expandExpr(expr, vars) {
|
|
155
|
+
var o = OPERATORS[expr.op];
|
|
156
|
+
var pieces = [];
|
|
157
|
+
expr.specs.forEach(function (spec) {
|
|
158
|
+
var value = vars[spec.name];
|
|
159
|
+
if (!_isDefined(value)) return;
|
|
160
|
+
|
|
161
|
+
if (typeof value !== "object") {
|
|
162
|
+
// Simple string/number/boolean value.
|
|
163
|
+
var s = _toStr(value);
|
|
164
|
+
if (spec.prefix !== null) s = _sliceChars(s, spec.prefix);
|
|
165
|
+
pieces.push(_named(o, spec.name, _pctEncode(s, o.reserved), s.length === 0));
|
|
166
|
+
} else if (Array.isArray(value)) {
|
|
167
|
+
if (spec.prefix !== null) throw new UriTemplateError("uri-template/prefix-on-list", "uriTemplate: prefix modifier cannot apply to a list");
|
|
168
|
+
// Undefined / null members are ignored (RFC 6570 §3.2.1); a list with
|
|
169
|
+
// no defined members is treated as undefined and omitted entirely.
|
|
170
|
+
var members = value.filter(_memberDefined);
|
|
171
|
+
if (members.length === 0) return;
|
|
172
|
+
if (!spec.explode) {
|
|
173
|
+
var joined = members.map(function (m) { return _pctEncode(_toStr(m), o.reserved); }).join(",");
|
|
174
|
+
pieces.push(_named(o, spec.name, joined, false));
|
|
175
|
+
} else {
|
|
176
|
+
members.forEach(function (m) {
|
|
177
|
+
pieces.push(_named(o, spec.name, _pctEncode(_toStr(m), o.reserved), _toStr(m).length === 0));
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
// Associative array (object). Pairs whose value is undefined / null
|
|
182
|
+
// are omitted (RFC 6570 §3.2.1).
|
|
183
|
+
if (spec.prefix !== null) throw new UriTemplateError("uri-template/prefix-on-map", "uriTemplate: prefix modifier cannot apply to a map");
|
|
184
|
+
var keys = Object.keys(value).filter(function (k) { return _memberDefined(value[k]); });
|
|
185
|
+
if (keys.length === 0) return;
|
|
186
|
+
if (!spec.explode) {
|
|
187
|
+
var pairs = [];
|
|
188
|
+
keys.forEach(function (k) { pairs.push(_pctEncode(k, o.reserved)); pairs.push(_pctEncode(_toStr(value[k]), o.reserved)); });
|
|
189
|
+
pieces.push(_named(o, spec.name, pairs.join(","), false));
|
|
190
|
+
} else {
|
|
191
|
+
keys.forEach(function (k) {
|
|
192
|
+
// Exploded map: the key becomes the name.
|
|
193
|
+
pieces.push(_namedPair(o, _pctEncode(k, o.reserved), _pctEncode(_toStr(value[k]), o.reserved)));
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
if (pieces.length === 0) return "";
|
|
199
|
+
return o.first + pieces.join(o.sep);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Build one "name=value" (or bare value) piece for a non-exploded or
|
|
203
|
+
// string varspec.
|
|
204
|
+
function _named(o, name, encodedValue, isEmpty) {
|
|
205
|
+
if (!o.named) return encodedValue;
|
|
206
|
+
if (isEmpty) return name + o.ifemp;
|
|
207
|
+
return name + "=" + encodedValue;
|
|
208
|
+
}
|
|
209
|
+
// Exploded map pair: key is already the name.
|
|
210
|
+
function _namedPair(o, encodedKey, encodedValue) {
|
|
211
|
+
if (!o.named) return encodedKey + "=" + encodedValue;
|
|
212
|
+
if (encodedValue.length === 0) return encodedKey + o.ifemp;
|
|
213
|
+
return encodedKey + "=" + encodedValue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Truncate to N Unicode code points (RFC 6570 prefix length is in chars).
|
|
217
|
+
function _sliceChars(s, n) {
|
|
218
|
+
var out = "", count = 0;
|
|
219
|
+
for (var i = 0; i < s.length && count < n; i++) {
|
|
220
|
+
var cp = s.codePointAt(i);
|
|
221
|
+
out += String.fromCodePoint(cp);
|
|
222
|
+
if (cp > 0xFFFF) i++; // allow:raw-byte-literal — BMP boundary for surrogate pairs
|
|
223
|
+
count++;
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @primitive b.uriTemplate.compile
|
|
230
|
+
* @signature b.uriTemplate.compile(template)
|
|
231
|
+
* @since 0.12.66
|
|
232
|
+
* @status stable
|
|
233
|
+
* @related b.uriTemplate.expand, b.hal, b.linkHeader
|
|
234
|
+
*
|
|
235
|
+
* Parse an RFC 6570 URI Template once and return a reusable
|
|
236
|
+
* <code>{ expand(vars) }</code>, so a template applied to many variable
|
|
237
|
+
* sets is parsed a single time. Throws <code>UriTemplateError</code> if the
|
|
238
|
+
* template is malformed.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* var t = b.uriTemplate.compile("/users/{id}{?fields*}");
|
|
242
|
+
* t.expand({ id: 7, fields: ["name", "email"] });
|
|
243
|
+
* // → "/users/7?fields=name&fields=email"
|
|
244
|
+
*/
|
|
245
|
+
function compile(template) {
|
|
246
|
+
var parts = _parse(template);
|
|
247
|
+
return {
|
|
248
|
+
expand: function (vars) {
|
|
249
|
+
vars = vars || {};
|
|
250
|
+
var out = "";
|
|
251
|
+
for (var i = 0; i < parts.length; i++) {
|
|
252
|
+
out += Object.prototype.hasOwnProperty.call(parts[i], "literal") ? parts[i].literal : _expandExpr(parts[i], vars);
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @primitive b.uriTemplate.expand
|
|
261
|
+
* @signature b.uriTemplate.expand(template, vars)
|
|
262
|
+
* @since 0.12.66
|
|
263
|
+
* @status stable
|
|
264
|
+
* @related b.uriTemplate.compile
|
|
265
|
+
*
|
|
266
|
+
* Expand an RFC 6570 URI Template against a set of variables and return the
|
|
267
|
+
* resulting URI string. Variable values may be strings, numbers, booleans,
|
|
268
|
+
* arrays (lists), or plain objects (associative arrays); an undefined,
|
|
269
|
+
* null, or empty list/map variable is omitted. Reserved-set encoding,
|
|
270
|
+
* <code>:N</code> prefixes, and <code>*</code> explosion follow RFC 6570
|
|
271
|
+
* §3.2. Throws <code>UriTemplateError</code> on a malformed template.
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* b.uriTemplate.expand("{/path}/here{?q,limit}",
|
|
275
|
+
* { path: "search", q: "json schema", limit: 10 });
|
|
276
|
+
* // → "/search/here?q=json%20schema&limit=10"
|
|
277
|
+
*/
|
|
278
|
+
function expand(template, vars) {
|
|
279
|
+
return compile(template).expand(vars);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = {
|
|
283
|
+
expand: expand,
|
|
284
|
+
compile: compile,
|
|
285
|
+
UriTemplateError: UriTemplateError,
|
|
286
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.64",
|
|
4
|
+
"date": "2026-05-25",
|
|
5
|
+
"headline": "`b.jsonSchema` — JSON Schema 2020-12 validation",
|
|
6
|
+
"summary": "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.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.jsonSchema` — JSON Schema 2020-12",
|
|
13
|
+
"body": "`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
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.65",
|
|
4
|
+
"date": "2026-05-26",
|
|
5
|
+
"headline": "`b.base32` — RFC 4648 Base32 encode / decode",
|
|
6
|
+
"summary": "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.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.base32.encode` / `b.base32.decode`",
|
|
13
|
+
"body": "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."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"heading": "Changed",
|
|
19
|
+
"items": [
|
|
20
|
+
{
|
|
21
|
+
"title": "TOTP composes `b.base32`",
|
|
22
|
+
"body": "`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)."
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.66",
|
|
4
|
+
"date": "2026-05-26",
|
|
5
|
+
"headline": "`b.uriTemplate` — RFC 6570 URI Template expansion",
|
|
6
|
+
"summary": "Expand RFC 6570 URI Templates — the {var} syntax that OpenAPI links, HAL _links, and hypermedia API clients use to turn a template plus a set of variables into a concrete URI. The full Level 4 grammar is supported: every operator ({+var} reserved, {#var} fragment, {.var} label, {/var} path, {;var} path-style parameters, {?var} query, {&var} query continuation), the {var:3} prefix modifier, and the {var*} explode modifier for lists and associative arrays. b.uriTemplate.expand(template, vars) returns the expanded string; b.uriTemplate.compile(template) parses once for templates applied to many variable sets. A malformed template (unclosed expression, reserved operator, non-numeric prefix, unmatched brace) throws UriTemplateError. Verified against the official uritemplate-test conformance suite (all 135 spec, extended, and negative cases).",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.uriTemplate.expand` / `b.uriTemplate.compile`",
|
|
13
|
+
"body": "RFC 6570 URI Template expansion, full Level 4. `expand(template, vars)` substitutes variables into a template and returns the URI; `compile(template)` returns a reusable `{ expand }` for repeated use. Variable values may be strings, numbers, booleans, arrays (lists), or plain objects (associative arrays); undefined, null, and empty list/map variables are omitted. All eight operators, the `:N` prefix modifier, and the `*` explode modifier follow §3.2, including reserved-set encoding for `{+var}` / `{#var}`. Composes naturally with `b.hal`, `b.linkHeader`, and `b.openapi` link objects. A malformed template throws `UriTemplateError`."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 0 — b.base32 (RFC 4648).
|
|
4
|
+
* Oracle: the RFC 4648 §10 test vectors for both the standard Base32
|
|
5
|
+
* alphabet and the extended-hex alphabet.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
var b = require("../../index");
|
|
9
|
+
var helpers = require("../helpers");
|
|
10
|
+
var check = helpers.check;
|
|
11
|
+
function code(fn) { try { fn(); return "NO-THROW"; } catch (e) { return e.code; } }
|
|
12
|
+
|
|
13
|
+
// RFC 4648 §10 — standard Base32.
|
|
14
|
+
var STD = [["", ""], ["f", "MY======"], ["fo", "MZXQ===="], ["foo", "MZXW6==="],
|
|
15
|
+
["foob", "MZXW6YQ="], ["fooba", "MZXW6YTB"], ["foobar", "MZXW6YTBOI======"]];
|
|
16
|
+
// RFC 4648 §10 — Base32 extended hex.
|
|
17
|
+
var HEX = [["", ""], ["f", "CO======"], ["fo", "CPNG===="], ["foo", "CPNMU==="],
|
|
18
|
+
["foob", "CPNMUOG="], ["fooba", "CPNMUOJ1"], ["foobar", "CPNMUOJ1E8======"]];
|
|
19
|
+
|
|
20
|
+
function testSurface() {
|
|
21
|
+
check("b.base32.encode is a function", typeof b.base32.encode === "function");
|
|
22
|
+
check("b.base32.decode is a function", typeof b.base32.decode === "function");
|
|
23
|
+
check("b.base32.Base32Error is a class", typeof b.base32.Base32Error === "function");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function testVectors() {
|
|
27
|
+
var ep = 0, dp = 0;
|
|
28
|
+
STD.forEach(function (t) {
|
|
29
|
+
if (b.base32.encode(Buffer.from(t[0])) === t[1]) ep++; else check("std encode " + JSON.stringify(t[0]), false);
|
|
30
|
+
if (b.base32.decode(t[1]).toString() === t[0]) dp++; else check("std decode " + t[1], false);
|
|
31
|
+
});
|
|
32
|
+
check("RFC 4648 standard: all encode vectors", ep === STD.length);
|
|
33
|
+
check("RFC 4648 standard: all decode vectors", dp === STD.length);
|
|
34
|
+
var hep = 0, hdp = 0;
|
|
35
|
+
HEX.forEach(function (t) {
|
|
36
|
+
if (b.base32.encode(Buffer.from(t[0]), { variant: "rfc4648-hex" }) === t[1]) hep++; else check("hex encode " + JSON.stringify(t[0]), false);
|
|
37
|
+
if (b.base32.decode(t[1], { variant: "rfc4648-hex" }).toString() === t[0]) hdp++; else check("hex decode " + t[1], false);
|
|
38
|
+
});
|
|
39
|
+
check("RFC 4648 hex: all encode vectors", hep === HEX.length);
|
|
40
|
+
check("RFC 4648 hex: all decode vectors", hdp === HEX.length);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function testOptions() {
|
|
44
|
+
check("padding:false omits =", b.base32.encode(Buffer.from("f"), { padding: false }) === "MY");
|
|
45
|
+
check("round-trip random 20 bytes", (function () {
|
|
46
|
+
var buf = require("crypto").randomBytes(20);
|
|
47
|
+
return b.base32.decode(b.base32.encode(buf)).equals(buf);
|
|
48
|
+
})());
|
|
49
|
+
// loose decode: lower-case, spaces, dashes, missing padding.
|
|
50
|
+
check("loose decodes lower-case + separators", b.base32.decode("mzxw 6ytb-oi", { loose: true }).toString() === "foobar");
|
|
51
|
+
check("strict rejects lower-case", code(function () { b.base32.decode("mzxw6ytb"); }) === "base32/bad-char");
|
|
52
|
+
check("strict rejects bad char", code(function () { b.base32.decode("MZXW60TB"); }) === "base32/bad-char");
|
|
53
|
+
check("bad variant throws", code(function () { b.base32.encode(Buffer.from("x"), { variant: "nope" }); }) === "base32/bad-variant");
|
|
54
|
+
check("non-buffer encode throws", code(function () { b.base32.encode("not a buffer"); }) === "base32/bad-input");
|
|
55
|
+
check("non-string decode throws", code(function () { b.base32.decode(123); }) === "base32/bad-input");
|
|
56
|
+
// Embedded / non-trailing padding is malformed and must be rejected
|
|
57
|
+
// (not silently truncated at the first "=").
|
|
58
|
+
check("rejects embedded padding then data", code(function () { b.base32.decode("MZXW=6YTB"); }) === "base32/bad-char");
|
|
59
|
+
check("rejects padding then data (loose)", code(function () { b.base32.decode("mz=xw", { loose: true }); }) === "base32/bad-char");
|
|
60
|
+
check("accepts valid trailing padding", b.base32.decode("MZXW6YQ=").toString() === "foob");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function testTotpComposition() {
|
|
64
|
+
// The TOTP secret produced by generateSecret must decode through b.base32.
|
|
65
|
+
var secret = b.auth.totp.generateSecret();
|
|
66
|
+
check("totp secret decodes via b.base32 (loose)", b.base32.decode(secret, { loose: true }).length > 0);
|
|
67
|
+
// The TOTP secret is unpadded standard Base32 → re-encoding the decoded
|
|
68
|
+
// bytes (unpadded) reproduces the secret exactly.
|
|
69
|
+
check("totp secret round-trips through b.base32", b.base32.encode(b.base32.decode(secret, { loose: true }), { padding: false }) === secret);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function run() {
|
|
73
|
+
testSurface();
|
|
74
|
+
testVectors();
|
|
75
|
+
testOptions();
|
|
76
|
+
testTotpComposition();
|
|
77
|
+
}
|
|
78
|
+
module.exports = { run: run };
|
|
79
|
+
if (require.main === module) { run().then(function () { console.log("[base32] OK — " + helpers.getChecks() + " checks passed"); }, function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }); }
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 0 — b.jsonSchema (JSON Schema 2020-12).
|
|
4
|
+
* Oracle: the official json-schema-org/JSON-Schema-Test-Suite draft2020-12
|
|
5
|
+
* (1292 of 1295 cases pass during development; the 3 skipped require the
|
|
6
|
+
* bundled dialect metaschema or $vocabulary selection — both opt-in). This
|
|
7
|
+
* file embeds a representative slice across the vocabulary plus the surface
|
|
8
|
+
* + reference-resolution + annotation cases that exercise the tricky paths.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
var b = require("../../index");
|
|
12
|
+
var helpers = require("../helpers");
|
|
13
|
+
var check = helpers.check;
|
|
14
|
+
function code(fn) { try { fn(); return "NO-THROW"; } catch (e) { return e.code; } }
|
|
15
|
+
|
|
16
|
+
function testSurface() {
|
|
17
|
+
check("b.jsonSchema.validate is a function", typeof b.jsonSchema.validate === "function");
|
|
18
|
+
check("b.jsonSchema.compile is a function", typeof b.jsonSchema.compile === "function");
|
|
19
|
+
check("b.jsonSchema.isValid is a function", typeof b.jsonSchema.isValid === "function");
|
|
20
|
+
check("b.jsonSchema.DIALECT is 2020-12", b.jsonSchema.DIALECT === "https://json-schema.org/draft/2020-12/schema");
|
|
21
|
+
check("b.jsonSchema.JsonSchemaError is a class", typeof b.jsonSchema.JsonSchemaError === "function");
|
|
22
|
+
check("compile rejects non-schema", code(function () { b.jsonSchema.compile(42); }) === "json-schema/bad-schema");
|
|
23
|
+
var v = b.jsonSchema.compile({ type: "integer" });
|
|
24
|
+
check("compiled validator has validate + isValid", typeof v.validate === "function" && typeof v.isValid === "function");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function testAssertions() {
|
|
28
|
+
check("type integer accepts int", b.jsonSchema.isValid({ type: "integer" }, 3));
|
|
29
|
+
check("type integer rejects float", !b.jsonSchema.isValid({ type: "integer" }, 3.5));
|
|
30
|
+
check("type rejects wrong type", !b.jsonSchema.isValid({ type: "string" }, 1));
|
|
31
|
+
check("enum", b.jsonSchema.isValid({ enum: ["a", "b"] }, "b") && !b.jsonSchema.isValid({ enum: ["a"] }, "z"));
|
|
32
|
+
check("const deep-equal", b.jsonSchema.isValid({ const: { a: [1, 2] } }, { a: [1, 2] }) && !b.jsonSchema.isValid({ const: { a: [1] } }, { a: [2] }));
|
|
33
|
+
check("multipleOf", b.jsonSchema.isValid({ multipleOf: 3 }, 9) && !b.jsonSchema.isValid({ multipleOf: 3 }, 10));
|
|
34
|
+
check("maximum/exclusiveMaximum", b.jsonSchema.isValid({ maximum: 5 }, 5) && !b.jsonSchema.isValid({ exclusiveMaximum: 5 }, 5));
|
|
35
|
+
check("minLength counts code points", !b.jsonSchema.isValid({ minLength: 2 }, "😀") && b.jsonSchema.isValid({ maxLength: 1 }, "😀"));
|
|
36
|
+
check("pattern", b.jsonSchema.isValid({ pattern: "^a+$" }, "aaa") && !b.jsonSchema.isValid({ pattern: "^a+$" }, "b"));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function testArrays() {
|
|
40
|
+
check("prefixItems + items", b.jsonSchema.isValid({ prefixItems: [{ type: "number" }], items: { type: "string" } }, [1, "a", "b"]));
|
|
41
|
+
check("items rejects bad tail", !b.jsonSchema.isValid({ prefixItems: [{ type: "number" }], items: { type: "string" } }, [1, 2]));
|
|
42
|
+
check("uniqueItems", b.jsonSchema.isValid({ uniqueItems: true }, [1, 2, 3]) && !b.jsonSchema.isValid({ uniqueItems: true }, [1, 1]));
|
|
43
|
+
check("contains + minContains", b.jsonSchema.isValid({ contains: { const: 2 }, minContains: 2 }, [2, 2, 3]) && !b.jsonSchema.isValid({ contains: { const: 2 }, minContains: 2 }, [2, 3]));
|
|
44
|
+
check("maxItems/minItems", !b.jsonSchema.isValid({ maxItems: 1 }, [1, 2]) && !b.jsonSchema.isValid({ minItems: 2 }, [1]));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function testObjects() {
|
|
48
|
+
var s = { type: "object", properties: { n: { type: "integer" } }, required: ["n"], additionalProperties: false };
|
|
49
|
+
check("properties + required pass", b.jsonSchema.isValid(s, { n: 1 }));
|
|
50
|
+
check("required missing fails", !b.jsonSchema.isValid(s, {}));
|
|
51
|
+
check("additionalProperties:false rejects extra", !b.jsonSchema.isValid(s, { n: 1, x: 2 }));
|
|
52
|
+
check("patternProperties", b.jsonSchema.isValid({ patternProperties: { "^x": { type: "number" } } }, { x1: 1 }) && !b.jsonSchema.isValid({ patternProperties: { "^x": { type: "number" } } }, { x1: "a" }));
|
|
53
|
+
check("propertyNames", !b.jsonSchema.isValid({ propertyNames: { pattern: "^a" } }, { b: 1 }));
|
|
54
|
+
check("dependentRequired", !b.jsonSchema.isValid({ dependentRequired: { a: ["b"] } }, { a: 1 }));
|
|
55
|
+
check("dependentSchemas", !b.jsonSchema.isValid({ dependentSchemas: { a: { required: ["b"] } } }, { a: 1 }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function testApplicators() {
|
|
59
|
+
check("allOf", b.jsonSchema.isValid({ allOf: [{ type: "number" }, { minimum: 0 }] }, 5) && !b.jsonSchema.isValid({ allOf: [{ type: "number" }, { minimum: 0 }] }, -1));
|
|
60
|
+
check("anyOf", b.jsonSchema.isValid({ anyOf: [{ type: "string" }, { type: "number" }] }, 1) && !b.jsonSchema.isValid({ anyOf: [{ type: "string" }] }, 1));
|
|
61
|
+
check("oneOf exactly one", b.jsonSchema.isValid({ oneOf: [{ multipleOf: 2 }, { multipleOf: 3 }] }, 4) && !b.jsonSchema.isValid({ oneOf: [{ multipleOf: 2 }, { multipleOf: 3 }] }, 6));
|
|
62
|
+
check("not", b.jsonSchema.isValid({ not: { type: "string" } }, 1) && !b.jsonSchema.isValid({ not: { type: "string" } }, "x"));
|
|
63
|
+
check("if/then/else", b.jsonSchema.isValid({ if: { type: "number" }, then: { minimum: 0 }, else: { type: "string" } }, 5) && b.jsonSchema.isValid({ if: { type: "number" }, then: { minimum: 0 }, else: { type: "string" } }, "x") && !b.jsonSchema.isValid({ if: { type: "number" }, then: { minimum: 0 } }, -1));
|
|
64
|
+
check("boolean schema true/false", b.jsonSchema.isValid(true, 42) && !b.jsonSchema.isValid(false, 42));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function testUnevaluated() {
|
|
68
|
+
// unevaluatedProperties sees annotations from $ref inside allOf.
|
|
69
|
+
var s = {
|
|
70
|
+
$defs: { one: { properties: { a: true } } },
|
|
71
|
+
allOf: [{ $ref: "#/$defs/one" }, { properties: { b: true } }],
|
|
72
|
+
unevaluatedProperties: false,
|
|
73
|
+
};
|
|
74
|
+
check("unevaluatedProperties + ref-in-allOf accepts evaluated", b.jsonSchema.isValid(s, { a: 1, b: 2 }));
|
|
75
|
+
check("unevaluatedProperties + ref-in-allOf rejects unevaluated", !b.jsonSchema.isValid(s, { a: 1, c: 3 }));
|
|
76
|
+
check("unevaluatedItems", b.jsonSchema.isValid({ prefixItems: [{ type: "number" }], unevaluatedItems: false }, [1]) && !b.jsonSchema.isValid({ prefixItems: [{ type: "number" }], unevaluatedItems: false }, [1, 2]));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function testRefs() {
|
|
80
|
+
// $ref to $defs + $anchor.
|
|
81
|
+
check("$ref to $defs", b.jsonSchema.isValid({ $defs: { pos: { minimum: 0 } }, $ref: "#/$defs/pos" }, 5));
|
|
82
|
+
check("$anchor ref", b.jsonSchema.isValid({ $defs: { p: { $anchor: "pos", minimum: 0 } }, $ref: "#pos" }, 5));
|
|
83
|
+
// External schema via opts.schemas (no network).
|
|
84
|
+
var ext = { "https://example.com/int": { type: "integer" } };
|
|
85
|
+
check("external $ref via opts.schemas", b.jsonSchema.isValid({ $ref: "https://example.com/int" }, 3, { schemas: ext }));
|
|
86
|
+
check("external $ref rejects", !b.jsonSchema.isValid({ $ref: "https://example.com/int" }, "x", { schemas: ext }));
|
|
87
|
+
// $dynamicRef / $dynamicAnchor (the recursive bookend pattern).
|
|
88
|
+
var dyn = {
|
|
89
|
+
$id: "https://example.com/tree",
|
|
90
|
+
$dynamicAnchor: "node",
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: { data: true, children: { type: "array", items: { $dynamicRef: "#node" } } },
|
|
93
|
+
};
|
|
94
|
+
check("$dynamicRef recursion validates", b.jsonSchema.isValid(dyn, { data: 1, children: [{ data: 2, children: [] }] }));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function testErrorsShape() {
|
|
98
|
+
var r = b.jsonSchema.validate({ type: "object", properties: { n: { type: "integer" } } }, { n: "bad" });
|
|
99
|
+
check("validate returns {valid, errors}", r.valid === false && Array.isArray(r.errors) && r.errors.length >= 1);
|
|
100
|
+
check("error names instancePath + keyword", r.errors[0].instancePath === "/n" && r.errors[0].keyword === "type");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function testFormat() {
|
|
104
|
+
// format is an annotation by default (does not assert).
|
|
105
|
+
check("format annotation by default", b.jsonSchema.isValid({ type: "string", format: "email" }, "not-an-email"));
|
|
106
|
+
// assertFormat:true turns it into an assertion.
|
|
107
|
+
check("assertFormat rejects bad email", !b.jsonSchema.isValid({ type: "string", format: "email" }, "nope", { assertFormat: true }));
|
|
108
|
+
check("assertFormat accepts good date-time", b.jsonSchema.isValid({ type: "string", format: "date-time" }, "2020-01-01T00:00:00Z", { assertFormat: true }));
|
|
109
|
+
// time requires an offset and valid ranges (RFC 3339 full-time).
|
|
110
|
+
check("time rejects missing offset", !b.jsonSchema.isValid({ format: "time" }, "12:00:00", { assertFormat: true }));
|
|
111
|
+
check("time rejects out-of-range", !b.jsonSchema.isValid({ format: "time" }, "25:61:61Z", { assertFormat: true }));
|
|
112
|
+
check("time accepts offset form", b.jsonSchema.isValid({ format: "time" }, "12:00:00+05:30", { assertFormat: true }));
|
|
113
|
+
// date enforces real field ranges.
|
|
114
|
+
check("date rejects month 13", !b.jsonSchema.isValid({ format: "date" }, "2020-13-01", { assertFormat: true }));
|
|
115
|
+
check("date accepts valid", b.jsonSchema.isValid({ format: "date" }, "2020-02-29", { assertFormat: true }));
|
|
116
|
+
// uri rejects raw spaces and relative refs.
|
|
117
|
+
check("uri rejects raw space", !b.jsonSchema.isValid({ format: "uri" }, "http://e xample.com", { assertFormat: true }));
|
|
118
|
+
check("uri rejects relative", !b.jsonSchema.isValid({ format: "uri" }, "/relative/path", { assertFormat: true }));
|
|
119
|
+
check("uri accepts absolute", b.jsonSchema.isValid({ format: "uri" }, "https://example.com/x", { assertFormat: true }));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function run() {
|
|
123
|
+
testSurface();
|
|
124
|
+
testAssertions();
|
|
125
|
+
testArrays();
|
|
126
|
+
testObjects();
|
|
127
|
+
testApplicators();
|
|
128
|
+
testUnevaluated();
|
|
129
|
+
testRefs();
|
|
130
|
+
testErrorsShape();
|
|
131
|
+
testFormat();
|
|
132
|
+
}
|
|
133
|
+
module.exports = { run: run };
|
|
134
|
+
if (require.main === module) { run().then(function () { console.log("[json-schema] OK — " + helpers.getChecks() + " checks passed"); }, function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }); }
|