@blamejs/blamejs-shop 0.3.70 → 0.3.72

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 (93) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -1
  3. package/lib/admin.js +254 -2
  4. package/lib/asset-manifest.json +1 -1
  5. package/lib/customer-segments.js +150 -0
  6. package/lib/vendor/MANIFEST.json +95 -83
  7. package/lib/vendor/blamejs/.github/workflows/actions-lint.yml +3 -3
  8. package/lib/vendor/blamejs/.github/workflows/cflite_batch.yml +1 -1
  9. package/lib/vendor/blamejs/.github/workflows/cflite_pr.yml +1 -1
  10. package/lib/vendor/blamejs/.github/workflows/ci.yml +10 -10
  11. package/lib/vendor/blamejs/.github/workflows/codeql.yml +3 -3
  12. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +2 -2
  13. package/lib/vendor/blamejs/.github/workflows/release-container.yml +4 -4
  14. package/lib/vendor/blamejs/.github/workflows/scorecard.yml +2 -2
  15. package/lib/vendor/blamejs/.github/workflows/sha-to-tag-verify.yml +1 -1
  16. package/lib/vendor/blamejs/CHANGELOG.md +4 -0
  17. package/lib/vendor/blamejs/README.md +1 -1
  18. package/lib/vendor/blamejs/SECURITY.md +2 -0
  19. package/lib/vendor/blamejs/api-snapshot.json +108 -4
  20. package/lib/vendor/blamejs/lib/auth/oauth.js +736 -1
  21. package/lib/vendor/blamejs/lib/auth/oid4vci.js +124 -5
  22. package/lib/vendor/blamejs/lib/auth/oid4vp.js +14 -4
  23. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc-holder.js +46 -1
  24. package/lib/vendor/blamejs/lib/break-glass.js +1 -2
  25. package/lib/vendor/blamejs/lib/config.js +28 -31
  26. package/lib/vendor/blamejs/lib/crypto-field.js +274 -17
  27. package/lib/vendor/blamejs/lib/dora.js +8 -5
  28. package/lib/vendor/blamejs/lib/dsr.js +2 -2
  29. package/lib/vendor/blamejs/lib/flag-evaluation-context.js +7 -0
  30. package/lib/vendor/blamejs/lib/guard-html-wcag-aria.js +4 -2
  31. package/lib/vendor/blamejs/lib/guard-html-wcag-forms.js +4 -2
  32. package/lib/vendor/blamejs/lib/guard-html-wcag-tables.js +4 -2
  33. package/lib/vendor/blamejs/lib/guard-html-wcag-tagwalk.js +20 -0
  34. package/lib/vendor/blamejs/lib/guard-html-wcag.js +1 -1
  35. package/lib/vendor/blamejs/lib/honeytoken.js +27 -20
  36. package/lib/vendor/blamejs/lib/mail-auth.js +333 -0
  37. package/lib/vendor/blamejs/lib/mail-deploy.js +1 -1
  38. package/lib/vendor/blamejs/lib/mail-send-deliver.js +13 -4
  39. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +140 -13
  40. package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +3 -0
  41. package/lib/vendor/blamejs/lib/middleware/csp-report.js +13 -9
  42. package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +115 -14
  43. package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +3 -0
  44. package/lib/vendor/blamejs/lib/middleware/scim-server.js +297 -19
  45. package/lib/vendor/blamejs/lib/middleware/security-headers.js +47 -0
  46. package/lib/vendor/blamejs/lib/middleware/security-txt.js +1 -2
  47. package/lib/vendor/blamejs/lib/middleware/trace-log-correlation.js +1 -2
  48. package/lib/vendor/blamejs/lib/network-smtp-policy.js +4 -4
  49. package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +11 -2
  50. package/lib/vendor/blamejs/lib/observability-tracer.js +1 -1
  51. package/lib/vendor/blamejs/lib/observability.js +39 -1
  52. package/lib/vendor/blamejs/lib/problem-details.js +56 -11
  53. package/lib/vendor/blamejs/lib/pubsub-cluster.js +16 -3
  54. package/lib/vendor/blamejs/lib/queue-sqs.js +20 -2
  55. package/lib/vendor/blamejs/lib/redis-client.js +32 -4
  56. package/lib/vendor/blamejs/lib/safe-redirect.js +16 -2
  57. package/lib/vendor/blamejs/memory/specs/node-26-map-getorinsert-migration.md +3 -2
  58. package/lib/vendor/blamejs/package.json +1 -1
  59. package/lib/vendor/blamejs/release-notes/v0.14.20.json +73 -0
  60. package/lib/vendor/blamejs/release-notes/v0.14.21.json +98 -0
  61. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +339 -0
  62. package/lib/vendor/blamejs/test/layer-0-primitives/asyncapi.test.js +37 -0
  63. package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +22 -0
  64. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +315 -5
  65. package/lib/vendor/blamejs/test/layer-0-primitives/config.test.js +46 -0
  66. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-unseal-rate-cap.test.js +176 -0
  67. package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +86 -0
  68. package/lib/vendor/blamejs/test/layer-0-primitives/dora.test.js +38 -0
  69. package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +29 -0
  70. package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +236 -1
  71. package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +190 -0
  72. package/lib/vendor/blamejs/test/layer-0-primitives/flag.test.js +23 -0
  73. package/lib/vendor/blamejs/test/layer-0-primitives/guard-html-wcag.test.js +59 -0
  74. package/lib/vendor/blamejs/test/layer-0-primitives/honeytoken.test.js +26 -0
  75. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +179 -0
  76. package/lib/vendor/blamejs/test/layer-0-primitives/mail-deploy-tlsrpt.test.js +16 -0
  77. package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +108 -0
  78. package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +269 -0
  79. package/lib/vendor/blamejs/test/layer-0-primitives/observability-tracing.test.js +28 -0
  80. package/lib/vendor/blamejs/test/layer-0-primitives/observability.test.js +39 -0
  81. package/lib/vendor/blamejs/test/layer-0-primitives/openapi.test.js +37 -0
  82. package/lib/vendor/blamejs/test/layer-0-primitives/problem-details.test.js +79 -0
  83. package/lib/vendor/blamejs/test/layer-0-primitives/pubsub.test.js +49 -0
  84. package/lib/vendor/blamejs/test/layer-0-primitives/queue-sqs.test.js +48 -0
  85. package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +60 -0
  86. package/lib/vendor/blamejs/test/layer-0-primitives/safe-redirect.test.js +118 -0
  87. package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +259 -0
  88. package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +46 -0
  89. package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +113 -0
  90. package/lib/vendor/blamejs/test/layer-0-primitives/security-txt.test.js +111 -0
  91. package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +62 -0
  92. package/lib/vendor/blamejs/test/layer-0-primitives/smtp-policy.test.js +39 -0
  93. package/package.json +1 -1
