@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 +2 -0
- package/lib/middleware/bot-guard.js +43 -6
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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
|
|
10
|
-
*
|
|
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
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
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
|
-
|
|
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
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:99cf7113-3c76-4163-809f-e0478728ac1c",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.12.69",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.12.69",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|