@blamejs/blamejs-shop 0.3.7 → 0.3.9

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 (41) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/checkout.js +125 -1
  4. package/lib/storefront.js +1 -1
  5. package/lib/vendor/MANIFEST.json +2 -2
  6. package/lib/vendor/blamejs/.github/workflows/ci.yml +27 -0
  7. package/lib/vendor/blamejs/.github/workflows/codeql.yml +2 -2
  8. package/lib/vendor/blamejs/.github/workflows/release-container.yml +4 -4
  9. package/lib/vendor/blamejs/.github/workflows/scorecard.yml +1 -1
  10. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  11. package/lib/vendor/blamejs/README.md +1 -0
  12. package/lib/vendor/blamejs/SECURITY.md +2 -0
  13. package/lib/vendor/blamejs/api-snapshot.json +6 -2
  14. package/lib/vendor/blamejs/lib/cra-report.js +3 -3
  15. package/lib/vendor/blamejs/lib/middleware/age-gate.js +20 -7
  16. package/lib/vendor/blamejs/lib/middleware/bearer-auth.js +36 -35
  17. package/lib/vendor/blamejs/lib/middleware/bot-guard.js +17 -5
  18. package/lib/vendor/blamejs/lib/middleware/cors.js +28 -12
  19. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +22 -14
  20. package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +27 -13
  21. package/lib/vendor/blamejs/lib/middleware/deny-response.js +140 -0
  22. package/lib/vendor/blamejs/lib/middleware/dpop.js +32 -19
  23. package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +21 -12
  24. package/lib/vendor/blamejs/lib/middleware/host-allowlist.js +19 -8
  25. package/lib/vendor/blamejs/lib/middleware/index.js +3 -0
  26. package/lib/vendor/blamejs/lib/middleware/network-allowlist.js +24 -10
  27. package/lib/vendor/blamejs/lib/middleware/rate-limit.js +22 -5
  28. package/lib/vendor/blamejs/lib/middleware/require-aal.js +25 -10
  29. package/lib/vendor/blamejs/lib/middleware/require-auth.js +32 -16
  30. package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +49 -18
  31. package/lib/vendor/blamejs/lib/middleware/require-content-type.js +19 -8
  32. package/lib/vendor/blamejs/lib/middleware/require-methods.js +17 -7
  33. package/lib/vendor/blamejs/lib/middleware/require-mtls.js +27 -14
  34. package/lib/vendor/blamejs/lib/network.js +4 -4
  35. package/lib/vendor/blamejs/package.json +1 -1
  36. package/lib/vendor/blamejs/release-notes/v0.14.6.json +60 -0
  37. package/lib/vendor/blamejs/scripts/check-actions-currency.js +256 -0
  38. package/lib/vendor/blamejs/scripts/release.js +11 -0
  39. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +138 -0
  40. package/lib/vendor/blamejs/test/layer-0-primitives/deny-response.test.js +228 -0
  41. package/package.json +1 -1
@@ -51,6 +51,7 @@
51
51
  var defineClass = require("../framework-error").defineClass;
52
52
  var lazyRequire = require("../lazy-require");
53
53
  var validateOpts = require("../validate-opts");
54
+ var denyResponse = require("./deny-response").denyResponse;
54
55
 
55
56
  var bCrypto = lazyRequire(function () { return require("../crypto"); });
56
57
  var audit = lazyRequire(function () { return require("../audit"); });
@@ -98,6 +99,8 @@ function _timingSafeStringEqual(a, b) {
98
99
  * errorMessage: string,
99
100
  * auditAction: string,
100
101
  * audit: object,
102
+ * onDeny: function(req, res, info): void, // own the refusal; info = { status, reason, ...metadata }
103
+ * problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of the default JSON envelope
101
104
  * }
102
105
  *
103
106
  * @example