@@ -507,6 +507,11 @@ function testNoRawTimeLiterals() {
507
507
  .replace(/'(?:[^'\\]|\\.)*'/g, "")
508
508
  .replace(/`(?:[^`\\]|\\.)*`/g, "")
509
509
  .replace(/\/(?:[^/\\\n]|\\.)+\/[gimsuy]*/g, "")
510
+ // Strip a trailing `//` line comment AFTER string/regex removal so
511
+ // a `// RFC 7800` / `// draft §4.1` annotation can't seed a phantom
512
+ // time-shape literal. String literals are already gone, so any
513
+ // remaining `//` opens a real comment; everything to EOL is prose.
514
+ .replace(/\/\/.*$/, "")
510
515
  .replace(/0x[0-9a-fA-F]+/g, "");
511
516
  var hit = false;
512
517
  // Any `* 1000` that isn't part of `* 1000 * 1000` (already caught
@@ -2590,13 +2595,44 @@ async function testNoDuplicateCodeBlocks() {
2590
2595
  ],
2591
2596
  reason: "v0.14.18 — coincidental shingle of the import-public-key-then-wrap-failure idiom (try { keyObj = nodeCrypto.createPublicKey({ key, format }); } catch (e) { throw new AuthError(code, msg + ((e && e.message) || String(e))); }). The alg/key-type cross-check (CVE-2026-22817) is ALREADY routed through the shared jwtExternal._assertAlgKtyMatch helper; what repeats here is only the per-primitive createPublicKey + typed-catch shell. oid4vci._verifyProofJwt imports an OID4VCI proof-JWT holder key and throws auth-oid4vci/*; openid-federation.verifyEntityStatement imports an entity-statement JWS key and throws auth-openid-federation/*; saml._decryptEncryptedAssertion / verifyResponse import SAML XML-DSig / EncryptedAssertion keys (a different signature mechanism entirely) and throw auth-saml/*. Each carries a primitive-local error code namespace operators grep for; consolidating would couple three unrelated credential formats on the createPublicKey boilerplate.",
2592
2597
  },
2598
+ {
2599
+ mode: "family-subset",
2600
+ files: [
2601
+ "lib/archive-adapters.js:fs",
2602
+ "lib/archive-adapters.js:http",
2603
+ "lib/archive-adapters.js:close",
2604
+ "lib/crypto-field.js:declarePerRowKey",
2605
+ "lib/crypto-field.js:assertColumnResidency",
2606
+ "lib/network-smtp-policy.js:mtaStsFetch",
2607
+ "lib/parsers/safe-env.js:readVar",
2608
+ "lib/mail-crypto-pgp.js:sign",
2609
+ "lib/metrics.js:shadowRegistry",
2610
+ "lib/tracing.js:spanSync",
2611
+ ],
2612
+ reason: "v0.14.20 — generic validate/guard control-flow shingle that the crypto-field plain-Error → typed-CryptoFieldError(code, msg) conversion tipped over the 3-file threshold. The throw now normalizes to `throw new _ID ( _STR , _STR )` (two-arg framework-error contract) instead of the keyword-`Error` one-arg form, so the early-return + typed-throw prelude in crypto-field.declarePerRowKey / assertColumnResidency now exact-matches the same prelude shape in unrelated primitives: archive-adapters fs/http/close adapter methods, network-smtp-policy.mtaStsFetch (MTA-STS policy fetch), parsers/safe-env.readVar (env-var read), mail-crypto-pgp.sign (PGP detached signature), metrics.shadowRegistry (Prometheus shadow registry), tracing.spanSync (OTEL span helper). Members are unrelated subsystems with primitive-local error namespaces operators grep for; there is no shared behaviour to extract — consolidating would couple field-level encryption, archive I/O, SMTP policy, env parsing, PGP, metrics, and tracing on a trivial guard-then-throw shell.",
2613
+ },
2593
2614
  {
2594
2615
  files: ["lib/api-key.js:issue", "lib/db-query.js:<top>", "lib/session.js:create"],
2595
2616
  reason: "Generic JS array helper / lambda shape — Object.keys(...).map(fn) + similar functional idioms appearing in any code that walks a column-or-key list.",
2596
2617
  },
2597
2618
  {
2598
- files: ["lib/guard-filename.js:verifyExtractionPath", "lib/hal.js:resource", "lib/vault-aad.js:_canonicalize"],
2599
- reason: "v0.13.13 — coincidental token shingle of the generic split-then-walk-segments idiom (`x.split(sep); for (...) { var seg = ...; if (...) throw/continue }`). guard-filename verifyExtractionPath walks path components refusing per-segment Windows-extraction hazards (reserved names / NTFS-ADS / trailing-dot); hal.js:resource builds a HAL resource by walking link/embedded keys; vault-aad.js:_canonicalize canonicalizes AAD key-value segments. Three unrelated domains (path safety / hypermedia link assembly / crypto AAD canonicalization) — no shared behaviour to extract; the only commonality is the universal split-and-loop control-flow shape.",
2619
+ mode: "family-subset",
2620
+ files: [
2621
+ "lib/auth/oauth.js:buildClientAttestation",
2622
+ "lib/guard-filename.js:verifyExtractionPath",
2623
+ "lib/hal.js:resource",
2624
+ "lib/vault-aad.js:_canonicalize",
2625
+ ],
2626
+ reason: "v0.13.13 — coincidental token shingle of the generic split-then-walk-segments / walk-own-keys idiom (`x.split(sep)` or `Object.keys(x)` then `for (...) { var seg = ...; if (...) throw/continue }`). guard-filename verifyExtractionPath walks path components refusing per-segment Windows-extraction hazards (reserved names / NTFS-ADS / trailing-dot); hal.js:resource builds a HAL resource by walking link/embedded keys; vault-aad.js:_canonicalize canonicalizes AAD key-value segments; oauth.js:buildClientAttestation merges operator extra-claims by walking Object.keys with a prototype-pollution + spec-field-collision guard before signing. Unrelated domains (path safety / hypermedia link assembly / crypto AAD canonicalization / attestation claim merge) — no shared behaviour to extract; the only commonality is the universal walk-and-loop control-flow shape.",
2627
+ },
2628
+ {
2629
+ mode: "family-subset",
2630
+ files: [
2631
+ "lib/auth/oauth.js:_validateAuthorizationDetailsArray",
2632
+ "lib/auth/step-up.js:parseAuthorizationDetails",
2633
+ "lib/middleware/speculation-rules.js:_validateRules",
2634
+ ],
2635
+ reason: "Coincidental shingle of the array-of-typed-objects validation loop (`if (!Array.isArray(value)) throw; for (...) { var entry = value[i]; if (!entry || typeof entry !== 'object' || Array.isArray(entry)) throw; if (typeof entry.type !== 'string' ...) throw; }`). oauth._validateAuthorizationDetailsArray and step-up.parseAuthorizationDetails both gate RFC 9396 authorization_details entries (one pre-parsed, one parsing a request string) and throw auth-oauth/* and auth-step-up/* respectively; speculation-rules._validateRules gates W3C Speculation-Rules prerender/prefetch rule objects and throws a speculation-rules error. The loop shape is the only commonality; each enforces a distinct spec grammar with its own per-entry field requirements and error-code namespace, so there is no shared validator to extract without coupling unrelated request grammars.",
2600
2636
  },
