@blamejs/blamejs-shop 0.3.69 → 0.3.71

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 (92) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -1
  3. package/lib/admin.js +254 -1
  4. package/lib/asset-manifest.json +1 -1
  5. package/lib/vendor/MANIFEST.json +95 -83
  6. package/lib/vendor/blamejs/.github/workflows/actions-lint.yml +3 -3
  7. package/lib/vendor/blamejs/.github/workflows/cflite_batch.yml +1 -1
  8. package/lib/vendor/blamejs/.github/workflows/cflite_pr.yml +1 -1
  9. package/lib/vendor/blamejs/.github/workflows/ci.yml +10 -10
  10. package/lib/vendor/blamejs/.github/workflows/codeql.yml +3 -3
  11. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +2 -2
  12. package/lib/vendor/blamejs/.github/workflows/release-container.yml +4 -4
  13. package/lib/vendor/blamejs/.github/workflows/scorecard.yml +2 -2
  14. package/lib/vendor/blamejs/.github/workflows/sha-to-tag-verify.yml +1 -1
  15. package/lib/vendor/blamejs/CHANGELOG.md +4 -0
  16. package/lib/vendor/blamejs/README.md +1 -1
  17. package/lib/vendor/blamejs/SECURITY.md +2 -0
  18. package/lib/vendor/blamejs/api-snapshot.json +108 -4
  19. package/lib/vendor/blamejs/lib/auth/oauth.js +736 -1
  20. package/lib/vendor/blamejs/lib/auth/oid4vci.js +124 -5
  21. package/lib/vendor/blamejs/lib/auth/oid4vp.js +14 -4
  22. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc-holder.js +46 -1
  23. package/lib/vendor/blamejs/lib/break-glass.js +1 -2
  24. package/lib/vendor/blamejs/lib/config.js +28 -31
  25. package/lib/vendor/blamejs/lib/crypto-field.js +274 -17
  26. package/lib/vendor/blamejs/lib/dora.js +8 -5
  27. package/lib/vendor/blamejs/lib/dsr.js +2 -2
  28. package/lib/vendor/blamejs/lib/flag-evaluation-context.js +7 -0
  29. package/lib/vendor/blamejs/lib/guard-html-wcag-aria.js +4 -2
  30. package/lib/vendor/blamejs/lib/guard-html-wcag-forms.js +4 -2
  31. package/lib/vendor/blamejs/lib/guard-html-wcag-tables.js +4 -2
  32. package/lib/vendor/blamejs/lib/guard-html-wcag-tagwalk.js +20 -0
  33. package/lib/vendor/blamejs/lib/guard-html-wcag.js +1 -1
  34. package/lib/vendor/blamejs/lib/honeytoken.js +27 -20
  35. package/lib/vendor/blamejs/lib/mail-auth.js +333 -0
  36. package/lib/vendor/blamejs/lib/mail-deploy.js +1 -1
  37. package/lib/vendor/blamejs/lib/mail-send-deliver.js +13 -4
  38. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +140 -13
  39. package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +3 -0
  40. package/lib/vendor/blamejs/lib/middleware/csp-report.js +13 -9
  41. package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +115 -14
  42. package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +3 -0
  43. package/lib/vendor/blamejs/lib/middleware/scim-server.js +297 -19
  44. package/lib/vendor/blamejs/lib/middleware/security-headers.js +47 -0
  45. package/lib/vendor/blamejs/lib/middleware/security-txt.js +1 -2
  46. package/lib/vendor/blamejs/lib/middleware/trace-log-correlation.js +1 -2
  47. package/lib/vendor/blamejs/lib/network-smtp-policy.js +4 -4
  48. package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +11 -2
  49. package/lib/vendor/blamejs/lib/observability-tracer.js +1 -1
  50. package/lib/vendor/blamejs/lib/observability.js +39 -1
  51. package/lib/vendor/blamejs/lib/problem-details.js +56 -11
  52. package/lib/vendor/blamejs/lib/pubsub-cluster.js +16 -3
  53. package/lib/vendor/blamejs/lib/queue-sqs.js +20 -2
  54. package/lib/vendor/blamejs/lib/redis-client.js +32 -4
  55. package/lib/vendor/blamejs/lib/safe-redirect.js +16 -2
  56. package/lib/vendor/blamejs/memory/specs/node-26-map-getorinsert-migration.md +3 -2
  57. package/lib/vendor/blamejs/package.json +1 -1
  58. package/lib/vendor/blamejs/release-notes/v0.14.20.json +73 -0
  59. package/lib/vendor/blamejs/release-notes/v0.14.21.json +98 -0
  60. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +339 -0
  61. package/lib/vendor/blamejs/test/layer-0-primitives/asyncapi.test.js +37 -0
  62. package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +22 -0
  63. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +315 -5
  64. package/lib/vendor/blamejs/test/layer-0-primitives/config.test.js +46 -0
  65. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-unseal-rate-cap.test.js +176 -0
  66. package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +86 -0
  67. package/lib/vendor/blamejs/test/layer-0-primitives/dora.test.js +38 -0
  68. package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +29 -0
  69. package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +236 -1
  70. package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +190 -0
  71. package/lib/vendor/blamejs/test/layer-0-primitives/flag.test.js +23 -0
  72. package/lib/vendor/blamejs/test/layer-0-primitives/guard-html-wcag.test.js +59 -0
  73. package/lib/vendor/blamejs/test/layer-0-primitives/honeytoken.test.js +26 -0
  74. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +179 -0
  75. package/lib/vendor/blamejs/test/layer-0-primitives/mail-deploy-tlsrpt.test.js +16 -0
  76. package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +108 -0
  77. package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +269 -0
  78. package/lib/vendor/blamejs/test/layer-0-primitives/observability-tracing.test.js +28 -0
  79. package/lib/vendor/blamejs/test/layer-0-primitives/observability.test.js +39 -0
  80. package/lib/vendor/blamejs/test/layer-0-primitives/openapi.test.js +37 -0
  81. package/lib/vendor/blamejs/test/layer-0-primitives/problem-details.test.js +79 -0
  82. package/lib/vendor/blamejs/test/layer-0-primitives/pubsub.test.js +49 -0
  83. package/lib/vendor/blamejs/test/layer-0-primitives/queue-sqs.test.js +48 -0
  84. package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +60 -0
  85. package/lib/vendor/blamejs/test/layer-0-primitives/safe-redirect.test.js +118 -0
  86. package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +259 -0
  87. package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +46 -0
  88. package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +113 -0
  89. package/lib/vendor/blamejs/test/layer-0-primitives/security-txt.test.js +111 -0
  90. package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +62 -0
  91. package/lib/vendor/blamejs/test/layer-0-primitives/smtp-policy.test.js +39 -0
  92. package/package.json +1 -1
