@blamejs/blamejs-shop 0.5.12 → 0.5.13

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 (521) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/vendor/MANIFEST.json +632 -416
  4. package/lib/vendor/blamejs/.clusterfuzzlite/Dockerfile +21 -10
  5. package/lib/vendor/blamejs/.clusterfuzzlite/build.sh +30 -9
  6. package/lib/vendor/blamejs/.github/dependabot.yml +8 -0
  7. package/lib/vendor/blamejs/.github/workflows/actions-lint.yml +3 -3
  8. package/lib/vendor/blamejs/.github/workflows/cflite_batch.yml +7 -2
  9. package/lib/vendor/blamejs/.github/workflows/cflite_pr.yml +7 -2
  10. package/lib/vendor/blamejs/.github/workflows/ci.yml +21 -21
  11. package/lib/vendor/blamejs/.github/workflows/codeql.yml +3 -3
  12. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +4 -4
  13. package/lib/vendor/blamejs/.github/workflows/release-container.yml +7 -7
  14. package/lib/vendor/blamejs/.github/workflows/scorecard.yml +3 -3
  15. package/lib/vendor/blamejs/.github/workflows/sha-to-tag-verify.yml +1 -1
  16. package/lib/vendor/blamejs/.gitignore +2 -0
  17. package/lib/vendor/blamejs/CHANGELOG.md +122 -0
  18. package/lib/vendor/blamejs/NOTICE +26 -1
  19. package/lib/vendor/blamejs/SECURITY.md +1 -1
  20. package/lib/vendor/blamejs/api-snapshot.json +207 -5
  21. package/lib/vendor/blamejs/docker/keycloak/realm-blamejs-test.json +29 -0
  22. package/lib/vendor/blamejs/examples/wiki/DEPLOY.md +1 -0
  23. package/lib/vendor/blamejs/examples/wiki/lib/build-app.js +31 -0
  24. package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +109 -0
  25. package/lib/vendor/blamejs/examples/wiki/nginx.conf +106 -0
  26. package/lib/vendor/blamejs/examples/wiki/package-lock.json +1 -1
  27. package/lib/vendor/blamejs/examples/wiki/test/e2e.js +92 -0
  28. package/lib/vendor/blamejs/fuzz/asn1-der.fuzz.js +28 -0
  29. package/lib/vendor/blamejs/fuzz/cms-codec.fuzz.js +17 -0
  30. package/lib/vendor/blamejs/fuzz/link-header.fuzz.js +18 -0
  31. package/lib/vendor/blamejs/index.js +6 -0
  32. package/lib/vendor/blamejs/lib/a2a.js +4 -4
  33. package/lib/vendor/blamejs/lib/agent-idempotency.js +18 -2
  34. package/lib/vendor/blamejs/lib/agent-saga.js +36 -8
  35. package/lib/vendor/blamejs/lib/agent-snapshot.js +63 -6
  36. package/lib/vendor/blamejs/lib/app-shutdown.js +23 -20
  37. package/lib/vendor/blamejs/lib/archive-read.js +8 -0
  38. package/lib/vendor/blamejs/lib/archive-tar.js +12 -2
  39. package/lib/vendor/blamejs/lib/arg-parser.js +49 -5
  40. package/lib/vendor/blamejs/lib/asn1-der.js +12 -0
  41. package/lib/vendor/blamejs/lib/audit-sign.js +47 -1
  42. package/lib/vendor/blamejs/lib/audit.js +71 -8
  43. package/lib/vendor/blamejs/lib/auth/ciba.js +14 -2
  44. package/lib/vendor/blamejs/lib/auth/fido-mds3.js +58 -47
  45. package/lib/vendor/blamejs/lib/auth/jwt.js +34 -0
  46. package/lib/vendor/blamejs/lib/auth/oauth.js +90 -13
  47. package/lib/vendor/blamejs/lib/auth/oid4vci.js +20 -17
  48. package/lib/vendor/blamejs/lib/auth/openid-federation.js +214 -23
  49. package/lib/vendor/blamejs/lib/auth/saml.js +38 -20
  50. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +10 -4
  51. package/lib/vendor/blamejs/lib/backup/bundle.js +13 -3
  52. package/lib/vendor/blamejs/lib/backup/index.js +16 -5
  53. package/lib/vendor/blamejs/lib/backup/manifest.js +18 -5
  54. package/lib/vendor/blamejs/lib/base32.js +21 -0
  55. package/lib/vendor/blamejs/lib/break-glass.js +19 -10
  56. package/lib/vendor/blamejs/lib/bundler.js +5 -1
  57. package/lib/vendor/blamejs/lib/cache-status.js +3 -7
  58. package/lib/vendor/blamejs/lib/calendar.js +21 -1
  59. package/lib/vendor/blamejs/lib/cbor.js +12 -1
  60. package/lib/vendor/blamejs/lib/cert.js +29 -11
  61. package/lib/vendor/blamejs/lib/cli.js +94 -24
  62. package/lib/vendor/blamejs/lib/cloud-events.js +2 -2
  63. package/lib/vendor/blamejs/lib/cluster-provider-db.js +13 -3
  64. package/lib/vendor/blamejs/lib/codepoint-class.js +85 -0
  65. package/lib/vendor/blamejs/lib/compliance-ai-act-prohibited.js +8 -4
  66. package/lib/vendor/blamejs/lib/compliance-ai-act-transparency.js +19 -4
  67. package/lib/vendor/blamejs/lib/compliance-ai-act.js +6 -8
  68. package/lib/vendor/blamejs/lib/content-credentials.js +1 -1
  69. package/lib/vendor/blamejs/lib/content-digest.js +2 -1
  70. package/lib/vendor/blamejs/lib/cookies.js +13 -3
  71. package/lib/vendor/blamejs/lib/cose.js +31 -16
  72. package/lib/vendor/blamejs/lib/crypto-field.js +32 -0
  73. package/lib/vendor/blamejs/lib/crypto-oprf.js +11 -2
  74. package/lib/vendor/blamejs/lib/crypto-xwing.js +22 -4
  75. package/lib/vendor/blamejs/lib/crypto.js +314 -19
  76. package/lib/vendor/blamejs/lib/csp.js +11 -3
  77. package/lib/vendor/blamejs/lib/daemon.js +270 -24
  78. package/lib/vendor/blamejs/lib/dark-patterns.js +5 -2
  79. package/lib/vendor/blamejs/lib/db-declare-view.js +2 -2
  80. package/lib/vendor/blamejs/lib/db-query.js +34 -7
  81. package/lib/vendor/blamejs/lib/db.js +18 -6
  82. package/lib/vendor/blamejs/lib/dbsc.js +17 -0
  83. package/lib/vendor/blamejs/lib/dev.js +21 -2
  84. package/lib/vendor/blamejs/lib/dsr.js +157 -102
  85. package/lib/vendor/blamejs/lib/external-db.js +41 -2
  86. package/lib/vendor/blamejs/lib/file-upload.js +25 -7
  87. package/lib/vendor/blamejs/lib/flag-providers.js +27 -15
  88. package/lib/vendor/blamejs/lib/forms.js +113 -27
  89. package/lib/vendor/blamejs/lib/gate-contract.js +23 -10
  90. package/lib/vendor/blamejs/lib/gdpr-ropa.js +8 -1
  91. package/lib/vendor/blamejs/lib/guard-agent-registry.js +1 -1
  92. package/lib/vendor/blamejs/lib/guard-archive.js +2 -2
  93. package/lib/vendor/blamejs/lib/guard-cidr.js +27 -1
  94. package/lib/vendor/blamejs/lib/guard-graphql.js +30 -11
  95. package/lib/vendor/blamejs/lib/guard-html.js +26 -53
  96. package/lib/vendor/blamejs/lib/guard-image.js +24 -5
  97. package/lib/vendor/blamejs/lib/guard-imap-command.js +10 -2
  98. package/lib/vendor/blamejs/lib/guard-jmap.js +19 -3
  99. package/lib/vendor/blamejs/lib/guard-jwt.js +18 -2
  100. package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +2 -2
  101. package/lib/vendor/blamejs/lib/guard-managesieve-command.js +10 -2
  102. package/lib/vendor/blamejs/lib/guard-markdown.js +11 -24
  103. package/lib/vendor/blamejs/lib/guard-pdf.js +18 -2
  104. package/lib/vendor/blamejs/lib/guard-pop3-command.js +10 -2
  105. package/lib/vendor/blamejs/lib/guard-regex.js +8 -5
  106. package/lib/vendor/blamejs/lib/guard-svg.js +31 -32
  107. package/lib/vendor/blamejs/lib/guard-time.js +1 -1
  108. package/lib/vendor/blamejs/lib/guard-uuid.js +2 -2
  109. package/lib/vendor/blamejs/lib/html-balance.js +3 -3
  110. package/lib/vendor/blamejs/lib/http-client-cache.js +82 -4
  111. package/lib/vendor/blamejs/lib/http-client-cookie-jar.js +16 -1
  112. package/lib/vendor/blamejs/lib/http-client.js +39 -7
  113. package/lib/vendor/blamejs/lib/http-message-signature.js +35 -1
  114. package/lib/vendor/blamejs/lib/i18n-messageformat.js +31 -6
  115. package/lib/vendor/blamejs/lib/i18n.js +35 -21
  116. package/lib/vendor/blamejs/lib/incident-report.js +8 -1
  117. package/lib/vendor/blamejs/lib/ip-utils.js +11 -1
  118. package/lib/vendor/blamejs/lib/json-path.js +23 -1
  119. package/lib/vendor/blamejs/lib/jwk.js +20 -3
  120. package/lib/vendor/blamejs/lib/link-header.js +2 -1
  121. package/lib/vendor/blamejs/lib/log-stream-otlp-grpc.js +5 -1
  122. package/lib/vendor/blamejs/lib/log-stream-otlp.js +5 -1
  123. package/lib/vendor/blamejs/lib/log-stream.js +8 -3
  124. package/lib/vendor/blamejs/lib/mail-auth.js +123 -22
  125. package/lib/vendor/blamejs/lib/mail-bimi.js +1 -1
  126. package/lib/vendor/blamejs/lib/mail-bounce.js +8 -0
  127. package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +46 -10
  128. package/lib/vendor/blamejs/lib/mail-dav.js +22 -3
  129. package/lib/vendor/blamejs/lib/mail-dkim.js +96 -30
  130. package/lib/vendor/blamejs/lib/mail-helo.js +6 -1
  131. package/lib/vendor/blamejs/lib/mail-scan.js +23 -16
  132. package/lib/vendor/blamejs/lib/mail-send-deliver.js +34 -8
  133. package/lib/vendor/blamejs/lib/mail-server-imap.js +3 -4
  134. package/lib/vendor/blamejs/lib/mail-server-managesieve.js +7 -11
  135. package/lib/vendor/blamejs/lib/mail-server-mx.js +93 -7
  136. package/lib/vendor/blamejs/lib/mail-server-registry.js +8 -2
  137. package/lib/vendor/blamejs/lib/mail.js +15 -5
  138. package/lib/vendor/blamejs/lib/metrics.js +268 -63
  139. package/lib/vendor/blamejs/lib/middleware/age-gate.js +7 -1
  140. package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +1 -1
  141. package/lib/vendor/blamejs/lib/middleware/body-parser.js +19 -4
  142. package/lib/vendor/blamejs/lib/middleware/bot-guard.js +5 -9
  143. package/lib/vendor/blamejs/lib/middleware/csp-report.js +40 -8
  144. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +24 -10
  145. package/lib/vendor/blamejs/lib/middleware/dpop.js +10 -5
  146. package/lib/vendor/blamejs/lib/middleware/index.js +6 -1
  147. package/lib/vendor/blamejs/lib/middleware/nel.js +1 -1
  148. package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +1 -1
  149. package/lib/vendor/blamejs/lib/middleware/rate-limit.js +12 -1
  150. package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +1 -1
  151. package/lib/vendor/blamejs/lib/middleware/scim-server.js +1 -1
  152. package/lib/vendor/blamejs/lib/middleware/tus-upload.js +6 -2
  153. package/lib/vendor/blamejs/lib/mtls-ca.js +18 -2
  154. package/lib/vendor/blamejs/lib/network-dns-resolver.js +11 -1
  155. package/lib/vendor/blamejs/lib/network-dns.js +21 -2
  156. package/lib/vendor/blamejs/lib/network-tls.js +69 -7
  157. package/lib/vendor/blamejs/lib/nonce-store.js +9 -1
  158. package/lib/vendor/blamejs/lib/object-store/gcs.js +7 -0
  159. package/lib/vendor/blamejs/lib/object-store/sigv4.js +31 -0
  160. package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +1 -1
  161. package/lib/vendor/blamejs/lib/outbox.js +1 -1
  162. package/lib/vendor/blamejs/lib/parsers/safe-ini.js +9 -1
  163. package/lib/vendor/blamejs/lib/parsers/safe-toml.js +10 -1
  164. package/lib/vendor/blamejs/lib/permissions.js +6 -3
  165. package/lib/vendor/blamejs/lib/pid-probe.js +55 -0
  166. package/lib/vendor/blamejs/lib/pqc-agent.js +8 -1
  167. package/lib/vendor/blamejs/lib/pqc-software.js +3 -3
  168. package/lib/vendor/blamejs/lib/pubsub.js +20 -9
  169. package/lib/vendor/blamejs/lib/queue-local.js +8 -1
  170. package/lib/vendor/blamejs/lib/queue-redis.js +5 -0
  171. package/lib/vendor/blamejs/lib/redact.js +54 -0
  172. package/lib/vendor/blamejs/lib/restore-bundle.js +10 -2
  173. package/lib/vendor/blamejs/lib/restore.js +19 -0
  174. package/lib/vendor/blamejs/lib/retention.js +12 -4
  175. package/lib/vendor/blamejs/lib/router.js +60 -6
  176. package/lib/vendor/blamejs/lib/safe-buffer.js +69 -0
  177. package/lib/vendor/blamejs/lib/safe-decompress.js +8 -2
  178. package/lib/vendor/blamejs/lib/safe-dns.js +25 -20
  179. package/lib/vendor/blamejs/lib/safe-ical.js +7 -3
  180. package/lib/vendor/blamejs/lib/safe-mime.js +12 -1
  181. package/lib/vendor/blamejs/lib/safe-object.js +80 -0
  182. package/lib/vendor/blamejs/lib/safe-path.js +52 -4
  183. package/lib/vendor/blamejs/lib/safe-redirect.js +10 -2
  184. package/lib/vendor/blamejs/lib/safe-schema.js +4 -1
  185. package/lib/vendor/blamejs/lib/safe-sieve.js +33 -1
  186. package/lib/vendor/blamejs/lib/safe-sql.js +88 -0
  187. package/lib/vendor/blamejs/lib/safe-vcard.js +7 -3
  188. package/lib/vendor/blamejs/lib/scheduler.js +8 -1
  189. package/lib/vendor/blamejs/lib/security-assert.js +6 -6
  190. package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +74 -27
  191. package/lib/vendor/blamejs/lib/self-update.js +517 -87
  192. package/lib/vendor/blamejs/lib/server-timing.js +2 -4
  193. package/lib/vendor/blamejs/lib/session.js +34 -12
  194. package/lib/vendor/blamejs/lib/sql.js +15 -1
  195. package/lib/vendor/blamejs/lib/ssrf-guard.js +52 -0
  196. package/lib/vendor/blamejs/lib/static.js +55 -13
  197. package/lib/vendor/blamejs/lib/storage.js +8 -2
  198. package/lib/vendor/blamejs/lib/subject.js +6 -1
  199. package/lib/vendor/blamejs/lib/tenant-quota.js +52 -13
  200. package/lib/vendor/blamejs/lib/tls-exporter.js +3 -3
  201. package/lib/vendor/blamejs/lib/tsa.js +34 -13
  202. package/lib/vendor/blamejs/lib/uri-template.js +8 -1
  203. package/lib/vendor/blamejs/lib/vault/index.js +13 -0
  204. package/lib/vendor/blamejs/lib/vc.js +44 -5
  205. package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +26 -26
  206. package/lib/vendor/blamejs/lib/vendor/noble-ciphers.cjs +792 -1
  207. package/lib/vendor/blamejs/lib/vendor/noble-curves.cjs +3443 -1
  208. package/lib/vendor/blamejs/lib/vendor/noble-post-quantum.cjs +2737 -1
  209. package/lib/vendor/blamejs/lib/vendor/pki.cjs +39550 -13
  210. package/lib/vendor/blamejs/lib/vendor/public-suffix-list.dat +55 -53
  211. package/lib/vendor/blamejs/lib/vendor/public-suffix-list.data.js +5551 -5554
  212. package/lib/vendor/blamejs/lib/vendor/simplewebauthn-server.cjs +22459 -32
  213. package/lib/vendor/blamejs/lib/watcher.js +89 -17
  214. package/lib/vendor/blamejs/lib/webhook-dispatcher.js +25 -3
  215. package/lib/vendor/blamejs/lib/webhook.js +12 -2
  216. package/lib/vendor/blamejs/lib/ws-client.js +32 -1
  217. package/lib/vendor/blamejs/lib/xml-c14n.js +29 -1
  218. package/lib/vendor/blamejs/oss-fuzz/projects/blamejs/Dockerfile +1 -1
  219. package/lib/vendor/blamejs/package-lock.json +2 -2
  220. package/lib/vendor/blamejs/package.json +1 -1
  221. package/lib/vendor/blamejs/release-notes/v0.16.x.json +1462 -0
  222. package/lib/vendor/blamejs/release-notes/v0.17.0.json +31 -0
  223. package/lib/vendor/blamejs/release-notes/v0.17.1.json +31 -0
  224. package/lib/vendor/blamejs/release-notes/v0.17.10.json +30 -0
  225. package/lib/vendor/blamejs/release-notes/v0.17.11.json +22 -0
  226. package/lib/vendor/blamejs/release-notes/v0.17.12.json +18 -0
  227. package/lib/vendor/blamejs/release-notes/v0.17.13.json +77 -0
  228. package/lib/vendor/blamejs/release-notes/v0.17.14.json +40 -0
  229. package/lib/vendor/blamejs/release-notes/v0.17.15.json +18 -0
  230. package/lib/vendor/blamejs/release-notes/v0.17.16.json +27 -0
  231. package/lib/vendor/blamejs/release-notes/v0.17.17.json +31 -0
  232. package/lib/vendor/blamejs/release-notes/v0.17.18.json +18 -0
  233. package/lib/vendor/blamejs/release-notes/v0.17.19.json +18 -0
  234. package/lib/vendor/blamejs/release-notes/v0.17.2.json +26 -0
  235. package/lib/vendor/blamejs/release-notes/v0.17.20.json +43 -0
  236. package/lib/vendor/blamejs/release-notes/v0.17.21.json +26 -0
  237. package/lib/vendor/blamejs/release-notes/v0.17.22.json +18 -0
  238. package/lib/vendor/blamejs/release-notes/v0.17.3.json +43 -0
  239. package/lib/vendor/blamejs/release-notes/v0.17.4.json +35 -0
  240. package/lib/vendor/blamejs/release-notes/v0.17.5.json +26 -0
  241. package/lib/vendor/blamejs/release-notes/v0.17.6.json +26 -0
  242. package/lib/vendor/blamejs/release-notes/v0.17.7.json +35 -0
  243. package/lib/vendor/blamejs/release-notes/v0.17.8.json +22 -0
  244. package/lib/vendor/blamejs/release-notes/v0.17.9.json +26 -0
  245. package/lib/vendor/blamejs/scripts/validate-source-comment-blocks.js +4 -0
  246. package/lib/vendor/blamejs/scripts/vendor-update.sh +293 -9
  247. package/lib/vendor/blamejs/socket.yml +94 -0
  248. package/lib/vendor/blamejs/test/00-primitives.js +605 -0
  249. package/lib/vendor/blamejs/test/helpers/drivers.js +12 -9
  250. package/lib/vendor/blamejs/test/integration/audit-stack-mysql.test.js +90 -1
  251. package/lib/vendor/blamejs/test/integration/audit-stack-postgres.test.js +122 -1
  252. package/lib/vendor/blamejs/test/integration/data-layer-mysql.test.js +30 -0
  253. package/lib/vendor/blamejs/test/integration/data-layer-pg.test.js +29 -0
  254. package/lib/vendor/blamejs/test/integration/dsr-cluster-mysql.test.js +479 -0
  255. package/lib/vendor/blamejs/test/integration/dsr-cluster-pg.test.js +487 -0
  256. package/lib/vendor/blamejs/test/integration/federation-auth.test.js +313 -5
  257. package/lib/vendor/blamejs/test/integration/mail-dkim.test.js +80 -0
  258. package/lib/vendor/blamejs/test/integration/openid-federation-chain.test.js +275 -0
  259. package/lib/vendor/blamejs/test/integration/webhook-dispatcher-pg.test.js +96 -0
  260. package/lib/vendor/blamejs/test/layer-0-primitives/a2a-tasks.test.js +401 -0
  261. package/lib/vendor/blamejs/test/layer-0-primitives/acme.test.js +1649 -2
  262. package/lib/vendor/blamejs/test/layer-0-primitives/age-gate.test.js +33 -0
  263. package/lib/vendor/blamejs/test/layer-0-primitives/agent-idempotency.test.js +0 -0
  264. package/lib/vendor/blamejs/test/layer-0-primitives/agent-saga.test.js +60 -0
  265. package/lib/vendor/blamejs/test/layer-0-primitives/agent-snapshot.test.js +524 -0
  266. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +891 -0
  267. package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +128 -1
  268. package/lib/vendor/blamejs/test/layer-0-primitives/archive-adapters.test.js +271 -0
  269. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +628 -1
  270. package/lib/vendor/blamejs/test/layer-0-primitives/archive-tar-read.test.js +0 -0
  271. package/lib/vendor/blamejs/test/layer-0-primitives/archive-wrap.test.js +227 -0
  272. package/lib/vendor/blamejs/test/layer-0-primitives/arg-parser.test.js +15 -0
  273. package/lib/vendor/blamejs/test/layer-0-primitives/asn1-der.test.js +73 -0
  274. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file.test.js +114 -0
  275. package/lib/vendor/blamejs/test/layer-0-primitives/audit-sign-anchor.test.js +141 -0
  276. package/lib/vendor/blamejs/test/layer-0-primitives/audit-tools.test.js +624 -0
  277. package/lib/vendor/blamejs/test/layer-0-primitives/audit.test.js +769 -0
  278. package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +436 -0
  279. package/lib/vendor/blamejs/test/layer-0-primitives/auth-oauth.test.js +1697 -0
  280. package/lib/vendor/blamejs/test/layer-0-primitives/auth-oid4vp.test.js +495 -0
  281. package/lib/vendor/blamejs/test/layer-0-primitives/auth-password.test.js +500 -0
  282. package/lib/vendor/blamejs/test/layer-0-primitives/auth-saml.test.js +1809 -0
  283. package/lib/vendor/blamejs/test/layer-0-primitives/auth-status-list.test.js +243 -1
  284. package/lib/vendor/blamejs/test/layer-0-primitives/backup-index.test.js +2226 -0
  285. package/lib/vendor/blamejs/test/layer-0-primitives/backup-manifest-signature.test.js +183 -0
  286. package/lib/vendor/blamejs/test/layer-0-primitives/base32.test.js +105 -0
  287. package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +693 -0
  288. package/lib/vendor/blamejs/test/layer-0-primitives/cache.test.js +603 -0
  289. package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +557 -0
  290. package/lib/vendor/blamejs/test/layer-0-primitives/case-insensitive-security-token-guard.test.js +214 -0
  291. package/lib/vendor/blamejs/test/layer-0-primitives/cbor.test.js +25 -0
  292. package/lib/vendor/blamejs/test/layer-0-primitives/cert.test.js +843 -6
  293. package/lib/vendor/blamejs/test/layer-0-primitives/ciba-authreqid-binding.test.js +77 -2
  294. package/lib/vendor/blamejs/test/layer-0-primitives/ciba.test.js +866 -0
  295. package/lib/vendor/blamejs/test/layer-0-primitives/clear-site-data.test.js +18 -0
  296. package/lib/vendor/blamejs/test/layer-0-primitives/cli-erase.test.js +19 -0
  297. package/lib/vendor/blamejs/test/layer-0-primitives/cli.test.js +2790 -0
  298. package/lib/vendor/blamejs/test/layer-0-primitives/cluster-lease-renewal-ttl.test.js +114 -0
  299. package/lib/vendor/blamejs/test/layer-0-primitives/cluster.test.js +1032 -0
  300. package/lib/vendor/blamejs/test/layer-0-primitives/cms-codec.test.js +575 -0
  301. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +739 -1
  302. package/lib/vendor/blamejs/test/layer-0-primitives/codepoint-class.test.js +8 -0
  303. package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +194 -0
  304. package/lib/vendor/blamejs/test/layer-0-primitives/compliance-lifecycle-proto-key.test.js +104 -0
  305. package/lib/vendor/blamejs/test/layer-0-primitives/compliance-sanctions.test.js +254 -0
  306. package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials.test.js +613 -3
  307. package/lib/vendor/blamejs/test/layer-0-primitives/cookies.test.js +140 -0
  308. package/lib/vendor/blamejs/test/layer-0-primitives/cose.test.js +407 -0
  309. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-adversarial.test.js +352 -0
  310. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +32 -0
  311. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field.test.js +575 -0
  312. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-hash-files-parallel.test.js +10 -0
  313. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-hash-stream.test.js +24 -0
  314. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-oprf.test.js +17 -0
  315. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-spki-pin.test.js +183 -0
  316. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-xwing.test.js +48 -0
  317. package/lib/vendor/blamejs/test/layer-0-primitives/crypto.test.js +345 -0
  318. package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +58 -0
  319. package/lib/vendor/blamejs/test/layer-0-primitives/csv.test.js +48 -0
  320. package/lib/vendor/blamejs/test/layer-0-primitives/daemon.test.js +459 -1
  321. package/lib/vendor/blamejs/test/layer-0-primitives/dark-patterns.test.js +19 -0
  322. package/lib/vendor/blamejs/test/layer-0-primitives/db-query.test.js +1019 -0
  323. package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +77 -0
  324. package/lib/vendor/blamejs/test/layer-0-primitives/db.test.js +1621 -0
  325. package/lib/vendor/blamejs/test/layer-0-primitives/ddl-change-control.test.js +462 -3
  326. package/lib/vendor/blamejs/test/layer-0-primitives/dnssec.test.js +323 -0
  327. package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-verify.test.js +562 -0
  328. package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +901 -1
  329. package/lib/vendor/blamejs/test/layer-0-primitives/dual-control.test.js +249 -0
  330. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-migrate.test.js +561 -0
  331. package/lib/vendor/blamejs/test/layer-0-primitives/external-db.test.js +1214 -0
  332. package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +21 -0
  333. package/lib/vendor/blamejs/test/layer-0-primitives/fedcm-dbsc.test.js +46 -20
  334. package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +147 -3
  335. package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3.test.js +907 -38
  336. package/lib/vendor/blamejs/test/layer-0-primitives/file-upload-content-safety-skip-audit.test.js +29 -0
  337. package/lib/vendor/blamejs/test/layer-0-primitives/file-upload.test.js +1135 -0
  338. package/lib/vendor/blamejs/test/layer-0-primitives/flag.test.js +30 -0
  339. package/lib/vendor/blamejs/test/layer-0-primitives/forms.test.js +747 -0
  340. package/lib/vendor/blamejs/test/layer-0-primitives/gate-contract.test.js +1855 -0
  341. package/lib/vendor/blamejs/test/layer-0-primitives/gdpr-ropa.test.js +30 -0
  342. package/lib/vendor/blamejs/test/layer-0-primitives/guard-archive.test.js +86 -0
  343. package/lib/vendor/blamejs/test/layer-0-primitives/guard-auth.test.js +117 -0
  344. package/lib/vendor/blamejs/test/layer-0-primitives/guard-cidr.test.js +151 -0
  345. package/lib/vendor/blamejs/test/layer-0-primitives/guard-domain.test.js +422 -0
  346. package/lib/vendor/blamejs/test/layer-0-primitives/guard-filename.test.js +424 -0
  347. package/lib/vendor/blamejs/test/layer-0-primitives/{guard-gate-disposition-coverage.test.js → guard-gate-disposition.test.js} +1 -1
  348. package/lib/vendor/blamejs/test/layer-0-primitives/guard-graphql.test.js +340 -0
  349. package/lib/vendor/blamejs/test/layer-0-primitives/guard-html.test.js +29 -0
  350. package/lib/vendor/blamejs/test/layer-0-primitives/guard-idempotency-key.test.js +28 -0
  351. package/lib/vendor/blamejs/test/layer-0-primitives/guard-image.test.js +519 -45
  352. package/lib/vendor/blamejs/test/layer-0-primitives/guard-imap-command.test.js +0 -0
  353. package/lib/vendor/blamejs/test/layer-0-primitives/guard-jmap.test.js +51 -0
  354. package/lib/vendor/blamejs/test/layer-0-primitives/guard-jsonpath.test.js +66 -0
  355. package/lib/vendor/blamejs/test/layer-0-primitives/guard-jwt.test.js +436 -0
  356. package/lib/vendor/blamejs/test/layer-0-primitives/guard-managesieve-command.test.js +25 -0
  357. package/lib/vendor/blamejs/test/layer-0-primitives/guard-markdown.test.js +16 -0
  358. package/lib/vendor/blamejs/test/layer-0-primitives/guard-mime.test.js +86 -0
  359. package/lib/vendor/blamejs/test/layer-0-primitives/guard-oauth.test.js +140 -0
  360. package/lib/vendor/blamejs/test/layer-0-primitives/guard-pdf.test.js +80 -0
  361. package/lib/vendor/blamejs/test/layer-0-primitives/guard-pop3-command.test.js +27 -0
  362. package/lib/vendor/blamejs/test/layer-0-primitives/guard-regex.test.js +99 -0
  363. package/lib/vendor/blamejs/test/layer-0-primitives/guard-shell.test.js +76 -0
  364. package/lib/vendor/blamejs/test/layer-0-primitives/{guard-sql-coverage.test.js → guard-sql.test.js} +3 -3
  365. package/lib/vendor/blamejs/test/layer-0-primitives/guard-svg.test.js +524 -0
  366. package/lib/vendor/blamejs/test/layer-0-primitives/guard-template.test.js +71 -0
  367. package/lib/vendor/blamejs/test/layer-0-primitives/guard-time.test.js +92 -0
  368. package/lib/vendor/blamejs/test/layer-0-primitives/guard-uuid.test.js +86 -0
  369. package/lib/vendor/blamejs/test/layer-0-primitives/html-balance.test.js +38 -0
  370. package/lib/vendor/blamejs/test/layer-0-primitives/http-client-cache-authorization.test.js +347 -0
  371. package/lib/vendor/blamejs/test/layer-0-primitives/http-client-cache.test.js +908 -0
  372. package/lib/vendor/blamejs/test/layer-0-primitives/http-client.test.js +2874 -0
  373. package/lib/vendor/blamejs/test/layer-0-primitives/http-message-signature.test.js +643 -0
  374. package/lib/vendor/blamejs/test/layer-0-primitives/i18n-messageformat.test.js +38 -0
  375. package/lib/vendor/blamejs/test/layer-0-primitives/i18n.test.js +1153 -1
  376. package/lib/vendor/blamejs/test/layer-0-primitives/idempotency-key.test.js +494 -0
  377. package/lib/vendor/blamejs/test/layer-0-primitives/ip-utils.test.js +30 -0
  378. package/lib/vendor/blamejs/test/layer-0-primitives/jsdoc-example-execution.test.js +177 -0
  379. package/lib/vendor/blamejs/test/layer-0-primitives/json-path.test.js +103 -0
  380. package/lib/vendor/blamejs/test/layer-0-primitives/json-schema.test.js +214 -0
  381. package/lib/vendor/blamejs/test/layer-0-primitives/jwk.test.js +41 -0
  382. package/lib/vendor/blamejs/test/layer-0-primitives/jwt-external.test.js +276 -0
  383. package/lib/vendor/blamejs/test/layer-0-primitives/keychain.test.js +0 -0
  384. package/lib/vendor/blamejs/test/layer-0-primitives/log-stream.test.js +241 -0
  385. package/lib/vendor/blamejs/test/layer-0-primitives/log.test.js +106 -0
  386. package/lib/vendor/blamejs/test/layer-0-primitives/mail-agent.test.js +441 -0
  387. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +2136 -4
  388. package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi.test.js +759 -12
  389. package/lib/vendor/blamejs/test/layer-0-primitives/mail-bounce.test.js +811 -1
  390. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-pgp-experimental.test.js +401 -3
  391. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-pgp.test.js +751 -8
  392. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +864 -0
  393. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto.test.js +110 -0
  394. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +830 -1
  395. package/lib/vendor/blamejs/test/layer-0-primitives/mail-deploy.test.js +635 -86
  396. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim.test.js +818 -0
  397. package/lib/vendor/blamejs/test/layer-0-primitives/mail-helo.test.js +27 -0
  398. package/lib/vendor/blamejs/test/layer-0-primitives/mail-scan.test.js +199 -0
  399. package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +686 -2
  400. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-imap.test.js +778 -0
  401. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-jmap.test.js +1438 -0
  402. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-managesieve.test.js +449 -6
  403. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +773 -2
  404. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-pop3.test.js +623 -3
  405. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-rate-limit.test.js +55 -0
  406. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-registry.test.js +13 -0
  407. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-submission.test.js +877 -1
  408. package/lib/vendor/blamejs/test/layer-0-primitives/mail-sieve.test.js +326 -0
  409. package/lib/vendor/blamejs/test/layer-0-primitives/mail-store-fts.test.js +55 -0
  410. package/lib/vendor/blamejs/test/layer-0-primitives/mail-store.test.js +242 -0
  411. package/lib/vendor/blamejs/test/layer-0-primitives/mail.test.js +1596 -2
  412. package/lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js +440 -4
  413. package/lib/vendor/blamejs/test/layer-0-primitives/metrics-snapshot.test.js +89 -0
  414. package/lib/vendor/blamejs/test/layer-0-primitives/metrics.test.js +1230 -0
  415. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-assetlinks.test.js +113 -0
  416. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-body-parser.test.js +1169 -0
  417. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-bot-disclose.test.js +123 -0
  418. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-cookies.test.js +108 -0
  419. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-gpc.test.js +121 -0
  420. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-headers.test.js +163 -0
  421. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-host-allowlist.test.js +154 -0
  422. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-require-content-type.test.js +127 -0
  423. package/lib/vendor/blamejs/test/layer-0-primitives/{middleware-tus-upload-coverage.test.js → middleware-tus-upload.test.js} +45 -0
  424. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-web-app-manifest.test.js +132 -0
  425. package/lib/vendor/blamejs/test/layer-0-primitives/mtls-ca-crl-fingerprint-only.test.js +92 -0
  426. package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver.test.js +65 -0
  427. package/lib/vendor/blamejs/test/layer-0-primitives/network-dns.test.js +1727 -1
  428. package/lib/vendor/blamejs/test/layer-0-primitives/network-nts.test.js +699 -0
  429. package/lib/vendor/blamejs/test/layer-0-primitives/network-smtp-policy.test.js +1181 -0
  430. package/lib/vendor/blamejs/test/layer-0-primitives/network-tls.test.js +1876 -3
  431. package/lib/vendor/blamejs/test/layer-0-primitives/network.test.js +32 -0
  432. package/lib/vendor/blamejs/test/layer-0-primitives/nonce-store-enforce-replay-nonboolean.test.js +81 -0
  433. package/lib/vendor/blamejs/test/layer-0-primitives/ntp-check.test.js +122 -0
  434. package/lib/vendor/blamejs/test/layer-0-primitives/observability-tracing.test.js +378 -0
  435. package/lib/vendor/blamejs/test/layer-0-primitives/observability.test.js +77 -0
  436. package/lib/vendor/blamejs/test/layer-0-primitives/oid4vci.test.js +1016 -0
  437. package/lib/vendor/blamejs/test/layer-0-primitives/openid-federation.test.js +946 -0
  438. package/lib/vendor/blamejs/test/layer-0-primitives/outbox.test.js +818 -0
  439. package/lib/vendor/blamejs/test/layer-0-primitives/own-property-membership-guards.test.js +109 -0
  440. package/lib/vendor/blamejs/test/layer-0-primitives/parsers-safe-toml.test.js +543 -0
  441. package/lib/vendor/blamejs/test/layer-0-primitives/{parsers-safe-yaml-coverage.test.js → parsers-safe-yaml.test.js} +2 -2
  442. package/lib/vendor/blamejs/test/layer-0-primitives/parsers-standalone.test.js +73 -0
  443. package/lib/vendor/blamejs/test/layer-0-primitives/passkey.test.js +649 -0
  444. package/lib/vendor/blamejs/test/layer-0-primitives/permissions.test.js +524 -0
  445. package/lib/vendor/blamejs/test/layer-0-primitives/pqc-agent-curve.test.js +22 -0
  446. package/lib/vendor/blamejs/test/layer-0-primitives/pubsub.test.js +295 -0
  447. package/lib/vendor/blamejs/test/layer-0-primitives/queue-flow-repeat.test.js +36 -0
  448. package/lib/vendor/blamejs/test/layer-0-primitives/queue.test.js +83 -0
  449. package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-xff-spoofing.test.js +34 -0
  450. package/lib/vendor/blamejs/test/layer-0-primitives/redact.test.js +1031 -0
  451. package/lib/vendor/blamejs/test/layer-0-primitives/request-helpers.test.js +94 -0
  452. package/lib/vendor/blamejs/test/layer-0-primitives/require-bound-key.test.js +401 -0
  453. package/lib/vendor/blamejs/test/layer-0-primitives/restore-empty-manifest-wipe.test.js +154 -0
  454. package/lib/vendor/blamejs/test/layer-0-primitives/retention.test.js +600 -0
  455. package/lib/vendor/blamejs/test/layer-0-primitives/router-host-path-injection.test.js +231 -0
  456. package/lib/vendor/blamejs/test/layer-0-primitives/router-tls0rtt.test.js +21 -0
  457. package/lib/vendor/blamejs/test/layer-0-primitives/router.test.js +1277 -0
  458. package/lib/vendor/blamejs/test/layer-0-primitives/safe-async.test.js +133 -0
  459. package/lib/vendor/blamejs/test/layer-0-primitives/safe-buffer-linear-scans.test.js +14 -0
  460. package/lib/vendor/blamejs/test/layer-0-primitives/safe-buffer.test.js +80 -0
  461. package/lib/vendor/blamejs/test/layer-0-primitives/safe-decompress.test.js +28 -0
  462. package/lib/vendor/blamejs/test/layer-0-primitives/safe-dns.test.js +245 -0
  463. package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +32 -0
  464. package/lib/vendor/blamejs/test/layer-0-primitives/safe-ini.test.js +376 -0
  465. package/lib/vendor/blamejs/test/layer-0-primitives/safe-json.test.js +175 -0
  466. package/lib/vendor/blamejs/test/layer-0-primitives/safe-mime.test.js +518 -0
  467. package/lib/vendor/blamejs/test/layer-0-primitives/safe-object.test.js +62 -0
  468. package/lib/vendor/blamejs/test/layer-0-primitives/safe-path.test.js +30 -0
  469. package/lib/vendor/blamejs/test/layer-0-primitives/safe-schema.test.js +104 -0
  470. package/lib/vendor/blamejs/test/layer-0-primitives/safe-sql.test.js +142 -0
  471. package/lib/vendor/blamejs/test/layer-0-primitives/safe-url.test.js +112 -0
  472. package/lib/vendor/blamejs/test/layer-0-primitives/safe-vcard.test.js +26 -0
  473. package/lib/vendor/blamejs/test/layer-0-primitives/scheduler.test.js +157 -0
  474. package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +396 -0
  475. package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +930 -0
  476. package/lib/vendor/blamejs/test/layer-0-primitives/security-assert.test.js +755 -82
  477. package/lib/vendor/blamejs/test/layer-0-primitives/self-update.test.js +1053 -0
  478. package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +200 -0
  479. package/lib/vendor/blamejs/test/layer-0-primitives/session-strict-binding-missing-failclosed.test.js +110 -0
  480. package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +636 -0
  481. package/lib/vendor/blamejs/test/layer-0-primitives/sql-offset-without-limit.test.js +94 -0
  482. package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +1190 -0
  483. package/lib/vendor/blamejs/test/layer-0-primitives/ssrf-guard.test.js +21 -0
  484. package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +850 -0
  485. package/lib/vendor/blamejs/test/layer-0-primitives/storage-presigned-url.test.js +146 -0
  486. package/lib/vendor/blamejs/test/layer-0-primitives/storage.test.js +621 -0
  487. package/lib/vendor/blamejs/test/layer-0-primitives/subject.test.js +47 -0
  488. package/lib/vendor/blamejs/test/layer-0-primitives/template.test.js +582 -0
  489. package/lib/vendor/blamejs/test/layer-0-primitives/tenant-quota.test.js +587 -25
  490. package/lib/vendor/blamejs/test/layer-0-primitives/time.test.js +11 -0
  491. package/lib/vendor/blamejs/test/layer-0-primitives/tsa.test.js +524 -63
  492. package/lib/vendor/blamejs/test/layer-0-primitives/uri-template.test.js +27 -0
  493. package/lib/vendor/blamejs/test/layer-0-primitives/vault-rotate.test.js +866 -0
  494. package/lib/vendor/blamejs/test/layer-0-primitives/vault.test.js +74 -0
  495. package/lib/vendor/blamejs/test/layer-0-primitives/vc.test.js +215 -0
  496. package/lib/vendor/blamejs/test/layer-0-primitives/watcher.test.js +550 -0
  497. package/lib/vendor/blamejs/test/layer-0-primitives/webhook-dispatcher.test.js +388 -0
  498. package/lib/vendor/blamejs/test/layer-0-primitives/webhook.test.js +262 -0
  499. package/lib/vendor/blamejs/test/layer-0-primitives/websocket.test.js +1294 -0
  500. package/lib/vendor/blamejs/test/layer-0-primitives/ws-client.test.js +629 -0
  501. package/lib/vendor/blamejs/test/layer-0-primitives/xml-c14n-attr-normalization.test.js +164 -0
  502. package/lib/vendor/blamejs/test/smoke.js +2 -0
  503. package/package.json +1 -1
  504. package/lib/vendor/blamejs/release-notes/v0.16.0.json +0 -44
  505. package/lib/vendor/blamejs/release-notes/v0.16.1.json +0 -36
  506. package/lib/vendor/blamejs/release-notes/v0.16.2.json +0 -71
  507. package/lib/vendor/blamejs/release-notes/v0.16.3.json +0 -71
  508. package/lib/vendor/blamejs/test/layer-0-primitives/acme-coverage.test.js +0 -441
  509. package/lib/vendor/blamejs/test/layer-0-primitives/auth-oauth-coverage.test.js +0 -482
  510. package/lib/vendor/blamejs/test/layer-0-primitives/auth-saml-coverage.test.js +0 -671
  511. package/lib/vendor/blamejs/test/layer-0-primitives/backup-index-coverage.test.js +0 -690
  512. package/lib/vendor/blamejs/test/layer-0-primitives/cli-coverage.test.js +0 -238
  513. package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials-coverage.test.js +0 -319
  514. package/lib/vendor/blamejs/test/layer-0-primitives/keychain-coverage.test.js +0 -0
  515. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-coverage.test.js +0 -437
  516. package/lib/vendor/blamejs/test/layer-0-primitives/mcp-coverage.test.js +0 -443
  517. package/lib/vendor/blamejs/test/layer-0-primitives/metrics-coverage.test.js +0 -395
  518. package/lib/vendor/blamejs/test/layer-0-primitives/network-smtp-policy-coverage.test.js +0 -565
  519. package/lib/vendor/blamejs/test/layer-0-primitives/router-coverage.test.js +0 -592
  520. package/lib/vendor/blamejs/test/layer-0-primitives/sql-coverage.test.js +0 -422
  521. package/lib/vendor/blamejs/test/layer-0-primitives/test-coverage.test.js +0 -573
