@blamejs/core 0.12.68 → 0.12.69

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,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.12.x
10
10
 
11
+ - v0.12.69 (2026-05-26) — **`b.middleware.botGuard` no longer blocks browsers that omit Sec-Fetch-Mode.** b.middleware.botGuard treated a missing Sec-Fetch-Mode header as a bot signal and returned 403 Forbidden, which refused legitimate browsers on any origin where the browser does not emit Fetch Metadata: every plain-HTTP non-localhost origin (Umbrel apps, LAN and *.local reverse-proxy deployments) and Safari before 16.4 even over HTTPS. Browsers only send Sec-Fetch-* in a secure context, so its absence is normal there — not a bot. Sec-Fetch-Mode is now advisory only: it never blocks, and it sets req.suspectedBot in mode:"tag" only on a secure-context HTML GET where a modern browser would have sent it. Drive-by bots are still blocked by the missing-Accept-Language and User-Agent heuristics. No configuration change is needed; if you had widened skipPaths or disabled bot-guard to work around this, you can revert that. **Fixed:** *`b.middleware.botGuard` no longer 403s browsers over plain HTTP or older Safari* — A missing `Sec-Fetch-Mode` was a blocking heuristic, but browsers omit Fetch Metadata outside a secure context (every plain-HTTP non-localhost origin — Umbrel, LAN, `*.local` proxies) and Safari < 16.4 omits it even over HTTPS. Those legitimate browsers were refused with `403 Forbidden`. `Sec-Fetch-Mode` is now advisory: it never blocks, and only sets `req.suspectedBot` in `mode: "tag"` on a secure-context HTML GET. The `Accept-Language` and User-Agent heuristics (which catch the same bots) are unchanged. **Detectors:** *reserved-hostname trailing-dot detector recognizes regex strips* — The codebase-patterns gate that requires stripping the RFC 1034 trailing root-zone dot before a reserved-hostname comparison now also recognizes end-anchored regex strips (`.replace(/\.$/, …)`), not only the `charAt` / `while`-loop forms.
12
+
11
13
  - v0.12.68 (2026-05-26) — **`b.jwk` — RFC 7638 JWK thumbprint.** Compute the RFC 7638 thumbprint of a JSON Web Key — the canonical base64url(SHA-256(canonical-JSON)) identifier used to name a key (DPoP jkt bindings, ACME account-key thumbprints, DBSC session pins, kid derivation). b.jwk.thumbprint(jwk) returns the digest; b.jwk.canonicalize(jwk) returns the exact JSON that is hashed — only the key-type's required members, member names in lexicographic order, no whitespace, so the same key always yields the same thumbprint regardless of how its JWK was serialized. The standard key types are supported (EC, RSA, oct, OKP per RFC 8037) plus AKP, the IANA key type Node uses for ML-DSA / SLH-DSA post-quantum public keys; SHA-256 is the default, with hash: "sha384" | "sha512" for RFC 9278 thumbprint-with-hash. Verified against the RFC 7638 §3.1 worked example. b.auth.dpop, b.acme, and b.dbsc now compute their thumbprints through this primitive. **Added:** *`b.jwk.thumbprint` / `b.jwk.canonicalize`* — RFC 7638 JWK thumbprint. `thumbprint(jwk, opts)` returns `base64url(hash(canonical-JSON))` — only the key-type's required members feed the hash, so optional fields (`kid`, `use`, `alg`, …) never change the result. `canonicalize(jwk)` returns the canonical JSON string itself. Supports EC / RSA / oct / OKP and the AKP post-quantum key type; SHA-256 default, `hash` selects SHA-384 / SHA-512. Throws `JwkError` on an invalid key or unknown hash. **Changed:** *DPoP, ACME, and DBSC compose `b.jwk`* — `b.auth.dpop` (the `jkt` proof-key thumbprint), `b.acme` (the RFC 8555 account-key authorization), and `b.dbsc` (the session-pin thumbprint) now compute RFC 7638 thumbprints through `b.jwk` instead of carrying their own implementations. Behavior is unchanged — DPoP still refuses symmetric key types, and each surface keeps its own error codes.
12
14
 
13
15
  - v0.12.66 (2026-05-26) — **`b.uriTemplate` — RFC 6570 URI Template expansion.** 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). **Added:** *`b.uriTemplate.expand` / `b.uriTemplate.compile`* — 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`.
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.12.68",
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",
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:fafe52fa-5ff1-4704-af03-e5aeca4e55cf",
5
+ "serialNumber": "urn:uuid:99cf7113-3c76-4163-809f-e0478728ac1c",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-26T14:23:38.200Z",
8
+ "timestamp": "2026-05-26T15:45:38.862Z",
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.68",
22
+ "bom-ref": "@blamejs/core@0.12.69",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.12.68",
25
+ "version": "0.12.69",
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.68",
29
+ "purl": "pkg:npm/%40blamejs/core@0.12.69",
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.68",
57
+ "ref": "@blamejs/core@0.12.69",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]