@blamejs/blamejs-shop 0.4.31 → 0.4.32

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 (336) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/vendor/MANIFEST.json +392 -278
  4. package/lib/vendor/blamejs/.github/workflows/ci.yml +34 -3
  5. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +21 -4
  6. package/lib/vendor/blamejs/.gitignore +6 -0
  7. package/lib/vendor/blamejs/CHANGELOG.md +26 -0
  8. package/lib/vendor/blamejs/MIGRATING.md +43 -0
  9. package/lib/vendor/blamejs/README.md +8 -6
  10. package/lib/vendor/blamejs/SECURITY.md +19 -3
  11. package/lib/vendor/blamejs/api-snapshot.json +2190 -664
  12. package/lib/vendor/blamejs/docker/caddy/localstack.Caddyfile +19 -0
  13. package/lib/vendor/blamejs/docker/init/generate-certs.sh +1 -1
  14. package/lib/vendor/blamejs/docker/otel/config.yaml +42 -0
  15. package/lib/vendor/blamejs/docker/otel/export/.gitkeep +0 -0
  16. package/lib/vendor/blamejs/docker/postgres/initdb/10-replication.sh +15 -0
  17. package/lib/vendor/blamejs/docker/postgres/replica-entrypoint.sh +38 -0
  18. package/lib/vendor/blamejs/docker/toxiproxy/toxiproxy.json +14 -0
  19. package/lib/vendor/blamejs/docker-compose.test.yml +209 -0
  20. package/lib/vendor/blamejs/examples/wiki/lib/page-generator.js +132 -0
  21. package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +221 -61
  22. package/lib/vendor/blamejs/examples/wiki/lib/source-doc-parser.js +144 -9
  23. package/lib/vendor/blamejs/examples/wiki/test/e2e.js +99 -0
  24. package/lib/vendor/blamejs/fuzz/guard-sql.fuzz.js +36 -0
  25. package/lib/vendor/blamejs/index.js +4 -0
  26. package/lib/vendor/blamejs/lib/agent-envelope-mac.js +104 -0
  27. package/lib/vendor/blamejs/lib/agent-event-bus.js +105 -4
  28. package/lib/vendor/blamejs/lib/agent-posture-chain.js +8 -42
  29. package/lib/vendor/blamejs/lib/ai-content-detect.js +9 -10
  30. package/lib/vendor/blamejs/lib/api-key.js +158 -77
  31. package/lib/vendor/blamejs/lib/atomic-file.js +62 -4
  32. package/lib/vendor/blamejs/lib/audit-chain.js +47 -11
  33. package/lib/vendor/blamejs/lib/audit-sign.js +77 -2
  34. package/lib/vendor/blamejs/lib/audit-tools.js +79 -51
  35. package/lib/vendor/blamejs/lib/audit.js +259 -123
  36. package/lib/vendor/blamejs/lib/auth/oauth.js +53 -9
  37. package/lib/vendor/blamejs/lib/auth/openid-federation.js +108 -47
  38. package/lib/vendor/blamejs/lib/auth/saml.js +6 -8
  39. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +31 -5
  40. package/lib/vendor/blamejs/lib/backup/index.js +45 -10
  41. package/lib/vendor/blamejs/lib/break-glass.js +355 -147
  42. package/lib/vendor/blamejs/lib/cache.js +174 -105
  43. package/lib/vendor/blamejs/lib/chain-writer.js +38 -16
  44. package/lib/vendor/blamejs/lib/cli.js +19 -14
  45. package/lib/vendor/blamejs/lib/cluster-provider-db.js +130 -104
  46. package/lib/vendor/blamejs/lib/cluster-storage.js +119 -22
  47. package/lib/vendor/blamejs/lib/cluster.js +119 -71
  48. package/lib/vendor/blamejs/lib/codepoint-class.js +23 -0
  49. package/lib/vendor/blamejs/lib/compliance.js +206 -4
  50. package/lib/vendor/blamejs/lib/consent.js +82 -29
  51. package/lib/vendor/blamejs/lib/constants.js +27 -11
  52. package/lib/vendor/blamejs/lib/crypto-field.js +916 -156
  53. package/lib/vendor/blamejs/lib/db-declare-row-policy.js +35 -22
  54. package/lib/vendor/blamejs/lib/db-file-lifecycle.js +3 -2
  55. package/lib/vendor/blamejs/lib/db-query.js +882 -260
  56. package/lib/vendor/blamejs/lib/db-schema.js +228 -44
  57. package/lib/vendor/blamejs/lib/db.js +249 -99
  58. package/lib/vendor/blamejs/lib/dsr.js +385 -55
  59. package/lib/vendor/blamejs/lib/error-page.js +14 -1
  60. package/lib/vendor/blamejs/lib/external-db-migrate.js +239 -137
  61. package/lib/vendor/blamejs/lib/external-db.js +549 -34
  62. package/lib/vendor/blamejs/lib/file-upload.js +52 -7
  63. package/lib/vendor/blamejs/lib/framework-error.js +20 -1
  64. package/lib/vendor/blamejs/lib/framework-files.js +73 -0
  65. package/lib/vendor/blamejs/lib/framework-schema.js +695 -394
  66. package/lib/vendor/blamejs/lib/gate-contract.js +659 -1
  67. package/lib/vendor/blamejs/lib/guard-agent-registry.js +26 -44
  68. package/lib/vendor/blamejs/lib/guard-all.js +1 -0
  69. package/lib/vendor/blamejs/lib/guard-auth.js +42 -112
  70. package/lib/vendor/blamejs/lib/guard-cidr.js +33 -154
  71. package/lib/vendor/blamejs/lib/guard-csv.js +46 -113
  72. package/lib/vendor/blamejs/lib/guard-domain.js +34 -157
  73. package/lib/vendor/blamejs/lib/guard-dsn.js +27 -43
  74. package/lib/vendor/blamejs/lib/guard-email.js +47 -69
  75. package/lib/vendor/blamejs/lib/guard-envelope.js +19 -32
  76. package/lib/vendor/blamejs/lib/guard-event-bus-payload.js +24 -42
  77. package/lib/vendor/blamejs/lib/guard-event-bus-topic.js +25 -43
  78. package/lib/vendor/blamejs/lib/guard-filename.js +42 -106
  79. package/lib/vendor/blamejs/lib/guard-graphql.js +42 -123
  80. package/lib/vendor/blamejs/lib/guard-html.js +53 -108
  81. package/lib/vendor/blamejs/lib/guard-idempotency-key.js +24 -42
  82. package/lib/vendor/blamejs/lib/guard-image.js +46 -103
  83. package/lib/vendor/blamejs/lib/guard-imap-command.js +18 -32
  84. package/lib/vendor/blamejs/lib/guard-jmap.js +16 -30
  85. package/lib/vendor/blamejs/lib/guard-json.js +38 -108
  86. package/lib/vendor/blamejs/lib/guard-jsonpath.js +38 -171
  87. package/lib/vendor/blamejs/lib/guard-jwt.js +49 -179
  88. package/lib/vendor/blamejs/lib/guard-list-id.js +25 -41
  89. package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +27 -43
  90. package/lib/vendor/blamejs/lib/guard-mail-compose.js +24 -42
  91. package/lib/vendor/blamejs/lib/guard-mail-move.js +26 -44
  92. package/lib/vendor/blamejs/lib/guard-mail-query.js +28 -46
  93. package/lib/vendor/blamejs/lib/guard-mail-reply.js +24 -42
  94. package/lib/vendor/blamejs/lib/guard-mail-sieve.js +24 -42
  95. package/lib/vendor/blamejs/lib/guard-managesieve-command.js +17 -31
  96. package/lib/vendor/blamejs/lib/guard-markdown.js +37 -104
  97. package/lib/vendor/blamejs/lib/guard-message-id.js +26 -45
  98. package/lib/vendor/blamejs/lib/guard-mime.js +39 -151
  99. package/lib/vendor/blamejs/lib/guard-oauth.js +54 -135
  100. package/lib/vendor/blamejs/lib/guard-pdf.js +45 -101
  101. package/lib/vendor/blamejs/lib/guard-pop3-command.js +21 -31
  102. package/lib/vendor/blamejs/lib/guard-posture-chain.js +24 -42
  103. package/lib/vendor/blamejs/lib/guard-regex.js +33 -107
  104. package/lib/vendor/blamejs/lib/guard-saga-config.js +24 -42
  105. package/lib/vendor/blamejs/lib/guard-shell.js +42 -172
  106. package/lib/vendor/blamejs/lib/guard-smtp-command.js +48 -54
  107. package/lib/vendor/blamejs/lib/guard-snapshot-envelope.js +24 -42
  108. package/lib/vendor/blamejs/lib/guard-sql.js +1491 -0
  109. package/lib/vendor/blamejs/lib/guard-stream-args.js +24 -43
  110. package/lib/vendor/blamejs/lib/guard-svg.js +47 -65
  111. package/lib/vendor/blamejs/lib/guard-template.js +35 -172
  112. package/lib/vendor/blamejs/lib/guard-tenant-id.js +26 -45
  113. package/lib/vendor/blamejs/lib/guard-time.js +32 -154
  114. package/lib/vendor/blamejs/lib/guard-trace-context.js +25 -44
  115. package/lib/vendor/blamejs/lib/guard-uuid.js +32 -153
  116. package/lib/vendor/blamejs/lib/guard-xml.js +38 -113
  117. package/lib/vendor/blamejs/lib/guard-yaml.js +51 -163
  118. package/lib/vendor/blamejs/lib/http-client.js +37 -9
  119. package/lib/vendor/blamejs/lib/inbox.js +120 -107
  120. package/lib/vendor/blamejs/lib/legal-hold.js +121 -50
  121. package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +47 -31
  122. package/lib/vendor/blamejs/lib/log-stream-otlp.js +32 -18
  123. package/lib/vendor/blamejs/lib/mail-auth.js +236 -0
  124. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +2 -6
  125. package/lib/vendor/blamejs/lib/mail-dkim.js +1 -0
  126. package/lib/vendor/blamejs/lib/mail-greylist.js +2 -6
  127. package/lib/vendor/blamejs/lib/mail-helo.js +2 -6
  128. package/lib/vendor/blamejs/lib/mail-journal.js +85 -64
  129. package/lib/vendor/blamejs/lib/mail-rbl.js +2 -6
  130. package/lib/vendor/blamejs/lib/mail-scan.js +2 -6
  131. package/lib/vendor/blamejs/lib/mail-server-jmap.js +117 -12
  132. package/lib/vendor/blamejs/lib/mail-server-mx.js +276 -7
  133. package/lib/vendor/blamejs/lib/mail-spam-score.js +2 -6
  134. package/lib/vendor/blamejs/lib/mail-store.js +293 -154
  135. package/lib/vendor/blamejs/lib/mail.js +8 -4
  136. package/lib/vendor/blamejs/lib/middleware/body-parser.js +71 -25
  137. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +19 -8
  138. package/lib/vendor/blamejs/lib/middleware/dpop.js +10 -1
  139. package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +17 -7
  140. package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +75 -51
  141. package/lib/vendor/blamejs/lib/middleware/rate-limit.js +102 -32
  142. package/lib/vendor/blamejs/lib/middleware/security-headers.js +21 -5
  143. package/lib/vendor/blamejs/lib/migrations.js +108 -66
  144. package/lib/vendor/blamejs/lib/network-heartbeat.js +7 -0
  145. package/lib/vendor/blamejs/lib/network-proxy.js +24 -1
  146. package/lib/vendor/blamejs/lib/nonce-store.js +31 -9
  147. package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +9 -4
  148. package/lib/vendor/blamejs/lib/object-store/azure-blob.js +57 -3
  149. package/lib/vendor/blamejs/lib/object-store/gcs.js +4 -1
  150. package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -2
  151. package/lib/vendor/blamejs/lib/object-store/sigv4.js +38 -6
  152. package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +9 -1
  153. package/lib/vendor/blamejs/lib/observability.js +124 -0
  154. package/lib/vendor/blamejs/lib/otel-export.js +12 -3
  155. package/lib/vendor/blamejs/lib/outbox.js +184 -83
  156. package/lib/vendor/blamejs/lib/parsers/safe-xml.js +47 -7
  157. package/lib/vendor/blamejs/lib/pqc-agent.js +44 -0
  158. package/lib/vendor/blamejs/lib/pubsub-cluster.js +42 -20
  159. package/lib/vendor/blamejs/lib/queue-local.js +225 -140
  160. package/lib/vendor/blamejs/lib/queue-redis.js +9 -1
  161. package/lib/vendor/blamejs/lib/queue-sqs.js +6 -0
  162. package/lib/vendor/blamejs/lib/queue.js +7 -0
  163. package/lib/vendor/blamejs/lib/redact.js +68 -11
  164. package/lib/vendor/blamejs/lib/redis-client.js +160 -31
  165. package/lib/vendor/blamejs/lib/request-helpers.js +7 -0
  166. package/lib/vendor/blamejs/lib/retention.js +101 -40
  167. package/lib/vendor/blamejs/lib/router.js +212 -5
  168. package/lib/vendor/blamejs/lib/safe-dns.js +29 -45
  169. package/lib/vendor/blamejs/lib/safe-ical.js +18 -33
  170. package/lib/vendor/blamejs/lib/safe-icap.js +27 -43
  171. package/lib/vendor/blamejs/lib/safe-sieve.js +21 -40
  172. package/lib/vendor/blamejs/lib/safe-sql.js +212 -3
  173. package/lib/vendor/blamejs/lib/safe-url.js +170 -3
  174. package/lib/vendor/blamejs/lib/safe-vcard.js +18 -33
  175. package/lib/vendor/blamejs/lib/scheduler.js +35 -12
  176. package/lib/vendor/blamejs/lib/seeders.js +122 -74
  177. package/lib/vendor/blamejs/lib/session-stores.js +42 -14
  178. package/lib/vendor/blamejs/lib/session.js +175 -77
  179. package/lib/vendor/blamejs/lib/sql.js +3842 -0
  180. package/lib/vendor/blamejs/lib/sse.js +26 -0
  181. package/lib/vendor/blamejs/lib/ssrf-guard.js +151 -4
  182. package/lib/vendor/blamejs/lib/static.js +177 -34
  183. package/lib/vendor/blamejs/lib/subject.js +96 -49
  184. package/lib/vendor/blamejs/lib/vault/index.js +3 -2
  185. package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +3 -2
  186. package/lib/vendor/blamejs/lib/vault/rotate.js +168 -108
  187. package/lib/vendor/blamejs/lib/vault-aad.js +6 -0
  188. package/lib/vendor/blamejs/lib/vendor-data.js +2 -0
  189. package/lib/vendor/blamejs/lib/websocket.js +35 -5
  190. package/lib/vendor/blamejs/lib/worker-pool.js +11 -0
  191. package/lib/vendor/blamejs/package.json +2 -2
  192. package/lib/vendor/blamejs/release-notes/v0.14.x.json +1503 -0
  193. package/lib/vendor/blamejs/release-notes/v0.15.0.json +77 -0
  194. package/lib/vendor/blamejs/release-notes/v0.15.1.json +22 -0
  195. package/lib/vendor/blamejs/release-notes/v0.15.2.json +22 -0
  196. package/lib/vendor/blamejs/release-notes/v0.15.3.json +39 -0
  197. package/lib/vendor/blamejs/release-notes/v0.15.4.json +39 -0
  198. package/lib/vendor/blamejs/release-notes/v0.15.5.json +22 -0
  199. package/lib/vendor/blamejs/release-notes/v0.15.6.json +59 -0
  200. package/lib/vendor/blamejs/scripts/check-services.js +21 -0
  201. package/lib/vendor/blamejs/scripts/gen-migrating.js +51 -0
  202. package/lib/vendor/blamejs/scripts/release.js +398 -38
  203. package/lib/vendor/blamejs/test/00-primitives.js +117 -0
  204. package/lib/vendor/blamejs/test/10-state.js +140 -14
  205. package/lib/vendor/blamejs/test/20-db.js +65 -2
  206. package/lib/vendor/blamejs/test/helpers/db.js +9 -0
  207. package/lib/vendor/blamejs/test/helpers/drivers.js +27 -15
  208. package/lib/vendor/blamejs/test/helpers/services.js +21 -0
  209. package/lib/vendor/blamejs/test/integration/audit-actor-binding-pg.test.js +246 -0
  210. package/lib/vendor/blamejs/test/integration/audit-chain-external-db.test.js +517 -0
  211. package/lib/vendor/blamejs/test/integration/audit-stack-mysql.test.js +639 -0
  212. package/lib/vendor/blamejs/test/integration/audit-stack-postgres.test.js +832 -0
  213. package/lib/vendor/blamejs/test/integration/backup-restore-objectstore.test.js +453 -0
  214. package/lib/vendor/blamejs/test/integration/data-layer-cluster-mysql.test.js +649 -0
  215. package/lib/vendor/blamejs/test/integration/data-layer-cluster-pg.test.js +770 -0
  216. package/lib/vendor/blamejs/test/integration/data-layer-mysql-privacy.test.js +630 -0
  217. package/lib/vendor/blamejs/test/integration/data-layer-mysql.test.js +610 -0
  218. package/lib/vendor/blamejs/test/integration/data-layer-pg.test.js +577 -0
  219. package/lib/vendor/blamejs/test/integration/data-layer-postgres.test.js +771 -0
  220. package/lib/vendor/blamejs/test/integration/db-layer-mysql.test.js +549 -0
  221. package/lib/vendor/blamejs/test/integration/db-layer-postgres.test.js +598 -0
  222. package/lib/vendor/blamejs/test/integration/distributed-scheduler-fencing-pg.test.js +602 -0
  223. package/lib/vendor/blamejs/test/integration/external-db-postgres.test.js +576 -0
  224. package/lib/vendor/blamejs/test/integration/framework-schema-mysql.test.js +353 -0
  225. package/lib/vendor/blamejs/test/integration/log-stream-cloudwatch.test.js +224 -0
  226. package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +142 -17
  227. package/lib/vendor/blamejs/test/integration/network-heartbeat.test.js +25 -10
  228. package/lib/vendor/blamejs/test/integration/object-store-azure.test.js +101 -0
  229. package/lib/vendor/blamejs/test/integration/object-store-gcs.test.js +239 -0
  230. package/lib/vendor/blamejs/test/integration/object-store-sigv4.test.js +35 -16
  231. package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +291 -0
  232. package/lib/vendor/blamejs/test/integration/pubsub.test.js +14 -0
  233. package/lib/vendor/blamejs/test/integration/queue-sqs.test.js +322 -0
  234. package/lib/vendor/blamejs/test/integration/redis-reconnect-toxiproxy.test.js +300 -0
  235. package/lib/vendor/blamejs/test/integration/sql-fts5-catalog-sqlite.test.js +154 -0
  236. package/lib/vendor/blamejs/test/integration/tls-classical-downgrade-audit.test.js +71 -0
  237. package/lib/vendor/blamejs/test/layer-0-primitives/agent-event-bus.test.js +175 -12
  238. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-exclusive-temp.test.js +216 -0
  239. package/lib/vendor/blamejs/test/layer-0-primitives/audit-checkpoint-false-rollback.test.js +203 -0
  240. package/lib/vendor/blamejs/test/layer-0-primitives/audit-query-self-log.test.js +126 -0
  241. package/lib/vendor/blamejs/test/layer-0-primitives/audit-safeemit-redacts-secrets.test.js +196 -0
  242. package/lib/vendor/blamejs/test/layer-0-primitives/audit-signing-key-rotation.test.js +197 -0
  243. package/lib/vendor/blamejs/test/layer-0-primitives/audit-verifybundle-tamper.test.js +209 -0
  244. package/lib/vendor/blamejs/test/layer-0-primitives/azure-blob-key-encoding.test.js +121 -0
  245. package/lib/vendor/blamejs/test/layer-0-primitives/backup-residency-posture.test.js +168 -0
  246. package/lib/vendor/blamejs/test/layer-0-primitives/backup-scheduletest-drill.test.js +318 -0
  247. package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +233 -7
  248. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +1120 -14
  249. package/lib/vendor/blamejs/test/layer-0-primitives/compliance.test.js +229 -0
  250. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +24 -7
  251. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-dual-read-migrate.test.js +165 -0
  252. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-per-row-key.test.js +350 -0
  253. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-unseal-rate-cap.test.js +27 -9
  254. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-upgrade-dialect.test.js +76 -0
  255. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-interop-oracles.test.js +392 -0
  256. package/lib/vendor/blamejs/test/layer-0-primitives/csrf-protect.test.js +159 -0
  257. package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +180 -1
  258. package/lib/vendor/blamejs/test/layer-0-primitives/db-query-cross-schema.test.js +5 -2
  259. package/lib/vendor/blamejs/test/layer-0-primitives/db-query-sealed-field-in.test.js +101 -0
  260. package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +128 -0
  261. package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +38 -5
  262. package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-reconcile-emittable.test.js +127 -0
  263. package/lib/vendor/blamejs/test/layer-0-primitives/db-stream-and-payload-shape.test.js +267 -0
  264. package/lib/vendor/blamejs/test/layer-0-primitives/db-worm.test.js +150 -0
  265. package/lib/vendor/blamejs/test/layer-0-primitives/defineguard-default-gate-posture-caps.test.js +30 -0
  266. package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-replaystore-required.test.js +46 -0
  267. package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +218 -0
  268. package/lib/vendor/blamejs/test/layer-0-primitives/erase-posture-vacuum.test.js +210 -0
  269. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +4 -1
  270. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-migrate.test.js +48 -2
  271. package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +237 -5
  272. package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +20 -9
  273. package/lib/vendor/blamejs/test/layer-0-primitives/file-upload-content-safety-skip-audit.test.js +193 -0
  274. package/lib/vendor/blamejs/test/layer-0-primitives/guard-csv.test.js +90 -0
  275. package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +85 -0
  276. package/lib/vendor/blamejs/test/layer-0-primitives/idempotency-key.test.js +10 -6
  277. package/lib/vendor/blamejs/test/layer-0-primitives/inbox.test.js +15 -4
  278. package/lib/vendor/blamejs/test/layer-0-primitives/legal-hold.test.js +146 -0
  279. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +189 -0
  280. package/lib/vendor/blamejs/test/layer-0-primitives/mail-journal.test.js +3 -1
  281. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-jmap.test.js +123 -4
  282. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +207 -2
  283. package/lib/vendor/blamejs/test/layer-0-primitives/mail-store.test.js +74 -0
  284. package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +43 -0
  285. package/lib/vendor/blamejs/test/layer-0-primitives/otel-export.test.js +133 -0
  286. package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +101 -0
  287. package/lib/vendor/blamejs/test/layer-0-primitives/outbox-inflight-reaper.test.js +136 -0
  288. package/lib/vendor/blamejs/test/layer-0-primitives/parsers-standalone.test.js +83 -0
  289. package/lib/vendor/blamejs/test/layer-0-primitives/passkey-real-vectors.test.js +429 -0
  290. package/lib/vendor/blamejs/test/layer-0-primitives/pqc-agent-curve.test.js +21 -11
  291. package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +40 -0
  292. package/lib/vendor/blamejs/test/layer-0-primitives/redact-dlp.test.js +83 -0
  293. package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +113 -0
  294. package/lib/vendor/blamejs/test/layer-0-primitives/retention-dryrun-no-vacuum.test.js +99 -0
  295. package/lib/vendor/blamejs/test/layer-0-primitives/router-use-path-scope.test.js +255 -0
  296. package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-canonicalize.test.js +309 -0
  297. package/lib/vendor/blamejs/test/layer-0-primitives/safe-xml.test.js +143 -0
  298. package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js +287 -0
  299. package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc-ecdsa-p1363.test.js +79 -0
  300. package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +50 -0
  301. package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +31 -4
  302. package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +45 -0
  303. package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +49 -0
  304. package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +595 -0
  305. package/lib/vendor/blamejs/test/layer-0-primitives/sse-backpressure.test.js +91 -0
  306. package/lib/vendor/blamejs/test/layer-0-primitives/ssrf-guard.test.js +69 -0
  307. package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +194 -2
  308. package/lib/vendor/blamejs/test/layer-0-primitives/websocket-extension-header.test.js +88 -0
  309. package/lib/vendor/blamejs/test/layer-0-primitives/worker-pool-recycle-race.test.js +66 -0
  310. package/lib/vendor/blamejs/test/layer-1-state/api-key.test.js +84 -0
  311. package/lib/vendor/blamejs/test/layer-5-integration/external-db-residency.test.js +638 -0
  312. package/lib/vendor/blamejs/test/layer-5-integration/guard-host-integration.test.js +21 -0
  313. package/lib/vendor/blamejs/test/smoke.js +79 -21
  314. package/package.json +1 -1
  315. package/lib/vendor/blamejs/release-notes/v0.14.0.json +0 -43
  316. package/lib/vendor/blamejs/release-notes/v0.14.1.json +0 -60
  317. package/lib/vendor/blamejs/release-notes/v0.14.10.json +0 -54
  318. package/lib/vendor/blamejs/release-notes/v0.14.11.json +0 -72
  319. package/lib/vendor/blamejs/release-notes/v0.14.12.json +0 -95
  320. package/lib/vendor/blamejs/release-notes/v0.14.13.json +0 -52
  321. package/lib/vendor/blamejs/release-notes/v0.14.14.json +0 -31
  322. package/lib/vendor/blamejs/release-notes/v0.14.16.json +0 -45
  323. package/lib/vendor/blamejs/release-notes/v0.14.17.json +0 -57
  324. package/lib/vendor/blamejs/release-notes/v0.14.18.json +0 -127
  325. package/lib/vendor/blamejs/release-notes/v0.14.19.json +0 -61
  326. package/lib/vendor/blamejs/release-notes/v0.14.2.json +0 -18
  327. package/lib/vendor/blamejs/release-notes/v0.14.20.json +0 -73
  328. package/lib/vendor/blamejs/release-notes/v0.14.21.json +0 -98
  329. package/lib/vendor/blamejs/release-notes/v0.14.22.json +0 -91
  330. package/lib/vendor/blamejs/release-notes/v0.14.3.json +0 -18
  331. package/lib/vendor/blamejs/release-notes/v0.14.4.json +0 -18
  332. package/lib/vendor/blamejs/release-notes/v0.14.5.json +0 -18
  333. package/lib/vendor/blamejs/release-notes/v0.14.6.json +0 -60
  334. package/lib/vendor/blamejs/release-notes/v0.14.7.json +0 -77
  335. package/lib/vendor/blamejs/release-notes/v0.14.8.json +0 -27
  336. package/lib/vendor/blamejs/release-notes/v0.14.9.json +0 -40