@@ -116,7 +119,7 @@ function create(opts) {
116
119
  validateOpts(opts, [
117
120
  "resolver", "requiredScopes", "getBoundField",
118
121
  "audit", "auditAction", "errorMessage",
119
- "tolerateMissingPeerCert",
122
+ "tolerateMissingPeerCert", "onDeny", "problemDetails",
120
123
  ], "middleware.requireBoundKey");
121
124
 
122
125
  if (typeof opts.resolver !== "function") {
@@ -150,6 +153,8 @@ function create(opts) {
150
153
  // even when peerCertFingerprints is set on the registered key.
151
154
  // Production deployments leave this at default false.
152
155
  var tolerateMissingPeerCert = !!opts.tolerateMissingPeerCert;
156
+ var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
157
+ var problemMode = opts.problemDetails === true;
153
158
 
154
159
  function _emitAudit(outcome, metadata) {
155
160
  if (!auditOn) return;
@@ -162,30 +167,56 @@ function create(opts) {
162
167
  } catch (_e) { /* drop-silent */ }
163
168
  }
164
169
 
165
- function _refuse(res, status, reason, metadata) {
170
+ // RFC 6750 §3 the Bearer challenge carries an error code that
171
+ // matches the failure: 401 with no token presented omits the code
172
+ // entirely; an unknown / revoked token is `invalid_token`; a 403
173
+ // missing-scope is `insufficient_scope`; a malformed bound-field
174
+ // request is `invalid_request`. Server-side failures (500 / 503)
175
+ // are not authentication challenges, so they advertise the scheme
176
+ // without an (incorrect) auth-error code.
177
+ function _bearerChallenge(status, reason) {
178
+ if (status === 401) {
179
+ if (reason === "no-bearer-token") return 'Bearer realm="api"';
180
+ return 'Bearer realm="api", error="invalid_token"';
181
+ }
182
+ if (status === 403) return 'Bearer realm="api", error="insufficient_scope"';
183
+ if (status === 400) return 'Bearer realm="api", error="invalid_request"'; // HTTP 400
184
+ return 'Bearer realm="api"';
185
+ }
186
+
187
+ function _refuse(req, res, status, reason, metadata) {
166
188
  _emitAudit("denied", Object.assign({ reason: reason }, metadata || {}));
167
- if (res.writableEnded || typeof res.writeHead !== "function") return;
168
- res.writeHead(status, {
169
- "Content-Type": "application/json; charset=utf-8",
170
- "WWW-Authenticate": 'Bearer realm="api", error="invalid_request"',
171
- "Cache-Control": "no-store",
189
+ denyResponse(req, res, {
190
+ onDeny: onDeny,
191
+ problem: problemMode,
192
+ status: status,
193
+ info: Object.assign({ status: status, reason: reason }, metadata || {}),
194
+ problemCode: "bound-key-refused",
195
+ problemTitle: errorMessage,
196
+ problemDetail: "API key authentication failed: " + reason + ".",
197
+ problemExt: { reason: reason },
198
+ headers: {
199
+ "WWW-Authenticate": _bearerChallenge(status, reason),
200
+ "Cache-Control": "no-store",
201
+ },
202
+ contentType: "application/json; charset=utf-8",
203
+ body: JSON.stringify({ error: errorMessage, reason: reason }),
172
204
  });
173
- res.end(JSON.stringify({ error: errorMessage, reason: reason }));
174
205
  }
175
206
 
176
207
  return async function requireBoundKeyMiddleware(req, res, next) {
177
208
  var apiKey = _parseBearer(req);
178
- if (!apiKey) return _refuse(res, 401, "no-bearer-token", {});
209
+ if (!apiKey) return _refuse(req, res, 401, "no-bearer-token", {});
179
210
 
180
211
  var record;
181
212
  try { record = await resolver(apiKey); }
182
213
  catch (e) {
183
- return _refuse(res, 503, "resolver-unavailable", {
214
+ return _refuse(req, res, 503, "resolver-unavailable", {
184
215
  error: (e && e.message) || String(e),
185
216
  });
186
217
  }
187
218
  if (!record || typeof record !== "object") {
188
- return _refuse(res, 401, "key-unknown-or-revoked", {});
219
+ return _refuse(req, res, 401, "key-unknown-or-revoked", {});
189
220
  }
190
221
 
191
222
  // Required-scope check — operator-supplied requiredScopes must be
@@ -193,7 +224,7 @@ function create(opts) {
193
224
  var keyScopes = Array.isArray(record.scopes) ? record.scopes : [];
194
225
  for (var rsi = 0; rsi < requiredScopes.length; rsi++) {
195
226
  if (keyScopes.indexOf(requiredScopes[rsi]) === -1) {
196
- return _refuse(res, 403, "missing-scope", {
227
+ return _refuse(req, res, 403, "missing-scope", {
197
228
  requiredScope: requiredScopes[rsi], keyId: record.id || null,
198
229
  });
199
230
  }
@@ -208,25 +239,25 @@ function create(opts) {
208
239
  var fieldName = registeredKeys[bfi];
209
240
  var getter = getBoundField[fieldName];
210
241
  if (!getter) {
211
- return _refuse(res, 500, "bound-field-no-getter", {
242
+ return _refuse(req, res, 500, "bound-field-no-getter", {
212
243
  field: fieldName, keyId: record.id || null,
213
244
  });
214
245
  }
215
246
  var presented;
216
247
  try { presented = getter(req); }
217
248
  catch (e) {
218
- return _refuse(res, 400, "bound-field-getter-threw", { // HTTP 400
249
+ return _refuse(req, res, 400, "bound-field-getter-threw", { // HTTP 400
219
250
  field: fieldName, error: (e && e.message) || String(e),
220
251
  });
221
252
  }
222
253
  if (typeof presented !== "string" || presented.length === 0) {
223
- return _refuse(res, 400, "bound-field-missing", { // HTTP 400
254
+ return _refuse(req, res, 400, "bound-field-missing", { // HTTP 400
224
255
  field: fieldName, keyId: record.id || null,
225
256
  });
226
257
  }
227
258
  var expected = String(registered[fieldName]);
228
259
  if (!_timingSafeStringEqual(presented, expected)) {
229
- return _refuse(res, 403, "bound-field-mismatch", {
260
+ return _refuse(req, res, 403, "bound-field-mismatch", {
230
261
  field: fieldName, keyId: record.id || null,
231
262
  });
232
263
  }
@@ -252,7 +283,7 @@ function create(opts) {
252
283
  // Audited bypass for dev fixtures.
253
284
  _emitAudit("denied", { reason: "peer-cert-bypass-tolerated", keyId: record.id });
254
285
  } else {
255
- return _refuse(res, 401, "peer-cert-required", {
286
+ return _refuse(req, res, 401, "peer-cert-required", {
256
287
  keyId: record.id || null,
257
288
  });
258
289
  }
@@ -262,7 +293,7 @@ function create(opts) {
262
293
  // because it does the same constant-time hex/colon comparison
263
294
  // we want for an allow-list. A future refactor can rename to
264
295
  // isCertFingerprintInSet — semantically identical.
265
- return _refuse(res, 403, "peer-cert-not-pinned", {
296
+ return _refuse(req, res, 403, "peer-cert-not-pinned", {
266
297
  fingerprint: fpColon, keyId: record.id || null,
267
298
  });
268
299
  }
@@ -18,6 +18,7 @@
18
18
  */
19
19
 
20
20
  var lazyRequire = require("../lazy-require");
21
+ var denyResponse = require("./deny-response").denyResponse;
21
22
  var { defineClass } = require("../framework-error");
22
23
 
23
24
  var RequireContentTypeError = defineClass("RequireContentTypeError", { alwaysPermanent: true });
@@ -58,8 +59,10 @@ function _normalizeAllowed(types) {
58
59
  *
59
60
  * @opts
60
61
  * {
61
- * methods: string[], // override default ["POST", "PUT", "PATCH"]
62
- * audit: boolean, // default true
62
+ * methods: string[], // override default ["POST", "PUT", "PATCH"]
63
+ * audit: boolean, // default true
64
+ * onDeny: function(req, res, info): void, // own the 415; info = { status, reason, contentType, accepted }
65
+ * problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of text/plain
63
66
  * }
64
67
  *
65
68
  * @example
@@ -82,6 +85,8 @@ function create(allowed, opts) {
82
85
  ? opts.methods.map(function (m) { return m.toUpperCase(); })
83
86
  : DEFAULT_BODY_METHODS.slice();
84
87
  var auditOn = opts.audit !== false;
88
+ var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
89
+ var problemMode = opts.problemDetails === true;
85
90
 
86
91
  return function requireContentTypeMiddleware(req, res, next) {
87
92
  var m = (req.method || "").toUpperCase();
@@ -90,13 +95,19 @@ function create(allowed, opts) {
90
95
  var bare = (typeof ct === "string" ? ct.split(";")[0].trim().toLowerCase() : "");
91
96
  if (bare.length > 0 && normalized.indexOf(bare) !== -1) return next();
92
97
  if (!res.headersSent) {
93
- var body = "Unsupported Media Type";
94
- res.writeHead(415, { // HTTP 415 status
95
- "Accept": normalized.join(", "),
96
- "Content-Type": "text/plain; charset=utf-8",
97
- "Content-Length": Buffer.byteLength(body),
98
+ denyResponse(req, res, {
99
+ onDeny: onDeny,
100
+ problem: problemMode,
101
+ status: 415,
102
+ info: { status: 415, reason: "unsupported-media-type",
103
+ contentType: bare || null, accepted: normalized },
104
+ problemCode: "unsupported-media-type",
105
+ problemTitle: "Unsupported Media Type",
106
+ problemDetail: "The request Content-Type is not accepted on this resource.",
107
+ headers: { "Accept": normalized.join(", ") },
108
+ contentType: "text/plain; charset=utf-8",
109
+ body: "Unsupported Media Type",
98
110
  });
99
- res.end(body);
100
111
  }
101
112
  if (auditOn) {
102
113
  try {
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  var lazyRequire = require("../lazy-require");
19
+ var denyResponse = require("./deny-response").denyResponse;
19
20
  var { defineClass } = require("../framework-error");
20
21
 
21
22
  var RequireMethodsError = defineClass("RequireMethodsError", { alwaysPermanent: true });
@@ -38,7 +39,9 @@ var observability = lazyRequire(function () { return require("../observability")
38
39
  *
39
40
  * @opts
40
41
  * {
41
- * audit: boolean, // default true
42
+ * audit: boolean, // default true
43
+ * onDeny: function(req, res, info): void, // own the 405; info = { status, reason, method, allowed }
44
+ * problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of text/plain
42
45
  * }
43
46
  *
44
47
  * @example
@@ -69,18 +72,25 @@ function create(allowed, opts) {
69
72
  var allowHeader = normalized.join(", ");
70
73
  opts = opts || {};
71
74
  var auditOn = opts.audit !== false;
75
+ var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
76
+ var problemMode = opts.problemDetails === true;
72
77
 
73
78
  return function requireMethodsMiddleware(req, res, next) {
74
79
  var m = (req.method || "").toUpperCase();
75
80
  if (normalized.indexOf(m) !== -1) return next();
76
81
  if (!res.headersSent) {
77
- var body = "Method Not Allowed";
78
- res.writeHead(405, { // HTTP 405 status
79
- "Allow": allowHeader,
80
- "Content-Type": "text/plain; charset=utf-8",
81
- "Content-Length": Buffer.byteLength(body),
82
+ denyResponse(req, res, {
83
+ onDeny: onDeny,
84
+ problem: problemMode,
85
+ status: 405,
86
+ info: { status: 405, reason: "method-not-allowed", method: m, allowed: normalized },
87
+ problemCode: "method-not-allowed",
88
+ problemTitle: "Method Not Allowed",
89
+ problemDetail: "The " + m + " method is not allowed on this resource.",
90
+ headers: { "Allow": allowHeader },
91
+ contentType: "text/plain; charset=utf-8",
92
+ body: "Method Not Allowed",
82
93
  });
83
- res.end(body);
84
94
  }
85
95
  if (auditOn) {
86
96
  try {
@@ -48,6 +48,7 @@
48
48
  var defineClass = require("../framework-error").defineClass;
49
49
  var lazyRequire = require("../lazy-require");
50
50
  var validateOpts = require("../validate-opts");
51
+ var denyResponse = require("./deny-response").denyResponse;
51
52
 
52
53
  var bCrypto = lazyRequire(function () { return require("../crypto"); });
53
54
  var audit = lazyRequire(function () { return require("../audit"); });
@@ -84,6 +85,8 @@ function _normalizeFingerprintEntry(entry) {
84
85
  * fingerprintAllowList: string[],
85
86
  * denyList: string[],
86
87
  * onAuthenticated: function(req, res, next): void,
88
+ * onDeny: function(req, res, info): void, // own the refusal (mirrors onAuthenticated); info = { status, reason, ...metadata }
89
+ * problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of the default JSON envelope
87
90
  * auditAction: string,
88
91
  * errorMessage: string,
89
92
  * audit: object,
@@ -100,7 +103,7 @@ function create(opts) {
100
103
  opts = opts || {};
101
104
  validateOpts(opts, [
102
105
  "fingerprintAllowList", "denyList",
103
- "onAuthenticated", "audit",
106
+ "onAuthenticated", "onDeny", "problemDetails", "audit",
104
107
  "auditAction", "errorMessage",
105
108
  ], "middleware.requireMtls");
106
109
 
@@ -109,6 +112,8 @@ function create(opts) {
109
112
  var denyList = Array.isArray(opts.denyList)
110
113
  ? opts.denyList.map(_normalizeFingerprintEntry) : [];
111
114
  var onAuthenticated = typeof opts.onAuthenticated === "function" ? opts.onAuthenticated : null;
115
+ var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
116
+ var problemMode = opts.problemDetails === true;
112
117
  var auditOn = opts.audit !== false;
113
118
  var actionBase = typeof opts.auditAction === "string" && opts.auditAction.length > 0
114
119
  ? opts.auditAction : "mtls.required";
@@ -126,16 +131,24 @@ function create(opts) {
126
131
  } catch (_e) { /* drop-silent — audit is best-effort, never blocks the request */ }
127
132
  }
128
133
 
129
- function _refuse(res, reason, metadata) {
134
+ function _refuse(req, res, reason, metadata) {
130
135
  _emit("denied", Object.assign({ reason: reason }, metadata || {}));
131
- if (typeof res.writeHead === "function") {
132
- res.writeHead(401, {
133
- "Content-Type": "application/json; charset=utf-8",
136
+ denyResponse(req, res, {
137
+ onDeny: onDeny,
138
+ problem: problemMode,
139
+ status: 401,
140
+ info: Object.assign({ status: 401, reason: reason }, metadata || {}),
141
+ problemCode: "client-certificate-required",
142
+ problemTitle: "Unauthorized",
143
+ problemDetail: errorMessage,
144
+ problemExt: { reason: reason },
145
+ headers: {
134
146
  "WWW-Authenticate": "Mutual",
135
147
  "Cache-Control": "no-store",
136
- });
137
- res.end(JSON.stringify({ error: errorMessage, reason: reason }));
138
- }
148
+ },
149
+ contentType: "application/json; charset=utf-8",
150
+ body: JSON.stringify({ error: errorMessage, reason: reason }),
151
+ });
139
152
  }
140
153
 
141
154
  return function requireMtlsMiddleware(req, res, next) {
@@ -158,10 +171,10 @@ function create(opts) {
158
171
 
159
172
  if (!authorized) {
160
173
  var authzError = (sock && sock.authorizationError) || "no-peer-cert";
161
- return _refuse(res, "tls-unauthorized", { authorizationError: String(authzError) });
174
+ return _refuse(req, res, "tls-unauthorized", { authorizationError: String(authzError) });
162
175
  }
163
176
  if (!peerCert || !peerCert.raw) {
164
- return _refuse(res, "no-peer-cert", {});
177
+ return _refuse(req, res, "no-peer-cert", {});
165
178
  }
166
179
 
167
180
  // Compute fingerprint via the framework's SHA3-512 helper. Buffer
@@ -171,17 +184,17 @@ function create(opts) {
171
184
  try {
172
185
  fp = bCrypto().hashCertFingerprint(peerCert.raw);
173
186
  } catch (e) {
174
- return _refuse(res, "fingerprint-failed", { error: (e && e.message) || String(e) });
187
+ return _refuse(req, res, "fingerprint-failed", { error: (e && e.message) || String(e) });
175
188
  }
176
189
 
177
190
  if (denyList.length > 0 && bCrypto().isCertRevoked(peerCert.raw, denyList)) {
178
- return _refuse(res, "fingerprint-on-deny-list", {
191
+ return _refuse(req, res, "fingerprint-on-deny-list", {
179
192
  fingerprint: fp.colon,
180
193
  subject: (peerCert.subject && peerCert.subject.CN) || null,
181
194
  });
182
195
  }
183
196
  if (allowList && allowList.length > 0 && !bCrypto().isCertRevoked(peerCert.raw, allowList)) {
184
- return _refuse(res, "fingerprint-not-allowed", {
197
+ return _refuse(req, res, "fingerprint-not-allowed", {
185
198
  fingerprint: fp.colon,
186
199
  subject: (peerCert.subject && peerCert.subject.CN) || null,
187
200
  });
@@ -199,7 +212,7 @@ function create(opts) {
199
212
  if (onAuthenticated) {
200
213
  try { return onAuthenticated(req, res, next); }
201
214
  catch (e) {
202
- return _refuse(res, "on-authenticated-threw", { error: (e && e.message) || String(e) });
215
+ return _refuse(req, res, "on-authenticated-threw", { error: (e && e.message) || String(e) });
203
216
  }
204
217
  }
205
218
  return next();
@@ -108,8 +108,8 @@ function _socketDefaults() {
108
108
  }
109
109
 
110
110
  /**
111
- * @primitive b.network.applyToSocket
112
- * @signature b.network.applyToSocket(socket)
111
+ * @primitive b.network.socket.applyToSocket
112
+ * @signature b.network.socket.applyToSocket(socket)
113
113
  * @since 0.7.68
114
114
  * @related b.network.bootFromEnv, b.network.snapshot
115
115
  *
@@ -124,7 +124,7 @@ function _socketDefaults() {
124
124
  * @example
125
125
  * var net = require("net");
126
126
  * var s = new net.Socket();
127
- * var ret = b.network.applyToSocket(s);
127
+ * var ret = b.network.socket.applyToSocket(s);
128
128
  * ret === s;
129
129
  * // → true
130
130
  * s.destroy();
@@ -164,7 +164,7 @@ var ntpFacade = {
164
164
  * @primitive b.network.bootFromEnv
165
165
  * @signature b.network.bootFromEnv(opts)
166
166
  * @since 0.7.68
167
- * @related b.network.snapshot, b.network.applyToSocket
167
+ * @related b.network.snapshot, b.network.socket.applyToSocket
168
168
  *
169
169
  * Read `BLAMEJS_*` environment variables once and apply the union to
170
170
  * the live network facade. Recognised keys cover NTP servers /
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.14.5",
3
+ "version": "0.14.6",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -0,0 +1,60 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.14.6",
4
+ "date": "2026-05-30",
5
+ "headline": "Access-refusal middleware can return RFC 9457 problem+json or a custom response, and several documented-but-uncallable APIs are now reachable",
6
+ "summary": "Every access-refusal middleware — the auth gates (bearer, DPoP, mTLS, AAL, bound-key), CSRF, CORS, rate-limit, bot-guard, age-gate, the host and network allowlists, and the method and content-type gates — now accepts two uniform options: `problemDetails: true` returns an RFC 9457 `application/problem+json` body, and `onDeny(req, res, info)` hands the response to the caller. With neither set the refusal is byte-for-byte what it was, so this is a drop-in change that lets a service standardize one error envelope across its API instead of working around each middleware's hardcoded body. Alongside that: `b.middleware.requireBoundKey` is now exported (it was documented and tested but never wired into the middleware surface), `b.middleware.bearerAuth` accepts `requiredScopes` (previously rejected at construction, which made its scope-enforcement path unreachable), API-key refusals send the RFC 6750 challenge code that matches the failure, two documented call paths that named a missing namespace segment are corrected, and the release flow now flags stale GitHub Actions and vendored bundles — with a ready-to-paste pin — before a dependency PR is needed.",
7
+ "sections": [
8
+ {
9
+ "heading": "Added",
10
+ "items": [
11
+ {
12
+ "title": "Uniform `onDeny` and `problemDetails` options on every access-refusal middleware",
13
+ "body": "Each request-lifecycle middleware that refuses a request now takes `problemDetails: true` to emit an RFC 9457 `application/problem+json` body (composing `b.problemDetails`) and `onDeny(req, res, info)` to take over the response entirely; `info` carries the status, a machine reason, and the middleware-specific fields. The deny-path response headers (`Allow`, `WWW-Authenticate`, `Retry-After`, `Accept`) survive every mode. When neither option is set the response is unchanged. Covers `requireAuth`, `requireAal`, `requireMethods`, `requireContentType`, `requireMtls`, `requireBoundKey`, `bearerAuth`, `dpop`, `csrfProtect`, `fetchMetadata`, `botGuard`, `ageGate`, `hostAllowlist`, `networkAllowlist`, `cors`, `rateLimit`, and `dailyByteQuota` (whose existing `onExceeded` keeps working as an alias of `onDeny`)."
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "heading": "Fixed",
19
+ "items": [
20
+ {
21
+ "title": "`b.middleware.requireBoundKey` is now callable",
22
+ "body": "The Bearer-API-key middleware was documented (with examples and tests) but never exported on `b.middleware`, so `b.middleware.requireBoundKey(...)` threw `undefined is not a function`. It is now wired into the middleware surface."
23
+ },
24
+ {
25
+ "title": "`b.middleware.bearerAuth` accepts `requiredScopes`",
26
+ "body": "The RFC 6750 scope-enforcement path read `opts.requiredScopes`, but the option was rejected at construction with `unknown option`, making the 403 `insufficient_scope` behavior unreachable. `requiredScopes` is now an accepted option."
27
+ },
28
+ {
29
+ "title": "RFC 6750 challenge codes on API-key refusals",
30
+ "body": "`b.middleware.requireBoundKey` now sends the `WWW-Authenticate` error code that matches the failure: `insufficient_scope` on a 403 missing-scope, `invalid_token` on an unknown or revoked token, and no error code on a 401 that presented no credentials (RFC 6750 §3). It previously sent `invalid_request` for every refusal."
31
+ },
32
+ {
33
+ "title": "Corrected two documented call paths",
34
+ "body": "The compliance and network references named a path that dropped a namespace segment: the conformity-assessment scaffold is at `b.cra.report.conformityAssessment` (not `b.cra.conformityAssessment`), and the per-socket tuning helper is at `b.network.socket.applyToSocket` (not `b.network.applyToSocket`). The documented signatures now match the callable paths."
35
+ },
36
+ {
37
+ "title": "GitHub Actions pins refreshed",
38
+ "body": "`github/codeql-action` 4.35.5 to 4.36.0, and `docker/login-action`, `docker/setup-buildx-action`, and `docker/setup-qemu-action` to their latest releases."
39
+ }
40
+ ]
41
+ },
42
+ {
43
+ "heading": "Detectors",
44
+ "items": [
45
+ {
46
+ "title": "`@primitive` reachability gate",
47
+ "body": "A new check resolves every documented `b.X.Y` primitive against the actual public surface and fails the build when a documented path is not callable (factory-instance shorthands excluded). This is the gate that would have caught the `requireBoundKey` and call-path issues above."
48
+ },
49
+ {
50
+ "title": "Deny-path composition gate",
51
+ "body": "A new check requires every access-refusal middleware to route its refusal through the shared deny-response writer, so a future middleware cannot reintroduce a hardcoded body that locks callers out of `onDeny` / `problemDetails`."
52
+ },
53
+ {
54
+ "title": "Actions and vendor currency in the release flow",
55
+ "body": "The release flow now fails the cut when a SHA-pinned GitHub Action or a vendored bundle is behind its latest upstream release. The actions report prints a ready-to-paste `owner/repo@<sha> # vX.Y.Z` pin and every file and line that uses it, so the bump is copy-paste rather than an after-the-fact dependency PR. Transient registry or API errors stay advisory so a flaky network response does not block an unrelated release."
56
+ }
57
+ ]
58
+ }
59
+ ]
60
+ }