@blamejs/core 0.14.20 → 0.14.22

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 (42) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +5 -1
  3. package/lib/auth/jar.js +190 -28
  4. package/lib/auth/jwt-external.js +213 -0
  5. package/lib/auth/oauth.js +115 -101
  6. package/lib/auth/oid4vci.js +124 -5
  7. package/lib/auth/oid4vp.js +14 -4
  8. package/lib/break-glass.js +1 -2
  9. package/lib/config.js +28 -31
  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/http-client.js +3 -4
  20. package/lib/lro.js +3 -4
  21. package/lib/mail-deploy.js +1 -1
  22. package/lib/mail-send-deliver.js +13 -4
  23. package/lib/middleware/api-encrypt.js +140 -13
  24. package/lib/middleware/asyncapi-serve.js +3 -0
  25. package/lib/middleware/csp-report.js +13 -9
  26. package/lib/middleware/deny-response.js +2 -10
  27. package/lib/middleware/health.js +1 -4
  28. package/lib/middleware/openapi-serve.js +3 -0
  29. package/lib/middleware/scim-server.js +297 -19
  30. package/lib/middleware/security-txt.js +1 -2
  31. package/lib/middleware/trace-log-correlation.js +4 -8
  32. package/lib/network-smtp-policy.js +4 -4
  33. package/lib/object-store/sigv4-bucket-ops.js +11 -2
  34. package/lib/observability-tracer.js +1 -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/lib/validate-opts.js +34 -0
  41. package/package.json +1 -1
  42. package/sbom.cdx.json +6 -6
@@ -317,10 +317,7 @@ function create(opts) {
317
317
  var entry = { ok: r.ok, ms: r.ms };
318
318
  if (r.detail) {
319
319
  // Merge detail keys other than `ok` into the entry.
320
- var keys = Object.keys(r.detail);
321
- for (var n = 0; n < keys.length; n++) {
322
- if (keys[n] !== "ok") entry[keys[n]] = r.detail[keys[n]];
323
- }
320
+ validateOpts.assignOwnEnumerable(entry, r.detail, ["ok"]);
324
321
  }
325
322
  if (r.error) entry.error = r.error;
326
323
  if (!r.critical) entry.critical = false;
@@ -179,6 +179,9 @@ function create(opts) {
179
179
  if (allowOriginHeader !== "*") headers["Vary"] = "Origin";
180
180
  }
181
181
  res.writeHead(200, headers); // HTTP 200
182
+ // HEAD carries the GET headers (incl. Content-Length) with no body
183
+ // (RFC 9110 §9.3.2).
184
+ if ((req.method || "GET").toUpperCase() === "HEAD") { res.end(); return; }
182
185
  res.end(body);
183
186
  }
184
187
 
@@ -57,7 +57,7 @@ var BEARER_RE = /^Bearer\s+(.+)$/i;
57
57
  * users: ScimResourceImpl,
58
58
  * groups?: ScimResourceImpl,
59
59
  * bearer?: (token) => Promise<actor>,