2601
2637
  {
2602
2638
  mode: "family-subset",
@@ -3107,6 +3143,9 @@ async function testNoDuplicateCodeBlocks() {
3107
3143
  "lib/auth/oauth.js:exchangeToken",
3108
3144
  "lib/auth/oauth.js:nativeSsoExchange",
3109
3145
  "lib/auth/oauth.js:pollDeviceCode",
3146
+ "lib/auth/oauth.js:_signAttestationJws",
3147
+ "lib/auth/oauth.js:_verifyAttestationJws",
3148
+ "lib/auth/oauth.js:verifyClientAttestation",
3110
3149
  "lib/auth/oid4vci.js:_verifyProofJwt",
3111
3150
  "lib/auth/oid4vci.js:createCredentialOffer",
3112
3151
  "lib/auth/oid4vci.js:exchangePreAuthorizedCode",
@@ -3122,6 +3161,7 @@ async function testNoDuplicateCodeBlocks() {
3122
3161
  "lib/auth/ciba.js:_registerInitialInterval",
3123
3162
  "lib/backup/index.js:scheduleTest",
3124
3163
  "lib/dsr.js:submit",
3164
+ "lib/fda-21cfr11.js:_validateSignatureInput",
3125
3165
  "lib/fedcm.js:accountsResponse",
3126
3166
  "lib/guard-saga-config.js:validate",
3127
3167
  "lib/guard-snapshot-envelope.js:validate",
@@ -3138,7 +3178,7 @@ async function testNoDuplicateCodeBlocks() {
3138
3178
  "lib/self-update.js:poll",
3139
3179
  "lib/self-update.js:verify",
3140
3180
  ],
3141
- reason: "v0.10.16 — JOSE / signature-verify / posture-check prelude across heterogeneous primitives: each verify/check pattern decomposes a token / envelope / posture set, asserts spec-required shape (header.alg in allowlist / kty in allowlist / iss CT-compare / aud match / time-window), and dispatches per-alg via shared helpers. The shingle similarity is the boilerplate header-parse + alg-allowlist + timing-safe compare; each primitive enforces a distinct spec (RFC 7519 JWT / RFC 7515 JWS / RFC 9449 DPoP / OASIS CSAF VEX / FIDO MDS / SAML 2.0 / RFC 9528 SD-JWT / W3C FedCM 2024 / RFC 8917 backchannel-logout / SBOM compliance / OIDC Federation / OID4VCI / CIBA / RFC 8460 TLS-RPT / restore-rollback). Consolidating would lose per-spec error code namespacing.",
3181
+ reason: "v0.10.16 — JOSE / signature-verify / posture-check prelude across heterogeneous primitives: each verify/check pattern decomposes a token / envelope / posture set, asserts spec-required shape (header.alg in allowlist / kty in allowlist / iss CT-compare / aud match / time-window), and dispatches per-alg via shared helpers. The shingle similarity is the boilerplate header-parse + alg-allowlist + timing-safe compare; each primitive enforces a distinct spec (RFC 7519 JWT / RFC 7515 JWS / RFC 9449 DPoP / OAuth 2.0 Client Attestation draft / OASIS CSAF VEX / FIDO MDS / SAML 2.0 / RFC 9528 SD-JWT / W3C FedCM 2024 / RFC 8917 backchannel-logout / SBOM compliance / OIDC Federation / OID4VCI / CIBA / RFC 8460 TLS-RPT / restore-rollback). The OAuth client-attestation sign/verify path (_signAttestationJws / _verifyAttestationJws / verifyClientAttestation) shares the JWS header-parse + per-alg nodeCrypto.sign/verify + constant-time aud/jti compare shell while throwing its own auth-oauth/attestation-* code namespace. Consolidating would lose per-spec error code namespacing.",
3142
3182
  },
3143
3183
  {
3144
3184
  mode: "family-subset",
@@ -5227,14 +5267,18 @@ async function testNoDuplicateCodeBlocks() {
5227
5267
  files: [
5228
5268
  "lib/ai-adverse-decision.js:wrap",
5229
5269
  "lib/audit-daily-review.js:create",
5270
+ "lib/auth/oauth.js:buildClientAttestationPop",
5271
+ "lib/auth/saml.js:create",
5230
5272
  "lib/cloud-events.js:wrap",
5273
+ "lib/data-act.js:shareWithThirdParty",
5231
5274
  "lib/ddl-change-control.js:create",
5232
5275
  "lib/external-db-migrate.js:create",
5233
5276
  "lib/fda-21cfr11.js:posture",
5234
5277
  "lib/observability-tracer.js:create",
5278
+ "lib/outbox.js:create",
5235
5279
  "lib/redact.js:installOutboundDlp",
5236
5280
  ],
5237
- reason: "Observability-emit + validateOpts prelude family — each primitive opens with the validateOpts cascade then attaches an observability.event call (tracer span / decision audit / DDL approval / migration / 21 CFR signature / DLP scan). Eight different domains; consolidating would force a single emit shape and lose per-primitive event-name conventions.",
5281
+ reason: "Observability-emit + validateOpts prelude family — each primitive opens with the `validateOpts(opts, [keys], label)` key-set cascade then a sequence of validateOpts.requireX / optionalX field checks (and most attach an observability.event call: tracer span / decision audit / DDL approval / migration / 21 CFR signature / DLP scan / third-party data share). The OAuth buildClientAttestationPop opens with the same validateOpts key-set + requireNonEmptyString / optionalPositiveInt prelude before minting the PoP JWT. Different domains; consolidating would force a single emit shape and lose per-primitive event-name conventions and error-code namespaces.",
5238
5282
  },
5239
5283
  {
5240
5284
  mode: "family-subset",
@@ -5253,6 +5297,41 @@ async function testNoDuplicateCodeBlocks() {
5253
5297
  ],
5254
5298
  reason: "Factory-create() opts-resolution scaffolding family — `var X = applyDefaults(opts, DEFAULTS); validateOpts.optionalY(...); validateOpts.optionalZ(...)` cascades. Eleven different domains (daily review / sanctions fetcher / migration / 21 CFR / FDX / db-role middleware / DPoP / TUS / outbox / static / sealed-PEM); each closure captures a different downstream binding. Same factory-prelude convention as the JSON-envelope cluster above; tracked separately because the file-set varies.",
5255
5299
  },
5300
+ {
5301
+ mode: "family-subset",
5302
+ files: [
5303
+ "lib/auth/oauth.js:buildClientAttestationPop",
5304
+ "lib/auth/saml.js:create",
5305
+ "lib/compliance-sanctions-fetcher.js:create",
5306
+ "lib/data-act.js:shareWithThirdParty",
5307
+ "lib/external-db-migrate.js:create",
5308
+ "lib/fda-21cfr11.js:posture",
5309
+ "lib/http-client.js:_validateDownloadOpts",
5310
+ "lib/http-client.js:_validateUploadOpts",
5311
+ "lib/mail-send-deliver.js:create",
5312
+ "lib/middleware/db-role-for.js:create",
5313
+ "lib/middleware/no-cache.js:create",
5314
+ "lib/middleware/tus-upload.js:create",
5315
+ "lib/outbox.js:create",
5316
+ "lib/pubsub-cluster.js:create",
5317
+ "lib/queue-sqs.js:create",
5318
+ "lib/storage.js:chunkScratch",
5319
+ "lib/vault/seal-pem-file.js:sealPemFile",
5320
+ "lib/watcher.js:_validateOpts",
5321
+ "lib/web-push-vapid.js:buildVapidAuthHeader",
5322
+ ],
5323
+ reason: "Config-time numeric-validation prelude family — `validateOpts.optionalPositiveInt(opts.X, label, ErrClass, code); var x = opts.X !== undefined ? opts.X : DEFAULT;` cascades at factory entry points. pubsub-cluster / queue-sqs / mail-send-deliver joined when their coerce-or-default numerics were converted to config-time throws; the older members (saml / sanctions / 21 CFR / data-act / outbox / watcher / http-client / vapid / sealed-PEM / storage chunkScratch / no-cache / tus / db-role) carry the same prelude convention. Each domain emits its own error class, code namespace, and opts vocabulary — consolidating past the call boundary would surface the wrong error code for operator typos; the shared helper (validateOpts) IS the extraction.",
5324
+ },
5325
+ {
5326
+ mode: "family-subset",
5327
+ files: [
5328
+ "lib/auth/bot-challenge.js:_normaliseAllowlist",
5329
+ "lib/auth/oid4vci.js:_parseX5cChain",
5330
+ "lib/middleware/cors.js:create",
5331
+ "lib/network-dns.js:setServers",
5332
+ ],
5333
+ reason: "Loop-over-string-array with per-entry shape refusal — bot-challenge normalises a provider allowlist, cors validates configured origins, network-dns validates resolver addresses, oid4vci validates RFC 7515 x5c base64-DER chain entries. Each refuses with its own domain error class and a per-entry indexed message (the message IS the operator diagnostic); the loop scaffold is the only shared shape.",
5334
+ },
5256
5335
  {
5257
5336
  files: [
5258
5337
  "lib/auth/sd-jwt-vc-holder.js:_emitAudit",
@@ -6110,6 +6189,7 @@ var KNOWN_ANTIPATTERNS = [
6110
6189
  regex: /var\s+\w+\s*=\s*\w+\.get\s*\([^;]+\)\s*;\s*\n\s*if\s*\(\s*!\s*\w+\s*\)\s*\{[\s\S]{0,300}?\.set\s*\(/,
6111
6190
  allowlist: [
6112
6191
  "lib/cache.js", // tagIndex (Map<tag, Set<key>>) — Set factory
6192
+ "lib/crypto-field.js", // _rateFailWindows (Map<actor:table:column, ts[]>) in _rateNoteFailure — timestamp-array factory
6113
6193
  "lib/deprecate.js", // _seen (Map<name:since, entry>) — object-literal factory
6114
6194
  "lib/i18n-messageformat.js", // _pluralRulesCache (Map<key, Intl.PluralRules>) — Intl factory
6115
6195
  "lib/i18n.js", // formatter cache (Map<key, formatter>) — closure factory
@@ -6169,6 +6249,106 @@ var KNOWN_ANTIPATTERNS = [
6169
6249
  allowlist: [],
6170
6250
  reason: "Codex P2 on v0.10.15 PR #104 flagged Number(summary['total-successful-session-count']) || 0 — silently accepted Infinity / NaN / negative on an audit-emitted summed path. Detector forces explicit validation discipline on new code.",
6171
6251
  },
6252
+ {
6253
+ // v0.14.21 — redis-client coerced entry-point numerics with bare
6254
+ // `Number(opts.X) || DEFAULT`: connectTimeoutMs:"abc" → NaN
6255
+ // silently became the default, a negative timeout sailed into
6256
+ // setTimeout, and maxReconnectAttempts:"abc" → NaN made the
6257
+ // `>= 0` reconnect-cap check false — silently DISABLING the
6258
+ // reconnect bound. Config-time entry-point opts THROW on bad
6259
+ // input; the coerce-or-default shape swallows exactly the typo
6260
+ // that tier exists to surface. Same class found and fixed in
6261
+ // pubsub-cluster + queue-sqs in the same release.
6262
+ id: "number-opts-coerce-or-default",
6263
+ primitive: "validateOpts.optionalPositiveInt / optionalPositiveFinite / optionalFiniteNonNegative (config-time throw) — never `Number(opts.X) || DEFAULT` coerce-or-default on an entry-point opt",
6264
+ regex: /=\s*Number\s*\(\s*opts\.\w+\s*\)\s*\|\|/,
6265
+ skipCommentLines: true,
6266
+ allowlist: [],
6267
+ reason: "v0.14.21 — `Number(opts.X) || DEFAULT` on a config-time entry-point opt silently converts an operator typo (string, NaN, negative-via-||-passthrough) into the default or into garbage downstream (negative setTimeout, NaN disabling a `>= 0` cap check). Entry-point numerics route through the validateOpts helpers so the typo throws at boot. A genuinely defensive request-shape reader (returns-defaults tier) reads from a request object, not `opts.`, and is out of this regex's scope by construction.",
6268
+ },
6269
+ {
6270
+ // v0.14.21 — openapi-serve / asyncapi-serve admitted HEAD at the
6271
+ // dispatcher (`method !== "GET" && method !== "HEAD"` → handle)
6272
+ // but the body writer had no HEAD branch: it set Content-Length
6273
+ // AND wrote the full payload body for HEAD, violating RFC 9110
6274
+ // §9.3.2 (a HEAD response carries no body). The framework
6275
+ // convention (assetlinks / web-app-manifest / security-txt /
6276
+ // health / static / protected-resource-metadata) is per-middleware
6277
+ // suppression: headers as for GET, then `if (req.method ===
6278
+ // "HEAD") { res.end(); return; }`. Any file admitting HEAD
6279
+ // alongside GET must carry that suppression somewhere.
6280
+ id: "head-admitted-without-body-suppression",
6281
+ primitive: "after writeHead: `if (req.method === \"HEAD\") { res.end(); return; }` — HEAD carries the GET headers (incl. Content-Length) with no body (RFC 9110 §9.3.2)",
6282
+ regex: /!==\s*["']GET["']\s*&&\s*[\w.]+\s*!==\s*["']HEAD["']/,
6283
+ requires: /===\s*["']HEAD["']/,
6284
+ skipCommentLines: true,
6285
+ allowlist: [
6286
+ // CSRF-token method gate on the form BUILDER — decides whether a
6287
+ // hidden token field is emitted; no HTTP response is written, so
6288
+ // there is no body to suppress.
6289
+ "lib/forms.js",
6290
+ // CLIENT-side cache-eligibility check (RFC 9111 — only GET/HEAD
6291
+ // responses are cacheable) — consumes responses, never writes one.
6292
+ "lib/http-client-cache.js",
6293
+ ],
6294
+ reason: "v0.14.21 — openapi-serve/asyncapi-serve served the full JSON/YAML payload as a HEAD response body (RFC 9110 §9.3.2 violation; tests only drove GET). A dispatcher that admits HEAD promises HEAD semantics; the response writer must suppress the body. The `requires` companion is satisfied by the framework-standard `req.method === \"HEAD\"` end-without-body branch anywhere in the file.",
6295
+ },
6296
+ {
6297
+ // v0.14.21 (Codex P2 on PR #301) — the apiEncrypt per-session
6298
+ // (sid, ctr) replay claim expired with the staleness window
6299
+ // (`now + replayWindowMs`). The post-handler session write is
6300
+ // best-effort, so a failed write leaves lastReqCtr stale, and the
6301
+ // envelope `_ts` is plaintext metadata not bound into the AEAD —
6302
+ // an expired claim let the same captured (sid, ctr, _ct) replay
6303
+ // later with a fresh _ts and execute twice. The claim must live
6304
+ // until session.expiresAt.
6305
+ id: "session-replay-claim-window-expiry",
6306
+ primitive: "nonceStore.checkAndInsert(ctrKey, session.expiresAt) — a session-scoped replay claim lives as long as the session can accept requests, never just the staleness window",
6307
+ regex: /checkAndInsert\s*\(\s*ctrKey\s*,\s*now\s*\+/,
6308
+ skipCommentLines: true,
6309
+ allowlist: [],
6310
+ reason: "Codex P2 on v0.14.21 PR #301 — a replay claim that expires with the staleness window re-opens late replay when the post-handler session write fails best-effort and the request timestamp is not authenticated with the ciphertext. The (sid, ctr) tuple stays burned until session.expiresAt; per-session claim count is bounded by sessionMaxResponses.",
6311
+ },
6312
+ {
6313
+ // v0.14.21 — the api-encrypt envelope's plaintext metadata
6314
+ // (_ts/_nonce/_sid/_ctr) rode OUTSIDE the AEAD: a captured
6315
+ // bootstrap/per-request envelope could be replayed past the
6316
+ // staleness window with a rewritten _ts, and a captured response
6317
+ // could be replayed to the client under a bumped _ctr (the
6318
+ // client's monotonic check reads the plaintext field). Every
6319
+ // packed AEAD call in the envelope protocol now binds the
6320
+ // canonical _requestAad/_responseAad string; a two-arg
6321
+ // encryptPacked/decryptPacked on a protocol key is the regression.
6322
+ id: "apienc-envelope-metadata-unbound",
6323
+ primitive: "bCrypto.encryptPacked/decryptPacked with _requestAad(ts, nonce, sid, ctr) / _responseAad(sid, ctr) — the api-encrypt envelope's plaintext metadata is AEAD-bound on both protocol halves",
6324
+ regex: /\b(?:encryptPacked|decryptPacked)\s*\(\s*\w+\s*,\s*(?:perSessionKey|sessionKey)\s*\)/,
6325
+ skipCommentLines: true,
6326
+ allowlist: [
6327
+ // OpenPGP message session key (RFC 9580 vocabulary, same variable
6328
+ // name by coincidence) — the PGP packet format carries no plaintext
6329
+ // framework-envelope metadata; integrity is the OpenPGP MDC/AEAD
6330
+ // packet's own concern.
6331
+ "lib/mail-crypto-pgp.js",
6332
+ ],
6333
+ reason: "v0.14.21 — envelope freshness/routing fields (_ts/_nonce/_sid/_ctr) were not authenticated with the ciphertext, so capture-and-rewrite defeated the staleness gate (requests) and the monotonic counter check (responses). All six packed AEAD calls in lib/middleware/api-encrypt.js carry the canonical AAD; both protocol halves live in that one module and must stay byte-identical.",
6334
+ },
6335
+ {
6336
+ // v0.14.21 (Codex P2 on PR #301) — the SCIM bulk dependency
6337
+ // planner scanned only operation DATA for bulkId references;
6338
+ // a reference in the operation PATH ("PATCH /Groups/bulkId:g1")
6339
+ // was neither ordered nor substituted, so the adapter could
6340
+ // receive the literal token as a resource id, or the operation
6341
+ // failed despite the referenced POST succeeding. Planner and
6342
+ // executor must scan/substitute path segments alongside data
6343
+ // (RFC 7644 §3.7.2 — path references resolve like data references).
6344
+ id: "bulk-ref-scan-misses-path",
6345
+ primitive: "_pathBulkIdRefs(op) feeding the dependency plan + _resolvePathBulkIdRefs(path, bulkIdMap) before path parsing — every operator-visible bulkId reference surface resolves",
6346
+ regex: /_collectBulkIdRefs\s*\(/,
6347
+ requires: /_pathBulkIdRefs/,
6348
+ skipCommentLines: true,
6349
+ allowlist: [],
6350
+ reason: "Codex P2 on v0.14.21 PR #301 — bulkId cross-references appear in operation paths as well as operation data (RFC 7644 §3.7.2); a planner that scans only data leaves path-referencing operations unordered and lets the literal bulkId:<id> token reach the resource adapter. Any file collecting data refs must collect and substitute path refs too.",
6351
+ },
6172
6352
  {
6173
6353
  // v0.10.15 — `zlib.gunzipSync` / `zlib.createGunzip` /
6174
6354
  // `zlib.brotliDecompress` without an output-size cap is the
@@ -6601,6 +6781,12 @@ var KNOWN_ANTIPATTERNS = [
6601
6781
 
6602
6782
  { id: "regex-polynomial-whitespace-in-repeated-group", primitive: "a regex literal must not place an optional-whitespace `\\s*` / `\\s+` at the END of a repeated group (the `(?:…\\s*)*` / `…\\s*)+` shape) — the same whitespace can be consumed either inside the group or by surrounding whitespace, so a crafted input backtracks polynomially (CWE-1333 ReDoS). Consume whitespace as a single disjoint alternative `(?:\\s|…)*` instead, and match block comments with the star-not-slash form, never a lazy `[\\s\\S]*?`.", scanScope: "lib", skipCommentLines: true, regex: /\\s[*+]\)[*+]/, allowlist: [], reason: "CodeQL js/polynomial-redos (alert 330) flagged lib/external-db.js's leading-keyword classifier `/^\\s*(?:\\/\\*…\\s*|--…\\s*)*([A-Za-z]+)/` — the `\\s*` both before AND at the tail of the repeated group gives two ways to consume the same whitespace run, so a SQL string of nested `/**/` or `*/--` comment runs backtracks polynomially; reused by the new OTel db.operation path it became a taint sink. Rewritten to `/^(?:\\s|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|--[^\\n]*\\n)*([A-Za-z]+)/` (disjoint single-char alternatives). codebase-patterns is a curated detector set, not a taint/ReDoS analyzer like CodeQL — this closes the specific shape locally so the next agent-authored comment-skip regex trips the gate before CI. Empty allowlist — a `\\s*)*` / `\\s+)+` tail in a lib regex is the ReDoS tell; allowlist a genuinely-anchored case with its structural reason." },
6603
6783
 
6784
+ { id: "attestation-pop-replay-store-must-await-thenable", primitive: "verifyClientAttestation's jti replay check (vopts.seenJti) MUST handle an async (Promise-returning) store — its result is awaited when it is a thenable so a Redis/DB store's resolved `false` (a replayed jti) refuses, instead of comparing a never-`false` Promise object with `=== false` and silently accepting the replay", scanScope: "lib", skipCommentLines: true, regex: /vopts\.seenJti\s*\(/, requires: /typeof\s+unseen\.then\s*===\s*["']function["']|unseen\s*=\s*await\s+unseen/, allowlist: [], reason: "v0.14.20 Codex P1 on PR #300 (replay-defense bypass, CWE-294) — verifyClientAttestation read `unseen = vopts.seenJti(jti, iat)` then `if (unseen === false) throw replay`. With an async (Redis/DB) atomic check-and-insert the callback returns a Promise; a Promise is never `=== false`, so a replayed jti was ACCEPTED and the draft-ietf-oauth-attestation-based-client-auth §12.1 replay defense was disabled for every multi-instance AS deployment. The verifier is now async and awaits a thenable result (`if (unseen && typeof unseen.then === \"function\") unseen = await unseen;`). The other oauth replay sinks (refreshAccessToken's ropts.checkAndInsert / ropts.seen, _normalizeTokens' vopts.seen) already await at the call site; only this path deviated. Anchored on the vopts.seenJti( token unique to this verifier; the requires-companion fails if a future edit drops the thenable-await, re-opening the silent-accept window. Empty allowlist — a seenJti result that is neither awaited nor thenable-checked is the bug." },
6785
+
6786
+ { id: "jose-jws-builder-fixed-classical-alg-default", primitive: "a JWS builder that signs with an operator-supplied key MUST NOT default `opts.algorithm` to a fixed classical JOSE alg (ES256/384/512, RS/PS*) via `|| \"<alg>\"` — that signs a key of a different type (RSA / Ed25519 / non-matching EC curve) under a header alg that disagrees with the key (un-signable, or a self-invalid JWS the verifier's alg/kty check rejects). Derive the alg from the key type (mirroring oauth _resolveAttestationAlg / sd-jwt-vc-holder _resolveHolderAlg), or use a PQC pass-through default that works with any key", scanScope: "lib", skipCommentLines: true, regex: /\.algorithm\s*\|\|\s*["'](?:ES256|ES384|ES512|RS256|RS384|RS512|PS256|PS384|PS512)["']/, allowlist: [], reason: "v0.14.20 — the OAuth client-attestation builders (Codex P2 on PR #300) AND the sd-jwt-vc holder (`var algorithm = opts.algorithm || \"ES256\"`, found by the post-fix adversarial review) both hardcoded a fixed ES256 default that overrode any key-type reconciliation. ES256 is only self-consistent for an EC P-256 key; an RSA / Ed25519 / EC-P384 key signed under an ES256 header is un-signable or yields a JWS whose header alg disagrees with the signature, which every alg/kty-checking verifier rejects (self-invalid token; broken authentication / presentation). Both are now key-derived (_resolveAttestationAlg / _resolveHolderAlg). This detector flags any lib JWS builder reintroducing a fixed CLASSICAL JOSE alg as the `opts.algorithm ||` default — it deliberately does NOT match a PQC pass-through default (`|| \"ML-DSA-87\"` / `|| DEFAULT_ALG` / `|| \"SLH-DSA-...\"`, which sign with a null digest and work for the matching key) nor non-JOSE alg selectors (hash `\"sha384\"`, DKIM `\"rsa-sha256\"`, `\"token-bucket\"`). Empty allowlist — a fixed classical-alg default on a sign path is the self-invalid-JWS bug; a genuine EC-P256-only builder should still derive/validate the key, not assume." },
6787
+
6788
+ { id: "attestation-alg-must-derive-from-key", primitive: "the OAuth client-attestation / PoP JWS builders must resolve the signing alg from the key type via _resolveAttestationAlg (infer a key-compatible default; refuse an explicit alg incompatible with the key) — never a fixed `opts.algorithm || \"ES256\"` default, which signs a non-EC key (RSA / Ed25519) under an ES256 header that verifyClientAttestation's alg/kty cross-check then rejects (self-invalid attestation)", scanScope: "lib", skipCommentLines: true, regex: /oauth-client-attestation(?:-pop)?\+jwt/, requires: /_resolveAttestationAlg\s*\(/, allowlist: [], reason: "v0.14.20 Codex P2 on PR #300 — buildClientAttestation / buildClientAttestationPop defaulted `var alg = opts.algorithm || \"ES256\"`, so an RSA / Ed25519 attester or instance key produced a compact JWS whose header said ES256 but whose signature was made with the real key; verifyClientAttestation's alg⇄kty cross-check then rejected the builder's OWN output for any non-P-256 key. Both builders now resolve the alg through _resolveAttestationAlg(explicitAlg, key): it infers a key-matched default (ES256/384/512 by curve, RS256 for RSA, EdDSA for Ed25519/Ed448) and refuses an explicit alg incompatible with the key BEFORE signing (auth-oauth/attestation-alg-key-mismatch). Anchored on the attestation `+jwt` typ literals unique to these builders (does NOT touch the generic `.algorithm || \"<selector>\"` strings in rate-limit / crypto / dkim, nor the sd-jwt holder KB-JWT path); the requires-companion fails if a future edit reverts to a hardcoded alg default. Empty allowlist — a fixed alg default on the attestation signing path is the self-invalid-JWS bug." },
6789
+
6604
6790
  {
6605
6791
  // ROTATION-EPOCH ACCEPT (v0.14.x): a vault-key rotation (b.vault.rotate)
6606
6792
  // re-keys the local dataDir, which changes the SHA3-512 fingerprint of
@@ -7667,8 +7853,12 @@ var KNOWN_ANTIPATTERNS = [
7667
7853
  // formatted message details that don't fit the helper's
7668
7854
  // (label + description) shape.
7669
7855
  "lib/protocol-dispatcher.js",
7856
+ // problem-details throws ProblemDetailsError with the 3rd
7857
+ // `permanent: true` arg (same class as external-db above) — the
7858
+ // validateOpts._throw factory signature would silently drop it.
7859
+ "lib/problem-details.js",
7670
7860
  ],
7671
- reason: "Extracted to validateOpts.optionalPlainObject. Replaces the recurring `if (X !== undefined && X !== null) { if (typeof X !== 'object' || Array.isArray(X)) throw }` shape used to validate optional plain-object opts. Two sites allowlisted: external-db needs the permanent-flag 3rd arg the helper drops; protocol-dispatcher uses multi-line formatted error messages that don't fit the helper's description slot.",
7861
+ reason: "Extracted to validateOpts.optionalPlainObject. Replaces the recurring `if (X !== undefined && X !== null) { if (typeof X !== 'object' || Array.isArray(X)) throw }` shape used to validate optional plain-object opts. Three sites allowlisted: external-db + problem-details need the permanent-flag 3rd arg the helper drops; protocol-dispatcher uses multi-line formatted error messages that don't fit the helper's description slot.",
7672
7862
  },
7673
7863
  {
7674
7864
  id: "inline-redis-client-opts-forwarding",
@@ -10049,6 +10239,122 @@ function testMailStoreFtsInTransaction() {
10049
10239
  bad);
10050
10240
  }
10051
10241
 
10242
+ // ---- Pattern: validateOpts-accepted key never read ----
10243
+ //
10244
+ // class: validateopts-key-never-read
10245
+ //
10246
+ // v0.14.21 — csp-report's @opts documented `audit: boolean // default
10247
+ // true` and validateOpts accepted the key, but create() never read
10248
+ // opts.audit: the documented disable knob was a silent no-op (the
10249
+ // violation audit row fired unconditionally). A sweep found the same
10250
+ // shape in honeytoken (injectable audit sink ignored), config
10251
+ // ("reserved for future" knob), and others — all wired or
10252
+ // de-advertised in the same release. The validateOpts allowlist IS
10253
+ // the operator contract: a key it accepts that no code reads is
10254
+ // advertised surface with no implementation.
10255
+ //
10256
+ // The detector: every string key in a direct
10257
+ // `validateOpts(<ident>, [ ... ])` call's array literal must be read
10258
+ // as `<ident>.<key>` (dot) or `<ident>["<key>"]` (literal bracket)
10259
+ // somewhere in the same file. Keys consumed STRUCTURALLY — via a
10260
+ // computed `ident[k]` loop over a key table, or by forwarding the
10261
+ // whole opts object to a sub-factory in another file — can't be
10262
+ // verified file-locally and carry an ALLOW entry below citing where
10263
+ // the key is actually consumed.
10264
+ function testValidateOptsAcceptedKeysAreRead() {
10265
+ // "<relative file>::<ident>.<key>" -> consumption cite (the reason
10266
+ // the file-local read is absent). Adding an entry requires naming
10267
+ // the real consumption site; "we'll wire it later" is not a reason.
10268
+ var ALLOW = Object.create(null);
10269
+ // jwt-external: the whole opts object is forwarded as `vopts` to
10270
+ // _selectKey; consumed as vopts.allowKidlessJwks (jwt-external.js:281).
10271
+ ALLOW["lib/auth/jwt-external.js::opts.allowKidlessJwks"] = true;
10272
+ // step-up: the validated `requirement` IS buildChallenge's `req`;
10273
+ // read as req.authorizationDetails (step-up.js:266 — emits the
10274
+ // RFC 9396 authorization_details challenge param).
10275
+ ALLOW["lib/auth/step-up.js::requirement.authorizationDetails"] = true;
10276
+ // cache: redis* keys are prefix-mapped via
10277
+ // redisClient.pickClientOpts(opts, "redis") (cache.js redis backend
10278
+ // branch → the prefix table in redis-client.js).
10279
+ ["redis", "redisPassword", "redisUsername", "redisTls", "redisCa",
10280
+ "redisServername", "redisConnectTimeoutMs", "redisCommandTimeoutMs",
10281
+ "redisMaxReconnectAttempts"].forEach(function (k) {
10282
+ ALLOW["lib/cache.js::opts." + k] = true;
10283
+ });
10284
+ // flag-providers: passed-through spec metadata — the whole spec is
10285
+ // stored (flags[key] = opts.flags[key]) and returned via
10286
+ // provider.get()/evaluate(); operator tooling reads the fields.
10287
+ ["description", "tags", "kind"].forEach(function (k) {
10288
+ ALLOW["lib/flag-providers.js::spec." + k] = true;
10289
+ });
10290
+ // body-parser: per-parser sub-configs read via the computed
10291
+ // `opts[name]` in _resolve(name) (body-parser.js _resolve loop).
10292
+ ["json", "urlencoded", "text", "raw", "multipart"].forEach(function (k) {
10293
+ ALLOW["lib/middleware/body-parser.js::opts." + k] = true;
10294
+ });
10295
+ // web-app-manifest: manifest fields read via the computed `opts[k]`
10296
+ // manifest-build loop (web-app-manifest.js Object.keys(opts) filter).
10297
+ ["short_name", "description", "scope", "display", "display_override",
10298
+ "orientation", "theme_color", "background_color", "screenshots",
10299
+ "shortcuts", "categories", "lang", "dir", "id",
10300
+ "prefer_related_applications", "related_applications"].forEach(function (k) {
10301
+ ALLOW["lib/middleware/web-app-manifest.js::opts." + k] = true;
10302
+ });
10303
+ // sigv4-bucket-ops: per-method opts forwarded to _actor(callerOpts) →
10304
+ // requestHelpers.resolveActorWithOverride (callerOpts.req read in
10305
+ // request-helpers.js; callerOpts.actor is the override seed at
10306
+ // sigv4-bucket-ops.js _actor).
10307
+ ALLOW["lib/object-store/sigv4-bucket-ops.js::opts.req"] = true;
10308
+ ALLOW["lib/object-store/sigv4-bucket-ops.js::opts.actor"] = true;
10309
+ // pubsub: the whole opts object is forwarded to
10310
+ // pubsubCluster().create(opts) / pubsubRedis().create(opts)
10311
+ // (pubsub.js _resolveBackend); keys consumed in those backends.
10312
+ ["cluster", "pollIntervalMs", "retentionMs", "pruneEveryMs",
10313
+ "redisUrl", "redisPassword", "redisUsername", "redisTls", "redisCa",
10314
+ "redisServername"].forEach(function (k) {
10315
+ ALLOW["lib/pubsub.js::opts." + k] = true;
10316
+ });
10317
+
10318
+ var files = _libFiles();
10319
+ var bad = [];
10320
+ var callRe = /\bvalidateOpts\s*\(\s*(\w+)\s*,\s*\[([\s\S]*?)\]/g;
10321
+ for (var i = 0; i < files.length; i++) {
10322
+ var rel = _relPath(files[i]);
10323
+ if (rel === "lib/validate-opts.js") continue;
10324
+ var content;
10325
+ try { content = fs.readFileSync(files[i], "utf8"); }
10326
+ catch (_e) { continue; }
10327
+ callRe.lastIndex = 0;
10328
+ var m;
10329
+ while ((m = callRe.exec(content)) !== null) {
10330
+ var ident = m[1];
10331
+ var arr = m[2];
10332
+ var keyRe = /["']([A-Za-z_$][\w$]*)["']/g;
10333
+ var km;
10334
+ while ((km = keyRe.exec(arr)) !== null) {
10335
+ var key = km[1];
10336
+ if (ALLOW[rel + "::" + ident + "." + key]) continue;
10337
+ var readRe = new RegExp(
10338
+ "\\b" + ident + "\\s*(?:\\.\\s*" + key + "\\b|\\[\\s*[\"']" + key + "[\"']\\s*\\])");
10339
+ if (!readRe.test(content)) {
10340
+ var lineNum = content.slice(0, m.index).split(/\r?\n/).length;
10341
+ bad.push({
10342
+ file: rel, line: lineNum,
10343
+ content: "validateOpts accepts \"" + key + "\" on `" + ident +
10344
+ "` but the file never reads " + ident + "." + key +
10345
+ " — wire the knob, de-advertise it, or ALLOW with a consumption cite",
10346
+ });
10347
+ }
10348
+ }
10349
+ }
10350
+ }
10351
+ bad = _filterMarkers(bad, "validateopts-key-never-read");
10352
+ _report("every validateOpts-accepted key is read in the same file " +
10353
+ "(v0.14.21 — an accepted-but-never-read key is an advertised knob " +
10354
+ "with no implementation; csp-report opts.audit shipped as a no-op)",
10355
+ bad);
10356
+ }
10357
+
10052
10358
  // ---- Pattern: b.fsm.define freezes without cloning ----
10053
10359
  //
10054
10360
  // class: fsm-define-no-clone-before-freeze
@@ -11491,6 +11797,10 @@ async function run() {
11491
11797
  // v0.12.1 compliance posture coverage detector: KNOWN_POSTURES ⊇
11492
11798
  // POSTURE_DEFAULTS + REGIME_MAP ⊇ KNOWN_POSTURES.
11493
11799
  testCompliancePostureCoverage();
11800
+ // v0.14.21 audit-fix detector: a validateOpts-accepted key that no
11801
+ // code reads is an advertised knob with no implementation
11802
+ // (csp-report opts.audit shipped as a silent no-op).
11803
+ testValidateOptsAcceptedKeysAreRead();
11494
11804
  testKnownAntipatterns();
11495
11805
 
11496
11806
  // Final cumulative assertion — every detector is a hard gate.
@@ -377,6 +377,51 @@ async function _testLoadDbBackedFailedReloadDoesNotSuppressOlderValid() {
377
377
  cfg.stop();
378
378
  }
379
379
 
380
+ async function _testLoadDbBackedAuditKnob() {
381
+ // The `audit` opt gates the per-poll config.reload.* audit emissions.
382
+ // Default (omitted) emits; audit:false silences. We drive the fetch
383
+ // failure path (config.reload.failed) and observe b.audit.safeEmit —
384
+ // config.js emits through the shared audit module instance.
385
+ var s = b.safeSchema;
386
+ var realSafeEmit = b.audit.safeEmit;
387
+ var reloadEvents = [];
388
+ b.audit.safeEmit = function (rec) {
389
+ if (rec && typeof rec.action === "string" && rec.action.indexOf("config.reload.") === 0) {
390
+ reloadEvents.push(rec.action);
391
+ }
392
+ return realSafeEmit.call(b.audit, rec);
393
+ };
394
+ try {
395
+ // Default — audit fires on the fetch failure.
396
+ var cfgDefault = b.config.loadDbBacked({
397
+ schema: s.object({ K: s.string().default("d") }),
398
+ env: {},
399
+ fetchRows: function () { throw new Error("simulated db outage"); },
400
+ intervalMs: 60 * 1000,
401
+ });
402
+ await cfgDefault.hydrated;
403
+ helpers.check("loadDbBacked.audit default: config.reload.failed emitted",
404
+ reloadEvents.indexOf("config.reload.failed") !== -1);
405
+ cfgDefault.stop();
406
+
407
+ // audit:false — same failure, zero emissions.
408
+ reloadEvents.length = 0;
409
+ var cfgSilent = b.config.loadDbBacked({
410
+ schema: s.object({ K: s.string().default("d") }),
411
+ env: {},
412
+ fetchRows: function () { throw new Error("simulated db outage"); },
413
+ intervalMs: 60 * 1000,
414
+ audit: false,
415
+ });
416
+ await cfgSilent.hydrated;
417
+ helpers.check("loadDbBacked.audit false: no config.reload.* emissions",
418
+ reloadEvents.length === 0);
419
+ cfgSilent.stop();
420
+ } finally {
421
+ b.audit.safeEmit = realSafeEmit;
422
+ }
423
+ }
424
+
380
425
  async function _testCryptoFieldDocAliases() {
381
426
  // sealDoc / unsealDoc are doc-shaped aliases of sealRow / unsealRow.
382
427
  helpers.check("b.cryptoField.sealDoc exists",
@@ -412,6 +457,7 @@ module.exports = { run: async function () {
412
457
  await _testLoadDbBackedRefresh();
413
458
  await _testLoadDbBackedConcurrentRefreshRace();
414
459
  await _testLoadDbBackedFailedReloadDoesNotSuppressOlderValid();
460
+ await _testLoadDbBackedAuditKnob();
415
461
  await _testCryptoFieldDocAliases();
416
462
  await _testHotReload();
417
463
  } };