@@ -1,5 +1,12 @@
1
1
  "use strict";
2
2
 
3
+ // SMOKE_RUN_SOLO — the smoke runner (test/smoke.js) runs this file ALONE
4
+ // with the whole machine instead of inside the parallel layer-0 pool.
5
+ // The duplicate-block scan fans out across worker_threads and is CPU-
6
+ // bound; sharing a low-core CI runner (macos-latest = 3 cores) with
7
+ // sibling forks oversubscribes the CPU and the scan overruns its
8
+ // per-file watchdog budget. Run alone, it finishes in its normal time.
9
+
3
10
  // Re-exec under a 6 GiB old-space ceiling when the parent process did
4
11
  // not already raise the heap cap. The test's cartesian fingerprint /
5
12
  // cluster index across ~300 lib/ files lands close to the v8 default
@@ -300,6 +307,7 @@ var VALID_ALLOW_CLASSES = {
300
307
  "dynamic-regex": 1,
301
308
  "dynamic-require": 1,
302
309
  "from-base64url-untrapped": 1,
310
+ "hand-rolled-sql": 1,
303
311
  "fs-path-from-operator-identifier-without-traversal-refusal": 1,
304
312
  "gitleaks-entropy": 1,
305
313
  "handrolled-buffer-collect": 1,
@@ -852,6 +860,178 @@ function testNoTierTerminologyInLib() {
852
860
  _report("no Tier-A / Tier-B / Tier-C terminology in lib/", matches);
853
861
  }
854
862
 
863
+ // ---- No hand-rolled SQL — compose b.sql / b.guardSql ----
864
+ //
865
+ // String-built SQL is the surface that breaks on Postgres (unquoted
866
+ // identifiers fold to lowercase) and that the b.sql builder eliminates
867
+ // by construction: it quotes every identifier through b.safeSql, binds
868
+ // every value as a placeholder, resolves table names + the configurable
869
+ // prefix, and routes raw fragments through b.guardSql. So no lib/ module
870
+ // should compose SQL by hand — it should build it through b.sql, and it
871
+ // should never hardcode a `_blamejs_*` table literal (that bypasses the
872
+ // configurable-prefix resolution). This detector flags both: a string
873
+ // literal that STARTS a SQL statement, and a hardcoded `_blamejs_*`
874
+ // literal, in any DB-touching lib file outside the migration backlog.
875
+ //
876
+ // Files still carrying hand-rolled SQL live on HAND_ROLLED_SQL_BACKLOG
877
+ // until migrated onto b.sql; remove a file from the backlog as it is
878
+ // migrated, and any residual hand-rolled SQL in it then fails the gate
879
+ // (so the migration runs to completion and can't silently stall). A new
880
+ // DB file that hand-rolls SQL without being on the backlog fails
881
+ // immediately. Only DB-touching files (a SQL execution sink or a
882
+ // `_blamejs_` literal) are scanned, so non-SQL `SELECT`/`WITH` text in
883
+ // guard-html / forms / i18n etc. never false-positives.
884
+ //
885
+ // PERMANENT exceptions: the builder/guard/primitive that legitimately
886
+ // produce or inspect SQL text.
887
+ var HAND_ROLLED_SQL_PERMANENT = {
888
+ "lib/sql.js": 1, // the b.sql builder itself
889
+ "lib/guard-sql.js": 1, // the b.guardSql guard inspects SQL text
890
+ "lib/safe-sql.js": 1, // identifier primitive (docstring examples)
891
+ "lib/framework-schema.js": 1, // declarative DDL + the canonical LOCAL_TO_EXTERNAL name source
892
+ };
893
+ // Migration backlog — every DB file still hand-rolling SQL. Shrinks to
894
+ // empty as the everything-sweep migrates each onto b.sql.
895
+ //
896
+ // db-declare-row-policy / outbox / inbox now compose b.sql: the builder
897
+ // grew the constructs they needed — Postgres RLS (enableRowLevelSecurity /
898
+ // createPolicy / dropPolicy), `FOR UPDATE SKIP LOCKED` (forUpdate), the
899
+ // single-bind `col = ANY(?)` array form (whereInArray), an allowlisted
900
+ // value-position SQL function (fn -> NOW() / CURRENT_TIMESTAMP) and cast
901
+ // (cast -> `?::jsonb` / `?::interval`), `ON CONFLICT DO NOTHING RETURNING`
902
+ // (doNothing().returning()), the sqlite `SELECT changes()` probe
903
+ // (catalog.changes), a partial index (createIndex { where }), and the
904
+ // driver-final `$1..$N` translation for code that hands SQL to an
905
+ // operator-supplied driver directly (toExternalSql).
906
+ //
907
+ // vault/rotate now composes b.sql end to end: the at-rest key-rotation
908
+ // pipeline walks its standalone node:sqlite handle through the catalog /
909
+ // pragma sub-API (`sqlite_master` list / `tableExists` / `PRAGMA
910
+ // table_info` / `journal_mode` / `synchronous` / `wal_checkpoint` /
911
+ // `ORDER BY RANDOM()`), and the per-column re-seal SELECT/UPDATE + drift
912
+ // sample + verification COUNT through b.sql.select / b.sql.update.
913
+ //
914
+ // mail-store now composes b.sql end to end: the sqlite-only sealed full-
915
+ // text mail store builds every cached prepared statement + the schema
916
+ // bootstrap through b.sql with { dialect: "sqlite", quoteName: true } (the
917
+ // store targets a concrete sqlite handle, never clusterStorage, so each
918
+ // prefixed table name emits as a quoted identifier). The FTS5 search runs
919
+ // through whereMatch (the `<fts> MATCH ?` IN-subquery), the hard-expunge
920
+ // candidate set through whereInJsonEach (json_each), and the FTS5 virtual-
921
+ // table DDL through createVirtualTable; the composite-PK flags table, the
922
+ // ON-DELETE-CASCADE FK back to messages, and the per-folder quota
923
+ // accumulator (`col = col + EXCLUDED.col`) compose createTable /
924
+ // upsert.doUpdate. The backlog is now empty.
925
+ var HAND_ROLLED_SQL_BACKLOG = {
926
+ };
927
+ function testNoHandRolledSql() {
928
+ // A DB-touching file: composes a SQL execution sink or hardcodes a
929
+ // framework table name. Only these are scanned (no non-SQL FPs).
930
+ var SINK = /clusterStorage\.(?:execute|executeOne|executeAll)|externalDb\.query|\bdb\(\)\.(?:prepare|exec|run)|\b_q\(|\b_psql\(|tx\.query|runSqlOnHandle/;
931
+ // A hardcoded framework TABLE-name literal - `_blamejs_<words>` whose
932
+ // token does NOT continue into a `.` (which would make it a file name
933
+ // like `_blamejs_rotate.tmp.db`, not a table reference). The trailing
934
+ // `(?![a-z_.])` asserts the whole token ended (no further word char) and
935
+ // is not immediately followed by `.`, keeping the rule on hardcoded table
936
+ // names and off staging-file path literals.
937
+ var LIT = /_blamejs_[a-z_]+(?![a-z_.])/;
938
+ // A string literal that STARTS a SQL statement.
939
+ // TRUNCATE requires a following TABLE keyword or a table-name token - a
940
+ // bare quoted "TRUNCATE" (e.g. a PRAGMA wal_checkpoint mode arg, or an
941
+ // FTS5 token) is not the TRUNCATE statement and must not false-positive.
942
+ var START = /(["'`])\s*(SELECT\b|INSERT\s+(?:INTO|OR)\b|REPLACE\s+INTO\b|UPDATE\s+["'`]?[A-Za-z_]|DELETE\s+FROM\b|CREATE\s+(?:TABLE|UNIQUE\s+INDEX|INDEX|TRIGGER|VIRTUAL\s+TABLE|OR\s+REPLACE)\b|ALTER\s+TABLE\b|DROP\s+(?:TABLE|TRIGGER|INDEX)\b|WITH\s+[A-Za-z_]|MERGE\s+INTO\b|TRUNCATE\s+(?:TABLE\s+)?["'`]?[A-Za-z_])/i;
943
+ // A SQL CLAUSE fragment assembled by string concatenation (mid-statement
944
+ // construction a START-only check misses): `... + " WHERE " + ...`,
945
+ // `" SET " +`, `" VALUES (" +`, `" FROM " +`, `" ORDER BY " +`, the JOIN
946
+ // family, ON CONFLICT / RETURNING / LIMIT / OFFSET / HAVING / GROUP BY.
947
+ // The leading/trailing `+` (or the unclosed `(` for VALUES) is the
948
+ // build-by-concat tell that separates it from a fragment passed whole to
949
+ // b.sql's whereRaw / setRaw (which carry their own allow marker).
950
+ var FRAG = /(?:\+\s*(["'`])\s*(?:SET|FROM|WHERE|VALUES|ORDER\s+BY|GROUP\s+BY|HAVING|RETURNING|LIMIT|OFFSET|ON\s+CONFLICT|(?:INNER\s+|LEFT\s+|RIGHT\s+|CROSS\s+)?JOIN)\b|(["'`])\s*(?:SET|FROM|WHERE|VALUES\s*\(|ORDER\s+BY|GROUP\s+BY|HAVING|RETURNING|ON\s+CONFLICT|(?:INNER\s+|LEFT\s+|RIGHT\s+|CROSS\s+)?JOIN)\b[^"'`]*\2\s*\+)/i;
951
+ var matches = [];
952
+ var files = _libFiles();
953
+ for (var i = 0; i < files.length; i++) {
954
+ var rel = _relPath(files[i]);
955
+ if (HAND_ROLLED_SQL_PERMANENT[rel] || HAND_ROLLED_SQL_BACKLOG[rel]) continue;
956
+ var content;
957
+ try { content = fs.readFileSync(files[i], "utf8"); }
958
+ catch (_e) { continue; }
959
+ if (!SINK.test(content) && !LIT.test(content)) continue; // not a DB file
960
+ var lines = content.split(/\r?\n/);
961
+ for (var j = 0; j < lines.length; j++) {
962
+ var line = lines[j];
963
+ if (/^\s*(\/\/|\*|\/\*)/.test(line)) continue; // comment line
964
+ if (START.test(line)) {
965
+ matches.push({ file: rel, line: j + 1, content: "hand-rolled SQL — use b.sql: " + line.trim().slice(0, 90) });
966
+ continue;
967
+ }
968
+ if (FRAG.test(line)) {
969
+ matches.push({ file: rel, line: j + 1, content: "hand-rolled SQL clause built by concatenation — use b.sql: " + line.trim().slice(0, 80) });
970
+ continue;
971
+ }
972
+ if (/_blamejs_[a-z_]+(?!\.)/.test(line)) {
973
+ matches.push({ file: rel, line: j + 1, content: "hardcoded _blamejs_* literal — use frameworkSchema.tableName / b.sql: " + line.trim().slice(0, 70) });
974
+ }
975
+ }
976
+ }
977
+ matches = _filterMarkers(matches, "hand-rolled-sql");
978
+ _report("no hand-rolled SQL outside the b.sql builder (compose b.sql / b.guardSql; no hardcoded _blamejs_* literals)", matches);
979
+ }
980
+
981
+ // ---- Pattern 9b: no hardcoded framework state file names ----
982
+ //
983
+ // The framework's on-disk state file names (db.enc, db.key.enc, vault.key,
984
+ // audit.tip, ...) are centralized in lib/framework-files.js so a rename /
985
+ // relocation is one edit and operators can override them. Every owner should
986
+ // resolve its file name via frameworkFiles.fileName(<logical>) instead of
987
+ // hardcoding the literal. This is the inverse detector that drives that
988
+ // migration in reverse (mirrors testNoHandRolledSql for table names): files
989
+ // still hardcoding a registered name live on FRAMEWORK_FILE_NAME_BACKLOG
990
+ // until migrated; remove a file as it migrates and any residual literal then
991
+ // fails the gate. A NEW lib file hardcoding a registered state-file name
992
+ // without being on the backlog fails immediately.
993
+ var FRAMEWORK_FILE_PERMANENT = {
994
+ "lib/framework-files.js": 1, // the registry that DEFINES the canonical names
995
+ };
996
+ var FRAMEWORK_FILE_NAME_BACKLOG = {
997
+ };
998
+ // Registered state-file names — kept in sync with framework-files.js
999
+ // DEFAULT_FILE_NAMES. A quoted literal of any of these outside the registry
1000
+ // or the backlog is a hardcoded name that should resolve via frameworkFiles.
1001
+ var FRAMEWORK_STATE_FILE_NAMES = [
1002
+ "db.enc", "db.key.enc", "vault.key", "audit.tip", "audit-sign.key",
1003
+ "rows.enc", "checkpoint.enc",
1004
+ ];
1005
+ function testNoHardcodedFrameworkFileNames() {
1006
+ var rx = new RegExp("[\"'`](" + FRAMEWORK_STATE_FILE_NAMES.map(function (n) {
1007
+ return n.replace(/\./g, "\\.");
1008
+ }).join("|") + ")[\"'`]");
1009
+ var matches = [];
1010
+ var files = _libFiles();
1011
+ for (var i = 0; i < files.length; i++) {
1012
+ var rel = _relPath(files[i]);
1013
+ if (FRAMEWORK_FILE_PERMANENT[rel] || FRAMEWORK_FILE_NAME_BACKLOG[rel]) continue;
1014
+ var content;
1015
+ try { content = fs.readFileSync(files[i], "utf8"); }
1016
+ catch (_e) { continue; }
1017
+ if (!rx.test(content)) continue;
1018
+ var lines = content.split(/\r?\n/);
1019
+ for (var j = 0; j < lines.length; j++) {
1020
+ var line = lines[j];
1021
+ if (/^\s*(\/\/|\*|\/\*)/.test(line)) continue; // comment line
1022
+ var m = line.match(rx);
1023
+ if (m) {
1024
+ matches.push({ file: rel, line: j + 1,
1025
+ content: "hardcoded framework state file name " + m[0] +
1026
+ " - resolve via frameworkFiles.fileName(<logical>): " + line.trim().slice(0, 60) });
1027
+ }
1028
+ }
1029
+ }
1030
+ matches = _filterMarkers(matches, "hardcoded-framework-file-name");
1031
+ _report("no hardcoded framework state file names outside lib/framework-files.js " +
1032
+ "(resolve via frameworkFiles.fileName)", matches);
1033
+ }
1034
+
855
1035
  // ---- Pattern 10: inline require() (should be top-of-file) ----
856
1036
 
857
1037
  function testNoInlineRequires() {
@@ -2559,6 +2739,161 @@ async function testNoDuplicateCodeBlocks() {
2559
2739
  // so the audit trail records exactly which body of code shares the
2560
2740
  // shape.
2561
2741
  var KNOWN_CLUSTERS = [
2742
+ {
2743
+ // v0.15.0 #103 guard-family consolidation — the genuine re-implementations
2744
+ // are EXTRACTED into gate-contract primitives (profile resolution ->
2745
+ // gateContract.makeProfileResolver, 24 guards; the sanitize/parse refuse-on-
2746
+ // severity throw -> gateContract.throwOnRefusalSeverity, 18 guards) and reused.
2747
+ // The residual STRONG-DUP across the guard validate/sanitize/parse entry
2748
+ // points is the guards uniformly + CORRECTLY COMPOSING those primitives
2749
+ // (resolveOpts -> _detectIssues -> throwOnRefusalSeverity / aggregateIssues /
2750
+ // numericBounds) — correct usage, not duplication. The only per-guard parts
2751
+ // (the _detectIssues body + the transform) are interleaved + distinct;
2752
+ // templating them would couple unrelated detection grammars. Re-hand-rolling
2753
+ // either primitive is caught by the use-makeProfileResolver-not-handrolled +
2754
+ // use-throwOnRefusalSeverity-not-handrolled inverse detectors. The structural /
2755
+ // primitive-aware detector (#102, v0.15.4) will recognise correct usage + retire
2756
+ // this entry (task #104).
2757
+ mode: "family-subset",
2758
+ files: [
2759
+ "lib/guard-agent-registry.js:validate",
2760
+ "lib/guard-auth.js:sanitize",
2761
+ "lib/guard-auth.js:validate",
2762
+ "lib/guard-cidr.js:sanitize",
2763
+ "lib/guard-cidr.js:validate",
2764
+ "lib/guard-domain.js:sanitize",
2765
+ "lib/guard-domain.js:validate",
2766
+ "lib/guard-email.js:sanitize",
2767
+ "lib/guard-email.js:validate",
2768
+ "lib/guard-event-bus-payload.js:validate",
2769
+ "lib/guard-event-bus-topic.js:validate",
2770
+ "lib/guard-filename.js:sanitize",
2771
+ "lib/guard-graphql.js:sanitize",
2772
+ "lib/guard-html.js:sanitize",
2773
+ "lib/guard-idempotency-key.js:validate",
2774
+ "lib/guard-image.js:sanitize",
2775
+ "lib/guard-image.js:validate",
2776
+ "lib/guard-imap-command.js:validate",
2777
+ "lib/guard-jmap.js:validate",
2778
+ "lib/guard-json.js:validate",
2779
+ "lib/guard-jsonpath.js:sanitize",
2780
+ "lib/guard-jsonpath.js:validate",
2781
+ "lib/guard-jwt.js:sanitize",
2782
+ "lib/guard-jwt.js:validate",
2783
+ "lib/guard-list-unsubscribe.js:validate",
2784
+ "lib/guard-mail-compose.js:validate",
2785
+ "lib/guard-mail-move.js:validate",
2786
+ "lib/guard-mail-reply.js:validate",
2787
+ "lib/guard-mail-sieve.js:validate",
2788
+ "lib/guard-managesieve-command.js:validate",
2789
+ "lib/guard-markdown.js:sanitize",
2790
+ "lib/guard-markdown.js:validate",
2791
+ "lib/guard-message-id.js:validate",
2792
+ "lib/guard-mime.js:sanitize",
2793
+ "lib/guard-mime.js:validate",
2794
+ "lib/guard-oauth.js:sanitize",
2795
+ "lib/guard-pdf.js:sanitize",
2796
+ "lib/guard-pdf.js:validate",
2797
+ "lib/guard-pop3-command.js:validate",
2798
+ "lib/guard-posture-chain.js:validate",
2799
+ "lib/guard-regex.js:gate",
2800
+ "lib/guard-regex.js:sanitize",
2801
+ "lib/guard-regex.js:validate",
2802
+ "lib/guard-saga-config.js:validate",
2803
+ "lib/guard-shell.js:sanitize",
2804
+ "lib/guard-shell.js:validate",
2805
+ "lib/guard-smtp-command.js:detectBodySmuggling",
2806
+ "lib/guard-smtp-command.js:validate",
2807
+ "lib/guard-snapshot-envelope.js:validate",
2808
+ "lib/guard-stream-args.js:validate",
2809
+ "lib/guard-svg.js:sanitize",
2810
+ "lib/guard-template.js:sanitize",
2811
+ "lib/guard-template.js:validate",
2812
+ "lib/guard-tenant-id.js:validate",
2813
+ "lib/guard-time.js:sanitize",
2814
+ "lib/guard-time.js:validate",
2815
+ "lib/guard-trace-context.js:validate",
2816
+ "lib/guard-uuid.js:sanitize",
2817
+ "lib/guard-uuid.js:validate",
2818
+ "lib/guard-xml.js:sanitize",
2819
+ "lib/guard-xml.js:validate",
2820
+ "lib/guard-yaml.js:parse",
2821
+ "lib/guard-yaml.js:validate",
2822
+ ],
2823
+ reason: "v0.15.0 #103 — guard-family consolidation CONTRACT SHAPE. The genuine re-implementations are extracted into gate-contract primitives and reused (gateContract.makeProfileResolver — profile resolution, 24 guards; gateContract.throwOnRefusalSeverity — sanitize/parse refuse-on-critical|high throw, 18 guards), so the residual cross-guard STRONG-DUP at the validate/sanitize/parse entry points is the guards uniformly + CORRECTLY composing those primitives plus aggregateIssues / numericBounds — correct usage, not duplication. The per-guard _detectIssues body + transform are distinct and interleaved; templating would couple unrelated detection grammars. Re-hand-rolling either primitive is a hard fail via the use-makeProfileResolver-not-handrolled + use-throwOnRefusalSeverity-not-handrolled inverse detectors. The structural + primitive-aware detector (task #102, v0.15.4) recognises correct primitive usage and retires this allowlist (task #104).",
2824
+ },
2825
+ {
2826
+ mode: "family-subset",
2827
+ files: [
2828
+ "lib/auth/oid4vp.js:matchDcql",
2829
+ "lib/gate-contract.js:_ctxValueForKind",
2830
+ "lib/http-message-signature.js:_parseUrl",
2831
+ ],
2832
+ reason: "v0.15.0 — coincidental 50-tok window across unrelated domains: a DCQL query matcher (auth/oid4vp.matchDcql), the guard ctx-field picker (gate-contract._ctxValueForKind — pick first present ctx field), and an HTTP-message-signature URL parser (http-message-signature._parseUrl). Three unrelated loops; no shared behaviour. Surfaced when the v0.15.0 ctxFields work reshaped _ctxValueForKind's window.",
2833
+ },
2834
+ {
2835
+ mode: "family-subset",
2836
+ files: [
2837
+ "lib/archive-adapters.js:close",
2838
+ "lib/crypto-field.js:listPerRowResidency",
2839
+ "lib/tracing.js:spanSync",
2840
+ ],
2841
+ reason: "v0.15.4 — coincidental 50-tok normalized window across three unrelated domains: an archive adapter's close() (destroy a readable / closeSync an fd), the crypto-field per-row-residency enumerator (listPerRowResidency — map declared tables to {table, residencyColumn, allowedTags}), and the tracing spanSync delegator (type-check fn, delegate to span()). The shingle is the short-function / object-literal-return shell, not behaviour — one releases a handle, one projects a config map, one delegates a call. archive-adapters:close + tracing:spanSync were already a sub-threshold pair; listPerRowResidency (added so backup.create can see per-row cross-border regions a deployment-level check is blind to) became the third member tipping it over the 3-file STRONG-DUP floor.",
2842
+ },
2843
+ {
2844
+ mode: "family-subset",
2845
+ files: [
2846
+ "lib/audit.js:_queryCluster",
2847
+ "lib/auth/ciba.js:startAuthentication",
2848
+ "lib/auth/oauth.js:endSessionUrl",
2849
+ "lib/auth/oauth.js:exchangeToken",
2850
+ ],
2851
+ reason: "v0.14.30 — guarded accumulator-fill idiom (`if (input.X) acc.method(\"name\", input.X)` repeated per optional field). audit._queryCluster builds a b.sql WHERE chain (`if (criteria.X) qb.where(...)`) after the hand-rolled cluster query migrated onto b.sql; auth/ciba.startAuthentication + auth/oauth.endSessionUrl/exchangeToken build URLSearchParams request bodies (`if (opts.X) body.set(...)`). The 50-tok shingle is the chain-of-guarded-single-statement-calls shell, not behaviour: one composes a SQL predicate set, the others compose form-encoded OAuth/CIBA bodies — different accumulators (b.sql builder vs URLSearchParams), different vocabularies, no shared body. Same cross-domain false-match class the SQL char-walk cluster documents.",
2852
+ },
2853
+ {
2854
+ mode: "family-subset",
2855
+ files: [
2856
+ "lib/cluster-storage.js:placeholderize",
2857
+ "lib/db-query.js:_assertRawNoStringLiteral",
2858
+ "lib/guard-sql.js:_hasEmbeddedStringLiteral",
2859
+ "lib/safe-sql.js:countPlaceholders",
2860
+ "lib/safe-sql.js:assertSingleStatement",
2861
+ "lib/sql.js:_assertRawNoStringLiteral",
2862
+ "lib/sql.js:_assertNoRawJsonbKeyOp",
2863
+ "lib/sql.js:_toPositional",
2864
+ ],
2865
+ reason: "v0.14.29 / v0.15.4 - the canonical quote- and comment-aware SQL char-walk shell. The ONE consolidatable instance, the single-statement OUTPUT gate, was extracted to safeSql.assertSingleStatement (v0.15.4): sql._assertEmittable + sql._assertCatalogEmittable now DELEGATE to it (a makeError preserves their sql-builder/* codes), and the raw-DDL paths (db-schema.reconcileTable, the DSR store) route through the same primitive instead of hand-rolling DDL - so a verbatim column type can no longer smuggle a stacked statement. safeSql.countPlaceholders was already the other extracted instance. What REMAINS shares only the char-walk SHELL for genuinely different purposes that cannot share a body: db-query._assertRawNoStringLiteral / sql._assertRawNoStringLiteral refuse a literal in an operator raw fragment (each its own typed error), guard-sql._hasEmbeddedStringLiteral is the tokenizer literal-mask step, sql._toPositional / clusterStorage.placeholderize translate ? placeholders to positional $N, sql._assertNoRawJsonbKeyOp guards a raw JSONB key op, and safeSql.assertSingleStatement is the extracted gate itself. Same shape-only family the external-db opaque-span cluster documents.",
2866
+ },
2867
+ {
2868
+ mode: "family-subset",
2869
+ files: [
2870
+ "lib/auth/fido-mds3.js:_validateChain",
2871
+ "lib/middleware/require-methods.js:create",
2872
+ "lib/network-dns.js:useDesignatedResolvers",
2873
+ "lib/safe-sql.js:quoteList",
2874
+ ],
2875
+ reason: "v0.14.29 — non-empty-array validation prelude (`if (!Array.isArray(x) || x.length === 0) throw; for (i) { if (typeof x[i] !== \"string\" || ...) throw }`). safeSql.quoteList validates a names array before quoting each identifier; fido-mds3._validateChain walks an x5c cert chain; require-methods.create validates an HTTP-verb allowlist; network-dns.useDesignatedResolvers validates a resolver-IP list. Each throws a primitive-local typed error; the shingle is the array-walk-then-throw idiom, not behaviour. Same family as the non-empty-array opt-validation cluster below.",
2876
+ },
2877
+ {
2878
+ mode: "family-subset",
2879
+ files: [
2880
+ "lib/audit-sign.js:init",
2881
+ "lib/framework-schema.js:ensureSchema",
2882
+ "lib/vault/index.js:init",
2883
+ ],
2884
+ reason: "v0.14.29 — schema/keystore bootstrap prelude (open-or-create the backing table(s), run idempotent CREATE-IF-NOT-EXISTS DDL, then read the current tip / key row). framework-schema.ensureSchema gained the quote-by-construction DDL emit (the Postgres identifier-casing fix), tipping its setup prelude past the shingle threshold against audit-sign.init (audit hash-chain table bootstrap) and vault.init (sealed-keystore bootstrap). Each bootstraps a different durable store with a primitive-local error class; the shared shape is the create-then-read setup idiom, not behaviour.",
2885
+ },
2886
+ {
2887
+ mode: "family-subset",
2888
+ files: [
2889
+ "lib/external-db.js:_skipOpaqueSpan",
2890
+ "lib/external-db.js:_cteMainKeyword",
2891
+ "lib/external-db.js:_explainResolve",
2892
+ "lib/external-db.js:_copyLoadsRows",
2893
+ "lib/external-db.js:_emitMetric",
2894
+ ],
2895
+ reason: "v0.14.24, expanded v0.15.0 — the residency-gate write-classifier in external-db.js walks SQL statements char-by-char to resolve a statement's effective verb (WITH/EXPLAIN-prefix resolution, dollar-quoted bodies $tag$...$tag$, bracket identifiers, doubled-quote re-entry, -- / /* */ comments). Its five span-walking helpers (_skipOpaqueSpan / _cteMainKeyword / _explainResolve / _copyLoadsRows / _emitMetric) share the opaque-span / quoted-token skip-loop shape, so the dup detector groups them. They already compose the single _skipOpaqueSpan primitive; the residual match across the four classifier helpers is the same skip-loop applied at different resolution stages (CTE main keyword, EXPLAIN unwrap, COPY row-load detection, metric emit), each with stage-specific keyword tables. No further extractable behaviour — splitting them would not remove the shared loop shape.",
2896
+ },
2562
2897
  {
2563
2898
  mode: "family-subset",
2564
2899
  files: [
@@ -2603,6 +2938,8 @@ async function testNoDuplicateCodeBlocks() {
2603
2938
  "lib/archive-adapters.js:close",
2604
2939
  "lib/crypto-field.js:declarePerRowKey",
2605
2940
  "lib/crypto-field.js:assertColumnResidency",
2941
+ "lib/crypto-field.js:declarePerRowResidency",
2942
+ "lib/crypto-field.js:getPerRowResidency",
2606
2943
  "lib/network-smtp-policy.js:mtaStsFetch",
2607
2944
  "lib/parsers/safe-env.js:readVar",
2608
2945
  "lib/mail-crypto-pgp.js:sign",
@@ -2612,8 +2949,14 @@ async function testNoDuplicateCodeBlocks() {
2612
2949
  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
2950
  },
2614
2951
  {
2615
- files: ["lib/api-key.js:issue", "lib/db-query.js:<top>", "lib/session.js:create"],
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.",
2952
+ mode: "family-subset",
2953
+ files: [
2954
+ "lib/api-key.js:issue",
2955
+ "lib/db-query.js:<top>",
2956
+ "lib/db-query.js:_assertLocalResidency",
2957
+ "lib/session.js:create",
2958
+ ],
2959
+ 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; db-query._assertLocalResidency joins via its allowedTags.join diagnostics + safeEmit metadata-object assembly, unrelated to api-key issuance / session creation beyond the walk-and-format shell.",
2617
2960
  },
2618
2961
  {
2619
2962
  mode: "family-subset",
@@ -3174,6 +3517,8 @@ async function testNoDuplicateCodeBlocks() {
3174
3517
  "lib/auth/jwt.js:verify",
3175
3518
  "lib/auth/jwt-external.js:verifyExternal",
3176
3519
  "lib/auth/jwt-external.js:_fetchJwks",
3520
+ "lib/auth/jwt-external.js:_signCompactJws",
3521
+ "lib/mail-auth.js:inboundVerify",
3177
3522
  "lib/auth/oauth.js:verifyBackchannelLogoutToken",
3178
3523
  "lib/auth/oauth.js:verifyIdToken",
3179
3524
  "lib/auth/oauth.js:exchangeToken",
@@ -3214,7 +3559,7 @@ async function testNoDuplicateCodeBlocks() {
3214
3559
  "lib/self-update.js:poll",
3215
3560
  "lib/self-update.js:verify",
3216
3561
  ],
3217
- 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.",
3562
+ 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. mail-auth.js:inboundVerify shares only the validateOpts prelude + per-step result-shape assertions while composing spf.verify / dkim.verify / dmarc.evaluate — a mail-domain pipeline whose verdict vocabulary (RFC 7489/8601 result enums) has nothing to consolidate with the JOSE error namespaces. Consolidating would lose per-spec error code namespacing.",
3218
3563
  },
3219
3564
  {
3220
3565
  mode: "family-subset",
@@ -3510,6 +3855,16 @@ async function testNoDuplicateCodeBlocks() {
3510
3855
  ],
3511
3856
  reason: "Same shared SMTP listener create() shingle as the entry above; observability-otlp-exporter create() coincidentally shares the bind + listen + connection-tracker pattern (it accepts inbound OTLP spans on a TCP socket). All three carry distinct domain-specific opts validation + protocol logic.",
3512
3857
  },
3858
+ {
3859
+ mode: "family-subset",
3860
+ files: [
3861
+ "lib/mail-server-imap.js:create",
3862
+ "lib/mail-server-pop3.js:create",
3863
+ "lib/observability-otlp-exporter.js:create",
3864
+ "lib/outbox.js:create",
3865
+ ],
3866
+ reason: "Domain-specific create() opts-validation boilerplate — each runs a sequence of validateOpts.optionalPositiveFinite(opts.X, ...) presence/type checks followed by `var X = opts.X || DEFAULT_X` default assignment. b.outbox.create joined this 50-token shingle when the in-flight reaper added the claimReclaimMs lease opt, lengthening its validation block to the threshold. The four create()s validate entirely different option sets (IMAP / POP3 listener bind opts, OTLP exporter socket opts, outbox poll / batch / backoff / lease opts) with different defaults and semantics; the shared shape IS the validateOpts call sequence, which is already the extracted primitive — the per-domain opt list can't be factored further without a domain-specific wrapper per call site. Shape-only, not an extractable dup.",
3867
+ },
3513
3868
  {
3514
3869
  mode: "family-subset",
3515
3870
  files: [
@@ -4118,6 +4473,10 @@ async function testNoDuplicateCodeBlocks() {
4118
4473
  "lib/data-act.js:shareWithThirdParty",
4119
4474
  "lib/data-act.js:recordSwitchRequest",
4120
4475
  "lib/db.js:declareRequireDualControl",
4476
+ // v0.14.24 — per-row residency declaration shares the same
4477
+ // validateOpts.requireNonEmptyString cascade + registry-write
4478
+ // + return-copy scaffold.
4479
+ "lib/crypto-field.js:declarePerRowResidency",
4121
4480
  // v0.8.77 — OAuth resource-server / SCIM / protected-resource-metadata
4122
4481
  // additions share the standard primitive scaffolding
4123
4482
  "lib/auth/oauth.js:pollDeviceCode",
@@ -4524,6 +4883,16 @@ async function testNoDuplicateCodeBlocks() {
4524
4883
  "lib/cache-status.js:_parseParamValue",
4525
4884
  "lib/client-hints.js:acceptList",
4526
4885
  "lib/daemon.js:_safeAuditEmit",
4886
+ // v0.15.0 SQL sweep — the data-layer's frozen SQL-keyword tables
4887
+ // (`Object.freeze({ SELECT: true, INSERT: true, ... })`) and the
4888
+ // residency write-classifier's verb-table walk share the same
4889
+ // Object.freeze token-table declaration cadence as the command
4890
+ // guards' verb tables, but over the SQL grammar. external-db's
4891
+ // _cteMainKeyword centroids on its _CTE_MAIN_VERBS table; sql.js's
4892
+ // dropPolicy centroids on the adjacent CATALOG_PRAGMA_VERBS table;
4893
+ // guard-sql's <top> centroids on STATEMENT_VERBS / LEADING_VERB_
4894
+ // FLOOR / MIGRATION_DDL_VERBS. Different tokens, different grammar.
4895
+ "lib/external-db.js:_cteMainKeyword",
4527
4896
  "lib/guard-dsn.js:<top>",
4528
4897
  "lib/guard-dsn.js:_resolveProfile",
4529
4898
  "lib/guard-envelope.js:check",
@@ -4543,6 +4912,7 @@ async function testNoDuplicateCodeBlocks() {
4543
4912
  "lib/guard-mail-move.js:<top>",
4544
4913
  "lib/guard-mail-move.js:_checkFolderName",
4545
4914
  "lib/guard-mail-query.js:<top>",
4915
+ "lib/guard-mail-reply.js:<top>",
4546
4916
  "lib/guard-mail-sieve.js:<top>",
4547
4917
  "lib/guard-managesieve-command.js:<top>",
4548
4918
  "lib/guard-managesieve-command.js:validate",
@@ -4555,6 +4925,7 @@ async function testNoDuplicateCodeBlocks() {
4555
4925
  "lib/guard-smtp-command.js:_parseAuthCommandSyntax",
4556
4926
  "lib/guard-smtp-command.js:_resolveProfile",
4557
4927
  "lib/guard-smtp-command.js:validate",
4928
+ "lib/guard-sql.js:<top>",
4558
4929
  "lib/guard-stream-args.js:<top>",
4559
4930
  "lib/keychain.js:_drain",
4560
4931
  "lib/mail-dav.js:_emit",
@@ -4610,11 +4981,12 @@ async function testNoDuplicateCodeBlocks() {
4610
4981
  "lib/safe-vcard.js:_parseContentLine",
4611
4982
  "lib/safe-vcard.js:_stripDoubleQuotes",
4612
4983
  "lib/sandbox.js:_validateAllowed",
4984
+ "lib/sql.js:dropPolicy",
4613
4985
  "lib/self-update.js:<top>",
4614
4986
  "lib/self-update.js:_safeAuditEmit",
4615
4987
  "lib/watcher.js:_compileIgnore",
4616
4988
  ],
4617
- reason: "v0.9.58 mail-stack bundle (multi-agent parallel ship: ManageSieve + ICAP + PGP/SMIME + DAV) — every new lib/ file written by the 4 sub-agents joins one or more existing family-subset clusters (guard-* validate / <top> banner shapes; mail-server-* listener scaffolds; safe-* line-folded parsers; emit-audit wrappers; resolveProfile dispatchers). Each underlying domain stays distinct (different RFCs, different wire grammars); the shared shingle is the framework's family-contract scaffolding (`b.gateContract` / listener template / safeBuffer.boundedChunkCollector / lazyRequire-audit / drop-silent emit). Consolidation into a single base module would couple unrelated wire-protocol grammars under one abstraction. Documented as one cluster rather than 49 individual family-subset entries because each cluster fingerprint is a subset of this union.",
4989
+ reason: "v0.9.58 mail-stack bundle (multi-agent parallel ship: ManageSieve + ICAP + PGP/SMIME + DAV), expanded v0.15.0 SQL sweep — every new lib/ file written by the sub-agents joins one or more existing family-subset clusters (guard-* validate / <top> banner shapes; mail-server-* listener scaffolds; safe-* line-folded parsers; emit-audit wrappers; resolveProfile dispatchers). Each underlying domain stays distinct (different RFCs, different wire grammars); the shared shingle is the framework's family-contract scaffolding (`b.gateContract` / listener template / safeBuffer.boundedChunkCollector / lazyRequire-audit / drop-silent emit). Consolidation into a single base module would couple unrelated wire-protocol grammars under one abstraction. Documented as one cluster rather than 49 individual family-subset entries because each cluster fingerprint is a subset of this union. The v0.15.0 data-layer additions (guard-sql <top>, external-db _cteMainKeyword, sql.js dropPolicy) match on the module-level `Object.freeze({ KEY: true, ... })` token-table declaration cadence the command guards also use, but over the SQL grammar — guard-sql's STATEMENT_VERBS/LEADING_VERB_FLOOR/MIGRATION_DDL_VERBS (SELECT/INSERT/CREATE/ALTER/DROP) and external-db's _CTE_MAIN_VERBS (SELECT/VALUES/MERGE/UPSERT/REPLACE) and sql.js's CATALOG_PRAGMA_VERBS (table_info/journal_mode/wal_checkpoint) carry SQL-keyword tokens, where POP3 carries USER/PASS/RETR, IMAP carries CAPABILITY/SELECT/FETCH, and safe-ical/vcard carry RFC 5545/6350 property names — different keys, different grammars, no shared values. The single genuinely-shared declaration (`var COMPLIANCE_POSTURES = gateContract.ALL_STRICT_POSTURES`) is already extracted-by-reference and guarded by the inline-all-strict-postures-map inverse detector; guard-sql keeps its own overlay posture map (PROFILES.strict + gdprRedact) which is not the strict-all literal.",
4618
4990
  },
4619
4991
  {
4620
4992
  files: [
@@ -4883,13 +5255,21 @@ async function testNoDuplicateCodeBlocks() {
4883
5255
  "lib/guard-auth.js:gate",
4884
5256
  "lib/guard-auth.js:sanitize",
4885
5257
  "lib/guard-auth.js:validate",
5258
+ "lib/guard-sql.js:gate",
5259
+ "lib/guard-sql.js:sanitize",
5260
+ "lib/guard-sql.js:validate",
5261
+ "lib/guard-sql.js:compliancePosture",
5262
+ "lib/guard-sql.js:_truncateForAudit",
5263
+ "lib/guard-sql.js:_firstRefusal",
4886
5264
  "lib/guard-smtp-command.js:<top>",
4887
5265
  "lib/guard-smtp-command.js:gate",
4888
5266
  "lib/guard-smtp-command.js:validate",
4889
5267
  "lib/guard-envelope.js:<top>",
4890
5268
  "lib/guard-envelope.js:check",
5269
+ "lib/gate-contract.js:defaultGate",
5270
+ "lib/gate-contract.js:_ctxValueForKind",
4891
5271
  ],
4892
- reason: "guard-* family ABI — every member's gate() factory header (function gate(opts) { opts = _resolveOpts(opts); return gateContract.buildGuardGate(...); }), bottom-of-file helper triplet (buildProfile = gateContract.makeProfileBuilder(PROFILES); function compliancePosture(name) { return gateContract.lookupCompliancePosture(...); }; var _xRulePacks = gateContract.makeRulePackLoader(...); var loadRulePack = _xRulePacks.load), and PROFILES literal block all share the family-shared vocabulary by design. The keys ARE the family contract; the values diverge per guard (csv handles operatorRules + sanitize re-emit; html has sanitize-eligibility branching; svg refuses SVGZ; filename operates on strings; archive on entries; json on parsed trees + source scan). Further extraction would either pull body decision logic that's genuinely per-guard into a shared place, or extract a one-line factory that hides the family contract from anyone reading the guard source.",
5272
+ reason: "guard-* family ABI — every member's gate() factory header (function gate(opts) { opts = _resolveOpts(opts); return gateContract.buildGuardGate(...); }), bottom-of-file helper triplet (buildProfile = gateContract.makeProfileBuilder(PROFILES); function compliancePosture(name) { return gateContract.lookupCompliancePosture(...); }; var _xRulePacks = gateContract.makeRulePackLoader(...); var loadRulePack = _xRulePacks.load), and PROFILES literal block all share the family-shared vocabulary by design. The keys ARE the family contract; the values diverge per guard (csv handles operatorRules + sanitize re-emit; html has sanitize-eligibility branching; svg refuses SVGZ; filename operates on strings; archive on entries; json on parsed trees + source scan). gateContract.defineGuard + its defaultGate / _ctxValueForKind ARE the canonical extraction of that header + triplet + serve->audit-only->refuse decision shape: the four kinds (content/filename/identifier/command) that ship a guard onto the factory delegate their wiring to it, while guards with a bespoke gate (csv/filename/jwt) pass their own gate body and the rest converge on defaultGate as they migrate. The remaining per-guard gate bodies still carry the literal shape until the family fan-out lands; consolidating them eagerly would either pull body decision logic that's genuinely per-guard into a shared place, or hide the family contract from anyone reading the guard source.",
4893
5273
  },
4894
5274
  {
4895
5275
  // v0.9.37 — guard-dsn / guard-mail-move / guard-smtp-command
@@ -4918,6 +5298,35 @@ async function testNoDuplicateCodeBlocks() {
4918
5298
  ],
4919
5299
  reason: "Three independently-domain'd entry points share an array-walk + per-item validation cascade. Each emits a domain-distinct error class (SdJwtVcIssuerError / GuardSagaConfigError / ComposePipelineError) and validates a different field tuple. Consolidating would couple unrelated specs.",
4920
5300
  },
5301
+ {
5302
+ // v0.15.0 — three unrelated functions share only a
5303
+ // walk-the-string / loop-over-a-small-list token shell.
5304
+ // gateContract._pascalCase rewrites a guard's short name to its
5305
+ // PascalCase audit prefix; oid4vp.matchDcql walks a DCQL
5306
+ // credential-query claim set; http-message-signature._parseUrl
5307
+ // walks URL components. Shape-only. (The factory consolidation
5308
+ // moved the matching 50-tok window off _ctxValueForKind onto
5309
+ // _pascalCase — same shape-only family, different gate-contract fn.)
5310
+ mode: "family-subset",
5311
+ files: [
5312
+ "lib/gate-contract.js:_pascalCase",
5313
+ "lib/auth/oid4vp.js:matchDcql",
5314
+ "lib/http-message-signature.js:_parseUrl",
5315
+ ],
5316
+ reason: "coincidental 50-tok window across unrelated domains; no shared behaviour. gateContract._pascalCase is a regex-driven name caser (\"smtp-command\" -> \"SmtpCommand\") that builds the default gate's audit/metric prefix; oid4vp.matchDcql evaluates a DCQL credential-query against a presented credential set; http-message-signature._parseUrl decomposes a request URL into the @authority / @path / @query derived-component pieces. Three different domains, three different return types — nothing extractable beyond the trivial walk shell.",
5317
+ },
5318
+ {
5319
+ // v0.15.0 — coincidental cross-domain 50-tok window: a regex
5320
+ // pascal-caser, a create-opts validator, and a UUID generator
5321
+ // happen to share the local-var / return-shape token sequence.
5322
+ mode: "family-subset",
5323
+ files: [
5324
+ "lib/api-key.js:_validateCreateOpts",
5325
+ "lib/cloud-events.js:_genId",
5326
+ "lib/gate-contract.js:_pascalCase",
5327
+ ],
5328
+ reason: "coincidental 50-tok window across unrelated domains (pascal-case helper / create-opts validator / UUID gen); no shared behaviour. api-key._validateCreateOpts type-checks the create() opts cascade; cloud-events._genId mints a random CloudEvents id; gateContract._pascalCase rewrites a guard short-name to its PascalCase audit prefix. Nothing extractable beyond the shared token shell.",
5329
+ },
4921
5330
  {
4922
5331
  // v0.9.40 — RFC 5322 header-injection control-char scans
4923
5332
  // (boolean variant: does this string contain CR/LF/NUL/C0/DEL?)
@@ -5527,13 +5936,15 @@ async function testNoDuplicateCodeBlocks() {
5527
5936
  reason: "_safeGlobalObs drop-silent observability helper — each primitive defines a local `function _safeGlobalObs(action, attrs) { try { observability.event({...}); } catch (_e) { /* drop-silent */ } }` because the global observability binding is module-load-time captured. Three auth-related primitives; the closure captures the per-primitive event-name namespace. Same observability-sink discipline noted in the cookies/gpc/headers _emitAudit cluster.",
5528
5937
  },
5529
5938
  {
5939
+ mode: "family-subset",
5530
5940
  files: [
5531
5941
  "lib/db-query.js:<top>",
5942
+ "lib/db-query.js:_assertLocalResidency",
5532
5943
  "lib/db.js:init",
5533
5944
  "lib/db.js:stream",
5534
5945
  "lib/external-db.js:_connectAs",
5535
5946
  ],
5536
- reason: "node:sqlite + external-db wiring scaffold — `var statement = database.prepare('...'); var rows = statement.all(...); for (i in rows) { ... }`. db-query top-level statement-cache setup, db.init schema-bootstrap walk, db.stream readable-walk, external-db.js role connect-as walk. Four sites within the db / external-db domain; the SQL bodies and result shapes differ per call.",
5947
+ reason: "node:sqlite + external-db wiring scaffold — `var statement = database.prepare('...'); var rows = statement.all(...); for (i in rows) { ... }` plus the posture/region lookup-then-branch shell. db-query top-level statement-cache setup, db.init schema-bootstrap walk, db.stream readable-walk, external-db.js role connect-as walk, db-query._assertLocalResidency region-set assembly. Sites within the db / external-db domain; the SQL bodies, result shapes, and refusal semantics differ per call.",
5537
5948
  },
5538
5949
  {
5539
5950
  files: [
@@ -6156,6 +6567,24 @@ function testStateStampScanningDeferred() {
6156
6567
  // 4. The catalog scans whole-file content (multiline regex) so
6157
6568
  // patterns split across lines still match.
6158
6569
  var KNOWN_ANTIPATTERNS = [
6570
+ {
6571
+ id: "use-makeProfileResolver-not-handrolled",
6572
+ primitive: "b.gateContract.makeProfileResolver",
6573
+ scanScope: "lib",
6574
+ skipCommentLines: true,
6575
+ regex: /function\s+_resolveProfile\s*\(/,
6576
+ allowlist: ["lib/safe-sieve.js"],
6577
+ reason: "v0.15.0 #103 — profile resolution (posture->profile map, profile||default, validate-or-throw on unknown) is owned by gateContract.makeProfileResolver; 24 guards reuse it. A hand-rolled `function _resolveProfile(opts)` re-implements the solved primitive and drifts downstream — this exact dup was previously ALLOWLISTED in KNOWN_CLUSTERS before extraction (feedback_codebase_patterns_is_a_drift_signal). lib/safe-sieve.js is the one genuine holdout (reads the public opts.compliancePosture not opts.posture, returns opts.profile unvalidated, never throws) pending its contract decision in task #104. Any other lib file declaring _resolveProfile must call gateContract.makeProfileResolver instead.",
6578
+ },
6579
+ {
6580
+ id: "use-throwOnRefusalSeverity-not-handrolled",
6581
+ primitive: "b.gateContract.throwOnRefusalSeverity",
6582
+ scanScope: "lib",
6583
+ skipCommentLines: true,
6584
+ regex: /ruleId\s*\|\|\s*['"][a-zA-Z0-9_-]+\.refused['"]/,
6585
+ allowlist: ["lib/guard-auth.js"],
6586
+ reason: "v0.15.0 #103 — the guard sanitize/parse refuse-on-critical|high throw (err(issue.ruleId || '<x>.refused', 'guard<Name>.<op>: ' + issue.snippet)) is owned by gateContract.throwOnRefusalSeverity; 18 guards reuse it (this was the failing STRONG-DUP fp:f349a8d1f51b before extraction). A hand-rolled `issues[i].ruleId || '<x>.refused'` throw re-implements it. lib/guard-auth.js is the one genuine holdout (its message embeds issues[i].source: 'guardAuth.sanitize [<source>]:') pending task #104; the primitive itself uses a `fallback` variable (no .refused literal) so it does not match. Any other lib file with this shape must call gateContract.throwOnRefusalSeverity (the severities / op options cover the critical-only + parse variants).",
6587
+ },
6159
6588
  {
6160
6589
  // A hard quota / rate / budget ceiling must be enforced with an
6161
6590
  // atomic conditional reserve — the limit test and the charge are
@@ -6174,6 +6603,523 @@ var KNOWN_ANTIPATTERNS = [
6174
6603
  allowlist: [],
6175
6604
  reason: "Hard quota / rate / budget ceilings must be enforced with an atomic conditional reserve (the limit test and the charge are one indivisible operation), never charge-then-refund (an unconditional increment plus a compensating `decrBy`). The refund shape transiently over-counts a shared counter and falsely denies concurrent calls that should fit (Codex P1 on PR #178, v0.12.27 — b.ai.quota originally shipped this shape and was reworked to store.reserve). A future store that genuinely needs a decrement for a non-ceiling gauge metric allowlists with a structural reason explaining why no limit decision reads the counter mid-refund.",
6176
6605
  },
6606
+ {
6607
+ // The cross-border residency WRITE gate must classify writes by what
6608
+ // a statement DOES, not by its leading keyword. A statement whose
6609
+ // effective verb is hidden behind a prefix — `WITH ... INSERT`,
6610
+ // `EXPLAIN ANALYZE INSERT` (Postgres EXECUTES the wrapped write),
6611
+ // `CALL` / `EXECUTE` / `DO`, `COPY ... FROM`, `REPLACE` — reads as a
6612
+ // harmless leading keyword and slips past a gate that enforces only
6613
+ // on `class === "DML"`. lib/external-db.js resolves WITH / EXPLAIN
6614
+ // prefixes to the effective verb in _classifyStatement and gates via
6615
+ // a positive pure-read exempt set (_RESIDENCY_READ_CLASS), treating
6616
+ // everything else — DML, ROUTINE, a COPY load, an unresolved or
6617
+ // unmapped statement — as a write that requires a residency tag
6618
+ // (Codex P1 on PR #304 flagged the WITH-wrapped-DML instance; the
6619
+ // COPY / EXPLAIN-ANALYZE / CALL / REPLACE / DO siblings were
6620
+ // confirmed in the same review). Comparing the statement class to
6621
+ // the single string "DML" reintroduces the bypass.
6622
+ id: "residency-gate-dml-equality",
6623
+ primitive: "gate SQL writes by a positive pure-read exempt set that resolves WITH/EXPLAIN prefixes and fails closed on unknown (lib/external-db.js _RESIDENCY_READ_CLASS + _classifyStatement); never discriminate writes by leading-keyword equality to a single class string like \"DML\"",
6624
+ regex: /[!=]==\s*["']DML["']/,
6625
+ allowlist: [],
6626
+ reason: "The cross-border residency write gate must enforce on what a statement DOES, not its leading keyword. `WITH ... INSERT`, `EXPLAIN ANALYZE INSERT` (Postgres EXECUTES the wrapped write), `CALL` / `EXECUTE` / `DO`, `COPY ... FROM`, and `REPLACE` all place rows while reading as a harmless prefix, so a gate enforcing only on `class === \"DML\"` waves them across a border untagged (Codex P1 on PR #304 flagged the WITH instance; the verifier confirmed the COPY / EXPLAIN-ANALYZE / CALL / REPLACE / DO siblings). lib/external-db.js resolves WITH / EXPLAIN to the effective verb in _classifyStatement and gates via the positive _RESIDENCY_READ_CLASS exempt set, treating every non-read (DML, ROUTINE, a COPY load, an unmapped or unresolved statement) as a write that needs a tag. A forensic-only comparison that does not gate a write may allowlist with a structural reason naming why no transfer decision rides on it.",
6627
+ },
6628
+ {
6629
+ // A per-row / per-record crypto-shred key (K_row) — or a keyed-MAC
6630
+ // that advertises vault-secret protection — must seed off a CSPRNG
6631
+ // secret (b.crypto.generateBytes) or the SEALED-at-rest
6632
+ // b.vault.getDerivedHashMacKey(), NEVER off kdf() over the
6633
+ // PLAINTEXT-on-disk b.vault.getDerivedHashSalt(). A key whose entire
6634
+ // input is recomputable from the data directory is re-derivable by a
6635
+ // disk-access attacker, so destroying the wrapped form shreds nothing
6636
+ // and a keyed-MAC over a low-entropy preimage is brute-forceable
6637
+ // offline — defeating the exact secrecy/erasure the primitive
6638
+ // advertises (v0.14.25: the per-row-key K_row and the idempotency
6639
+ // fingerprint HMAC both shipped this shape and were reseeded).
6640
+ // The salted-sha3 derived-hash INDEX (crypto-field.js:325) uses the
6641
+ // plaintext salt via sha3Hash() — a deterministic equality index that
6642
+ // disclaims MAC-grade secrecy, a DIFFERENT shape (not kdf) — so it
6643
+ // does not match and is covered by its own detector.
6644
+ id: "kdf-key-from-plaintext-derived-hash-salt",
6645
+ primitive: "seed per-row crypto-shred keys / vault-secret keyed-MACs from a CSPRNG secret (b.crypto.generateBytes) or the sealed b.vault.getDerivedHashMacKey(); never kdf() over the plaintext-on-disk b.vault.getDerivedHashSalt()",
6646
+ regex: /\bkdf\s*\([^\n]*getDerivedHashSalt\s*\(/,
6647
+ allowlist: [],
6648
+ reason: "v0.14.25 — the per-row-key K_row was kdf(...getDerivedHashSalt()...) over the PLAINTEXT-on-disk salt, so a disk-access attacker re-derived it and destroyPerRowKey/eraseHard shred NOTHING (advertised as crypto-shred since v0.7.27, false); the idempotency fingerprint HMAC seeded off the same plaintext salt despite promising the vault root was the trust root. Both reseeded — K_row onto a fresh b.crypto.generateBytes(32) row-secret AAD-wrapped via b.vault.aad.seal, the fingerprint HMAC onto the sealed b.vault.getDerivedHashMacKey() (since v0.14.7). This detector refuses the inline regression `kdf(...getDerivedHashSalt()...)`; the legitimate `kdf(getDerivedHashMacKey()...)` (the sealed key) and the `sha3Hash(getDerivedHashSalt()...)` deterministic equality index are different shapes and do not match. NOTE the historical bug assigned the salt to a var first (`saltHex = getDerivedHashSalt()...; kdf(...saltHex...)`) — a data-flow shape regex can't trace, so reviewers must also reject any kdf/HMAC key whose IKM transitively names getDerivedHashSalt.",
6649
+ },
6650
+ {
6651
+ // A break-glass grant pin (pinIp / sessionPin — both documented
6652
+ // default-ON) binds redemption to the IP / session captured when the
6653
+ // grant was minted. The enforcement MUST fail closed when the
6654
+ // captured binding is absent: a grant that recorded no IP (or no
6655
+ // session) is refused, never waved through. The historical fail-open
6656
+ // was a `grantRow.ip != null &&` short-circuit around the comparison
6657
+ // — "no binding recorded, so there is nothing to check, so allow" —
6658
+ // which lets a grant minted without a binding be redeemed from any
6659
+ // origin, defeating the pin exactly when it is the only control left.
6660
+ // The fixed shape is `if (grantRow.ip == null) throw ...` BEFORE the
6661
+ // `redeemIp !== grantRow.ip` comparison (see _enforceGrantPins). The
6662
+ // `grantRow.` receiver is unique to lib/break-glass.js, so this
6663
+ // bad-shape regex needs no companion; skipCommentLines keeps the
6664
+ // narrative comment that quotes the bad form from self-matching.
6665
+ id: "break-glass-pin-fails-open-on-null-binding",
6666
+ primitive: "fail closed in break-glass pin enforcement — refuse a grant whose pinIp/sessionPin binding was never captured (if grantRow.ip == null throw); never short-circuit the comparison with a `grantRow.ip != null &&` guard that treats an absent binding as 'nothing to check, allow'",
6667
+ regex: /grantRow\.(?:ip|sessionId)\s*!=\s*null\s*&&/,
6668
+ skipCommentLines: true,
6669
+ allowlist: [],
6670
+ reason: "pinIp / sessionPin are documented default-ON; redemption binds to the IP / session captured at mint time. A `grantRow.ip != null &&` (or sessionId) guard around the pin comparison fails OPEN: a grant minted without a captured binding skips the check and is redeemable from any origin — the pin's whole point is lost in the one case it must hold. Enforce by refusing the unbound grant first (`if (grantRow.ip == null) throw`), then comparing. Resolve the redeeming client IP from the redemption request (falling back to req.ip), not from a value the redeemer can omit.",
6671
+ },
6672
+ {
6673
+ // The b.dsr database-backed ticket store holds the data subject's
6674
+ // identifiers and the raw request payload — PII under GDPR Art. 15 /
6675
+ // 17 that an erasure request must be able to destroy. Those columns
6676
+ // MUST be sealed via cryptoField.registerTable(DSR_SEAL_TABLE, { aad:
6677
+ // true, ... }) so the row's plaintext is encrypted at rest and goes
6678
+ // with the shredded row key; storing them plaintext leaves
6679
+ // un-erasable PII and defeats b.subject.eraseHard for DSR tickets.
6680
+ // DSR_SEAL_TABLE is unique to lib/dsr.js; the companion `requires`
6681
+ // exempts the file once the registerTable call is present (the fix),
6682
+ // so this fires only if a future edit drops the registration while
6683
+ // keeping the table.
6684
+ id: "dsr-ticket-store-pii-must-be-sealed",
6685
+ primitive: "seal the b.dsr database ticket store's subject identifiers + request payload via cryptoField.registerTable(DSR_SEAL_TABLE, { aad: true, columns: [...] }); plaintext PII in the ticket store is un-erasable and defeats DSR erasure",
6686
+ regex: /\bDSR_SEAL_TABLE\b/,
6687
+ requires: /registerTable\s*\(\s*DSR_SEAL_TABLE/,
6688
+ allowlist: [],
6689
+ reason: "The DSR dbTicketStore persists the data subject's identifiers and the raw request body — the exact PII an Art. 17 erasure must destroy. Those columns must be sealed via cryptoField.registerTable(DSR_SEAL_TABLE, { aad: true }) so they are encrypted at rest under a per-row key bound to (table, rowId) and shredded with the row; leaving them plaintext means an erasure request cannot delete the data it is processing. The companion registerTable(DSR_SEAL_TABLE call satisfies the discipline; this entry fires only if the registration is removed while the table remains.",
6690
+ },
6691
+ // #114 — legal-hold + subject-restriction local tables seal their PII columns.
6692
+ {
6693
+ id: "legal-hold-store-pii-must-be-sealed",
6694
+ primitive: "seal the b.legalHold _blamejs_legal_hold PII columns (reason/placedBy/custodian/citation) via cryptoField.sealRow(HOLD_TABLE, ...) on insert + unseal on read — the legal-basis / custodian / citation free text links a data subject to a legal matter and must not be stored plaintext",
6695
+ regex: /sql\.insert\(HOLD_TABLE/,
6696
+ requires: /\bsealRow\(HOLD_TABLE/,
6697
+ skipCommentLines: true,
6698
+ allowlist: [],
6699
+ reason: "#114 — _blamejs_legal_hold stored legal-basis / custodian / ticket-citation free text in clear via the raw sql.insert + db.prepare().run() path, which bypasses the structured builder's auto-seal. db.js declares sealedFields on the table; legal-hold.js must seal on write (cryptoField.sealRow(HOLD_TABLE, ...)) and unseal on read (get/list/release). Fires if an insert into HOLD_TABLE lands without the seal.",
6700
+ },
6701
+ {
6702
+ id: "subject-restriction-store-pii-must-be-sealed",
6703
+ primitive: "seal the b.subject restriction reason (a PII ticket reference) via cryptoField.sealRow(RESTRICTIONS_TABLE, ...) on insert into _blamejs_subject_restrictions",
6704
+ regex: /sql\.insert\(RESTRICTIONS_TABLE/,
6705
+ requires: /\bsealRow\(RESTRICTIONS_TABLE/,
6706
+ skipCommentLines: true,
6707
+ allowlist: [],
6708
+ reason: "#114 — _blamejs_subject_restrictions declares sealedFields:[\"reason\"] but subject.js wrote the reason in clear via the raw sql.insert path. Seal on write (cryptoField.sealRow(RESTRICTIONS_TABLE, ...)); the reason is write-only (isRestricted reads only the PK) so there is no unseal site. Fires if the restriction insert lands without the seal.",
6709
+ },
6710
+ {
6711
+ // Vault keypair rotation stages every output file (the re-encrypted
6712
+ // db, resealed vault/db keys, additional sealed files, derived-hash
6713
+ // material, and the transient PLAINTEXT db) inside opts.stagingDir.
6714
+ // Those writes must go through _writeStagedFileExclusive — O_CREAT |
6715
+ // O_EXCL | O_NOFOLLOW, owner-only 0o600 — so a same-user pre-planted
6716
+ // file or symlink swap in the staging dir is a hard failure rather
6717
+ // than a followed write (CWE-377 / CWE-379 / CWE-59). A raw
6718
+ // nodeFs.writeFileSync into the staging dir (or to the tmpDbPath /
6719
+ // verifyTmp markers) follows whatever is already at the path. The
6720
+ // identifiers tmpDbPath / verifyTmp / stagingDir are unique to
6721
+ // lib/vault/rotate.js, so this bad-shape regex self-scopes there and
6722
+ // needs no companion; the exclusive helper's own write targets an fd,
6723
+ // not these names, so it does not match.
6724
+ id: "vault-rotate-staged-write-not-exclusive",
6725
+ primitive: "write vault-rotation staging files via lib/vault/rotate.js _writeStagedFileExclusive (O_CREAT|O_EXCL|O_NOFOLLOW, 0o600); never raw nodeFs.writeFileSync into opts.stagingDir or the tmpDbPath/verifyTmp markers — a non-exclusive create follows a pre-planted file/symlink in the staging dir (CWE-377/379/59)",
6726
+ regex: /writeFileSync\s*\(\s*(?:tmpDbPath\b|verifyTmp\b|nodePath\.join\(\s*stagingDir)/,
6727
+ allowlist: [],
6728
+ reason: "vault rotation re-encrypts the database and reseals keys through framework-named files in opts.stagingDir, including a transient PLAINTEXT copy of the whole database. A raw nodeFs.writeFileSync to those paths follows a pre-planted regular file or symlink (CWE-59) and inherits a umask-wide mode (CWE-377/379). Every staged write must go through _writeStagedFileExclusive, which unlinks any stale entry then creates with O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW at 0o600 and fsyncs; the exclusive create turns a pre-plant into a hard error and O_NOFOLLOW refuses a symlinked target. This detector encodes the CodeQL js/insecure-temporary-file finding fixed in v0.14.26 so the raw-write shape cannot return to the rotation path.",
6729
+ },
6730
+ {
6731
+ // The local queue seals job rows via cryptoField.sealRow(SEAL_TABLE,
6732
+ // ...), but cryptoField.sealRow silently passes the row through as
6733
+ // PLAINTEXT for a table that was never registerTable'd. The queue
6734
+ // therefore MUST self-register its seal table on init via
6735
+ // _ensureSealTable (an idempotent getSchema-probe + registerTable),
6736
+ // or a queue node that never ran db.init writes job payloads to the
6737
+ // backend in cleartext (fail-open). SEAL_TABLE and _ensureSealTable
6738
+ // are queue-local identifiers, so the bad-shape regex self-scopes;
6739
+ // the companion `requires` (the helper's presence) is the fix marker.
6740
+ id: "queue-seal-table-not-self-registered",
6741
+ primitive: "the local queue must self-register its seal table on init via _ensureSealTable (cryptoField.registerTable(SEAL_TABLE)) so job payloads seal at rest from the first write; cryptoField.sealRow/unsealRow is a silent no-op against an unregistered table, leaving jobs in plaintext (fail-open)",
6742
+ regex: /cryptoField\.(?:sealRow|unsealRow)\s*\(\s*SEAL_TABLE\b/,
6743
+ requires: /function _ensureSealTable\b/,
6744
+ allowlist: [],
6745
+ reason: "v0.14.26 — queue-local seals job rows with cryptoField.sealRow(SEAL_TABLE, ...), but cryptoField.sealRow writes PLAINTEXT (silent no-op) for a table that was never registered. A standalone redis/sqs queue node that never ran db.init would therefore persist job payloads in cleartext. The fix self-registers the seal table on queue.init via _ensureSealTable (idempotent). This detector fires if a future edit seals/unseals SEAL_TABLE rows while the _ensureSealTable self-register is removed — reopening the fail-open-to-plaintext window. The companion _ensureSealTable declaration satisfies the discipline once the self-register is present.",
6746
+ },
6747
+ {
6748
+ // When the DSR ticket store adds the derived subject-hash lookup
6749
+ // columns to an existing table, ensureSchema MUST backfill legacy /
6750
+ // vault-less rows: compute the hashes from the plaintext subject + re-
6751
+ // seal. Once a vault is present, list({ subject }) matches on the hash
6752
+ // columns (the plaintext columns are sealed and unmatchable), so a
6753
+ // pre-upgrade row with NULL hashes is never found for its subject — and
6754
+ // the erasure-completion purge, which lists by subject, skips exactly
6755
+ // the tickets it must remove (GDPR Art. 17). The regex matches the
6756
+ // list-by-hash spec (always present in dsr.js); the companion `requires`
6757
+ // is the backfill SELECT, so the file is skipped while the backfill is
6758
+ // in place and flagged if it is removed. subject_email_hash is unique
6759
+ // to lib/dsr.js, so this self-scopes.
6760
+ id: "dsr-schema-upgrade-without-legacy-hash-backfill",
6761
+ primitive: "when the DSR ticket store queries subject-hash lookup columns, ensureSchema must backfill legacy/vault-less rows (compute hashes from plaintext + re-seal) so list({ subject }) finds pre-upgrade tickets and the erasure purge does not skip them",
6762
+ regex: /hashCol:\s*["']subject_email_hash["']/,
6763
+ requires: /subject_email_hash IS NULL/,
6764
+ allowlist: [],
6765
+ reason: "v0.14.26 — the DSR dbTicketStore matches list({ subject }) on derived-hash columns once a vault is present. A row written before the sealed-store upgrade (or while vault-less) has plaintext subject columns with NULL hashes, so it is invisible to a subject lookup — and the erasure-completion purge that lists by subject silently skips it, leaving un-erased PII (GDPR Art. 17 / CWE-noted advertised-vs-actual). ensureSchema must backfill: SELECT rows with NULL subject_*_hash, computeDerived from the plaintext, sealRow, and write hashes + sealed columns back (idempotent; also makes the legacy plaintext erasable). This detector fires if the hash-lookup path remains but the `subject_email_hash IS NULL` backfill SELECT is removed.",
6766
+ },
6767
+ {
6768
+ // The break-glass TOTP factor must reserve the accepted step ATOMICALLY
6769
+ // as part of acceptance (_reserveTotpStep — one compare-and-advance
6770
+ // cache update). The earlier shape read the replay floor
6771
+ // (_readLastTotpStep), verified against it, then committed the step in a
6772
+ // separate step (_commitTotpStep): two concurrent grant() calls with the
6773
+ // same in-window code both observe the old floor before either commits,
6774
+ // so both verify and the same code is redeemed twice (replay). Those two
6775
+ // function names are unique to lib/break-glass.js; their reappearance is
6776
+ // the racy read-then-commit pattern returning. skipCommentLines so the
6777
+ // historical reference in this catalog / docstrings doesn't self-match.
6778
+ id: "totp-step-read-then-commit-race",
6779
+ primitive: "reserve the break-glass TOTP replay step atomically as part of acceptance (_reserveTotpStep — one compare-and-advance); never read the floor, verify, then commit in a separate step (the _readLastTotpStep + _commitTotpStep shape) — two concurrent grants observe the same floor and both pass (replay)",
6780
+ regex: /\b_readLastTotpStep\b|\b_commitTotpStep\b/,
6781
+ skipCommentLines: true,
6782
+ allowlist: [],
6783
+ reason: "v0.14.26 (Codex P2 on PR #306) — break-glass grant() read the highest accepted TOTP step, verified against it, then committed the new step in a separate cache write. Two concurrent grants for the same (actor, secret, code) both read the old floor before either committed, so both _verifyTotpFactor calls passed and the same in-window code was redeemed more than once. The fix reserves the step atomically in _reserveTotpStep (a single _factorLockoutCache.update that advances the floor only if the step is strictly above it, reporting whether THIS caller won), so the second concurrent grant is refused. This detector flags reintroduction of the read-then-commit helpers (_readLastTotpStep / _commitTotpStep) that carried the race.",
6784
+ },
6785
+ // ---- v0.14.27 security-hardening sweep detectors ----
6786
+ // CodeQL js/path-injection — the static file server must re-confine a
6787
+ // request-derived path at the fs sink; the serve stream reads the confined
6788
+ // streamTarget, not the raw request-derived absPath.
6789
+ {
6790
+ id: "static-serve-stream-path-not-confined",
6791
+ primitive: "stream the static-served file from the root-confined streamTarget (lib/static.js _assertInsideRoot), never the request-derived absPath",
6792
+ regex: /createReadStream\(\s*absPath\s*,\s*streamOpts/,
6793
+ requires: /createReadStream\(\s*streamTarget\b/,
6794
+ skipCommentLines: true,
6795
+ allowlist: [],
6796
+ reason: "CWE-22 — the static file server resolves a request URL to a disk path; the path handed to fs.createReadStream must flow from the per-sink root-confinement barrier _assertInsideRoot (resolves under root, refuses anything outside via startsWith(root+sep)), not directly from the request-derived candidate. Streaming from the bare absPath re-opens the traversal-read class CodeQL flags.",
6797
+ },
6798
+ // CodeQL js/file-system-race + js/insecure-temporary-file — the content-safety
6799
+ // gate read must open the confined path with O_NOFOLLOW and anchor to one fd.
6800
+ {
6801
+ id: "static-gate-open-not-nofollow",
6802
+ primitive: "open the static content-safety gate read with O_RDONLY | O_NOFOLLOW on the confined path (lib/static.js) — refuse a final-component symlink swap, single-fd anchored",
6803
+ regex: /fsp\.open\(\s*\w*[Aa]bsPath\s*,\s*["']r["']\s*\)/,
6804
+ requires: /O_NOFOLLOW/,
6805
+ skipCommentLines: true,
6806
+ allowlist: [],
6807
+ reason: "CWE-367/CWE-59 — the pre-serve content-safety read must open the root-confined path with O_NOFOLLOW so a final-component symlink swap between the directory stat and the read cannot redirect it, and take size + bytes from that single descriptor. The bare fsp.open(absPath, \"r\") form drops both defenses.",
6808
+ },
6809
+ // CodeQL js/insecure-temporary-file — atomic-file stages every write into a
6810
+ // sibling temp file before rename; that create must be exclusive + no-follow.
6811
+ {
6812
+ id: "atomic-file-temp-create-not-exclusive",
6813
+ primitive: "create the atomic-file rename-staging temp via _openExclTemp (O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW) — never the truncating, symlink-following \"w\" flag",
6814
+ regex: /openSync\(\s*tmpPath\s*,\s*"w"/,
6815
+ requires: /_openExclTemp\s*\(/,
6816
+ skipCommentLines: true,
6817
+ allowlist: [],
6818
+ reason: "CWE-377/CWE-59 — atomic-file stages each write into a sibling temp before rename; that temp create must be O_EXCL (refuse a pre-planted file) + O_NOFOLLOW (refuse a planted symlink), not the truncating, symlink-following \"w\" flag.",
6819
+ },
6820
+ // CodeQL js/insecure-temporary-file — http-client download staging must be
6821
+ // exclusive + no-follow.
6822
+ {
6823
+ id: "http-client-download-temp-stream-not-exclusive",
6824
+ primitive: "stage the http-client download with O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW (numeric flag), not createWriteStream flags:\"w\"",
6825
+ regex: /createWriteStream\([^\n]*flags:\s*"w"/,
6826
+ requires: /O_EXCL/,
6827
+ skipCommentLines: true,
6828
+ allowlist: [],
6829
+ reason: "CWE-377/CWE-59 — downloadStream streams a remote body into a sibling temp before the hash-gated rename; that create must be O_EXCL + O_NOFOLLOW so an attacker can't pre-plant a file (truncated) or symlink (written through to a victim) at the staging path.",
6830
+ },
6831
+ // CodeQL js/remote-property-injection — body-parser must build maps from
6832
+ // [key,value] pairs (Object.fromEntries), never a request-keyed computed write.
6833
+ {
6834
+ id: "body-parser-request-keyed-map-write",
6835
+ primitive: "build body-parser header/param/field maps via Object.fromEntries (_mapFromPairs), never assign a request-derived key directly (target[bareKey|currentField|fieldName] = v)",
6836
+ regex: /\b\w+\[\s*(?:bareKey|currentField|fieldName)\s*\]\s*=(?!=)/,
6837
+ skipCommentLines: true,
6838
+ allowlist: [],
6839
+ reason: "CWE-915/CWE-1321 — body-parser's multipart Content-Disposition parser and field accumulator wrote attacker-controlled key names into a map; a part named __proto__/constructor/prototype reaches the Object.prototype setter. They now collect [key,value] pairs and materialize via _mapFromPairs (Object.fromEntries onto Object.create(null), poisoned keys dropped) / Object.assign(fields, Object.fromEntries([[fieldName,value]])). The bareKey/currentField/fieldName key vars are unique to these parsers.",
6840
+ },
6841
+ {
6842
+ id: "websocket-extension-params-keyed-write",
6843
+ primitive: "build the Sec-WebSocket-Extensions params map via Object.fromEntries onto Object.create(null) (lib/websocket.js _parseExtensionHeader), never ext.params[name] = v",
6844
+ regex: /\bext\.params\[\s*\w+\s*\]\s*=(?!=)/,
6845
+ skipCommentLines: true,
6846
+ allowlist: [],
6847
+ reason: "CWE-915/CWE-1321 — the RFC 7692 extension-parameter name comes from the client Sec-WebSocket-Extensions header; written as ext.params[k]=v a param named __proto__/constructor/prototype is the sink. The parser now collects paramPairs (poisoned names skipped) and builds via Object.assign(Object.create(null), Object.fromEntries(paramPairs)).",
6848
+ },
6849
+ {
6850
+ id: "body-parser-header-maps-compose-mapFromPairs",
6851
+ primitive: "lib/middleware/body-parser.js must compose _mapFromPairs to build its request-header/param maps — dropping the helper means a raw request-keyed computed write returned",
6852
+ regex: /function _parseMultipartHeaders\s*\(/,
6853
+ requires: /_mapFromPairs\s*\(/,
6854
+ skipCommentLines: true,
6855
+ allowlist: [],
6856
+ reason: "CWE-915/CWE-1321 — the generic-key parser sites (_contentType, _parseMultipartHeaders, _parseHeaderParams) build maps keyed by a request-controlled name; they're guarded structurally by the one composing primitive _mapFromPairs (Object.fromEntries onto Object.create(null), poisoned keys dropped). Anchored on _parseMultipartHeaders (unique to body-parser); fails if a future edit drops _mapFromPairs.",
6857
+ },
6858
+ // M10 — azure blob key must be percent-encoded before URL interpolation.
6859
+ {
6860
+ id: "azure-blob-key-unencoded-in-url",
6861
+ primitive: "percent-encode each azure blob-key path segment via _encodeBlobKey (sigv4.awsUriEncode) before URL interpolation",
6862
+ regex: /config\.container\s*\+\s*"\/"\s*\+\s*(?:opts\.)?key\b/,
6863
+ requires: /_encodeBlobKey\s*\(/,
6864
+ allowlist: [],
6865
+ reason: "CWE-20 — an azure blob key with ?/#/space truncates the URL path or corrupts the request line; keys must route through _encodeBlobKey (per-segment RFC 3986 encoding, preserving / separators) before interpolation, the encoder GCS already uses.",
6866
+ },
6867
+ // M7 — every file-upload content-safety skip path must audit the bypass.
6868
+ {
6869
+ id: "file-upload-content-safety-skip-unaudited",
6870
+ primitive: "every fileUpload content-safety skip path must emit a fileUpload.content_safety_skipped audit (_emitContentSafetySkipped) naming the reason, not just an obs counter",
6871
+ regex: /content_safety_skipped_streamed/,
6872
+ requires: /_emitContentSafetySkipped\s*\(/,
6873
+ allowlist: [],
6874
+ reason: "CWE-778 — an upload that bypasses the byte-level content scan (opt-out / no gate for the extension / over the reassembly cap) must be visible in the audit log, not just an observability counter, so a reviewer can tell a scanned upload from a bypassed one.",
6875
+ },
6876
+ // api-key rotate-on-verify re-hash must compare-and-swap on the value it read.
6877
+ {
6878
+ id: "apikey-rehash-on-verify-without-cas",
6879
+ primitive: "guard the rotate-on-verify secret re-hash UPDATE with a compare-and-swap on the read hash (.where(\"secretHash\", row.secretHash)) so a concurrent rotate() is not clobbered",
6880
+ regex: /touchFields\.secretHash\s*=\s*freshSecretHash/,
6881
+ requires: /\.where\(\s*"secretHash"\s*,\s*row\.secretHash\s*\)/,
6882
+ allowlist: [],
6883
+ reason: "CWE-362 (lost update) — verify() reads the stored secretHash, computes the upgraded hash, then writes it in a later UPDATE. Without a compare-and-swap on the exact hash that was read, a rotate()/hardRotate() landing between the read and the write is overwritten with the OLD secret's re-hash: the rotated token is invalidated and the old token keeps verifying. The re-hash UPDATE must carry `.where(\"secretHash\", row.secretHash)` so it no-ops when the row changed underneath it.",
6884
+ },
6885
+ // SigV4 canonical path must be service-aware (S3 single-encodes, others double).
6886
+ {
6887
+ id: "sigv4-canonical-path-unconditional-double-encode",
6888
+ primitive: "branch the SigV4 canonical path on doubleEncodePath (S3/GCS single-encode the already-encoded pathname; only sqs/logs/sns double-encode) — never unconditionally awsUriEncode the path",
6889
+ regex: /awsUriEncode\(\s*path\b/,
6890
+ requires: /doubleEncodePath\s*\?/,
6891
+ allowlist: [],
6892
+ reason: "Object-store correctness — a WHATWG URL pathname is ALREADY the single-encoded wire form, and S3/S3-compatible/GCS sign the canonical path with exactly that one encoding. A second awsUriEncode(path) signs '/a%2520b' for a key the wire carries as '/a%20b' → SignatureDoesNotMatch (403) on any key with a space/+/&/unicode. canonicalRequest must single-encode for S3 (doubleEncodePath=false, the default) and keep the second pass only for the genuinely double-encoding AWS services. Shipped green because every test key was plain ASCII (awsUriEncode is a no-op there).",
6893
+ },
6894
+ // awsUriEncode must iterate by Unicode code point, not UTF-16 code unit.
6895
+ {
6896
+ id: "sigv4-awsuriencode-utf16-unit-iteration",
6897
+ primitive: "iterate awsUriEncode by code point (Array.from / codePointAt), not by UTF-16 index + charAt — a per-unit encodeURIComponent throws URIError on a non-BMP key's split surrogate pair",
6898
+ regex: /function awsUriEncode\([\s\S]{0,400}?encodeURIComponent/,
6899
+ requires: /Array\.from|codePointAt/,
6900
+ allowlist: [],
6901
+ reason: "Object-store correctness — encodeURIComponent on a lone surrogate throws 'URIError: URI malformed', so iterating awsUriEncode by str.charAt(i) and escaping each UTF-16 unit breaks any object key containing a non-BMP character (emoji, CJK Extension B, ...) before the request is signed. The encoder must walk Unicode code points (Array.from(str) keeps surrogate pairs together) so the whole character reaches encodeURIComponent as one UTF-8 sequence.",
6902
+ },
6903
+ // sql.js createTable must route its emitted DDL through the quote-aware catalog gate.
6904
+ {
6905
+ id: "sql-createtable-ddl-not-catalog-gated",
6906
+ primitive: "route createTable's emitted CREATE TABLE through _assertCatalogEmittable (its quote-aware single-statement scan is the injection backstop for the one raw-emission position — the verbatim column type) — never return a bare { sql, params }",
6907
+ regex: /var sql = "CREATE TABLE " \+ ifNot[\s\S]{0,420}?return \{ sql:/,
6908
+ allowlist: [],
6909
+ reason: "SQL injection — _ddlType returns an unrecognised column type verbatim into the DDL; it is the one raw-emission position in an otherwise quote-by-construction builder (constraints route through _checkRawFragment, names through _quoteId). The injection backstop is the quote-aware _assertCatalogEmittable scan, which refuses a top-level ';' / comment / unbalanced quote / unbalanced paren while CORRECTLY allowing those characters inside a balanced quoted label (ENUM('needs;review')). createTable must therefore return _assertCatalogEmittable(sql, []) — a bare { sql, params } would let a type like 'text); DROP TABLE x; --' emit a stacked statement. A non-quote-aware pre-scan on the type was removed precisely because it over-rejected valid quoted labels.",
6910
+ },
6911
+ {
6912
+ // v0.15.4 R2 — every hand-rolled DDL (CREATE/ALTER TABLE, CREATE INDEX)
6913
+ // concatenated and handed to runSql/exec must route through
6914
+ // safeSql.assertSingleStatement first, the same quote-aware single-statement
6915
+ // gate the b.sql builder enforces. db-schema.reconcileTable shipped a
6916
+ // verbatim-column-type injection (a type "TEXT); DROP TABLE x; --" smuggled a
6917
+ // stacked statement) until this gate; this enforces the invariant across the
6918
+ // whole raw-DDL family (schema reconcile, DSR store, migrations), not one site.
6919
+ id: "ddl-concat-to-runsql-without-single-statement-gate",
6920
+ primitive: "wrap a hand-rolled CREATE TABLE / ALTER TABLE / CREATE INDEX string in safeSql.assertSingleStatement(sql, { label }) before runSql/exec — the raw-DDL paths use the same single-statement gate the b.sql builder does",
6921
+ regex: /\b(?:runSql|exec)\(\s*(?:\w+,\s*)?"(?:CREATE TABLE|ALTER TABLE|CREATE INDEX|DROP TABLE)/,
6922
+ skipCommentLines: true,
6923
+ allowlist: [],
6924
+ reason: "A finished DDL string built by concatenating a (possibly operator-controlled) value and passed straight to runSql/exec bypasses the quote-aware single-statement scan the b.sql builder enforces on its own DDL — a verbatim column type like 'TEXT); DROP TABLE x; --' smuggles a stacked statement (lib/db-schema.js reconcileTable shipped exactly this until v0.15.4). Route the finished string through safeSql.assertSingleStatement(sql, { label }); the gated form does not match because the string literal no longer sits directly after the runSql/exec open-paren + optional db arg.",
6925
+ },
6926
+ // #63 — safe-xml must reject prototype-poisoning element/attribute names and
6927
+ // build null-prototype accumulators.
6928
+ {
6929
+ // v0.15.4 R1 — the raw write entry points (execRaw / prepare) must route a
6930
+ // write to a per-row-residency table through the residency gate, like the
6931
+ // structured builder's insert/update. Without it a raw INSERT/UPDATE lands a
6932
+ // cross-border row past the gate. The function body must reference the gate:
6933
+ // _assertRawWriteResidency (execRaw) or _isRawWriteToResidencyTable (prepare).
6934
+ id: "db-raw-write-entry-skips-residency-gate",
6935
+ primitive: "execRaw / prepare must call the residency gate (_assertRawWriteResidency / _isRawWriteToResidencyTable) so a raw INSERT/UPDATE to a per-row-residency table is validated like b.db.from().insertOne/updateOne",
6936
+ regex: /function (?:execRaw|prepare)\s*\([^)]*\)\s*\{(?:(?!_assertRawWriteResidency|_isRawWriteToResidencyTable|\n\})[\s\S]){0,12000}?\n\}/,
6937
+ allowlist: [
6938
+ // localDb.thin is an isolated lightweight node:sqlite wrapper with no
6939
+ // cryptoField / residency policy and a separate DB file - its prepare
6940
+ // cannot write a per-row-residency row, so the residency gate is N/A.
6941
+ "lib/local-db-thin.js",
6942
+ ],
6943
+ reason: "The structured builder runs every insert/update through _assertLocalResidency, but the raw paths b.db.runSql (execRaw) and b.db.prepare(sql).run(...) bypass it, so under a regulated posture a cross-border row lands straight on disk (shipped this way until v0.15.4). execRaw must call _assertRawWriteResidency(sql); prepare must wrap a write to a residency table via _isRawWriteToResidencyTable. This detector fires if either function reaches its closing brace without referencing the gate.",
6944
+ },
6945
+ {
6946
+ id: "xml-parsename-no-prototype-key-rejection",
6947
+ primitive: "reject element/attribute names __proto__/constructor/prototype in the XML name parser (lib/parsers/safe-xml.js parseName → FORBIDDEN_KEYS)",
6948
+ regex: /xml\/bad-name[\s\S]{0,200}?return\s+input\.substring/,
6949
+ requires: /FORBIDDEN_KEYS\.has/,
6950
+ skipCommentLines: true,
6951
+ allowlist: [],
6952
+ reason: "CWE-1321 — b.safeXml built its key accumulators from parser-controlled names with no poisoned-key rejection (unlike its toml/yaml/ini siblings); an attribute named constructor tripped the duplicate guard via an inherited member, and __proto__/constructor/prototype landed as result-tree keys. parseName (uniquely scoped by the xml/bad-name code) must reject FORBIDDEN_KEYS before returning the parsed name.",
6953
+ },
6954
+ {
6955
+ id: "xml-make-wrapper-plain-object",
6956
+ primitive: "build the XML element-name wrapper with Object.create(null) (lib/parsers/safe-xml.js _make), not a plain {} keyed by an attacker-influenced element name",
6957
+ regex: /function _make\(name, value\)[\s\S]{0,600}?var out = \{\}/,
6958
+ requires: /var out = Object\.create\(null\)/,
6959
+ skipCommentLines: true,
6960
+ allowlist: [],
6961
+ reason: "CWE-1321 — _make wrapped each parsed element as `var out = {}; out[name] = value` keyed by the element name; with a plain object, out[\"__proto__\"]=value reassigns the wrapper prototype and the returned tree exposes inherited Object members on absent keys. The accumulator must be Object.create(null).",
6962
+ },
6963
+ // #64 — router.use must branch on a string/array path prefix, not drop it.
6964
+ {
6965
+ id: "router-use-drops-path-argument",
6966
+ primitive: "Router.use must classify its first argument (function = global; string/array = path-scoped) — never a single-arg use(fn){this.middleware.push(fn)} that drops the path",
6967
+ regex: /\buse\s*\(\s*fn\s*\)\s*\{\s*this\.middleware\.push\s*\(\s*fn\s*\)\s*;?\s*\}/,
6968
+ requires: /_usePrefixesFromFirstArg|typeof\s+first\s*===\s*["']function["']/,
6969
+ skipCommentLines: true,
6970
+ allowlist: [],
6971
+ reason: "CWE-670 — router.use(path, mw) is documented across ~11 security middleware but was unimplemented: use(fn) pushed the first arg and dropped the rest, so a path-scoped security gate either 500'd every request (the path string invoked as a function) or never ran where mounted (silent control-scoping bypass). The fix classifies the first argument before pushing.",
6972
+ },
6973
+ // M4 — JMAP must gate a client accountId against accountsFor before dispatch.
6974
+ {
6975
+ id: "jmap-accountid-forwarded-without-accountsfor-gate",
6976
+ primitive: "gate a client-supplied JMAP accountId against accountsFor(actor)'s permitted set (lib/mail-server-jmap.js _permittedAccountIds → accountNotFound) before dispatching to a method/blob handler",
6977
+ regex: /\b(?:uploadBlob|downloadBlob)\([^)]*accountId[^)]*\)/,
6978
+ requires: /_permittedAccountIds|accountNotFound/,
6979
+ allowlist: [],
6980
+ reason: "RFC 8620 §3.6.1 — a client-supplied accountId must be checked against accountsFor(actor)'s permitted set and rejected with accountNotFound BEFORE reaching a method/blob handler. Forwarding it on format-validation alone lets one tenant reach another tenant's account.",
6981
+ },
6982
+ // #69 / #125 — EVERY OTLP attribute-map encoder must route its values
6983
+ // through the telemetry redactor. The class is "a function that turns a raw
6984
+ // { key: value } attribute map into OTLP KeyValues": _attrsToOtlp (metric /
6985
+ // event JSON, lib/otel-export.js), _attrToOtlp (span / event / resource JSON,
6986
+ // lib/observability-otlp-exporter.js), _attrsToProto (span / event / resource
6987
+ // protobuf, same file). Each must call observability.redactAttrs() (or the
6988
+ // legacy per-value _redactAttrValue) before emitting; the value type-encoders
6989
+ // (_valueToOtlp / _anyValueToProto / _encodeValue) run AFTER redaction and
6990
+ // are deliberately NOT anchored. Span-anchored so a body that reaches its
6991
+ // column-0 close without a redactor reference fires.
6992
+ {
6993
+ id: "otlp-attr-encoder-skips-telemetry-redactor",
6994
+ primitive: "route every OTLP attribute-map encoder (_attrsToOtlp / _attrToOtlp / _attrsToProto) through observability.redactAttrs() before serialization — telemetry is a first-class EGRESS sink and an unredacted attribute value ships secrets/PII onto the OTLP wire (CWE-532)",
6995
+ scanScope: "lib",
6996
+ regex: /function (?:_attrToOtlp|_attrsToProto|_attrsToOtlp)\s*\([^)]*\)\s*\{(?:(?!redactAttrs|_redactAttrValue|\n\})[\s\S]){0,4000}?\n\}/,
6997
+ allowlist: [],
6998
+ reason: "CWE-532 — span/metric/resource attributes are a first-class egress sink. #69 fixed lib/otel-export.js _attrsToOtlp but pinned its detector to that one function name, leaving the SPAN exporter's two sibling encoders (lib/observability-otlp-exporter.js _attrToOtlp JSON + _attrsToProto protobuf) shipping attribute values verbatim to the collector (#125). The shared root is 'an attribute-map encoder that serializes without scrubbing'; every such encoder must pass each value through observability.redactAttrs() (default composes b.redact.redact, fail-toward-dropping on a throwing redactor) before the wire payload. The negative lookahead exempts the per-value type-encoders (_valueToOtlp/_anyValueToProto) which run after redaction; the {0,4000} bound is a ReDoS backstop well above the real encoder bodies (<2000 chars).",
6999
+ },
7000
+ // #131 — the b.middleware.dpop factory must REQUIRE its replayStore at config
7001
+ // time. The store is DPoP's jti-replay defense (RFC 9449 §11.1); reading it
7002
+ // optionally and gating the check behind `if (replayStore)` silently mounts a
7003
+ // proof-of-possession middleware that performs no replay check when the
7004
+ // operator omits the store. The middleware (operator security default) must
7005
+ // enforce presence + the checkAndInsert shape with validateOpts.requireMethods;
7006
+ // the low-level verify() primitive keeps its documented optional replayStore.
7007
+ {
7008
+ id: "dpop-middleware-replaystore-not-required",
7009
+ primitive: "enforce opts.replayStore presence + checkAndInsert shape at b.middleware.dpop create() via validateOpts.requireMethods(opts.replayStore, [\"checkAndInsert\"], ...) — a missing store silently disables DPoP jti-replay defense (RFC 9449)",
7010
+ scanScope: "lib",
7011
+ regex: /var replayStore\s*=\s*opts\.replayStore/,
7012
+ requires: /requireMethods\(\s*opts\.replayStore/,
7013
+ skipCommentLines: true,
7014
+ allowlist: [],
7015
+ reason: "#131 — b.middleware.dpop documented replayStore as required but create() read it optionally (`var replayStore = opts.replayStore`) and gated the replay check behind `if (replayStore)`, so omitting it mounted a DPoP gate with NO jti-replay defense — a captured proof replays indefinitely (RFC 9449 §11.1). The operator-facing middleware must fail closed at config time: validateOpts.requireMethods(opts.replayStore, [\"checkAndInsert\"], ...) throws on both a missing store and a store lacking checkAndInsert. The unique `var replayStore = opts.replayStore` token is the middleware's optional read (the low-level lib/auth/dpop.js verify() primitive uses opts.replayStore inline and keeps it deliberately optional, so it is not matched). This entry fires only if the create-time requireMethods enforcement is removed while the optional read remains.",
7016
+ },
7017
+ // #135 — SD-JWT-VC ES256/ES384 must sign/verify with JOSE encoding (raw r||s).
7018
+ {
7019
+ id: "sd-jwt-vc-ecdsa-der-not-ieee-p1363",
7020
+ primitive: "sd-jwt-vc _signJwt / _verifyJwt must wrap the EC key with dsaEncoding: \"ieee-p1363\" for ES256/ES384 — node:crypto defaults to DER, which every conformant JOSE / EUDI-wallet verifier rejects (and the library would reject their raw-r||s signatures)",
7021
+ scanScope: "lib",
7022
+ regex: /nodeCrypto\.sign\(\s*sigAlgo\s*,\s*Buffer\.from\([^)]*\)\s*,\s*privateKey\s*\)|nodeCrypto\.verify\(\s*sigAlgo\s*,\s*Buffer\.from\([^)]*\)\s*,\s*publicKey\s*,/,
7023
+ allowlist: [],
7024
+ reason: "#135 — _signJwt/_verifyJwt passed the bare EC key (`, privateKey)` / `, publicKey,`) to nodeCrypto.sign/verify, so ES256/ES384 ECDSA signatures were DER-encoded (ASN.1 SEQUENCE, leading 0x30, ~70-72 bytes for P-256). JOSE/JWS — and EUDI wallets — require raw r||s (\"ieee-p1363\", 64 bytes for ES256, 96 for ES384), so tokens this issuer signed were rejected by conformant verifiers and the library rejected conformant wallets' KB-JWTs. The fix routes both calls through _ecKeyParam(algorithm, key), which returns { key, dsaEncoding: \"ieee-p1363\" } for ES256/ES384 — matching oauth.js / dpop.js / jwt-external.js _verifyParamsForAlg. The anchor is the bare-key call shape (3rd arg is the literal `privateKey` / `publicKey` identifier); once wrapped in _ecKeyParam the shape no longer matches. The KB-JWT (holder) and issuer JWT both sign through this single _signJwt, so the one fix closes both.",
7025
+ },
7026
+ // #137 — verifyIdToken's skipExpCheck must be gated to logout tokens.
7027
+ {
7028
+ id: "oauth-verifyidtoken-skipexpcheck-ungated",
7029
+ primitive: "verifyIdToken must NOT honor a caller-passable skipExpCheck on a regular ID token — the exp bypass is only valid for OIDC Back-Channel-Logout tokens (events claim), and even then bounded by an iat freshness floor; a bare `if (!vopts.skipExpCheck)` gate lets any caller verify an expired/replayed credential clean",
7030
+ scanScope: "lib",
7031
+ regex: /if \(!vopts\.skipExpCheck\)/,
7032
+ requires: /skip-exp-check-not-allowed/,
7033
+ skipCommentLines: true,
7034
+ allowlist: [],
7035
+ reason: "#137 — verifyIdToken wrapped its exp validation in `if (!vopts.skipExpCheck) { ... }` so any external caller (verifyIdToken is a public API) could pass skipExpCheck: true and verify an EXPIRED id_token clean — token-replay of expired-but-once-valid credentials. skipExpCheck exists only because OIDC Back-Channel Logout 1.0 §2.4 logout tokens carry no exp; the internal verifyBackchannelLogoutToken passes it. The fix flips the branch to `if (vopts.skipExpCheck) { <require the backchannel-logout events claim> + <iat freshness floor> } else { <exp check> }`, so skipExpCheck is refused (auth-oauth/skip-exp-check-not-allowed) on any token lacking the logout event claim and a stale logout token is refused (auth-oauth/logout-token-stale). The detector anchors on the bare negative gate and requires the new refusal code in-file; after the fix the bare gate is gone and the code is present, so it stays silent.",
7036
+ },
7037
+ // #116 — crypto-field upgrade-on-read rewrite must honor the handle's dialect.
7038
+ {
7039
+ id: "cryptofield-upgrade-on-read-hardcodes-sqlite-dialect",
7040
+ primitive: "_upgradeDerivedHashesOnRead must build its durable re-hash UPDATE with the resolved dialect of the writable handle, not a hardcoded dialect: \"sqlite\" — unsealRow accepts a caller-supplied Postgres/MySQL handle, and a sqlite-quoted UPDATE (double quotes) is rejected by MySQL (backticks), so the advertised keyed-MAC migration silently no-ops off sqlite",
7041
+ scanScope: "lib",
7042
+ regex: /function _upgradeDerivedHashesOnRead(?:(?!\nfunction )[\s\S]){0,4000}?sql\.update\(\s*table\s*,\s*\{\s*dialect:\s*"sqlite"/,
7043
+ allowlist: [],
7044
+ reason: "#116 — the upgrade-on-read durable rewrite (re-hash a legacy salted-sha3 derived-hash column to the keyed MAC) hardcoded sql.update(table, { dialect: \"sqlite\", quoteName: true }). unsealRow's 4th arg is a caller-supplied dbHandle that db-query threads from an external Postgres/MySQL connection; on a MySQL handle the sqlite-dialected UPDATE emits double-quoted identifiers (\"users\"), which MySQL parses as a string literal and rejects, so the rewrite throws into the best-effort try/catch and the legacy digest stays on disk — keyed-MAC migration never happens off sqlite. The fix resolves the dialect from handle.dialect (validated to postgres|mysql|sqlite, default sqlite — db-query._dialect()'s contract). The detector anchors INSIDE _upgradeDerivedHashesOnRead (the function-scoped span) so the legitimately-sqlite-only _PER_ROW_SQL_OPTS literal at module scope is not flagged; it fires while the literal dialect: \"sqlite\" remains and goes silent once it reads the handle's dialect.",
7045
+ },
7046
+ // #126 — SSE _writeRaw must bound its outbound buffer (slow-consumer DoS).
7047
+ {
7048
+ id: "sse-writeraw-no-bounded-buffer",
7049
+ primitive: "sse _writeRaw must bound the per-channel outbound buffer (res.writableLength vs a maxBufferedBytes cap) and evict a slow consumer — res.write() returning false is unbounded backpressure; a stalled client otherwise grows the server heap without limit (memory-exhaustion DoS)",
7050
+ scanScope: "lib",
7051
+ regex: /function _writeRaw\b/,
7052
+ requires: /writableLength/,
7053
+ skipCommentLines: true,
7054
+ allowlist: [],
7055
+ reason: "#126 — _writeRaw called res.write(s) and discarded the boolean return (the backpressure signal) with no bound on res.writableLength (Node's count of buffered-but-unflushed bytes). SSE is server-push: when a client stalls, the app keeps calling send() and the writable buffer grows without limit until the heap is exhausted — one slow connection is a memory-exhaustion DoS. The fix reads res.writableLength after each write and, when it exceeds the per-channel maxBufferedBytes cap (default 1 MiB), closes the channel and throws sse/backpressure — evicting the slow consumer instead of buffering forever (the bounded-write discipline lib/archive.js already follows). The detector fires while _writeRaw never consults writableLength and goes silent once the cap lands; a healthy client (writableLength ~0) is never evicted.",
7056
+ },
7057
+ // #141 — sealed-field membership (IN) must hash each candidate element.
7058
+ {
7059
+ id: "db-query-sealed-in-hashes-whole-array",
7060
+ primitive: "db-query's sealed-field → derived-hash rewrite must map EACH element of an IN candidate list through cryptoField.lookupHash (and include each one's legacy digest for dual-read) — passing the whole array to lookupHash as a single value produces one bogus hash and makes whereIn/$in on a sealed column throw or silently miss",
7061
+ scanScope: "lib",
7062
+ regex: /if \(this\._isSealedField\(field\)\)\s*\{(?:(?!op === "IN")[\s\S]){0,200}?cryptoField\.lookupHash/,
7063
+ allowlist: [],
7064
+ reason: "#141 — _resolvePredicate's sealed-field branch called cryptoField.lookupHash(this._cryptoFieldKey(), field, value) once with the raw value. For op \"=\"/\"!=\" that value is a scalar, but for op \"IN\" (b.db.from().whereIn(sealedCol, [...]) / b.db.collection().find({ sealedCol: { $in: [...] } })) it is the candidate ARRAY — lookupHash then hashes the array-as-one-value, and the later `where IN requires a non-empty array` shape check throws, so membership queries on a sealed column were unusable (the documented derived-hash query path supported equality but not membership). The fix branches on op === \"IN\" inside the sealed block and maps each element through lookupHash, building the combined IN-list with each element's keyed + legacy digest (the same dual-read the \"=\" path does across the v0.15.0 keyed-MAC flip). The tempered span fires while the sealed block reaches lookupHash with no `op === \"IN\"` branch first and goes silent once that branch precedes the per-element lookup; the {0,200} bound is a ReDoS backstop above the ~60-char buggy span.",
7065
+ },
7066
+ // #127 — worker-pool must mark a slot recycling BEFORE _finishTask drains the queue.
7067
+ {
7068
+ id: "workerpool-finishtask-before-recycling-mark",
7069
+ primitive: "_onTaskTimeout / _onWorkerError must set slot.recycling = true BEFORE calling _finishTask — _finishTask sets slot.busy = false and drains the queue, so a freshly-queued task would be dispatched to the slot whose worker is about to be terminated (the task dies with workerpool/worker-exit instead of running on the replacement worker)",
7070
+ scanScope: "lib",
7071
+ regex: /function _onTaskTimeout\s*\([^)]*\)\s*\{(?:(?!slot\.recycling = true)[\s\S]){0,600}?_finishTask\(slot|function _onWorkerError\s*\([^)]*\)\s*\{(?:(?!slot\.recycling = true)[\s\S]){0,600}?_finishTask\(slot/,
7072
+ allowlist: [],
7073
+ reason: "#127 — _finishTask() sets slot.busy = false and ends with _drainQueue(); _findIdleSlot() returns any slot that is `!busy && !recycling`. _onTaskTimeout/_onWorkerError called _finishTask FIRST and only marked the slot recycling afterward (in _recycleWorker), so the synchronous drain inside _finishTask handed a just-queued task to the dying slot — its message went to a worker about to be terminate()d and came back as workerpool/worker-exit (or hung), even though a healthy replacement was about to spawn. The fix sets slot.recycling = true before _finishTask in both handlers so the drain skips the dying slot and the queued task waits for the replacement. The tempered span fires while _finishTask(slot is reached before the recycling mark in either handler and goes silent once the mark precedes it; the {0,600} bound is a ReDoS backstop above the ~15-line handler bodies.",
7074
+ },
7075
+ // #128 — outbox must reap stale in-flight claims before claiming new work.
7076
+ {
7077
+ id: "outbox-processonce-claims-without-reaping",
7078
+ primitive: "outbox._processOnce must call _reapStaleInflight() BEFORE _claimBatch() — a claim flips a row to in-flight (status), and the claim path only selects status='pending', so a crash between claim and mark-published strands the row in-flight forever (at-least-once violated). The poll must reclaim stale in-flight rows each cycle",
7079
+ scanScope: "lib",
7080
+ regex: /async function _processOnce\s*\([^)]*\)\s*\{(?:(?!_reapStaleInflight)[\s\S]){0,400}?_claimBatch/,
7081
+ allowlist: [],
7082
+ reason: "#128 — the outbox claims jobs by flipping status pending → in-flight, but _claimBatch only SELECTs status='pending'. With no reaper, a publisher that claims a row then crashes before _markPublished/_markRetry/_markDead leaves the row in-flight forever — the event is silently dropped and the advertised at-least-once delivery is violated (b.queue has sweepExpired; outbox had nothing). The fix stamps claimed_at on claim and reaps any in-flight row older than claimReclaimMs (or NULL claimed_at, a legacy/crashed claim) back to pending at the top of every poll, before _claimBatch. The tempered span fires while _processOnce reaches _claimBatch without a preceding _reapStaleInflight call and goes silent once the reap precedes the claim; the {0,400} bound is a ReDoS backstop above the short body. The behavioral guard is test/layer-0-primitives/outbox-inflight-reaper.test.js.",
7083
+ },
7084
+ // #133 — SAML Bearer/HoK SubjectConfirmation NotOnOrAfter must fail closed.
7085
+ { id: "saml-subjectconfirmation-notonorafter-must-fail-closed", primitive: "verifyResponse's Bearer SubjectConfirmationData NotOnOrAfter check must fail closed: `var notOnOrAfter = _attr(scd, \"NotOnOrAfter\"); if (!notOnOrAfter) continue;` followed by a parseability+expiry check — never the optional `if (notOnOrAfter) { ... }` shape, which accepts a confirmation with NO NotOnOrAfter as fresh-forever", scanScope: "lib", regex: /var notOnOrAfter\s*=\s*_attr\([^)]*\);\s*if \(notOnOrAfter\)/, allowlist: [], reason: "SAML 2.0 Web Browser SSO Profile §4.1.4.2 requires every Bearer SubjectConfirmationData to carry a NotOnOrAfter that bounds the assertion's freshness window. lib/auth/saml.js verifyResponse once read `var notOnOrAfter = _attr(scd, \"NotOnOrAfter\"); if (notOnOrAfter) { ... }` — a MISSING NotOnOrAfter skipped the whole block, so a confirmation with no time bound was accepted as fresh-forever (an unbounded, replay-forever assertion; CWE-613 insufficient session expiration / CWE-294 replay). The Holder-of-Key sibling (Profile §3.1, which incorporates §3 time-bounding) had the same missing-attribute hole PLUS a second latent one: `if (noaHok && isFinite(...) && Date.parse(noaHok)/1000 < ...)` short-circuited on `&&`, so an UNPARSEABLE NotOnOrAfter was also accepted. Both sites now require presence + parseability + not-expired before the confirmation is honored (`if (!notOnOrAfter) continue;` then the isFinite/expiry continue). This detector fires while the Bearer check is the optional `if (notOnOrAfter)` shape and goes silent once it is the fail-closed `if (!notOnOrAfter) continue;` shape; the behavioral guard is test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js (RED on the optional shape for the missing-attr case, GREEN on the fix). Empty allowlist — an optional NotOnOrAfter on a Bearer confirmation is the unbounded-freshness bug." },
7086
+ // #109 — defineGuard's default gate must resolve profile + posture.
7087
+ {
7088
+ id: "defineguard-defaultgate-skips-profile-posture-resolution",
7089
+ primitive: "defineGuard's defaultGate must resolve profile + posture (resolveProfileAndPosture) before passing opts to buildGuardGate — otherwise the gate reads forensicSnippetBytes / maxRuntimeMs from RAW opts, dropping the profile's runtime cap and the posture's forensic-snippet cap",
7090
+ scanScope: "lib",
7091
+ regex: /function defaultGate\s*\([^)]*\)\s*\{(?:(?!resolveProfileAndPosture)[\s\S]){0,2000}?return buildGuardGate/,
7092
+ allowlist: [],
7093
+ reason: "#109 — defineGuard's defaultGate passed RAW opts straight to buildGuardGate, which reads opts.forensicSnippetBytes / opts.maxRuntimeMs directly. Those caps live in the resolved PROFILE (maxRuntimeMs) and POSTURE (forensicSnippetBytes), not the raw caller opts — so gate({ compliancePosture: \"hipaa\" }) dropped the 128-byte forensic cap to 0 (forensic snapshots disabled on a regulated-posture refusal) and dropped the profile's runtime cap to uncapped. The hand-written gates call resolveProfileAndPosture(opts, ...) first; the default gate must too. The span anchors on the single defaultGate and fires if its body reaches `return buildGuardGate` without a resolveProfileAndPosture call; the {0,2000} bound is a ReDoS backstop above the ~700-char body.",
7094
+ },
7095
+ // #129 — session.rotate must re-key the sid-bound device fingerprint.
7096
+ {
7097
+ id: "session-rotate-skips-fingerprint-rekey",
7098
+ primitive: "session.rotate must re-key the sid-bound __bj_fingerprint to the NEW sid via _hashFingerprint(newSid, ...) — a rotated session that carries the old-sid hash makes verify() falsely report fingerprintDrift (logout under strict operators) or silently breaks the device binding",
7099
+ scanScope: "lib",
7100
+ regex: /async function rotate\s*\([^)]*\)\s*\{(?=[\s\S]*?newSidHash)(?:(?!_hashFingerprint|\n\})[\s\S]){0,3000}?\n\}/,
7101
+ allowlist: [],
7102
+ reason: "#129 — __bj_fingerprint is sid-keyed (_hashFingerprint(sid, inputs), so a stolen DB can't replay the binding). rotate() moves the sid but left the stored fingerprint bound to the OLD sid; verify(newToken, sameReq) then recomputes _hashFingerprint(newSid, inputs) and mismatches → a false fingerprintDrift (strict operators destroy the session = self-DoS on every rotation) or a silently-broken binding. rotate must re-key the fingerprint to the new sid from the live request: _hashFingerprint(newSid, _buildFingerprintInputs(req, fpFields)). The span anchors on the single async rotate() in lib/session.js and fires if its body reaches the column-0 close with no _hashFingerprint re-key; the {0,3000} bound is a ReDoS backstop above the ~2KB body. updateData legitimately PRESERVES the old hash (sid unchanged) so it is a different function and not matched.",
7103
+ },
7104
+ // #120 — a retention dry-run / preview must not VACUUM the database.
7105
+ {
7106
+ id: "retention-erase-vacuums-before-dryrun-gate",
7107
+ primitive: "retention._erase must compute its sealed/hash field set and pass the `if (dryRun) return` gate BEFORE calling cryptoField.eraseRow — eraseRow schedules a full DB VACUUM and emits db.vacuum_after_erase under a regulated posture, so calling it before the dry-run gate makes a preview rewrite the whole database file per candidate row",
7108
+ scanScope: "lib",
7109
+ regex: /function _erase\s*\([^)]*\)\s*\{(?:(?!if \(dryRun\) return)[\s\S]){0,1500}?cryptoField\.eraseRow/,
7110
+ allowlist: [],
7111
+ reason: "#120 — cryptoField.eraseRow, under a posture whose POSTURE_DEFAULTS sets requireVacuumAfterErase (gdpr/hipaa/dpdp/pipl-cn/lgpd-br), calls db().vacuumAfterErase({ mode: \"full\" }) → VACUUM; → db.vacuum_after_erase. retention._erase called eraseRow at the top of the function, before the `if (dryRun) return { wouldErase: 1 }` gate, so b.retention.run(name, { dryRun: true }) (and the CLI `retention preview`) ran a full-table VACUUM per past-TTL row — a preview that locks the DB and rewrites the file, the opposite of a dry-run. eraseRow's return value is discarded (void erased), so it only ran for that side effect; the fix computes sealedFields/hashFields (no side effect) and returns on dryRun BEFORE eraseRow. The tempered span anchors on the single _erase and the `if (dryRun) return` structural boundary: it fires while eraseRow is reachable before the gate and goes silent once eraseRow moves after it; the {0,1500} bound is a ReDoS backstop above the ~400-char body.",
7112
+ },
7113
+ // #71 — registerTable must honor the pinned posture's seal-envelope floor.
7114
+ {
7115
+ id: "crypto-field-register-without-seal-floor-gate",
7116
+ primitive: "enforce the pinned posture's sealEnvelopeFloor at cryptoField.registerTable (_assertSealEnvelopeFloor) — a regulated posture must refuse a table sealing columns below its envelope floor",
7117
+ regex: /function registerTable\b/,
7118
+ requires: /_assertSealEnvelopeFloor/,
7119
+ skipCommentLines: true,
7120
+ allowlist: [],
7121
+ reason: "CWE-311/CWE-326 — POSTURE_DEFAULTS gained a sealEnvelopeFloor (hipaa/pci-dss → aad); registerTable must throw at config-time when a regulated posture is pinned and the table seals columns under a weaker envelope (plain below aad/per-row-key), or a HIPAA deployment can register a copy-paste-vulnerable plain-sealed table.",
7122
+ },
6177
7123
  {
6178
7124
  // Node 26 ships `Map.prototype.getOrInsertComputed(key, factory)`
6179
7125
  // (TC39 stage-4, lands in V8 13.x). It replaces the two-step
@@ -6423,6 +7369,34 @@ var KNOWN_ANTIPATTERNS = [
6423
7369
  // derives a different signing input or refuses the critical header.
6424
7370
  // The `requires` companion is satisfied by the refusal branch
6425
7371
  // naming 'b64' somewhere in the same file.
7372
+ id: "db-query-write-without-residency-gate",
7373
+ primitive: "_assertLocalResidency(table, plaintextRow, op) before cryptoField.sealRow on every local write path",
7374
+ // A local write method that seals a row without first running the
7375
+ // residency gates can land a region-bound row (or region-bound
7376
+ // column value) outside the deployment's declared region set —
7377
+ // the cross-border transfer shape GDPR Art 44-46 / PIPL Art 38 /
7378
+ // DPDP §16 regulate. The gate must see the PLAINTEXT row, so it
7379
+ // runs before sealRow in the same method.
7380
+ regex: /sealRow\(this\._cryptoFieldKey\(\)/,
7381
+ requires: /_assertLocalResidency\(this\._cryptoFieldKey\(\)/,
7382
+ skipCommentLines: true,
7383
+ allowlist: [],
7384
+ reason: "v0.14.24 — declareColumnResidency/assertColumnResidency shipped in v0.7.27 documenting a write-time gate that was never wired into any write path; rows and region-bound columns landed on any backend unchecked. Every db-query method that seals a row must run _assertLocalResidency on the plaintext first; a future write method (upsert, bulk path) inherits the requirement automatically.",
7385
+ },
7386
+ {
7387
+ id: "ar-header-prepend-without-forged-strip",
7388
+ primitive: "_stripForgedAuthResults(messageBuf, authservId) before prepending a computed Authentication-Results header",
7389
+ // A receiver that prepends its own Authentication-Results header
7390
+ // without first deleting sender-attached instances claiming the
7391
+ // same authserv-id lets a forged pre-attached verdict shadow the
7392
+ // computed one for downstream consumers (RFC 8601 §5 MUST).
7393
+ regex: /Buffer\.from\(\s*\w+\.authResults\s*\+/,
7394
+ requires: /_stripForgedAuthResults/,
7395
+ skipCommentLines: true,
7396
+ allowlist: [],
7397
+ reason: "v0.14.23 — a receiver that prepends its computed Authentication-Results header without stripping sender-forged instances carrying its own authserv-id lets a downstream consumer reading 'the receiver's A-R header' read the attacker's instead. RFC 8601 §5 requires deleting (or renaming) same-authserv-id instances before adding the new one. Any code path that prepends an emitted A-R header must compose the strip helper in the same file.",
7398
+ },
7399
+ {
6426
7400
  id: "jose-header-passthrough-without-b64-crit-refusal",
6427
7401
  primitive: "refuse own 'b64'/'crit' members on any caller-supplied JOSE protected-header object before signing",
6428
7402
  regex: /assignOwnEnumerable\s*\(\s*\{\s*\}\s*,\s*opts\.header/,
@@ -7468,6 +8442,30 @@ var KNOWN_ANTIPATTERNS = [
7468
8442
  allowlist: ["lib/gate-contract.js"],
7469
8443
  reason: "Extracted across guard-csv / guard-html / guard-svg compliancePosture(name) entry points. Identical 5-line `if (!COMPLIANCE_POSTURES[name]) throw; return Object.assign({}, COMPLIANCE_POSTURES[name])` shape consolidated.",
7470
8444
  },
8445
+ {
8446
+ // Every command / protocol / pipeline guard whose four baseline
8447
+ // postures (hipaa / pci-dss / gdpr / soc2) all map to the bare string
8448
+ // "strict" composes the single frozen gateContract.ALL_STRICT_POSTURES
8449
+ // map instead of re-declaring the literal. The literal was byte-
8450
+ // identical across ~36 guard/safe/mail files (the POP3 / IMAP / SMTP /
8451
+ // ManageSieve command validators, mail-compose / query / sieve / move /
8452
+ // reply, the envelope + event-bus shapes, the mail-pipeline scorers,
8453
+ // the safe-* line-protocol parsers, and mail-crypto-smime) — a genuine
8454
+ // duplicate, not a shape-only coincidence — so it was extracted to a
8455
+ // shared constant in lib/gate-contract.js and the call sites rewritten
8456
+ // to `var COMPLIANCE_POSTURES = gateContract.ALL_STRICT_POSTURES`. The
8457
+ // STRONG-DUP block detector only fires at 3+ files, so a single future
8458
+ // file re-inlining the literal would slip past it; this n=1 inverse
8459
+ // detector catches the re-introduction the moment it lands. The
8460
+ // negative lookaheads exclude the content-guard overlay shape
8461
+ // (`Object.assign({}, PROFILES["strict"], { ... })`), which is a
8462
+ // genuinely per-guard posture map and is NOT centralized.
8463
+ id: "inline-all-strict-postures-map",
8464
+ primitive: "gateContract.ALL_STRICT_POSTURES — the canonical frozen strict-all posture map (hipaa/pci-dss/gdpr/soc2 → \"strict\"); compose it (`var COMPLIANCE_POSTURES = gateContract.ALL_STRICT_POSTURES`) instead of re-declaring the strict-all posture map inline",
8465
+ regex: /COMPLIANCE_POSTURES\s*=\s*Object\.freeze\(\s*\{(?=(?:(?!PROFILES|Object\.assign|COMPLIANCE_POSTURES\s*=)[\s\S]){0,400}?["']?hipaa["']?\s*:\s*["']strict["'])(?=(?:(?!PROFILES|Object\.assign|COMPLIANCE_POSTURES\s*=)[\s\S]){0,400}?["']pci-dss["']\s*:\s*["']strict["'])(?=(?:(?!PROFILES|Object\.assign|COMPLIANCE_POSTURES\s*=)[\s\S]){0,400}?["']?gdpr["']?\s*:\s*["']strict["'])(?=(?:(?!PROFILES|Object\.assign|COMPLIANCE_POSTURES\s*=)[\s\S]){0,400}?["']?soc2["']?\s*:\s*["']strict["'])(?:(?!PROFILES|Object\.assign|COMPLIANCE_POSTURES\s*=)[\s\S]){0,400}?\}\s*\)/,
8466
+ allowlist: ["lib/gate-contract.js"],
8467
+ reason: "The strict-all COMPLIANCE_POSTURES map (hipaa/pci-dss/gdpr/soc2 all → \"strict\") was a byte-identical duplicate across ~36 command/protocol/pipeline guards (guard-pop3/imap/smtp/managesieve-command, guard-mail-compose/query/sieve/move/reply, guard-list-id/list-unsubscribe, guard-event-bus-topic/payload, guard-dsn/envelope/message-id/idempotency-key/jmap/tenant-id/trace-context/saga-config/snapshot-envelope/agent-registry/posture-chain/stream-args, safe-ical/vcard/sieve/icap/dns, mail-helo/scan/rbl/greylist/spam-score, ai-content-detect's posture overlay, and mail-crypto-smime). Extracted to the single frozen gateContract.ALL_STRICT_POSTURES and every call site rewritten to read it by reference — the object is frozen once and shared, never mutated. This inverse detector refuses any re-inlined strict-all literal outside lib/gate-contract.js (the primitive home, allowlisted); the STRONG-DUP block detector would only catch a re-introduction once it reached 3+ files, so the n=1 gate is what makes the extraction durable. Content guards that overlay per-posture byte-limits or redaction flags (CSV / HTML / JSON / XML / YAML / JWT / OAuth / template / ... use `Object.assign({}, PROFILES[\"strict\"], { ... })`) keep their own posture map and are excluded by the negative lookaheads.",
8468
+ },
7471
8469
  {
7472
8470
  id: "inline-rule-pack-loader",
7473
8471
  primitive: "gateContract.makeRulePackLoader(errorClass, codePrefix)",
@@ -7912,8 +8910,48 @@ var KNOWN_ANTIPATTERNS = [
7912
8910
  regex: /\b(FROM|INTO|UPDATE|TABLE|INDEX|TRIGGER|VIEW|JOIN)\s+["']\s*\+\s*(?![qQ][A-Za-z0-9_]|quoted)\w+\s*\+/,
7913
8911
  allowlist: [
7914
8912
  "lib/safe-sql.js", // the helper itself emits quote chars
8913
+ // lib/sql.js IS the b.sql query builder — the canonical composer
8914
+ // that assembles SQL from safeSql-quoted identifiers + bound
8915
+ // placeholders. Its CREATE TABLE / INDEX assembly interpolates a
8916
+ // resolved table reference (ref.ref(dialect), which validates +
8917
+ // quotes via the table() contract) after the keyword-string `ifNot`
8918
+ // ("IF NOT EXISTS "), which the keyword+next-token regex misreads as
8919
+ // a raw identifier. The builder is the thing every other module must
8920
+ // route through, so it's exempt like safe-sql.js itself.
8921
+ "lib/sql.js",
7915
8922
  ],
7916
- reason: "Identifier ALWAYS reaches SQL through safeSql.quoteIdentifier(name, dialect). Validates shape + quotes for the dialect; a future shape-regex bypass can't reach raw concatenation. Local variables holding quoted identifiers use a `q`/`Q`/`quoted` prefix so the detector can skip them.",
8923
+ reason: "Identifier ALWAYS reaches SQL through safeSql.quoteIdentifier(name, dialect). Validates shape + quotes for the dialect; a future shape-regex bypass can't reach raw concatenation. Local variables holding quoted identifiers use a `q`/`Q`/`quoted` prefix so the detector can skip them. lib/sql.js (the b.sql builder) is exempt — it is the composer that assembles SQL from safeSql-quoted parts, so the must-compose rule is satisfied by construction there.",
8924
+ },
8925
+ {
8926
+ id: "framework-table-sql-without-dialect",
8927
+ primitive: "thread the configured backend dialect into every framework-table b.sql call — { dialect: clusterStorage.dialect() }, the module's _sqlOpts() helper, or dbSchema.handleDialect/sqlOpts",
8928
+ skipCommentLines: true,
8929
+ // Inverse detector for the tri-dialect data layer. A framework table —
8930
+ // a "_blamejs_..." literal, a FOO_TABLE constant, or a _fooTable()
8931
+ // table-name helper — addressed through the b.sql builder MUST carry the
8932
+ // configured backend dialect. Omit it and the builder emits the sqlite
8933
+ // default, so the statement parses locally and in the test backend
8934
+ // (both sqlite) but breaks on a Postgres / MySQL deployment — a silent
8935
+ // dialect-default footgun. Every framework module threads the dialect:
8936
+ // inline `{ dialect: ... }`, a module-local `_sqlOpts()` returning
8937
+ // `{ dialect: clusterStorage.dialect() }`, or dbSchema.handleDialect /
8938
+ // dbSchema.sqlOpts. The regex marks the framework-table builder call;
8939
+ // the companion `requires` is satisfied by any threading marker anywhere
8940
+ // in the file.
8941
+ //
8942
+ // Anchors are framework-internal: a "_blamejs_" table literal, an
8943
+ // UPPER_SNAKE *_TABLE constant, or an underscore-prefixed _fooTable()
8944
+ // helper. Operator-supplied names (cli.js `safeTable` on the local
8945
+ // single-node b.db handle) are deliberately NOT matched.
8946
+ //
8947
+ // Per-FILE durability guard: a framework-table b.sql module that threads
8948
+ // NO dialect at all is the drift this catches. Per-CALL precision (one
8949
+ // missed dialect in a file that threads elsewhere) belongs to the
8950
+ // structural/primitive-aware detector tracked for a later cycle.
8951
+ regex: /\bsql(?:\(\))?\.(?:select|insert|insertMany|update|upsert|del|delete|create|createTable|alter|drop|truncate)\s*\(\s*(?:["']_blamejs_|[A-Z][A-Z0-9_]*_TABLE\b|_[a-z][A-Za-z0-9]*Table\s*\()/,
8952
+ requires: /dialect\s*:|sqlOpts\s*\(|handleDialect/,
8953
+ allowlist: [],
8954
+ reason: "v0.15.0 — the data layer is tri-dialect (sqlite / postgres / mysql). A framework-table b.sql call that omits the dialect emits the sqlite default and breaks on a Postgres/MySQL backend, silently, because the local default and the test backend are both sqlite. Every framework module threads it (inline `{ dialect: clusterStorage.dialect() }`, a `_sqlOpts()` helper, or dbSchema.handleDialect / dbSchema.sqlOpts); the requires-marker confirms the threading is present. Locks in the tri-dialect data layer so a newly-added framework table cannot silently default to sqlite.",
7917
8955
  },
7918
8956
  {
7919
8957
  id: "inline-optional-plain-object-validation",
@@ -8407,6 +9445,22 @@ var KNOWN_ANTIPATTERNS = [
8407
9445
  reason: "Hardcoded bind ports race under SMOKE_PARALLEL=64 when two parallel tests pick the same value. Convention: .listen(0) + server.address().port. Read-only protocol-constant references (autoconfig XML port: 993 / 587, mock-server config port: 1025) don't trip this detector — only .listen() with a literal non-zero port does.",
8408
9446
  },
8409
9447
 
9448
+ {
9449
+ // Constructing a "malformed" base64url test input by replacing only the
9450
+ // FIRST standard-base64-only character ('+' or '/') of a freshly generated
9451
+ // certificate/key is non-deterministic: the per-run base64 carries no such
9452
+ // character ~0.4% of the time (a 400-char cert), so the replace is a no-op
9453
+ // and the input stays a VALID value that is correctly accepted — flaking
9454
+ // any assertion that expects refusal. Inject a base64url-only char
9455
+ // unconditionally (prepend one) so the malformed entry is guaranteed.
9456
+ id: "test-malformed-base64url-via-noop-replace",
9457
+ primitive: "prepend a base64url-only char ('-' / '_') unconditionally to build a guaranteed-malformed x5c / JOSE base64 test input — never a single non-global replace of the first '+' / '/', which is a no-op when the input carries neither",
9458
+ scanScope: "test",
9459
+ regex: /\.replace\(\s*\/\[\+\/\]\/\s*,\s*["'][-_]["']\s*\)/,
9460
+ allowlist: [],
9461
+ reason: "A single non-global replace of the first standard-base64-only character to forge a base64url-charset string is a no-op whenever that run's base64 happens to carry no such character, leaving a still-valid input that is correctly accepted — so the refusal assertion flakes (measured ~0.4% per run on a 400-char certificate; surfaced as the OID4VCI base64url-x5c refusal flake). Build the malformed entry deterministically by prepending a base64url-only char.",
9462
+ },
9463
+
8410
9464
  {
8411
9465
  // v0.11.13 — `fs.watchFile` / `fs.watch` MUST NOT be called
8412
9466
  // directly from tests. The framework exposes `b.watcher`
@@ -8443,6 +9497,29 @@ var KNOWN_ANTIPATTERNS = [
8443
9497
  reason: "v0.11.13 — every recurring flake in the fs.watch test class (vault-seal-pem-file + watcher) shared the same root cause: the test wrote a file with a future mtime expecting the watcher's first poll to detect the change, but the first poll could land AFTER the mutation under runner contention. helpers.backdateFile establishes an unambiguously-older baseline; pairing it with future-mtime writes makes the watcher's transition detection deterministic.",
8444
9498
  },
8445
9499
 
9500
+ {
9501
+ // v0.15.0 — an encrypted-at-rest b.db.init refuses a tmpDir that is not a
9502
+ // recognized tmpfs mount (/dev/shm, /run/shm, /run/user, /tmp): a decrypted
9503
+ // working copy on persistent disk leaks into backup snapshots, replicas,
9504
+ // and forensic images. That gate is a NO-OP on win32, so a test that builds
9505
+ // its scratch dir under the repo-local test/.test-output and passes it to a
9506
+ // BESPOKE db.init PASSES on the author's Windows host and FAILS on the
9507
+ // Linux/macOS CI floor with db/tmpdir-not-tmpfs (audit-checkpoint-false-
9508
+ // rollback shipped this exact bug). The shared setupTestDb / setupTestDbForMW
9509
+ // helpers already pass allowNonTmpfsTmpDir:true; a bespoke db.init must opt in
9510
+ // the same way, OR base its scratch on os.tmpdir() (/tmp on Linux is a
9511
+ // recognized tmpfs), OR run atRest:"plain" (no decrypted working copy, so the
9512
+ // gate does not apply). The companion `requires` is satisfied by any one.
9513
+ id: "test-bespoke-db-init-nontmpfs-tmpdir",
9514
+ primitive: "a bespoke b.db.init with a tmpDir must pass allowNonTmpfsTmpDir:true (as setupTestDb does), base the scratch on os.tmpdir(), or run atRest:\"plain\" — so the encrypted-at-rest non-tmpfs gate does not fail it on the Linux/macOS CI floor",
9515
+ scanScope: "test",
9516
+ skipCommentLines: true,
9517
+ regex: /\bb\.db\.init\s*\([\s\S]{0,400}?\btmpDir\s*:/,
9518
+ requires: /allowNonTmpfsTmpDir|atRest\s*:\s*["']plain["']|os\.tmpdir\s*\(/,
9519
+ allowlist: [],
9520
+ reason: "v0.15.0 — the encrypted-at-rest db.init disk-residency gate refuses a non-tmpfs tmpDir on Linux/macOS but is a no-op on win32, so a bespoke db.init with a repo-local (.test-output) scratch dir passes on Windows and fails on CI. setupTestDb / setupTestDbForMW already pass allowNonTmpfsTmpDir:true; a bespoke db.init must do the same, base its scratch on os.tmpdir() (/tmp = recognized tmpfs), or run atRest:\"plain\". The requires-marker confirms one mitigation is present in the file.",
9521
+ },
9522
+
8446
9523
  {
8447
9524
  // N3 (v0.10.14) — tests creating a real DB handle without an
8448
9525
  // isolation primitive. Any test file calling `b.db.create(` MUST
@@ -10875,11 +11952,14 @@ function testCompliancePostureCoverage() {
10875
11952
  // examples/wiki/Dockerfile, the workflow's `-p host:container` map +
10876
11953
  // curl host MUST also reference X.
10877
11954
  // Internal working notes (planning documents, scratch output, session
10878
- // residue) live outside the repository — a tracked file under memory/,
10879
- // notes/, or a .scratch* path ships internal planning narrative to
10880
- // everyone who clones the repo. v0.14.22 removed the one such file that
10881
- // had been committed (a migration planning note, added v0.11.2); this
10882
- // gate refuses any recurrence at commit time instead of at code review.
11955
+ // residue) and editor/tool atomic-write temp artifacts
11956
+ // (<name>.tmp.<pid>.<hash>) live outside the repository a tracked
11957
+ // file under memory/, notes/, a .scratch* path, or a *.tmp.* name
11958
+ // ships internal residue to everyone who clones the repo, and tmp
11959
+ // copies under lib/ ship in the npm tarball (`files` publishes lib/
11960
+ // wholesale). v0.14.22 removed a committed planning note; v0.14.23
11961
+ // caught four committed editor temp copies pre-merge. This gate
11962
+ // refuses any recurrence at commit time instead of at code review.
10883
11963
  function testNoTrackedInternalNotes() {
10884
11964
  var out;
10885
11965
  try {
@@ -10887,18 +11967,41 @@ function testNoTrackedInternalNotes() {
10887
11967
  // file — child_process is only touched on the two paths that talk
10888
11968
  // to the host (re-exec + this git query).
10889
11969
  out = require("node:child_process").execFileSync(
10890
- "git", ["ls-files", "memory", "notes", ".scratch", ".scratch-*"],
11970
+ "git", ["ls-files", "memory", "notes", ".scratch", ".scratch-*", "*.tmp.*"],
10891
11971
  { stdio: ["ignore", "pipe", "ignore"] }
10892
11972
  ).toString().trim();
10893
11973
  } catch (_e) {
10894
11974
  // Not a git checkout (npm tarball / exported tree) — nothing to gate.
10895
11975
  return;
10896
11976
  }
10897
- check("no tracked internal-notes files (memory/ notes/ .scratch*)" +
11977
+ check("no tracked internal-notes or temp-artifact files (memory/ notes/ .scratch* *.tmp.*)" +
10898
11978
  (out ? " — found: " + out.split("\n").join(", ") : ""),
10899
11979
  out === "");
10900
11980
  }
10901
11981
 
11982
+ // The residency write gates exist only if they're actually wired —
11983
+ // declareColumnResidency/assertColumnResidency shipped in v0.7.27
11984
+ // advertising a write-time gate that no write path called for 7 minor
11985
+ // versions. This gate pins the wiring: the local write methods run
11986
+ // _assertLocalResidency, the external query/transaction paths run
11987
+ // _assertRowResidency, and assertColumnResidency has a real lib/
11988
+ // caller outside its own definition file.
11989
+ function testResidencyGatesWired() {
11990
+ var dbq, edb;
11991
+ try {
11992
+ dbq = fs.readFileSync("lib/db-query.js", "utf8");
11993
+ edb = fs.readFileSync("lib/external-db.js", "utf8");
11994
+ } catch (_e) { return; }
11995
+ var localCalls = (dbq.match(/_assertLocalResidency\(this\._cryptoFieldKey\(\)/g) || []).length;
11996
+ check("db-query wires the local residency gate on insert AND update", localCalls >= 2);
11997
+ check("db-query wires the long-advertised assertColumnResidency",
11998
+ dbq.indexOf("assertColumnResidency(") !== -1);
11999
+ var extCalls = (edb.match(/_assertRowResidency\(sql,/g) || []).length;
12000
+ check("external-db wires the row residency gate on query AND transaction", extCalls >= 2);
12001
+ check("external-db replica reads honor the row tag",
12002
+ edb.indexOf("REPLICA_RESIDENCY_INCOMPATIBLE") !== -1);
12003
+ }
12004
+
10902
12005
  function testWikiPortAgreesAcrossArtifacts() {
10903
12006
  var bad = [];
10904
12007
  var dockerfile;
@@ -11773,6 +12876,8 @@ async function run() {
11773
12876
  testSafeGuardWiredInIndex();
11774
12877
  testSafeGuardHasMustComposeDetector();
11775
12878
  testNoTierTerminologyInLib();
12879
+ testNoHandRolledSql();
12880
+ testNoHardcodedFrameworkFileNames();
11776
12881
  testNoInlineRequires();
11777
12882
  testRequireBindingConsistency();
11778
12883
  testRequireBlockAlignment();
@@ -11895,6 +13000,7 @@ async function run() {
11895
13000
  // step's port mapping + curl host.
11896
13001
  testWikiPortAgreesAcrossArtifacts();
11897
13002
  testNoTrackedInternalNotes();
13003
+ testResidencyGatesWired();
11898
13004
  testWikiStopGraceExceedsShutdownBudget();
11899
13005
  testOrchestratorRegistryReadsTenantScoped();
11900
13006
  testErrorCodesNamespacedKebab();