@blamejs/blamejs-shop 0.1.29 → 0.1.31

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +4 -1
  3. package/SECURITY.md +9 -0
  4. package/lib/admin.js +317 -1
  5. package/lib/asset-manifest.json +1 -1
  6. package/lib/product-qa.js +88 -0
  7. package/lib/storefront.js +640 -1
  8. package/lib/vendor/MANIFEST.json +2 -2
  9. package/lib/vendor/blamejs/CHANGELOG.md +10 -0
  10. package/lib/vendor/blamejs/README.md +4 -0
  11. package/lib/vendor/blamejs/api-snapshot.json +116 -2
  12. package/lib/vendor/blamejs/index.js +8 -0
  13. package/lib/vendor/blamejs/lib/acme.js +2 -2
  14. package/lib/vendor/blamejs/lib/auth/dpop.js +14 -44
  15. package/lib/vendor/blamejs/lib/base32.js +154 -0
  16. package/lib/vendor/blamejs/lib/dbsc.js +5 -18
  17. package/lib/vendor/blamejs/lib/json-schema.js +740 -0
  18. package/lib/vendor/blamejs/lib/jwk.js +127 -0
  19. package/lib/vendor/blamejs/lib/middleware/bot-guard.js +43 -6
  20. package/lib/vendor/blamejs/lib/totp.js +10 -31
  21. package/lib/vendor/blamejs/lib/uri-template.js +286 -0
  22. package/lib/vendor/blamejs/package.json +1 -1
  23. package/lib/vendor/blamejs/release-notes/v0.12.64.json +18 -0
  24. package/lib/vendor/blamejs/release-notes/v0.12.65.json +27 -0
  25. package/lib/vendor/blamejs/release-notes/v0.12.66.json +18 -0
  26. package/lib/vendor/blamejs/release-notes/v0.12.68.json +27 -0
  27. package/lib/vendor/blamejs/release-notes/v0.12.69.json +27 -0
  28. package/lib/vendor/blamejs/test/layer-0-primitives/base32.test.js +79 -0
  29. package/lib/vendor/blamejs/test/layer-0-primitives/bot-guard.test.js +102 -0
  30. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +4 -1
  31. package/lib/vendor/blamejs/test/layer-0-primitives/json-schema.test.js +134 -0
  32. package/lib/vendor/blamejs/test/layer-0-primitives/jwk.test.js +72 -0
  33. package/lib/vendor/blamejs/test/layer-0-primitives/uri-template.test.js +99 -0
  34. package/package.json +1 -1
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.jwk
4
+ * @nav Identity
5
+ * @title JWK Thumbprint
6
+ *
7
+ * @intro
8
+ * Compute the <a href="https://www.rfc-editor.org/rfc/rfc7638">RFC 7638</a>
9
+ * thumbprint of a JSON Web Key — the canonical, hash-based identifier used
10
+ * to name a key (DPoP <code>jkt</code> bindings, ACME account-key
11
+ * thumbprints per RFC 8555, DBSC session pins, and <code>kid</code>
12
+ * derivation). The thumbprint is
13
+ * <code>base64url(SHA-256(canonical-JSON))</code>, where the canonical
14
+ * JSON contains only the key-type's required members, with member names
15
+ * in lexicographic order and no whitespace — so the same key always
16
+ * produces the same thumbprint regardless of how its JWK was serialized.
17
+ *
18
+ * <code>thumbprint(jwk)</code> returns the base64url digest;
19
+ * <code>canonicalize(jwk)</code> returns the exact JSON string that is
20
+ * hashed. The standard key types are supported — EC, RSA, oct, and OKP
21
+ * (RFC 8037 Ed25519 / X25519) — plus AKP, the IANA key type Node uses for
22
+ * ML-DSA / SLH-DSA post-quantum public keys. SHA-256 is the default;
23
+ * <code>hash: "sha384" | "sha512"</code> selects a longer digest
24
+ * (RFC 9278 thumbprint-with-hash).
25
+ *
26
+ * @card
27
+ * RFC 7638 JWK thumbprint — the canonical
28
+ * <code>base64url(SHA-256(canonical-JSON))</code> identifier for a JSON
29
+ * Web Key (EC / RSA / oct / OKP / AKP), behind DPoP <code>jkt</code>,
30
+ * ACME account keys, and DBSC session pins.
31
+ */
32
+
33
+ var nodeCrypto = require("node:crypto");
34
+ var canonicalJson = require("./canonical-json");
35
+ var { defineClass } = require("./framework-error");
36
+
37
+ var JwkError = defineClass("JwkError", { alwaysPermanent: true });
38
+
39
+ var HASHES = { sha256: "sha256", sha384: "sha384", sha512: "sha512" };
40
+
41
+ // RFC 7638 §3.2 + JWA: the required members per key type, which (and only
42
+ // which) participate in the thumbprint. Listed for documentation; the
43
+ // canonical form is produced with lexicographic ordering regardless.
44
+ var REQUIRED = {
45
+ EC: ["crv", "kty", "x", "y"],
46
+ RSA: ["e", "kty", "n"],
47
+ oct: ["k", "kty"],
48
+ OKP: ["crv", "kty", "x"], // RFC 8037
49
+ AKP: ["alg", "kty", "pub"], // IANA AKP — ML-DSA / SLH-DSA public keys
50
+ };
51
+
52
+ function _requiredMembers(jwk) {
53
+ if (!jwk || typeof jwk !== "object" || Array.isArray(jwk)) {
54
+ throw new JwkError("jwk/bad-jwk", "jwk: must be a JWK object");
55
+ }
56
+ if (typeof jwk.kty !== "string" || jwk.kty.length === 0) {
57
+ throw new JwkError("jwk/bad-jwk", "jwk: 'kty' is required");
58
+ }
59
+ var names = REQUIRED[jwk.kty];
60
+ if (!names) throw new JwkError("jwk/unsupported-kty", "jwk: unsupported kty '" + jwk.kty + "'");
61
+ var out = {};
62
+ for (var i = 0; i < names.length; i++) {
63
+ var n = names[i];
64
+ if (typeof jwk[n] !== "string" || jwk[n].length === 0) {
65
+ throw new JwkError("jwk/bad-jwk", "jwk: " + jwk.kty + " key requires a string '" + n + "' member");
66
+ }
67
+ out[n] = jwk[n];
68
+ }
69
+ return out;
70
+ }
71
+
72
+ /**
73
+ * @primitive b.jwk.canonicalize
74
+ * @signature b.jwk.canonicalize(jwk)
75
+ * @since 0.12.68
76
+ * @status stable
77
+ * @related b.jwk.thumbprint
78
+ *
79
+ * Return the RFC 7638 canonical JSON string for a JWK — only the key-type's
80
+ * required members, member names in lexicographic order, no whitespace.
81
+ * This is the exact input that <code>thumbprint</code> hashes. Throws
82
+ * <code>JwkError</code> for a missing <code>kty</code>, an unsupported key
83
+ * type, or a missing required member.
84
+ *
85
+ * @example
86
+ * b.jwk.canonicalize({ kty: "EC", crv: "P-256", x: "...", y: "...", use: "sig" });
87
+ * // → '{"crv":"P-256","kty":"EC","x":"...","y":"..."}' (use omitted)
88
+ */
89
+ function canonicalize(jwk) {
90
+ return canonicalJson.stringify(_requiredMembers(jwk));
91
+ }
92
+
93
+ /**
94
+ * @primitive b.jwk.thumbprint
95
+ * @signature b.jwk.thumbprint(jwk, opts?)
96
+ * @since 0.12.68
97
+ * @status stable
98
+ * @related b.jwk.canonicalize
99
+ *
100
+ * Compute the RFC 7638 thumbprint of a JWK:
101
+ * <code>base64url(hash(canonicalJSON))</code>. Only the key-type's required
102
+ * members feed the hash, so optional fields (<code>kid</code>,
103
+ * <code>use</code>, <code>alg</code>, …) never change the result. SHA-256
104
+ * is the default digest; <code>hash</code> selects a longer one. Throws
105
+ * <code>JwkError</code> on an invalid JWK or unknown hash.
106
+ *
107
+ * @opts
108
+ * hash: "sha256" | "sha384" | "sha512", // default: "sha256"
109
+ *
110
+ * @example
111
+ * b.jwk.thumbprint({ kty: "RSA", e: "AQAB", n: "0vx7ago...DKgw" });
112
+ * // → "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
113
+ */
114
+ function thumbprint(jwk, opts) {
115
+ opts = opts || {};
116
+ var hash = HASHES[opts.hash || "sha256"];
117
+ if (!hash) throw new JwkError("jwk/bad-hash", "jwk.thumbprint: hash must be sha256, sha384, or sha512");
118
+ var canon = canonicalize(jwk);
119
+ return nodeCrypto.createHash(hash).update(canon, "utf8").digest("base64url");
120
+ }
121
+
122
+ module.exports = {
123
+ thumbprint: thumbprint,
124
+ canonicalize: canonicalize,
125
+ REQUIRED: REQUIRED,
126
+ JwkError: JwkError,
127
+ };
@@ -6,8 +6,12 @@
6
6
  *