60
- * maxPageSize?: number,
60
+ * maxPageSize?: number, // default: 200 (config-time positive int)
61
61
  * bulk?: {
62
62
  * maxOperations?: number, // default: 1000 (config-time positive int)
63
63
  * maxPayloadSize?: number, // default: 1 MiB (config-time positive int, bytes)
@@ -80,6 +80,12 @@ function create(opts) {
80
80
  if (opts.groups) _validateResourceImpl(opts.groups, "groups");
81
81
 
82
82
  var basePath = opts.basePath || "/scim/v2";
83
+ // Config-time / entry-point tier: a bad page-size cap THROWS so the
84
+ // operator catches the typo at boot rather than at request time, where
85
+ // a non-number would propagate NaN into impl.list({ count }) and
86
+ // ServiceProviderConfig.filter.maxResults.
87
+ validateOpts.optionalPositiveInt(opts.maxPageSize, "middleware.scimServer: opts.maxPageSize",
88
+ ScimServerError, "middleware/scim-server/bad-max-page-size");
83
89
  var maxPageSize = opts.maxPageSize || 200; // page-size count, not bytes
84
90
  var bearer = opts.bearer || null;
85
91
  var bulkCfg = _resolveBulkConfig(opts.bulk);
@@ -297,11 +303,15 @@ async function _dispatch(req, res, basePath, bearer, opts, maxPageSize, bulkCfg)
297
303
  }
298
304
 
299
305
  // RFC 7644 §3.7 — /Bulk POST. Parses a BulkRequest, enforces the
300
- // config-time maxOperations / maxPayloadSize caps, optionally
301
- // short-circuits at failOnErrors, resolves bulkId cross-references
302
- // (§3.7.2), dispatches each operation through the same per-resource
303
- // create/update/delete logic the singleton endpoints use, and returns
304
- // a BulkResponse carrying one result object per operation.
306
+ // config-time maxOperations / maxPayloadSize caps, plans a
307
+ // dependency-ordered execution so bulkId cross-references (§3.7.2) —
308
+ // including forward references resolve to real ids before dispatch,
309
+ // fails operations with undeclared / circular / failed-dependency
310
+ // references per-op, optionally short-circuits at failOnErrors,
311
+ // dispatches each surviving operation through the same per-resource
312
+ // create/update/delete logic the singleton endpoints use, and returns a
313
+ // BulkResponse carrying one result object per operation in the ORIGINAL
314
+ // request order.
305
315
  async function _handleBulk(req, res, opts, bulkCfg, ctx) {
306
316
  var body;
307
317
  try {
@@ -336,26 +346,265 @@ async function _handleBulk(req, res, opts, bulkCfg, ctx) {
336
346
 
337
347
  var failOnErrors = _parseFailOnErrors(body.failOnErrors);
338
348
  var bulkIdMap = Object.create(null); // client bulkId -> assigned resource id
339
- var results = [];
340
- var errorCount = 0;
349
+ var ops = body.Operations;
350
+
351
+ // RFC 7644 §3.7.2 — a bulkId reference may point at a resource a LATER
352
+ // operation creates (a forward reference), so processing strictly in
353
+ // request order leaves the token unresolved. Pre-scan to (a) collect
354
+ // every declared bulkId and (b) build each operation's dependency set
355
+ // by walking its data for "bulkId:<id>" cross-references, then execute
356
+ // in dependency order. The response array is still emitted in the
357
+ // ORIGINAL request order (results indexed by request position).
358
+ var plan = _planBulkOperations(ops);
359
+
360
+ var results = new Array(ops.length); // request-order result entries
361
+ var executed = new Array(ops.length); // index -> { isError } once run
362
+ var errorCount = 0;
363
+ var stopped = false;
364
+
365
+ for (var s = 0; s < plan.order.length && !stopped; s++) {
366
+ var idx = plan.order[s];
367
+ var planned = plan.ops[idx];
368
+ var outcome;
369
+
370
+ if (planned.staticError) {
371
+ // Undeclared reference or a reference that lands in a dependency
372
+ // cycle — fail the op without ever dispatching to the adapter.
373
+ outcome = _bulkErr(ops[idx], planned.staticError.status,
374
+ planned.staticError.scimType, planned.staticError.detail);
375
+ } else if (_anyDependencyFailed(planned.refs, plan, executed, bulkIdMap)) {
376
+ // RFC 7644 §3.7.2 — a reference to an operation that FAILED cannot
377
+ // resolve to a real id; fail the dependent op with invalidValue
378
+ // rather than letting a literal "bulkId:<id>" token reach the
379
+ // adapter as if it were a real resource identifier.
380
+ outcome = _bulkErr(ops[idx], "400", "invalidValue",
381
+ "Operations[" + idx + "] references a bulkId whose creating operation failed");
382
+ } else {
383
+ outcome = await _runBulkOperation(ops[idx], idx, opts, ctx, bulkIdMap);
384
+ }
341
385
 
342
- for (var i = 0; i < body.Operations.length; i++) {
343
- var result = await _runBulkOperation(body.Operations[i], i, opts, ctx, bulkIdMap);
344
- results.push(result.entry);
345
- if (result.isError) {
386
+ results[idx] = outcome.entry;
387
+ executed[idx] = { isError: outcome.isError };
388
+ if (outcome.isError) {
346
389
  errorCount++;
347
390
  // RFC 7644 §3.7 — once the error count reaches failOnErrors the
348
- // service stops processing and returns the results so far.
349
- if (failOnErrors !== null && errorCount >= failOnErrors) break;
391
+ // service stops processing and returns the results so far. Results
392
+ // already produced keep their request-order slots; unreached
393
+ // operations are omitted from the response.
394
+ if (failOnErrors !== null && errorCount >= failOnErrors) stopped = true;
350
395
  }
351
396
  }
352
397
 
398
+ // Compact to the operations actually reached, preserving request order.
399
+ var emitted = [];
400
+ for (var e = 0; e < results.length; e++) {
401
+ if (results[e] !== undefined) emitted.push(results[e]);
402
+ }
403
+
353
404
  _writeJson(res, H.OK, {
354
405
  schemas: [SCIM_MESSAGE_BULK_RESPONSE],
355
- Operations: results,
406
+ Operations: emitted,
356
407
  });
357
408
  }
358
409
 
410
+ // RFC 7644 §3.7.2 — build the dependency-ordered execution plan for a
411
+ // bulk job. Returns { ops, order } where ops[i] = { refs, staticError? }
412
+ // (refs = the set of declared bulkIds operation i depends on) and order
413
+ // is the request indices in a dependency-respecting (topological)
414
+ // sequence. Operations referencing an UNDECLARED bulkId, or caught in a
415
+ // dependency CYCLE, carry a staticError so the executor fails them
416
+ // without dispatching to the adapter. A POST that declares a bulkId is
417
+ // the only operation kind that can satisfy a reference.
418
+ function _planBulkOperations(ops) {
419
+ var declared = Object.create(null); // bulkId -> declaring index
420
+ var planned = new Array(ops.length);
421
+
422
+ for (var i = 0; i < ops.length; i++) {
423
+ var op = ops[i];
424
+ var bulkId = op && typeof op.bulkId === "string" ? op.bulkId : null;
425
+ var method = op && typeof op.method === "string" ? op.method.toUpperCase() : null;
426
+ // Only a POST (resource creation) assigns a server id to a bulkId.
427
+ if (bulkId && method === "POST" && declared[bulkId] === undefined) {
428
+ declared[bulkId] = i;
429
+ }
430
+ planned[i] = { refs: [], staticError: null, ownBulkId: method === "POST" ? bulkId : null };
431
+ }
432
+
433
+ for (var j = 0; j < ops.length; j++) {
434
+ // RFC 7644 §3.7.2 — a reference can appear in the operation DATA
435
+ // ("value": "bulkId:u1") or as the resource id in the operation
436
+ // PATH ("PATCH /Groups/bulkId:g1" targeting a group another
437
+ // operation in this request creates). Both surfaces feed the same
438
+ // dependency set so path-referencing operations order and fail
439
+ // exactly like data-referencing ones.
440
+ var refs = _collectBulkIdRefs(ops[j] && ops[j].data).concat(_pathBulkIdRefs(ops[j]));
441
+ var depSet = [];
442
+ for (var r = 0; r < refs.length; r++) {
443
+ var refId = refs[r];
444
+ var decl = declared[refId];
445
+ if (decl === undefined) {
446
+ // RFC 7644 §3.7.2 — a reference to a bulkId no operation declares
447
+ // can never resolve; fail this op with invalidValue.
448
+ planned[j].staticError = {
449
+ status: "400",
450
+ scimType: "invalidValue",
451
+ detail: "Operations[" + j + "] references undeclared bulkId '" + refId + "'",
452
+ };
453
+ depSet = [];
454
+ break;
455
+ }
456
+ if (decl !== j && depSet.indexOf(decl) === -1) depSet.push(decl);
457
+ }
458
+ planned[j].refs = depSet;
459
+ }
460
+
461
+ var order = _topoOrderBulk(planned);
462
+ return { ops: planned, order: order };
463
+ }
464
+
465
+ // Walk operation data for "bulkId:<id>" cross-references, returning the
466
+ // list of referenced bulkIds (the "<id>" portion). Bounded: the whole
467
+ // payload was capped at maxPayloadSize before parse.
468
+ function _collectBulkIdRefs(value) {
469
+ var out = [];
470
+ _walkBulkIdRefs(value, out);
471
+ return out;
472
+ }
473
+
474
+ // RFC 7644 §3.7.2 — bulkId references in an operation's PATH segments
475
+ // (e.g. "PATCH /Groups/bulkId:g1"). Returns the referenced bulkIds.
476
+ function _pathBulkIdRefs(op) {
477
+ if (!op || typeof op.path !== "string") return [];
478
+ var out = [];
479
+ var segs = op.path.split("/");
480
+ for (var i = 0; i < segs.length; i++) {
481
+ var ref = BULK_ID_REF_RE.exec(segs[i]);
482
+ if (ref) out.push(ref[1]);
483
+ }
484
+ return out;
485
+ }
486
+
487
+ // Substitute "bulkId:<id>" PATH segments with the server-assigned id,
488
+ // exactly like data references resolve (RFC 7644 §3.7.2). Throws on an
489
+ // unresolved token — surfaced as a per-op invalidValue by the caller —
490
+ // so a literal token never reaches the path parser or the adapter.
491
+ function _resolvePathBulkIdRefs(path, bulkIdMap) {
492
+ var segs = path.split("/");
493
+ for (var i = 0; i < segs.length; i++) {
494
+ var ref = BULK_ID_REF_RE.exec(segs[i]);
495
+ if (!ref) continue;
496
+ var resolved = bulkIdMap[ref[1]];
497
+ if (resolved === undefined) {
498
+ throw new ScimServerError("middleware/scim-server/unresolved-bulkid",
499
+ "references unresolved bulkId '" + ref[1] + "' in path");
500
+ }
501
+ segs[i] = resolved;
502
+ }
503
+ return segs.join("/");
504
+ }
505
+
506
+ function _walkBulkIdRefs(value, out) {
507
+ if (typeof value === "string") {
508
+ var ref = BULK_ID_REF_RE.exec(value);
509
+ if (ref) out.push(ref[1]);
510
+ return;
511
+ }
512
+ if (Array.isArray(value)) {
513
+ for (var i = 0; i < value.length; i++) _walkBulkIdRefs(value[i], out);
514
+ return;
515
+ }
516
+ if (value && typeof value === "object") {
517
+ var keys = Object.keys(value);
518
+ for (var k = 0; k < keys.length; k++) {
519
+ if (keys[k] === "__proto__" || keys[k] === "constructor" || keys[k] === "prototype") continue;
520
+ _walkBulkIdRefs(value[keys[k]], out);
521
+ }
522
+ }
523
+ }
524
+
525
+ // Dependency-respecting order over the planned operations. Operations
526
+ // already carrying a staticError (undeclared reference) are treated as
527
+ // dependency-free roots so they surface their own error in request
528
+ // order. Any operation that cannot be ordered because it participates in
529
+ // a CYCLE is marked with a 409 staticError per RFC 7644 §3.7.1 — "The
530
+ // service provider MUST try to resolve circular cross-references ... but
531
+ // MAY ... return HTTP status code 409 (Conflict)". The returned order
532
+ // always covers every index exactly once.
533
+ function _topoOrderBulk(planned) {
534
+ var n = planned.length;
535
+ var visited = new Array(n); // 0 = unseen, 1 = on-stack, 2 = done
536
+ for (var v = 0; v < n; v++) visited[v] = 0;
537
+ var order = [];
538
+ var inCycle = Object.create(null);
539
+
540
+ // Iterative depth-first post-order so a dependency is emitted before
541
+ // the operation that needs it. A back-edge (a node still on-stack)
542
+ // marks a cycle; every node on the active stack at that point is part
543
+ // of an unresolvable circular reference.
544
+ for (var start = 0; start < n; start++) {
545
+ if (visited[start] !== 0) continue;
546
+ var stack = [{ node: start, edge: 0 }];
547
+ while (stack.length > 0) {
548
+ var top = stack[stack.length - 1];
549
+ var node = top.node;
550
+ if (top.edge === 0) visited[node] = 1;
551
+ var deps = (planned[node].staticError) ? [] : planned[node].refs;
552
+ if (top.edge < deps.length) {
553
+ var dep = deps[top.edge];
554
+ top.edge++;
555
+ if (visited[dep] === 0) {
556
+ stack.push({ node: dep, edge: 0 });
557
+ } else if (visited[dep] === 1) {
558
+ // Back-edge to a node still on the active stack — every node
559
+ // from that node up to the current top forms the cycle.
560
+ var depPos = -1;
561
+ for (var p = 0; p < stack.length; p++) {
562
+ if (stack[p].node === dep) { depPos = p; break; }
563
+ }
564
+ for (var q = depPos; q >= 0 && q < stack.length; q++) {
565
+ inCycle[stack[q].node] = true;
566
+ }
567
+ }
568
+ } else {
569
+ visited[node] = 2;
570
+ order.push(node);
571
+ stack.pop();
572
+ }
573
+ }
574
+ }
575
+
576
+ for (var c = 0; c < n; c++) {
577
+ if (inCycle[c] && !planned[c].staticError) {
578
+ planned[c].staticError = {
579
+ status: "409",
580
+ scimType: "invalidValue",
581
+ detail: "Operations[" + c + "] is part of an unresolvable circular bulkId reference",
582
+ };
583
+ }
584
+ }
585
+ return order;
586
+ }
587
+
588
+ // True when any of the bulkIds this operation references belongs to a
589
+ // creating operation that ran and FAILED (so no id was recorded in
590
+ // bulkIdMap). A still-pending dependency cannot occur here because the
591
+ // topological order guarantees dependencies execute first.
592
+ function _anyDependencyFailed(refs, plan, executed, bulkIdMap) {
593
+ for (var i = 0; i < refs.length; i++) {
594
+ var declIdx = refs[i];
595
+ var rec = executed[declIdx];
596
+ if (rec && rec.isError) return true;
597
+ // A successfully-created dependency records its id in bulkIdMap; if
598
+ // it ran without error but produced no id, it cannot satisfy a
599
+ // reference either.
600
+ var declOp = plan.ops[declIdx];
601
+ if (rec && !rec.isError && declOp.ownBulkId && bulkIdMap[declOp.ownBulkId] === undefined) {
602
+ return true;
603
+ }
604
+ }
605
+ return false;
606
+ }
607
+
359
608
  // RFC 7644 §3.7 — failOnErrors is an OPTIONAL integer >= 1. Absent /
360
609
  // non-conforming values mean "process every operation" (null).
361
610
  function _parseFailOnErrors(value) {
@@ -380,7 +629,19 @@ async function _runBulkOperation(op, index, opts, ctx, bulkIdMap) {
380
629
  "Operations[" + index + "].method '" + op.method + "' not in POST/PUT/PATCH/DELETE");
381
630
  }
382
631
 
383
- var parsed = _parseBulkPath(op.path);
632
+ // Resolve path bulkId references BEFORE parsing — the dependency-
633
+ // ordered executor guarantees a referenced creation already ran, so
634
+ // an unresolved token here is an unsatisfiable reference, failed
635
+ // per-op (RFC 7644 §3.7.2) rather than handed to the adapter.
636
+ var path = op.path;
637
+ if (typeof path === "string" && path.indexOf("bulkId:") !== -1) {
638
+ try { path = _resolvePathBulkIdRefs(path, bulkIdMap); }
639
+ catch (refErr) {
640
+ return _bulkErr(op, "400", "invalidValue",
641
+ "Operations[" + index + "] " + (refErr && refErr.message ? refErr.message : "has an unresolved bulkId reference in path"));
642
+ }
643
+ }
644
+ var parsed = _parseBulkPath(path);
384
645
  if (!parsed) {
385
646
  return _bulkErr(op, "400", "invalidValue",
386
647
  "Operations[" + index + "].path '" + String(op.path) + "' is not a valid bulk path");
@@ -402,8 +663,17 @@ async function _runBulkOperation(op, index, opts, ctx, bulkIdMap) {
402
663
  var data = op.data;
403
664
  if (method === "POST" || method === "PUT" || method === "PATCH") {
404
665
  // RFC 7644 §3.7.2 — substitute any bulkId cross-references that
405
- // earlier operations have resolved before handing data to the adapter.
406
- data = _resolveBulkIdRefs(op.data, bulkIdMap);
666
+ // earlier operations resolved before handing data to the adapter.
667
+ // The dependency-ordered executor guarantees every referenced
668
+ // operation ran first; an unresolved token here would mean an
669
+ // unsatisfiable reference, which is failed per-op rather than passed
670
+ // through to the adapter as a literal "bulkId:<id>" string.
671
+ try {
672
+ data = _resolveBulkIdRefs(op.data, bulkIdMap);
673
+ } catch (refErr) {
674
+ return _bulkErr(op, "400", "invalidValue",
675
+ "Operations[" + index + "] " + (refErr && refErr.message ? refErr.message : "has an unresolved bulkId reference"));
676
+ }
407
677
  }
408
678
 
409
679
  try {
@@ -462,7 +732,15 @@ function _resolveBulkIdRefs(value, bulkIdMap) {
462
732
  var ref = BULK_ID_REF_RE.exec(value);
463
733
  if (ref) {
464
734
  var resolved = bulkIdMap[ref[1]];
465
- return resolved !== undefined ? resolved : value;
735
+ // A literal "bulkId:<id>" token MUST never reach the adapter as a
736
+ // real id. The dependency-ordered executor resolves every
737
+ // reference before dispatch; an unresolved token here signals an
738
+ // unsatisfiable reference, surfaced as a per-op error by the caller.
739
+ if (resolved === undefined) {
740
+ throw new ScimServerError("middleware/scim-server/unresolved-bulkid",
741
+ "references unresolved bulkId '" + ref[1] + "'");
742
+ }
743
+ return resolved;
466
744
  }
467
745
  return value;
468
746
  }
@@ -82,7 +82,6 @@ function _isoFuture(s) {
82
82
  * hiring: string,
83
83
  * canonical: string|string[],
84
84
  * alsoAtRoot: boolean,
85
- * audit: boolean,
86
85
  * }
87
86
  *
88
87
  * @example
@@ -99,7 +98,7 @@ function create(opts) {
99
98
  validateOpts(opts, [
100
99
  "contact", "expires", "encryption", "policy", "ack",
101
100
  "preferredLanguages", "hiring", "canonical",
102
- "alsoAtRoot", "audit",
101
+ "alsoAtRoot",
103
102
  ], "middleware.securityTxt");
104
103
 
105
104
  var contact = _arrayOfStrings(opts.contact, "contact");
@@ -52,13 +52,10 @@ function _baggageToObject(entries) {
52
52
 
53
53
  function _wrapLogger(baseLogger, req, opts) {
54
54
  if (!baseLogger || typeof baseLogger !== "object") return baseLogger;
55
- var wrapped = Object.create(null);
56
55
  // Preserve any non-level properties the operator put on the
57
- // logger (e.g. boot context, child-logger metadata).
58
- var keys = Object.keys(baseLogger);
59
- for (var i = 0; i < keys.length; i++) {
60
- if (LOG_LEVELS.indexOf(keys[i]) === -1) wrapped[keys[i]] = baseLogger[keys[i]];
61
- }
56
+ // logger (e.g. boot context, child-logger metadata); the level
57
+ // methods themselves are re-wrapped below.
58
+ var wrapped = validateOpts.assignOwnEnumerable(Object.create(null), baseLogger, LOG_LEVELS);
62
59
 
63
60
  function _enrichMeta(meta) {
64
61
  var enriched = Object.assign({}, meta || {});
@@ -123,7 +120,6 @@ function _wrapLogger(baseLogger, req, opts) {
123
120
  * logger: object, // required b.log instance
124
121
  * reqField: string, // default "log" → req.log
125
122
  * includeBaggage: boolean, // default true
126
- * audit: boolean,
127
123
  * }
128
124
  *
129
125
  * @example
@@ -138,7 +134,7 @@ function _wrapLogger(baseLogger, req, opts) {
138
134
  function create(opts) {
139
135
  validateOpts.requireObject(opts, "middleware.traceLogCorrelation", TraceLogError);
140
136
  validateOpts(opts, [
141
- "logger", "reqField", "includeBaggage", "audit",
137
+ "logger", "reqField", "includeBaggage",
142
138
  ], "middleware.traceLogCorrelation");
143
139
 
144
140
  if (!opts.logger || typeof opts.logger !== "object") {
@@ -21,7 +21,7 @@
21
21
  *
22
22
  * policy.tlsRpt.recordShape({
23
23
  * organization: "example.com",
24
- * reportingMta: "mx1.example.com",
24
+ * policies: [ ... ],
25
25
  * ...
26
26
  * }) → { ... RFC 8460 TLS-RPT JSON shape ... }
27
27
  *
@@ -519,8 +519,8 @@ function daneVerifyChain(certChain, tlsaRecords, opts) {
519
519
  function tlsRptRecordShape(opts) {
520
520
  opts = opts || {};
521
521
  validateOpts(opts, [
522
- "organization", "reportingMta", "contact",
523
- "datestart", "dateend", "policies",
522
+ "organization", "contact",
523
+ "datestart", "dateend", "policies", "reportId",
524
524
  ], "tlsRpt.recordShape");
525
525
 
526
526
  if (typeof opts.organization !== "string") {
@@ -637,7 +637,7 @@ async function tlsRptSubmit(report, opts) {
637
637
  "tlsRpt.submit: report must be an object");
638
638
  }
639
639
  opts = opts || {};
640
- validateOpts(opts, ["rua", "httpClient", "timeoutMs", "audit"], "tlsRpt.submit");
640
+ validateOpts(opts, ["rua", "httpClient", "timeoutMs"], "tlsRpt.submit");
641
641
  if (!Array.isArray(opts.rua) || opts.rua.length === 0) {
642
642
  throw new SmtpPolicyError("smtp/tls-rpt-bad-rua",
643
643
  "tlsRpt.submit: opts.rua must be a non-empty array of URIs");
@@ -418,7 +418,6 @@ function create(config) {
418
418
  "protocol", "region", "accessKeyId", "secretAccessKey", "sessionToken",
419
419
  "endpoint", "pathStyle", "forcePathStyle",
420
420
  "allowedProtocols", "allowInternal", "timeoutMs",
421
- "ca",
422
421
  "audit", "observability", "auditSuccess", "auditFailures",
423
422
  ], "bucketOps");
424
423
  if (config.protocol && config.protocol !== "sigv4") {
@@ -464,7 +463,17 @@ function create(config) {
464
463
  }
465
464
 
466
465
  function _actor(callerOpts) {
467
- return requestHelpers.resolveActorWithOverride(callerOpts || {}, null);
466
+ // `req` resolves IP / user-agent / userId from the live request;
467
+ // `actor` is an explicit override bag (e.g. { userId: "ops-admin" })
468
+ // for callers that perform a compliance-sensitive bucket change on
469
+ // behalf of an operator and want that identity on the audit row.
470
+ // Passed as the override seed: explicit `actor` fields win over the
471
+ // request-derived ones, while request-derived fields fill any key the
472
+ // operator left unset.
473
+ var seed = (callerOpts && callerOpts.actor && typeof callerOpts.actor === "object")
474
+ ? callerOpts.actor
475
+ : null;
476
+ return requestHelpers.resolveActorWithOverride(callerOpts || {}, seed);
468
477
  }
469
478
 
470
479
  // S3 subresource queries (`?lifecycle`, `?cors`, `?object-lock`,
@@ -156,7 +156,7 @@ function create(opts) {
156
156
  validateOpts(opts, [
157
157
  "service", "resource", "scope",
158
158
  "maxAttributes", "maxEvents", "maxAttributeValueLength",
159
- "onEnd", "onStart", "audit",
159
+ "onEnd", "onStart",
160
160
  ], "tracer.create");
161
161
  validateOpts.requireNonEmptyString(opts.service,
162
162
  "tracer.create: service", TracerError, "tracer/bad-service");
@@ -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;