@blamejs/core 0.14.19 → 0.14.21

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/README.md +1 -1
  3. package/lib/auth/oauth.js +736 -1
  4. package/lib/auth/oid4vci.js +124 -5
  5. package/lib/auth/oid4vp.js +14 -4
  6. package/lib/auth/sd-jwt-vc-holder.js +46 -1
  7. package/lib/break-glass.js +1 -2
  8. package/lib/config.js +28 -31
  9. package/lib/crypto-field.js +274 -17
  10. package/lib/dora.js +8 -5
  11. package/lib/dsr.js +2 -2
  12. package/lib/flag-evaluation-context.js +7 -0
  13. package/lib/guard-html-wcag-aria.js +4 -2
  14. package/lib/guard-html-wcag-forms.js +4 -2
  15. package/lib/guard-html-wcag-tables.js +4 -2
  16. package/lib/guard-html-wcag-tagwalk.js +20 -0
  17. package/lib/guard-html-wcag.js +1 -1
  18. package/lib/honeytoken.js +27 -20
  19. package/lib/mail-auth.js +333 -0
  20. package/lib/mail-deploy.js +1 -1
  21. package/lib/mail-send-deliver.js +13 -4
  22. package/lib/middleware/api-encrypt.js +140 -13
  23. package/lib/middleware/asyncapi-serve.js +3 -0
  24. package/lib/middleware/csp-report.js +13 -9
  25. package/lib/middleware/fetch-metadata.js +115 -14
  26. package/lib/middleware/openapi-serve.js +3 -0
  27. package/lib/middleware/scim-server.js +297 -19
  28. package/lib/middleware/security-headers.js +47 -0
  29. package/lib/middleware/security-txt.js +1 -2
  30. package/lib/middleware/trace-log-correlation.js +1 -2
  31. package/lib/network-smtp-policy.js +4 -4
  32. package/lib/object-store/sigv4-bucket-ops.js +11 -2
  33. package/lib/observability-tracer.js +1 -1
  34. package/lib/observability.js +39 -1
  35. package/lib/problem-details.js +56 -11
  36. package/lib/pubsub-cluster.js +16 -3
  37. package/lib/queue-sqs.js +20 -2
  38. package/lib/redis-client.js +32 -4
  39. package/lib/safe-redirect.js +16 -2
  40. package/package.json +1 -1
  41. package/sbom.cdx.json +6 -6
@@ -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;
package/lib/queue-sqs.js CHANGED
@@ -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;
package/package.json CHANGED
@@ -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",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:c7c9f488-0e96-4295-af9d-359b64bbfce6",
5
+ "serialNumber": "urn:uuid:37cb0e0e-7cba-440b-89c3-febfeb9f7eef",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-06-02T19:28:34.781Z",
8
+ "timestamp": "2026-06-05T04:48:42.555Z",
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.14.19",
22
+ "bom-ref": "@blamejs/core@0.14.21",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.14.19",
25
+ "version": "0.14.21",
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.14.19",
29
+ "purl": "pkg:npm/%40blamejs/core@0.14.21",
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.14.19",
57
+ "ref": "@blamejs/core@0.14.21",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]