@@ -132,19 +132,28 @@ function getBase() {
132
132
  * - `status` (recommended) must be an integer 100..599.
133
133
  * - `detail` (optional) must be a string when given.
134
134
  * - `instance` (optional) must be a URI reference string when given.
135
- * - Extensions: every additional key whose name is NOT in
135
+ * - Extensions: every additional top-level key whose name is NOT in
136
136
  * `RESERVED_FIELDS` is preserved at the top level. Reserved-name
137
- * collisions throw `problem-details/reserved-extension`.
137
+ * collisions throw `problem-details/reserved-extension`;
138
+ * prototype-pollution-shaped top-level keys throw the same.
139
+ * - `extensions`: a plain object whose keys are spread as top-level
140
+ * sibling members (RFC 9457 §3.2) — the literal `extensions`
141
+ * member is never emitted. Keys colliding with `RESERVED_FIELDS`
142
+ * are ignored (reserved fields can't be overridden by an
143
+ * extension); prototype-pollution-shaped keys are dropped
144
+ * silently. When the same name appears both as a direct top-level
145
+ * key and inside `extensions`, the direct top-level key wins.
138
146
  *
139
147
  * Returns a frozen plain object suitable for `JSON.stringify`.
140
148
  *
141
149
  * @opts
142
- * type: string, // problem-type URI reference (default "about:blank")
143
- * title: string, // short summary
144
- * status: number, // integer 100..599
145
- * detail: string, // human-readable explanation
146
- * instance: string, // URI reference for this specific occurrence
147
- * ...extensions // additional fields preserved as-is
150
+ * type: string, // problem-type URI reference (default "about:blank")
151
+ * title: string, // short summary
152
+ * status: number, // integer 100..599
153
+ * detail: string, // human-readable explanation
154
+ * instance: string, // URI reference for this specific occurrence
155
+ * extensions: object, // keys spread as top-level siblings (§3.2); direct top-level key wins on collision
156
+ * ...extensions // additional top-level keys preserved as-is
148
157
  *
149
158
  * @example
150
159
  * var p = b.problemDetails.create({
@@ -213,15 +222,44 @@ function create(opts) {
213
222
 
214
223
  // Extensions — every additional key. §3.2 endorses sibling
215
224
  // extensions as long as their names don't collide with reserved.
225
+ // The `extensions` key itself is NOT emitted as a literal nested
226
+ // member: a plain-object value is spread so each of its keys lands
227
+ // as a top-level sibling, subject to the same reserved / poisoned
228
+ // guards as direct top-level keys. A direct top-level extension key
229
+ // wins over the same name nested under `extensions`.
216
230
  var keys = Object.keys(opts);
217
- for (var i = 0; i < keys.length; i += 1) {
218
- var k = keys[i];
231
+ var directKeys = Object.create(null);
232
+ var i, k;
233
+ for (i = 0; i < keys.length; i += 1) {
234
+ k = keys[i];
235
+ if (k === "extensions") continue;
219
236
  if (RESERVED_FIELDS.indexOf(k) !== -1) continue;
220
237
  if (POISONED_KEYS.indexOf(k) !== -1) {
221
238
  throw new ProblemDetailsError("problem-details/reserved-extension",
222
239
  "create: extension key '" + k + "' refused (prototype-pollution shape)", true);
223
240
  }
224
241
  out[k] = opts[k];
242
+ directKeys[k] = true;
243
+ }
244
+
245
+ // Spread `extensions` (RFC 9457 §3.2 sibling members). Reserved
246
+ // names can't be overridden by an extension key; poisoned keys are
247
+ // dropped silently (an inbound extension map is a less-trusted shape
248
+ // than a hand-authored top-level key — a direct poisoned key still
249
+ // throws). A direct top-level key already present wins.
250
+ if (opts.extensions !== undefined && opts.extensions !== null) {
251
+ if (typeof opts.extensions !== "object" || Array.isArray(opts.extensions)) {
252
+ throw new ProblemDetailsError("problem-details/bad-extensions",
253
+ "create: extensions must be a plain object when provided", true);
254
+ }
255
+ var extKeys = Object.keys(opts.extensions);
256
+ for (i = 0; i < extKeys.length; i += 1) {
257
+ k = extKeys[i];
258
+ if (RESERVED_FIELDS.indexOf(k) !== -1) continue;
259
+ if (POISONED_KEYS.indexOf(k) !== -1) continue;
260
+ if (directKeys[k]) continue;
261
+ out[k] = opts.extensions[k];
262
+ }
225
263
  }
226
264
 
227
265
  return Object.freeze(out);
@@ -386,13 +424,20 @@ function respond(res, problem, req) {
386
424
  * `Cache-Control: no-store` are written; status code defaults to
387
425
  * 500 when omitted.
388
426
  *
427
+ * `extensions` keys are spread as top-level sibling members (RFC 9457
428
+ * §3.2) via `create` — the literal `extensions` member is never
429
+ * emitted. Keys colliding with the reserved `type` / `title` /
430
+ * `status` / `detail` / `instance` are ignored; prototype-pollution-
431
+ * shaped keys are dropped. A direct top-level key wins over the same
432
+ * name nested under `extensions`.
433
+ *
389
434
  * @opts
390
435
  * status: number, // HTTP status code (100..599); default 500
391
436
  * title: string, // operator-supplied short title
392
437
  * detail: string, // operator-supplied human-readable explanation
393
438
  * type: string, // problem-type URI (defaults to "about:blank")
394
439
  * instance: string, // optional per-occurrence URI
395
- * extensions: object, // operator-specific extension fields
440
+ * extensions: object, // keys spread as top-level siblings (§3.2); direct top-level key wins on collision
396
441
  *
397
442
  * @example
398
443
  * // Migrating from inline JSON-error shape:
@@ -24,18 +24,31 @@
24
24
  var clusterStorage = require("./cluster-storage");
25
25
  var C = require("./constants");
26
26
  var lazyRequire = require("./lazy-require");
27
+ var validateOpts = require("./validate-opts");
28
+ var { defineClass } = require("./framework-error");
27
29
 
28
30
  var logger = lazyRequire(function () { return require("./log").boot("pubsub-cluster"); });
29
31
 
32
+ var PubsubError = defineClass("PubsubError");
33
+
30
34
  var DEFAULT_POLL_INTERVAL_MS = 100;
31
35
  var DEFAULT_RETENTION_MS = C.TIME.minutes(1);
32
36
  var DEFAULT_PRUNE_EVERY_MS = C.TIME.minutes(5);
33
37
 
34
38
  function create(opts) {
35
39
  var clusterInstance = opts.cluster;
36
- var pollIntervalMs = Number(opts.pollIntervalMs) || DEFAULT_POLL_INTERVAL_MS;
37
- var retentionMs = Number(opts.retentionMs) || DEFAULT_RETENTION_MS;
38
- var pruneEveryMs = Number(opts.pruneEveryMs) || DEFAULT_PRUNE_EVERY_MS;
40
+ // Config-time: a typo (NaN-coercing string / negative / fractional) must
41
+ // surface at create, not silently fall back to the default and ship a
42
+ // mis-tuned poll loop. THROW on present-but-bad; absent keeps the default.
43
+ validateOpts.optionalPositiveInt(opts.pollIntervalMs,
44
+ "pubsub: pollIntervalMs", PubsubError, "BAD_OPT");
45
+ validateOpts.optionalPositiveInt(opts.retentionMs,
46
+ "pubsub: retentionMs", PubsubError, "BAD_OPT");
47
+ validateOpts.optionalPositiveInt(opts.pruneEveryMs,
48
+ "pubsub: pruneEveryMs", PubsubError, "BAD_OPT");
49
+ var pollIntervalMs = opts.pollIntervalMs !== undefined ? opts.pollIntervalMs : DEFAULT_POLL_INTERVAL_MS;
50
+ var retentionMs = opts.retentionMs !== undefined ? opts.retentionMs : DEFAULT_RETENTION_MS;
51
+ var pruneEveryMs = opts.pruneEveryMs !== undefined ? opts.pruneEveryMs : DEFAULT_PRUNE_EVERY_MS;
39
52
 
40
53
  var lastSeenId = 0;
41
54
  var primed = false;
@@ -50,6 +50,7 @@ var httpClient = require("./http-client");
50
50
  var cryptoField = require("./crypto-field");
51
51
  var safeJson = require("./safe-json");
52
52
  var safeUrl = require("./safe-url");
53
+ var validateOpts = require("./validate-opts");
53
54
  var { generateToken } = require("./crypto");
54
55
  var { QueueError } = require("./framework-error");
55
56
 
@@ -102,8 +103,25 @@ function create(opts) {
102
103
  var accountId = opts.accountId ? String(opts.accountId) : null;
103
104
  var timeoutMs = opts.timeoutMs;
104
105
  var allowInternal = opts.allowInternal != null ? opts.allowInternal : null;
105
- var visibilityTimeoutSec = Number(opts.visibilityTimeoutSec) || DEFAULT_VISIBILITY_TIMEOUT_SEC;
106
- var waitTimeSec = Number(opts.waitTimeSec) || DEFAULT_WAIT_TIME_SEC;
106
+ // Config-time: a typo (NaN-coercing string / negative / fractional)
107
+ // must surface at create, not silently fall back to the default and ship
108
+ // a mis-tuned lease loop. THROW on present-but-bad; absent keeps default.
109
+ validateOpts.optionalPositiveInt(opts.visibilityTimeoutSec,
110
+ "queue-sqs: visibilityTimeoutSec", QueueError, "INVALID_CONFIG");
111
+ // waitTimeSec=0 is the valid SQS short-poll sentinel (the default), so a
112
+ // positive-int check would wrongly reject it — allow non-negative integers.
113
+ if (opts.waitTimeSec !== undefined &&
114
+ (typeof opts.waitTimeSec !== "number" || !isFinite(opts.waitTimeSec) ||
115
+ opts.waitTimeSec < 0 || Math.floor(opts.waitTimeSec) !== opts.waitTimeSec)) {
116
+ throw _err("INVALID_CONFIG",
117
+ "queue-sqs: waitTimeSec must be a non-negative integer (0 = short-poll), got " +
118
+ (typeof opts.waitTimeSec === "number" ? String(opts.waitTimeSec) : typeof opts.waitTimeSec),
119
+ true);
120
+ }
121
+ var visibilityTimeoutSec = opts.visibilityTimeoutSec !== undefined
122
+ ? opts.visibilityTimeoutSec : DEFAULT_VISIBILITY_TIMEOUT_SEC;
123
+ var waitTimeSec = opts.waitTimeSec !== undefined
124
+ ? opts.waitTimeSec : DEFAULT_WAIT_TIME_SEC;
107
125
 
108
126
  var queueUrlResolver = typeof opts.queueUrlByName === "function"
109
127
  ? opts.queueUrlByName
@@ -169,11 +169,36 @@ function create(opts) {
169
169
  var useTls = opts.tls !== undefined ? !!opts.tls : parsed.tls;
170
170
  var password = opts.password !== undefined ? opts.password : parsed.password;
171
171
  var username = opts.username !== undefined ? opts.username : parsed.username;
172
- var db = opts.db !== undefined ? Number(opts.db) : parsed.db;
173
- var connectTimeoutMs = Number(opts.connectTimeoutMs) || 5000;
174
- var commandTimeoutMs = Number(opts.commandTimeoutMs) || 10000;
172
+ // Config-time entry-point opts: a bad type must fail at create() rather
173
+ // than coerce-or-default silently. connectTimeoutMs:"abc" NaN would
174
+ // otherwise fall through to the default; a negative timeout would sail
175
+ // into setTimeout; maxReconnectAttempts:"abc" → NaN would make the
176
+ // `>= 0` reconnect-cap check below false and SILENTLY disable the bound
177
+ // (unbounded reconnects). db and maxReconnectAttempts must allow 0
178
+ // (db 0 = no SELECT; maxReconnectAttempts 0 = give up immediately).
179
+ if (opts.db !== undefined &&
180
+ (typeof opts.db !== "number" || !Number.isInteger(opts.db) || opts.db < 0)) {
181
+ throw _err("BAD_OPTS",
182
+ "redis.create: opts.db must be a non-negative integer, got " +
183
+ (typeof opts.db === "number" ? String(opts.db) : typeof opts.db));
184
+ }
185
+ if (opts.maxReconnectAttempts !== undefined &&
186
+ (typeof opts.maxReconnectAttempts !== "number" ||
187
+ !Number.isInteger(opts.maxReconnectAttempts) || opts.maxReconnectAttempts < 0)) {
188
+ throw _err("BAD_OPTS",
189
+ "redis.create: opts.maxReconnectAttempts must be a non-negative integer, got " +
190
+ (typeof opts.maxReconnectAttempts === "number"
191
+ ? String(opts.maxReconnectAttempts) : typeof opts.maxReconnectAttempts));
192
+ }
193
+ validateOpts.optionalPositiveInt(opts.connectTimeoutMs,
194
+ "redis.create: opts.connectTimeoutMs", RedisError, "BAD_OPTS");
195
+ validateOpts.optionalPositiveInt(opts.commandTimeoutMs,
196
+ "redis.create: opts.commandTimeoutMs", RedisError, "BAD_OPTS");
197
+ var db = opts.db !== undefined ? opts.db : parsed.db;
198
+ var connectTimeoutMs = opts.connectTimeoutMs !== undefined ? opts.connectTimeoutMs : 5000;
199
+ var commandTimeoutMs = opts.commandTimeoutMs !== undefined ? opts.commandTimeoutMs : 10000;
175
200
  var maxReconnectAttempts = opts.maxReconnectAttempts === undefined ? 10
176
- : Number(opts.maxReconnectAttempts);
201
+ : opts.maxReconnectAttempts;
177
202
  // TLS verification controls. Operators using rediss:// against private
178
203
  // CAs (managed Redis services, on-prem clusters with internal PKI)
179
204
  // pin the trust roots via opts.ca; rejectUnauthorized stays on by
@@ -470,6 +495,9 @@ function create(opts) {
470
495
  pending: pending.length, backlog: backlog.length,
471
496
  reconnect: reconnectAttempt,
472
497
  host: host, port: port, db: db, tls: useTls,
498
+ connectTimeoutMs: connectTimeoutMs,
499
+ commandTimeoutMs: commandTimeoutMs,
500
+ maxReconnectAttempts: maxReconnectAttempts,
473
501
  };
474
502
  },
475
503
  };
@@ -76,8 +76,21 @@ function resolve(rawTarget, opts) {
76
76
  // Full URL — parse and check against allowlist.
77
77
  var allowedOrigins = Array.isArray(opts.allowedOrigins) ? opts.allowedOrigins : null;
78
78
  var allowedHosts = Array.isArray(opts.allowedHosts) ? opts.allowedHosts : null;
79
- if (!allowedOrigins && !allowedHosts) {
80
- // Operator gave no allowlist refuse all full URLs (the safe default).
79
+
80
+ // The application's own origin (opts.base) is same-origin by
81
+ // definition, so a full URL pointing at it is safe even when the
82
+ // operator supplied no explicit allowedOrigins / allowedHosts. Derive
83
+ // the origin from base and treat it as an implicitly-allowed origin.
84
+ var baseOrigin = null;
85
+ if (typeof opts.base === "string" && opts.base.length > 0) {
86
+ try {
87
+ baseOrigin = safeUrl.parse(opts.base, { allowedProtocols: safeUrl.ALLOW_HTTP_TLS }).origin;
88
+ } catch (_e) { baseOrigin = null; }
89
+ }
90
+
91
+ if (!allowedOrigins && !allowedHosts && baseOrigin === null) {
92
+ // Operator gave no allowlist and no usable base — refuse all full
93
+ // URLs (the safe default).
81
94
  return fallback;
82
95
  }
83
96
 
@@ -85,6 +98,7 @@ function resolve(rawTarget, opts) {
85
98
  try { parsed = safeUrl.parse(rawTarget, { allowedProtocols: safeUrl.ALLOW_HTTP_TLS }); }
86
99
  catch (_e) { return fallback; }
87
100
 
101
+ if (baseOrigin !== null && parsed.origin === baseOrigin) return rawTarget;
88
102
  if (allowedOrigins) {
89
103
  for (var i = 0; i < allowedOrigins.length; i += 1) {
90
104
  if (parsed.origin === allowedOrigins[i]) return rawTarget;
@@ -94,6 +94,7 @@ shapes, no get-or-insert.)
94
94
  | `lib/mail-server-rate-limit.js` | 209, 261, 291 | `connectionTimes` / `authFailureTimes` / `rcptFailureTimes` | `[]` (array) |
95
95
  | `lib/middleware/rate-limit.js` | 130 | `buckets` | object-literal |
96
96
  | `lib/network-byte-quota.js` | 82 | `store` | `_newEntry()` |
97
+ | `lib/crypto-field.js` | 592 | `_rateFailWindows` (in `_rateNoteFailure`) | `[]` (array) |
97
98
 
98
99
  ### Pubsub / websocket-channels
99
100
 
@@ -111,8 +112,8 @@ shapes, no get-or-insert.)
111
112
 
112
113
  ### Totals
113
114
 
114
- - **Migratable call sites:** ~17 across 11 files (variants A + B; metrics + rate-limit + otel each have multiple sites per file).
115
- - **Files allowlisted (migratable):** 12 (`cache.js`, `deprecate.js`, `i18n-messageformat.js`, `i18n.js`, `mail-server-rate-limit.js`, `metrics.js`, `middleware/rate-limit.js`, `network-byte-quota.js`, `observability-otlp-exporter.js`, `otel-export.js`, `pubsub.js`, `websocket-channels.js`).
115
+ - **Migratable call sites:** ~18 across 12 files (variants A + B; metrics + rate-limit + otel each have multiple sites per file).
116
+ - **Files allowlisted (migratable):** 13 (`cache.js`, `crypto-field.js`, `deprecate.js`, `i18n-messageformat.js`, `i18n.js`, `mail-server-rate-limit.js`, `metrics.js`, `middleware/rate-limit.js`, `network-byte-quota.js`, `observability-otlp-exporter.js`, `otel-export.js`, `pubsub.js`, `websocket-channels.js`).
116
117
  - **Files allowlisted (do-not-migrate edge cases):** 2 (`mail-greylist.js`, `dsr.js`).
117
118
 
118
119
  The user-supplied "~137 sites across 80+ files" figure was an estimate
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.14.19",
3
+ "version": "0.14.21",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -0,0 +1,73 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.14.20",
4
+ "date": "2026-06-02",
5
+ "headline": "OAuth Rich Authorization Requests and client attestation, a sealed-field unseal rate cap, DMARC forensic-report parsing, monitor-mode browser-isolation headers, and FedCM / Storage-Access fetch-metadata",
6
+ "summary": "This release extends several standards surfaces the framework already covered in part. The OAuth client gains RFC 9396 Rich Authorization Requests: a typed `authorizationDetails` array is validated before the request and the granted details in the token response are cross-checked, refusing a grant the OP broadened beyond what was asked. The client also gains the OAuth 2.0 Attestation-Based Client Authentication primitives — it can build the `OAuth-Client-Attestation` / `-PoP` JWT pair and verify an inbound attestation. Sealed-field reads gain an opt-in unseal-failure rate cap that throttles a decryption-oracle / brute-force burst against attacker-written sealed columns (CWE-307). The inbound mail stack gains a DMARC forensic (RUF) report parser, the inverse of the aggregate-report parser. On the browser side, the security-headers middleware adds report-only COOP / COEP / Document-Policy variants for safe cross-origin-isolation rollout plus the embedder Require-Document-Policy and Service-Worker-Allowed headers, and the fetch-metadata middleware recognizes the FedCM `webidentity` destination and the Storage-Access request headers first-class. Observability adds a batch of current OpenTelemetry semantic-convention attributes. Every addition is additive or opt-in: an operator who sets no new option, and configures no rate cap, sees prior behavior unchanged.",
7
+ "sections": [
8
+ {
9
+ "heading": "Added",
10
+ "items": [
11
+ {
12
+ "title": "OAuth client: RFC 9396 Rich Authorization Requests (`authorizationDetails`)",
13
+ "body": "The OAuth / OIDC client accepts an `authorizationDetails` array (RFC 9396 §2 — each element a typed object) on the authorization and pushed-authorization-request paths; it is validated at config time (every element must be an object carrying a string `type`) and serialized as the `authorization_details` parameter, with a cap on the serialized payload. When `verifyAuthorizationDetails` is set, the granted `authorization_details` returned in the token response (RFC 9396 §7) is cross-checked against the request, and a grant that exceeds what was requested is refused — defending against an upstream broadened-grant privilege escalation. Without `authorizationDetails`, the request is unchanged."
14
+ },
15
+ {
16
+ "title": "OAuth 2.0 Attestation-Based Client Authentication",
17
+ "body": "`b.auth.oauth.buildClientAttestation` and `buildClientAttestationPop` mint the `OAuth-Client-Attestation` JWT (a client-backend-signed assertion binding a per-instance key, `typ: oauth-client-attestation+jwt`) and its per-instance `OAuth-Client-Attestation-PoP` proof; `verifyClientAttestation` validates the pair, including the alg allowlist (`ATTESTATION_ALGS`), the audience, and a constant-time `jti` check. This is the wallet / per-device client-authentication shape used in OpenID4VCI and the EUDI Wallet, an alternative to a shared `client_secret`."
18
+ },
19
+ {
20
+ "title": "`b.cryptoField.configureUnsealRateCap` — sealed-field decryption-oracle throttle",
21
+ "body": "An opt-in per-(actor, table, column) sliding-window cap on sealed-column unseal failures. A DB-write attacker who can place crafted `vault:` / `vault.aad:` bytes in sealed columns can force KEM decapsulation / AEAD verification on attacker-controlled input on every read; past `threshold` failures within `windowMs`, further unseal attempts for that tuple are refused for `cooldownMs` with a typed `CryptoFieldRateError` and a distinct `system.crypto.unseal_rate_exceeded` audit event (CWE-307; OWASP ASVS v5 §2.2.1; NIST SP 800-63B §5.2.2). Default off — with no cap configured, `unsealRow` behaves exactly as before (null the field, emit `system.crypto.unseal_failed`); the cap is count-based and lazily pruned, with no background timer."
22
+ },
23
+ {
24
+ "title": "`b.mail.dmarc.parseForensicReport` — RFC 6591 forensic (RUF) report parser",
25
+ "body": "Parses a DMARC failure (forensic) report: a `multipart/report; report-type=feedback-report` message (RFC 6591 §3) whose `message/feedback-report` subpart carries the `Feedback-Type: auth-failure` fields, with the required-field set validated (`Auth-Failure` and the other §3.1 fields) and a part-count and byte cap bounding a hostile report. It is the inbound inverse of the aggregate-report path, so an operator ingesting RUF mail has a parser symmetric with the existing aggregate parser and the v0.14.19 aggregate builder."
26
+ },
27
+ {
28
+ "title": "Monitor-mode cross-origin-isolation and embedder headers",
29
+ "body": "`b.middleware.securityHeaders` gains `coopReportOnly`, `coepReportOnly`, and `documentPolicyReportOnly` — set a policy string to emit the matching `*-Report-Only` header so a UA evaluates and reports violations without enforcing, the safe way to stage a COOP / COEP / Document-Policy rollout (WHATWG HTML cross-origin isolation; W3C Document Policy). `requireDocumentPolicy` emits the embedder `Require-Document-Policy` a parent demands of subframes, and `serviceWorkerAllowed` emits `Service-Worker-Allowed` to broaden a service worker's registration scope (W3C Service Workers). All default off."
30
+ },
31
+ {
32
+ "title": "fetch-metadata: FedCM `webidentity` and Storage-Access headers",
33
+ "body": "`b.middleware.fetchMetadata` recognizes the `webidentity` `Sec-Fetch-Dest` (a FedCM credentialed request) first-class and adds `deniedDest` — a list of destinations refused outright on the gated methods regardless of site, so a `webidentity` request hitting a route that is not an identity endpoint is refused. `allowStorageAccess` (default true) governs whether a request carrying `Sec-Fetch-Storage-Access: active` / `inactive` (the Storage Access API headers) is allowed, and `strictDest` throws at config time on an `allowedDest` / `deniedDest` value outside the known `Sec-Fetch-Dest` vocabulary, catching a typo at boot."
34
+ },
35
+ {
36
+ "title": "OpenTelemetry semantic-convention attributes",
37
+ "body": "The observability semantic-convention map gains a batch of current stable attributes: `peer.service`, the `faas.*` function attributes (`name` / `version` / `instance` / `trigger`), `deployment.environment.name`, `telemetry.distro.*`, `otel.scope.*`, and a Kubernetes subset (`k8s.cluster.name` and the node / container / cronjob / daemonset / job / replicaset / statefulset names), so a span or metric carrying these keys is recognized and emitted under the canonical name."
38
+ }
39
+ ]
40
+ },
41
+ {
42
+ "heading": "Fixed",
43
+ "items": [
44
+ {
45
+ "title": "`b.auth.sdJwtVc.holder` signed the key-binding JWT with a non-key-derived algorithm",
46
+ "body": "The holder helper defaulted the key-binding JWT (KB-JWT) signing algorithm to a fixed `ES256` regardless of the holder key type, so a non-EC-P-256 holder key produced a presentation that either could not be built (an Ed25519 or P-384 key) or whose KB-JWT header advertised `ES256` while the signature used the actual key — a self-invalid token a verifier rejects. The algorithm is now derived from the holder key: ES256 / ES384 by EC curve, EdDSA for Ed25519 / Ed448, and ML-DSA-87 / ML-DSA-65 for an ML-DSA key. An RSA holder key, which has no supported KB-JWT algorithm, is refused at `holder.create` with a clear error instead of producing a broken presentation. An EC P-256 holder key, and any explicit `algorithm`, behave exactly as before."
47
+ }
48
+ ]
49
+ },
50
+ {
51
+ "heading": "Security",
52
+ "items": [
53
+ {
54
+ "title": "Throttle a sealed-column decryption oracle (opt-in)",
55
+ "body": "`b.cryptoField.configureUnsealRateCap` lets an operator bound repeated unseal failures against sealed columns, so an attacker who can write crafted bytes into a sealed field cannot hammer the KEM / AEAD verify path indefinitely while only an off-band alert rule notices the burst (CWE-307). It is opt-in because a sensible threshold and window depend on a deployment's legitimate sealed-read volume; the always-on defense (null the field plus an audit event on every unseal failure) is unchanged."
56
+ },
57
+ {
58
+ "title": "Refuse a broadened OAuth grant",
59
+ "body": "With `verifyAuthorizationDetails` enabled, the OAuth client refuses a token response whose granted `authorization_details` exceed the requested set (RFC 9396 §7), so a compromised or misbehaving authorization server cannot silently widen a client's authorization beyond what the request asked for."
60
+ }
61
+ ]
62
+ },
63
+ {
64
+ "heading": "Migration",
65
+ "items": [
66
+ {
67
+ "title": "No action required; additions are additive or opt-in",
68
+ "body": "The OAuth `authorizationDetails` request parameter, the granted-details cross-check, the client-attestation builders / verifier, the sealed-field unseal rate cap, the DMARC forensic-report parser, the monitor-mode and embedder browser headers, the fetch-metadata FedCM / Storage-Access options, and the new OpenTelemetry attribute keys are all additive or opt-in. A client that passes no `authorizationDetails`, an operator who calls no `configureUnsealRateCap`, and a security-headers / fetch-metadata configuration that sets none of the new options all see prior behavior byte-for-byte unchanged. The one behavior change is the SD-JWT VC holder fix: a `b.auth.sdJwtVc.holder` built with an RSA holder key is now refused at `holder.create` (RSA has no supported KB-JWT algorithm; it previously produced a self-invalid presentation) — switch such a holder to an EC P-256 / P-384, Ed25519, or ML-DSA key. EC P-256 holders and any explicit `algorithm` are unaffected."
69
+ }
70
+ ]
71
+ }
72
+ ]
73
+ }
@@ -0,0 +1,98 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.14.21",
4
+ "date": "2026-06-04",
5
+ "headline": "SCIM Bulk forward references, an atomic api-encrypt replay gate, OID4VCI `x5c` proofs, HEAD responses without bodies, and a sweep that makes every accepted option do what its documentation says",
6
+ "summary": "This release closes correctness and conformance gaps across recently shipped standards surfaces, plus a framework-wide sweep for options that were accepted but never read. SCIM `/Bulk` resolves `bulkId` cross-references regardless of operation order; the `apiEncrypt` middleware closes a concurrent-replay window on multi-replica session stores and validates its numeric options at boot; OID4VCI accepts `x5c` holder-key binding; `problemDetails` spreads its documented `extensions` object as RFC 9457 sibling members; `openapiServe` / `asyncapiServe` answer HEAD without a body; and a batch of entry points now throw on mistyped numeric options instead of silently defaulting. Options whose documented behavior was never implemented are now wired; options that could never do anything are removed and refuse as unknown.",
7
+ "sections": [
8
+ {
9
+ "heading": "Fixed",
10
+ "items": [
11
+ {
12
+ "title": "SCIM `/Bulk` resolves `bulkId` cross-references regardless of operation order (RFC 7644 §3.7.2)",
13
+ "body": "Forward references (an operation referencing a resource a later operation creates — the shape Okta and Entra emit) now execute in dependency order; circular references are refused with status 409; a reference to an undeclared `bulkId`, or to an operation that failed, fails that operation with `invalidValue`. References are resolved on BOTH surfaces the spec allows: operation data (`\"value\": \"bulkId:u1\"`) and the operation path (`PATCH /Groups/bulkId:g1` targeting a group created in the same request) — path references order, substitute, and fail exactly like data references. Previously an unresolvable reference passed the literal `bulkId:<id>` token through to your resource adapter as if it were a real id. The Bulk response keeps results in original request order, and `failOnErrors` still short-circuits."
14
+ },
15
+ {
16
+ "title": "OID4VCI `x5c` holder-key binding implemented (RFC 7515 §4.1.6; OID4VCI §8.2.1.1)",
17
+ "body": "The proof-JWT verifier named `x5c` as a valid holder-key binding in its own error message but always refused `x5c`-only proofs. The certificate chain is now shape-validated (standard base64 DER, leaf first), the leaf certificate's public key becomes the holder key at the same self-asserted trust level as an inline `jwk`, and a new optional `validateX5c(chainDerBuffers, header)` hook lets the issuer enforce chain trust (PKI anchoring, EKU checks, attestation-CA allowlists) before the key is accepted."
18
+ },
19
+ {
20
+ "title": "OID4VCI expired `c_nonce` refuses with a typed error",
21
+ "body": "A wallet whose access token outlived the shorter `c_nonce` TTL hit an untyped `TypeError` from the nonce comparison; issuance now refuses with `auth-oid4vci/c-nonce-expired` so handlers keying on typed codes respond correctly. The refusal direction is unchanged — no credential was ever minted on this path."
22
+ },
23
+ {
24
+ "title": "OID4VP DCQL numeric claim-path segments must be non-negative integers (OpenID4VP 1.0 §7.1.1)",
25
+ "body": "A query carrying `-1`, `1.5`, `NaN`, or `Infinity` as an array-index segment previously validated and then silently never matched; it now throws at build time, surfacing the malformed query to the verifier author instead of degrading to a silent non-match."
26
+ },
27
+ {
28
+ "title": "`problemDetails` `extensions` spread as sibling members (RFC 9457 §3.2)",
29
+ "body": "`send` and `create` documented an `extensions` object as the way to attach extension members, but emitted it as a literal nested `extensions` member instead. The keys now land as top-level siblings; reserved fields (`type` / `title` / `status` / `detail` / `instance`) cannot be overridden by an extension key, prototype-pollution-shaped keys are dropped, and a direct top-level key wins on collision."
30
+ },
31
+ {
32
+ "title": "`cspReport` honors `audit: false`",
33
+ "body": "The documented audit knob was accepted but never read; the `csp.violation` audit row fired unconditionally for every report. `audit: false` now suppresses the row while reports are still normalized and delivered to `onReport`. The default (audit on) is unchanged, and `maxBytes` now throws at config time on a non-positive-integer value instead of silently reverting to 64 KiB."
34
+ },
35
+ {
36
+ "title": "`openapiServe` / `asyncapiServe` HEAD responses carry no body (RFC 9110 §9.3.2)",
37
+ "body": "Both middlewares advertised GET/HEAD but answered HEAD with the full JSON / YAML document as a body. HEAD now returns the GET headers (including `Content-Length`) with an empty body, matching the rest of the framework's document-serving middlewares."
38
+ },
39
+ {
40
+ "title": "Config-time numeric options throw on bad input across entry points",
41
+ "body": "A mistyped numeric option now throws at `create()` instead of silently becoming the default or garbage downstream: `scimServer` `maxPageSize` (a non-number propagated `NaN` into your `impl.list({ count })` and `ServiceProviderConfig`), `mail.send.deliver` `retry.maxAttempts` / `timeouts.mxLookupMs` / `timeouts.perHostMs`, the `redis` client `db` / `connectTimeoutMs` / `commandTimeoutMs` / `maxReconnectAttempts` (a non-numeric value made the reconnect-cap check false and silently disabled the bound entirely), `pubsub` cluster `pollIntervalMs` / `retentionMs` / `pruneEveryMs`, and SQS queue `visibilityTimeoutSec` / `waitTimeSec` (`0` short-polling stays valid)."
42
+ },
43
+ {
44
+ "title": "Accepted-but-unread options now do what their documentation says",
45
+ "body": "The db-backed `config` reloader's `audit` knob gates its `config.reload.*` rows; `honeytoken` honors the documented injectable audit sink (`{ audit: yourSink }`) instead of always emitting to the global sink; `dora`'s `observability` knob gates its report counter, and that counter now actually emits (it previously called a method the observability module doesn't export, and the failure was swallowed); flag evaluation-context `tenantKey` sets the tenant axis; the WCAG `aria` / `forms` / `tables` sub-scanners stamp `scopeUrl` on every finding so direct callers can correlate findings to a source document; `safeRedirect`'s documented `base` lets a same-origin absolute URL pass without an explicit allowlist (cross-origin still refused); and object-store bucket operations honor a per-call `actor` override on audit rows."
46
+ }
47
+ ]
48
+ },
49
+ {
50
+ "heading": "Removed",
51
+ "items": [
52
+ {
53
+ "title": "Options that could never do anything now refuse as unknown",
54
+ "body": "The sweep removed accepted-but-impossible option keys: the `securityTxt` / `traceLogCorrelation` / tracer / TLS-RPT `audit` keys (these surfaces emit no audit rows), TLS-RPT `reportingMta` (not an RFC 8460 report field), `dsr.create` `observability` and create-time `verifyContext` (the per-call `process()` option of the same name is unchanged), `breakGlass.init` `now` (a single init-time timestamp cannot coherently override later time reads), WCAG `checkAll`, mail-deploy `compliance`, and bucket-ops `ca` (TLS trust is owned by the PQC agent — use `NODE_EXTRA_CA_CERTS` or `opts.agent`). Passing one of these now throws the standard unknown-option error."
55
+ }
56
+ ]
57
+ },
58
+ {
59
+ "heading": "Security",
60
+ "items": [
61
+ {
62
+ "title": "api-encrypt concurrent-replay window closed (CWE-367)",
63
+ "body": "On a multi-replica session store, two concurrent requests carrying the same valid counter could both pass the monotonic replay check and execute twice — an attacker who captured one encrypted request could replay it concurrently and have a non-idempotent route run twice. The per-session path now claims each `(session, counter)` tuple through the same atomic nonce store the bootstrap path uses; exactly one concurrent request wins and the loser is refused with the standard rejection shape. The claim lives until the session expires (not just the staleness window), so a failed best-effort session write cannot re-open the tuple for late replay. The bootstrap response counter is also persisted correctly on serializing session stores, fixing a response-replay false positive on the second request of a session."
64
+ },
65
+ {
66
+ "title": "api-encrypt envelope metadata is authenticated (AEAD-bound)",
67
+ "body": "The envelope's plaintext fields — `_ts`, `_nonce`, `_sid`, `_ctr` — were not bound into the ciphertext, so a captured request could be replayed past the staleness window with a rewritten `_ts`, and a captured response could be replayed to the client under a bumped `_ctr` (the client's monotonic check reads the plaintext field). Every request and response envelope now binds its metadata as AEAD associated data on both protocol halves; any rewrite fails authenticated decryption and is refused (server: standard rejection; client: typed `CLIENT_RESPONSE_TAMPERED`). The client also advances its response counter only after authenticated decryption, so a refused forgery cannot poison the monotonic check and block subsequent genuine responses."
68
+ },
69
+ {
70
+ "title": "api-encrypt numeric options validated at boot",
71
+ "body": "A mistyped `replayWindowMs` (for example the string `\"5m\"`) made the timestamp-staleness comparison always false and silently disabled that replay defense. `replayWindowMs`, `maxDecryptedBytes`, and `pruneIntervalMs` now throw at config time across `create`, `client`, and `httpClient.encrypted`."
72
+ }
73
+ ]
74
+ },
75
+ {
76
+ "heading": "Detectors",
77
+ "items": [
78
+ {
79
+ "title": "Three new codebase-pattern gates",
80
+ "body": "An option key accepted by a validation allowlist must be read by the file that accepts it (an accepted-but-never-read key is an advertised knob with no implementation); entry-point numeric options must validate rather than coerce-or-default (`Number(opts.x) || DEFAULT` swallows exactly the typo the config-time tier exists to surface); and a dispatcher that admits HEAD must suppress the response body somewhere in the same file."
81
+ }
82
+ ]
83
+ },
84
+ {
85
+ "heading": "Migration",
86
+ "items": [
87
+ {
88
+ "title": "Delete removed option keys; everything else is a behavioral fix or additive",
89
+ "body": "If you pass one of the removed keys listed under Removed, delete it — it did nothing before and now throws the standard unknown-option error. Callers passing valid options see conforming behavior with no code change; the new `validateX5c` hook and the per-finding `scopeUrl` stamp are additive."
90
+ },
91
+ {
92
+ "title": "apiEncrypt middleware and client must upgrade together",
93
+ "body": "Binding the envelope metadata into the AEAD changes what the ciphertext authenticates, so a pre-0.14.21 client cannot talk to a 0.14.21 middleware or vice versa — mixed-version peers fail authenticated decryption and are refused. Both halves ship in this package; a single service upgrading normally is unaffected. If separate services pin different framework versions and speak apiEncrypt to each other, upgrade them together."
94
+ }
95
+ ]
96
+ }
97
+ ]
98
+ }