@blamejs/core 0.7.86 → 0.7.87
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.7.x
|
|
10
10
|
|
|
11
|
+
- **0.7.87** (2026-05-06) — Two route-guard middlewares for API hardening: `b.middleware.requireMethods` + `b.middleware.requireContentType`. **`b.middleware.requireMethods(["GET", "POST"])`** refuses any HTTP method outside the allowlist with `405 Method Not Allowed` + `Allow:` header listing the allowed methods (per RFC 9110 §15.5.6). Defends against unexpected verb routing — many CVE-class bugs trace to a route handler wired for GET that accidentally accepts arbitrary verbs (PROPFIND, OPTIONS, custom). **`b.middleware.requireContentType(["application/json"])`** refuses requests with a body (POST/PUT/PATCH by default) whose `Content-Type` isn't in the allowlist with `415 Unsupported Media Type` + `Accept:` header listing the allowed types (RFC 9110 §15.5.16). Defends against MIME-type confusion — a route that processes JSON shouldn't accept `application/x-www-form-urlencoded` even if the body parses. Both middlewares emit observability events (`middleware.requireMethods.denied` / `middleware.requireContentType.denied`) on every refusal for triage. Operators wanting to enforce content-type on idempotent verbs that DO carry bodies (rare DELETE-with-body shapes) override the default body-method list via `requireContentType(types, { methods })`.
|
|
12
|
+
|
|
11
13
|
- **0.7.86** (2026-05-06) — `b.middleware.csrfProtect({ requireJsonContentType: true })` — strict-fetch mode for JSON-only API surfaces. State-changing requests (POST/PUT/PATCH/DELETE) without `Content-Type: application/json` are refused before the token check with `CSRF: state-changing requests require Content-Type: application/json.` and audit emission `csrf.denied` with `reason: "non-JSON content-type: ..."`. The browser's form-encoded POST shape is the canonical CSRF vector — a malicious page can `<form action="/transfer" method=POST>` a victim into a state-changing request without a preflight. An `application/json` body forces a CORS preflight (the browser refuses to skip it for non-simple Content-Type values), so an attacker without an operator-allowlisted CORS origin can't reach the route at all. Default `false` — operators with HTML form submissions on the same routes (mixed SPA + classic form pages) keep current behavior; pure-fetch API operators opt in.
|
|
12
14
|
|
|
13
15
|
- **0.7.85** (2026-05-06) — `b.auth.statusList` — OAuth Token Status List (draft-ietf-oauth-status-list-20). The canonical credential-revocation mechanism for SD-JWT VC and OpenID for Verifiable Credentials. An issuer publishes a JWT-wrapped bitstring at a URL; relying parties fetch + check the bit at index N to determine if the credential whose `status_list` claim points at that URL+index is valid / invalid / suspended / application-specific. **`b.auth.statusList.create({ size, bits?, fill? })`** allocates a bit-packed buffer (`bits` ∈ {1, 2, 4, 8} per draft §6.1.1, default 1). The returned object exposes `.set(idx, status)` / `.get(idx)` / `.snapshot()` / `.toJwt({ issuer, subject, privateKey, algorithm, expiresInSec?, ... })`. **`b.auth.statusList.fromJwt(token, { publicKey | keyResolver, algorithms?, expectedIssuer?, ... })`** verifies the JWT through `b.auth.jwt.verify` and returns `{ list, claims }` — the list exposes `.get(idx)` to check status of an individual credential without decompressing into a separate Buffer. The bitstring is zlib-deflated (RFC 1951 raw deflate per draft §6.1.4) before base64url encoding so a million-entry list collapses to ~125 KB on the wire when most bits are zero. Caps the compressed payload at 1 MiB; operators publishing larger lists shard. Status constants exported as `b.auth.statusList.STATUS_{VALID,INVALID,SUSPENDED,APPLICATION_SPECIFIC}`. Foundational for the v0.7.58 EU AI Act + eIDAS 2.0 wallet slice.
|
package/lib/middleware/index.js
CHANGED
|
@@ -40,6 +40,8 @@ var requestId = require("./request-id");
|
|
|
40
40
|
var requestLog = require("./request-log");
|
|
41
41
|
var requireAal = require("./require-aal");
|
|
42
42
|
var requireAuth = require("./require-auth");
|
|
43
|
+
var requireContentType = require("./require-content-type");
|
|
44
|
+
var requireMethods = require("./require-methods");
|
|
43
45
|
var securityHeaders = require("./security-headers");
|
|
44
46
|
var securityTxt = require("./security-txt");
|
|
45
47
|
var sse = require("./sse");
|
|
@@ -55,6 +57,8 @@ module.exports = {
|
|
|
55
57
|
bearerAuth: bearerAuth.create,
|
|
56
58
|
requireAal: requireAal.create,
|
|
57
59
|
requireAuth: requireAuth.create,
|
|
60
|
+
requireContentType: requireContentType.create,
|
|
61
|
+
requireMethods: requireMethods.create,
|
|
58
62
|
csrfProtect: csrfProtect.create,
|
|
59
63
|
fetchMetadata: fetchMetadata.create,
|
|
60
64
|
gpc: gpc.create,
|
|
@@ -85,6 +89,8 @@ module.exports = {
|
|
|
85
89
|
bearerAuth: bearerAuth,
|
|
86
90
|
requireAal: requireAal,
|
|
87
91
|
requireAuth: requireAuth,
|
|
92
|
+
requireContentType: requireContentType,
|
|
93
|
+
requireMethods: requireMethods,
|
|
88
94
|
csrfProtect: csrfProtect,
|
|
89
95
|
fetchMetadata: fetchMetadata,
|
|
90
96
|
bodyParser: bodyParser,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* require-content-type middleware — refuses requests with a body
|
|
4
|
+
* (POST/PUT/PATCH) whose `Content-Type` header isn't in the
|
|
5
|
+
* operator-supplied allowlist.
|
|
6
|
+
*
|
|
7
|
+
* Defense against MIME-type confusion: a route that processes JSON
|
|
8
|
+
* shouldn't accept `application/x-www-form-urlencoded` even if the
|
|
9
|
+
* body parses (and vice versa). The middleware refuses with 415
|
|
10
|
+
* before the body parser runs, per RFC 9110 §15.5.16.
|
|
11
|
+
*
|
|
12
|
+
* router.use(b.middleware.requireContentType(["application/json"]));
|
|
13
|
+
*
|
|
14
|
+
* GET / HEAD / DELETE / OPTIONS without a body bypass the check by
|
|
15
|
+
* default. Operators wanting to enforce content-type on idempotent
|
|
16
|
+
* verbs that DO carry bodies (rare DELETE-with-body shapes) pass
|
|
17
|
+
* `methods` to override.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
var lazyRequire = require("../lazy-require");
|
|
21
|
+
var { defineClass } = require("../framework-error");
|
|
22
|
+
|
|
23
|
+
var RequireContentTypeError = defineClass("RequireContentTypeError", { alwaysPermanent: true });
|
|
24
|
+
|
|
25
|
+
var observability = lazyRequire(function () { return require("../observability"); });
|
|
26
|
+
|
|
27
|
+
var DEFAULT_BODY_METHODS = ["POST", "PUT", "PATCH"];
|
|
28
|
+
|
|
29
|
+
function _normalizeAllowed(types) {
|
|
30
|
+
if (!Array.isArray(types) || types.length === 0) return null;
|
|
31
|
+
var out = [];
|
|
32
|
+
for (var i = 0; i < types.length; i += 1) {
|
|
33
|
+
var t = types[i];
|
|
34
|
+
if (typeof t !== "string" || t.length === 0) return null;
|
|
35
|
+
var bare = t.split(";")[0].trim().toLowerCase();
|
|
36
|
+
if (bare.length === 0) return null;
|
|
37
|
+
out.push(bare);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function create(allowed, opts) {
|
|
43
|
+
var normalized = _normalizeAllowed(allowed);
|
|
44
|
+
if (!normalized) {
|
|
45
|
+
throw new RequireContentTypeError("require-content-type/no-allowlist",
|
|
46
|
+
"middleware.requireContentType: first argument must be a non-empty array of content-type strings");
|
|
47
|
+
}
|
|
48
|
+
opts = opts || {};
|
|
49
|
+
var methods = Array.isArray(opts.methods) && opts.methods.length > 0
|
|
50
|
+
? opts.methods.map(function (m) { return m.toUpperCase(); })
|
|
51
|
+
: DEFAULT_BODY_METHODS.slice();
|
|
52
|
+
var auditOn = opts.audit !== false;
|
|
53
|
+
|
|
54
|
+
return function requireContentTypeMiddleware(req, res, next) {
|
|
55
|
+
var m = (req.method || "").toUpperCase();
|
|
56
|
+
if (methods.indexOf(m) === -1) return next();
|
|
57
|
+
var ct = req.headers && req.headers["content-type"];
|
|
58
|
+
var bare = (typeof ct === "string" ? ct.split(";")[0].trim().toLowerCase() : "");
|
|
59
|
+
if (bare.length > 0 && normalized.indexOf(bare) !== -1) return next();
|
|
60
|
+
if (!res.headersSent) {
|
|
61
|
+
var body = "Unsupported Media Type";
|
|
62
|
+
res.writeHead(415, { // allow:raw-byte-literal — HTTP 415 status
|
|
63
|
+
"Accept": normalized.join(", "),
|
|
64
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
65
|
+
"Content-Length": Buffer.byteLength(body),
|
|
66
|
+
});
|
|
67
|
+
res.end(body);
|
|
68
|
+
}
|
|
69
|
+
if (auditOn) {
|
|
70
|
+
try {
|
|
71
|
+
observability().safeEvent("middleware.requireContentType.denied", 1, {
|
|
72
|
+
method: m, contentType: bare || "<absent>", route: req.url,
|
|
73
|
+
});
|
|
74
|
+
} catch (_e) { /* drop-silent */ }
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
create: create,
|
|
81
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* require-methods middleware — refuses HTTP methods outside an
|
|
4
|
+
* operator-supplied allowlist.
|
|
5
|
+
*
|
|
6
|
+
* Defense against unexpected verb routing. Many CVE-class bugs trace
|
|
7
|
+
* to a route handler that was wired for GET but accidentally also
|
|
8
|
+
* accepts arbitrary verbs (PROPFIND, OPTIONS, custom). Mounting
|
|
9
|
+
* `requireMethods(["GET", "POST"])` on the route blocks anything
|
|
10
|
+
* outside the allowlist before the handler sees the request.
|
|
11
|
+
*
|
|
12
|
+
* router.use(b.middleware.requireMethods(["GET", "POST"]));
|
|
13
|
+
*
|
|
14
|
+
* Refusal returns 405 with `Allow:` listing the allowed methods, per
|
|
15
|
+
* RFC 9110 §15.5.6.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
var lazyRequire = require("../lazy-require");
|
|
19
|
+
var { defineClass } = require("../framework-error");
|
|
20
|
+
|
|
21
|
+
var RequireMethodsError = defineClass("RequireMethodsError", { alwaysPermanent: true });
|
|
22
|
+
|
|
23
|
+
var observability = lazyRequire(function () { return require("../observability"); });
|
|
24
|
+
|
|
25
|
+
function create(allowed, opts) {
|
|
26
|
+
if (!Array.isArray(allowed) || allowed.length === 0) {
|
|
27
|
+
throw new RequireMethodsError("require-methods/no-allowlist",
|
|
28
|
+
"middleware.requireMethods: first argument must be a non-empty array of HTTP methods");
|
|
29
|
+
}
|
|
30
|
+
var normalized = [];
|
|
31
|
+
for (var i = 0; i < allowed.length; i += 1) {
|
|
32
|
+
if (typeof allowed[i] !== "string" || allowed[i].length === 0) {
|
|
33
|
+
throw new RequireMethodsError("require-methods/bad-method",
|
|
34
|
+
"middleware.requireMethods: method[" + i + "] must be a non-empty string");
|
|
35
|
+
}
|
|
36
|
+
if (/[\r\n\0\s,;]/.test(allowed[i])) {
|
|
37
|
+
throw new RequireMethodsError("require-methods/bad-method",
|
|
38
|
+
"middleware.requireMethods: method[" + i + "] contains forbidden whitespace / separator characters");
|
|
39
|
+
}
|
|
40
|
+
normalized.push(allowed[i].toUpperCase());
|
|
41
|
+
}
|
|
42
|
+
var allowHeader = normalized.join(", ");
|
|
43
|
+
opts = opts || {};
|
|
44
|
+
var auditOn = opts.audit !== false;
|
|
45
|
+
|
|
46
|
+
return function requireMethodsMiddleware(req, res, next) {
|
|
47
|
+
var m = (req.method || "").toUpperCase();
|
|
48
|
+
if (normalized.indexOf(m) !== -1) return next();
|
|
49
|
+
if (!res.headersSent) {
|
|
50
|
+
var body = "Method Not Allowed";
|
|
51
|
+
res.writeHead(405, { // allow:raw-byte-literal — HTTP 405 status
|
|
52
|
+
"Allow": allowHeader,
|
|
53
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
54
|
+
"Content-Length": Buffer.byteLength(body),
|
|
55
|
+
});
|
|
56
|
+
res.end(body);
|
|
57
|
+
}
|
|
58
|
+
if (auditOn) {
|
|
59
|
+
try {
|
|
60
|
+
observability().safeEvent("middleware.requireMethods.denied", 1, {
|
|
61
|
+
method: m, route: req.url,
|
|
62
|
+
});
|
|
63
|
+
} catch (_e) { /* drop-silent */ }
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
create: create,
|
|
70
|
+
};
|
package/lib/ntp-check.js
CHANGED
|
@@ -124,10 +124,22 @@ function querySingle(server, opts) {
|
|
|
124
124
|
return done({ code: "ntp/bad-reply", message: "reply too short (" + (msg && msg.length) + " bytes)" });
|
|
125
125
|
}
|
|
126
126
|
// Bytes 40-47 = Transmit Timestamp (NTP epoch seconds.fraction)
|
|
127
|
-
var ntpSeconds = msg.readUInt32BE(40);
|
|
128
|
-
var ntpFraction = msg.readUInt32BE(44);
|
|
127
|
+
var ntpSeconds = msg.readUInt32BE(40); // allow:raw-byte-literal — NTP packet offset
|
|
128
|
+
var ntpFraction = msg.readUInt32BE(44); // allow:raw-byte-literal — NTP packet offset
|
|
129
|
+
// Refuse a reply whose Transmit Timestamp is zero or earlier than
|
|
130
|
+
// the NTP epoch (1900-01-01). RFC 5905 §7.3 — a Stratum-16
|
|
131
|
+
// unsynchronized server emits 0 here; fed to the Unix-offset
|
|
132
|
+
// subtraction it produces a large-negative serverUnixSeconds
|
|
133
|
+
// that crashes downstream C.TIME helpers (which require non-
|
|
134
|
+
// negative finite). Treat as "unsynchronized peer — no drift
|
|
135
|
+
// measurement possible" rather than throw out of the dgram
|
|
136
|
+
// 'message' handler.
|
|
137
|
+
if (ntpSeconds < NTP_TO_UNIX_OFFSET_SECONDS) {
|
|
138
|
+
return done({ code: "ntp/unsynchronized",
|
|
139
|
+
message: "server returned NTP transmit timestamp < Unix epoch (likely Stratum-16 unsynchronized)" });
|
|
140
|
+
}
|
|
129
141
|
var serverUnixSeconds = ntpSeconds - NTP_TO_UNIX_OFFSET_SECONDS;
|
|
130
|
-
var fracMs = Math.round(C.TIME.seconds(ntpFraction / 0x100000000));
|
|
142
|
+
var fracMs = Math.round(C.TIME.seconds(ntpFraction / 0x100000000)); // allow:raw-byte-literal — NTP fraction divisor (2^32)
|
|
131
143
|
var serverTimeMs = C.TIME.seconds(serverUnixSeconds) + fracMs;
|
|
132
144
|
|
|
133
145
|
// Round-trip-corrected drift: assume the server's reply transmit
|
package/package.json
CHANGED
package/sbom.cyclonedx.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:96ee5394-43c1-471e-98e9-f3b59c96f3e0",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-06T06:
|
|
8
|
+
"timestamp": "2026-05-06T06:37:03.759Z",
|
|
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.7.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.7.87",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.7.
|
|
25
|
+
"version": "0.7.87",
|
|
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.7.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.7.87",
|
|
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.7.
|
|
57
|
+
"ref": "@blamejs/core@0.7.87",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|