@blamejs/core 0.8.4 → 0.8.5

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.8.x
10
10
 
11
+ - **0.8.5** (2026-05-06) — Vendor-currency CI gate + `b.middleware.requireMtls` primitive. **Vendor-currency check** — `scripts/check-vendor-currency.js` + new CI job in `ci.yml` assert every npm-mapped vendored bundle in `lib/vendor/MANIFEST.json` matches the latest published version on the npm registry. Per-component check on meta-bundles (e.g. `peculiar-pki` → `@peculiar/x509` + `pkijs`). Master-branch corpus entries (`SecLists`) are checked against the GitHub Commits API for the bundled file's path on the source repo's default branch — if the upstream has commits newer than the manifest's `bundledAt` date, the gate fails. Registry errors stay advisory unless `BLAMEJS_VENDOR_CURRENCY_STRICT=1`. Operators run locally with `npm run check:vendor-currency`. **`b.middleware.requireMtls`** — new soft-enforcement middleware that rejects requests without an authenticated client certificate. Composes with `b.crypto.hashCertFingerprint` / `isCertRevoked` (added in v0.8.4) so operators pass `fingerprintAllowList: [...]` and `denyList: [...]` and the middleware does the constant-time match. `req.peerCert` + `req.peerFingerprint` are attached for downstream handlers. Audits `mtls.required.allowed` / `mtls.required.refused` with reason metadata.
12
+
11
13
  - **0.8.4** (2026-05-06) — Supply-chain scanner findings + outbound HTTP posture + npm-publish unblock. **Outbound network surface** — `b.observability.otlpExporter` no longer defaults to `globalThis.fetch`; the default transport is now `b.httpClient` (`node:https` through the framework's PQC-hybrid agent + cert-pinning + SSRF guard). The prior default leaked an outbound network surface that supply-chain scanners flagged because it sat outside the framework's TLS posture; operators on fetch-only edge runtimes still override via `opts.fetchImpl`. **Dev-mode child-process isolation** — `b.dev.create()` now lazy-requires `child_process` (was top-level — flagged on every install regardless of whether `b.dev` was used) and refuses to construct when `NODE_ENV=production` unless the operator passes `opts.allowProduction:true` with an audited reason. A misconfigured production deploy that accidentally wires the dev-mode restart loop now crashes loudly at boot rather than spawning shells on every save. **Outbound posture additions** — `b.httpClient.request` adds `responseMode: "always-resolve"` (every response resolves with `{statusCode, headers, body}` regardless of HTTP status) plus `onRedirect({from, to, hop, headersStripped, statusCode, method})` hook (operators can throw to abort or rewrite the redirect chain). `b.wsClient.connect` adds `urlFor(attempt)` and `tlsOptsFor(attempt)` per-dial overrides for between-reconnect URL / TLS rotation; the new URL is re-validated through `ssrfGuard` so a hostile upstream can't redirect a reconnecting client at a private address. `b.wsClient` swallows post-`close()` `ECONNRESET` / `EPIPE` so a clean shutdown doesn't surface a noisy unhandled-error event. `b.pqcAgent.create({ ecdhCurve })` accepts a caller-supplied stricter list — operators can drop a group from the framework default but cannot widen with non-PQ groups (the prior hardcoded value blocked legitimate per-deployment narrowing). **Crypto helpers** — `b.crypto.hashCertFingerprint(pem|der)` returns `{ hex, colon }` SHA3-512 digests and `b.crypto.isCertRevoked(pemOrDer, denyList)` does a constant-time match. **Scheduler surface** — `b.scheduler.register(name, intervalMs, fn)` shorthand for the every-N-ms registration shape; `b.scheduler.getStatus()` returns an aggregate health surface for probes / dashboards (started flag, isLeader, per-task list, totals). **npm-publish unblock** — `test/layer-0-primitives/sd-jwt-vc.test.js` was asserting `DEFAULT_ALG === "ES256"` after v0.8.1 flipped the default to `ML-DSA-87`; the assertion now matches the lib (and `DEFAULT_HASH_ALG` for `sha3-512`). v0.8.1 / v0.8.2 / v0.8.3 all failed the npm-publish gate on this single test; this release re-enables the publish workflow.
12
14
 
13
15
  - **0.8.3** (2026-05-06) — Release-gate fixes + post-v0.8.2 hardening. Wiki primitive-section validator gate flagged `b.middleware.csrfProtect` opts-key drift — the `requireOrigin` opt added in v0.8.1 wasn't documented in the wiki seeder; now listed alongside `checkOrigin` / `allowedOrigins`. Gitleaks secret-scan gate flagged `{ privateKey, cipherText }` KEM-envelope shapes in `lib/crypto.js` error messages + `CHANGELOG.md` v0.8.0 entry as generic-api-key false positives — `.gitleaks.toml` adds an explicit regex allowlist for the parameter-name shape and pins the v0.8.0 commit fingerprints. Functional additions: `b.httpClient.request` adds `responseMode: "always-resolve"` opt — every request resolves with `{ statusCode, headers, body }` regardless of HTTP status (operators using the framework as an inbound-proxy upstream no longer have to wrap each call in a try/catch to recover the body of a 4xx/5xx). `b.wsClient` swallows post-close `ECONNRESET` / `EPIPE` errors so a clean `close()` doesn't surface a noisy unhandled-error event when the kernel races the FIN with an in-flight write. `SECURITY.md` documents the `allowInternal: true` test-pattern (legitimate same-host integration tests opt in explicitly with audited reason — never as a production default).
@@ -47,6 +47,7 @@ var requireAal = require("./require-aal");
47
47
  var requireAuth = require("./require-auth");
48
48
  var requireContentType = require("./require-content-type");
49
49
  var requireMethods = require("./require-methods");
50
+ var requireMtls = require("./require-mtls");
50
51
  var requireStepUp = require("./require-step-up");
51
52
  var securityHeaders = require("./security-headers");
52
53
  var securityTxt = require("./security-txt");
@@ -70,6 +71,7 @@ module.exports = {
70
71
  requireAuth: requireAuth.create,
71
72
  requireContentType: requireContentType.create,
72
73
  requireMethods: requireMethods.create,
74
+ requireMtls: requireMtls.create,
73
75
  requireStepUp: requireStepUp.create,
74
76
  csrfProtect: csrfProtect.create,
75
77
  fetchMetadata: fetchMetadata.create,
@@ -113,6 +115,7 @@ module.exports = {
113
115
  requireAuth: requireAuth,
114
116
  requireContentType: requireContentType,
115
117
  requireMethods: requireMethods,
118
+ requireMtls: requireMtls,
116
119
  requireStepUp: requireStepUp,
117
120
  csrfProtect: csrfProtect,
118
121
  fetchMetadata: fetchMetadata,
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ /**
3
+ * requireMtls middleware — soft-enforcement gate for routes that
4
+ * require a client certificate.
5
+ *
6
+ * Operators terminate TLS at the framework's HTTPS server with
7
+ * `requestCert: true` (the framework already wires this when
8
+ * `b.app({ tlsOptions: { requestCert: true, ca: [...] } })` is
9
+ * configured). For routes that MUST receive an authenticated peer
10
+ * cert — e.g. the inbound side of an mTLS service mesh, OAuth 2.0
11
+ * mTLS Client Authentication (RFC 8705), or operator-specific
12
+ * service-to-service endpoints — wire this middleware in front of
13
+ * the route to reject any request that didn't present a valid
14
+ * client cert.
15
+ *
16
+ * var requireMtls = b.middleware.requireMtls({
17
+ * fingerprintAllowList: [
18
+ * "AB:CD:EF:...", // colon-separated SHA3-512 hex
19
+ * ],
20
+ * denyList: [], // explicit revocations
21
+ * onAuthenticated: function (req, res, next) {
22
+ * req.peerSubject = req.peerCert.subject;
23
+ * next();
24
+ * },
25
+ * audit: b.audit,
26
+ * });
27
+ * router.use("/internal", requireMtls);
28
+ *
29
+ * Failure modes (all reject 401):
30
+ * - No peer cert presented (client did not negotiate mTLS)
31
+ * - Peer cert present but unauthorized at TLS layer
32
+ * (req.client.authorized === false)
33
+ * - Fingerprint not on the operator-supplied allow-list
34
+ * - Fingerprint on the operator-supplied deny-list
35
+ *
36
+ * Audit shape (when audit is wired): emits `mtls.required.allowed`
37
+ * (success) or `mtls.required.refused` (denied) with the peer-cert
38
+ * fingerprint + subject + reason in metadata. Drop-silent if no
39
+ * audit is wired.
40
+ *
41
+ * The fingerprint allow / deny comparison routes through
42
+ * b.crypto.isCertRevoked — both forms (lowercase hex / uppercase
43
+ * colon-separated) match. Allow-list of empty / null = "any
44
+ * peer cert authorized at the TLS layer"; specifying a non-empty
45
+ * allow-list ALSO requires the fingerprint to match.
46
+ */
47
+
48
+ var defineClass = require("../framework-error").defineClass;
49
+ var lazyRequire = require("../lazy-require");
50
+ var validateOpts = require("../validate-opts");
51
+
52
+ var crypto = lazyRequire(function () { return require("../crypto"); });
53
+ var audit = lazyRequire(function () { return require("../audit"); });
54
+
55
+ var RequireMtlsError = defineClass("RequireMtlsError", { alwaysPermanent: true });
56
+
57
+ function _normalizeFingerprintEntry(entry) {
58
+ if (typeof entry !== "string" || entry.length === 0) {
59
+ throw new RequireMtlsError("require-mtls/bad-fingerprint",
60
+ "fingerprint allow/deny entries must be non-empty strings " +
61
+ "(SHA3-512 hex or colon-separated form)");
62
+ }
63
+ return entry;
64
+ }
65
+
66
+ function create(opts) {
67
+ opts = opts || {};
68
+ validateOpts(opts, [
69
+ "fingerprintAllowList", "denyList",
70
+ "onAuthenticated", "audit",
71
+ "auditAction", "errorMessage",
72
+ ], "middleware.requireMtls");
73
+
74
+ var allowList = Array.isArray(opts.fingerprintAllowList)
75
+ ? opts.fingerprintAllowList.map(_normalizeFingerprintEntry) : null;
76
+ var denyList = Array.isArray(opts.denyList)
77
+ ? opts.denyList.map(_normalizeFingerprintEntry) : [];
78
+ var onAuthenticated = typeof opts.onAuthenticated === "function" ? opts.onAuthenticated : null;
79
+ var auditOn = opts.audit !== false;
80
+ var actionBase = typeof opts.auditAction === "string" && opts.auditAction.length > 0
81
+ ? opts.auditAction : "mtls.required";
82
+ var errorMessage = typeof opts.errorMessage === "string" && opts.errorMessage.length > 0
83
+ ? opts.errorMessage : "client certificate required";
84
+
85
+ function _emit(outcome, metadata) {
86
+ if (!auditOn) return;
87
+ try {
88
+ audit().safeEmit({
89
+ action: actionBase + (outcome === "success" ? ".allowed" : ".refused"),
90
+ outcome: outcome,
91
+ metadata: metadata || {},
92
+ });
93
+ } catch (_e) { /* drop-silent — audit is best-effort, never blocks the request */ }
94
+ }
95
+
96
+ function _refuse(res, reason, metadata) {
97
+ _emit("denied", Object.assign({ reason: reason }, metadata || {}));
98
+ if (typeof res.writeHead === "function") {
99
+ res.writeHead(401, {
100
+ "Content-Type": "application/json; charset=utf-8",
101
+ "WWW-Authenticate": "Mutual",
102
+ "Cache-Control": "no-store",
103
+ });
104
+ res.end(JSON.stringify({ error: errorMessage, reason: reason }));
105
+ }
106
+ }
107
+
108
+ return function requireMtlsMiddleware(req, res, next) {
109
+ // Node's TLSSocket exposes:
110
+ // req.client.authorized — boolean, peer cert chain valid
111
+ // req.client.authorizationError — string when authorized=false
112
+ // req.socket.getPeerCertificate() — the cert (raw + parsed fields)
113
+ // Behind a TLS-terminating proxy (e.g. nginx, envoy) operators
114
+ // pass the peer cert as a header (X-Client-Cert) and pre-populate
115
+ // req.peerCert before this middleware fires. We don't inject a
116
+ // proxy-header parser here — that's an operator-side decision tied
117
+ // to the chosen proxy's signing model.
118
+ var sock = req.socket || req.connection || null;
119
+ var authorized = sock && sock.authorized === true;
120
+ var peerCert = req.peerCert || null;
121
+ if (!peerCert && sock && typeof sock.getPeerCertificate === "function") {
122
+ try { peerCert = sock.getPeerCertificate(true) || null; }
123
+ catch (_e) { peerCert = null; }
124
+ }
125
+
126
+ if (!authorized) {
127
+ var authzError = (sock && sock.authorizationError) || "no-peer-cert";
128
+ return _refuse(res, "tls-unauthorized", { authorizationError: String(authzError) });
129
+ }
130
+ if (!peerCert || !peerCert.raw) {
131
+ return _refuse(res, "no-peer-cert", {});
132
+ }
133
+
134
+ // Compute fingerprint via the framework's SHA3-512 helper. Buffer
135
+ // form: peerCert.raw is the DER. Hex/colon both available for
136
+ // allow/deny matching.
137
+ var fp;
138
+ try {
139
+ fp = crypto().hashCertFingerprint(peerCert.raw);
140
+ } catch (e) {
141
+ return _refuse(res, "fingerprint-failed", { error: (e && e.message) || String(e) });
142
+ }
143
+
144
+ if (denyList.length > 0 && crypto().isCertRevoked(peerCert.raw, denyList)) {
145
+ return _refuse(res, "fingerprint-on-deny-list", {
146
+ fingerprint: fp.colon,
147
+ subject: (peerCert.subject && peerCert.subject.CN) || null,
148
+ });
149
+ }
150
+ if (allowList && allowList.length > 0 && !crypto().isCertRevoked(peerCert.raw, allowList)) {
151
+ return _refuse(res, "fingerprint-not-allowed", {
152
+ fingerprint: fp.colon,
153
+ subject: (peerCert.subject && peerCert.subject.CN) || null,
154
+ });
155
+ }
156
+
157
+ // Authenticated — attach the parsed peer cert + fingerprint to
158
+ // the request so downstream handlers don't have to re-parse, then
159
+ // emit success and call next (or operator's onAuthenticated hook).
160
+ req.peerCert = peerCert;
161
+ req.peerFingerprint = fp;
162
+ _emit("success", {
163
+ fingerprint: fp.colon,
164
+ subject: (peerCert.subject && peerCert.subject.CN) || null,
165
+ });
166
+ if (onAuthenticated) {
167
+ try { return onAuthenticated(req, res, next); }
168
+ catch (e) {
169
+ return _refuse(res, "on-authenticated-threw", { error: (e && e.message) || String(e) });
170
+ }
171
+ }
172
+ return next();
173
+ };
174
+ }
175
+
176
+ module.exports = {
177
+ create: create,
178
+ RequireMtlsError: RequireMtlsError,
179
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -70,7 +70,8 @@
70
70
  ],
71
71
  "scripts": {
72
72
  "test": "node test/smoke.js",
73
- "prepack": "node scripts/check-pack-against-gitignore.js"
73
+ "prepack": "node scripts/check-pack-against-gitignore.js",
74
+ "check:vendor-currency": "node scripts/check-vendor-currency.js"
74
75
  },
75
76
  "dependencies": {},
76
77
  "devDependencies": {}
@@ -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:91e8b760-7fe3-4fef-a317-8b1e4f8cc238",
5
+ "serialNumber": "urn:uuid:1f60d33c-fc3b-4da1-a753-bb1c98de9968",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-06T22:02:35.725Z",
8
+ "timestamp": "2026-05-06T22:33:10.715Z",
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.8.4",
22
+ "bom-ref": "@blamejs/core@0.8.5",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.8.4",
25
+ "version": "0.8.5",
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.8.4",
29
+ "purl": "pkg:npm/%40blamejs/core@0.8.5",
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.8.4",
57
+ "ref": "@blamejs/core@0.8.5",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]