7
7
  * Heuristics (all combined):
8
8
  * - Missing Accept-Language header (real browsers always send one)
9
- * - Missing Sec-Fetch-Mode header (modern browsers send these on every
10
- * navigation; absence is suspicious for HTML routes but not API)
9
+ * - Missing Sec-Fetch-Mode header ADVISORY ONLY (never blocks). Tagged
10
+ * in mode:"tag" on secure-context HTML GETs where a modern browser
11
+ * would have sent it. It cannot block because the header is absent for
12
+ * entire browser families (Safari < 16.4) and for every plain-HTTP
13
+ * non-localhost origin (Umbrel, LAN / *.local proxies) — a 403 on it
14
+ * alone would refuse real users.
11
15
  * - User-Agent matches known automation libraries (curl, wget, python-
12
16
  * requests, axios, Go-http-client) — operators can add or remove
13
17
  * entries via config
@@ -86,9 +90,13 @@ function _xffIpFor(trustProxy) {
86
90
  * Cheap fingerprint-based detection of obviously-non-browser requests.
87
91
  * Constructed via `b.middleware.botGuard(opts)`; the resulting
88
92
  * middleware has the `(req, res, next)` shape shown above.
89
- * Combines three heuristics: missing `Accept-Language`, missing
90
- * `Sec-Fetch-Mode` (HTML routes), and User-Agent regex match against
91
- * a default list (curl / wget / python-requests / axios / etc.). Not
93
+ * Two blocking heuristics missing `Accept-Language` and a User-Agent
94
+ * regex match against a default list (curl / wget / python-requests /
95
+ * axios / etc.) plus one advisory signal: a missing `Sec-Fetch-Mode`
96
+ * on a secure-context HTML GET sets `req.suspectedBot` in `mode: "tag"`
97
+ * but NEVER blocks (the header is absent for Safari < 16.4 and every
98
+ * plain-HTTP non-localhost origin, so blocking on it refuses real
99
+ * users). Not
92
100
  * a substitute for proper authentication — catches drive-by scrapers
93
101
  * and low-effort bots. In `mode: "block"` (default) the request is
94
102
  * refused; in `mode: "tag"` `req.suspectedBot = true` is set and the
@@ -152,6 +160,28 @@ function create(opts) {
152
160
  return /^\/api\//.test(path);
153
161
  }
154
162
 
163
+ // Browsers only emit Fetch Metadata (Sec-Fetch-*) in a *secure context*
164
+ // (W3C Secure Contexts): an HTTPS origin, or a localhost-family origin
165
+ // even over plain HTTP. On a plain-HTTP non-localhost origin — an Umbrel
166
+ // app, a LAN / *.local reverse-proxy deployment — the browser omits
167
+ // Sec-Fetch-* entirely, so a missing Sec-Fetch-Mode is NORMAL there and
168
+ // must not be read as a bot signal. The effective scheme honours
169
+ // X-Forwarded-Proto only under trustProxy (otherwise it is forgeable).
170
+ function _isSecureContext(req) {
171
+ if (requestHelpers.requestProtocol(req, { trustProxy: trustProxy }) === "https") return true;
172
+ var host = (req.headers && req.headers.host) || "";
173
+ host = String(host).toLowerCase().replace(/:\d+$/, ""); // strip :port
174
+ if (host.charAt(0) === "[") { // [::1] IPv6 literal
175
+ var end = host.indexOf("]");
176
+ host = end === -1 ? host.slice(1) : host.slice(1, end);
177
+ }
178
+ host = host.replace(/\.$/, ""); // strip trailing root-zone dot (RFC 1034 §3.1) so "localhost." matches
179
+ if (host === "localhost" || /\.localhost$/.test(host)) return true;
180
+ if (host === "::1") return true;
181
+ if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host)) return true; // allow:regex-no-length-cap — bounded dotted-quad loopback
182
+ return false;
183
+ }
184
+
155
185
  function _checkHeuristics(req) {
156
186
  var headers = req.headers || {};
157
187
  var ua = headers["user-agent"] || "";
@@ -167,7 +197,14 @@ function create(opts) {
167
197
  return null;
168
198
  }
169
199
  if (!headers["accept-language"]) return "missing-accept-language";
170
- if (req.method === "GET" && !headers["sec-fetch-mode"]) return "missing-sec-fetch-mode";
200
+ // Missing Sec-Fetch-Mode NEVER blocks: the header is absent for entire
201
+ // browser families (Safari < 16.4 omits Fetch Metadata even over HTTPS)
202
+ // and for every plain-HTTP non-localhost origin (Umbrel, LAN / *.local
203
+ // reverse proxies), so a 403 on it alone refuses real users. It survives
204
+ // only as an advisory TAG in mode:"tag", and even then only in a secure
205
+ // context where a modern browser would have sent it. Drive-by bots are
206
+ // still blocked by missing Accept-Language + the User-Agent deny-list.
207
+ if (mode === "tag" && req.method === "GET" && _isSecureContext(req) && !headers["sec-fetch-mode"]) return "missing-sec-fetch-mode";
171
208
  return null;
172
209
  }
173
210
 
@@ -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
- var BASE32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
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
- var bits = "";
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
- var bits = "";
111
- for (var i = 0; i < str.length; i++) {
112
- var c = str[i].toUpperCase();
113
- if (c === "=" || c === " " || c === "-") continue; // spaces + dashes + padding
114
- var idx = BASE32.indexOf(c);
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>{&amp;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
+ };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.12.63",
3
+ "version": "0.12.69",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -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
+ }