@@ -0,0 +1,1462 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-consolidated-schema.json",
3
+ "minor": "0.16",
4
+ "releases": [
5
+ {
6
+ "version": "0.16.40",
7
+ "date": "2026-07-16",
8
+ "headline": "Four request-path fixes: the router no longer lets the Host header steer which route runs, per-route rate limits can no longer be evaded with a rotating query string, and the message-format and URI-template expanders no longer leak inherited object properties",
9
+ "summary": "b.router derived the dispatch path from the client-controlled Host header, so a path-like Host value bled into the parsed pathname and could steer a request to a different route than its request line named -- a path-ACL bypass in front-proxied deployments. b.middleware.rateLimit in per-route scope fell back to the full request URL (query string included) when the router had not populated the path, letting a rotating throwaway query parameter mint a fresh bucket per request. And b.i18n message-format and b.uriTemplate resolved a template-derived variable name with a bare property read, so a name like {toString} or {constructor} returned an inherited Object.prototype member (or a prototype-polluted value) instead of treating the variable as absent.",
10
+ "sections": [
11
+ {
12
+ "heading": "Security",
13
+ "items": [
14
+ {
15
+ "title": "The Host header can no longer influence which route is dispatched",
16
+ "body": "b.router derived the request path it matches routes against (req.pathname) by parsing the Host header concatenated with the request URL. WHATWG URL parsing folds any path-like characters in the Host value into the parsed pathname, so a request whose request line was /x but whose Host header was trusted/admin dispatched as /admin/x while req.url stayed /x. Because req.pathname is what the route matcher and every path-scoped guard (auth, CSRF, mTLS) compare against, a client could steer which handler runs -- and a front proxy or WAF that authorizes on the visible request path is bypassed. The router now derives the path and query from the request URL alone, resolved against a fixed internal authority, so the Host header cannot perturb routing; a request target that is not origin-form (a single leading slash) -- an absolute-form, authority-form, asterisk-form, or network-path-reference target -- is rejected with 400 rather than coerced into a routable path. The request's real scheme and host remain available to the consumers (canonical URL, CORS, host allowlist) that legitimately need them."
17
+ },
18
+ {
19
+ "title": "Per-route rate limits key on the path only, not the query string",
20
+ "body": "b.middleware.rateLimit with scope: \"per-route\" composed its bucket key from req.pathname, falling back to the full request URL when pathname was absent (a raw Node request handed to the middleware without the router populating it). The fallback included the query string, which is not part of route identity, so an attacker could rotate a throwaway parameter (?nonce=1, ?nonce=2, ...) to mint a fresh bucket per request from the same client and evade the limit. The per-route key now strips the query in the fallback, matching the path-normalization the other request-scoped guards already apply."
21
+ },
22
+ {
23
+ "title": "Message-format and URI-template variable lookups are own-property only",
24
+ "body": "b.i18n message-format ({arg}, {n, plural, ...}, {s, select, ...}) and b.uriTemplate ({var} expansion) resolved a variable name parsed from the template with a bare property read of the caller's variables object. A template naming an inherited member -- {toString}, {constructor}, {valueOf}, {__proto__} -- therefore returned the Object.prototype member (a function's source, or a prototype-polluted value) instead of treating the variable as absent, leaking it into rendered output or an expanded URI. Both now resolve variables through an own-property check, so an inherited or polluted name renders empty (message-format argument) or is omitted per RFC 6570 (URI template), matching the own-property discipline the HTML template engine and the simple interpolator already enforce."
25
+ }
26
+ ]
27
+ }
28
+ ]
29
+ },
30
+ {
31
+ "version": "0.16.39",
32
+ "date": "2026-07-16",
33
+ "headline": "Three fixes across the outbound HTTP cache, session device binding, and XML canonicalization: a shared cache no longer serves one principal's authenticated response to another, a strict device-binding policy no longer admits an unbound session, and XML canonicalization no longer collides literal and character-reference line endings",
34
+ "summary": "The RFC 9111 outbound HTTP cache (b.httpClient) leaked across principals: a shared cache (the default) stored and re-served the response to an Authorization-bearing GET to a subsequent request from a different caller, violating RFC 9111 §3.5. b.session.verify failed open under a strict device-binding policy for a session that carried no stored fingerprint -- the normal state for any session created without a request context, including API-token, OAuth-callback, and admin-created flows -- so requireFingerprintMatch / maxAnomalyScore silently admitted a session from any device. And b.xmlC14n canonicalized a literal TAB / CR / LF and the equivalent character reference to identical bytes, a distinct-input / identical-output collision in the exact primitive whose purpose is preventing XML-signature-wrapping, affecting the attribute values and the element text and CDATA that XMLDSig signatures cover.",
35
+ "sections": [
36
+ {
37
+ "heading": "Security",
38
+ "items": [
39
+ {
40
+ "title": "Shared HTTP cache no longer serves an authenticated response across principals",
41
+ "body": "b.httpClient's RFC 9111 response cache, in shared mode (the default), stored and re-served the response to a GET carrying an Authorization header to a later request from a different principal. RFC 9111 §3.5 forbids a shared cache from reusing a stored response to an Authorization-bearing request unless the response opts in via public, s-maxage, or must-revalidate; the storage decision never inspected the request headers, so an authenticated per-user response with an ordinary max-age was cached and served to other users. The storage decision now refuses to persist an Authorization-bearing request's response in a shared cache unless the origin supplies one of those opt-ins. Private caches (sharedCache: false) and the opt-in directives are unaffected."
42
+ },
43
+ {
44
+ "title": "Strict session device binding refuses an unbound session instead of admitting it",
45
+ "body": "b.session.verify treats requireFingerprintMatch: true or a maxAnomalyScore threshold as a per-request assertion that the session is device-bound and the current device matches. Those refusals were reached only when a stored fingerprint was present, so a session with no binding -- the state of any session created without a request context (API-token, OAuth-callback, admin-created, and 'remember me' flows) -- skipped the strict gate entirely and was admitted from any device. verify now fails closed (returns null, audit event auth.session.binding_missing) whenever a strict binding policy is requested but the session carries no comparable fingerprint, covering both a never-bound session and one whose sealed binding cannot be decrypted. Bind any session you intend to verify strictly by passing the request context to create(); verifications that do not request a strict policy are unchanged."
46
+ },
47
+ {
48
+ "title": "XML canonicalization distinguishes literal whitespace from character references",
49
+ "body": "b.xmlC14n produces the canonical byte form that XML signatures cover, so distinct inputs must yield distinct bytes or a signed document can be swapped for a different one whose canonical form still matches (signature wrapping). A literal TAB, CR, or LF and the equivalent character reference (	 / 
 / 
) canonicalized to identical bytes: attribute values were not normalized per XML 1.0 §3.3.3, and character data (element text and CDATA) was not line-ending-normalized per §2.11. Attribute-value whitespace now folds to a single space while character-reference whitespace is preserved, and literal CR / CRLF in character data now folds to a single LF while a 
 reference is preserved -- so a literal control character and its character-reference form canonicalize distinctly everywhere a signature covers. The SAML XMLDSig verification and b.guardXml signature-wrapping defense that consume the canonical form inherit the fix."
50
+ }
51
+ ]
52
+ }
53
+ ]
54
+ },
55
+ {
56
+ "version": "0.16.38",
57
+ "date": "2026-07-16",
58
+ "headline": "Three fixes to data-subject scoping, break-glass grant limits, and idempotent-retry replay: a data-subject filter no longer matches every subject when it has no indexable key, a single-row break-glass grant can no longer be spent twice concurrently, and an idempotent retry returns its cached result under a vault",
59
+ "summary": "b.dsr's subject-scoped ticket filter failed open: a subject carrying none of the indexable keys (email / subjectId) -- a phone-only, alias-only, or empty subject -- matched EVERY ticket instead of none, so listBySubject returned other subjects' tickets and the erasure-completion purge deleted them. Both ticket stores now fail closed. b.breakGlass's per-row grant limit could be exceeded under concurrency: two simultaneous unseals of a one-row grant against different rows both succeeded, because the claim was decided from a re-read of the shared counter that already reflected the other caller's increment; the claim is now decided from the atomic update's affected-row count. And b.agent.idempotency's putIfAbsent replay parsed the sealed result blob without unsealing it, so under a vault (the production default) every idempotent retry that landed on a completed key threw instead of returning the cached result -- and get() on a pending claim threw on a null result blob rather than reporting no cached result.",
60
+ "sections": [
61
+ {
62
+ "heading": "Fixed",
63
+ "items": [
64
+ {
65
+ "title": "Data-subject request filter fails closed when a subject has no indexable key",
66
+ "body": "b.dsr's subject-scoped ticket filter matched on the indexable keys email and subjectId. When the supplied subject carried neither -- a phone-only subject (a legitimate SMS-first identity), an alias-only subject, or an empty object -- the filter added no predicate and returned every ticket in the store instead of none. Through the exported API this meant listBySubject(subject) disclosed every subject's tickets, and the erasure-completion purge (which lists a subject's other tickets and deletes them) deleted every other subject's tickets. Both the in-memory and database ticket stores now fail closed: a subject filter that produces no usable predicate matches nothing, so an unindexable subject can neither read nor delete another subject's data. Filters that supply an indexable key are unchanged."
67
+ },
68
+ {
69
+ "title": "A single-row break-glass grant can no longer be spent twice under concurrency",
70
+ "body": "b.breakGlass.unsealRow enforces a per-grant row limit with an atomic compare-and-increment (update the consumed counter where it is still below the cap). It then decided whether the caller won the slot by re-reading the counter and comparing it to the caller's own stale pre-value -- but a concurrent winner's increment is visible to the loser's re-read, so both callers saw a change and both proceeded, unsealing two rows under a one-row grant. The claim is now decided from the atomic update's affected-row count: exactly one caller's compare-and-increment modifies the row, and the loser (zero rows modified) is refused with grant-exhausted. The re-read is retained only for the audit's remaining-rows hint."
71
+ },
72
+ {
73
+ "title": "Idempotent retries return their cached result under a vault, and a pending-claim read reports absent",
74
+ "body": "b.agent.idempotency seals the cached result at rest via b.cryptoField when a vault is configured (the production default). putIfAbsent's replay branch parsed the stored result blob as JSON without unsealing it first, so a retry that landed on an already-completed key threw a corrupt-result error instead of returning the cached result -- breaking the primitive's exactly-once replay guarantee exactly where operators run it. It now unseals before parsing, mirroring get(). Separately, get() on a pending claim (whose result blob is null because no result has been written yet) fed null to the JSON parser and threw the same corrupt-result error; it now reports no cached result, so a concurrent status check during another worker's in-flight claim no longer throws."
75
+ }
76
+ ]
77
+ }
78
+ ]
79
+ },
80
+ {
81
+ "version": "0.16.37",
82
+ "date": "2026-07-16",
83
+ "headline": "Three fail-open / injection fixes: the age gate no longer admits a user whose age fails to compute, the query builder's OFFSET-without-LIMIT runs on every backend, and metrics exemplars can no longer carry an unsanitized value into the scrape stream",
84
+ "summary": "Three defects, each in a class the framework treats as security-relevant. b.middleware.ageGate classified a non-finite age (a NaN or Infinity returned by getAge when a birth field fails to parse) as an adult -- admitting the request with none of the child-safety privacy defaults -- instead of treating an uncomputable age as unknown. b.sql (and b.db.from / b.db.collection over it) emitted a bare OFFSET with no LIMIT, which is valid only on Postgres and is a syntax error on SQLite -- the framework's own backend -- and MySQL, so a valid builder chain failed to run on two of three dialects. And b.metrics exemplars rendered their labels, value, and timestamp into the OpenMetrics scrape surface without the credential-scrub and numeric-coercion regular labels receive, so an operator-supplied exemplar could leak a credential-shaped label or inject a forged metric line through exemplar.value / exemplar.timestamp.",
85
+ "sections": [
86
+ {
87
+ "heading": "Fixed",
88
+ "items": [
89
+ {
90
+ "title": "Age gate treats a non-finite age as unknown, not as an adult",
91
+ "body": "b.middleware.ageGate now classifies a non-finite age (NaN or +/-Infinity, the shape getAge returns when a birth field fails to parse or date math goes wrong) as \"unknown\" rather than letting it fall through to \"above-threshold\". Because typeof NaN === \"number\" and every comparison against NaN is false, an uncomputable age previously bypassed the below-threshold branch and was admitted as a confirmed adult -- with none of the child-safety privacy defaults (Cache-Control: private, no-store; Referrer-Policy: no-referrer; the privacy-posture header) the unknown path applies. When the birth value is request-derived this is attacker-influenced. A non-finite age is now handled exactly like a null return: the request is classified unknown and the privacy defaults are applied."
92
+ },
93
+ {
94
+ "title": "Query builder OFFSET without LIMIT runs on SQLite and MySQL, not only Postgres",
95
+ "body": "b.sql SELECT (and the b.db.from / b.db.collection consumers built on it) emitted a bare \"OFFSET n\" when .offset() was set without .limit(). A bare OFFSET is valid only on Postgres; SQLite (the framework's own node:sqlite backend) and MySQL both reject it as a syntax error, so a valid builder chain produced SQL that failed to prepare on two of the three supported dialects, including the default one. The builder now emits the dialect's unbounded-limit sentinel before the OFFSET -- SQLite LIMIT -1, MySQL the maximum unsigned BIGINT, Postgres LIMIT ALL -- so one query text runs unchanged across all three. Statements that set an explicit LIMIT are byte-for-byte unchanged."
96
+ },
97
+ {
98
+ "title": "Metrics exemplars are sanitized on the scrape surface the same way regular labels are",
99
+ "body": "b.metrics histogram exemplars rendered their labels, value, and timestamp into the OpenMetrics /metrics exposition -- the same broadly-readable scrape surface regular labels reach -- without the sanitization regular labels receive. Exemplar label values bypassed the credential scrubber, so a credential-shaped value an operator attached to an exemplar (e.g. tapping a raw header alongside trace context) egressed in cleartext (CWE-532); and exemplar.value / exemplar.timestamp were appended to the exposition line raw, so a non-numeric operator-supplied value such as \"1.0\\n# forged 999\" could inject a forged metric line. Exemplar labels now flow through the same credential scrubber as regular labels, and exemplar value and timestamp are coerced to a finite number (value falling back to the observed value, timestamp to none) at store time, so only sanitized labels and bare numbers ever reach the wire. Trace context (trace_id / span_id) and numeric values pass through unchanged."
100
+ }
101
+ ]
102
+ }
103
+ ]
104
+ },
105
+ {
106
+ "version": "0.16.36",
107
+ "date": "2026-07-16",
108
+ "headline": "DKIM simple header canonicalization now signs and verifies the DKIM-Signature header verbatim, so simple-canon signatures round-trip and interoperate",
109
+ "summary": "b.mail.dkim's simple header canonicalization (c=simple/... in a signature) signs and verifies each header exactly as it appears on the wire, per RFC 6376 §3.4.1. Two defects broke that for the DKIM-Signature header itself: the signer canonicalized the header UNFOLDED while emitting it FOLDED on the wire, and the verifier prefixed a spurious extra space to the parsed header value. Together they meant a simple-header-canonicalization signature never matched the bytes that were signed -- neither the framework's own signatures nor an RFC-compliant peer's would verify under simple canon. The signer now canonicalizes and emits the same folded header (appending the signature to the folded, b-emptied form), and the verifier canonicalizes the parsed header verbatim, so a simple-canon signature round-trips and is byte-compatible with other implementations. Relaxed canonicalization, the common default, is unchanged. Separately, a supply-chain gate now fails the build if a vendored component in lib/vendor/MANIFEST.json has no attribution entry in NOTICE.",
110
+ "sections": [
111
+ {
112
+ "heading": "Added",
113
+ "items": [
114
+ {
115
+ "title": "b.safeBuffer.byteLengthOfIfMeasurable — measure a value's byte length, or null when it is not a byte carrier",
116
+ "body": "byteLengthOfIfMeasurable(value) returns the byte length of a string, Buffer, or Uint8Array, and null for anything else (a plain Array, an array-like object, a number, null). It is the safe way to cap the size of an untrusted metadata bag whose byte field may be any shape: measure the cap only when the value is measurable, rather than gating byteLengthOf (which throws on a non-byte-carrier) on a hand-rolled length check that admits array-likes and crashes. The image and PDF content guards now compose it for their byte caps instead of each vetting the type inline."
117
+ }
118
+ ]
119
+ },
120
+ {
121
+ "heading": "Fixed",
122
+ "items": [
123
+ {
124
+ "title": "DKIM simple header canonicalization signs and verifies the DKIM-Signature header verbatim",
125
+ "body": "Under simple header canonicalization (RFC 6376 §3.4.1) the DKIM-Signature header is signed and verified byte-for-byte as it appears on the wire, including its folding. b.mail.dkim canonicalized the header UNFOLDED when computing the signature but emitted it FOLDED on the wire, and the verifier prepended an extra space to the parsed header value before canonicalizing -- so the bytes signed never matched the bytes verified. A simple-header-canonicalization signature therefore never verified, whether produced by the framework or by an RFC-compliant peer. The signer now canonicalizes the folded, b-emptied header and builds the wire header by appending the signature to it, and the verifier canonicalizes the parsed (folded) header verbatim -- including the DKIM-Signature field name exactly as it appears on the wire, so a peer that signs a lowercase dkim-signature: field name also verifies -- so a simple-canon signature round-trips and interoperates. This was fail-closed -- a broken simple-canon signature was reported as a verification failure, never a false pass -- and relaxed canonicalization (the common default, which normalizes folding and whitespace on both sides) was and remains correct."
126
+ }
127
+ ]
128
+ },
129
+ {
130
+ "heading": "Detectors",
131
+ "items": [
132
+ {
133
+ "title": "Every vendored component in the manifest must be attributed in NOTICE",
134
+ "body": "A gate fails the build when a component recorded in lib/vendor/MANIFEST.json has no attribution entry in the NOTICE file. Third-party components ship with their license and attribution obligations; this catches a vendored library or data file added to the manifest without its NOTICE entry before the package is published, rather than after a downstream scanner flags the omission."
135
+ }
136
+ ]
137
+ }
138
+ ]
139
+ },
140
+ {
141
+ "version": "0.16.35",
142
+ "date": "2026-07-16",
143
+ "headline": "A resumed saga that later fails now compensates the steps it completed before the crash, and a webhook delivery retries a transient DNS failure during its safety re-check instead of dead-lettering",
144
+ "summary": "Two durability fixes in the agent orchestration and webhook delivery primitives. b.agent.saga rebuilt its completed-step list from empty when a saga resumed from a persisted checkpoint, so a failure after the resume compensated only the steps that ran in that resumed pass -- the steps completed before the crash were never rolled back, defeating the saga's whole purpose (a charge committed before the crash would never be refunded when a later step failed). Resume now seeds the completed-step list with the steps that finished before the crash, so a failure compensates the full set in reverse order. Separately, b.webhook.dispatcher re-checks a delivery's destination for an SSRF rebind just before each attempt; that check resolves the destination host, and a transient resolver failure (a temporary DNS error) during it was treated as a permanent failure and dead-lettered the delivery on the first attempt. The re-check now dead-letters only a genuine SSRF refusal or malformed URL and treats a transient resolver fault as retryable, like every other transport error.",
145
+ "sections": [
146
+ {
147
+ "heading": "Fixed",
148
+ "items": [
149
+ {
150
+ "title": "A resumed saga compensates the steps it completed before the crash, not only those in the resumed run",
151
+ "body": "b.agent.saga runs a sequence of steps and, on a step failure, compensates the completed steps in reverse order. When a saga resumed from a persisted checkpoint, it started its completed-step list empty, so a failure after the resume compensated only the steps that ran in the resumed pass -- the steps completed before the crash were left uncompensated. For a distributed transaction that is the exact failure the pattern exists to prevent: work committed before the crash (a charge, a reservation, an external call) would never be rolled back when a later step failed. Resume now seeds the completed-step list with the steps that finished before the crash, reconstructed from the saga definition, so a subsequent failure compensates the full completed set in reverse; compensation runs against the resumed state, which already reflects those steps' effects, and the failing step itself is not compensated. To keep that reseeding safe against replay, resume also refuses a saga the state store marks terminal (failed-and-compensated or completed) rather than re-running its compensators, and the state-store interface documents that compensators must be idempotent -- a crash mid-compensation can replay a compensation on the next resume, so compensating twice must be safe."
152
+ },
153
+ {
154
+ "title": "Webhook delivery retries a transient DNS failure during its SSRF re-check instead of dead-lettering",
155
+ "body": "b.webhook.dispatcher re-validates a delivery's destination against SSRF (private / loopback / metadata IPs, or a rebind since registration) just before each attempt, which resolves the destination host. A transient resolver failure during that resolution -- a lookup timeout or system/resolve failure -- was caught and marked a permanent failure, dead-lettering the delivery on its first attempt rather than retrying. The re-check now classifies the failure: a genuine SSRF refusal or malformed URL dead-letters, a resolver failure honors the framework DNS resolver's own terminal-versus-transient verdict (a permanent failure such as a host with no addresses or a removed record dead-letters immediately, a transient one is retried on the backoff curve, capped at maxAttempts), matching how the dispatcher already treats a transient DNS error during the delivery POST itself. So a webhook is no longer lost to a momentary DNS blip, a genuine rebind to an internal address is still dead-lettered immediately, and a permanently unresolvable destination dead-letters without burning every retry attempt."
156
+ }
157
+ ]
158
+ },
159
+ {
160
+ "heading": "Added",
161
+ "items": [
162
+ {
163
+ "title": "b.webhook.dispatcher accepts a dnsLookup override for the destination SSRF check",
164
+ "body": "b.webhook.dispatcher now accepts an optional dnsLookup(host) resolver, forwarded to the SSRF destination check, so an operator can point destination resolution at a specific resolver (and a test can drive the transient-versus-permanent classification offline). It defaults to the framework's DNS-over-TLS resolver, unchanged."
165
+ }
166
+ ]
167
+ }
168
+ ]
169
+ },
170
+ {
171
+ "version": "0.16.34",
172
+ "date": "2026-07-16",
173
+ "headline": "The DPoP middleware returns the correct multiple-proof rejection when a request carries a repeated DPoP header, instead of mislabeling it as a missing proof",
174
+ "summary": "RFC 9449 §4.1 permits only one DPoP header value per request. b.middleware.dpop rejected a request that carried the header as an array -- repeated DPoP: lines a custom server or proxy did not collapse -- but its array-shape check sat after the missing-header guard, and an array is not a string, so the missing-header guard always ran first. A duplicated DPoP proof was therefore rejected as a missing proof (and, when a DPoP nonce was required, answered with use_dpop_nonce, prompting the client into a pointless nonce-retry loop) rather than with the invalid_dpop_proof / multiple-DPoP-headers rejection the specification calls for. Both paths already refused the request, so this was never a fail-open -- only an incorrect diagnostic and a wasted round trip. The array-shape check now runs before the missing-header guard, so a repeated DPoP header is rejected with the correct error.",
175
+ "sections": [
176
+ {
177
+ "heading": "Fixed",
178
+ "items": [
179
+ {
180
+ "title": "DPoP middleware rejects a repeated DPoP header with the correct multiple-proof error",
181
+ "body": "b.middleware.dpop enforces the RFC 9449 §4.1 single-value rule, but its Array.isArray check for a repeated header (when a server or proxy delivered the DPoP header as an array rather than a comma-joined string) sat after the non-string / empty guard. Because an array fails the non-string check first, the dedicated multiple-DPoP-headers branch never ran: a duplicated proof was reported as a missing DPoP header, and under a required-nonce policy it returned use_dpop_nonce, driving the client into a fruitless nonce-retry loop. The array-shape check now runs first, so a repeated DPoP header is rejected with invalid_dpop_proof and a multiple-DPoP-headers message. This changes only the error code and message for that malformed-request case; both orderings already refused the request, so no valid request is affected."
182
+ }
183
+ ]
184
+ }
185
+ ]
186
+ },
187
+ {
188
+ "version": "0.16.33",
189
+ "date": "2026-07-16",
190
+ "headline": "Agent-snapshot restore now authenticates a sealed snapshot's tenant and capture-time against its signature, closing a cross-tenant restore path, alongside fail-closed and never-throw hardening across the DKIM/ARC, crypto-envelope, and image/PDF verifiers",
191
+ "summary": "A sealed agent snapshot's authenticated envelope binds its table, snapshot id, and schema version, but the decorative wrapper fields a hostile or compromised storage backend can rewrite -- the tenant id that loadLatest filters on and the capture time it sorts on -- were trusted without being checked against the signed body. b.agent.snapshot restore now cross-checks both wrapper fields against the signature-covered values and refuses a mismatch, so a relabelled row can no longer surface one tenant's authentic snapshot to another (cross-tenant restore) or misrepresent when the restored state was captured. The release also hardens several verifiers that a valid-but-unusual or hostile input could push off their documented contract: the DKIM, inbound-authentication, and ARC verifiers now accept a bare-LF (Unix line-ending) message and return an authentication verdict instead of throwing; the crypto envelope and packed-secret decoders reject a truncated ciphertext with their typed error instead of leaking a raw cipher exception; the image guard routes a byte-order-mark-prefixed SVG to refusal at every profile instead of serving it as unknown content; and the image and PDF guards no longer throw on a hostile metadata bag whose bytes field is an array-like object, honoring their never-throw inspection contract.",
192
+ "sections": [
193
+ {
194
+ "heading": "Security",
195
+ "items": [
196
+ {
197
+ "title": "Agent-snapshot restore binds the requested tenant and capture-time to the sealed snapshot's signature",
198
+ "body": "b.agent.snapshot seals each snapshot under an authenticated envelope whose AAD binds the table, snapshot id, and schema version. The metadata a backend stores alongside the sealed blob -- the tenant id that loadLatest({ tenantId }) filters on and the takenAt it sorts on to pick the latest -- is not covered by that AAD, and a hostile or compromised backend can return independently tampered list() and get() results. A backend could therefore relabel tenant A's list() entry as tenant B (leaving A's get() row honest) so that loadLatest({ tenantId: 'tenant-b' }) selected and returned tenant A's authentic snapshot -- a cross-tenant restore of in-flight sagas, streams, and idempotency state -- or inflate a row's list() age to serve an older snapshot as the latest. loadLatest now binds the requested selection criteria to the loaded snapshot's signature-covered values: the authenticated tenant id must equal the requested tenant id, and the list() sort key that selected a row must equal that row's authenticated capture time; a divergence is refused (agent-snapshot/tenant-id-mismatch, agent-snapshot/taken-at-mismatch), and the load path additionally cross-checks the get() wrapper against the signed body. The fix is at load time and does not change the seal format, so previously persisted snapshots remain restorable. A hostile backend can still withhold snapshots it never reveals, but every snapshot returned is authentic and bound to the requested tenant."
199
+ }
200
+ ]
201
+ },
202
+ {
203
+ "heading": "Fixed",
204
+ "items": [
205
+ {
206
+ "title": "DKIM, inbound-authentication, and ARC verifiers accept bare-LF messages instead of throwing",
207
+ "body": "b.mail.dkim.verify, b.mail.inbound.verify, and b.mail.arc.verify canonicalize over CRLF and split the header block on a CRLF-CRLF separator. A message read from a Unix file or mbox, or passed through operator tooling that stripped carriage returns, arrives with bare-LF line endings and previously raised an uncaught error out of the verifier rather than returning an authentication verdict. The header/body split now normalizes bare-LF to canonical CRLF before locating the separator (a no-op on a proper CRLF message), so a bare-LF message produces a verdict -- and a message signed on the CRLF wire but transported bare-LF now verifies correctly rather than failing. inbound.verify and arc.verify additionally treat a message with no separator at all as headers-only, returning a verdict in keeping with their always-return-a-verdict contract."
208
+ },
209
+ {
210
+ "title": "Crypto envelope and packed-secret decoders reject truncated ciphertext with a typed error",
211
+ "body": "b.crypto.decryptEnvelope and b.crypto.decryptPacked verify that each declared component of an untrusted ciphertext -- the length-prefixed KEM ciphertext and hybrid ephemeral public key, and the trailing nonce and authentication tag -- fits within the envelope before handing it to the cipher or the key-agreement step. A ciphertext truncated inside any of those components previously reached Node crypto as an under-length value and surfaced as a raw exception (a cipher RangeError on the nonce, or a Failed to perform decapsulation / key-parse error on the KEM ciphertext or ephemeral key), escaping the documented Invalid envelope error contract and leaking implementation detail. Both decoders now reject a truncated input with their typed Invalid envelope / Invalid packed format error, while a truncation inside the ciphertext body still surfaces as the genuine authentication-tag failure; a well-formed input is never affected."
212
+ },
213
+ {
214
+ "title": "Image guard routes a BOM-prefixed SVG to refusal at every profile",
215
+ "body": "b.guardImage detects SVG by its leading markup so it can route it to the SVG guard or refuse it. A UTF-8 byte-order-mark before the markup previously defeated the offset-anchored signature scan, so a BOM-prefixed SVG fell through as unknown content -- served rather than refused under the balanced and permissive profiles. The magic-byte scanner now skips a leading BOM when matching the SVG and XML signatures, so a BOM-prefixed SVG is detected and refused at every profile. The BOM skip applies only to those text-family signatures: a binary raster's magic must sit at its real offset, so a BOM-prefixed PNG or JPEG is still refused as unknown content rather than accepted as a valid raster."
216
+ },
217
+ {
218
+ "title": "Image and PDF guards no longer throw on a hostile array-like metadata bag",
219
+ "body": "b.guardImage.validate and b.guardPdf.validate document pure inspection that never throws on hostile metadata. Their byte-size cap measured any value carrying a numeric length, but the measurement primitive accepts only strings, Buffers, and Uint8Arrays and threw on a plain Array or array-like object -- crashing a direct validate or sanitize caller (the gate path already fails closed). The cap now measures only those byte-carrying types and passes an unmeasurable array-like through to magic detection, which reads only the leading bytes and refuses unrecognized content, so validate returns a refusal instead of throwing."
220
+ }
221
+ ]
222
+ },
223
+ {
224
+ "heading": "Detectors",
225
+ "items": [
226
+ {
227
+ "title": "A byte-size cap must vet its input type before measuring it",
228
+ "body": "A codebase-patterns gate refuses a guard that measures a metadata bag's byte length gated only on a numeric length property -- the shape that let an array-like bytes field crash the image and PDF guards. A byte-size cap over untrusted metadata must confirm the value is a string, Buffer, or Uint8Array before measuring it, so a future guard cannot reintroduce the never-throw-contract violation."
229
+ }
230
+ ]
231
+ }
232
+ ]
233
+ },
234
+ {
235
+ "version": "0.16.32",
236
+ "date": "2026-07-15",
237
+ "headline": "The vendored Public Suffix List is refreshed to the current upstream snapshot, and vendor-update.sh gains the --refresh-data mode its file headers and verifier messages have always pointed at",
238
+ "summary": "The vendored Mozilla Public Suffix List is updated to the latest upstream build, so organizational-domain derivation for DMARC alignment, BIMI issuer scoping, cookie-scope confinement, and same-site policy reflects the current registry delegations; the data module is regenerated and re-signed (SHA-256 + SHA3-512 + SLH-DSA), the manifest hashes refreshed, and the NOTICE attribution date updated. The refresh itself now runs through scripts/vendor-update.sh --refresh-data — the maintenance command that every vendored data file's header and lib/vendor-data.js's tamper-error messages reference — which fetches the upstream where one exists, sanity-checks the body before anything reaches the signer, re-appends the in-payload integrity canary, regenerates and re-signs the .data.js carrier, updates the manifest and NOTICE dates, and verifies all four integrity layers. A codebase-patterns gate now refuses any script flag referenced from error messages, file headers, or operator docs that the target script does not implement.",
239
+ "sections": [
240
+ {
241
+ "heading": "Added",
242
+ "items": [
243
+ {
244
+ "title": "scripts/vendor-update.sh --refresh-data — fetch, canary, re-sign, and verify the vendored data files in one command",
245
+ "body": "The mode that vendored data-file headers and lib/vendor-data.js tamper-error messages direct operators to now exists. `vendor-update.sh --refresh-data [entry]` refreshes the Public Suffix List and the SecLists common-password corpus from their upstreams (refusing a truncated or error body before it can reach the signer), re-appends each file's in-payload integrity canary, regenerates and re-signs the .data.js carrier with the operator-local SLH-DSA key whenever the raw file changed OR the carrier fails four-layer verification (so a corrupted signature block or a stripped provenance header is repairable through the documented path), updates the MANIFEST bundledAt and NOTICE attribution dates, refreshes the manifest hashes, and finishes by running the four-layer verifier (SHA-256 + SHA3-512 + SLH-DSA signature + canary) over every vendored data file. The operator-managed BIMI trust-anchor bundle is never fetched — it is re-signed only when the local .pem was edited per its file-header procedure. A Public Suffix List fetch whose VERSION timestamp is older than the vendored one is refused rather than signed (the list is CDN-served, and a lagging edge can return an older snapshot than the one already vendored). Entries whose upstream is unchanged are left byte-identical, so a no-op refresh produces a clean working tree; in-flight downloads live in gitignored *.refresh-tmp files that are removed on exit."
246
+ }
247
+ ]
248
+ },
249
+ {
250
+ "heading": "Changed",
251
+ "items": [
252
+ {
253
+ "title": "Vendored Public Suffix List refreshed to the current upstream snapshot",
254
+ "body": "The vendored Mozilla Public Suffix List is updated to the latest upstream build (2026-07-15), so organizational-domain derivation for DMARC alignment, BIMI issuer scoping, cookie-scope confinement, and same-site policy reflects the current registry delegations. The data module is regenerated and re-signed (SHA-256 + SHA3-512 + SLH-DSA), the manifest hashes refreshed, and the NOTICE attribution date updated."
255
+ }
256
+ ]
257
+ },
258
+ {
259
+ "heading": "Detectors",
260
+ "items": [
261
+ {
262
+ "title": "Script flags referenced from error messages, file headers, and operator docs must exist in the target script",
263
+ "body": "A codebase-patterns gate resolves every `<script>.sh --flag` / `<script>.js --flag` reference in lib/, scripts/, and the operator docs against the scripts/ directory and refuses any flag with no whole-token occurrence outside comment lines in the target script — a usage-header comment alone does not count as an implementation. A maintenance command recommended by a verification-failure message or a stale-data gate must work when the operator reaches for it."
264
+ }
265
+ ]
266
+ }
267
+ ]
268
+ },
269
+ {
270
+ "version": "0.16.31",
271
+ "date": "2026-07-13",
272
+ "headline": "Vendored library bundles ship as unminified, reviewable source at the same pinned versions, removing the last dynamic-code-execution shims from the package",
273
+ "summary": "The five vendored library bundles -- @noble/ciphers 2.2.0, @noble/curves 2.2.0, @noble/post-quantum 0.6.1, @simplewebauthn/server 13.3.2, and the @peculiar/x509 2.0.0 + pkijs 3.4.0 PKI meta-bundle -- now ship as unminified esbuild output of the same pinned upstream versions, so an operator can open any lib/vendor/*.cjs and read it, or diff it against upstream at the MANIFEST-pinned version, instead of auditing multi-hundred-KB minified lines. Their reflect-metadata dependency now resolves to upstream's lite build, which drops the legacy global-object probes (Function(\"return this\") and indirect eval) that could never execute on supported Node versions -- the published package now contains no eval or Function-constructor construct outside the documented worker-thread sandbox compiler. The vendor pipeline records the exact build invocation in MANIFEST.json, can rebuild the PKI meta-bundle at pinned component versions, and a codebase-patterns gate refuses any future refresh that reintroduces minified or dynamic-execution output under lib/vendor/. The repository supply-chain policy adds audited dispositions for shell, filesystem, and debug access, re-enables the eval alert class now that the package contains none, and narrows the minified-code disposition to the signed data payload carriers -- the code bundles themselves are enforced unminified by an in-repo gate.",
274
+ "sections": [
275
+ {
276
+ "heading": "Changed",
277
+ "items": [
278
+ {
279
+ "title": "Vendored bundles are unminified, reviewable esbuild output at the same pinned upstream versions",
280
+ "body": "lib/vendor/noble-ciphers.cjs, noble-curves.cjs, noble-post-quantum.cjs, simplewebauthn-server.cjs, and pki.cjs are rebuilt without minification from the same pinned upstream versions recorded in lib/vendor/MANIFEST.json (@noble/ciphers 2.2.0, @noble/curves 2.2.0, @noble/post-quantum 0.6.1, @simplewebauthn/server 13.3.2, @peculiar/x509 2.0.0 + pkijs 3.4.0). Exports, versions, and runtime behavior are unchanged; upstream license headers and esbuild's bundled-license footers are preserved. The five bundles grow from roughly 1.2 MB to 2.7 MB on disk (the npm tarball transfers compressed), in exchange for source an operator or scanner can actually review and diff against upstream."
281
+ },
282
+ {
283
+ "title": "reflect-metadata inside the WebAuthn and PKI bundles resolves to upstream's lite build, removing Function(\"return this\") and indirect-eval global probes",
284
+ "body": "The @simplewebauthn/server and PKI meta-bundles pull in reflect-metadata (decorator metadata for their ASN.1 schema serializers). The bundles now resolve it to the package's own ./lite entry at the same pinned version: an identical metadata API with the same cross-copy registry protocol, built for runtimes with native globalThis -- it contains none of the legacy global-object probes (Function(\"return this\") / indirect eval) that were unreachable dead code on the framework's Node floor anyway. With this change the published package contains no eval, indirect eval, or Function-constructor construct outside b.sandbox's worker-thread compiler, which is the documented isolation boundary for operator-submitted code."
285
+ },
286
+ {
287
+ "title": "Vendor pipeline: pinned PKI meta-bundle rebuilds, and the manifest bundler field derives from the actual build invocation",
288
+ "body": "scripts/vendor-update.sh now accepts the PKI meta-bundle's manifest version form directly (for example: vendor-update.sh peculiar-pki \"2.0.0+pkijs-3.4.0\") and installs exactly those @peculiar/x509 and pkijs component versions, where previously it could only bundle latest. The bundler field in lib/vendor/MANIFEST.json is now written by the script from the esbuild invocation that actually produced the artifact, so the recorded build command can no longer drift from the real one."
289
+ },
290
+ {
291
+ "title": "Package-scanner policy adds shell, filesystem, and debug access dispositions and re-enables the eval alert class",
292
+ "body": "socket.yml documents why shell access (all process execution goes through b.processSpawn -- fixed binary, argument array, never shell:true), filesystem access, and debug access (no debugger statements or node:inspector in first-party code; Reflect metadata in the vendored ASN.1 serializers is reflection, not code execution) are inherent to a server framework. The eval alert class is deliberately no longer suppressed: the package contains no eval or Function-constructor construct outside the documented sandbox worker, so a future occurrence is a genuine regression signal rather than recurring noise. The minified-code class stays dispositioned, narrowed to the signed data payload carriers (lib/vendor/*.data.js): minified-code detectors also fire on high-density embedded-asset files, which those carriers are by design -- a 76-char-wrapped base64 payload plus a multi-KB signature line, verified at every load. The unminified property of the code bundles is enforced by the repository's own gate, not by the scanner class."
293
+ }
294
+ ]
295
+ },
296
+ {
297
+ "heading": "Detectors",
298
+ "items": [
299
+ {
300
+ "title": "Vendored bundles must stay unminified and free of dynamic-code-execution constructs",
301
+ "body": "A codebase-patterns gate walks every JS artifact under lib/vendor/ and refuses minified bundle output (whole-file average line length, with wide margins around the measured unminified and minified populations) and any eval, indirect-eval, Function-constructor, createRequire, or process.binding token. A vendor refresh that reintroduces --minify or an upstream global-object eval probe fails the gate before it can ship."
302
+ },
303
+ {
304
+ "title": "Scanner policy keeps the minified-code class dispositioned while signed data carriers ship",
305
+ "body": "A companion gate refuses removing socket.yml's minifiedFile disposition while lib/vendor/*.data.js payload carriers ship: minified-code detectors match the carriers' high-density base64-plus-signature shape, and an omitted issue rule falls back to dashboard defaults, so dropping the entry would re-report the known-benign artifacts on every routine scan."
306
+ }
307
+ ]
308
+ }
309
+ ]
310
+ },
311
+ {
312
+ "version": "0.16.30",
313
+ "date": "2026-07-13",
314
+ "headline": "Reject a malformed IPv6 address whose '::' compresses no groups (and a leading-zero embedded-IPv4 tail) in the shared IP validator and the CIDR guard, and complete third-party license attribution for the vendored elliptic-curve library and the Public Suffix List",
315
+ "summary": "The shared IPv6 text parser accepted an address where a '::' sat next to a full eight explicit groups (for example 1:2:3:4:5:6:7:8::, ::1:2:3:4:5:6:7:8, or 1:2:3:4:5:6:7::8). RFC 4291 §2.2 defines '::' as an abbreviation for one or more all-zero groups, so those forms compress nothing and are not valid IPv6 text -- Node's built-in net.isIP rejects every one of them. The parser computed the number of groups to insert but only rejected a negative count, so a zero-insert '::' passed. Because each accepted spelling still normalizes to the same address as its canonical form, no allow/deny decision was bypassed, but the validator disagreed with net.isIP and with any strict peer parser on a whole class of inputs. b.mail (SPF/DMARC/RBL/greylist/HELO) and b.guardCidr both parse IPv6 through the affected code, so both now reject these forms. The embedded IPv4 tail of an IPv4-mapped address is also now validated with the framework's strict dotted-quad check, so a leading-zero, octal-ambiguous octet such as ::ffff:01.2.3.4 is refused rather than silently reinterpreted. Separately, the third-party NOTICE file was missing two vendored components that ship in the package -- the @noble/curves elliptic-curve library (MIT, used by the OPRF primitive) and the Mozilla Public Suffix List (MPL-2.0) -- both are now attributed, and a repository supply-chain policy records why the framework's inherent network, configuration, vendored-bundle, and data-file characteristics are expected.",
316
+ "sections": [
317
+ {
318
+ "heading": "Security",
319
+ "items": [
320
+ {
321
+ "title": "b.mail and b.guardCidr reject a malformed IPv6 address whose '::' compresses zero groups, matching RFC 4291 and net.isIP",
322
+ "body": "The shared IPv6 text parser inserted 8 - (left groups) - (right groups) zero groups for a '::' and rejected only a negative result, so an address with a full eight explicit groups adjacent to a '::' -- 1:2:3:4:5:6:7:8::, ::1:2:3:4:5:6:7:8, 1:2:3:4:5:6:7::8, 1:2:3:4::5:6:7:8 -- was accepted even though RFC 4291 §2.2 requires '::' to stand for at least one all-zero group and net.isIP rejects all of them. The parser now requires a '::' to insert at least one group. Every affected spelling normalized to the same address as its canonical form, so no CIDR or SPF/DMARC allow/deny decision was bypassed; the fix removes a parser divergence from the platform and peer parsers on a whole class of inputs. The same zero-group check was applied to the separate IPv6 parser inside b.guardCidr, and the embedded IPv4 tail of an IPv4-mapped IPv6 address is now validated with the framework's strict dotted-quad check so a leading-zero / octal-ambiguous octet (for example ::ffff:01.2.3.4) is refused instead of reinterpreted. Legitimate compressed and IPv4-mapped addresses are unchanged."
323
+ }
324
+ ]
325
+ },
326
+ {
327
+ "heading": "Changed",
328
+ "items": [
329
+ {
330
+ "title": "Complete third-party license attribution for the vendored @noble/curves library and the Public Suffix List, and refresh the vendored Public Suffix List",
331
+ "body": "The NOTICE file, which attributes every third-party component vendored under lib/vendor/, was missing two that ship in the package: the @noble/curves elliptic-curve library (MIT, used by the OPRF primitive) and the Mozilla Public Suffix List (MPL-2.0, used for organizational-domain derivation in DMARC alignment, BIMI scoping, and cookie-scope confinement). Both are now attributed; the Public Suffix List entry records that it is vendored verbatim under MPL-2.0 with its canonical source URL, satisfying MPL-2.0 §3.2. A stale vendored-library version in the same file was also corrected. The vendored Public Suffix List itself was refreshed to the current upstream snapshot so organizational-domain derivation reflects the latest registry delegations. A repository supply-chain policy also documents why the framework's inherent network egress, configuration reads, vendored minified bundles, and permissively- or open-data-licensed data files are expected characteristics rather than findings."
332
+ }
333
+ ]
334
+ }
335
+ ]
336
+ },
337
+ {
338
+ "version": "0.16.29",
339
+ "date": "2026-07-13",
340
+ "headline": "Reject a backup manifest's absolute, drive-letter, or NTFS-stream path at validation and resolve every path through the framework path-safety primitive on both backup and restore, and stop a bound-key auth middleware from hanging when a required peer certificate is absent",
341
+ "summary": "Two defects surfaced while covering the backup/restore path handling and the bound-key auth middleware. A backup manifest's file paths were checked only for a leading separator and .., so a Windows drive-letter path such as C:\\Windows\\evil, or a colon-bearing NTFS alternate-data-stream marker such as db.enc:evil, passed b.backupManifest.validate, and the backup and restore steps joined the caller- or manifest-declared relativePath and encryptedPath directly, without the framework's path-safety primitive -- so a path built from untrusted input could read a file outside dataDir on backup, or aim a restored file (or a blob read) outside the staging directory on restore. validate now rejects a colon anywhere (both the drive-letter prefix and the alternate-data-stream marker) alongside .. and a leading separator, and both b.backupBundle.create and b.restoreBundle resolve every manifest-declared path through b.safePath (which refuses traversal, absolute, drive-letter, UNC, NTFS alternate-data-stream, and reserved-name paths), so each sink is contained even if a path slips past the first-line check. Separately, b.middleware.requireBoundKey dereferenced req.peerCert.raw in its peer-certificate pinning branch when a fingerprint had been pre-attached by upstream mTLS but the certificate object was absent, throwing an uncaught TypeError that rejected the middleware promise -- the request hung with no response instead of a clean fail-closed denial; the branch now guards the certificate and denies.",
342
+ "sections": [
343
+ {
344
+ "heading": "Security",
345
+ "items": [
346
+ {
347
+ "title": "Backup and restore resolve every filesystem path through b.safePath, and b.backupManifest.validate rejects an absolute, drive-letter, or NTFS-stream path",
348
+ "body": "A backup manifest's per-file relativePath and encryptedPath were validated only against .. and a leading / or \\, so a Windows drive-absolute path (C:\\...) or an NTFS alternate-data-stream marker (db.enc:evil) passed b.backupManifest.validate, and the backup, restore, and storage-adapter steps built their filesystem paths with a plain path join of the caller- or manifest-supplied values -- meaning a path built from untrusted input could read a file outside dataDir on backup, aim a decrypted-file write or an encrypted-blob read outside the intended directory on restore, or escape the storage root through the filesystem adapter. validate() now rejects a colon anywhere (covering both the drive-letter prefix and the alternate-data-stream marker) alongside .. and a leading separator, and every filesystem sink -- b.backupBundle.create (the source read), b.restoreBundle (the encrypted-blob read and the restore destination), and the b.backup.bundleAdapterStorage.fsAdapter key resolver -- resolves its path through b.safePath, which refuses traversal, absolute, drive-letter, UNC, NTFS alternate-data-stream, and Windows reserved-name components and contains the result under its base. (The object-store storage adapter builds an object key, not a filesystem path, and keeps its traversal/NUL check -- a colon is a legal object-key character.) Legitimate relative paths back up and restore unchanged."
349
+ }
350
+ ]
351
+ },
352
+ {
353
+ "heading": "Fixed",
354
+ "items": [
355
+ {
356
+ "title": "b.middleware.requireBoundKey fails closed instead of hanging when a required peer certificate is absent",
357
+ "body": "In the peer-certificate pinning branch, the middleware read req.peerCert.raw without checking that req.peerCert was present. When an upstream mTLS layer had pre-attached a peer fingerprint (req.peerFingerprint) but no certificate object, that read threw an uncaught TypeError, which rejected the middleware's promise -- the request received no response and hung, rather than a clean fail-closed 401/403. The branch now checks for the certificate before dereferencing it and denies when it is missing, so a peer-cert-pinned key without a usable certificate is refused, not stalled."
358
+ }
359
+ ]
360
+ }
361
+ ]
362
+ },
363
+ {
364
+ "version": "0.16.28",
365
+ "date": "2026-07-13",
366
+ "headline": "Fix a Sieve filter bypass where an explicit :comparator silently disabled a header/address/envelope test, plus a JSONPath normalized-path round-trip fault and a rejected daemon cwd option",
367
+ "summary": "Three defects surfaced while covering the Sieve interpreter, JSONPath engine, and daemon supervisor. In b.mail.sieve, an explicit :comparator on a header, address, or envelope test silently disabled the whole test: the Sieve parser did not bind the comparator name to its tag, so the comparator string was consumed as the first positional argument (the header/address name) and every real argument shifted by one -- the test looked up a non-existent header, never matched, and the message fell through to implicit keep, so a discard / fileinto / redirect that should have fired instead delivered the message. The parser now binds :comparator to its tag (RFC 5228 §2.7.3), so the comparator is applied and the arguments stay aligned. b.jsonPath.paths emitted a normalized path containing raw control characters (only ' and \\ were escaped), which the query parser then rejected, so a path returned by paths() did not round-trip through query(); control characters are now escaped per RFC 9535 §2.7. And b.daemon.start rejected its own documented cwd option with an unknown-option error; cwd is now accepted and forwarded as the detached child's working directory.",
368
+ "sections": [
369
+ {
370
+ "heading": "Security",
371
+ "items": [
372
+ {
373
+ "title": "b.mail.sieve applies an explicit :comparator instead of silently disabling the test",
374
+ "body": "A Sieve script that used an explicit :comparator on a header, address, or envelope test -- for example header :comparator \"i;octet\" :is \"Subject\" \"...\" -- silently stopped filtering. The parser did not attach the comparator's value string to the :comparator tag, so it was consumed as the first positional argument (the header or address name) and shifted the real name and key list by one position. The test then queried a non-existent header, never matched, and evaluation fell through to implicit keep, so a discard, fileinto, or reject the operator intended was not applied and the message was delivered anyway -- a filter bypass for any rule with an explicit comparator. The parser now binds :comparator to its tag per RFC 5228 §2.7.3, so the chosen comparator (i;octet exact vs the default i;ascii-casemap case-insensitive) is applied and the positional arguments stay aligned; a :comparator with no following comparator-name string is refused at parse time."
375
+ }
376
+ ]
377
+ },
378
+ {
379
+ "heading": "Fixed",
380
+ "items": [
381
+ {
382
+ "title": "b.jsonPath.paths emits a normalized path that round-trips through b.jsonPath.query",
383
+ "body": "A normalized path returned by paths() escaped only the single-quote and backslash characters inside a member name, leaving raw control characters (newline, tab, and the rest of U+0000 through U+001F) in the output. The query parser rejects an unescaped control character in a name selector, so such a path could not be fed back into query(). Control characters in a normalized-path name are now escaped as their short form (\\b \\t \\n \\f \\r) or \\uXXXX, per RFC 9535 §2.7, so paths() output round-trips."
384
+ },
385
+ {
386
+ "title": "b.daemon.start accepts and applies the documented cwd option",
387
+ "body": "The start() options validator did not list cwd among the accepted options, so passing the documented cwd (the working directory for the detached child) was rejected with a daemon/bad-opts unknown-option error and the feature was unusable. cwd is now an accepted optional string and is forwarded to the detached child spawn as its working directory."
388
+ }
389
+ ]
390
+ }
391
+ ]
392
+ },
393
+ {
394
+ "version": "0.16.27",
395
+ "date": "2026-07-12",
396
+ "headline": "Fix b.pubsub pattern subscriptions silently dropping every message for a single-wildcard pattern such as orders.*.created",
397
+ "summary": "b.pubsub.subscribePattern with a * wildcard between dotted segments never matched any channel, so a pattern subscriber received nothing. The documented flagship pattern orders.*.created silently dropped orders.eu.created, and the same held for a.*.c, a.*.b.*.c, and any pattern whose wildcard sits between literal segments -- the source @example itself did not work. The segment matcher required a literal that follows a * to fit entirely before the next dot, but a trailing literal like .created contains a dot and legitimately begins before that boundary, so the match was rejected. The matcher now lets a post-wildcard literal begin at or before the next dot and matches it verbatim, while a single-segment * still never spans a dot -- so orders.*.created matches orders.eu.created but not orders.eu.fr.created or orders.created, and a pattern subscriber never receives a message from an extra path segment. Surfaced while covering the pub/sub matcher's branches.",
398
+ "sections": [
399
+ {
400
+ "heading": "Fixed",
401
+ "items": [
402
+ {
403
+ "title": "b.pubsub.subscribePattern matches a single-segment wildcard between literal segments",
404
+ "body": "A pattern with a * between dotted segments (the documented orders.*.created, or any a.*.c / a.*.b.*.c) matched no channel at all, so the pattern subscriber's handler never fired. The matcher rejected the literal that follows a wildcard unless it fit entirely before the next dot, but a trailing literal such as .created contains a dot and starts before that boundary, so a legitimate match was dropped. The literal after a wildcard is now allowed to begin at or before the next dot and is matched verbatim; the wildcard still matches exactly one non-dot segment, so orders.*.created matches orders.eu.created but not orders.eu.fr.created (two segments) or orders.created (none) -- a pattern subscription never widens to an extra path segment. Behaviour is now consistent with the documented example."
405
+ }
406
+ ]
407
+ }
408
+ ]
409
+ },
410
+ {
411
+ "version": "0.16.26",
412
+ "date": "2026-07-12",
413
+ "headline": "Stop b.db.exportCsv from crashing on an out-of-range timestamp value, and fix two CLI reporting faults (a vault-status stack trace and rollback-point metadata that never rendered)",
414
+ "summary": "Three defects surfaced while covering the db, cli, and http-client/mail-store error branches. b.db.exportCsv formatted a declared timestampFields column with new Date(v).toISOString(), which throws a RangeError for a finite millisecond value outside JavaScript's representable date range (beyond +/-8.64e15) and aborted the entire export; the one out-of-range value now degrades to its raw numeric string. The blamejs vault status command crashed with an uncaught VaultPassphraseError and a stack trace when the data directory did not exist, because the status path lacked the try/catch its sibling seal / unseal / rotate paths use; it now reports the same one-line reporter error. And blamejs restore list-rollbacks annotated each point from a non-existent recordedAt / bundleId, so the bundle id and timestamp never rendered; it now reads them from the fields b.restoreRollback.list returns (swappedAt, and bundleId / reason on the marker's operator metadata).",
415
+ "sections": [
416
+ {
417
+ "heading": "Fixed",
418
+ "items": [
419
+ {
420
+ "title": "b.db.exportCsv degrades an out-of-range timestamp value instead of aborting the export",
421
+ "body": "For a column named in timestampFields, exportCsv rendered a numeric value as new Date(value).toISOString(). A finite millisecond value beyond JavaScript's representable date range (greater than +/-8.64e15) produces an Invalid Date whose toISOString() throws a RangeError, which propagated out and aborted the whole export -- one out-of-range row could deny the entire CSV. Such a value now degrades to its raw numeric string, so the rest of the export completes."
422
+ },
423
+ {
424
+ "title": "b.cli vault status reports a clean error when the data directory is absent",
425
+ "body": "The vault status subcommand called the sealable/unsealable preflight checks without the try/catch that its sibling seal / unseal / rotate subcommands use, so a missing or unreadable data directory raised an uncaught VaultPassphraseError -- the CLI shim printed a stack trace and exited on an unhandled rejection instead of the one-line \"data-dir does not exist\" reporter error the other vault subcommands give. The status path now catches the preflight error and reports it cleanly."
426
+ },
427
+ {
428
+ "title": "b.cli restore list-rollbacks renders each rollback point's metadata",
429
+ "body": "The list-rollbacks row annotated each point with recordedAt and bundleId read from the top level of the point object, but b.restoreRollback.list returns { rollbackPath, swappedAt, marker } with the operator metadata (bundleId, reason) on marker.operator -- so those annotations were always empty. The listing now reads swappedAt from the point and bundleId / reason from the marker's operator metadata, so a rollback point's provenance is actually shown."
430
+ }
431
+ ]
432
+ }
433
+ ]
434
+ },
435
+ {
436
+ "version": "0.16.25",
437
+ "date": "2026-07-12",
438
+ "headline": "Correct the OTLP protobuf span encoder's dropped_links_count field number so a future non-zero count cannot corrupt the Status field",
439
+ "summary": "The OTLP/protobuf span encoder wrote the dropped_links_count placeholder on proto field 15, which is the Span's status field, instead of field 14 (dropped_links_count) per the OpenTelemetry trace proto. The emitted bytes are identical today because the count is always zero and proto3 omits zero-valued scalars, so no exported span is affected; but if a non-zero dropped-links count were ever emitted it would place a bare varint on the status field number and corrupt the Status message for a strict OTLP collector. The field number is corrected to 14. This surfaced while covering the OTLP exporter's previously-untested wire-format branches; a field-layout assertion now pins the span message's field numbers so a regression onto the status field is caught.",
440
+ "sections": [
441
+ {
442
+ "heading": "Fixed",
443
+ "items": [
444
+ {
445
+ "title": "b.observability OTLP protobuf span encoder emits dropped_links_count on the correct proto field",
446
+ "body": "In the protobuf span serializer, the dropped_links_count placeholder was encoded on field 15 -- the Span's status field, a length-delimited message -- rather than field 14, its correct number in the OpenTelemetry trace proto schema (the code's own inline comment already named field 14). Because the count is hardcoded to zero and proto3 omits zero-valued scalar fields, the serialized bytes are byte-identical today and no exported trace is affected. Were the placeholder ever set to a non-zero value (for example when span-link counting is added), each protobuf-encoded span would carry a wire-type-0 varint on the status field number and corrupt the Status message for a strict OTLP collector. The field number is now 14, and a wire-layout test pins the span message's field numbers (dropped_attributes_count at 10, dropped_events_count at 12, status length-delimited at 15) so this cannot regress silently."
447
+ }
448
+ ]
449
+ }
450
+ ]
451
+ },
452
+ {
453
+ "version": "0.16.24",
454
+ "date": "2026-07-12",
455
+ "headline": "Restore the file-persistence mode of the outbound HTTP cookie jar, which failed on its first run before any cookies were saved",
456
+ "summary": "b.httpClient.cookieJar.create({ persist: \"file\", file }) threw a CookieJarError with code LOAD_FAILED on the first run -- when the persist file does not yet exist -- instead of starting with an empty jar as documented (\"loaded at create() if the file exists\"). The load path recognized only a raw ENOENT for the missing file, but the safe reader it uses reports a missing file as the typed code atomic-file/enoent, so the normal first-use case fell through to LOAD_FAILED and the file-persistence mode never worked from a clean state. A missing persist file now starts an empty jar; a genuine read failure (symlink, over-size, or a concurrent-truncation TOCTOU) still fails closed as LOAD_FAILED. This surfaced while covering the cookie jar's previously-untested parse, match, and persistence branches.",
457
+ "sections": [
458
+ {
459
+ "heading": "Security",
460
+ "items": [
461
+ {
462
+ "title": "Continuous fuzzing now covers the DER/ASN.1, CMS, and HTTP Link-header parsers",
463
+ "body": "The DER/ASN.1 byte parser -- which sits under every untrusted-certificate path in the framework (peer TLS certificates, S/MIME, BIMI VMCs, CMS, ACME and TSA responses) -- the CMS SignedData / EnvelopedData parser (b.cms), and the HTTP Link response-header parser (b.linkHeader.parse) all consume attacker-controlled bytes, but because they are not named safe-* or guard-* they had sat outside the fuzz-harness discipline every other parser in the framework follows and had no fuzz coverage. Each now ships a coverage-guided fuzz harness run in CI, and the fuzz gate now requires a harness for an untrusted-byte parser regardless of its filename, so a regression that crashes one of these parsers instead of refusing the input cannot ship unnoticed. The current parsers refuse malformed input with typed errors under this fuzzing; the change is preventive coverage for the whole hostile-input parsing surface, not a fix for a present crash."
464
+ }
465
+ ]
466
+ },
467
+ {
468
+ "heading": "Fixed",
469
+ "items": [
470
+ {
471
+ "title": "b.httpClient.cookieJar file persistence starts an empty jar on first run",
472
+ "body": "A cookie jar created with persist: \"file\" loads the persist file at create() time. On the very first run the file does not exist yet, and the safe file reader surfaces that missing file as the typed code atomic-file/enoent; the jar's load handler only treated a raw ENOENT as \"start empty\", so every first use threw CookieJarError LOAD_FAILED and the advertised file-persistence mode could never be initialized. The handler now treats both the raw ENOENT and the typed atomic-file/enoent as an empty starting state, while any other read failure (a symlink where a regular file was expected, an over-size file, or a truncation race) still fails closed as LOAD_FAILED."
473
+ }
474
+ ]
475
+ }
476
+ ]
477
+ },
478
+ {
479
+ "version": "0.16.23",
480
+ "date": "2026-07-12",
481
+ "headline": "Merge an OpenID-Federation trust chain's per-level metadata policies into one combined policy and validate the operator combination, so a subordinate can only narrow a superior's constraint and cannot downgrade it through a different operator",
482
+ "summary": "b.auth.openidFederation.applyMetadataPolicy resolved a chain's metadata_policy by applying each level's block in sequence, anchor-first then leaf-ward, so a leaf-ward level could override a value the anchor pinned -- and even after that was closed, a subordinate could still escape a superior's constraint by expressing the override with a DIFFERENT operator, because the resolution never checked that the combined operator set was consistent. An attacker controlling an intermediate could pin token_endpoint_auth_method with value 'none' against an anchor's one_of ['private_key_jwt'], widen a scope set past an anchor's subset_of ceiling with add, or replace an anchor's superset_of-mandated grant with a weaker one via value -- each resolved to the downgraded metadata with no error. OpenID Federation 1.0 §6.1.3 defines a fixed operator processing order and a set of operator-combination rules (value combines only with essential; add must stay within subset_of and cover superset_of; default must satisfy a co-present one_of / subset_of / superset_of; one_of does not combine with the array operators; superset_of must be within subset_of). applyMetadataPolicy now gathers every superior-signed subordinate statement's policy for the metadata kind, merges them superior-first (value / default equal-or-conflict, one_of / subset_of intersect, add / superset_of union, essential OR), validates the merged operator combination for every claim -- refusing the chain when a subordinate's operator is inconsistent with a superior's -- and applies the combined policy once in the fixed operator order so the value-check operators validate the final value. Consistent narrowings (a subordinate tightening a one_of or subset_of, adding within a subset_of, defaulting within a one_of) are unaffected.",
483
+ "sections": [
484
+ {
485
+ "heading": "Security",
486
+ "items": [
487
+ {
488
+ "title": "b.auth.openidFederation resolves metadata policy by merging every chain level and validating the operator combination, blocking cross-operator trust downgrades",
489
+ "body": "applyMetadataPolicy walked the chain applying each level's metadata_policy block in turn, so the leaf-ward block won any disagreement -- a subordinate could override a value the anchor pinned. Merging the levels closes the same-operator case, but a subordinate could still escape a superior's constraint expressed with a different operator: an anchor's one_of ['private_key_jwt'] on token_endpoint_auth_method was overridden by a leaf-ward value 'none' (client authentication removed at the token endpoint), an anchor's subset_of scope ceiling was widened by a subordinate add, and an anchor's superset_of-mandated grant was dropped by a subordinate value substituting the implicit flow -- each resolved to the downgraded metadata with no error, because the resolution validated only same-operator value/default conflicts and applied operators in object-key order rather than the OpenID Federation 1.0 §6.1.3 fixed order, so the value-check operators ran before the value they were meant to guard was set. Policy resolution now gathers each superior-signed subordinate statement's policy for the metadata kind (the leaf's own self-published policy is never read), merges them superior-first (value / default must agree across levels or the chain is refused, one_of / subset_of intersect, add / superset_of union, essential OR), and applies the single combined policy in the OpenID Federation 1.0 §6.1.3 fixed operator order (value, add, default, then the one_of / subset_of / superset_of / essential checks) so the merged, most-restrictive constraints validate the final value. A value outside a merged one_of, an add that widens past a merged subset_of, or a value that drops a superset_of-mandated member is therefore refused; a subordinate cannot widen an exact pinned value by adding a value not already in it; and a one_of whose values are disjoint across chain levels leaves no admissible value and is refused. A subordinate can only narrow a superior's constraint, never override it through any operator; a value pinned inside a superior's allowed set, and consistent narrowings generally, are unaffected. Resolution also refuses a prototype-pollution key (__proto__, constructor, prototype) in a policy block's claim names or in the base metadata, since those are never valid metadata parameter names and merging them into the combined policy would write Object.prototype."
490
+ },
491
+ {
492
+ "title": "b.auth.openidFederation enforces the array metadata-policy operators on a claim the entity declares as a space-delimited string",
493
+ "body": "The subset_of metadata-policy operator restricts an array-valued claim (grant_types, response_types, redirect_uris, and similar) to an allow-list, but the check only ran when the claim was already an array. Because an entity fully controls its own self-published metadata, a leaf could declare a subset_of-constrained claim as a single string and the array guard skipped, so the anchor's allow-list was silently not enforced and the entity kept a value the trust anchor forbade. The array operators (subset_of, superset_of, add) now process a space-delimited string as an array of strings, per OpenID Federation 1.0 §6.1.3.1.8 -- so an OAuth scope like \"openid email\" is checked (and each token in a leaf's string is validated against the allow-list, catching a smuggled value), while a genuine non-array, non-string value is refused. An absent claim under subset_of is still left absent, since subset_of constrains a value that is present rather than requiring presence."
494
+ }
495
+ ]
496
+ }
497
+ ]
498
+ },
499
+ {
500
+ "version": "0.16.22",
501
+ "date": "2026-07-12",
502
+ "headline": "Bind an OpenID-Federation entity's configuration to the keys its superior attests, bind the COSE null-digest signature algorithms (EdDSA / ML-DSA-87) to the key type, make the verifiable-credential COSE media-type header mandatory, and turn two PGP parse crashes into typed refusals",
503
+ "summary": "Covering untested adversarial branches across the federation, COSE, verifiable-credential, and PGP verifiers surfaced a set of fail-open and crash defects, fixed at the root. b.auth.openidFederation.buildTrustChain verified each entity configuration only against its own self-published keys (integrity, not provenance) and never re-verified it against the keys the superior's subordinate statement attests, so an attacker controlling an entity's .well-known endpoint (but not its attested signing key) could serve forged metadata and have the chain accept it; each non-anchor entity's configuration is now bound to its superior-attested keys. b.cose.verify bound only the ECDSA algorithms to their curve and left the null-digest algorithms (EdDSA, ML-DSA-87) with no key binding, so node:crypto -- which dispatches on the key type, not the declared COSE alg -- accepted an EdDSA signature under an ML-DSA-only allowlist against an Ed25519 key (and the reverse); every signable COSE algorithm is now bound to its key type or curve, on both sign and verify. b.vc enforced the vc+cose / vp+cose media-type header only when present, so a COSE credential omitting it was accepted with no media-type binding; the header is now mandatory, symmetric with the JOSE path. And two PGP parsers (an rsa-pss key type that cannot express the module's signatures, and a truncated post-quantum decrypt envelope) crashed with an untyped Error instead of a typed refusal.",
504
+ "sections": [
505
+ {
506
+ "heading": "Security",
507
+ "items": [
508
+ {
509
+ "title": "b.auth.openidFederation binds each entity configuration to its superior-attested keys",
510
+ "body": "buildTrustChain verified every entity configuration against its own self-published JWKS (integrity) and separately verified the superior-signed subordinate statement, but never re-verified an entity's own configuration -- the source of the effective metadata resolveLeaf returns -- against the keys the superior attests. An attacker who controls an entity's <entity_id>/.well-known/openid-federation endpoint but not its attested signing key could serve a self-signed configuration carrying forged metadata and attacker keys; the honest superior's subordinate statement pinned the entity's real keys, the chain was accepted, and the forged metadata was returned (OpenID Federation 1.0 §9 requires the leaf/intermediate configuration to be verified with the JWKS the superior pins). Each non-anchor entity's configuration is now verified against its superior-attested JWKS before those keys are adopted, so the operator-pinned trust anchor flows down to gate every entity's own configuration; the anchor is already verified against the operator-pinned keys."
511
+ },
512
+ {
513
+ "title": "b.cose binds the null-digest signature algorithms (EdDSA / ML-DSA-87) to the key type",
514
+ "body": "verify() honored the declared COSE algorithm allowlist but bound only the ECDSA algorithms to their curve; the null-digest algorithms (EdDSA -8, ML-DSA-87 -50) had no key binding at all. node:crypto.verify(null, ...) dispatches on the key type and ignores the declared COSE alg, so an EdDSA signature was accepted and reported as ML-DSA-87 when the verifier's allowlist was PQC-only and it held an Ed25519 key (and the reverse), fully bypassing the algorithm allowlist; sign() had the mirror defect and could emit an EdDSA-labeled token signed with an ML-DSA key. Every signable algorithm is now bound to its key -- ECDSA by curve, EdDSA / ML-DSA-87 by key type -- on both sign and verify. The AEAD and HMAC paths bind the algorithm into the AAD / MAC structure, so no key-dispatch confusion exists there."
515
+ },
516
+ {
517
+ "title": "b.vc requires the COSE media-type header on a verifiable credential and presentation",
518
+ "body": "The COSE verify path enforced the vc+cose / vp+cose type header (RFC 9596) only when it was present, while the JOSE path mandated it -- so an issuer-signed COSE credential or presentation that omitted the header was accepted with no binding to its media type (a cross-media-type-confusion fail-open on the verify seam, where two securing forms diverged on the same should-be-mandatory check). The COSE path now requires the type header unconditionally, symmetric with the JOSE path; b.vc.issue / b.vc.present always emit it, so no round-trip regresses."
519
+ }
520
+ ]
521
+ },
522
+ {
523
+ "heading": "Fixed",
524
+ "items": [
525
+ {
526
+ "title": "b.mail.crypto.pgp turns an rsa-pss key and a truncated decrypt envelope into typed refusals",
527
+ "body": "Two adversarial inputs escaped as an untyped Error (which an operator catch keyed on b.mail.crypto.isMailCryptoError never catches) instead of the module's typed refusal. An rsa-pss key type was declared-accepted by sign() and verify() but cannot express the module's PKCS#1-v1.5 signatures and crashes in the shared JWK extraction before any crypto runs; it is now refused as a typed bad-key-type / key-alg-mismatch. And the post-quantum decrypt envelope parser advanced its offset using attacker-controlled length fields without bounds-checking, so a truncated envelope threw a RangeError; every read and slice in the envelope parser is now bounds-checked and a truncated envelope fails closed with a typed refusal. Valid signatures and envelopes are unaffected."
528
+ },
529
+ {
530
+ "title": "b.vc rejects a non-XSD validity date",
531
+ "body": "The validFrom / validUntil parser used Date.parse, which is lenient -- a bare year, a slash-date, a date without a time, and locale strings all parsed to some instant and were given a heuristic validity window, weakening the expiry decision the verifier relies on (Verifiable Credentials Data Model 2.0 §4.9 requires an XSD dateTime). The parser now asserts an XSD dateTime shape before parsing, so a non-conformant validity value fails closed at both issue and verify."
532
+ },
533
+ {
534
+ "title": "b.audit.checkpoint is idempotent on an already-anchored tip",
535
+ "body": "Anchoring a checkpoint read the audit-log tip, optionally skipped if unchanged, then inserted a checkpoint row whose atMonotonicCounter is unique -- but the read and the insert were not atomic, so two checkpoints of the same tip (concurrent anchors, or a second checkpoint() call without skipIfUnchanged) both computed the same counter and the loser threw a raw UNIQUE-constraint error instead of the documented no-op. Because two anchors of one tip sign the identical payload, the collision means the counter is already anchored: checkpoint() now returns null in that case (matching the skipIfUnchanged already-anchored path) and any unrecognized database error still surfaces."
536
+ }
537
+ ]
538
+ }
539
+ ]
540
+ },
541
+ {
542
+ "version": "0.16.21",
543
+ "date": "2026-07-12",
544
+ "headline": "Bind the declared JWS/HTTP-signature algorithm to the key's real type across b.auth.jwt and b.crypto.httpSig (closing an algorithm-confusion allowlist bypass), refuse a JWT whose payload is not a JSON object in b.guardJwt, and enforce the SAML InResponseTo replay check on the holder-of-key confirmation",
545
+ "summary": "Covering untested adversarial branches across the token and assertion verifiers surfaced a set of fail-open auth defects, fixed at the root. b.auth.jwt.verify gated its algorithm allowlist on the self-declared JWS alg header while node:crypto verified against the key's intrinsic type, so a token signed by an ML-DSA key but declaring a SLH-DSA alg passed a SLH-only allowlist and verified against the ML-DSA key -- a full algorithm-allowlist bypass (CWE-347). The declared alg is now bound to the key's real type at both sign and verify. The same class existed unenforced in b.crypto.httpSig (a peer could sign with a classical ed25519 key while declaring the PQC alg ml-dsa-65, emitting an authenticated but false PQC-signed label); it now binds the alg to the key's type at sign and verify too. b.guardJwt skipped its entire claim-sanity block (required-claims + exp/nbf/iat) when the payload did not decode to a JSON object, so a token carrying no readable claims set was admitted at strict; a non-object payload is now refused symmetrically with the header. And b.auth.saml validated InResponseTo on the Bearer subject-confirmation but not the holder-of-key one, silently dropping an operator's replay binding on every HoK assertion; the holder-of-key path now applies the identical check.",
546
+ "sections": [
547
+ {
548
+ "heading": "Security",
549
+ "items": [
550
+ {
551
+ "title": "b.auth.jwt binds the declared algorithm to the key's real type (algorithm-confusion bypass)",
552
+ "body": "verify() constrained the algorithm allowlist against the self-declared JWS `alg` header -- a value the token issuer controls -- while node:crypto.verify(null, ...) selects the actual algorithm from the KeyObject's intrinsic type. The two could diverge: a token signed by an ML-DSA-87 key but declaring alg=\"SLH-DSA-SHAKE-256f\" passed an SLH-only allowlist and was verified against the ML-DSA public key, and sign() could emit a token whose header alg misstated its own signing algorithm -- the exact mislabeled artifact the verify bypass consumes (CWE-347 algorithm confusion). Both sign() and verify() now assert the declared alg matches the resolved key's asymmetricKeyType, so the allowlist genuinely constrains which algorithm authenticated the token; a matched alg/key round-trip is unaffected."
553
+ },
554
+ {
555
+ "title": "b.crypto.httpSig binds the declared algorithm to the key's real type",
556
+ "body": "HTTP Message Signature sign() and verify() validated the declared `alg` against the supported set but never bound it to the key's type -- so a peer could sign with a classical ed25519 key while declaring the post-quantum alg ml-dsa-65 (an authenticated but false PQC-signed label a verifier trusting the label would honor), the same algorithm-confusion class. sign() now refuses to emit a token whose alg misstates the private key's type, and verify() refuses -- before the crypto check -- a resolved key whose type differs from the declared alg. Legitimate ed25519 and ml-dsa-65 round-trips are unaffected."
557
+ },
558
+ {
559
+ "title": "b.guardJwt refuses a JWT whose payload is not a JSON object",
560
+ "body": "The guard skipped its entire payload claim-sanity block -- required-claims and exp/nbf/iat -- whenever the payload segment did not decode to a JSON object (undecodable base64url, non-JSON bytes, a JSON primitive, or a JSON array), so a token carrying no readable claims set was admitted at the strict profile (validate returned ok and gate returned serve), defeating the guard's advertised missing-claim refusals. A non-object payload is now refused with a high-severity payload-decode finding, symmetric with the header-decode path, at every profile."
561
+ },
562
+ {
563
+ "title": "b.auth.saml enforces the InResponseTo replay check on holder-of-key confirmations",
564
+ "body": "verifyResponse validated the SubjectConfirmationData InResponseTo against the expected AuthnRequest ID on the Bearer confirmation path but not the holder-of-key one, so an operator that opted into InResponseTo binding (solicited-response / replay correlation) silently lost it on every holder-of-key assertion -- a HoK assertion with a mismatched or absent InResponseTo was accepted, and the returned value was hardcoded to null. The holder-of-key path now applies the identical constant-time InResponseTo check the Bearer path uses and echoes the validated value, so the replay binding holds on both confirmation methods."
565
+ }
566
+ ]
567
+ }
568
+ ]
569
+ },
570
+ {
571
+ "version": "0.16.20",
572
+ "date": "2026-07-12",
573
+ "headline": "Make the ClamAV scan-verdict classifier fail closed on a coalesced infected+clean reply, refuse a session under a strict device-binding threshold when the anomaly score can't be computed, guard the MessageFormat select renderer against prototype-chain keys, and reject a non-canonical webhook timestamp",
574
+ "summary": "Covering untested adversarial branches across the mail scanner, the session device-binding verifier, the i18n message renderer, and the webhook verifier surfaced a set of fail-open and correctness defects, fixed at the root. The mail scanner's ClamAV INSTREAM reply classifier tested the benign OK token before the malign FOUND token, so a reply carrying both (a coalesced or stale-then-fresh reply on a reused connection) resolved to clean and delivered an infected message; the classifier now tests FOUND, then ERROR, then OK, so a malign signal always dominates and only an OK with no FOUND/ERROR is clean. Session verification under a strict maxAnomalyScore device-binding policy admitted a relocated device whenever no decisive anomaly score could be produced (no scorer supplied, or a scorer that threw or returned a non-finite value) -- it now fails closed, matching the unreadable-binding discipline. The ICU-style MessageFormat select renderer looked up an end-user-supplied select value with a bare object index, so a value naming an Object.prototype member (__proto__ / constructor / toString) returned an inherited member and either rendered garbage or threw a render-time DoS; every case lookup is now own-property only. And the webhook verifier accepted any string that Number() maps to the signed timestamp (scientific, trailing .0, hex, leading zero/plus), making the authenticated t= field malleable; it now requires the canonical decimal.",
575
+ "sections": [
576
+ {
577
+ "heading": "Security",
578
+ "items": [
579
+ {
580
+ "title": "b.mail.scan fails closed on a coalesced infected+clean ClamAV reply",
581
+ "body": "The ClamAV INSTREAM reply classifier tested the benign `stream: OK` token before the malign `... FOUND` token, so a reply that carried both -- a coalesced or stale-then-fresh reply on a reused connection, or an intermediary that concatenates two responses -- resolved to a clean verdict and delivered an infected message (a scan bypass). The classifier is a security verdict, so it now tests FOUND (infected) first, then ERROR (do-not-deliver), then OK (clean); an unrecognized shape is still an error. A reply containing a FOUND signal can no longer be downgraded to clean by an adjacent OK token."
582
+ },
583
+ {
584
+ "title": "b.session.verify fails closed under a strict anomaly threshold when the score can't be computed",
585
+ "body": "Under the strict maxAnomalyScore device-binding policy, verify() only assigned a fingerprint anomaly score when the supplied scorer returned a finite number. With no scorer (the option is documented as pairing with one but is omittable), or a scorer that threw or returned a non-finite value, the score stayed null and the refusal guard (score present AND above threshold) was false, so verify RETURNED a session bound to one device when presented from another. An uncomputable anomaly score on genuine fingerprint drift now fails closed (verify returns null), matching the fail-closed discipline the unreadable-binding branch already applied; the legitimate path (a finite score at or below the threshold admits, with the score surfaced) is unchanged."
586
+ },
587
+ {
588
+ "title": "b.i18n MessageFormat select renderer guards against prototype-chain keys",
589
+ "body": "The ICU-style MessageFormat select renderer resolved a case with a bare object index on an end-user-supplied select value, so a value naming an Object.prototype member -- __proto__ / constructor / toString / hasOwnProperty / valueOf -- returned a truthy INHERITED member, bypassing the `other` fallback: an inherited value with no length rendered as empty (silent output corruption), and an inherited function threw when rendered as a non-array (a request-level DoS). Every select/plural case lookup is now own-property only, so an attacker-supplied select value that names a prototype member falls through to the `other` case."
590
+ }
591
+ ]
592
+ },
593
+ {
594
+ "heading": "Fixed",
595
+ "items": [
596
+ {
597
+ "title": "b.webhook verifier rejects a non-canonical authenticated timestamp",
598
+ "body": "The verifier parsed the authenticated t= field with Number() and validated only the post-coercion integer, then re-composed the signed string from that integer -- so any string Number() maps to the same value (scientific 1.7e9, a trailing .0, hex 0x..., a leading zero/plus/whitespace) re-canonicalized into the identical signed string and verified, making the authenticated timestamp field malleable. The verifier now also requires the raw bytes to equal the canonical decimal the signer emits, the same strict digits-only parse the Stripe-compatible verifier already applied; the only legitimate producer (b.webhook.signer) emits the canonical form, so there is no interop cost."
599
+ },
600
+ {
601
+ "title": "b.i18n.dir honors a custom RTL language configured in any case",
602
+ "body": "dir() folds the requested locale's primary subtag to lower case before the RTL-membership test, but a custom rtlLanguages list was stored verbatim, so an operator-supplied entry like \"AR\" or \"CKB\" never matched the lower-cased lookup and the language rendered left-to-right. The configured list is now folded to lower case the same way the lookup is, so a custom RTL entry matches regardless of case."
603
+ },
604
+ {
605
+ "title": "b.i18n.t resolves a leaf shadowed by a namespace in a more-specific locale",
606
+ "body": "The fallback-chain lookup returned the first locale where the dotted key resolved to any non-undefined value, including a nested namespace object -- so a namespace at that path in the requested locale halted the chain and shadowed a real translation leaf defined in a fallback locale, leaking the raw key into the UI (and has() reported false). The lookup now treats only a renderable leaf (a string or a plural-shaped object) as a hit and keeps walking the chain past a namespace object, so a leaf in a fallback locale resolves."
607
+ }
608
+ ]
609
+ },
610
+ {
611
+ "heading": "Detectors",
612
+ "items": [
613
+ {
614
+ "title": "MessageFormat case lookups must be own-property",
615
+ "body": "A structural check asserts that the MessageFormat renderer resolves a select/plural case through the own-property helper rather than a bare object index, so an end-user-supplied select value can never reach the prototype chain and re-open the corruption/DoS class."
616
+ }
617
+ ]
618
+ }
619
+ ]
620
+ },
621
+ {
622
+ "version": "0.16.19",
623
+ "date": "2026-07-12",
624
+ "headline": "Close a family of entity- and whitespace-hidden dangerous-URL-scheme and CSS-injection bypasses across b.guardHtml / b.guardSvg / b.guardMarkdown behind one shared normalizer, bound a DNS decompression-pointer name bomb, refuse an ambiguous audit-anchor canonicalization, and stop an empty-string sealed field from crashing an AAD-table write",
625
+ "summary": "Covering untested adversarial branches across the content guards, a DNS wire parser, the audit-log anchor, and the sealed-field codec surfaced a family of fail-open defects, fixed at the root. The most serious span the content-guard URL-scheme and CSS-injection denylists: a browser removes tab/lf/cr from anywhere in a URL and trims a leading/trailing control-or-space run before resolving the scheme, and character-reference-decodes a style attribute before the CSS parser sees it, so an entity-encoded tab, an entity-encoded leading space, an HTML named entity, or an entity-encoded CSS token could all navigate/execute while reading past a denylist that matched the raw bytes. b.guardHtml missed the whitespace normalization entirely and matched CSS against raw bytes; b.guardMarkdown decoded only numeric entities; b.guardSvg had a partial fix. All three now route their scheme and CSS-token checks through one pair of shared codepoint-class normalizers, so no guard can strip a different set of encodings than another. A DNS compression-pointer chain could decompress a single name past the RFC 1035 255-octet / label caps (a decompression-amplification DoS); the audit-log anchor's newline-delimited signed bytes let one signature validate for several different tip/previous-tip splits (defeating the linkage tamper-evidence); an SVG animation element permitted under a permissive profile lost its open tag; and an empty-string sealed column crashed an AAD-table insert.",
626
+ "sections": [
627
+ {
628
+ "heading": "Security",
629
+ "items": [
630
+ {
631
+ "title": "b.guardHtml / b.guardSvg / b.guardMarkdown refuse entity- and whitespace-hidden dangerous URL schemes",
632
+ "body": "The URL-scheme denylist on every URL-bearing attribute (href / xlink:href / markdown link, image, autolink, reference-definition) is resolved after normalizing the value the way a browser does: character-reference decoding (numeric AND the HTML5 named-entity ASCII subset browsers honor, e.g. &Tab; / &colon;), removing tab/lf/cr from anywhere, and trimming a leading/trailing C0-control-or-space run. Previously b.guardHtml stripped neither tab/lf/cr nor an entity-encoded leading space, and b.guardMarkdown decoded only numeric entities, so payloads such as java&#9;script:, java&Tab;script:, and &#32;javascript: resolved to an empty scheme and were served/rendered as safe while a browser executed them as javascript:. The normalization now lives in two shared codepoint-class primitives (b.codepointClass.decodeMarkupEntities and b.codepointClass.stripUrlSchemeWhitespace) that all three guards compose, so no guard can fold away a different set of encodings than another."
633
+ },
634
+ {
635
+ "title": "b.guardHtml / b.guardSvg detect entity-encoded and whitespace-hidden CSS injection in style attributes",
636
+ "body": "A style attribute is HTML/XML character-reference-decoded before the CSS parser sees it, so width:ex&#x70;ression(, background:url(&#x6A;avascript:...), and behavior&colon;url(...) reach CSS as expression( / url(javascript:...) / behavior: with no literal danger token in the raw bytes. The CSS-danger check matched the raw attribute value, so these bypassed the denylist and were served verbatim (a stored CSS-injection XSS). Both guards now match the entity-decoded value AND fold the URL-scheme whitespace a browser strips inside url(...) (tab / lf / cr), so an entity-hidden tab in a CSS URL scheme (url(java&Tab;script:) reaching CSS as url(java<TAB>script:), which navigates as javascript:) can no longer defeat the contiguous javascript: pattern either. A plain (unencoded) dangerous style is still caught and a benign style is untouched."
637
+ },
638
+ {
639
+ "title": "b.safeDns bounds a decompressed name across the whole compression-pointer chain",
640
+ "body": "readName tracked its per-name octet and label budgets per stack frame, so each compression-pointer jump restarted a fresh RFC 1035 255-octet / label budget and a pointer was charged as only its 2 on-wire bytes. A crafted pointer chain therefore decompressed a single name well past the advertised 255-octet / label caps (a decompression-amplification DoS on untrusted DNS responses). The octet and label accountants now thread through the whole pointer chain, so the composite decompressed name is bounded by the same caps that bound a single in-line name; independent names are still measured independently and no name that decompresses within the caps is newly rejected."
641
+ },
642
+ {
643
+ "title": "b.auditSign refuses an anchor whose fields carry the record delimiter",
644
+ "body": "The chain-anchor signed bytes are a newline-delimited layout of format / counter / tipHash / prevTipHash / createdAt. A literal newline inside any operator-influenced string field (format / tipHash / prevTipHash) let content migrate across a field boundary without changing the signed bytes, so one signature was valid for several different { tipHash, prevTipHash } splits -- an attacker could rewrite a stored anchor's apparent tip/linkage while keeping verify() happy, defeating the prevTipHash linkage tamper-evidence. anchor() now refuses a delimiter-bearing field at sign time and verifyAnchor refuses one at verify time, so an ambiguous anchor can neither be minted nor accepted; every legitimate (delimiter-free) anchor is byte-identical and unaffected."
645
+ }
646
+ ]
647
+ },
648
+ {
649
+ "heading": "Fixed",
650
+ "items": [
651
+ {
652
+ "title": "b.guardSvg preserves a profile-permitted safe animation element",
653
+ "body": "Under a permissive profile that allows animation, a safe animation element (for example an <animate> whose attributeName targets a visual property) had its open tag dropped while its end tag was still emitted, leaving an orphan close tag and silently stripping an element the profile permits. The sanitize path now affirmatively re-permits the safe-target case; an unsafe-target animation (attributeName=href and similar) is still dropped and neutralized."
654
+ },
655
+ {
656
+ "title": "b.cryptoField.sealRow seals an empty-string field as a tamper-evident envelope",
657
+ "body": "sealRow dispatched every non-null value -- including an empty string -- to one of three envelope-seal branches that disagreed on empty plaintext: the plain and per-row-key branches handled it, but the AAD branch is fail-closed and threw, so a write to an AAD-sealed table whose sealed column held an empty string crashed the insert. An empty string now encodes to a non-empty typed marker before sealing, so it becomes a real authenticated envelope (never a bare plaintext empty string) that round-trips to an empty string on read. On the read side, a bare empty string found in an AAD-bound or per-row-key sealed column -- which after this change can only be an envelope-downgrade by a DB-write attacker -- fails closed (the cell is nulled and audited) instead of being accepted as a valid empty value, so the sealed-column tamper-evidence guarantee holds for empty values too."
658
+ }
659
+ ]
660
+ },
661
+ {
662
+ "heading": "Detectors",
663
+ "items": [
664
+ {
665
+ "title": "Guard scheme extractors and CSS-danger checks must compose the shared normalizers",
666
+ "body": "Two structural checks assert that a content guard which resolves a URL scheme for a denylist routes the decoded value through b.codepointClass.stripUrlSchemeWhitespace, and that a guard's CSS-danger check matches the entity-decoded style value via b.codepointClass.decodeMarkupEntities -- so a future guard cannot silently fold away a narrower set of encodings than the browser and re-open the bypass class."
667
+ }
668
+ ]
669
+ }
670
+ ]
671
+ },
672
+ {
673
+ "version": "0.16.18",
674
+ "date": "2026-07-12",
675
+ "headline": "Refuse a stale FIDO metadata BLOB on the operator-fetch path, harden the GraphQL DoS-shape guard against escaped strings and malformed queries, parse POSIX leading-space octal in tar headers, and fix the FIDO certified-level, self-update 304, and swap-maxBytes paths — surfaced by covering previously-untested error and adversarial branches",
676
+ "summary": "Covering untested error and adversarial branches across four security-relevant primitives surfaced seven defects, fixed at the root. The most serious: b.auth.fidoMds3.fetch() reimplemented the metadata-BLOB validation inline and omitted the stale-BLOB refusal the internal path enforced — so a signed-but-expired BLOB (nextUpdate in the past) was accepted and cached, letting an attacker who serves an ancient correctly-signed BLOB freeze an operator's revoked/compromised-authenticator list at a time of their choosing. Both paths now route through one _verifyBlobWithRoots source of truth, so no check can be present on one and missing on the other. The GraphQL query-shape guard (the pre-schema depth/alias DoS walker) miscounted on a string literal containing an escaped quote and on a brace-unbalanced malformed query, weakening the depth/alias limits it exists to enforce. The FIDO certified-level resolver lexically compared status reports and let an undated later report (a decertification or downgrade) lose to an earlier dated one, so a now-decertified authenticator could still read as certified. A tar reader misparsed POSIX leading-space-padded octal header fields to 0 (a size misparse desynced the block walker and rejected archives other tars extract). self-update's documented 304 If-None-Match fast-path was dead (it threw on every unchanged conditional poll), and selfUpdate.swap read an opts.maxBytes it never declared, so it could refuse a large binary that selfUpdate.verify accepted.",
677
+ "sections": [
678
+ {
679
+ "heading": "Security",
680
+ "items": [
681
+ {
682
+ "title": "b.auth.fidoMds3.fetch refuses a stale (expired) metadata BLOB",
683
+ "body": "The operator-facing fetch path (which trusts caCertificate roots) reimplemented the metadata-BLOB parse and verify inline and omitted the stale-BLOB refusal that the default-roots path enforced, so a BLOB whose nextUpdate is already in the past was accepted and cached even though FIDO MDS3 §3.1.7 says it must not be trusted. An attacker serving an ancient, correctly-signed BLOB could pin an operator to a revoked/compromised-authenticator list frozen at that time. The JWS + x5c-chain verify, payload-shape checks, and stale-BLOB refusal now live in one _verifyBlobWithRoots helper that both the operator-fetch and default-roots paths route through, so the checks can never drift apart again."
684
+ },
685
+ {
686
+ "title": "b.guardGraphql hardens the pre-schema DoS-shape walker against escaped strings and malformed queries",
687
+ "body": "The query-shape walker that enforces depth and alias limits before the query reaches a schema miscounted in two ways: it treated an escaped quote inside a valid GraphQL string literal as the string's terminator (desyncing its in-string state), and it popped its depth-indexed alias counter on every closing brace even for a brace-unbalanced malformed query (underflowing the counter). Both weakened the depth/alias caps the guard exists to enforce against a DoS-shaped query. The walker now tracks string escapes correctly and guards the alias-counter stack against imbalance."
688
+ }
689
+ ]
690
+ },
691
+ {
692
+ "heading": "Fixed",
693
+ "items": [
694
+ {
695
+ "title": "b.auth.fidoMds3 certified-level honors an undated later decertification or downgrade",
696
+ "body": "_certifiedLevel compared status reports lexically and coerced a missing effectiveDate to an empty string, which sorts before any real date — so a later report with no effectiveDate (a NOT_FIDO_CERTIFIED decertification, or a downgrade/upgrade) always lost to an earlier dated grant, and certifiedLevel froze at the stale, higher historical value. A step-up policy reading certifiedLevel >= N could therefore accept a now-decertified authenticator. Reports without a comparable date now fall back to array order (append order = chronological), so a later decertification/downgrade wins."
697
+ },
698
+ {
699
+ "title": "b.archive.read.tar parses POSIX leading-space-padded octal header fields",
700
+ "body": "A tar numeric header field (mode / uid / gid / size / mtime / checksum) that is left-padded with ASCII spaces — POSIX-legal, and emitted by star / BSD / Java / Perl tars — was misparsed to 0 because the octal reader treated a leading space as a field terminator. A misparsed size desynced the 512-byte block walker (it read the file body as the next header) and rejected an archive other tars extract cleanly. The reader now skips leading-space padding before reading octal digits."
701
+ },
702
+ {
703
+ "title": "b.selfUpdate.poll returns a no-update result on a 304 Not Modified",
704
+ "body": "The documented If-None-Match / ETag fast-path was unreachable: poll delegated HTTP status handling to the HTTP client, which rejects every non-2xx (304 included) as an error before poll could inspect the status. A conditional poll that correctly received a 304 threw selfupdate/poll-failed instead of returning { available: false, statusCode: 304 }. The 304 (and the non-2xx) branch is now reached, so ETag-conditional polling works as documented."
705
+ },
706
+ {
707
+ "title": "b.selfUpdate.swap accepts the maxBytes it re-reads under",
708
+ "body": "swap re-reads the newly-installed bytes to re-hash them (closing the verify→swap window) under an opts.maxBytes cap, but its option schema never declared maxBytes — so a caller who passed it was refused with selfupdate/bad-opts and swap always used the fixed 1 GiB default, which could refuse a large binary that selfUpdate.verify (which does declare maxBytes) accepted. swap now declares maxBytes (validated like verify's), so the two caps match; rollback, which re-reads nothing, still does not accept it."
709
+ }
710
+ ]
711
+ }
712
+ ]
713
+ },
714
+ {
715
+ "version": "0.16.17",
716
+ "date": "2026-07-12",
717
+ "headline": "Reject INI float literals that overflow to Infinity, return a verdict (not an exception) when a reverse-DNS lookup faults, wrap an async redirect-hook rejection like a sync throw, and refuse a fractional --max-rows — four defects surfaced by covering previously-untested error branches",
718
+ "summary": "Covering previously-untested error and adversarial branches across four primitives surfaced four genuine defects, each fixed at the root. b.parsers.ini.parse coerced a value like `x = 1e999` straight to ±Infinity — its integer and hex branches already reject out-of-range numbers, but the float branch had no finiteness guard, so an overflowing float slipped through and could poison a downstream size cap or timeout; it is now rejected with ini/value-out-of-range. b.mail.iprev.verify threw an unhandled exception when the forward-confirm DNS lookup returned an error code outside the handful it enumerated (EREFUSED / ENOTIMP / …), even though its reverse-lookup path and every sibling (SPF/DKIM/DMARC/ARC) return a verdict for such faults; it now returns a temperror verdict, and — like those siblings — accepts an operator opts.dnsLookup resolver so the confirm path is resolvable offline. b.httpClient wrapped a synchronous onRedirect hook throw into a REDIRECT_ABORTED error but let an async hook rejection escape unwrapped; both now abort the redirect identically. And the blamejs audit verify-chain --max-rows flag accepted a fractional value (2.5), which truncated the chain walk mid-row and reported a nonsensical fractional count; it now requires a whole positive integer, matching its own error message.",
719
+ "sections": [
720
+ {
721
+ "heading": "Fixed",
722
+ "items": [
723
+ {
724
+ "title": "b.parsers.ini.parse rejects an overflowing float instead of coercing to Infinity",
725
+ "body": "A float literal that exceeds the representable range (e.g. `x = 1e999`) coerced to ±Infinity. The integer and hex coercion branches already reject out-of-range numbers via Number.isSafeInteger, but the float branch returned Number(raw) with no finiteness check — so an Infinity could flow into a downstream size cap or timeout, a denial-of-service vector. The float branch now rejects a non-finite result with ini/value-out-of-range; a large-but-finite float (1e308) and underflow (1e-999 → 0) still parse."
726
+ },
727
+ {
728
+ "title": "b.mail.iprev.verify returns a temperror verdict on an un-enumerated reverse/forward DNS fault",
729
+ "body": "The forward-confirm DNS lookup's error handler enumerated a few transient codes and threw for anything else, so a resolver returning EREFUSED / ENOTIMP / EBADRESP produced an unhandled exception from the public API rather than a verdict. The reverse-lookup path and every sibling result type (SPF / DKIM / DMARC / ARC) return a verdict for a DNS-derived fault; the forward path now does too (temperror). iprev.verify also gains an operator opts.dnsLookup resolver, matching the dnsLookup contract the other types already honor, so the forward-confirm path is resolvable offline."
730
+ },
731
+ {
732
+ "title": "b.httpClient aborts a redirect on an async onRedirect hook rejection",
733
+ "body": "A synchronous throw from the onRedirect hook was wrapped into a REDIRECT_ABORTED error, but an async hook that rejected let the rejection escape unwrapped — inconsistent handling for the same operator control point. An async onRedirect rejection now aborts the redirect with REDIRECT_ABORTED, identical to the synchronous throw."
734
+ },
735
+ {
736
+ "title": "blamejs audit verify-chain --max-rows requires a whole positive integer",
737
+ "body": "The --max-rows flag validated only that the value was finite and >= 1, so a fractional value (2.5) was accepted and passed to the chain walk, where it truncated the verification mid-row and reported a fractional rowsVerified count — despite the flag's own error message promising a positive integer. It now rejects a non-integer value, matching the sibling --steps flag."
738
+ }
739
+ ]
740
+ }
741
+ ]
742
+ },
743
+ {
744
+ "version": "0.16.16",
745
+ "date": "2026-07-12",
746
+ "headline": "Build the data-subject-request ticket store's SQL through the shared b.sql query builder instead of hand-assembled statements, and add a static check that keeps db-handle primitives composing b.sql",
747
+ "summary": "A maintainability change with no behavior difference for operators. The b.dsr ticket store built its reads and writes by concatenating table and column names into SQL strings passed to db.prepare, re-implementing the identifier quoting and sealed-field handling that b.sql — the same builder b.db.from() uses — already provides. That hand-rolled shape is how b.tenant.quota's storage query drifted from the query builder and accrued a run of parity defects fixed in 0.16.15 (reserved-word names, schema-qualified names, sealed-column filtering). The store's DML now composes b.sql (its schema DDL, which is not a b.sql concern, stays as direct statements), so its SQL cannot diverge from the builder. A new codebase-patterns check flags any db-handle primitive that passes an inline SELECT / INSERT / UPDATE / DELETE string literal to db.prepare / runSql, directing it to compose b.sql instead, so this class of drift cannot recur.",
748
+ "sections": [
749
+ {
750
+ "heading": "Changed",
751
+ "items": [
752
+ {
753
+ "title": "b.dsr ticket store composes b.sql for its reads and writes",
754
+ "body": "The data-subject-request ticket store (insert / get / list / update / delete / purge and the legacy re-seal backfill) now builds its DML with the b.sql query builder — sql.select / insert / update / delete(table, { dialect, quoteName }).…toSql() — and prepares the resulting statement, rather than concatenating identifiers into SQL strings by hand. This removes a hand-rolled identifier-quoting surface that could drift from what b.db.from() accepts. Schema provisioning (CREATE TABLE / INDEX, ALTER, PRAGMA) is not a b.sql concern and remains as direct statements. One behavior change: on a store backed by a vault, a ticket payload is AEAD-sealed and base64-encoded (~4/3 expansion) before it is bound, and the bound cell must fit the query builder's 64 MiB per-value ceiling — so the payload is now capped at an expansion-safe plaintext size (~48 MiB) and a larger ticket is refused with dsr/ticket-too-large (route large access/portability exports through chunked storage rather than one giant sealed cell). Plaintext stores keep the full 64 MiB limit. When a vault is first enabled on a table that already holds an over-cap legacy plaintext row, the one-time re-seal backfill still migrates that row's subject columns and derived hashes — so it stays findable by subject lookup and erasable by the data-subject erasure purge — and leaves only the over-cap payload plaintext (still under the read ceiling, DB-encrypted at rest, and removed when the row is erased), rather than failing provisioning with a query-builder error."
755
+ }
756
+ ]
757
+ },
758
+ {
759
+ "heading": "Detectors",
760
+ "items": [
761
+ {
762
+ "title": "Static check: db-handle primitives must compose b.sql for DML",
763
+ "body": "A new codebase-patterns check flags any primitive holding a db handle that runs DML by passing an inline SELECT / INSERT / UPDATE / DELETE string literal to db.prepare / runSql — the shape that lets a query drift from b.sql's identifier quoting and sealed-field rewrite (the b.tenant.quota storage defect class). It directs authors to build the query with b.sql and prepare the resulting string. DDL and PRAGMA (not b.sql verbs) and queries already built through a b.sql variable are out of scope."
764
+ }
765
+ ]
766
+ }
767
+ ]
768
+ },
769
+ {
770
+ "version": "0.16.15",
771
+ "date": "2026-07-11",
772
+ "headline": "Restore break-glass certificate key escrow, hand a failed production-security assertion its real diagnostic message, and make tenant storage-byte quotas actually enforce — three defects surfaced by broadening test coverage and fixed at the root",
773
+ "summary": "Three primitives had defects that only a hostile or previously-untested path reached. b.cert key escrow — the optional break-glass path that seals a renewed private key to an operator's offline recipient — never worked: writeEscrow called a b.crypto method that does not exist, so any certificate configured with keyEscrow threw the moment renewal tried to seal the key. It now seals via b.crypto.encrypt (ML-KEM-1024, plus the P-384 hybrid leg when the recipient supplies an ecPublicKey) and the operator recovers the key offline with b.crypto.decrypt; the recipient accepts an ML-KEM-1024 public-key PEM string or a { publicKey, ecPublicKey } pair from b.crypto.generateEncryptionKeyPair(). b.security.assertProduction constructed its error with the code and message transposed, so a failed production-security assertion threw with a bare token (BAD_OPT / ASSERT_FAILED) as its .message and buried the human-readable explanation in .code — operators now get the full diagnostic where they read it. And b.tenant.quota storage-byte accounting was broken several ways: the per-tenant byte sum issued a query the builder rejects, so snapshot / assert / list always threw once a storage cap was set; it read rows through the auto-unsealing ORM, so a sealed column was measured as its small decrypted plaintext rather than the larger on-disk vault envelope (letting sealed-column tenants slip under the cap); when the tenant identifier itself was a sealed column, the plaintext lookup matched no rows at all and the cap silently counted zero; and BLOB columns (handed back as Uint8Array by node:sqlite) were stringified before measuring, roughly tripling their counted size and refusing writes far below the real cap. All are fixed — the sum now filters a sealed tenant id by its derived-hash blind index, reads the raw stored rows, and measures true on-disk byte lengths — so storage quotas enforce at the configured limit.",
774
+ "sections": [
775
+ {
776
+ "heading": "Fixed",
777
+ "items": [
778
+ {
779
+ "title": "b.cert break-glass key escrow seals the renewed key instead of throwing",
780
+ "body": "A certificate configured with keyEscrow forwarded the private key to writeEscrow, which called a b.crypto.encryptEnvelope method that does not exist — so escrow threw on every renewal and the break-glass recovery path was unusable. It now seals the key to the operator's offline recipient with b.crypto.encrypt: ML-KEM-1024 always, plus a P-384 hybrid leg when the recipient carries an ecPublicKey. The recipient accepts an ML-KEM-1024 public-key PEM string or a { publicKey, ecPublicKey } pair from b.crypto.generateEncryptionKeyPair(); the sealed key is never decrypted by the framework and is recovered offline with b.crypto.decrypt and the matching private key(s)."
781
+ },
782
+ {
783
+ "title": "b.security.assertProduction throws with the diagnostic in .message",
784
+ "body": "SecurityAssertError was constructed with its code and message arguments transposed, so a failed production-security assertion surfaced a bare token (BAD_OPT / ASSERT_FAILED) as its .message while the explanatory text — including the per-assertion failure list — landed in .code. Operators catching the error now read the full diagnostic in .message and the stable token in .code, as documented."
785
+ },
786
+ {
787
+ "title": "b.tenant.quota enforces storage-byte caps at the configured limit",
788
+ "body": "The per-tenant storage-bytes accounting had several defects. It issued a query the query builder rejects (a literal '*' column), so snapshot / assert / list threw as soon as a storage cap was configured — the storage half of tenant quotas never ran against a real database. It read rows through the ORM, which auto-unseals sealed columns, so a sealed cell was measured as its small decrypted plaintext rather than the much larger vault envelope actually on disk — a tenant whose data lives in sealed columns could sail under the cap. When the tenant identifier column itself was sealed, the plaintext lookup compared against the on-disk envelope and matched no rows, so the cap silently counted zero for those tenants. And BLOB columns, which node:sqlite returns as a Uint8Array rather than a Node Buffer, were stringified before measuring: String(uint8array) is the decimal-joined bytes, roughly a 3x overcount that refused writes well below the real cap. The sum now filters a sealed tenant identifier by its derived-hash blind index (as the query builder does), reads the raw stored rows (no unseal), and counts text as its UTF-8 byte length and typed-array views by their true byte length, so a storage cap — including data in sealed columns — enforces at the limit operators set."
789
+ }
790
+ ]
791
+ }
792
+ ]
793
+ },
794
+ {
795
+ "version": "0.16.14",
796
+ "date": "2026-07-11",
797
+ "headline": "Make the object-store single-backend shorthand work for remote backends, and return a string time zone (not an array) when importing an iCalendar event — two defects found by covering previously-untested configuration and import branches",
798
+ "summary": "Covering more configuration and import branches surfaced two genuine defects, now fixed at the root. The documented object-store single-backend shorthand — b.storage.init({ backend: 'sigv4' | 'gcs' | 'azure-blob' | 'http-put', ... }) — never worked for a remote backend: it forwarded the caller's options with the backend key intact, but the object-store backend builder resolves protocol, so the backend was constructed with no protocol and initialization threw a missing-protocol error. Only the local shorthand (which happens to name the key correctly) worked. The shorthand now translates backend to protocol, so all four remote backends construct as documented. And b.calendar.fromIcal mapped a DTSTART;TZID=<zone> parameter to a JSCalendar timeZone that was an array (['America/New_York']) instead of the string RFC 8984 §4.7.1 requires — it only round-tripped by accident because a single-element array coerces to a string; the parameter is now unwrapped to a scalar string.",
799
+ "sections": [
800
+ {
801
+ "heading": "Fixed",
802
+ "items": [
803
+ {
804
+ "title": "b.storage remote single-backend shorthand constructs the backend",
805
+ "body": "b.storage.init({ backend: 'sigv4' | 'gcs' | 'azure-blob' | 'http-put', ... }) forwarded the options with the backend key, but the object-store backend builder reads protocol — so the default backend had no protocol and initialization threw a missing-protocol ObjectStoreError. The remote shorthand never worked (only the { backend: 'local' } form, which names protocol correctly under the hood, did). The shorthand now maps backend to protocol and drops the backend key, so all four remote backends build as documented."
806
+ },
807
+ {
808
+ "title": "b.calendar.fromIcal returns a string time zone for DTSTART/DUE;TZID",
809
+ "body": "An imported event's DTSTART;TZID=<zone> (or a task's DUE;TZID) mapped to a JSCalendar timeZone that was a single-element array rather than the string RFC 8984 §4.7.1 requires. It happened to round-trip back through toIcal because a one-element array coerces to a string, but consumers reading timeZone as a string saw an array. The property parameter is now unwrapped to its scalar first value."
810
+ }
811
+ ]
812
+ }
813
+ ]
814
+ },
815
+ {
816
+ "version": "0.16.13",
817
+ "date": "2026-07-11",
818
+ "headline": "Make SD-JWT VC verification accept a raw JWK issuer key (its own documented common path), and emit the SMTP command-smuggling audit on a NUL-byte injection — two defects found by covering previously-untested verifier and inbound-server branches",
819
+ "summary": "Covering the uncovered verifier and adversarial-input branches of two more subsystems surfaced two genuine defects, now fixed at the root. b.auth.sdJwtVc.verify rejected a valid credential when the issuerKeyResolver returned a raw JWK object — the very path the code's own comment calls the common one — because the JWK was handed straight to node:crypto.verify, which cannot consume a bare JWK, so verification threw a low-level type error instead of validating; the resolver's JWK is now imported to a key object before verification, matching the holder key-binding path. And the inbound SMTP server refused a command line containing a NUL byte (correct) but never emitted the command-smuggling audit event it emits for bare-CR / bare-LF injection, because it checked for the wrong error code; the audit now fires so a NUL-injection attempt is recorded for forensic triage. A misleading comment in the permissions MFA gate that advertised a non-existent no-freshness-window escape hatch is corrected — the freshness window is always enforced by design.",
820
+ "sections": [
821
+ {
822
+ "heading": "Security",
823
+ "items": [
824
+ {
825
+ "title": "SMTP inbound server records a command-smuggling audit on NUL-byte injection",
826
+ "body": "The inbound MX server's command handler refused a command line containing a NUL byte with a 500, but the branch meant to emit the mail.server.mx.smtp_smuggling_detected audit checked for the error code guard-smtp-command/nul-byte while the guard actually raises guard-smtp-command/nul. As a result a NUL-injection command was rejected but not recorded, unlike bare-CR / bare-LF smuggling which was audited. The code match is corrected, so a NUL-byte command-smuggling attempt now produces the forensic audit event."
827
+ }
828
+ ]
829
+ },
830
+ {
831
+ "heading": "Fixed",
832
+ "items": [
833
+ {
834
+ "title": "b.auth.sdJwtVc.verify accepts a raw JWK from issuerKeyResolver",
835
+ "body": "When the issuerKeyResolver returned a JWK object — described in the code as the common path — the JWK was passed directly to node:crypto.verify, which requires a key object, so a valid credential failed verification with a raw ERR_INVALID_ARG_TYPE rather than validating. The resolver's JWK is now imported to a public key object (with the existing algorithm/key-type cross-check preserved) before verification, mirroring the holder key-binding JWT path. Verification succeeds for EC and Ed25519 JWK resolvers."
836
+ },
837
+ {
838
+ "title": "Corrected a misleading comment in the permissions MFA freshness gate",
839
+ "body": "A comment in the requireMfa gate described mfaWindowMs: Infinity as an operator escape hatch for a no-freshness-window pass-through. No such escape hatch exists — both the role- and route-level validators reject a non-finite mfaWindowMs, so MFA freshness is always enforced (defaulting to 15 minutes). Enabling it would let a stolen long-lived cookie with a stale mfaAt bypass the gate. The comment now states the freshness window is always enforced; behavior is unchanged."
840
+ }
841
+ ]
842
+ }
843
+ ]
844
+ },
845
+ {
846
+ "version": "0.16.12",
847
+ "date": "2026-07-11",
848
+ "headline": "Restore the static server's drive-by-execution defense, stop the retention sweep from aborting on subject-scoped rules, make wss:// connections to IP addresses work, and return a verdict instead of throwing on a malformed OpenPGP signature — four defects found by covering previously-untested error and adversarial branches",
849
+ "summary": "Writing behavioral tests for the uncovered error and adversarial branches of the framework's most under-tested files surfaced four genuine defects, now fixed at the root. The static file server's safeAttachmentForRiskyMimes option — the drive-by-execution defense that forces Content-Disposition: attachment for risky inline types (text/html, image/svg+xml, application/javascript) — was silently inert: it was read from a value bag that only carries known-default keys, and this option has no default entry, so it always evaluated to false and the whole defense was dead code. The retention sweep threw and aborted entirely whenever a retention rule with a subjectField met a row with a subject value, because it called a lazily-required module as if it were already resolved. A wss:// connection to an IP-address host threw synchronously and was unusable, because the client set the TLS SNI to the IP literal, which the TLS stack forbids. And b.mail.crypto.pgp.verify threw on a truncated or malformed signature integer instead of returning its documented { ok: false } verdict, so a consumer iterating over untrusted signatures crashed rather than getting a negative result. A static gate now flags any option read from a defaults-applied value bag when that option has no default entry, so the static-server class cannot recur.",
850
+ "sections": [
851
+ {
852
+ "heading": "Security",
853
+ "items": [
854
+ {
855
+ "title": "Static server's safeAttachmentForRiskyMimes drive-by-execution defense works",
856
+ "body": "b.staticServe's documented safeAttachmentForRiskyMimes option forces Content-Disposition: attachment for risky inline MIME types (text/html, image/svg+xml, application/javascript) so a browser can't execute an operator-hosted file inline. It was read from the option bag produced by applyDefaults, which strips every key absent from the defaults table — and this option is not a defaults key — so it was always undefined and the defense never engaged. It is now read directly from the caller's options, so enabling it forces the attachment disposition as documented. Operators who set it were relying on a defense that did nothing; it is now active."
857
+ },
858
+ {
859
+ "title": "b.mail.crypto.pgp.verify returns a verdict on a malformed signature instead of throwing",
860
+ "body": "verify() threw MailCryptoError('mail-crypto/pgp/bad-mpi') on a truncated or malformed signature integer, unlike every other malformed-signature path (bad armor, packet parse, hash mismatch), which returns { ok: false, code, reason }. A consumer looping over untrusted signatures and branching on .ok would crash on a crafted input rather than receive a negative verdict. Malformed signature integers now route through the same failure path and return { ok: false }."
861
+ }
862
+ ]
863
+ },
864
+ {
865
+ "heading": "Fixed",
866
+ "items": [
867
+ {
868
+ "title": "Retention sweep no longer aborts on subject-scoped rules",
869
+ "body": "A retention rule declared with a subjectField consults the subject-level legal-hold registry when a candidate row carries a subject value. That path called the legal-hold module as though it were already resolved, but it is a lazily-required getter that must be invoked to load the module — so the call threw a TypeError, which the sweep converted into a SWEEP_FAILED and aborted the entire run. The getter is now invoked correctly, so subject-scoped retention sweeps proceed."
870
+ },
871
+ {
872
+ "title": "wss:// connections to an IP-address host work",
873
+ "body": "Connecting the WebSocket client to a wss:// URL whose host is an IP literal threw ERR_INVALID_ARG_VALUE synchronously, because the client set the TLS SNI servername to the IP literal, which RFC 6066 and the Node TLS stack forbid. SNI is now sent only for hostname targets and omitted for IP literals; certificate identity is still verified against the address."
874
+ }
875
+ ]
876
+ },
877
+ {
878
+ "heading": "Detectors",
879
+ "items": [
880
+ {
881
+ "title": "applydefaults-dropped-opt",
882
+ "body": "A static gate flags any option read from the result of applyDefaults(opts, DEFAULTS) when that option is absent from the DEFAULTS table — the exact shape that silently disabled the static server's attachment defense. The result of applyDefaults carries only the defaults' keys, so such a read is always undefined. The fix is to read the option from the caller's opts directly, or to add it to the defaults table."
883
+ }
884
+ ]
885
+ }
886
+ ]
887
+ },
888
+ {
889
+ "version": "0.16.11",
890
+ "date": "2026-07-11",
891
+ "headline": "Fix a TOML parser bug where every double-quoted key decoded to the empty string — which also let a quoted \"__proto__\" key slip past the prototype-pollution guard — and return a real 201 from SCIM resource creation, both surfaced by covering previously-untested error and adversarial branches",
892
+ "summary": "Writing behavioral tests for the uncovered error, adversarial, and defensive branches of the framework's most under-tested files surfaced two genuine defects, now fixed at the root. b.parsers.toml decoded every double-quoted (basic) key to the empty string: a document like \"host\" = \"db1\" parsed to { \"\": \"db1\" }, multiple distinct quoted keys collided onto one empty key, and — the security consequence — a quoted \"__proto__\" or \"constructor\" key decoded to the empty string and so slipped past the poisoned-key guard that rejects those names. TOML keys are usually written bare, so the bug was latent since the parser shipped. It is fixed to decode quoted keys correctly, and a quoted \"__proto__\" / \"constructor\" is now rejected with toml/poisoned-key. Separately, the SCIM server returned an undefined HTTP status instead of 201 Created when creating a User or Group singleton, because it referenced a status constant that did not exist; it now returns 201 with the created representation.",
893
+ "sections": [
894
+ {
895
+ "heading": "Security",
896
+ "items": [
897
+ {
898
+ "title": "b.parsers.toml rejects a quoted __proto__ / constructor key",
899
+ "body": "The double-quoted-key decoder never appended the key's characters for a basic key, so every double-quoted key resolved to the empty string. A quoted \"__proto__\" = ... or \"constructor\" = ... therefore decoded to \"\", which the poisoned-key guard does not flag, letting those names through. Quoted keys now decode to their real value, and a quoted __proto__ / constructor is rejected with toml/poisoned-key. Bare-key documents were unaffected; only double-quoted keys were mis-decoded."
900
+ }
901
+ ]
902
+ },
903
+ {
904
+ "heading": "Fixed",
905
+ "items": [
906
+ {
907
+ "title": "b.parsers.toml decodes double-quoted keys correctly",
908
+ "body": "A basic (double-quoted) key with no escape sequence decoded to the empty string — \"host\" = \"db1\" became { \"\": \"db1\" }, and multiple distinct quoted keys in one table collided onto a single empty key. The decoder now appends the verbatim key run for both quoted-key kinds, so quoted, literal, dotted, and escaped keys all decode to their intended names."
909
+ },
910
+ {
911
+ "title": "SCIM resource creation returns 201 Created",
912
+ "body": "Creating a User or Group via POST returned an undefined HTTP status because the handler referenced a CREATED status constant that was not defined, so the created representation was written with no valid status code. It now returns 201 with the created resource."
913
+ }
914
+ ]
915
+ }
916
+ ]
917
+ },
918
+ {
919
+ "version": "0.16.10",
920
+ "date": "2026-07-11",
921
+ "headline": "Restore five APIs that silently didn't work — certificate-fingerprint hashing from PEM, the tus-upload close handler, the mail-delivery factory, the log-stream local-sink env boot, and the CIBA internal-IdP opt — and make single-base guard profile composition work, all surfaced by writing a test for every documented primitive",
922
+ "summary": "Writing a behavioral test for every documented primitive that previously had none surfaced a batch of APIs that were advertised but silently non-working, now fixed at the root. b.crypto.hashCertFingerprint and b.crypto.isCertRevoked threw a TypeError on the PEM-string input their own examples pass (a helper was called on its lazy-require getter instead of the module), so only the raw-DER path worked; b.middleware.tusUpload.close and b.mail.send.deliver.create were documented but never attached to their export, so calling the documented form threw 'not a function'; b.logStream.bootFromEnv advertised a 'local' protocol that threw at init because it wired the sink with the wrong option; and the CIBA client dropped the allowInternal opt on its own backchannel and token POSTs, so an operator who explicitly opted into an internal or loopback identity provider was still SSRF-blocked. Separately, gate-contract profile composition now honors its documented extends: string | string[] contract — a single base profile name passed as a bare string was silently ignored, which made the buildProfile example every guard shares a no-op. The EU AI Act fundamental-rights-impact-assessment and GPAI training-data-summary builders now raise typed ComplianceError refusals with descriptive messages instead of a bare Error whose message was only the error code. Five documentation examples are corrected to their real emitted values (the SRI integrity attribute, two guard rule-ids, a guard issue kind, and the guard-archive profile-composition call). The static gate that requires every documented primitive to be referenced by a test now has an empty backlog register: every documented primitive is exercised.",
923
+ "sections": [
924
+ {
925
+ "heading": "Fixed",
926
+ "items": [
927
+ {
928
+ "title": "b.crypto.hashCertFingerprint / isCertRevoked accept PEM input again",
929
+ "body": "Both threw 'TypeError: safeBuffer.byteLengthOf is not a function' on a PEM-string certificate — the internal PEM-to-DER step called the byte-length helper on its lazy-require getter rather than the resolved module, so only the pre-parsed DER/Buffer path (which returns before that line) worked. Their documented examples pass a PEM string, so the advertised behavior was silently broken. Fixed at the call site."
930
+ },
931
+ {
932
+ "title": "b.middleware.tusUpload.close is reachable",
933
+ "body": "The middleware export wired tusUpload.create and .memoryStore but never attached the documented .close handler, so b.middleware.tusUpload.close(middleware) threw 'not a function'. It is now attached to the export."
934
+ },
935
+ {
936
+ "title": "b.mail.send.deliver.create is reachable",
937
+ "body": "b.mail.send.deliver was bound directly to the factory function but never carried the documented .create property, so the documented b.mail.send.deliver.create(opts) form resolved to undefined. The .create factory is now attached; the existing collapsed b.mail.send.deliver(opts) callable is unchanged."
938
+ },
939
+ {
940
+ "title": "b.logStream.bootFromEnv local protocol works",
941
+ "body": "BLAMEJS_LOG_STREAM_PROTOCOL=local threw 'log-stream local requires { dir }' at init: bootFromEnv wired the local sink with a path option, but the directory-based local sink requires dir. BLAMEJS_LOG_STREAM_PATH is now mapped to the sink directory, so the advertised local protocol boots and writes end-to-end."
942
+ },
943
+ {
944
+ "title": "CIBA client honors allowInternal on its own requests",
945
+ "body": "The backchannel-authentication and token POSTs of the CIBA client dropped the allowInternal option that create() accepts and threads to the OAuth client for discovery/JWKS, so an operator who explicitly set allowInternal:true for an internal or loopback identity provider was still refused with ssrf-guard/blocked-loopback on exactly those two endpoints. The option is now threaded through per request. The SSRF guard stays on by default; nothing changes unless the operator opts in."
946
+ },
947
+ {
948
+ "title": "EU AI Act FRIA / training-data-summary raise typed refusals with real messages",
949
+ "body": "b.compliance.aiAct.fundamentalRightsImpactAssessment and b.compliance.aiAct.gpai.trainingDataSummary validated required fields by passing the built-in Error as the error class, so the descriptive message was dropped (its slot became the Error options bag) and the thrown message was only the error code. Both now raise a typed ComplianceError with the descriptive message and a stable code, matching the sibling adherenceForm builder."
950
+ },
951
+ {
952
+ "title": "Five documentation examples corrected to real values",
953
+ "body": "The b.crypto.sri example carried a fabricated SHA-384 value (an operator copy-pasting the integrity attribute would get a mismatch); it now shows the correct digest. The b.guardTime.validate example named rule-id time.year-out-of-range (actual time.year-window); b.guardUuid named uuid.nil / uuid.max (actual uuid.nil-uuid / uuid.max-uuid); b.htmlBalance.checkSafe named issue kind event-handler-attribute (actual event-handler). The b.guardArchive.buildProfile example passed { profile: \"strict\" }, which resolves no caps; it now uses the correct { extends: \"strict\" } form."
954
+ }
955
+ ]
956
+ },
957
+ {
958
+ "heading": "Changed",
959
+ "items": [
960
+ {
961
+ "title": "Guard profile composition accepts a single base profile as a bare string",
962
+ "body": "gate-contract profile composition documents extends: string | string[], but a bare-string extends was silently ignored — only the array form composed — which made the buildProfile example every guard shares (buildProfile({ extends: \"strict\" })) a no-op that returned an empty caps object. A string extends is now normalized to a one-element list, so single-base composition works across every b.guard*.buildProfile. The array form is unchanged."
963
+ }
964
+ ]
965
+ },
966
+ {
967
+ "heading": "Added",
968
+ "items": [
969
+ {
970
+ "title": "A test for every documented primitive",
971
+ "body": "The primitive-without-test static gate's backlog register, which listed documented primitives that no test referenced, is now empty: every documented primitive is exercised by a behavioral test through its real consumer path. A new primitive must land with its own test reference; the register accepts an entry only with a specific documented reason coverage is indirect."
972
+ }
973
+ ]
974
+ }
975
+ ]
976
+ },
977
+ {
978
+ "version": "0.16.9",
979
+ "date": "2026-07-10",
980
+ "headline": "`b.safeBuffer.quoteString` — one RFC quoted-string serializer behind eight header and protocol emitters — plus a consolidated per-primitive test suite and a static gate that refuses a documented primitive no test references",
981
+ "summary": "Eight places in the framework serialized an RFC quoted-string by hand — escape backslash and DQUOTE, wrap in DQUOTEs — for Cache-Status and Signature-Input sf-strings, Link header parameters, Authentication-Results reason, IMAP quoted strings and METADATA values, ManageSieve responses, and Server-Timing descriptions. Those copies are consolidated into one primitive, b.safeBuffer.quoteString, so an unescaped quote can never terminate a string early and smuggle extra parameters into a protocol line, and the escaping cannot drift between emitters. Output is byte-identical to before. The test suite is reorganized so every primitive's tests live in a canonical per-primitive file (thirty-eight auxiliary files folded or renamed; every assertion preserved — the full suite passes with the exact same check count). On top of that reorganization, the comment-block validator gains a primitive-without-test check: a documented @primitive that no test references — neither the full dotted form nor its namespace plus a method invocation — now fails the static gate. One hundred six existing primitives currently lack any test reference; they are recorded in an explicit shrink-only register in the validator, each entry an open test-backfill item, and the gate refuses any new primitive shipping untested.",
982
+ "sections": [
983
+ {
984
+ "heading": "Added",
985
+ "items": [
986
+ {
987
+ "title": "b.safeBuffer.quoteString — RFC quoted-string serialization",
988
+ "body": "Coerces to string, escapes every backslash and DQUOTE with a leading backslash, and wraps the result in DQUOTEs — the quoted-string grammar shared by RFC 8941 §3.3.3 Structured Fields sf-string, RFC 8288 Link header parameters, RFC 8601 §2.2 Authentication-Results reason, RFC 3501 §4.3 IMAP quoted strings, and RFC 5804 §1.2 ManageSieve strings. Escaping only: a grammar with a restricted character range (sf-string is printable-ASCII; IMAP quoted strings cannot carry CR/LF) enforces its range check before calling it."
989
+ },
990
+ {
991
+ "title": "Static gate: a documented primitive must be referenced by a test",
992
+ "body": "The comment-block validator now scans the test corpus for every @primitive: the full dotted form (b.namespace.method) anywhere under test/, or the namespace / owning module referenced together with the method invoked in the same test file — the same-file rule stops a ubiquitous method name like .create( in an unrelated test from counting as coverage. A primitive with no reference fails the pre-push static gate. One hundred six existing primitives currently have no reference; they are listed in a shrink-only register inside the validator — each entry an open test-backfill item that is deleted when its test lands — and a new primitive cannot ship untested."
993
+ }
994
+ ]
995
+ },
996
+ {
997
+ "heading": "Changed",
998
+ "items": [
999
+ {
1000
+ "title": "Eight quoted-string emitters route through the one serializer",
1001
+ "body": "Cache-Status key/detail, Signature-Input sf-strings, Link header parameter values, Authentication-Results reason, IMAP METADATA values and quoted strings, ManageSieve OK/NO/BYE/LISTSCRIPTS responses, and Server-Timing desc now compose b.safeBuffer.quoteString instead of hand-rolling the escape. Emitted bytes are unchanged."
1002
+ },
1003
+ {
1004
+ "title": "Per-primitive canonical test files",
1005
+ "body": "Thirty-eight auxiliary test files are folded into (or renamed to) their primitive's canonical test file — one file per primitive domain, concern-named splits kept (e.g. http-client-cache / -stream / -throttle). No assertion was changed, added, or removed by the reorganization; the full suite passes with the exact same check count before and after. The b.*-surface reference gate that previously ran inside the smoke suite is superseded by the stronger validator check above."
1006
+ }
1007
+ ]
1008
+ },
1009
+ {
1010
+ "heading": "Detectors",
1011
+ "items": [
1012
+ {
1013
+ "title": "rfc-quoted-string-escape-owned-by-safeBuffer",
1014
+ "body": "Flags an inline backslash-then-DQUOTE escape chain in lib/ outside the primitive's home — the quoted-string serializer re-implemented, whose escaping can drift from the canonical one. While being proven against the pre-consolidation tree it caught an eighth emitter the manual sweep had missed (an IMAP response-literal helper), which is consolidated in this release too."
1015
+ }
1016
+ ]
1017
+ }
1018
+ ]
1019
+ },
1020
+ {
1021
+ "version": "0.16.8",
1022
+ "date": "2026-07-10",
1023
+ "headline": "`b.metrics.snapshot.render` now emits the labeled registry — counters, gauges and histogram buckets with their label sets — matching the live exposition endpoint byte-for-byte, plus a Public Suffix List refresh and supply-chain pin maintenance",
1024
+ "summary": "A snapshot written with b.metrics.snapshot.startWriter's registry option always carried the full labeled registry in its metrics field, but b.metrics.snapshot.render ignored that field: the prometheus format emitted only the flat numeric fields, so a sidecar wanting a complete /metrics exposition of labeled series had to re-implement label-value escaping and bucketed line formatting itself. Both render formats now emit the labeled series. The prometheus output is produced by the same family encoder the live exposition() endpoint uses — one encoder now backs the live exposition, the shadow registry and snapshot rendering — so series names and escaping are identical whichever source a scraper reads. Snapshot files are parsed defensively: a malformed family, a non-Prometheus metric or label name, or a non-numeric sample in a hand-edited snapshot file is dropped rather than rendered, so a tampered file cannot forge exposition lines. Separately, twelve documentation examples opened with require(\"blamejs\").create(), a factory the export does not have; the examples are corrected and the @example-execution test no longer supplies a substitute create(), so any future example calling a method the shipped export lacks fails the suite. The vendored Public Suffix List is refreshed to the 2026-07-09 upstream revision, and the CI toolchain pins (github/codeql-action, the ClusterFuzzLite base-builder digest) are brought current.",
1025
+ "sections": [
1026
+ {
1027
+ "heading": "Fixed",
1028
+ "items": [
1029
+ {
1030
+ "title": "b.metrics.snapshot.render emits the labeled registry from snap.metrics (#430)",
1031
+ "body": "Snapshots written with startWriter's registry option carry every registered counter / gauge / histogram — label sets and bucket counts — in a structured metrics field, but render() only emitted the flat numeric fields. The prometheus format now renders those families with the same labeled / bucketed sample lines (name{label=\"value\"} n, name_bucket{le=…}, name_sum, name_count) the live exposition() endpoint serves, family names verbatim rather than prefix-qualified, so dashboards see one series name regardless of scrape source; the text format lists each labeled sample as a name{label=\"value\"} row. Malformed families, non-Prometheus metric / label names, and non-numeric samples in a snapshot file are dropped rather than rendered, so a hand-edited file cannot forge exposition lines."
1032
+ },
1033
+ {
1034
+ "title": "Twelve documentation examples no longer call a nonexistent create() factory",
1035
+ "body": "@example blocks in b.a2a, b.cloudEvents, b.pqcSoftware and b.tlsExporter opened with var b = require(\"blamejs\").create(), but the top-level export has no create() — copying those examples threw a TypeError on the first line. The examples now open with var b = require(\"blamejs\"). The @example-execution test previously supplied a substitute create() to the examples' require, which masked exactly this drift; the substitute is removed, so an example calling any method the shipped export lacks now fails the suite."
1036
+ }
1037
+ ]
1038
+ },
1039
+ {
1040
+ "heading": "Changed",
1041
+ "items": [
1042
+ {
1043
+ "title": "One shared Prometheus family encoder behind every exposition surface",
1044
+ "body": "The live registry exposition(), the shadow registry's prometheus render and the new snapshot rendering now share a single labeled-sample encoder (label ordering, value escaping, histogram bucket lines). Output of the existing surfaces is unchanged, including the guarantee that a label literally named constructor / prototype / __proto__ survives rendering."
1045
+ },
1046
+ {
1047
+ "title": "Public Suffix List refreshed to the 2026-07-09 upstream revision",
1048
+ "body": "The vendored, signature-verified Public Suffix List bundle is rebuilt from the current upstream publication (previously 2026-07-02). This keeps DMARC / BIMI organizational-domain alignment and cookie-scope decisions current with registry changes."
1049
+ },
1050
+ {
1051
+ "title": "CI toolchain pins brought current",
1052
+ "body": "github/codeql-action is pinned to v4.37.0 across the CodeQL and Scorecard workflows in one change (its init / analyze / upload-sarif sub-actions must move together — mixed versions break the CodeQL analysis run), and Dependabot now groups codeql-action updates into a single pull request. The ClusterFuzzLite base-builder image digest is bumped to the current upstream publication, with the oss-fuzz submission mirror kept in sync."
1053
+ }
1054
+ ]
1055
+ },
1056
+ {
1057
+ "heading": "Detectors",
1058
+ "items": [
1059
+ {
1060
+ "title": "prometheus-exposition-escape-owned-by-metrics",
1061
+ "body": "Flags a hand-rolled Prometheus escape chain (backslash-doubling plus newline escaping) outside lib/metrics.js — the signature of a second exposition encoder growing whose escaping can drift from the canonical one. The two-step backslash+quote escapes used by RFC quoted-string wire formats (sf-string, IMAP, Sieve, Link header) are out of scope."
1062
+ }
1063
+ ]
1064
+ }
1065
+ ]
1066
+ },
1067
+ {
1068
+ "version": "0.16.7",
1069
+ "date": "2026-07-06",
1070
+ "headline": "Restore two APIs that silently didn't work — the Clear-Site-Data header helper and data-subject export — and correct five documentation examples, all surfaced by a new test that executes every JSDoc @example end-to-end",
1071
+ "summary": "The comment-block validator only ever PARSE-checked each documentation @example, so an example could compile and still be dead — calling a method that no longer exists, passing an option the API rejects, or misreading a return shape. A new test now EXECUTES every self-contained @example against the real framework, and on its first run it caught two genuine framework defects plus five stale examples. b.middleware.clearSiteData.headerValue (documented @status stable since 0.15.9) was unreachable: the middleware export was a bare factory that never carried the headerValue helper (nor KNOWN_TYPES / DEFAULT_TYPES), so calling it threw — it is now attached to the export, mirroring b.middleware.idempotencyKey. b.subject.export / b.subject.exportData returned undefined instead of the documented empty object when no data-subject tables are declared, so an operator running an export before tagging any subjectField column got undefined and crashed on the first property access — it now returns {}. Five @example blocks are corrected to runnable code (b.guardRegex.gate's return usage; the invalid title option on b.openapi.create / b.asyncapi.create, which belongs under info; b.retention.complianceFloor; b.safeSchema.object). No security surface changes.",
1072
+ "sections": [
1073
+ {
1074
+ "heading": "Fixed",
1075
+ "items": [
1076
+ {
1077
+ "title": "b.middleware.clearSiteData.headerValue is reachable again",
1078
+ "body": "b.middleware.clearSiteData was exported as a bare factory function, so the advertised b.middleware.clearSiteData.headerValue(types, label?) — plus KNOWN_TYPES and DEFAULT_TYPES — were undefined off the export and threw a TypeError, even though the helper (documented stable since 0.15.9) existed on the module. Those members are now attached to the middleware export, so the documented call works."
1079
+ },
1080
+ {
1081
+ "title": "b.subject.export / exportData returns {} when no data-subject tables are declared",
1082
+ "body": "On the no-subject-tables path, export returned the value of its internal audit-write helper — which has no return statement, i.e. undefined — instead of the documented empty dump. An operator running a data-subject export before tagging any subjectField column therefore received undefined and crashed on Object.keys(dump) / dump.<table>. It now returns {} as documented (export and exportData are the same function)."
1083
+ },
1084
+ {
1085
+ "title": "Five documentation examples corrected to runnable code",
1086
+ "body": "b.guardRegex.gate's example now uses the gate return value correctly; b.openapi.create and b.asyncapi.create examples move title/version under info (title is not a top-level option); and the b.retention.complianceFloor and b.safeSchema.object examples are made self-contained. These are documentation-only corrections."
1087
+ }
1088
+ ]
1089
+ },
1090
+ {
1091
+ "heading": "Added",
1092
+ "items": [
1093
+ {
1094
+ "title": "JSDoc @example execution validation",
1095
+ "body": "A new test executes every self-contained JSDoc @example against the real framework — not just parse-checks it — so an example that references a renamed or removed method, passes an option the API rejects, or misreads a return shape now fails the suite instead of shipping as dead documentation. Examples with side effects (network, database, filesystem, long-lived work) or that abstract external setup are skipped; the executed set runs in a sandbox with a wall-clock ceiling."
1096
+ }
1097
+ ]
1098
+ }
1099
+ ]
1100
+ },
1101
+ {
1102
+ "version": "0.16.6",
1103
+ "date": "2026-07-05",
1104
+ "headline": "Repair the ClusterFuzzLite / OSS-Fuzz build script so its fuzz targets actually install the jazzer.js runtime and pair with their seed corpora",
1105
+ "summary": "This patch fixes the OSS-Fuzz / ClusterFuzzLite build script (.clusterfuzzlite/build.sh), which was latently broken: it compiled every fuzz/<name>.fuzz.js harness without first installing @jazzer.js/core, so the generated targets referenced a runtime that was never present and could not start, and it named each seed-corpus archive after the .fuzz.js-stripped base (guard-csv_seed_corpus.zip) rather than the compiled target (guard-csv.fuzz), so the fuzzing engine never associated a corpus with its target and bootstrapped from nothing. Both failures were silent because the compile step still exits 0 and the in-repo CI fuzz workflows invoke jazzer.js directly rather than through this script — the script is the OSS-Fuzz-upstream integration spec. The build script now installs the jazzer.js runtime into the project-root node_modules before compiling and names each corpus after its target, and a codebase-patterns check locks both invariants in. The build image's inline documentation is corrected (jazzer.js is not present in the base image; the CI workflows do not consume this image) and now records why the upstream path is still latent (the base image ships Node 20 / GLIBC 2.31, below the framework's Node 22+ and jazzer.js's GLIBC 2.38 needs). No shipped framework code changes; fuzz and build-image assets are dev-only and are not part of the published package.",
1106
+ "sections": [
1107
+ {
1108
+ "heading": "Fixed",
1109
+ "items": [
1110
+ {
1111
+ "title": "OSS-Fuzz / ClusterFuzzLite build script installs the jazzer.js runtime before compiling",
1112
+ "body": "compile_javascript_fuzzer emits a runnable that executes <project>/node_modules/@jazzer.js/core at fuzz time from a wholesale copy of the source tree, so the runtime must exist in the project-root node_modules at compile time. The build script never ran an install, so @jazzer.js/core (declared in fuzz/package.json) was absent and every compiled target referenced a runtime that wasn't there. The script now installs it before the compile loop."
1113
+ },
1114
+ {
1115
+ "title": "Fuzz seed corpora are paired with their compiled target",
1116
+ "body": "compile_javascript_fuzzer names each target after basename -s .js (keeping the .fuzz stem, e.g. guard-csv.fuzz), but the script zipped each seed corpus under the .fuzz.js-stripped base (guard-csv_seed_corpus.zip). The fuzzing engine pairs <target>_seed_corpus.zip, so no corpus was ever associated and targets started from an empty corpus. Each corpus is now named after its target."
1117
+ },
1118
+ {
1119
+ "title": "Build-image documentation corrected",
1120
+ "body": "The build image's comments claimed jazzer.js was pre-installed in the base image and that the CI fuzz workflows consume this image; neither is true. The comments now state that build.sh installs jazzer.js and that the CI workflows invoke jazzer.js directly, and record that the OSS-Fuzz-upstream path remains latent until the base image advances to Node 24 / a newer GLIBC."
1121
+ }
1122
+ ]
1123
+ },
1124
+ {
1125
+ "heading": "Detectors",
1126
+ "items": [
1127
+ {
1128
+ "title": "Fuzz-build invariants checked in codebase-patterns",
1129
+ "body": "A cross-artifact check asserts that .clusterfuzzlite/build.sh installs a jazzer.js runtime before compile_javascript_fuzzer and names each seed-corpus archive after the compiled target, so neither gap can silently reappear."
1130
+ }
1131
+ ]
1132
+ }
1133
+ ]
1134
+ },
1135
+ {
1136
+ "version": "0.16.5",
1137
+ "date": "2026-07-03",
1138
+ "headline": "Correctness fixes across the ReDoS guard, Base32 codec, HTTP multipart builder, SAML encrypted-assertion decryption, and the OAuth device grant — several restore advertised functionality that was silently non-working, plus a multipart header-injection fix",
1139
+ "summary": "This patch fixes a batch of correctness and security defects, several of which restore advertised behaviour that never actually worked. The ReDoS guard (b.guardRegex) no longer false-rejects LINEAR patterns that use a quantified non-capturing group (`(?:…)?`, `(?:…)*`) or an optional quantified group (`(X+)?`, `(?:X+)?`) — those repeat the group at most once and are not catastrophic; the detector now correctly requires the OUTER quantifier to be unbounded, so operator regex screening and b.selfUpdate asset patterns that use an optional SemVer suffix work again while genuine `(a+)+` shapes are still refused. The Base32 decoder now rejects NON-CANONICAL input (a final symbol whose unused low bits are non-zero — previously two distinct strings decoded to the same bytes, a malleability problem for the TOTP secrets and identifiers Base32 backs) and impossible symbol counts (1/3/6 mod 8 that silently produced a truncated buffer). The HTTP multipart/form-data builder now refuses CR/LF/NUL in a field name, filename, or content-type, closing a part-header-injection / form-part-forgery path. Two SAML EncryptedAssertion decryption paths that were completely dead — ML-KEM-1024 key transport and XChaCha20-Poly1305 content — are fixed (they referenced crypto entry points that were never exported, so the framework's advertised PQC-first / AEAD SAML encryption always failed and misreported the cause as a wrong-key or tag-mismatch error). The OAuth device grant now works against spec-compliant identity providers (its poll aborted on the first `authorization_pending`, which RFC 8628 delivers as an HTTP 400 the client was rejecting before reading), and a static (non-discovery) OAuth client can now use introspection, dynamic registration, and device authorization by configuring those endpoints. Smaller fixes round it out: the HTTP client's default error carries statusCode/permanent; the CLI dev server's repeatable flags accumulate; DANE and MTA-STS failures are scoped correctly per RFC; and typed errors replace raw TypeErrors on a couple of malformed-input paths.",
1140
+ "sections": [
1141
+ {
1142
+ "heading": "Security",
1143
+ "items": [
1144
+ {
1145
+ "title": "b.guardRegex no longer false-rejects linear quantified-group patterns (#432, #429)",
1146
+ "body": "The nested-quantifier ReDoS detector treated the `?` in a `(?:` group prefix as an inner quantifier and treated a bounded outer `?` / `{0,1}` as a dangerous outer quantifier, so it wrongly refused LINEAR patterns like `^(?:/page/\\d+)?$`, `^foo(?:bar)*$`, `^(a+)?$`, and `(?:[-+][0-9A-Za-z.-]+)?`. The catastrophic class requires the OUTER quantifier to be unbounded (`*`/`+`/`{n,}`). The detector now relies solely on the paren-aware structural scanner, which requires an unbounded outer quantifier and does not miscount a group prefix — so genuine `(a+)+` / `((a)+)+` shapes stay refused while these linear ones are accepted. Consumers screening operator-supplied regexes (route rules, and b.selfUpdate asset patterns using an optional SemVer prerelease/build group) are no longer forced to rewrite valid input."
1147
+ },
1148
+ {
1149
+ "title": "Base32 decoder rejects non-canonical encodings and impossible lengths",
1150
+ "body": "b.base32.decode discarded the final symbol's unused low bits without checking they were zero (RFC 4648 §3.5), so two distinct strings — e.g. `MY======` and `MZ======` — decoded to the same bytes (decoder malleability), and it silently accepted impossible symbol counts (1/3/6 mod 8) that can't represent whole bytes, returning a truncated buffer. Both are now refused (`base32/non-canonical`, `base32/bad-length`), giving a one-to-one mapping between a byte sequence and its Base32 string — important where the string is a key / secret / dedup handle (TOTP, identifiers). Valid input, including unpadded and loose-mode input, is unaffected."
1151
+ },
1152
+ {
1153
+ "title": "HTTP multipart/form-data builder refuses CR/LF/NUL in part-header values",
1154
+ "body": "The multipart body builder interpolated the field name, file field, filename, and content-type onto `Content-Disposition:` / `Content-Type:` part-header lines without a control-character check, so an attacker controlling one of those values could smuggle a CRLF and inject additional part headers or forge form parts. CR, LF, and NUL in any of those values are now refused, matching the header-safety the mail-header sweep already applies to RFC 822 lines."
1155
+ },
1156
+ {
1157
+ "title": "SAML EncryptedAssertion ML-KEM-1024 and XChaCha20-Poly1305 decryption now work",
1158
+ "body": "verifyResponse's post-quantum key-transport branch and its XChaCha20-Poly1305 content branch called crypto entry points that the crypto module never exported, so every ML-KEM-1024-wrapped or XChaCha20-Poly1305-encrypted SAML assertion failed with an internal TypeError that was re-reported as a key-unwrap or tag-mismatch error — the advertised PQC-first / AEAD SAML encryption was dead. Both paths now route through the exported envelope-open and packed-AEAD primitives, verified with a full encrypt→decrypt round trip. Fails closed (no auth bypass) either way."
1159
+ },
1160
+ {
1161
+ "title": "OAuth device grant polls correctly against spec-compliant providers",
1162
+ "body": "pollDeviceCode issued its token request in the default buffering mode, so the HTTP client rejected the HTTP 400 that RFC 8628 §3.5 / RFC 6749 §5.2 use to carry `authorization_pending` / `slow_down` before the poll loop could read the OAuth error body — the grant aborted on the first poll (which is almost always `authorization_pending`, since the user hasn't approved yet). The request now resolves 4xx OAuth error responses so the pending / slow-down / terminal handling runs and the device grant completes."
1163
+ }
1164
+ ]
1165
+ },
1166
+ {
1167
+ "heading": "Fixed",
1168
+ "items": [
1169
+ {
1170
+ "title": "HTTP client default error carries statusCode and permanent",
1171
+ "body": "request() fell back to the base FrameworkError when no explicit opts.errorClass was passed, so error.statusCode and error.permanent were undefined on the default path despite the documented default of HttpClientError. The default is now HttpClientError, so those fields are populated for every consumer."
1172
+ },
1173
+ {
1174
+ "title": "Static OAuth clients can configure introspection / registration / device-authorization endpoints",
1175
+ "body": "create() never read opts.introspectionEndpoint / registrationEndpoint / deviceAuthorizationEndpoint into its static-endpoint set, so introspectToken / registerClient / deviceAuthorization resolved those endpoints only via OIDC discovery — a static (non-discovery) client could not use them, even though introspectToken's own error told operators to set the endpoint on create(). Those three endpoints are now honored from static config."
1176
+ },
1177
+ {
1178
+ "title": "CLI dev server repeatable flags accumulate",
1179
+ "body": "`blamejs dev --arg / --watch / --ignore` are documented repeatable, but the argument parser overwrote each on repeat, so only the last occurrence survived — only the last --watch directory was monitored, only the last --ignore applied, and the child received only the last --arg. Repeated occurrences of these flags now accumulate as documented."
1180
+ },
1181
+ {
1182
+ "title": "DANE per-recipient failure isolation and MTA-STS testing-mode handling",
1183
+ "body": "A DANE-enforce TLSA-lookup failure threw out of the entire deliver() batch instead of failing just the one recipient; it now fails that recipient and lets the rest proceed. And an MTA-STS policy published in `testing` mode was hard-bounced under the default enforce posture, violating RFC 8461 §5.2 (testing mode is report-only); a testing-mode policy no longer blocks delivery."
1184
+ },
1185
+ {
1186
+ "title": "external-db validates defaultBackend and enforces the pool min floor",
1187
+ "body": "init() did not validate that opts.defaultBackend named a registered backend, so a typo surfaced as an opaque TypeError at the first query instead of a typed config-time error; it is now validated at init. The pool `min` (documented as a floor on idle clients) was never enforced; the reaper now respects it."
1188
+ },
1189
+ {
1190
+ "title": "mail-bounce returns a typed error for a null SES SNS message",
1191
+ "body": "An SES SNS notification whose Message field is JSON literal `null` dereferenced null and threw a raw TypeError (also risking an internal-message leak); it now throws the typed MailBounceError like the other malformed-input paths."
1192
+ }
1193
+ ]
1194
+ }
1195
+ ]
1196
+ },
1197
+ {
1198
+ "version": "0.16.4",
1199
+ "date": "2026-07-03",
1200
+ "headline": "A CalDAV/CardDAV path-traversal fix, an SMTP BDAT body-corruption fix, and a database-stream off-by-one — plus five smaller robustness fixes — surfaced by a large expansion of command-handler and error-branch test coverage across the mail servers, DNS, TLS and database primitives",
1201
+ "summary": "This patch fixes a batch of defects concentrated in the error and adversarial-input paths of the mail-server, DNS, TLS and database primitives, found while substantially expanding the automated test suite over their command handlers and failure branches. The most serious is a CalDAV/CardDAV path-traversal: the request-path guard rejected a literal `..` or `%2e%2e` before decoding, but each path segment was then percent-decoded and handed to the storage backend — so a MIXED encoding such as `.%2e` (neither form the guard checks) slipped through and decoded to `..`, reaching the operator's storage backend with a traversal segment. Path segments are now validated AFTER decoding. On the SMTP submission path, the message body was dot-stuffed (SMTP DATA transparency) inside the message builder and that same body was reused for RFC 3030 BDAT/CHUNKING chunks — which are length-framed and are NOT un-stuffed by the receiver — so any body line beginning with `.` was delivered with a spurious doubled dot to every peer advertising CHUNKING (Gmail/Outlook/Exchange/Postfix all do); dot-stuffing now applies only on the DATA path. The database row-streamer had an off-by-one: a result set of exactly `streamLimit` rows errored with `db/stream-limit-exceeded` instead of completing, because the cap was checked before the iterator could report done — so a valid, complete export surfaced as a stream error. Five smaller fixes round it out: CalDAV/CardDAV now return 400 (not 500) for a malformed/empty client request body; `dns.resolve` rejects a non-string record type with a typed error instead of an untyped TypeError; the DNS negative cache is honored when an explicit negative TTL is set even if the positive TTL is 0; and the Resend mail transport handles a literal JSON `null` response body without taking a mislabeled error path.",
1202
+ "sections": [
1203
+ {
1204
+ "heading": "Security",
1205
+ "items": [
1206
+ {
1207
+ "title": "CalDAV/CardDAV path-traversal via mixed percent-encoding is refused",
1208
+ "body": "`_parsePath` rejected a literal `..`, `%2e%2e`, `%00` or NUL in the RAW request path, but then percent-decoded each segment and passed the decoded value (principalId / collection / component id) straight to the storage backend (getComponent / putComponent / listComponents / …). A mixed encoding such as `.%2e` or `%2e.` contains neither a literal `..` nor `%2e%2e`, so it passed the pre-decode guard and decoded to `..`, reaching the backend with a traversal segment. Each segment is now validated AFTER decoding — any decoded `.` / `..` / NUL / embedded path separator is refused — matching the decode-once-then-validate discipline the router already applies."
1209
+ }
1210
+ ]
1211
+ },
1212
+ {
1213
+ "heading": "Fixed",
1214
+ "items": [
1215
+ {
1216
+ "title": "SMTP BDAT/CHUNKING no longer corrupts body lines beginning with a dot",
1217
+ "body": "The RFC 822 message builder applied SMTP DATA-transparency dot-stuffing (a leading `.` on a line is doubled) to the body, and the submission transport reused that same body for RFC 3030 BDAT chunks. BDAT framing is purely length-based and receivers do NOT un-stuff it, so a body line starting with `.` was delivered with an extra leading dot to any peer advertising CHUNKING (which Gmail, Outlook, Exchange and Postfix all do — CHUNKING is default-on). Dot-stuffing is now applied only when framing via DATA; the message the builder produces (and that DKIM signs) is the un-stuffed body the receiver verifies, and the BDAT path sends it verbatim."
1218
+ },
1219
+ {
1220
+ "title": "db.stream no longer errors on a result set of exactly streamLimit rows",
1221
+ "body": "The stream's per-read cap was checked at the top of the read callback, before the row iterator could report completion, so after emitting exactly `streamLimit` rows the next read destroyed the stream with `db/stream-limit-exceeded` even though the count never exceeded the cap — turning a valid, complete export into an error event. The cap is now enforced after pulling the next row and checking for end-of-results, so exactly-`streamLimit` rows complete cleanly and only a row BEYOND the cap trips the guard."
1222
+ },
1223
+ {
1224
+ "title": "CalDAV/CardDAV return 400 for a malformed or empty client request body",
1225
+ "body": "A malformed PROPFIND/REPORT XML body (or an empty REPORT body) threw from the body parser into the generic dispatch catch, which unconditionally returned 500 — inconsistent with MKCALENDAR/MKCOL, which already return 400 for the same input. Client-fault body-parse errors now return 400 (Bad Request); a genuine server-side backend throw still returns 500."
1226
+ },
1227
+ {
1228
+ "title": "dns.resolve rejects a non-string record type with a typed error",
1229
+ "body": "`dns.resolve(host, type)` called `.toUpperCase()` on whatever `type` was passed, so a non-string (e.g. a number) threw a raw `TypeError` with no code and no `.permanent` flag — unlike an unknown STRING type, which throws a typed `dns/unsupported-type`. A caller keying on the documented `err.permanent` retry contract would have read undefined (falsy) and treated an unfixable input as retryable. A non-string type is now rejected with the typed error."
1230
+ },
1231
+ {
1232
+ "title": "DNS negative cache honors an explicit negative TTL when the positive TTL is 0",
1233
+ "body": "The negative-response cache early-returned whenever the positive `cacheTtlMs` was 0, so `setCacheTtlMs(0, 60000)` (positive caching off, negative caching 60s) silently never populated the negative cache — the accepted negative TTL was not applied. The negative cache now uses the explicit negative TTL even when the positive TTL is 0."
1234
+ },
1235
+ {
1236
+ "title": "Resend transport handles a literal JSON null response body",
1237
+ "body": "The Resend mail transport parsed the response body then dereferenced `data.id`; a literal JSON `null` body parses to `null`, so the dereference threw a raw TypeError that was reported as an interpret failure rather than the intended bad-response verdict. A null/absent parsed body is now treated as a bad response directly."
1238
+ }
1239
+ ]
1240
+ }
1241
+ ]
1242
+ },
1243
+ {
1244
+ "version": "0.16.3",
1245
+ "date": "2026-07-03",
1246
+ "headline": "A batch of fail-open, injection-bypass and error-path fixes across the metrics, guard-sql, MCP, YAML-parser, content-credentials, tus-upload and buffer primitives — including a credential-leak fix on the /metrics exposition surface — alongside a further expansion of the automated test suite",
1247
+ "summary": "This patch closes a set of security and robustness defects concentrated in the error-handling and adversarial-input paths of the observability, SQL-guard, MCP, YAML-parsing, content-credentials and resumable-upload primitives, surfaced while continuing to expand the automated test suite. The most consequential: credential-shaped metric label values were redacted only in the internal cardinality key, not in the rendered exposition, so a raw bearer token / API key / JWT placed on a label leaked verbatim to every `/metrics` scrape reader — redaction now applies to the stored labels the exposition renders. The SQL guard's fragment mode (the `whereRaw` escape hatch) refused an embedded single-quoted literal but not a PostgreSQL dollar-quoted string (`$tag$…$tag$`) or a lone top-level `;`, so operator concatenation into a dollar-quoted context bypassed the floor that forces `?`-placeholder binding. MCP tool-input validation was fail-open when a JSON Schema omitted an explicit top-level `type:\"object\"` (a valid, common shape) — `required`/`properties`/`additionalProperties` were skipped and any input accepted; and its server-initiated sampling token cap was bypassable with a string `maxTokens`. The YAML parser's flow style (`{…}` / `[…]`) accepted duplicate keys and the merge key that block style rejects — letting a later value silently override an earlier one a preceding check had already read — and mis-parsed a root-level JSON object. content-credentials returned a only shallowly-frozen manifest (nested claim fields stayed mutable before signing), and its identity-assertion hash re-check used set membership rather than multiset matching, so duplicate referenced assertions could leave a bound assertion never re-confirmed. Robustness fixes round out the batch: metrics now reject `±Infinity`; the buffer collector preserves a caller error class's code/message (they were swapped for framework error classes, so a caller branching on the error code — e.g. a resumable-upload `413` vs `400` — never matched); resumable uploads return `413` (not a `400` that leaked the internal error code) on an oversized chunk, reject a prototype-key checksum algorithm with `400` (not `500`), and parse a deferred `Upload-Length` as strictly as creation; the MCP guard refuses `/register/` (trailing slash) like `/register`, answers a missing/invalid bearer with `401` and its `WWW-Authenticate` challenge instead of `400`, honors an explicit empty accepted-version list, and accepts a loopback `http` redirect URI for native/CLI clients while still refusing non-loopback `http`.",
1248
+ "sections": [
1249
+ {
1250
+ "heading": "Security",
1251
+ "items": [
1252
+ {
1253
+ "title": "Credential-shaped metric labels are redacted in the `/metrics` exposition, not just the cardinality key",
1254
+ "body": "The metrics registry rewrote a credential-shaped label value to `[REDACTED-CREDENTIAL]`, but only inside the function that builds the internal Map cardinality key. The stored label object — the one the exposition renderer emits — kept the raw value, so a bearer token, API key or JWT placed on a label (e.g. an `authorization` label) was written verbatim to every `/metrics` scrape reader, contradicting the documented guarantee that the bytes never reach the scrape stream. Redaction now runs on the resolved labels themselves, so the key and the rendered output both carry the marker."
1255
+ },
1256
+ {
1257
+ "title": "SQL guard fragment mode refuses PostgreSQL dollar-quoted literals and any top-level semicolon",
1258
+ "body": "In fragment mode (`whereRaw`), the embedded-string-literal floor that forces every value through a `?` placeholder detected a single-quoted literal but not a dollar-quoted one (`$tag$…$tag$` or `$$…$$`), so operator input concatenated into a dollar-quoted context passed the strict floor that the equivalent single-quoted value is refused by. The floor now recognizes dollar-quoted literals. Fragment mode also now refuses any top-level `;` — including a lone or trailing one — rather than relying on the stacked-statement scan, which only fired when a second statement followed the `;`."
1259
+ },
1260
+ {
1261
+ "title": "MCP tool-input validation enforces the schema without an explicit `type:\"object\"`",
1262
+ "body": "`validateToolInput` gated the `required` / `properties` / `additionalProperties` checks on the schema declaring `type:\"object\"`. A schema that carries `properties`/`required` but omits `type` — a valid and common JSON Schema shape — was treated as untyped, so those constraints were skipped and any argument object was accepted, defeating the primitive's stated purpose (OWASP LLM07). Such a schema is now treated as an object schema and its constraints enforced."
1263
+ },
1264
+ {
1265
+ "title": "MCP sampling token cap rejects a non-numeric `maxTokens`",
1266
+ "body": "`sampling.guard` applied the per-request token cap only when `maxTokens` was a number, so a hostile-tool-initiated sampling request carrying `maxTokens` as a string skipped the cap entirely — and a downstream SDK that coerces the string would then exceed the budget. A non-numeric `maxTokens` on this server-initiated surface is now refused with a typed error."
1267
+ },
1268
+ {
1269
+ "title": "content-credentials identity-assertion re-check uses multiset matching",
1270
+ "body": "`verifyIdentityAssertion` re-confirmed the signed referenced-assertion hash binding with count-equality plus set membership, never consuming a matched entry. Supplying referenced assertions that hash to `[A, A]` against a signed binding of `[A, B]` passed — the counts matched and each supplied hash was present — even though the bound `hash(B)` was never re-confirmed against a real assertion (a transplant fail-open). The re-check now consumes each bound hash as it is matched, so duplicates can no longer stand in for a distinct bound assertion."
1271
+ },
1272
+ {
1273
+ "title": "YAML flow-style mappings refuse duplicate keys and the merge key like block style",
1274
+ "body": "Block mappings rejected a duplicate key (`yaml/duplicate-key`) and the anchor-merge key `<<` (`yaml/merge-key-banned`), but flow mappings (`{ a: 1, a: 2 }`) enforced neither — silently keeping the last value on a repeat and accepting `<<` as an ordinary key. A value an earlier check has already read could thus be overridden through flow style. Flow mappings now apply both guards."
1275
+ }
1276
+ ]
1277
+ },
1278
+ {
1279
+ "heading": "Fixed",
1280
+ "items": [
1281
+ {
1282
+ "title": "Metrics reject non-finite values",
1283
+ "body": "`gauge.set`, `histogram.observe` and `counter.inc` guarded their value with an `isNaN`-only check (counter only checked `< 0`), so `±Infinity` passed and produced `Infinity` / `-Infinity` in the exposition — invalid Prometheus 0.0.4 / OpenMetrics, which a scraper rejects. All three now require a finite number, matching their `must be a finite number` error text."
1284
+ },
1285
+ {
1286
+ "title": "Buffer stream collector preserves a caller error class's code and message",
1287
+ "body": "`safeBuffer.collectStream` constructs its size-limit error with `(message, code)`, matching its own error class — but a framework error class (the convention elsewhere) takes `(code, message)`, the opposite order, so a caller that passed its own error class received an error with `code` and `message` swapped. A caller branching on the error code then never matched — for example the resumable-upload handler mapped an oversized chunk to `400` instead of `413`. The collector now sets both fields explicitly so they are correct whichever constructor order the passed class uses."
1288
+ },
1289
+ {
1290
+ "title": "Resumable upload: correct status codes and strict deferred-length parse",
1291
+ "body": "An oversized `PATCH`/creation chunk now returns `413 Payload Too Large` instead of a `400` whose body leaked the internal error code; an `Upload-Checksum` naming a prototype key (e.g. `__proto__`) returns `400 unsupported` instead of a `500` from an unguarded lookup; and a deferred-length finalization now parses `Upload-Length` with the same strict check as creation, rejecting trailing junk (`\"10abc\"`) rather than silently truncating it."
1292
+ },
1293
+ {
1294
+ "title": "MCP guard: trailing-slash registration refusal, 401 bearer challenge, empty accepted-version list",
1295
+ "body": "The static-registration refusal matched `/register` exactly or by suffix, so `/register/` — which most routers normalize to the same handler — slipped through; it is now refused too. A missing or invalid bearer now returns `401 Unauthorized` with its `WWW-Authenticate` challenge (RFC 6750 §3) instead of `400`, so clients keyed on `401` re-authenticate. And an explicit empty `accepted` protocol-version list now rejects every version instead of silently widening to the default set."
1296
+ },
1297
+ {
1298
+ "title": "MCP accepts a loopback `http` redirect URI",
1299
+ "body": "`redirect_uri` validation parsed the URI with the default HTTPS-only protocol set, so a loopback `http://localhost/…` redirect (RFC 8252 native-app / CLI client, permitted by RFC 9700 §4.1.1) failed as `did not parse` before the local-host allowance could run. The parse now admits `http` so the loopback allowance applies; a non-loopback `http` redirect is still refused."
1300
+ },
1301
+ {
1302
+ "title": "YAML: trailing content after a flow collection and root-level JSON objects",
1303
+ "body": "`root: [1, 2] junk` silently dropped the trailing content where the quoted-scalar path rejects it; trailing content after a flow collection is now refused (`yaml/trailing-content`). A root-level flow/JSON object (`{\"a\": 1, \"b\": 2}`) was mis-scanned as a block mapping with a garbage key and returned structurally-wrong data; the leading flow bracket is now recognized before the block-mapping scan so a root JSON object parses correctly."
1304
+ },
1305
+ {
1306
+ "title": "content-credentials: deep-frozen manifest and typed rejections for bad build/sign inputs",
1307
+ "body": "`build` returned a shallowly-frozen manifest, leaving every nested claim field (`provider`/`system`/`content`) mutable before signing despite the documented pre-sign immutability guarantee; the manifest is now deep-frozen. A non-finite or out-of-Date-range `generatedAt` (`NaN` / `Infinity`) crashed with an untyped `RangeError`, and a non-Buffer `certChain` entry to `signCose` threw an untyped Node `TypeError`; both now reject with a typed `content-credentials/*` error at call time."
1308
+ }
1309
+ ]
1310
+ }
1311
+ ]
1312
+ },
1313
+ {
1314
+ "version": "0.16.2",
1315
+ "date": "2026-07-03",
1316
+ "headline": "Security hardening across the mail, OAuth/SAML, ACME, keychain and router primitives — six fail-open / injection / bypass fixes and seven correctness fixes — alongside a large expansion of the automated test suite",
1317
+ "summary": "This patch closes a batch of security and correctness defects surfaced while substantially expanding the test suite across the framework's mail-authentication, federated-identity, ACME, keychain, backup, CLI and router primitives. The security fixes: SPF evaluation no longer throws (and thus can no longer be laundered into a temperror that a permissive inbound policy accepts) on a malformed `ip4:` CIDR mask; the Authentication-Results builder now refuses CR/LF/NUL in the `version` field, closing the last header-injection gap left by the earlier mail-header sweep; `res.redirect` now rejects a horizontal TAB (which user agents strip before URL parsing, turning `/\\t/evil.example` into a protocol-relative redirect to an attacker origin); the OAuth refresh-token one-time-use replay gate now treats any truthy `seen()` result as seen (a Redis `1` / SQL `COUNT` no longer slips a stolen refresh token past a `=== true` compare) — with the same normalization applied to its sibling replay checks; SAML Single-Logout over HTTP-POST/SOAP now fails closed (throws) when a verification key is supplied without its algorithm, matching the redirect binding instead of silently skipping signature verification; and the OAuth authorization-URL / PAR builders now refuse operator `extraParams` that collide with framework-managed parameters (`redirect_uri`, `state`, `code_challenge`, …). The correctness fixes: concurrent keychain file-store writes are serialized under the existing file lock (no more lost bindings); TLS-RPT submission reads the real `statusCode` (successful reports are no longer misreported as failures); ACME account key-rollover stops double-encoding its inner JWS payload (rollover was always rejected by the CA); the response-schema validator now also validates JSON bodies sent via `res.end`; keychain `remove` validates a relative fallback path like `store`/`retrieve`; `bundleAdapterStorage` honors the ambient compliance posture like `create`; and a refused `dev --ignore` pattern now returns exit code 2 instead of rejecting the CLI promise.",
1318
+ "sections": [
1319
+ {
1320
+ "heading": "Security",
1321
+ "items": [
1322
+ {
1323
+ "title": "SPF `ip4:` with a malformed CIDR mask fails closed instead of throwing (fail-open)",
1324
+ "body": "`_ipv4InCidr` lacked the finite-mask guard its IPv6 sibling has, so an `ip4:` mechanism with a non-numeric prefix (e.g. `ip4:192.0.2.0/`) reached `BigInt(32 - NaN)` and threw an uncaught `RangeError` out of `b.mail.spf.verify` / `b.mail.inbound.verify`. Upstream mail handling catches that throw as a temperror, and an inbound policy configured to accept on temperror would then admit a sender that should have been evaluated. SPF now returns a `permerror` result for a malformed mask, honoring the primitive's documented never-throw-on-message-content contract."
1325
+ },
1326
+ {
1327
+ "title": "Authentication-Results builder refuses CR/LF/NUL in the `version` field (header injection)",
1328
+ "body": "`b.mail.authResults.emit` validated the `authserv-id` against control characters but not the operator-supplied `version`, which is interpolated onto the `Authentication-Results:` header line — the last field left unguarded by the earlier mail-header CRLF sweep. A `version` containing CRLF could smuggle an arbitrary header. It is now refused with a typed error, like every other field on that line."
1329
+ },
1330
+ {
1331
+ "title": "`res.redirect` rejects horizontal TAB (open-redirect / same-origin bypass)",
1332
+ "body": "The redirect target's control-character guard rejected CR/LF/NUL but not TAB (0x09). Node permits a TAB in a header value, but the WHATWG URL parser strips ASCII TAB/LF/CR from a URL before resolving it, so a `Location: /\\t/evil.example` collapses to the protocol-relative `//evil.example` and navigates to the attacker origin — bypassing the same-origin/allowlist heuristic. TAB is now rejected alongside CR/LF/NUL."
1333
+ },
1334
+ {
1335
+ "title": "OAuth refresh-token replay gate treats any truthy `seen()` result as seen (fail-open)",
1336
+ "body": "The legacy `seen(refreshToken)` replay gate compared the callback's return with `=== true`, but the documented contract is that it returns a truthy value when the token was presented before. A store returning a truthy non-`true` value — a Redis `EXISTS`/`SISMEMBER` `1`, a SQL `COUNT` — slipped a replayed (stolen) refresh token past the one-time-use gate. Any truthy result now counts as seen; the same normalization was applied to the sibling replay checks in the module."
1337
+ },
1338
+ {
1339
+ "title": "SAML Single-Logout over HTTP-POST/SOAP fails closed on a key without its algorithm",
1340
+ "body": "The POST and SOAP SLO parsers gated signature verification with an OR condition and the verify helper early-returned when the algorithm was absent, so supplying a verification key without `idpVerifyAlg` silently accepted a forged, unsigned LogoutRequest — while the HTTP-Redirect binding failed closed. All four SLO parse paths now route through one config check that throws when a key is supplied without its algorithm."
1341
+ },
1342
+ {
1343
+ "title": "OAuth authorization-URL / PAR builders refuse `extraParams` that collide with managed parameters",
1344
+ "body": "`authorizationUrl` and `pushAuthorizationRequest` merged operator `extraParams` with `URLSearchParams.set`, which replaces — so an `extraParams` entry for `redirect_uri`, `state`, or `code_challenge` overwrote the framework-generated value while the call still returned the framework's `state`/PKCE verifier, diverging the returned session seed from the emitted URL. Reserved parameter keys in `extraParams` are now refused with a typed error at both builders."
1345
+ }
1346
+ ]
1347
+ },
1348
+ {
1349
+ "heading": "Fixed",
1350
+ "items": [
1351
+ {
1352
+ "title": "Concurrent keychain file-store writes no longer drop bindings",
1353
+ "body": "`store` and the file-remove path performed an unlocked read-modify-write of the fallback file, so two concurrent stores could each read the pre-update document and the later write would clobber the earlier binding. Both read-modify-write sequences are now serialized under the file's existing lock."
1354
+ },
1355
+ {
1356
+ "title": "TLS-RPT submission reads the response `statusCode`",
1357
+ "body": "`tlsRpt.submit` read `status` from the HTTP client response, but the client resolves `statusCode`, so a successful (2xx) report POST was reported as `{ ok: false, error: 'HTTP undefined' }`. It now reads `statusCode`, so delivered reports are distinguished from rejected ones."
1358
+ },
1359
+ {
1360
+ "title": "ACME account key-rollover no longer double-encodes its inner JWS payload",
1361
+ "body": "`accountKeyRollover` passed an already-stringified payload to a signer that stringifies internally, so the inner keyChange JWS payload encoded a JSON string instead of the JSON object and every rollover was rejected by the CA (RFC 8555 §7.3.5). The payload is now passed once."
1362
+ },
1363
+ {
1364
+ "title": "Response-schema validator also validates `res.end` JSON bodies",
1365
+ "body": "The response validator wrapped only `res.json`, so a handler shipping an invalid body via `res.end(JSON.stringify(...))` escaped validation despite the documented contract. JSON-shaped `res.end` bodies are now validated on the same path."
1366
+ },
1367
+ {
1368
+ "title": "keychain `remove` validates a relative fallback path like `store`/`retrieve`",
1369
+ "body": "`remove` checked file existence before validating the fallback path, so a relative path silently no-op'd instead of throwing the config error that `store`/`retrieve` raise. It now validates first, consistent with the other file-fallback paths."
1370
+ },
1371
+ {
1372
+ "title": "`bundleAdapterStorage` honors the ambient compliance posture",
1373
+ "body": "`bundleAdapterStorage` only refused an unencrypted strategy when an explicit posture was passed, unlike `create`, which reads the ambient `b.compliance` posture and refuses `encrypt:false` under HIPAA. `bundleAdapterStorage` now falls back to the ambient posture, closing the asymmetry."
1374
+ },
1375
+ {
1376
+ "title": "`dev --ignore` pattern refusal returns exit code 2",
1377
+ "body": "A `dev --ignore` pattern that exceeded the length cap or was refused by the regex guard threw out of the CLI promise instead of writing to stderr and returning exit code 2 like every other CLI validation failure. It now returns the standard non-zero exit."
1378
+ }
1379
+ ]
1380
+ }
1381
+ ]
1382
+ },
1383
+ {
1384
+ "version": "0.16.1",
1385
+ "date": "2026-07-03",
1386
+ "headline": "Every source file now carries an SPDX license + copyright header; the wiki example emits its full HSTS when it runs behind a TLS-terminating reverse proxy; and a good-first-issue on-ramp lands for new contributors",
1387
+ "summary": "This patch stamps a per-file `SPDX-License-Identifier: Apache-2.0` and a copyright line onto every first-party source file, so the license of any single file is unambiguous when it is read, audited, or vendored in isolation. The project stays Apache-2.0 — this is a clarity change, not a license change, and vendored third-party files under lib/vendor/ keep their own upstream headers. The wiki example now passes its trusted-proxy CIDRs to the securityHeaders middleware: a deployment behind a TLS-terminating reverse proxy (Caddy or nginx) that forwards `X-Forwarded-Proto: https` now emits the framework's strong HSTS (two-year max-age, includeSubDomains, preload) instead of suppressing it on the plain-HTTP hop from the proxy — the wiki e2e asserts both the proxied-https and the direct-http cases. CONTRIBUTING gains a good-first-issue on-ramp so new contributors have a clear, small first step.",
1388
+ "sections": [
1389
+ {
1390
+ "heading": "Security",
1391
+ "items": [
1392
+ {
1393
+ "title": "Wiki example emits its full HSTS behind a TLS-terminating reverse proxy",
1394
+ "body": "The wiki example app now hands its trusted-proxy CIDRs to `securityHeaders`, so behind Caddy or nginx forwarding `X-Forwarded-Proto: https` it emits `Strict-Transport-Security: max-age=63072000; includeSubDomains; preload` rather than dropping HSTS on the internal HTTP hop from the proxy. Operators modelling their own app on the example inherit the fix by declaring their proxy CIDRs via `WIKI_ADMIN_TRUSTED_PROXIES` (the production compose already defaults to the private ranges). The framework's `securityHeaders` primitive is unchanged; this closes a wiring gap in the example."
1395
+ }
1396
+ ]
1397
+ },
1398
+ {
1399
+ "heading": "Added",
1400
+ "items": [
1401
+ {
1402
+ "title": "Per-file SPDX license and copyright headers",
1403
+ "body": "Every first-party source file now begins with `// SPDX-License-Identifier: Apache-2.0` and a copyright line, so the license and holder of any individual file are explicit when it is read or extracted on its own. No license change — the project remains Apache-2.0. Vendored files under `lib/vendor/` are untouched and retain their upstream license headers."
1404
+ }
1405
+ ]
1406
+ },
1407
+ {
1408
+ "heading": "Changed",
1409
+ "items": [
1410
+ {
1411
+ "title": "Good-first-issue on-ramp for new contributors",
1412
+ "body": "CONTRIBUTING now points new contributors at issues labelled `good first issue` — small, self-contained tasks (a doc or wiki-example fix, a test for an uncovered branch) that are the easiest first step into the codebase."
1413
+ }
1414
+ ]
1415
+ }
1416
+ ]
1417
+ },
1418
+ {
1419
+ "version": "0.16.0",
1420
+ "date": "2026-07-03",
1421
+ "headline": "Raises the minimum Node.js engine to 24.18.0; the status-list verifier now binds the token type against a typ-confusion replay; and CI supply-chain pinning is consolidated behind committed dev/example lockfiles, `npm ci`, and a single aggregating pin tool that closes the OpenSSF Scorecard pinned-dependency gaps",
1422
+ "summary": "This minor raises the supported Node.js floor from 24.16.0 to 24.18.0 — operators must run Node 24.18.0 or newer (24.17.0 was a security release; 24.18.0 bundles SQLite 3.53.1 and a backup-path fix, and guarantees the native OpenSSL 3.5 SLH-DSA surface). b.auth.statusList.fromJwt now enforces RFC 8725 §3.11 by binding the JWS header typ to the \"statuslist+jwt\" that toJwt stamps, so a token minted for another purpose can't be replayed as a status list. The build's supply-chain pinning is reworked so OpenSSF Scorecard sees pinned installs everywhere it can: the dev-tool, example, and fuzz manifests now ship committed lockfiles and CI installs them with `npm ci` (Scorecard keys pinning on the `ci` verb, not on an `@version` specifier), the vendored fuzz toolchain and the oss-fuzz base image are pinned, and a new scripts/pin-all.js aggregates every pin — regenerating all lockfiles, pinning the GitHub Action SHAs, and syncing the Docker base-image digests in one `--fix`, with a `--check` gate wired into CI and the release flow so no pin can silently drift. The committed lockfiles are dev/example-only and excluded from the published tarball; the zero-npm-RUNTIME-deps rule is unchanged.",
1423
+ "sections": [
1424
+ {
1425
+ "heading": "Changed",
1426
+ "items": [
1427
+ {
1428
+ "title": "Minimum Node.js engine raised to 24.18.0",
1429
+ "body": "engines.node is now >=24.18.0 (was >=24.16.0). Operators must run Node 24.18.0 or newer. 24.17.0 was a security release and 24.18.0 bundles SQLite 3.53.1 plus a backup-keepalive fix; the floor also guarantees the native OpenSSL 3.5 SLH-DSA sign/verify surface. CI, the release container, and the docs are updated to the new floor."
1430
+ },
1431
+ {
1432
+ "title": "Supply-chain pinning consolidated behind committed lockfiles + `npm ci` + one aggregating pin tool",
1433
+ "body": "The root dev-tool (esbuild, postject), examples/wiki, and fuzz (jazzer.js) manifests now ship committed package-lock.json files, and CI installs them with `npm ci` instead of `npm install pkg@version` — OpenSSF Scorecard's Pinned-Dependencies check treats only the `npm ci` verb as pinned, so this is what actually clears those alerts (an exact `@version` specifier does not). The vendored fuzz toolchain is pinned to an exact jazzer version and the oss-fuzz base image is digest-pinned (mirroring the Dependabot-tracked .clusterfuzzlite digest). A new scripts/pin-all.js aggregates every pinning segment — it regenerates all committed lockfiles, pins the GitHub Action SHAs (via check-actions-currency), and syncs the Docker base-image digests in one `--fix`; a `--check` mode runs in CI and scripts/release.js so a stale lockfile or drifted digest fails closed. The committed lockfiles are dev/example-only, excluded from the published tarball's files allowlist; the zero-npm-RUNTIME-deps rule is unchanged."
1434
+ }
1435
+ ]
1436
+ },
1437
+ {
1438
+ "heading": "Security",
1439
+ "items": [
1440
+ {
1441
+ "title": "Status-list verification binds the token type (RFC 8725 §3.11 typ-confusion)",
1442
+ "body": "b.auth.statusList.fromJwt now passes expectedTyp \"statuslist+jwt\" to the JWT verifier, matching the typ that b.auth.statusList.toJwt stamps and every sibling verifier enforces. A token minted for another purpose — even one that happens to carry a status_list.lst claim — is now refused on the type binding before the claim is read, closing the typ-confusion class where such a token could be replayed as a status list."
1443
+ }
1444
+ ]
1445
+ },
1446
+ {
1447
+ "heading": "Added",
1448
+ "items": [
1449
+ {
1450
+ "title": "scripts/pin-all.js — aggregated supply-chain pin tool",
1451
+ "body": "One command re-pins every supply-chain segment: `--fix` regenerates the committed lockfiles (root / examples/wiki / fuzz), pins the GitHub Action SHAs, and syncs the Docker base-image digests; `--check` (wired into CI and the release flow) verifies the lockfile and digest segments — including each lockfile's package version — are in sync and fails closed on drift. Contributor tooling — it does not ship in the tarball."
1452
+ },
1453
+ {
1454
+ "title": "Project-governance and assurance documentation; OpenSSF Best Practices silver badge",
1455
+ "body": "Adds a ROADMAP.md (documented direction plus an explicit out-of-scope list), a Developer Certificate of Origin policy in CONTRIBUTING.md (contributions carry a Signed-off-by; releases sign off through the release tooling), and an explicit security assurance case in SECURITY.md (a top security claim decomposed into evidence-backed sub-claims, with a residual-risk statement). The project earned the OpenSSF Best Practices silver badge, linked from the README."
1456
+ }
1457
+ ]
1458
+ }
1459
+ ]
1460
+ }
1461
+ ]
1462
+ }