@blamejs/core 0.7.18 → 0.7.19

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 (167) hide show
  1. package/CHANGELOG.md +425 -423
  2. package/README.md +150 -150
  3. package/bin/blamejs.js +0 -0
  4. package/index.js +310 -308
  5. package/lib/api-key.js +660 -660
  6. package/lib/api-snapshot.js +338 -338
  7. package/lib/app-shutdown.js +385 -385
  8. package/lib/app.js +365 -365
  9. package/lib/archive.js +250 -250
  10. package/lib/atomic-file.js +544 -544
  11. package/lib/audit-chain.js +177 -177
  12. package/lib/audit-sign.js +344 -344
  13. package/lib/audit-tools.js +677 -677
  14. package/lib/audit.js +766 -766
  15. package/lib/auth/jwt-external.js +365 -0
  16. package/lib/auth/jwt.js +337 -311
  17. package/lib/auth/lockout.js +436 -436
  18. package/lib/auth/oauth.js +721 -721
  19. package/lib/auth/passkey.js +181 -181
  20. package/lib/auth/password.js +628 -594
  21. package/lib/backup/bundle.js +217 -217
  22. package/lib/backup/crypto.js +176 -176
  23. package/lib/backup/index.js +515 -515
  24. package/lib/backup/manifest.js +282 -282
  25. package/lib/break-glass.js +1338 -1338
  26. package/lib/bundler.js +441 -441
  27. package/lib/cache-redis.js +256 -256
  28. package/lib/cache.js +1206 -1206
  29. package/lib/canonical-json.js +115 -115
  30. package/lib/chain-writer.js +234 -234
  31. package/lib/cli-helpers.js +206 -206
  32. package/lib/cli.js +2334 -2334
  33. package/lib/cluster-provider-db.js +317 -317
  34. package/lib/cluster-storage.js +226 -226
  35. package/lib/cluster.js +703 -703
  36. package/lib/config-drift.js +301 -301
  37. package/lib/consent.js +222 -222
  38. package/lib/constants.js +191 -191
  39. package/lib/cookies.js +315 -315
  40. package/lib/credential-hash.js +322 -322
  41. package/lib/crypto.js +266 -266
  42. package/lib/csv.js +275 -275
  43. package/lib/db-declare-row-policy.js +267 -267
  44. package/lib/db-declare-view.js +420 -420
  45. package/lib/db-query.js +406 -406
  46. package/lib/db-schema.js +319 -319
  47. package/lib/db.js +1288 -1288
  48. package/lib/deprecate.js +222 -222
  49. package/lib/dev.js +335 -335
  50. package/lib/dual-control.js +473 -473
  51. package/lib/error-page.js +420 -420
  52. package/lib/external-db-migrate.js +441 -441
  53. package/lib/external-db.js +1061 -1061
  54. package/lib/file-type.js +273 -273
  55. package/lib/forms.js +422 -422
  56. package/lib/framework-error.js +293 -293
  57. package/lib/framework-schema.js +717 -717
  58. package/lib/handlers.js +350 -350
  59. package/lib/http-client-cookie-jar.js +508 -508
  60. package/lib/http-client.js +1195 -1195
  61. package/lib/i18n.js +878 -878
  62. package/lib/jobs.js +185 -185
  63. package/lib/log-stream-cloudwatch.js +369 -369
  64. package/lib/log-stream-local.js +146 -146
  65. package/lib/log-stream-otlp-grpc.js +410 -410
  66. package/lib/log-stream-otlp.js +286 -286
  67. package/lib/log-stream-syslog.js +302 -302
  68. package/lib/log-stream-webhook.js +199 -199
  69. package/lib/log-stream.js +330 -330
  70. package/lib/log.js +500 -500
  71. package/lib/mail-bounce.js +528 -528
  72. package/lib/mail-dkim.js +369 -369
  73. package/lib/mail.js +981 -981
  74. package/lib/metrics.js +683 -683
  75. package/lib/middleware/api-encrypt.js +936 -936
  76. package/lib/middleware/attach-user.js +157 -157
  77. package/lib/middleware/bearer-auth.js +152 -0
  78. package/lib/middleware/body-parser.js +1170 -1170
  79. package/lib/middleware/bot-guard.js +178 -178
  80. package/lib/middleware/compression.js +452 -452
  81. package/lib/middleware/cors.js +314 -314
  82. package/lib/middleware/csp-nonce.js +348 -348
  83. package/lib/middleware/csrf-protect.js +316 -316
  84. package/lib/middleware/db-role-for.js +264 -264
  85. package/lib/middleware/health.js +392 -392
  86. package/lib/middleware/index.js +82 -79
  87. package/lib/middleware/rate-limit.js +358 -358
  88. package/lib/middleware/request-id.js +61 -61
  89. package/lib/middleware/request-log.js +168 -168
  90. package/lib/middleware/require-auth.js +104 -104
  91. package/lib/middleware/security-headers.js +116 -116
  92. package/lib/middleware/sse.js +166 -166
  93. package/lib/migrations.js +383 -383
  94. package/lib/mtls-ca.js +518 -518
  95. package/lib/mtls-engine-default.js +481 -481
  96. package/lib/network-dns.js +632 -632
  97. package/lib/network-heartbeat.js +290 -290
  98. package/lib/network-nts.js +574 -574
  99. package/lib/network-proxy.js +265 -265
  100. package/lib/network-tls.js +328 -328
  101. package/lib/network.js +233 -233
  102. package/lib/notify.js +612 -612
  103. package/lib/ntp-check.js +229 -229
  104. package/lib/numeric-bounds.js +111 -111
  105. package/lib/object-store/azure-blob-bucket-ops.js +349 -349
  106. package/lib/object-store/azure-blob.js +488 -488
  107. package/lib/object-store/gcs-bucket-ops.js +351 -351
  108. package/lib/object-store/gcs.js +519 -519
  109. package/lib/object-store/http-put.js +153 -153
  110. package/lib/object-store/index.js +197 -197
  111. package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
  112. package/lib/object-store/sigv4.js +903 -903
  113. package/lib/observability.js +151 -151
  114. package/lib/otel-export.js +269 -269
  115. package/lib/pagination.js +464 -464
  116. package/lib/parsers/index.js +80 -80
  117. package/lib/parsers/safe-env.js +642 -642
  118. package/lib/parsers/safe-ini.js +292 -292
  119. package/lib/parsers/safe-toml.js +784 -784
  120. package/lib/parsers/safe-xml.js +390 -390
  121. package/lib/parsers/safe-yaml.js +1015 -1015
  122. package/lib/permissions.js +708 -708
  123. package/lib/pqc-agent.js +87 -87
  124. package/lib/pqc-gate.js +279 -279
  125. package/lib/protobuf-encoder.js +190 -190
  126. package/lib/protocol-dispatcher.js +161 -161
  127. package/lib/pubsub-redis.js +167 -167
  128. package/lib/pubsub.js +429 -429
  129. package/lib/queue-local.js +476 -476
  130. package/lib/queue-redis.js +745 -745
  131. package/lib/queue-sqs.js +319 -319
  132. package/lib/queue.js +695 -695
  133. package/lib/redis-client.js +519 -519
  134. package/lib/request-helpers.js +340 -340
  135. package/lib/restore-bundle.js +237 -237
  136. package/lib/restore-rollback.js +259 -259
  137. package/lib/restore.js +409 -409
  138. package/lib/retry.js +376 -376
  139. package/lib/router.js +748 -748
  140. package/lib/safe-async.js +735 -735
  141. package/lib/safe-buffer.js +237 -237
  142. package/lib/safe-json.js +541 -541
  143. package/lib/safe-schema.js +1266 -1266
  144. package/lib/safe-url.js +159 -159
  145. package/lib/scheduler.js +706 -706
  146. package/lib/security-assert.js +373 -373
  147. package/lib/seeders.js +618 -618
  148. package/lib/session.js +535 -478
  149. package/lib/slug.js +269 -269
  150. package/lib/ssrf-guard.js +401 -401
  151. package/lib/storage.js +471 -471
  152. package/lib/subject.js +281 -281
  153. package/lib/template.js +791 -791
  154. package/lib/testing.js +798 -798
  155. package/lib/time.js +310 -310
  156. package/lib/totp.js +302 -302
  157. package/lib/tracing.js +494 -494
  158. package/lib/uuid.js +132 -132
  159. package/lib/validate-opts.js +340 -340
  160. package/lib/vault/index.js +308 -308
  161. package/lib/vault/rotate.js +784 -784
  162. package/lib/vault/wrap.js +296 -296
  163. package/lib/vendor/noble-ciphers.cjs +9 -9
  164. package/lib/webhook.js +595 -595
  165. package/lib/websocket.js +1048 -1048
  166. package/package.json +77 -77
  167. package/sbom.cyclonedx.json +7 -7
package/CHANGELOG.md CHANGED
@@ -1,423 +1,425 @@
1
- # Changelog
2
-
3
- One entry per released tag, grouped by minor. Latest first.
4
-
5
- Pre-1.0 the surface is intentionally evolving — every release may
6
- change something operators depend on. Read each entry before
7
- upgrading across more than a few patches at a time.
8
-
9
- ## v0.7.x
10
-
11
- - **0.7.18** (2026-05-05) — transport-layer smuggling hardening (four ship-blocker fixes). **HTTP request-smuggling defense** in `b.middleware.bodyParser` per RFC 9112 §6.1: rejects requests with both `Content-Length` and `Transfer-Encoding` headers (CL.TE / TE.CL smuggling — CVE-2022-31394 / CVE-2024-27316 class), multiple `Content-Length` values, `Transfer-Encoding` whose final coding is not `chunked`, and duplicate `chunked` tokens (TE.TE smuggling). Each rejection responds 400 + `Connection: close` so the upstream proxy doesn't reuse the socket. **Static-serve symlink-escape + filename safety** in `b.staticServe`: `_resolveSafe` now `fs.realpathSync`-es the resolved path (defeats symlink-out-of-root) AND validates the basename through `b.guardFilename` at the balanced profile (rejects path traversal, null-byte, NTFS alternate data streams, UNC paths, RTLO bidi, overlong UTF-8, Windows reserved device names, double-extension; balanced profile chosen over strict so legitimate operator-deposited shell-exec extensions like `.exe`/`.bin` remain serveable). **Outbound SMTP smuggling defense** in `b.mail` SMTP transport: every produced RFC 822 wire (post-DKIM-sign) is run through `b.guardEmail.validateMessage` at strict profile before the socket opens; refuses on critical issues bare CR / bare LF + smuggled SMTP verbs (CVE-2023-51764 Postfix / CVE-2023-51765 Sendmail / CVE-2023-51766 Exim / CVE-2026-32178 .NET class) cannot leave the framework even when operator-supplied subject/body/headers contain the pattern. **DKIM `l=` body-length tag forbidden** in `b.mail.dkim.create`: passing `bodyLength` now throws `dkim/l-tag-forbidden` at create-time. M³AAWG / Gmail / Microsoft 365 guidance is "never use l="it enables append-after-signature attacks where an attacker appends arbitrary content past the signed length and the DKIM signature still validates against the original prefix. The body is always hashed in full. Smoke 84508458 / wiki e2e 178 / Linux container smoke 8458 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
12
-
13
- - **0.7.17** (2026-05-05) — `b.guardEmail` email content-safety primitive (single-address validation + full RFC 822 / 5322 message validation). Threat catalog grounded in current research (SMTP smuggling CVE-2023-51764 Postfix / CVE-2023-51765 Sendmail / CVE-2023-51766 Exim / CVE-2026-32178 .NET System.Net.Mail; SEC Consult / smtpsmuggling.com class; IDN homograph attacks; CRLF header injection). Surface: `validateAddress(addr, opts)` validates a single address; `validateMessage(rfc822, opts)` validates a full message; `validate(input, opts)` auto-detects; `sanitize(input, opts)` strips character-class threats but throws on critical (smuggling / CRLF injection / multi-@ / mixed-script no safe sanitization for these); `gate(opts)` returns a `b.gateContract`-shaped gate auto-routed by `b.guardAll` for `message/rfc822` / `message/global`. KIND="content". **Threat catalog**: SMTP smuggling (bare CR / bare LF outside CRLF pairs combined with embedded SMTP verbs `MAIL FROM`/`RCPT TO`/`DATA`/`EHLO`/`HELO`/`RSET`/`QUIT`); CRLF header injection in single-line headers; IDN homograph spoofing (mixed-script Cyrillic / Greek / Armenian / Cherokee codepoints in domains operator opts in via `allowedScripts`); Punycode/IDN flag; display-name spoofing (`"support@apple.com" <attacker@evil>` display contains @-address that doesn't match envelope domain); IP literal addresses (`user@[1.2.3.4]` bypasses DNS/DMARC alignment); RFC 5322 comment syntax in addresses; multiple @ characters; RFC 5321 length caps (local-part 64 / domain 255 / address 320); RFC 5322 line cap (998); BOM injection; bidi/null/control/zero-width chars in addresses + headers. **Profiles** strict / balanced / permissive (SMTP smuggling + CRLF header injection + multi-@ + null bytes refused at every profile universal class). **Compliance postures** hipaa / pci-dss / gdpr / soc2-cc7. **Strict default-on via v0.7.12**: every `b.fileUpload` + `b.staticServe` deploy gets this gate at strict profile automatically. Smoke 8387smoke / wiki e2e / Linux container smoke + wiki e2e green / eslint clean / api-snapshot baseline refreshed.
14
-
15
- - **0.7.16** (2026-05-05) — `b.guardMarkdown` markdown content-safety primitive. Threat catalog grounded in current research (CVE-2026-30838 CommonMark DisallowedRawHtml whitespace-tag bypass; CVE-2025-9540 Markup Markdown javascript: link XSS; CVE-2025-7969 markdown-it ReDoS; CVE-2025-6493 CodeMirror Markdown catastrophic backtracking; CVE-2025-24981 MDC autolink XSS; CVE-2026-33500 AVideo Parsedown link bypass; GHSA-gwjh-c548-f787 NuGetGallery autolink XSS; Joplin GHSA-hff8-hjwv-j9q7 RCE via untrusted markdown link). Surface: `validate(input, opts)` returns `{ ok, issues }`; `sanitize(input, opts)` strips character-class threats but throws on critical (dangerous tags / dangerous URL schemes / DOCTYPE / code-fence injection / image scheme / autolink scheme — no safe sanitization for these); `gate(opts)` returns a `b.gateContract`-shaped gate auto-routed by `b.guardAll` for `text/markdown` / `text/x-markdown` / `text/x-gfm`. KIND="content". Source-level discipline runs BEFORE any downstream renderer (marked / markdown-it / commonmark / remark / parsedown) sees the input, the same shape that catches `__proto__` in JSON before parse. **Threat catalog**: dangerous tags (script/iframe/object/embed/form/input/meta/link/base/svg/math/video/audio/style/template/portal/marquee refused at every profile, whitespace-tolerant per CVE-2026-30838); dangerous URL schemes in inline links + images + autolinks + reference-link definitions (javascript:/vbscript:/livescript:/mocha:/view-source:/data:/jar:/blob:/feed:/tel:/facetime:refused at every profile); HTML-entity scheme bypass (`&#x6A;avascript:` / `&#106;avascript:` decoded BEFORE scheme matching); reference-link smuggling (`[label]: javascript:...` definition smuggle); autolink scheme bypass (`<javascript:alert(1)>`); front-matter YAML/TOML blocks; HTML comments; code-fence language injection (language tag containing `<>"' `` blocks attribute breakout); inline DOCTYPE; catastrophic emphasis runs (`*`/`_` of length 20+ — CVE-2025-6493 class); bidi/null/control/zero-width chars; total-bytes + line + link + image + autolink + ref-def + list-depth + blockquote-depth caps. **Profiles** — strict / balanced / permissive (dangerous tags + dangerous schemes + image schemes + autolink schemes refused at every profile — script-tag and javascript: are universal class). **Compliance postures** — hipaa / pci-dss / gdpr / soc2-cc7. **Strict default-on via v0.7.12**: every `b.fileUpload` + `b.staticServe` deploy gets this gate at strict profile automatically. Smoke 83208387 / wiki e2e 178 / Linux container smoke 8387 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
16
-
17
- - **0.7.15** (2026-05-04) — `b.guardXml` XML content-safety primitive + smoke parallel mode + persistent test output. **`b.guardXml`** threat catalog grounded in current research (CVE-2026-24400 AssertJ XXE; CVE-2025-3225 sitemap parser; CVE-2024-1455 LangChain; CVE-2024-25062 libxml2 use-after-free with DTD + XInclude; CVE-2024-56171 + CVE-2025-24928 + CVE-2025-32415 + CVE-2025-27113 libxml2 family; CVE-2024-8176 libexpat stack overflow via recursive entity expansion). Surface: `validate(input, opts)` returns `{ ok, issues }`; `sanitize(input, opts)` strips character-class threats but throws on critical (DOCTYPE / ENTITY / external — no safe sanitization); `gate(opts)` returns a `b.gateContract`-shaped gate auto-routed by `b.guardAll` for `application/xml` / `text/xml`. KIND="content". **Threat catalog**: DOCTYPE refused unconditionally (XXE / billion-laughs vector); `<!ENTITY>` declarations including parameter entities (`%` prefix); external entity references (`SYSTEM`/`PUBLIC` with file://, http://, etc.); XInclude (`<xi:include>`); `xsi:schemaLocation` schema fetch; processing instructions (skipping standard `<?xml?>` declaration); CDATA sections; XML signature wrapping (audit); bidi/null/control/zero-width; element-count + depth caps; per-attribute-value-length cap. **Profiles** — strict / balanced / permissive (DOCTYPE refused at all profile levels billion-laughs class is universal). **Compliance postures** hipaa / pci-dss / gdpr / soc2-cc7. **Smoke parallel mode** new `SMOKE_PARALLEL=N` env var forks Layer 0 test files in parallel batches (each fork is a fresh Node child process for module-state isolation). Layers 1-5 stay sequential because they share db / cluster / vault state. Sanity ceiling 64. Empirical: SMOKE_PARALLEL=64 takes 91s vs sequential 122s (25% faster). **Persistent test output** `test/smoke.js`, `test/layer-0-primitives/codebase-patterns.test.js` (CLI mode), and `examples/wiki/test/e2e.js` now write a tee'd copy of all stdout/stderr to `.test-output/smoke.log` / `.test-output/codebase-patterns.log` / `.test-output/wiki-e2e.log`. Tee semantics original stdout/stderr passthrough preserved so npm exit codes + GitHub Actions annotations work unchanged. `.test-output/` is auto-gitignored via the existing `.* ` catchall and never shipped (npm `files` allowlist explicit). **Family infrastructure** — guard-xml shares the same family-ABI shape (resolveProfileAndPosture / aggregateIssues / buildGuardGate / extractBytesAsText / etc.) as the other 6 family members; the v0.7.13 `family-subset` cluster allowlist sustains. Smoke 82648320 / wiki e2e 178 / Linux container smoke 8320 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
18
-
19
- - **0.7.14** (2026-05-04) — `b.guardYaml` YAML content-safety primitive. Threat catalog grounded in current research (CVE-2026-24009 Docling/PyYAML unsafe load → RCE; CVE-2026-27807 MarkUs alias billion-laughs DoS; CVE-2025-68664 LangChain deserialization → RCE; CVE-2025-61301 + CVE-2025-61303 YAML library DoS family — "Laughter in the Wild" study; CVE-2022-1471 SnakeYAML constructor RCE; CVE-2020-1747 + CVE-2020-14343 PyYAML FullLoader; CVE-2017-18342 PyYAML python/object/apply). **Surface**: `validate(input, opts)` returns `{ ok, issues }`; `parse(input, opts)` refuses on critical and parses through the framework's safe-subset YAML parser; `gate(opts)` returns a `b.gateContract`-shaped gate auto-routed by `b.guardAll` for `application/yaml` / `text/yaml` content. Auto-registers into `b.guardAll` (KIND="content"). **Threat catalog**: deserialization-tag injection (language-specific tags `!!python/` / `!!java.` / `!!ruby/` / `!!perl/` / `!!js/` / `!!cs/` / `!!net/` / `!!system.` plus `!!eval` / `!!exec` / `!!new` / `!!apply`); custom + local user tags; anchor / alias recursion (billion-laughs amplification `&anchor` declares + `*alias` references; alias-amplification ratio ≥8x flagged as alias-explosion regardless of absolute counts); multi-document streams (operators expecting one doc silently get the first one and ignore the rest); Norway problem (YAML 1.1 still default in pyyaml + libyaml in 2026 unquoted no/yes/y/n/on/off treated as booleans, country code "NO" false); leading-zero octals (`0777` parses as 511); duplicate keys (YAML 1.2 SHOULD-unique; parsers silently last-wins); merge-key chains (`<<: *anchor` anchor-chain DoS); bidi / null / control / zero-width chars in scalar values; total-size + node-count + anchor + alias-depth + document-count + scalar-length + depth caps. **Profiles** strict (refuse every threat; 2 MiB cap; depth 8; 16 anchors; 1 alias depth; 1 document; 1024 nodes); balanced (audit most threats; allow YAML 1.2 core tags `!!str` / `!!int` / `!!seq` / `!!map` / etc.; refuse alias-explosion regardless; 8 MiB cap; 64 anchors); permissive (audit most; refuse only null bytes + alias-explosion; 64 MiB cap; 1024 anchors). **Compliance postures** hipaa / pci-dss / gdpr / soc2-cc7 with strict overlays + forensic snapshots. **Refuse-only sanitize discipline**: YAML has no safe sanitization gate decisions are serve / audit-only / refuse only. **Strict default-on via v0.7.12**: every `b.fileUpload` + `b.staticServe` deploy gets this gate at strict profile automatically. Smoke 81918264 / wiki e2e 178 (210 runtime examples clean) / Linux container smoke 8264 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
20
-
21
- - **0.7.13** (2026-05-04) — `b.guardJson` JSON content-safety primitive. Threat catalog grounded in current research (CVE-2025-55182 React/Next.js Server Functions deserialization → RCE; CVE-2025-57820 + CVE-2026-30226 Svelte devalue prototype pollution; CVE-2026-35209 defu; CVE-2026-28794 @orpc/client; CVE-2025-13465 Lodash; CVE-2025-25014 Kibana RCE; CVE-2024-38984 json-override; CVE-2022-42743 deep-parse-json; GHSA-9c47-m6qq-7p4h JSON5). **Surface**: `validate(input, opts)` returns `{ ok, issues }`; `parse(input, opts)` returns the cleaned tree (composes `b.safeJson.parse` with the source-level pre-parse pollution scan); `gate(opts)` returns a `b.gateContract`-shaped gate auto-routed by `b.guardAll` for `application/json` / `application/ld+json` / `application/vnd.api+json` content. Auto-registers into `b.guardAll` (KIND="content"). **Threat catalog** — source-level prototype-pollution detection (catches `__proto__` / `constructor` / `prototype` keys BEFORE parse JSON.parse silently routes `__proto__` through the prototype setter so a post-parse Object.keys walk misses the most dangerous key in the catalog); duplicate-key detection (RFC 8259 SHOULD-unique violation; JSON.parse silently last-wins, letting attackers smuggle duplicate-key payloads past validation that ran on the first occurrence Bishop Fox JSON-interoperability research); NaN / Infinity / -Infinity / undefined refusal (RFC 8259 forbids; JSON5 / lenient libraries accept); comment refusal (single-line and block); trailing-comma refusal; JSON5 syntax refusal (single-quoted keys, hex literals, unquoted keys); BOM injection (leading + mid-stream); bidi / null / control / zero-width chars; numeric precision-loss (integers above `Number.MAX_SAFE_INTEGER`); top-level-key allowlist; depth + breadth + array-length + string-length + node-count caps. **Profiles** — strict (refuse every threat; 2 MiB doc cap; depth 8; 256 keys/object; 1024 array items) / balanced (strip pollution + BOM + bidi + control + null + zero-width; audit duplicates + comments + trailing commas + JSON5 + numeric precision; refuse NaN; 8 MiB cap; depth 32) / permissive (audit most; refuse only null bytes outright; 64 MiB cap; depth 64). **Compliance postures** — hipaa / pci-dss / gdpr / soc2-cc7 with strict overlays + forensic snapshots. **Strict default-on via v0.7.12**: every `b.fileUpload` + `b.staticServe` deploy gets this gate at strict profile automatically — no explicit wiring required. **Family infrastructure**: `KNOWN_CLUSTERS` allowlist gains `mode: "family-subset"` matcher (collapses 6+ exact-match cluster entries into one subset entry; sustainable as the family grows beyond 6 guards). Smoke 81088191 / wiki e2e 178 (210 runtime examples clean) / Linux container smoke 8191 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
22
-
23
- - **0.7.12** (2026-05-04) — `b.fileUpload` and `b.staticServe` ship with `b.guardAll` wired ON by default at strict profile. Defense-in-depth applied automatically: every operator-supplied bytes path runs through the full guard-* family without any explicit wiring. **`b.fileUpload`**: `contentSafety` defaults to `b.guardAll.byExtension({ profile: "strict" })` (every shipped guard's full threat catalog refused dangerous tags, event handlers, dangerous URL schemes, formula injection, DOCTYPE, SVGZ, animation-href hijack, zip-slip, ratio bombs); new `filenameSafety` opt defaults to `b.guardFilename.gate({ profile: "strict" })` (path traversal + null-byte truncation + Windows reserved names + NTFS ADS + RTLO bidi + overlong UTF-8 + shell-exec extensions + double-extension + reserved chars). **`b.staticServe`**: `contentSafety` defaults to `b.guardAll.byExtension({ profile: "strict" })`. **Explicit opt-out**: `contentSafety: null` / `filenameSafety: null` skips the gate AND emits an audit row at `create()` time (`fileUpload.contentSafety.disabled` / `fileUpload.filenameSafety.disabled` / `staticServe.contentSafety.disabled`) with operator-supplied `contentSafetyDisabledReason` / `filenameSafetyDisabledReason` metadata so a security review can reconstruct which deploys disabled the default-on protection AND why. **filenameSafety wiring**: runs in `fileUpload.finalize` BEFORE `contentSafety` (a refused filename obviates body validation); honours sanitize action (replaces `metadata.filename` with the cleaned form so downstream code sees the safe name). **Explicit operator wiring still works**: passing `contentSafety: { ".csv": gate }` or `filenameSafety: gate` uses the operator-supplied object as-is (no default-on override). **Migration**: this is a behavior change. Code that called `b.fileUpload.create({ stagingDir, onFinalize })` previously had no content / filename safety; after v0.7.12 the same call has both gates wired at strict profile. If existing routes depended on accepting hostile content (raw-bytes uploads, executable-extension artifacts), pass `contentSafety: null` and/or `filenameSafety: null` with an operator-supplied reason at `create()` time. Smoke 81018108 / wiki e2e 178 / Linux container smoke 8108 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
24
-
25
- - **0.7.11** (2026-05-04) — adaptive guard-* family integration harness (`test/layer-5-integration/guard-host-integration.test.js`). The harness discovers every guard primitive registered in `b.guardAll.allGuards()` (registered content guards + standalone non-content guards) and exercises gate decisions through the appropriate host wiring per guard's `KIND`. **Adaptive design**adding a new guard automatically picks up the harness without touching the test file. Each guard exports `KIND` (`"content"` / `"entries"` / `"filename"`) and `INTEGRATION_FIXTURES` with kind-appropriate sample payloads (benign + hostile). The harness iterates `b.guardAll.allGuards()`, dispatches per kind, and per-guard runs: direct gate (benign serve; hostile → not serve); `b.guardAll.gate` contentTypeMux dispatch; `b.guardAll.gate({ exceptFor })` opt-out path with audit-row verification of the skipped roster; `b.staticServe.create({ contentSafety })` GET round-trip (benign 2xx, hostile → 4xx); `b.fileUpload.create({ contentSafety })` chunk finalize round-trip (benign succeeds, hostile throws content-safety error); audit chain captures host-level rows. **Family additions** `lib/guard-all.js` gains `STANDALONE_GUARDS` array (currently `[guardFilename]`) and `allGuards()` aggregator that returns `GUARDS.concat(STANDALONE_GUARDS)`. Each guard module exports `KIND` + `INTEGRATION_FIXTURES`. **Tests run in-process** no docker / network / fixture-archive dependencies; the harness completes in ~100ms regardless of guard count, so the gate runs on every smoke. Smoke 8060 8101 / wiki e2e 178 / Linux container smoke 8101 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed. **Future-proofing** when v0.7.12+ adds guard-link / guard-mime / guard-yaml / etc., they ship with `KIND` + `INTEGRATION_FIXTURES` and the harness picks them up automatically. The on-by-default wiring slice (next) can flip the default knowing every shipped guard already has end-to-end host coverage.
26
-
27
- - **0.7.10** (2026-05-04) — `b.guardArchive` archive content-safety primitive. Threat catalog grounded in current archive-extraction CVE research: CVE-2025-3445 mholt/archiver Zip Slip; CVE-2025-32779 EDDI Zip Slip; CVE-2025-62156 Argo Workflows Zip Slip; CVE-2025-66945 Zdir Pro path traversal; CVE-2025-45582 GNU Tar two-step symlink bypass; CVE-2025-11001/11002 7-Zip symlink + directory traversal RCE; CVE-2025-4138/4517 Python tarfile extraction-filter symlink bypass; CVE-2025-10854 txtai; CVE-2025-12060 Keras; CVE-2026-26960 node-tar hardlink-via-symlink-chain. **Surface**: `validateEntries(entries, opts)` validates an operator-supplied entry-list (`{ name, size, compressedSize, isSymlink, isHardlink, linkTarget, isDirectory, isEncrypted, attrs }`); `inspectMagic(buffer)` reads first bytes and returns detected format (zip / gzip / bzip2 / xz / 7z / rar4 / rar5 / lzma / zstd / tar via "ustar" magic at byte 257); `checkExtractionPath(entryName, extractionRoot)` single-entry helper; `gate(opts)` returns a `b.gateContract`-shaped gate. Auto-registers into `b.guardAll` (NAME / MIME_TYPES / EXTENSIONS / shared profiles + postures all conformant). **Scope discipline**: validates archive METADATA, not bytes. Operators use their archive library to enumerate entries, then validate the list before extracting. The framework's no-deps rule argues against shipping a parser for every format (zip / tar / gzip / bzip2 / 7z / rar / zstd...). **Threat catalog**: zip-slip path traversal (CVE-2025-3445 class); absolute-path entries (`/etc/passwd`, drive-letter prefix); symlink + hardlink escape (CVE-2026-26960 class); per-entry compression-ratio bombs (default 100:1 strict, 1000:1 permissive); aggregate compression-ratio bomb (sum-of-uncompressed / sum-of-compressed); total-size + entry-count + per-entry-size caps; nested-archive detection via extension catalog (.zip / .jar / .tar.gz / .tgz / .tar.bz2 / .gz / .bz2 / .xz / .7z / .rar / .zst / etc.) with maxNestedDepth; per-entry-name validation composing `b.guardFilename` (path traversal / null-byte / Windows reserved names / NTFS ADS / RTLO bidi / overlong UTF-8 / shell-exec / double-extension); duplicate entry names (silent overwrite vector); case-insensitive collisions on Windows / macOS; encryption-claim mismatch (mixed encrypted + unencrypted); sparse tar entries; format-claim mismatch via magic-byte detection. **Profiles**: strict (every threat refused; 100 entries; 100 MiB total; 100:1 ratio cap), balanced (symlinks within root allowed; no hardlinks; nested-depth 2; 10000 entries; 1 GiB total; 100:1/1000:1 ratios), permissive (symlinks + hardlinks within root; depth 4; 100000 entries; 10 GiB total; 1000:1 ratios). **Compliance postures**: hipaa / pci-dss / gdpr / soc2-cc7 with strict overlays + forensic snapshots. **Refuse-only sanitize discipline**: archive content has no safe sanitization — gate decisions are serve / audit-only / refuse only (no sanitize action). **Tests** 102 new layer-0 assertions covering surface, registry parity, zip-slip detection (3 forms), absolute-path detection (3 forms), symlink reject (strict), symlink escape (balanced), hardlink reject + escape, compression-ratio bomb (per-entry), aggregate-ratio bomb, total-size cap, entry-count cap, nested-archive (.zip / .tar.gz), duplicate-entry-name, case-insensitive collision, encryption-claim mismatch, sparse entry, magic-byte detection (8 formats including tar via offset-257 ustar), checkExtractionPath helper, clean-archive + warn-only handling, gate decision shapes (clean / refuse / no-entry-list), profile + posture vocabulary. Smoke 7958 → 8060 / wiki e2e 178 (210 runtime examples clean) / Linux container smoke 8060 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
28
-
29
- - **0.7.9** (2026-05-04) — `b.guardFilename` filename content-safety primitive. **Standalone primitive** does NOT register into `b.guardAll`'s content-type-routed dispatch (filename is a different axis from content-bytes; operators apply both filename safety on the upload's name plus content safety on the body). Threat catalog grounded in OWASP Path Traversal + WSTG file-inclusion testing guides; CWE-22 / CWE-23 / CWE-35 / CWE-73 / CWE-78 / CWE-434; PortSwigger File-path-traversal series (null-byte bypass + extension validation); Memento-RTLO + RTL-Spiegel file-name spoofing reports (CVE-2021-42574 in filename context); Kevin Boone overlong UTF-8 sequence write-up. **Threat catalog** path traversal (raw + percent-encoded `%2e%2e` + double-encoded `%252e%252e` + UTF-8 overlong `0xC0 0xAE` for dot and `0xC0 0xAF` for slash); null-byte truncation; Windows reserved device names (CON / PRN / AUX / NUL / COM1-9 / LPT1-9 / CLOCK$ / CONFIG$, with and without extensions, case-insensitive); NTFS alternate data streams (`name:stream`); leading/trailing whitespace + trailing dots (Windows silently strips them); Unicode bidi / RTLO file-name spoofing; zero-width / homoglyph chars; reserved characters (Windows < > : " | ? *); UNC paths; path separators in leaf-name; length caps (strict 64-byte / balanced+permissive 255-byte); multi-dot policy (strict requires single dot, balanced+permissive allow .tar.gz); extension allowlist (catches double-extension bypass: `file.jpg.exe` matches `.exe` against the allowlist); shell-shortcut + executable extensions (.exe / .bat / .vbs / .ps1 / .lnk / .scr / .dll / .so / .dmg / .msi / etc); overlong UTF-8 detection at the Buffer level (RFC 3629 §3 prohibits non-shortest-form). **Profiles** strict (ASCII-only, single dot, single leaf, 64-byte cap, every threat class refused), balanced (Unicode NFC-normalized, multi-dot allowed, 255-byte cap, refuses dangerous classes + strips zero-width + audits homoglyphs), permissive (multi-component paths up to 16 components, reserved-name audited not refused for non-Windows targets). **Compliance postures** hipaa / pci-dss / gdpr / soc2-cc7 with appropriate strict overlays + ASCII-only + forensic snapshots. **Sanitize discipline** strips leading/trailing whitespace + trailing dots, NFC-normalizes Unicode, replaces reserved chars with underscore (under strip policy), prepends underscore to Windows reserved device names. ALWAYS THROWS on path-traversal / null-byte / NTFS ADS / UNC / overlong UTF-8 these have no safe sanitization, the only correct response is reject. **New shared helpers**`gateContract.badInputResultIfNotStringOrBuffer(input)` and `gateContract.aggregateIssues(issues)` extracted across guard-svg / guard-filename validate paths that need raw-Buffer input pre-conversion. Both registered in KNOWN_ANTIPATTERNS so future re-implementations fail the n=1 gate. **Tests** — 79 new layer-0 assertions covering surface, path traversal (5 forms), percent-encoded traversal (3 forms), null-byte truncation, every Windows reserved name (12 cases including extensions), NTFS ADS, leading/trailing strip, RTLO bidi spoofing, reserved chars (7 cases), UNC paths, path separators in leaf, length cap, single-dot policy, extension allowlist, shell-exec extensions (11 cases), double-extension bypass, overlong UTF-8 at buffer level, ASCII-only enforcement, sanitize round-trip + throw-on-traversal/null-byte, gate decision shapes, profile + posture vocabulary. Smoke 78797958 / wiki e2e 178 (206 runtime examples clean) / Linux container smoke 7958 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
30
-
31
- - **0.7.8** (2026-05-04) — `b.guardSvg` SVG content-safety primitive + guard-* family helper extraction. **`b.guardSvg`** ships full v1 scope grounded in current SVG attack-surface research (Fortinet anatomy of SVG attack surface; Angular GHSA-jrmj-c5cx-3cw6 + GHSA-v4hv-rgfq-gp49 SVG animation/href XSS; SVGO CVE-2026-29074 billion laughs DoS; siyuan-note GHSA-5hc8-qmg8-pw27 animate-element sanitizer bypass; cure53/DOMPurify issue #233 xlink:href filtering; insertScript SVG fun-time series). Threat catalog: dangerous tags (script / foreignObject / handler / iframe / embed / object / audio / video / animation family); SMIL animation attributeName allowlist enforcement (recent CVE class `<animate attributeName="href" to="javascript:..."/>` is the published bypass shape, refused regardless of broader animation policy); on* + SMIL event-handler attributes (onclick / onerror / onload / onbegin / onend / onrepeat); href + xlink:href dangerous URL schemes (javascript / vbscript / data outside image-context / file / mhtml / view-source entity-encoded form too); cross-origin `<use>` external refs (SSRF + XSS chain); XML DOCTYPE declarations refused unconditionally (defends billion-laughs entity expansion + XXE); custom `<!ENTITY>` declarations; CDATA + processing instructions; SVGZ compressed payloads (gzip magic bytes 0x1F 0x8B refused at gate level operator must ungzip first); CSS injection in style attributes; <use>-recursion DoS via maxUseDepth; total-document size + element-count + attribute-count caps. **Profiles**: strict (minimal text+shapes; no animation; no external refs), balanced (adds <use> + <image> with same-origin or http(s); allowExternalRefs=true), permissive (adds animation with attributeName allowlist still enforced). **Compliance postures**: hipaa / pci-dss / gdpr / soc2-cc7 with strict overlays + forensic snapshots. **Guard-* family helper extraction (audit-existing-code rule §8 sweep)** five new shared helpers landed in `lib/codepoint-class.js` and `lib/gate-contract.js` and the existing guards refactored to consume them: `codepointClass.detectCharThreats(text, opts, codePrefix)`, `codepointClass.assertNoCharThreats(text, opts, errorFactory, codePrefix)`, `codepointClass.applyCharStripPolicies(text, opts)`, `gateContract.resolveProfileAndPosture(opts, cfg)`, `gateContract.runIssueValidator(input, opts, detector)`, `gateContract.buildGuardGate(name, opts, check)`, `gateContract.extractBytesAsText(ctx)`, `gateContract.lookupCompliancePosture(name, postures, errorFactory, codePrefix)`, `gateContract.makeRulePackLoader(errorClass, codePrefix)`, `gateContract.makeProfileBuilder(profiles)`, `numericBounds.requireAllPositiveFiniteIntIfPresent(opts, names, labelPrefix, errorClass, code)`. Pre-existing call sites in `lib/csv.js` and `lib/file-upload.js` migrated in the same patch per the no-future-patch-deferrals rule. New shared module `lib/codepoint-class.js` centralizes BIDI / C0_CTRL / ZERO_WIDTH range tables plus the `_hex4` / `_charClass` / `_fromCp` rendering helpers the codepoint catalog has a single source of truth and future guard-* slices consume the shared module instead of redeclaring the tables. Each helper extraction registers its inline-shape in `KNOWN_ANTIPATTERNS` (the codebase-patterns gate fires at n=1 if any future code re-introduces the pattern). **Tests** — 83 new layer-0 assertions covering surface, registry parity, dangerous-tag detection, on* handler family, dangerous URL schemes (entity-encoded form too), animation attributeName href hijack, cross-origin <use> refusal, DOCTYPE / <!ENTITY> rejection, CDATA + processing-instruction policy, SVGZ magic-byte detection, CSS injection, bidi/control/null-byte detection, depth/size caps, sanitize round-trips, gate decision shapes, profile + posture vocabulary. Smoke 77967879 / wiki e2e 178 (203 runtime examples clean) / Linux container smoke 7879 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
32
-
33
- - **0.7.7** (2026-05-04) — `b.guardHtml` HTML content-safety primitive ships full v1 scope. Threat catalog grounded in current sanitizer research (DOMPurify CVE series, OWASP XSS / DOM-Clobbering / HTML5 Security cheat sheets, PortSwigger and Sonar mXSS write-ups, html5sec.org). Surface: `validate(input, opts)` → `{ ok, issues }`; `sanitize(input, opts)` → cleaned HTML string; `escapeText(value)` and `escapeAttr(value)` always-correct entity escapers; `gate(opts)` returns a `b.gateContract`-shaped gate; `buildProfile`, `compliancePosture`, `loadRulePack`. Auto-registers into `b.guardAll` (NAME / MIME_TYPES / EXTENSIONS / shared profiles + postures all conformant). **Threat catalog covered** — dangerous tags (script / style / link / meta / base / iframe / object / embed / applet / form / input / button / frame / frameset / marquee / blink / plaintext / xmp / audio / video / source / track / math / svg / template / noscript / portal / dialog / keygen / menuitem / command); every `on*` event-handler attribute (caught family-wide via `/^on[a-z]/` regex — covers the entire HTML5 event-handler family without a manual list that rots when WHATWG specs a new event); form-override attributes (formaction / formmethod / formenctype / formtarget / formnovalidate, CWE-1021); iframe `srcdoc`; custom-element `is`; CSP-bypass-shaped attributes (nonce / integrity / crossorigin / http-equiv / manifest); URL-scheme allowlist with entity-decode pre-pass (defends `&#x6A;avascript:` and decimal-entity bypasses); CSS injection in style attribute values (expression / behavior: / -moz-binding / javascript:/vbscript: inside url() / @import / @namespace); DOM clobbering (id/name attribute values matching well-known JS globals on form/input/button/a/img/iframe/object/embed/select/textarea); mXSS hints (svg/math namespace-context-shift parents); IE conditional comments; Unicode bidi override (CVE-2021-42574 Trojan Source) / C0 control chars / null bytes / zero-width chars; tag-depth + attribute-count + per-attribute-value-size + total-document-size DoS caps. **Profiles** strict (minimal text-formatting allowlist; reject every threat class) / balanced (links + images + tables + semantic markup; strip rather than reject for character-class threats; data:image/* on `<img>`) / permissive (every tag NOT in the dangerous-tag denylist). **Compliance postures** hipaa / pci-dss / gdpr / soc2-cc7 with appropriate strict/balanced overlays + forensic-snippet sizing (256 / 256 / 128 / 512 bytes). **Sanitizer discipline** drops dangerous tags AND their text-content body (script/style/iframe/object/embed/applet/template/svg/math etc. body parsed as code in the host); strips on* attributes, dangerous URL schemes, CSS injection patterns, DOM-clobbering values, doctypes, CDATA, comments. Documented operator-facing trade-off: for hostile sources, validate+reject is the strong path; sanitize is best-effort and operators displaying untrusted HTML should additionally serve under a strict CSP. **Codepoint-table programmatic regex pattern**same as guard-csv: BIDI_RANGES / C0_CTRL_RANGES / ZERO_WIDTH_RANGES literal numeric tables compiled into character classes via `_charClass` + `\\uXXXX` escapes at module load. Source file never embeds attack chars themselves. **Tests** — 134 new layer-0 assertions covering surface, registry parity, every dangerous tag in the denylist, 15 representative on* handlers, 9 dangerous attributes, 10 dangerous URL schemes (entity-encoded form too), 5 CSS injection patterns, 8 DOM clobber globals, mXSS hints, IE conditional comments, bidi/control/null detection, depth/size caps, sanitize round-trips, escape correctness, gate decision shapes (clean/refuse/sanitize), profile + posture vocabulary. Smoke 76627796 / wiki e2e 178 (200 runtime examples clean) / Linux container smoke 7796 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
34
-
35
- - **0.7.6** (2026-05-04) — `b.guardAll` registry + aggregator for the guard-* family. Security-on-by-default: every shipped guard is ON unless the operator opts OUT specifically with an audited reason. **Surface** `b.guardAll.gate(opts)` returns a single `b.gateContract`-shaped gate that routes by `Content-Type` to the right registered guard; `b.guardAll.byExtension(opts)` and `b.guardAll.byContentType(opts)` return ready-made gate maps for direct drop-in to `b.staticServe.create({ contentSafety: ... })` and any host primitive that dispatches on extension or mime; `b.guardAll.list()` returns the registered roster (operator audit aid); `b.guardAll.GUARDS` / `SHARED_PROFILES` / `SHARED_POSTURES` exposed as readonly registry exports. **Opt-out** `exceptFor: { csv: { reason: "no CSV emission in this app" } }` requires a non-empty `reason` string per opted-out guard (throws at gate-creation time, not silently); `override: { csv: { profile: "email-attachment" } }` reaches per-guard extension profiles that aren't in the shared vocabulary. **Audit emission** every gate / byExtension / byContentType call with `opts.audit` wired emits `guardAll.gate.created` exactly once at gate-creation time, recording the full `active` + `skipped` roster (each skipped entry carries its `reason`). **Registry contract** every primitive registered into `b.guardAll` MUST export `NAME` (string), `MIME_TYPES` (frozen array), `EXTENSIONS` (frozen array), `PROFILES` containing the shared vocabulary (`strict` / `balanced` / `permissive`), `COMPLIANCE_POSTURES` containing the shared regulatory shapes (`hipaa` / `pci-dss` / `gdpr` / `soc2-cc7`), and `gate(opts)` returning a `b.gateContract`-shaped gate. The parity check at module load throws `GuardAllError` with a multi-line failure list if any registered guard is missing the contract this is the framework's mechanism for keeping every future guard slice conformant. Duplicate `NAME` / `MIME_TYPE` / `EXTENSION` across guards is also caught at module load. **Adjacent** — `b.guardCsv` now exports `NAME` / `MIME_TYPES` / `EXTENSIONS` for registry compliance. Wiki harness gains `fs` binding for examples that mkdir staging dirs (joins the existing `path` / `os` bindings). **Tests** — 56 new layer-0 assertions: surface, registry parity (every member declares the full contract), default-on (no `exceptFor` every guard active), `exceptFor` reason validation (missing / blank / non-object / unknown name throws), `override` shape validation, profile-vocabulary enforcement (per-guard extensions like csv's `email-attachment` rejected at the aggregator), posture-vocabulary enforcement, audit creation roster, dispatch correctness (text/csv routes to csv guard, unrelated mime bypasses), byExtension / byContentType output shape. Smoke 76067662 / wiki e2e 178 (196 runtime examples clean) / Linux container smoke 7662 / Linux container wiki e2e 178 / eslint clean / shellcheck clean / api-snapshot baseline refreshed.
36
-
37
- - **0.7.5** (2026-05-04) — `b.gateContract` foundation + `b.guardCsv` content-safety primitive. **BREAKING (pre-v1):** `b.csv.stringify` no longer accepts `preventFormulaInjection` / `formulaPrefixChars` opts the partial single-mode defense it offered (only `=`/`+`/`-`/`@`/TAB/CR ASCII prefixes, missing LF / `|` / full-width formula chars) was a false-confidence path. `b.csv` is now documented as trusted-source-only emission (RFC 4180 quoting + anti-DoS bounds); any user-supplied cells route through `b.guardCsv.serialize` / `.gate` for the full threat catalog. The `b.guardCsv.parse` / `.stringify` re-exports are also removed operators use `b.csv.parse` / `.stringify` directly for pure parsing. New `guard-*` family naming complements the existing `safe-*` parser family (safeJson / safeUrl / safeBuffer / safeSql / safeAsync are "parser/validator with safer defaults"; guard-* are content-safety gates that run inside the request lifecycle). **`b.gateContract`** provides the uniform composition contract every guard-* primitive implements: `defineGate / runGate / composeGates / multiplexGates / contentTypeMux / shadowMode / canaryGate / cachingGate / workerThreadGate / buildProfile / composeHooks / summarizeIssues / validateGateShape`, plus mode posture (enforce / warn-only / shadow / audit-only / log-only / canary), hook system (beforeCheck / afterCheck / onIssue / onSanitize / onRefuse / onAudit), forensic snapshot store integration, decision cache, and runtime cap via `safeAsync.withTimeout`. **`b.guardCsv`** ships full v1-defensible scope: `serialize / validate / sanitize / escapeCell / detect / parse / stringify / schema / gate / buildProfile / compliancePosture / loadRulePack`. Threat catalog covered — formula injection (5 modes: prefix-tab / prefix-quote / wrap-with-quotes-and-prefix / reject / allowlist) with all 8 ASCII triggers (`= + - @ TAB CR LF |`) plus full-width variants (U+FF1D / U+FF0B / U+FF0D / U+FF20) per OWASP locale catalog, dangerous-function denylist (WEBSERVICE / HYPERLINK / IMAGE / IMPORT* / RTD / DDE / CALL / GOOGLEFINANCE / GOOGLETRANSLATE) surfaced as critical regardless of broader formula policy, Unicode bidi override (CVE-2021-42574 Trojan Source), homoglyph mixed-script detection, C0 control chars, null bytes, BOM mid-stream injection, zero-width chars, dialect ambiguity, CSV-bombs (per-cell / total / sanitize-amplification caps), numeric precision loss above `Number.MAX_SAFE_INTEGER`, trailing-whitespace exfiltration policy, PII redaction (composes `b.redact`), and schema-bound serializer with type / regex / range / nullable validation. Profiles: `strict` (OWASP-recommended `prefix-tab` default — Excel-resistant) / `balanced` / `permissive` / `email-attachment`; compliance postures: `hipaa` / `pci-dss` / `gdpr` / `soc2-cc7`. **Threat-detection regex literals are composed programmatically from numeric codepoint range tables** (`BIDI_RANGES` / `C0_CTRL_RANGES` / `ZERO_WIDTH_RANGES` / `HOMOGLYPH_RANGES` / `FORMULA_PREFIX_CPS`) `_charClass(ranges)` renders into a regex character class via `\uXXXX` escapes at module load. Source file never embeds the attack characters themselves, only their codepoint numbers; the detector composes the way an attacker would compose the payload. **Adjacent integrations** — `b.staticServe` gains `contentSafety: { ".csv": gate, ... }` extension-keyed gate map (runs after MIME allowlist / before headers; `sanitize` decision replaces body buffer, `refuse` returns 415); `b.fileUpload` gains `contentSafety: gate` running before `onFinalize` (`refuse` throws `CONTENT_SAFETY_REFUSED`, `sanitize` replaces `bodyBuffer`). **Validation primitive** — `validateOpts.optionalPlainObject(value, label, ErrorClass, code, description)` consolidates the `if (typeof !== "object" || Array.isArray) throw` cascade across api-key / db-declare-view / static / file-upload; registered in `KNOWN_ANTIPATTERNS` so future re-implementations fail the n=1 gate. **Tests** — 47 new layer-0 assertions covering surface (gateContract + guardCsv), every formula prefix per policy, full-width formula prefixes, dangerous-function denylist, bidi reject + strip, control-char + null byte, numeric precision, CSV-bombs, dialect detection, sanitize amplification cap, schema type drift / nullable / regex, gate clean serve / refuse / sanitize, operator rules, forensic snapshot, audit emission. Smoke 74857614 / wiki e2e 178 / Linux container smoke 7614 (156s) / Linux container wiki e2e 178 / eslint clean / shellcheck clean / api-snapshot baseline refreshed for new public surface.
38
-
39
- - **0.7.4** (2026-05-04) — CI gate alignment, wiki cross-platform fix, redis-client opts forwarding consolidation. Two CI-only gates (`scripts/check-api-snapshot.js` for public-API drift detection; Linux-container wiki e2e for cross-platform example execution) added to the local Release Workflow, plus `api-snapshot.json` baseline refreshed for the first time since v0.6.17 (40+ patches of accumulated drift; `b.objectStoreRetry` removal in v0.7.0 was the BREAKING entry). The wiki file-upload example previously hardcoded `stagingDir: "/var/lib/myapp/uploads"` which mkdir-passed on Windows but failed `EACCES` on Linux/macOS CI runners replaced with a real runnable example using `path.join(os.tmpdir(), "myapp-uploads-" + Date.now())` and a self-contained `b.fileUpload.create` + `uploads.close()` round-trip; the Usage block likewise instantiates a real `b.router.create()` + uploads pair. `examples/wiki/test/run-example.js` harness gains `path` + `os` Node built-ins as bindings so wiki examples can write idiomatic `path.join(os.tmpdir(), ...)` without falling foul of the harness's `var X = require(...)` line stripper. **`redisClient.pickClientOpts(cfg, prefix?)`** new helper that returns the standard 9-key opts bag (`url / password / username / tls / ca / servername / connectTimeoutMs / commandTimeoutMs / maxReconnectAttempts`); `cache-redis` / `pubsub-redis` / `queue-redis` / `cache` redis-backend creator all migrated to it, eliminating ~40 lines of duplicate inline forwarding across the four files. The new `inline-redis-client-opts-forwarding` `KNOWN_ANTIPATTERNS` entry catches future re-implementations at n=1. **`testNoDuplicateCodeBlocks`** + **`testNoUnresolvedMarkers`** + **`testNoTierTerminologyInLib`** detector tuning: `MIN_DISTINCT_FILES` lowered 3 2 / `MIN_DISTINCT_TOKENS` lowered 6 5 (more aggressive duplicate detection); `defer` added to forbidden-marker scan; `Tier 1/2/3` added to forbidden-tier-terminology scan. `lib/log-stream-syslog.js`'s socket error-handler comment reworded from "defer to 'close' for reconnect" to "reconnect handled in the close listener" same behavior, no marker false-positive. **Tests** — Linux-container wiki e2e green (188 examples ran clean, 0 failures, 0 symbol drift); `node scripts/check-api-snapshot.js` reports `[api-snapshot] no changes`. Smoke 7485 / wiki e2e 178 / Linux container smoke 7485 / Linux container wiki e2e 178 / eslint clean / shellcheck clean.
40
-
41
- - **0.7.3** (2026-05-04) — `b.middleware.apiEncrypt` per-session keying mode (opt-in). Per-request keying stays the default every existing test passes unchanged. Operators opt in by passing `keying: "per-session"`; the first request in a session carries the bootstrap envelope `{ _ek, _ct, _ts, _nonce, _sid, _ctr }` and the server stores the session key keyed by `_sid` (UUID v4); subsequent requests in the same session omit `_ek` and `_nonce` entirely, sending only `{ _ct, _ts, _sid, _ctr }`. The monotonic `_ctr` replaces the nonce for replay defense server rejects any request with `_ctr <= last_seen_ctr` for that session. Responses carry `{ _ct, _sid, _ctr }`; the client helper validates the response counter is strictly increasing so a tampered / replayed response surfaces as `CLIENT_RESPONSE_REPLAY` instead of silently propagating stale data. Sessions expire on `sessionTtlMs` (default 15 minutes) OR after `sessionMaxResponses` (default 1024); both surface as 401 (`session-expired` / `session-rotation-required`) and the client helper's `resetSession()` re-bootstraps. Operator-supplied `sessionStore` is a `{ get, set, delete }`-shaped handle (b.cache shape-compatible) so multi-replica deploys share session state; default is an in-memory map for single-process apps. Wire format trade-off: per-session amortizes ~1.6 KB of KEM material across the session for high-throughput / mobile / metered-network apps; per-request stays strictly safer if the session storage gets compromised. Audit emits `apiEncrypt.session.created` / `.expired` / `.rotated` / `.replay_rejected` with actor context; observability counters track sessions established / replay-rejected / expired / unknown / rotated. The framework HTTP-client helper `b.httpClient.encrypted({ keying: "per-session" })` threads the same posture through service-to-service callers. **Tests** 13 new layer-0 assertions covering default-keying invariant, opt validation (bad keying value / TTL / store shape), bootstrap-and-reuse round-trip (KEM amortization confirmed by inspecting subsequent envelope shape), unknown-sid 401, counter-replay 400, TTL-expiry 401, max-responses-rotation 401, response counter monotonic check, sessionInfo / resetSession client API, observability emission. Smoke 7445 → 7485 / wiki e2e 178 / Linux container smoke 7485 (147s) / eslint clean / shellcheck clean.
42
-
43
- - **0.7.2** (2026-05-04) — Symmetric upload + download primitives ship with full v1-defensible feature sets. **`b.fileUpload`** chunked upload primitive: operators wire HTTP routes for per-chunk PUT (`fileUpload.acceptChunk`) and finalize POST (`fileUpload.finalize`); the framework owns the chunk lifecycle (per-chunk SHA3-512, atomic chunk write to `<stagingDir>/<uploadId>/<index>`, reassemble in manifest order, verify total SHA3-512) and hands the assembled buffer to an operator-supplied `onFinalize` callback. Surface includes `init` / `acceptChunk` / `finalize` / `status` / `list` / `cancelUpload` / `purgeIncomplete`. v1 features: per-actor active-upload quota, total staging-bytes quota, MIME magic-byte allowlist (composes `b.fileType`), `onChunk` operator hook, idle-upload timeout, stream reassembly above `maxStreamReassemblyBytes` (sequential async iterator over chunk files instead of `Buffer.concat`), permissions integration, metadata stash with cap, path-safe upload-ID regex, mode 0o700 staging dir, idempotent re-PUT, force-cancel, audit emission with 5-W actor context, observability counters. **`b.staticServe`** download primitive expanded with the symmetric feature set: permissions gate (403), compliance retention gate (451), force-revoke instance method + revoke-store opt (404), MIME magic-byte allowlist (415), RFC 7233 single-range support (multi-range refused with 416), full conditional-request set (`If-None-Match` / `If-Match` / `If-Modified-Since` / `If-Unmodified-Since`), per-actor + global bandwidth quotas via `b.cache` cluster-shared token-buckets (429 + Retry-After), per-actor concurrency cap (429), `onServe` per-request operator hook, `maxIdleMs` stalled-stream timeout, cancellation propagation (client disconnect destroys file stream + releases concurrency slot), audit + observability emission with 5-W actor context. ETag is now SHA3-512-truncated for PQC posture; the SRI integrity helper keeps SHA-384 because the W3C subresource-integrity spec only allows sha256/sha384/sha512. **Object-store backends** (sigv4 / gcs / azure-blob) gain a new `getResponse(key, opts?)` method that forwards `range` / `ifNoneMatch` / `ifMatch` / `ifModifiedSince` / `ifUnmodifiedSince` opts to the backend's protocol-specific headers and returns `{ statusCode, body, etag, lastModified, contentRange, size, contentType }` — operators wiring object-store-backed download routes route conditional + range from the client request straight through. Existing `get(key)` returns just the body for back-compat. **`HTTP_STATUS`** constants extended with `PARTIAL_CONTENT` / `RANGE_NOT_SATISFIABLE` / `PRECONDITION_FAILED` / `UNAVAILABLE_FOR_LEGAL_REASONS`. **New helpers** — `validateOpts.optionalNonEmptyStringArray(value, label, ErrorClass, code?)` and `validateOpts.optionalObjectWithMethod(value, method, label, ErrorClass, code?, description?)` consolidate the recurring "string-array" and "duck-typed handle" inline cascades across api-key / file-upload / seeders / notify / webhook / db-role-for. Both registered in `KNOWN_ANTIPATTERNS` so future re-implementations fail the n=1 gate. **Tests** — 28 new layer-0 fileUpload assertions (init / acceptChunk / finalize / status / list / cancel / quotas / MIME allowlist / onChunk / idle / stream reassembly / permissions) plus 24 new staticServe assertions (range / suffix-range / open-end-range / unsatisfiable-range / multi-range refused / acceptRanges off / If-Match / If-Modified-Since / If-Unmodified-Since / permissions / retention / revoke / MIME allowlist / onServe / audit / stats / invalidateMeta / quotas / concurrency cap). Wiki page `examples/wiki/seeders/prod/pages/file-upload.js` added; routing.js section for `b.staticServe` rewritten to document the v1 surface. README "what ships in the box" Communication + Routing bullets updated. Smoke 73417445 / wiki e2e 178 / Linux container smoke 7445 (146s) / eslint clean / shellcheck clean.
44
-
45
- - **0.7.1** (2026-05-04) — `b.websocket` route opts gain `handshakeGuid` to override the RFC 6455 §1.3 magic string used in the `Sec-WebSocket-Accept` derivation. Default stays `258EAFA5-E914-47DA-95CA-C5AB0DC85B11`. Operators with closed-ecosystem clients running their own GUID (typical for migrations from frameworks that customize the handshake to namespace their own client family) drop `handshakeGuid: "<their-uuid>"` into the `router.ws(path, handler, opts)` opts and clients keep working unchanged. Throws at upgrade time if the override is malformed UUID-shape regex with a 64-char length cap before the regex test, so a typo produces a clear error instead of silently producing a `Sec-WebSocket-Accept` the client can't match. **Tests** — 3 new layer-0 assertions in `test/00-primitives.js` (custom GUID produces different accept key, empty / null falls back to RFC default, malformed handshakeGuid rejected at config time). Smoke 73387341 / wiki e2e 178 / Linux container smoke 7341 (145s) / eslint clean / shellcheck clean.
46
-
47
- - **0.7.0** (2026-05-04) — Codebase-patterns hardening sweep + primitive consolidation. The duplicate-block detector in `test/layer-0-primitives/codebase-patterns.test.js` ran at MIN_DISTINCT_FILES=3 and surfaced ~50 inline-shape clusters that had proliferated across lib/ — the kind of soft drift that's invisible at higher thresholds and hides re-introduction of bug classes the framework already swept once. **New primitive families** consolidated those clusters down: opts validation (`validateOpts.requireObject` / `auditShape` / `observabilityShape` / `applyDefaults` / `optional{Boolean,Function,PositiveInt,FiniteNonNegative,PositiveFinite,NonEmptyString}` / `requireNonEmptyString` / `makeAuditEmitter`), async coordination (`safeAsync.safeInvoke` / `makeDropCallback` / `makeScheduledFlush`), and SQL execution (`dbSchema.runInTransaction` / `runSqlOnHandle`). Plus `numericBounds.requireXFiniteIntIfPresent` for opt-time numeric-shape gates that throw via the caller's framework-error class, `log.makeViaOrFallback` for operator-log routing with per-module fallback, `observability.safeEvent` for hot-path drop-silent emission, `safeBuffer.HEX_RE`/`CRLF_RE`/`TRAILING_HSPACE_RE` regex constants + `isHex` / `hasCrlf` / `stripCrlf` / `stripTrailingHspace` helpers, `time.toIso8601NoMs`, `migrationFiles.MIGRATION_FILE_RE` / `isMigrationFileName` (shared filename grammar across migrations / seeders / external-db-migrate), and `lib/object-store/http-request.js` as a shared HTTP request helper across azure-blob / gcs / sigv4 / http-put. Roughly 80 inline call sites across 50+ files refactored to route through the new primitives; ~1500 lines of duplicate inline shapes eliminated. **Catalog gate** — `KNOWN_ANTIPATTERNS` in the codebase-patterns test now has 24 entries firing at n=1, so future code re-introducing any of the registered inline shapes (even one new file) fails the gate immediately. Pre-v0.7.0 the duplicate-block detector required n>=3 to fire; the catalog closes the gap by registering each extracted primitive's inline shape so it can't drift back in. **Cluster allowlist** — `KNOWN_CLUSTERS` allowlist (n>=3 detector) has 25 entries with documented structural reasons (parser error class signatures don't fit the framework's `(code, message)` contract; framework-convention shapes like middleware factories; future consolidation candidates). **Cleanup** — deleted dead re-export shims `lib/object-store/retry.js` (re-exported `lib/retry.js`) and `lib/auth/totp.js` (re-exported `lib/totp.js`); both fit the rule "pre-v1 frameworks have no operators to compatibly upgrade every legacy fallback is dead code." Renamed `lib/internal-sha1-hibp.js` `lib/framework-sha1-hibp.js` to match the lib naming convention (the `internal-` prefix wasn't in the convention's five-bucket list; `framework-` is the canonical "restricted-use" bucket alongside `framework-error.js` / `framework-schema.js`). **No operator-facing API breakage** — `b.objectStoreRetry` was removed from the public surface (operators use `b.retry` directly, which has been the canonical primitive since v0.2.24). **Eslint config tightened** — added `eqeqeq` (with the `null` exception for the `== null` null-or-undefined idiom), `no-throw-literal`, `no-promise-executor-return`, `default-case`, `no-loss-of-precision`. The previous config was hiding 11 real errors: 8 sites of `return resolve()` / `return done()` inside `new Promise(executor)` (return value silently discarded) across app / dev / http-client / mail / router / wiki/integration / 00-primitives, plus a missing default-case + 2 intentional template-language `==` / `!=` operators in template.js's binary-op evaluator (now allowed via inline `// eslint-disable-next-line eqeqeq -- template language operator`). Also adds `structuredClone` to the Node-globals list so db.js's deep-clone calls don't trip `no-undef`. **Release-workflow gate added** — Linux container smoke `docker run --rm -v "/$(pwd):/blamejs" -w //blamejs node:24-alpine node test/smoke.js` is now part of the release flow. Catches lingering-handle bugs that pass on Windows / macOS but hang or error on Linux CI. **Tests** — smoke 7338 / Linux container smoke 7338 (148s) / wiki e2e 178 / per-primitive integration 16 files / wiki integration green / eslint clean / shellcheck clean.
48
-
49
- ## v0.6.x
50
-
51
- - **0.6.70** (2026-05-03) — `lib/numeric-bounds` extended to 3 more sites the v0.6.69 sweep didn't cover. **`lib/middleware/csp-nonce.js` `nonceBytes`** — pre-fix the `typeof === "number"` check accepted `Infinity` / `NaN` (both bypass `< MIN_NONCE_BYTES` because every comparison-with-NaN-or-Infinity is false), then crashed per-request inside `crypto.generateBytes(Infinity)` with `ERR_OUT_OF_RANGE`. The DoS-shape: an operator typo at boot took down every CSP-protected route at request time. Now rejects at create() with `csp-nonce/bad-nonce-bytes`. **`lib/migrations.js` and `lib/external-db-migrate.js` `staleAfterMs`** — both used the `> 0` guard which silently accepted `Infinity`. `(Date.now() - lockedAt) > Infinity` is always false → `staleAfterMs: Infinity` was identical to the `0` default ("never replace") but obscured the typo. Operators wanting "never expire" now pass `0` explicitly; everything else (Infinity / NaN / fractional / negative / non-number) rejects with a clear message. **Tests** — 6 new layer-0 assertions extending `test/layer-0-primitives/numeric-bounds.test.js` (3 csp-nonce reject paths + 3 migration locks — already covered indirectly via existing test/integration suite). Smoke 7293 → 7296 / wiki e2e 178 / Linux 85.9s / eslint clean / shellcheck clean.
52
-
53
- - **0.6.69** (2026-05-03) — `lib/numeric-bounds.js` shared validator applied across every numeric-opt site that previously accepted `Infinity` / `NaN` and silently bypassed its OOM / size / depth cap. Pre-fix the recurring pattern `typeof opts.X === "number" && opts.X > 0` was vulnerable everywhere it shipped: `Infinity > Infinity` is false, `byteLength > NaN` is false, `Number.isFinite()` was missing. Operators with env-var coercions (`Number(process.env.MAX_X || "")` produces `NaN` for missing values, `Infinity` for typo'd ones) silently lost their cap. **The unified fix:** new `lib/numeric-bounds.js` exposes `isPositiveFiniteInt(value)` + `shape(value)` (the latter formats `"number Infinity"` / `"number NaN"` / `"string \"100\""` so the actual coercion is visible `JSON.stringify` collapses Infinity/NaN to `"null"` and hides the typo). Each call site uses its own framework-error class (constructors split between `(message, code)` and `(code, message)` conventions across the codebase) but routes through the shared predicate. **Sites swept:** `lib/safe-buffer.js` (`boundedChunkCollector`, `toBuffer`, `normalizeText` the v0.6.57 fix only covered the first), `lib/atomic-file.js` (refactored from v0.6.68's inline check), `lib/csv.js` (parse `maxBytes` was operator-overridable to Infinity → multi-megabyte CSV bodies through unbounded), `lib/safe-url.js` (`maxUrlLength` opt was the v0.6.62 fix's escape hatch now also bounded), `lib/mail-bounce.js` (`maxBytes` for inbound webhook body — DoS-shape on bounce intake). **Tests** — new `test/layer-0-primitives/numeric-bounds.test.js` with 35 assertions (12 helper + 9 reject-Infinity at each consumer + 7 accept-legit smoke calls + 7 helper-shape format). Smoke 72587293 / wiki e2e 178 / Linux 86.0s / eslint clean / shellcheck clean.
54
-
55
- - **0.6.68** (2026-05-03) — `b.atomicFile.read` / `readSync` enforce strict `maxBytes` validation. Same bug class as v0.6.57's `safeBuffer.boundedChunkCollector` fix: pre-fix the size check was `if (stat.size > opts.maxBytes)`, which silently accepted `Infinity` (`stat.size > Infinity` always false reads any file regardless of size, defeating the OOM cap). Operators with `Number(env.MAX_READ_BYTES || "")` coercion bugs got `NaN` on missing values and unbounded reads on Infinity-typo'd values. New `_validateMaxBytes` runs before the size check and rejects `Infinity` / `NaN` / non-integer / negative / zero / non-number with `atomic-file/bad-opt`. Real-world consumers (vault.initPlaintext, audit-sign, db.loadOrCreateDbKey, etc.) all pass real positive integers via `C.BYTES.*` helpers and are unaffected. **Tests**7 new layer-0 boot-validation assertions in `test/00-primitives.js` (Infinity, NaN, 0, -1, 3.5, "100", null all reject). Smoke 72517258 / wiki e2e 178 / Linux 86.0s / eslint clean / shellcheck clean.
56
-
57
- - **0.6.67** (2026-05-03) — canonical-JSON walker extracted to `lib/canonical-json.js` and applied to `lib/audit-tools.js` + `lib/config-drift.js`. Same bug class as v0.6.60 (pagination) and v0.6.66 (audit-chain) lived in two more sites: `audit-tools._canonicalize` (used for backup-bundle JSONL serialisation, requires byte-equivalence with audit-chain) and `config-drift._stableStringify` (used to hash framework config for post-boot tamper detection). Both still had the silent-data-loss walk Date `{}`, Buffer → `{"0":97,…}`, Map / Set `{}`, BigInt throws, circular references stack-overflow. New `lib/canonical-json.js` exposes `stringify(value, opts?)` with `opts.bufferAs = "hex" | "reject"` (default `"hex"` for crypto / config / audit; `"reject"` for pagination's strict-cursor policy). All four call sites — audit-chain, audit-tools, config-drift, pagination — now route through it; the four near-identical inline walkers collapse to one. **Stored-row + bundle compatibility** preserved: existing audit rows verify identically (their `metadata` columns are JSON strings); backup bundles produced pre-v0.6.67 round-trip correctly because byte output for plain types is unchanged. **Tests** — 17 layer-0 audit-chain assertions from v0.6.66 cover the unified walker; existing pagination tests cover `bufferAs: "reject"`. Smoke 7251 / wiki e2e 178 / Linux 86.3s / eslint clean / shellcheck clean.
58
-
59
- - **0.6.66** (2026-05-03) — `b.auditChain.canonicalize` deep-walks values and rejects non-plain types instead of silently round-tripping garbage. Same bug class as the v0.6.60 pagination canonicalize fix: pre-fix the walker did `Object.keys(row).map(...)` and JSON.stringify'd whatever fell through, which silently encoded **Map / Set / RegExp** as `{}`, **Symbol / function** as missing keys, **BigInt** as a thrown `Do not know how to serialize` mid-emit (DoS-shape on any operator routing bigint IDs into audit metadata), and **circular references** as the unwrapped JSON.stringify error message. The post-fix walker: BigInt decimal string (no data loss for large account IDs / 64-bit counters from external-DB drivers); Date → ISO string (already worked via JSON.stringify but now explicit before the recursive walk); Map / Set / RegExp / Symbol / function reject with a clear constructor-name message; circular refs detected via WeakSet and rejected as a framework error. Walks recursively so nested structures (`{ a: [BigInt(1), BigInt(2)] }`, `{ a: { b: Uint8Array(…) } }`) all serialise correctly. **Stored-row compat** existing audit rows verify identically because their stored `metadata` columns are already JSON strings; the change only affects EMIT-time serialisation. **Tests** — 17 new layer-0 assertions in `test/00-primitives.js`. Smoke 7234 → 7251 / wiki e2e 178 / Linux 86.1s / eslint clean / shellcheck clean.
60
-
61
- - **0.6.65** (2026-05-03) — `b.scheduler.parseCron` rejects step values exceeding the field's range. Pre-fix `*/99999 * * * *` was silently accepted and degenerated to "minute 0 of every hour" (because the for-loop adding values stopped at the first iteration when `step > range`); an operator typing the typo got a once-per-hour schedule when they probably meant once per N minutes. Now `_parseCronField` rejects with `scheduler/invalid-cron` when `step > (range.max - range.min + 1)`. The bound is inclusive so `*/60 * * * *` (= "minute 0 of every hour" written with a redundant explicit step) still accepts. **Tests** — 4 new layer-0 assertions: `*/60` accepts, `*/61` rejects, `*/99999` rejects (the silent-degenerate case), `* */25 * * *` rejects (step > 24-hour range). Smoke 72307234 / wiki e2e 178 / Linux 86.9s / eslint clean / shellcheck clean.
62
-
63
- - **0.6.64** (2026-05-03) — `b.credentialHash.verify` now enforces the same 16-byte minimum payload that `hash()` enforces. Pre-fix the asymmetry was a real risk: `hash()` refused to *create* a hash shorter than 16 bytes (`length < 16` rejected with `credential-hash/bad-opt`), but `verify()` silently accepted any payload length, including 1-byte payloads where the collision space is **256** — a hand-crafted envelope `{ magic, algoId, SHAKE256(targetPassword, 1)[0] }` would verify against the target password in microseconds. A storage bug or attacker tampering that truncated the stored envelope produced a verifiable but catastrophically weak hash; an attacker with DB write access who couldn't replace the hash outright could *truncate* it to a nearly-cleartext-equivalent value. Both ends now refuse short payloads symmetrically via the new `SHAKE256_MIN_LENGTH = 16` constant; verify returns false (with `payload-too-short` observability label) when the envelope's payload is < 16 bytes. Legitimate truncation that keeps the payload 16 bytes still verifies (the framework's design intentionally supports XOF variable-length digests via `params: { length: N }`); the bound only refuses hashes too short to provide meaningful collision resistance. **Tests** — 4 new layer-0 assertions on `b.credentialHash.verify` (1-byte attack envelope rejected, 15-byte rejected, 16-byte accepts, hash() rejects length 15). Smoke 72267230 / wiki e2e 178 / Linux 86.5s / eslint clean / shellcheck clean.
64
-
65
- - **0.6.63** (2026-05-03) — `b.parsers.toml.parse` enforces `maxDepth` on dotted-key paths. Pre-fix the parser walked arbitrarily deep table headers (`[a.b.c.d.e…]`) without applying the existing `maxDepth` (which only ran inside `_parseValue` for inline tables / arrays); a 10,000-segment path built a tree deep enough to stack-overflow the recursive `_normalize` walker post-parse. An attacker submitting a malicious TOML config to `b.config` would crash the framework at boot or whenever the file was reloaded. Now `_parseDottedKey` rejects a path with more than `maxDepth` segments via the same `toml/too-deep` error code already used for value-side depth. **Tests** — 4 new layer-0 assertions: 99-segment path under default depth accepts, 150-segment path rejects with `toml/too-deep`, 10K-segment path rejects (no stack overflow), `maxDepth: 1000` opt allows a 500-segment path. Smoke 72227226 / wiki e2e 178 / Linux 85.6s / eslint clean / shellcheck clean.
66
-
67
- - **0.6.62** (2026-05-03) — URL-length DoS-shape gap closed in two places. **`b.safeSchema.string().url()`** was regex-only with no length cap, accepting arbitrarily long matching strings — same DoS class as v0.6.61's `.email()` gap; an operator chaining `.url()` on a request body would feed multi-megabyte URLs into downstream HTTP clients / SSRF gates / log lines. Now rejects with `string/url-too-long` at 8193+ chars. **`b.safeUrl.parse`** had the same gap at the framework's primary URL-validation surface (used by httpClient + ssrfGuard + every operator-supplied URL). The 8 KB cap now applies BEFORE handing the string to Node's `new URL()` parser; operator-supplied URLs feeding `b.httpClient.request({ url })` from request bodies / webhook configs are bounded. Operators with legitimate non-standard use (proxies, tunnels with embedded payloads) override via `opts.maxUrlLength`. Both bounds reference RFC 7230 §3.1.1's 8000-octet recommendation. **Tests** — 4 new layer-0 assertions on `string().url()` (8 KB / 8193 / 100K / error code) + 4 on `safeUrl.parse` (8192 accept / 8193 reject / error code / `maxUrlLength` opt override). Smoke 72157222 / wiki e2e 178 / Linux 85.5s / per-primitive integration 16 files / eslint clean / shellcheck clean.
68
-
69
- - **0.6.61** (2026-05-03) — `b.safeSchema.string().email()` enforces RFC 5321 §4.5.3.1.3's 254-character cap on the address. Pre-fix the validator was the regex-only `/^[^\s@]+@[^\s@]+\.[^\s@]+$/`, which accepts arbitrarily long matching strings — an operator chaining `.email()` on a request body was open to a DoS shape (50 KB email passes validation, downstream DB writes / log lines unbounded). Now `.email()` rejects with `string/email-too-long` at 255+ chars before the regex even runs, with a message naming the RFC. Operators with a legitimate non-RFC reason for longer addresses skip `.email()` and chain `.regex(custom)` directly. **Tests** — 4 new layer-0 assertions: 254-char address accepts, 255 / 500 char addresses reject, error code `string/email-too-long` exposed for operator handler routing. Smoke 72117215 / wiki e2e 178 / Linux 85.8s / eslint clean / shellcheck clean.
70
-
71
- - **0.6.60** (2026-05-03) — `b.pagination.encodeCursor` correctness fixes for non-plain types in cursor state. The `_canonicalize` walker did `if (typeof === "object") Object.keys(...)`, which silently lost data on three classes of input: **Date** silently encoded as `{}` (Date instances have no own enumerable keys); **Buffer / Uint8Array** silently encoded as `{"0":97,"1":98,…}` (indexed-key serialisation); **circular references** stack-overflowed instead of throwing a clean error. After encode decode the operator's data was either gone or unrecognisable. The fix: Date now serialises to its ISO string (matches stdlib `JSON.stringify` semantics round-trips through encode/decode as the ISO string operators expect); Buffer / Uint8Array / Map / Set / RegExp throw `pagination/bad-state` with a message naming the offending constructor and pointing operators at the right conversion (`"convert to a plain primitive (string / number / iso-string) first"`); circular references throw `pagination/bad-state` cleanly via WeakSet cycle detection. **Tests** — 7 new layer-0 assertions: Date round-trip preserved, all 5 non-plain types reject cleanly, circular reference rejects without stack overflow. Smoke 7204→7211 / wiki e2e 178 / Linux 85.3s / eslint clean / shellcheck clean.
72
-
73
- - **0.6.59** (2026-05-03) — HTTP/2 session teardown deduplication + http-client sweep. v0.6.58's inline `session.close() + session.destroy()` block in `lib/log-stream-otlp-grpc.js` was the right fix for the OTLP-gRPC sink hang, but the same bug class lives in `lib/http-client.js` the h2 transport pool had 5 call sites running the bare `session.close()` (`_resetTransports`, the ALPN-fallback path, the h2 connect-error path, the h2c connect-error path, the idle-timeout handler, and `_resetForTest`) all of which leak the underlying TCP socket on idle / error / fallback paths in exactly the same way. New `lib/http2-teardown.js` exports `tearDownH2Session(session)` which performs the close()-then-destroy() routine; `lib/http-client.js` and `lib/log-stream-otlp-grpc.js` both import it. The OTLP-gRPC v0.6.58 inline block is removed in favour of the shared helper. **Tests** — verified in a node:24-alpine docker container: smoke 7204 checks in 85.3 seconds. Wiki e2e 178 / eslint clean / shellcheck clean.
74
-
75
- - **0.6.58** (2026-05-03) — `b.logStream` OTLP-gRPC sink hang fix that has been silently breaking the npm-publish gate since v0.6.38. The sink's `close()` was calling `session.close()` (HTTP/2 *graceful* close — waits for in-flight streams before freeing the socket) but never `session.destroy()`. The graceful close completed but the underlying TCP socket stayed connected, blocking the test fixture's `server.close()` indefinitely on Linux CI runners. Same bug also added ~120 seconds of lingering latency to every local smoke run on Windows (smoke 196s → 76s after fix). The fix calls `session.close()` then `session.destroy()`; by the time we reach close(), all buffered records have been flushed via the awaited `inflightPromise` + final `_doExport`, so destroy() is structurally safe. **Operator impact** the npm registry has been stuck at v0.6.37 since 2026-05-02; every tag from v0.6.38 v0.6.57 timed out at the publish workflow's smoke step. With this fix the publish should reach `npm publish`. No layer-0 or integration test changes — the existing `log-stream-otlp-grpc.test.js` round-trip was passing locally because tests use small batches and the lingering socket happens to terminate before the test driver's overall timeout, but it was leaving the process unable to exit cleanly until the OS-level TCP timeout fired. Smoke 7204 / wiki e2e 178 / integration 16 files / eslint clean / shellcheck clean.
76
-
77
- - **0.6.57** (2026-05-03) — `b.safeBuffer.boundedChunkCollector` strict `maxBytes` validation. Pre-fix the validator was `typeof opts.maxBytes === "number" && opts.maxBytes > 0` which silently accepted `Infinity` (defeating the OOM-cap purpose entirely a hostile 10-GB upstream would accumulate fully) and non-integer floats like `3.5` (which set a fractional cap that confused downstream `total + chunk.length > maxBytes` arithmetic). Now requires positive finite integer; everything else throws `buffer/bad-arg` at boot with the offending value in the message. Real-world consumers (`b.httpClient` request cap, `b.atomicFile.read`, `b.parsers.*`, multipart body parser) are unaffected because they pass real positive integers via `C.BYTES.*` helpers; the fix catches operator typos / misconfigured env-var coercions where `Number(env.MAX_BYTES)` produces NaN or Infinity. **Tests** 7 new layer-0 boot-validation assertions in `test/00-primitives.js` (smoke 7197→7204). Wiki e2e 178 / eslint clean / shellcheck clean.
78
-
79
- - **0.6.56** (2026-05-03) — `b.auth.totp.verify` strips whitespace + common separators (`\s`, `-`, `.`, `_`) from the user-supplied code before the timing-safe comparison. Authenticator UIs (Google Authenticator, Authy, Duo, 1Password, etc.) and clipboard paste typically introduce these `"123 456"`, `"123-456"`, `"123.456"`, `" 123456\t"`and the framework was rejecting all of them as if they were real typos. RFC 6238 / NIST 800-63B don't mandate normalisation, but every consumer-facing TOTP implementation strips these because the alternative is a silent operator footgun: a user mashing a code from their phone into a login form that the verifier rejects on the space character. Letters and other non-numeric characters are NOT stripped those are real input errors, not paste artefacts. Comparison stays timing-safe via the framework's `crypto.timingSafeEqual` wrapper. **Tests** — 6 new layer-0 paste-form assertions in `test/00-primitives.js` (smoke 71917197). Wiki e2e 178 / per-primitive integration 16 files / eslint clean / shellcheck clean.
80
-
81
- - **0.6.55** (2026-05-03) — two bug fixes. **`b.mail` SMTP transport**: TLS handshake (implicit on port 465 + STARTTLS upgrade) was unconditionally setting `servername: cfg.host`, which Node's `tls.connect` rejects when host is an IP literal `Setting the TLS ServerName to an IP address is not permitted`. Operators with `host: "127.0.0.1"` (or any IPv4/IPv6 literal) hit the error before a real cert-verification or auth issue could surface. Now: SNI is auto-suppressed on IP literals (matching `lib/redis-client.js`'s v0.6.28 convention); operators with private CAs and an IP-only target pass `opts.servername: "expected-cn.example"` explicitly. **`docker/mongo/init-tls.sh`**: SC2015 `id mongodb >/dev/null && chown mongodb:mongodb ... || true` is not if-then-else; the `|| true` would fire if `chown` failed (treating it as if `id` had failed), masking real failures. Rewritten as plain `if id mongodb …; then chown …; fi`. The shellcheck failure was blocking the v0.6.54 npm-publish gate added in this morning's release. Smoke 7191 / wiki e2e 178 / per-primitive integration 16 files / eslint clean / shellcheck clean.
82
-
83
- - **0.6.54** (2026-05-03) — npm-publish workflow fixes. The npm registry has been stuck at v0.6.37 because every tag from v0.6.38 v0.6.53 timed out at the workflow's 15-minute cap and got cancelled before reaching the `npm publish` step (smoke alone is ~3 min locally / 6-8 min on CI runners; combined with eslint cold-pull + wiki e2e + SBOM generation that exceeded 15 min). Cap raised to 30 minutes, matching `release-container.yml`. The publish gate also now runs `ludeeus/action-shellcheck@master` against every tracked `.sh` (vendor-update.sh + docker init scripts ARE shipped in the npm tarball, so a shell-script regression should fail the publish). Hadolint stays in release-container.yml only because the Dockerfile is container-only and is not part of the npm artifact. No framework code changes release-infrastructure-only patch.
84
-
85
- - **0.6.53** (2026-05-03) — `b.objectStore.bucketOps` SigV4 backend gains audit + observability emissions across the full state-changing surface closes the v0.6.47 compliance audit gap (Object Lock + retention + legal hold were positioned as SEC 17a-4 / FINRA / HIPAA-grade primitives but had zero audit trail; an auditor asking "who set the retention period for this object, and when?" got nothing). New opts on `bucketOps.create`: `audit` (operator passes `b.audit`), `observability` (operator passes `b.observability`), `auditSuccess` (default true; opt-out at extreme volume), `auditFailures` (default true). Audit emissions for every state-changing op: `objectstore.bucket.create / delete / setLifecycle / setCorsRules / setObjectLockConfiguration` and `objectstore.object.setRetention / setLegalHold`. Observability counter on every op including reads (`getObjectLockConfiguration / getRetention / getLegalHold`). Each state-changing call accepts `{ req }` so the audit chain populates the WHO/WHERE/HOW columns (actorIp, actorUserAgent, actorSessionId, requestId, method, route) via `requestHelpers.resolveActorWithOverride`. **`bypassGovernance` is the high-risk metadata** `setObjectRetention` audit metadata sets `bypassGovernance: true` when the operator used the bypass-shorten path; operators wire alerting on this field per compliance posture. New `objectstore` namespace registered in `audit.FRAMEWORK_NAMESPACES`. **Tests** 10 new layer-0 audit/observability assertions (smoke 7179→7191) including auditSuccess:false suppresses success-audit but failure-audit still fires. Wiki e2e 178 / per-primitive integration 16 files / eslint clean.
86
-
87
- - **0.6.52** (2026-05-03) — `b.mtlsCa` serial-number normalisation now rejects gibberish instead of silently stripping non-hex chars to whatever's left. Pre-fix: `_normalizeSerial("xyz-not-hex")` ran `.replace(/[^0-9a-fA-F]/g, "")` which kept just the single `e` and registered a phantom `revoke("e")` row. A typo in an `openssl x509 -noout -serial` paste or any non-serial string silently became a real revocation entry that would land in the next CRL the operator published. **The fix:** the normalizer now strips only the documented operator-paste shapes (leading `0x`, separators `:` / `-` / whitespace) and asserts that what remains is all-hex; anything else throws `mtls-ca/bad-serial`. Real shapes still accepted (`"0xABC123"`, `"AB:C1:23"`, `"AB-C1-23"`, `"abc 123"`, `"abc123"` all normalise to `abc123`); gibberish (`"xyz-not-hex"`, `" "`, `"nope"`) rejects cleanly. **Tests** — 3 new live-CA integration assertions on top of the 41 in `mtls-ca.test.js` (44 total). Smoke 7179 / wiki e2e 178 / per-primitive integration 16 files / eslint clean.
88
-
89
- - **0.6.51** (2026-05-03) — `b.objectStore.bucketOps` Object Lock readback methods now return clean defaults for "never configured" state instead of throwing the raw HTTP 4xx error. Surfaced via the same live MinIO probing that found v0.6.49: `getObjectLockConfiguration` on a bucket created without `objectLockEnabled` was throwing `HTTP 404 ObjectLockConfigurationNotFoundError` a forced try/catch on the operationally-trivial question "is this bucket lock-enabled?". Same UX problem on `getObjectRetention` (HTTP 400 `NoSuchObjectLockConfiguration` when the object never had retention set) and `getObjectLegalHold` (same error code when never set). **The fix:** new `_isLockNotConfigured` helper recognizes both error shapes; the three `get*` methods catch the error and return: `getObjectLockConfiguration` `{ enabled: false, mode: null, days: null, years: null }`, `getObjectRetention` `{ mode: null, retainUntil: null }`, `getObjectLegalHold` `{ status: "OFF" }` (operationally identical to "no hold ever applied"). Real errors (auth failures, network errors, bucket-not-found) still throw. **Tests** — 7 layer-0 mock-server assertions for the not-configured paths (smoke 7172→7179) + 8 live MinIO integration assertions in `_runObjectLockOnEndpoint` and a new no-lock-bucket variant in `_runOnEndpoint` (object-store-sigv4 39→47 checks). Wiki e2e 178 / eslint clean.
90
-
91
- - **0.6.50** (2026-05-03) — `b.objectStore` SigV4 multipart upload subresource-query consistency + live integration coverage. Same-class follow-up to v0.6.49: `lib/object-store/sigv4.js` was the only remaining `URLSearchParams.set(k, "")` empty-value site in the framework `initiateUrl.searchParams.set("uploads", "")` produced `?uploads=` (with trailing `=`) for the InitiateMultipartUpload subresource. AWS S3 + MinIO both accept `?uploads=` (in contrast to `?retention=`, `?legal-hold=`, `?object-lock=` which strict implementations route to the put-object handler), so this wasn't broken in the real world — but the convention drift would mask future regressions and a future stricter S3 implementation could route `?uploads=` differently. Fixed for consistency: `?uploads` is now bare on the wire, identical to the v0.6.49 convention. SigV4 canonicalization unchanged (still presents `uploads=` to the signer per AWS spec). **Live integration coverage** `test/integration/object-store-sigv4.test.js` `_runOnEndpoint` gains a multipart-upload + get round-trip pass (forces multipart with `multipartThresholdBytes: 1`, `partSizeBytes: 5 MiB`, payload 6 MiB 2-part flow). Until v0.6.50 the multipart path had only mock-server coverage; the live MinIO assertion locks in the `?uploads`, `?partNumber=N&uploadId=...`, and `?uploadId=...` (CompleteMultipartUpload) wire flow. Smoke 7172 / wiki e2e 178 / per-primitive integration 16 files (object-store-sigv4 3539 checks) / eslint clean.
92
-
93
- - **0.6.49** (2026-05-03) — `b.objectStore.bucketOps` SigV4 backend wire-form fix for subresource queries (`?lifecycle`, `?cors`, `?object-lock`, `?retention`, `?legal-hold`). The v0.6.47 implementation built URLs via `URLSearchParams.set("retention", "")` which produces `?retention=` (with a trailing `=`) on the wire. Strict S3 implementations (MinIO in particular) interpret the trailing `=` as "this is a body PUT with a query parameter" rather than "this is a subresource request" — the request gets routed to the put-object handler, which then rejects it with `InvalidRequest: Object is WORM protected and cannot be overwritten` instead of routing to the retention/legal-hold handler. **The fix:** `_bucketUrl` and `_objectUrl` now build the URL string manually with `?subresource` (no `=`) for empty-value query keys, then pass it to `new URL(s)`. The URL constructor preserves the bare token in `url.search` (the wire form) while `url.searchParams` still presents it as `subresource=` (so the SigV4 canonicalizer's signing path is unchanged — AWS spec REQUIRES `key=` with the empty `=` in the canonical query string for signature computation). Both behaviors are preserved without forking the URL across the signer + transport. **Live integration coverage** — `test/integration/object-store-sigv4.test.js` gains a third endpoint variant (`_runObjectLockOnEndpoint`) that creates a bucket with `objectLockEnabled: true` against live MinIO and exercises the full surface end-to-end: per-object retention set + get, per-object legal hold ON/OFF round-trip, bucket-level `setObjectLockConfiguration` + `getObjectLockConfiguration` round-trip, `bypassGovernance` retention shortening, and cleanup. Closes the v0.6.47 gap where the new methods only had mock-server coverage. **Layer-0 regression tests** `test/layer-0-primitives/sigv4-bucket-ops.test.js` gains 3 wire-form assertions on `?retention`, `?legal-hold`, `?object-lock` ("bare subresource, no '=' suffix") to catch this class of regression before it reaches operators. Smoke 7172 / wiki e2e 178 / per-primitive integration 16 files (object-store-sigv4 2235 checks) / eslint clean.
94
-
95
- - **0.6.48** (2026-05-03) — doc-artifact catch-up for v0.6.47 Object Lock surface. The v0.6.47 release shipped the lib + tests + wiki + CHANGELOG entry but missed the README + SECURITY.md weave-ins (`feedback_doc_sweep_with_release.md` rule §10 "doc artifacts ride with the release commit"). README "What ships in the box" object-store bullet now mentions S3 Object Lock + per-object retention + legal hold for write-once-read-many compliance workloads. SECURITY.md application checklist gains a sibling line to the existing `b.retention` TTL entry covering the operator's hardening checklist for write-once-read-many object archives (SEC 17a-4, FINRA, HIPAA-shaped retention) `objectLockEnabled: true` at create time only, `COMPLIANCE` mode irrevocable by anyone (including root), `bypassGovernance` requires the `s3:BypassGovernanceRetention` permission. No code changes; doc-only patch.
96
-
97
- - **0.6.47** (2026-05-03) — `b.objectStore.bucketOps` SigV4 backend gains S3 Object Lock, retention, and legal-hold support for compliance workloads (SEC 17a-4, FINRA, HIPAA retention). New surface on the `sigv4` protocol only (Azure / GCS handle write-once-read-many out-of-band): `create(name, { objectLockEnabled: true })` flips the underlying versioning + WORM at create time (the only point S3 allows it); `setObjectLockConfiguration(name, { mode, days|years })` and `getObjectLockConfiguration(name)` apply / read the bucket-level default retention; `setObjectRetention(name, key, { mode, retainUntil, bypassGovernance? })` and `getObjectRetention(name, key)` apply / read per-object retention dates; `setObjectLegalHold(name, key, "ON"|"OFF")` and `getObjectLegalHold(name, key)` apply / read per-object legal holds. Up-front validation rejects bad inputs at the call site before any request is signed: unknown mode, `days`+`years` together, fractional or negative durations, non-Date or past-dated `retainUntil`, statuses outside `ON`/`OFF` all throw `INVALID_OBJECT_LOCK` / `INVALID_RETENTION` / `INVALID_LEGAL_HOLD` (or `INVALID_KEY`) before TCP. `bypassGovernance: true` adds the `x-amz-bypass-governance-retention: true` header for accounts with the `s3:BypassGovernanceRetention` permission; `COMPLIANCE` mode cannot be shortened or bypassed by anyone (including root) pick it deliberately. The wiki's Object Store page documents the full surface with COMPLIANCE-vs-GOVERNANCE guidance. **Tests**: layer-0 sigv4-bucket-ops gains 44 new mock-server assertions covering Object Lock header on create, configuration set/get + XML body shape, retention set/get + URL query-arg + body, bypass-governance header propagation, legal-hold round-trip, and the four validation paths (lock-config / retention / legal-hold / key). Smoke 7169 / wiki e2e 178 / eslint clean.
98
-
99
- - **0.6.46** (2026-05-02) — `b.i18n.messageFormat` ICU MessageFormat parser + evaluator. The pre-v0.6.46 `b.i18n` translation format is JSON-shaped with CLDR plural keys at the JSON level (`one / few / many / other` under a key like `inbox.unread`) covers the simple plural case but not the nested / inline patterns common in real-world translations: gendered selects, plural forms with embedded variables, ordinals, arguments inside cases. **`lib/i18n-messageformat.js`** (new, ~330 LOC) minimal-but-correct ICU MessageFormat parser + evaluator. Supports: `{argName}` simple replacement, `{argName, plural, =N {...} category {...} other {...}}` with optional `offset:N` and `#` placeholder for the plural arg minus offset, `{argName, selectordinal, ...}` (CLDR ordinal categories via `Intl.PluralRules({ type: "ordinal" })`), `{argName, select, caseA {...} other {...}}`, nested arguments inside any case body, ICU-spec apostrophe escaping (`''` `\'`, `\'{\'` / `\'}\'` for literal braces, `\'X\'` runs to next apostrophe). CLDR cardinal + ordinal categories via `Intl.PluralRules` per locale (cached). **Out of scope** (operators with these reach for the full `messageformat` package): inline `number` / `date` / `time` formatters (use `formatNumber` / `formatDate` from `b.i18n` separately and inline the result), choice-format (deprecated by ICU in favor of plural), custom argument types. Bespoke parser, no vendoring needed (the `messageformat-parser` subset would have been ~50 KB; bespoke is ~330 LOC + zero npm dep). **`b.i18n.t(key, vars, opts)` integration** auto-detects MessageFormat-shaped entries via `messageFormat.looksLikeMessageFormat(template)` (regex match for `{name, plural / select / selectordinal, ...}`); operators force the path with `opts.messageFormat = true`. Plain `{var}` interpolation and existing CLDR plural-shaped JSON entries continue to work unchanged on the legacy interpolator. **Tests**: 54 layer-0 assertions covering surface, plain literal pass-through, simple argument interpolation (including null / undefined / number arg handling), plural exact-match (`=N`), CLDR cardinal categories across English + Russian (`one / few / many / other`), ordinal categories (`1st / 2nd / 3rd / 4th / 21st`), select with operator-supplied + missing arg → `other` fallback, nested plural-inside-select-inside-plural, ICU apostrophe escaping (`\'\'` literal apostrophe; `\'{name}\'` literal braces; `\'#\'` literal hash inside plural body), parse rejection (non-string input, plural without `other`, select without `other`, unsupported type, missing argument name), evaluator rejection (plural with non-numeric arg → `BAD_VAR`), `looksLikeMessageFormat` detection (plural / select / selectordinal yes; plain `{var}` and plain text no), end-to-end through `b.i18n.t()` (plural-shape, select, simple `{var}` legacy path). Smoke 7125 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
100
- - **0.6.45** (2026-05-02) — `b.mtlsCa` revocation registry + signed CRL generation. The wiki has documented "keeps the revocation list in `cert_revocations`" since the primitive landed, but the actual surface didn\'t exist — operators couldn\'t revoke an issued cert without rebuilding the CA from scratch. This patch adds the revocation primitives that close that gap. **`ca.revoke(serialNumber, opts?)`** — records a revocation in `dataDir/revocations.json` with serial / reason / revokedAt timestamp. Serials are normalized (strips `0x` prefix, removes non-hex chars, lowercases) so operators paste any of `"0xABC123"` / `"AB:C1:23"` / `"abc123"` and they all hit the same row. Reason names map to RFC 5280 numeric codes (`"keyCompromise"` → 1, `"superseded"` → 4, `"cessationOfOperation"` → 5, full set: `unspecified / keyCompromise / caCompromise / affiliationChanged / superseded / cessationOfOperation / certificateHold / removeFromCRL / privilegeWithdrawn / aACompromise`). Idempotent — repeated revoke() of the same serial preserves the original revokedAt. **`ca.isRevoked(serialNumber)` / `ca.getRevocations()`** — registry queries, also serial-format-agnostic. **`ca.generateCrl(opts?)`** → `{ crlPem, thisUpdate, nextUpdate, entryCount, path }`. Builds an RFC 5280 X.509 CRL signed with the CA private key by routing through `engine.generateCrl({ caCertPem, caKeyPem, revocations, thisUpdate, nextUpdate })`. The default engine implementation lives at `lib/mtls-engine-default.js`\'s new `generateCrl` (uses the vendored peculiar/x509 library\'s `X509CrlGenerator.create`, signs with the same algorithm `_selectAlgorithm` chose for the CA). Default `nextUpdate` is 7 days after `thisUpdate`; operators publishing at a different cadence pass explicit dates. CRL persists to `dataDir/ca.crl` by default; pass `{ persist: false }` to inspect without writing. Operators serve `ca.crl` at the CRL distribution point referenced from issued certs; revocation status checks at the TLS layer pick it up server-side. **Tests**: 19 new live integration assertions on top of the existing 22 in `test/integration/mtls-ca.test.js` — covers revoke happy path (records the right shape with reasonCode mapped), serial-format normalisation (`0xABC123` ≡ `abc:12:3` ≡ `ABC123`), idempotency (duplicate revoke preserves revokedAt), error paths (empty serial throws `bad-serial`, unknown reason throws `bad-reason`), CRL generation against the real engine + real CA (PEM output decodes to non-trivial DER, entryCount matches registry size, default nextUpdate ~7 days out, persisted to `dataDir/ca.crl`, regenerate after a new revoke picks up the new entry). Smoke 7071 / wiki e2e 178 / per-primitive integration 16 files (mtls-ca up from 22→41 checks) / wiki integration 32 / shellcheck clean / eslint clean.
101
- - **0.6.44** (2026-05-02) — `b.bundler` engine surface for ESM module-graph bundling. The previous `b.bundler` was a content-hash + manifest pipeline only operators wanting tree-shake / minify / source-map / multi-file ESM bundling pre-concatenated their input or ran an external bundler. This patch adds an explicit pluggable engine layer: `b.bundler.create({ engine })` accepts any object implementing `{ name, transform(entryPath, contentBuf) { content, sourceMap?, imports? } }` between the file read and the cache-bust hash. **`b.bundler.engine.passthrough`** is the default same byte-verbatim behavior every pre-v0.6.44 caller got, no breaking change. **`b.bundler.engine.fromEsbuild(esbuild, opts?)`** wraps a supplied esbuild module (the operator\'s own `require("esbuild")` or `require("esbuild-wasm")`) into the engine contract — routes through `esbuild.build({ entryPoints, write: false, ...opts })`, picks the JS output + `.map` sibling out of `outputFiles`, returns `{ content, sourceMap }` for the framework to hash + write atomically. Source maps land as `<hashed-filename>.map` siblings; the bundler\'s `outputs[i].sourceMapPath` reports the on-disk path. **The framework deliberately does NOT vendor `esbuild-wasm` itself.** The wasm blob is ~10 MB bigger than every other vendored framework dep combined; it would 3–5× the `@blamejs/core` npm tarball for a build-time tool most operators only need at deploy time. Treating esbuild as an operator-supplied driver matches `b.externalDb` (operator brings the pg / mysql client), `b.mtlsCa.create({ engine })` (operator can swap to HSM-backed signer), `b.objectStore.bucketOps.create({ protocol })` (operator picks the cloud) the framework owns the integration seam, the operator brings the heavy machinery. Operators who want ESM bundling install esbuild themselves (`devDependency` is the typical pick bundling is a deploy-time concern, not a runtime one), pass it via `bundler.engine.fromEsbuild`. Operators with a different bundler (rollup / swc / vite / parcel / a custom JS-only minifier) implement the same `{ name, transform }` interface directly. **Tests**: 25 layer-0 assertions covering surface (`b.bundler.engine.passthrough`, `b.bundler.engine.fromEsbuild` factory), passthrough as default behavior unchanged, engine validation rejects bad shapes (non-object / missing transform / missing name / fromEsbuild rejects non-esbuild input), custom engine transform applied + hashed AFTER transform (so the hash reflects the bundled output, not the source), source-map sibling write at `<hashed>.<ext>.map`, fromEsbuild adapter passes entryPoints + forces `write: false` + threads operator opts (minify, sourcemap) through to esbuild + correctly identifies the JS output vs the .map sibling among `outputFiles`, empty esbuild output throws `bundler/engine-empty`. Smoke 7071 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
102
- - **0.6.43** (2026-05-02) — AWS SQS queue backend. `b.queue` gains a third backend alongside `local` (SQLite) and `redis`: `protocol: "sqs"` for operators on AWS who want a fully-managed queue without standing up a Redis cluster. Wire protocol is AWSJsonProtocol_1.0 over HTTPS (Content-Type `application/x-amz-json-1.0`, `X-Amz-Target: AmazonSQS.<Action>`), SigV4-signed via the framework\'s service-agnostic `lib/object-store/sigv4.js`. Action mapping: `enqueue SendMessage`, `lease ReceiveMessage` (long-poll up to `WaitTimeSeconds`), `extendLease ChangeMessageVisibility`, `complete DeleteMessage`, `fail ChangeMessageVisibility(VisibilityTimeout=0)` for immediate re-delivery (DLQ routing then happens server-side via the queue\'s `RedrivePolicy` attribute, configured at queue creation outside the framework), `sizeGetQueueAttributes(ApproximateNumberOfMessages)`, `purgePurgeQueue`. Queue-nameURL: by default `https://sqs.{region}.amazonaws.com/{accountId}/{queueName}`; operators with cross-account / FIFO / VPCE endpoints pass an explicit `queueUrlByName(name) url` resolver. STS session tokens propagate as `x-amz-security-token`. Sealing posture matches local + redis: payloads pass through `cryptoField.sealRow("_blamejs_jobs", row)` before SendMessage so SQS only ever sees the sealed envelope. **Out of scope for the SQS adapter** (operator wiring required, framework-side guard rails not appropriate for a managed service): DLQ inspection (`dlqList / dlqRetry / dlqSize`) SQS DLQs are separate queues that operators wire as a second framework backend; flow / cron / parent-child dependencies SQS has no native flow primitives so those stay on `local` / `redis`; `sweepExpired` SQS handles visibility-timeout server-side. `DelaySeconds` is clamped to the 900-second SQS hard cap; `MaxNumberOfMessages` is clamped to 10 (SQS\'s per-call ceiling). Removed `sqs` from `DEFERRED_PROTOCOLS`. **Tests**: 41 layer-0 mock-based assertions covering factory validation (rejects missing region / accessKeyId / secretAccessKey / accountId-without-resolver; accepts `queueUrlByName` as accountId substitute), full SendMessage wire shape (Content-Type / X-Amz-Target / Authorization carries `Credential=AKIATEST/.../sqs/aws4_request` / QueueUrl built from accountId+name / MessageBody is JSON-serialized sealed envelope with `_id` + `queueName`), DelaySeconds clamping at 900s, ReceiveMessage round-trip with sealed-envelope unsealing back into `{ jobId, queueName, payload, receiptHandle, leaseExpiresAt, ... }`, MaxNumberOfMessages clamping at 10, complete/extendLease/fail (all assert receiptHandle plumbing + correct VisibilityTimeout values), MISSING_RECEIPT thrown on missing receiptHandle, size + purge, custom queueUrlByName resolver (cross-account), STS session token surfacing as `x-amz-security-token` header. Smoke 7046 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
103
- - **0.6.42** (2026-05-02) — wiki schema-then-example sweep, batch 3 (testing + format-helpers + backup-restore). Closes the W1 backlog item every wiki page that documents an operator-facing primitive now has its primary primitive surfaces in the four-piece template (heading + opts model + description prose + example). New primitive sections: **testing** `b.testing.mockReq(opts?)` / `.mockRes()`, `b.testing.fakeClock(initialMs?)` (returns `{ now, advance, set }`), `b.testing.captureAudit()` (with corrected `.byAction` / `.captured` surface wiki had non-existent `.events`), `b.testing.captureObservability()` (with corrected `.byName` / `.captured` surface wiki had non-existent `.byEvent` / `.spans`), `b.testing.fakeHttpClient(handler)`, `b.testing.runMiddleware(fn, req, res)`, `b.testing.waitFor(predicate, opts?)`, `b.testing.tempDir(name?)`. **format-helpers** — `b.csv.parse(input, opts?)` / `.stringify(rows, opts?)`, `b.uuid.v4 / .v7 / .parse / .isValid`, `b.slug(input, opts?)` / `.unique(input, exists, opts?)`, `b.time.toParts / .format / .addDays / .addMonths / .startOfDay / .endOfDay / .diffDays / .parseISO / .tzOffsetMs`, `b.archive.zip()`, `b.pagination.cursor(query, opts)` (corrected from non-existent `b.pagination.create({...}).parse(...)` shape actual API is `cursor(query, opts) { items, nextCursor, hasMore }` taking a db Query), `b.forms.render(spec, opts?)` / `.validate(spec, body)` / `.generateCsrfToken / .verifyCsrfToken`. **backup-restore** `b.backup.create(opts)`, `b.restore.create(opts)`. Surfaced + corrected real API drifts: `b.testing.captureAudit().byAction` (not `.byActions`), `b.testing.captureObservability().byName` (not `.byEvent`), `b.pagination.cursor(query, opts)` (not `b.pagination.create({...})`), `b.pagination.encodeCursor(state, secret)` argument order (state first, secret second). Wiki primitive-runtime gate up from **147 186** clean exec runs (+39 in this patch alone, +111 across v0.6.39–v0.6.42 from the v0.6.38 baseline of 75). Smoke 7004 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
104
- - **0.6.41** (2026-05-02) — wiki schema-then-example sweep, batch 2 (welcome + crypto-vault + network-crypto + safe-parsers). New primitive sections following the four-piece template (heading + opts model + description prose + example): **welcome** `b.createApp(opts)` (the operator entry point that wires the dependency-ordered boot vault → external-DB cluster lease → framework schema local DB router middleware stackoperator routes error handler). **crypto-vault** `b.vault.seal(plaintext) / .unseal(sealed)`, `b.cryptoField.registerTable(name, opts)` (sealedFields + derivedHashes registry the db-query layer reads on every read/write). Removed the API-drift `b.webhook.signer.create({...})` / `b.webhook.verifier.create({...})` examples actual API is `b.webhook.signer({...})` and `b.webhook.verifier({...})` documented at outbound-http; crypto-vault now cross-links instead of restating. **network-crypto** `b.mtlsCa.create(opts)` (lib allow-list reflects: `dataDir` / `vault` / `paths` / `caKeySealedMode` / `generation` / `engine`corrected from prior `sealedMode / caValidityDays / leafValidityDays / audit` shape that wasn\'t in the validate-opts list), `b.pqcGate.create(opts)` (corrected: `internalPort` / `internalHost` / `bypass` / `clientHelloTimeoutMs` / `maxClientHelloBytes` / `log` plus underscore-prefixed test-injection points; previously documented `externalPort` / `mode` / `acceptable` / `audit` weren\'t in the lib allow-list at all), `b.pqcAgent.create(opts?)` (with the `b.pqcAgent.agent` and `.enforced` shared-singleton handles documented). **safe-parsers** `b.safeJson.parse(text, opts?)`, `b.safeBuffer.normalizeText(input, opts?)` / `.boundedChunkCollector(opts)` / `.secureZero(buf)` (corrected to use `bytesCollected()` not `bytes()` per the actual API), `b.safeSql.validateIdentifier(value, opts?)` (corrected from `b.safeSql.identifier` which doesn\'t exist), `b.safeSchema` builder factories (`object` / `string` / `number` / `boolean` / `array` / `literal` / `union` with refinements). Wiki primitive-runtime gate up from **130 147** clean exec runs (+17 in this patch alone). Smoke 7004 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
105
- - **0.6.40** (2026-05-02) — wiki harness upgrade + H1 batch 1 complete. Closes the gap surfaced in v0.6.39: the wiki primitive-runtime gate exec-runs every code block with only `b` in scope; compound-primitive examples that reference operator-supplied `app` / `users` / `template` / `metrics` / `currentToken` / `largeBuffer` / `body` / `loginUrl` / `meUrl` / `authMiddleware` / `loginHandler` identifiers couldn\'t round-trip cleanly. **Harness extension** (`examples/wiki/test/run-example.js`) — added stub bindings for those operator-side identifiers: `app` (Express-shape stub with `use / get / post / put / patch / delete / head / listen / close`), `users` (db-model stub with `create / findOne / findMany / updateOne / deleteOne / count`), `template` (template-engine stub), `metrics` (counter / histogram / gauge stubs), `currentToken` (token-source function), `largeBuffer` / `body` (request-body shape), `loginUrl` / `meUrl` (URL strings), `authMiddleware` / `loginHandler` (passthrough middleware functions). With those bindings every multi-step compound-primitive example reaches the framework boundary it cares about. **`routing` page** — converted to `<h3>b.X.Y(opts)</h3>` primitive sections for the full surface: `new b.router.Router(opts?)`, `router.METHOD(path, spec?, ...handlers)` (covers schema-validated routes via `b.safeSchema`), `router.openapi(opts)`, `b.render.htmlString / .json / .text / .redirect`, `b.htmlBalance.check`, `b.staticServe.create(opts)` (corrected from prior `b.staticServe(rootDir, opts)` shape — actual API is opts-only with required `root`), `b.errorPage.create(opts)`, `b.requestHelpers.extractActorContext / .parseQualityList / .parseListHeader`. **`middleware` page** primitive sections for `b.middleware.rateLimit / .csrfProtect / .cspNonce / .bodyParser / .bodyParser.raw / .sse(handler, opts?) / .requestLog / .networkAllowlist`, `b.cookies.create(opts?)` / `.parse(headerValue)`, `b.validateOpts`. Several real API drifts surfaced + corrected against the lib allow-lists: `rateLimit` uses `refillPerSecond` (not `refillPerSec`), accepts memory-backend `burst` + cluster-backend `limit / windowMs / pruneIntervalMs`; `csrfProtect` uses `tokenLookup / fieldName` (not `session / paramName`); `requestLog` uses `logger` (not `log`); `cookies.create` accepts `vault` for sealing values; `sse(handler, opts?)` requires the handler as the FIRST positional arg, not in opts. **`outbound-http` page** — primitive sections for `b.httpClient.request(opts)` (with subsections for `maxRedirects`, `multipart`, `before / after` interceptors), `b.httpClient.cookieJar.create`, `b.ssrfGuard.checkUrl / .classify`, `b.safeUrl.parse`, `b.webhook.signer / .verifier`. **Wiki primitive-runtime gate up from 87130 clean exec runs** (+43 in this patch alone, +55 across v0.6.39 + v0.6.40 from the v0.6.38 baseline of 75). Smoke 7004 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
106
- - **0.6.39** (2026-05-02) — wiki schema-then-example sweep, batch 1 partial. The original H1 plan was to convert the prose-style headings on `routing` / `middleware` / `outbound-http` to `<h3>b.X.Y(opts)</h3>` primitive sections wholesale. In practice the wiki harness exec-runs every example with only `b` in scope examples that reference operator-supplied identifiers (`app`, `router`, `req`, `res`) fail the runtime gate, which would have meant rewriting every multi-step example to use only `b.X.Y(...)` calls. Rather than ship doc that papers over how operators actually use the framework, this patch lands the *self-contained* primitive sections only pure-function primitives whose example fits in two or three lines: **`b.requestHelpers.parseQualityList(value, opts?)`** (RFC 9110 §12.5 Accept-* parser, returns `[{value, q}, ...]` sorted by q desc), **`b.requestHelpers.parseListHeader(value, opts?)`** (comma-separated header parser with trim / lowercase / unique opts), **`b.htmlBalance.check(html)`** (structural HTML check returning `null` when balanced or `{code, message, line, column}` when not), **`b.ssrfGuard.classify(ip)`** (offline IP classifier returning `"private"` / `"loopback"` / `"link-local"` / `"cloud-metadata"` / `"ula"` / `"reserved"` / `"public"`), **`b.safeUrl.parse(input, opts?)`** (URL shape + protocol gate, defaults to https-only, rejects userinfo by default). Each new section follows the four-piece template (heading + opts model where applicable + description prose + example). Wiki primitive-runtime gate up from 75 87 clean exec runs. The compound primitives (`b.middleware.bodyParser`, `b.middleware.csrfProtect`, `b.httpClient.request`, `b.webhook.signer / .verifier`, `b.cookies.create`) stay in the page\'s prose H2 sections for now they need a harness convention upgrade (multi-line examples with operator-stub identifiers) before they round-trip cleanly through the runtime gate; that lands in a follow-up patch with the matching harness change. Smoke 7004 / wiki e2e 178 / wiki primitive examples 87 clean / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
107
- - **0.6.38** (2026-05-02) — OTLP gRPC + protobuf transport. **`b.logStream` `otlp-grpc` sink** first-class `protocol: "otlp-grpc"` for `b.logStream.init({ sinks: ... })`. Companion to the existing `otlp` HTTP/JSON sink: same OTel Logs Data Model, but transported over HTTP/2 + gRPC framing for the higher-throughput path operators reach for when they're pushing >100K logs/s straight to a collector. **`lib/protobuf-encoder.js`** (new) — minimal proto3 wire-format encoder (write-only no decoder ships). Implements the four wire types the OTel logs schema needs (varint, 64-bit fixed, length-delimited, plus reserved spot for 32-bit fixed); helpers for `uint32 / uint64 / bool / fixed64 / double / string / bytes / embeddedMessage / repeatedMessage`. Operators reach for it when constructing protobuf bodies for any external service that accepts proto over HTTP gRPC, AWS sigv4-protobuf, GCP protobuf APIs. Consistent with the framework's vendoring stance: zero npm runtime deps, zero protobuf parser shipped, the encoder is the framework's own. **`lib/log-stream-otlp-grpc.js`** (new) encodes `ExportLogsServiceRequest` per the OTel logs.proto schema (Resource ScopeLogs LogRecord AnyValue / KeyValue), wraps in gRPC framing (1-byte compression flag + 4-byte big-endian length + protobuf body), POSTs over `node:http2` to `/opentelemetry.proto.collector.logs.v1.LogsService/Export` with `content-type: application/grpc+proto` + `te: trailers`. Reads `grpc-status` + `grpc-message` from response trailers; non-zero gRPC status surfaces as `HTTP_ERROR` with the gRPC error code + message preserved. Same back-pressure semantics as the JSON sink (ring buffer, batched flush on size or maxBatchAgeMs, exponential-backoff retry, drop-on-overflow with operator `onDrop`). Severity mapping debug=5 / info=9 / warn=13 / error=17 per spec. Single HTTP/2 session per sink, kept alive across many Export calls; recreated on disconnect. **Tests**: 27 layer-0 protobuf-encoder assertions verifying canonical encoding (varint(150) = 96 01, varint(300) = ac 02, varint(16384) = 80 80 01; tag computation; string field shape; embedded message length-delimited framing; repeated message; BigInt-path varint for >2^53 ranges; rejection of negative varints). 26 layer-0 OTLP gRPC assertions covering frame shape, log-record encoding (verifies "hello" / "test-service" / "INFO" UTF-8 bytes appear in the output), AnyValue type-tagging per OTel oneof spec (string→1, bool→2, int→3, double→4, bytes→7), full HTTP/2 roundtrip against an h2c mock server (path / content-type / te / framing / body length-prefix all asserted on the wire), gRPC server-side error trailer (status 13 + message text surfaces through `onDrop`), URL validation. Smoke 7004 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
108
- - **0.6.37** (2026-05-02) — Azure + GCS bucket-ops parity. **`b.objectStore.bucketOps`** is now a protocol-dispatching factory: pass `{ protocol: 'sigv4' | 'azure-blob' | 'gcs', ... }` to get a service-scoped client for the matching cloud. Previously SigV4 only. **`lib/object-store/azure-blob-bucket-ops.js`** (new) Azure Storage container lifecycle via Shared Key auth (reuses `azure-blob.js`'s `signRequest`): `create(name, {publicAccess?})` (PUT `/{container}?restype=container`), `delete(name)` (DELETE), `list({prefix?, maxResults?})` (GET `/?comp=list` with XML response parsed into `[{ name, lastModified, etag, leaseStatus, leaseState, publicAccess }]`), `setCorsRules(rules)` (PUT `/?restype=service&comp=properties` Azure CORS is account-level, not per-container). `setLifecycle` is intentionally not implemented because Azure Storage lifecycle management policies live on Azure Resource Manager (`management.azure.com`) and require Azure AD bearer-token auth a different scheme entirely; calling it throws `NOT_SUPPORTED` with operator guidance pointing at Terraform / Bicep / az CLI. **`lib/object-store/gcs-bucket-ops.js`** (new) — GCS bucket lifecycle via service-account JWT exchanged for an OAuth2 access token (reuses `gcs.js`'s `_signJwt`); admin-scoped (`devstorage.full_control`) so list-buckets + create + delete + lifecycle + CORS all succeed: `create(name, {location?, storageClass?, iamConfiguration?})` (POST `/storage/v1/b?project=`), `delete(name)`, `list({prefix?, maxResults?, pageToken?})`, `setLifecycle(name, rules)` (PATCH bucket with `lifecycle: { rule: [{ action, condition }] }`; rules support `Delete` / `SetStorageClass` / `AbortIncompleteMultipartUpload` actions), `setCorsRules(name, rules)` (PATCH bucket with `cors: [{ origin, method, responseHeader, maxAgeSeconds }]`). **Bucket-name validation** matches each cloud's spec: Azure containers (3-63 lowercase alphanumeric + hyphens, no consecutive hyphens, must start/end with alphanumeric); GCS buckets (3-63 lowercase + digits + hyphens + underscores + dots, no consecutive dots, no `goog` prefix). Bad names are rejected at the call site before the request leaves the process. **404 / 409 semantic mapping** — both modules treat 404 on delete as "already gone" (returns false), 409 on create as `BUCKET_ALREADY_OWNED`; everything else surfaces with the original HTTP status preserved on the thrown ObjectStoreError. **Tests**: 47 layer-0 mock-based assertions for Azure (surface, factory validation, container-name validation, create/delete/list wire shape including XML response parsing, CORS validation + wire shape, setLifecycle NOT_SUPPORTED guidance) + 50 layer-0 mock-based assertions for GCS (surface, factory validation including missing service-account / project, bucket-name validation, create/delete/list wire shape including JWT bearer auth, lifecycle validation + JSON body shape, CORS shape). HTTP mock servers record every request shape so signing, URL params, headers, and body-format are all asserted. **Wiki page** `examples/wiki/seeders/prod/pages/object-store.js` updated end-to-end with all three clouds' bucket-ops examples and the Azure-vs-Resource-Manager lifecycle gap documented. Smoke 6951 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
109
- - **0.6.36** (2026-05-02) — `b.db.from("schema.table")` cross-schema chain. **`b.db.from("audit.events")`**the chainable Query builder now accepts a two-part `schema.table` identifier. Both halves validated separately as SQL identifiers (rejects three-part names, empty parts, identifiers with embedded quotes / SQL keywords); both halves wrapped in `"..."` when interpolated so the generated SQL is `SELECT * FROM "audit"."events" WHERE ...` etc. The bare `b.db.from("users")` form still works unchanged. Sealed-field registry lookup tries the qualified name (`audit.users`) first when schema is set, falls back to the bare tableoperators registering `cryptoField.registerTable("audit.users", { sealedFields: ... })` get per-schema sealed columns; existing table-name-only registrations keep working. Use cases: cross-schema joins on Postgres external-db (`public.users` vs `audit.events` from one app), SQLite `ATTACH DATABASE` (per-classification audit / archive databases mounted on the side and queried through the same Query API). **Tests**: 14 layer-0 SQL-shape assertions against a fake DB (verify schema-qualified SELECT / INSERT / UPDATE / DELETE / count / sub-select-on-rowid all emit `"schema"."table"`; reject three-part / empty / invalid identifiers; bare table name preserved unqualified) + 7 layer-2 end-to-end assertions against a real `ATTACH DATABASE` schema (insert / select / count / update / delete round-trip via `b.db.from("audit.events")`). Smoke 6854 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
110
- - **0.6.35** (2026-05-02) — `cluster-provider-db` MySQL dialect. **`b.cluster.create({ provider: clusterProviderDb.create({ dialect: "mysql", ... }) })`** operators on MySQL no longer have to supply their own provider; the framework's default DB-row leader-election provider now speaks all three of postgres / sqlite / mysql. The MySQL acquireLease shape uses `INSERT INTO _blamejs_leader (...) VALUES (...) ON DUPLICATE KEY UPDATE col = IF(expiresAt < ?, VALUES(col), col), ..., expiresAt = IF(expiresAt < ?, VALUES(expiresAt), expiresAt)` so a still-valid lease is preserved untouched and an expired one is overwritten atomic at the row level followed by a `SELECT FROM _blamejs_leader WHERE scope='leader'` to read who holds (MySQL has no `RETURNING`). The expiresAt assignment runs LAST so per-column IF() predicates evaluate against pre-update row state. Renew uses `UPDATE ... SET expiresAt=?, endpoint=? WHERE scope='leader' AND nodeId=? AND leaseId=?` followed by a check-SELECT to surface takeover races as `LEASE_LOST`. Schema generation: `BIGINT` int columns (was `INTEGER` for SQLite), `VARCHAR(64)` for primary-key text columns and `VARCHAR(255)` for body text (MySQL needs explicit lengths on PRIMARY KEY columns), `CHECK (scope = 'leader' / 'state')` constraint dropped on MySQL because some MariaDB / MySQL 5.x versions parse-then-silently-drop CHECK clauses (would surface as version drift); the constant-scope invariant is enforced by application code anyway. Placeholder style auto-flips to `?` for MySQL (Postgres / SQLite continue to use `$1..$N`). **Tests**: 16 layer-1 assertions exercising the MySQL dialect path against a fake mysql-shaped driver (`_makeFakeMysqlDriver` in `test/helpers/drivers.js`) that emulates `INSERT ... ON DUPLICATE KEY UPDATE` with the per-column `IF()` semantics; covers acquire-empty, blocked-while-held, renew-no-fencing-bump, takeover-with-fencing-bump, old-leader-renew-throws-LEASE_LOST, currentLeader, releaseLease plus a SQL-shape audit (validates VARCHAR primary keys, ON DUPLICATE KEY syntax, ?-placeholders, IF() gating). 14 live integration assertions against the docker MySQL 8.4 container via a docker-exec-based external-db driver shim (no npm mysql client; the framework already requires operator-supplied driver wiring, the shim demonstrates one path) covering ensureSchema, acquireLease, blocked-second-node, currentLeader, renewLease, release, takeover-after-expiry, fencingToken bump, old-leader LEASE_LOST. Smoke 6833 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
111
- - **0.6.34** (2026-05-02) — distributed pub/sub primitive. **`b.pubsub`** — single API for cross-node fan-out across the framework, with three backends. `local` dispatches in-process to registered handlers (zero coordination overhead for single-node deploys); `cluster` polls a shared `_blamejs_pubsub_messages` table at `pollIntervalMs` (default 100ms) and dispatches new rows past `lastSeenId` from other nodes (publishedBy=self filter prevents loopback); `redis` opens a SUBSCRIBE-mode connection on `lib/redis-client.js` (now with new push-message demultiplexing server-pushed `["message", channel, payload]` and `["pmessage", pattern, channel, payload]` arrays route through `setOnPushMessage` instead of consuming a pending request slot, while SUBSCRIBE / UNSUBSCRIBE acks still flow through the normal command pipeline). Per-instance nonce stamped on outgoing redis payloads so the SUBSCRIBE socket recognizes its own publishes and skips the loopback (without it every same-instance publish would fire local handlers twice). Operator API: `ps.subscribe(channel, handler) token`, `ps.subscribePattern(pattern, handler) token` (glob-style on local + cluster, native PSUBSCRIBE on redis), `ps.unsubscribe(token)`, `await ps.publish(channel, payload) → { local, remote }`, `await ps.close()`. `topicPrefix` opt scopes channel names so independent pubsub instances sharing a backend (cache invalidation + websocket channels + custom) don't collide. Handler errors are caught + logged via the framework's boot logger; they never abort dispatch to other handlers on the same channel. **`lib/websocket-channels.js`** — replaces the inline cluster-poll-and-fan-out logic with `b.pubsub` consumption. The hub now owns one pubsub instance per primitive; cross-node delivery is `pubsub.subscribe` per channel the hub joins, with the hub's `_localDispatch` as the handler. Per-channel pubsub subscription is refcounted by local conn count (subscribe on first conn, unsubscribe on last). The `_blamejs_ws_messages` table is renamed to `_blamejs_pubsub_messages` (column `channel` → `topic`) reflecting the generalization; pre-v1, no compat shim — operators upgrading wipe the previous table (which carried only ephemeral fan-out rows with default 60s retention). **`b.cache.create({ invalidationPubsub })`** — passing a `b.pubsub.create()` instance auto-publishes on every successful `del` / `clear` / `invalidateTag`, and subscribes for the same events so other cache instances on other nodes (or processes sharing the pubsub backend) react locally primarily useful for the memory backend so stale per-node entries don't survive a global tag wipe. Re-entrancy guard prevents inbound invalidation events from re-publishing (no fan-out loops). Tests: 29 layer-0 assertions covering local + cluster behavior, topicPrefix isolation, pattern subscribe, handler error isolation, post-close error path, end-to-end cache invalidationPubsub fan-out via local pubsub. 10 live integration assertions against the docker redis container covering single-instance round-trip, PSUBSCRIBE pattern, cross-instance fan-out, and cache invalidation through redis PUB/SUB. Smoke 6817 / wiki e2e 178 / per-primitive integration 15 files / wiki integration 32 / shellcheck clean / eslint clean.
112
- - **0.6.33** (2026-05-02) — log-stream sinks parity sweep. **`b.logStream` syslog sink (RFC 5424)** — first-class `protocol: "syslog"` for `b.logStream.init({ sinks: { ... } })`; new module `lib/log-stream-syslog.js`. URL-driven transport selection: `udp://host:514` / `tcp://host:514` / `tls://host:6514`. UDP is best-effort one-datagram-per-record; TCP / TLS use RFC 6587 octet-counting framing (`<length> <message>`) so collectors that prefer it over the older non-transparent newline framing parse cleanly. TLS is TLS 1.3 minimum; operators with private CAs pass `ca` (PEM string or array) for trust pinning, `servername` for SNI override (auto-suppressed on IP literals per the v0.6.28 redis-client convention). Outgoing records are formatted with PRI = `(facility << 3) | severity`, default facility `local0` (16), severity mapped from the framework's level field (debug=7 / info=6 / warn=4 / error=3); operators override via `facility`, `appName` (default `blamejs`), `procId` (default `process.pid`), `hostname` (default `os.hostname()`), `structuredData` (default `-`). Meta is JSON-encoded into the MSG body so the structured payload survives the wire as a single token. TCP / TLS reconnect with exponential backoff (default 250ms→30s); records buffer during the down window with `bufferLimit`-bounded oldest-drop semantics, replay on reconnect. `close()` waits up to 3s for an in-flight (re)connect to drain the buffer before tearing down without this the slower TLS handshake raced fire-and-forget shutdown emits and silently dropped records. Operator-supplied `onDrop({reason, batch, error})` surfaces every drop class (`overflow` / `udp-send-error` / `write-error` / `sink-closed`). Removed `syslog` from `DEFERRED_PROTOCOLS`. **`b.logStream` cloudwatch sink: `autoCreate`** — pass `{ autoCreate: true }` to have the framework issue `CreateLogGroup` + `CreateLogStream` on first emit so operators provisioning collectors via env vars / runbooks don't need a separate aws-cli step. Idempotent: AWS's `ResourceAlreadyExistsException` is treated as success on both calls. Hard failures (5xx, AccessDenied, etc.) drop the batch with `onDrop` reason `autocreate-failed`. Default remains `autoCreate: false` so the AWS posture (operator pre-creates via aws / CDK / Terraform) stays the recommended path; `autoCreate` exists for ephemeral / dynamic-stream deployments where pre-provisioning is impractical. **Test suite**: 19 new framework-level cloudwatch assertions cover autoCreate fires both Create calls in order before PutLogEvents, ResourceAlreadyExists doesn't abort the post-create PutLogEvents, hard 5xx during CreateLogGroup drops the batch via `onDrop`, autoCreate=false skips Create calls entirely. Live syslog integration covers UDP / TCP wire delivery against the docker syslog-ng container (`/var/log/blamejs-test.log`) plus an in-process `tls.createServer` receiver that asserts the on-the-wire RFC 6587 octet-counting framing + RFC 5424 PRI / timestamp / structured-data slot the framework emits over TLS. **Test infrastructure fix**: `docker/init/generate-certs.sh` was overwriting an existing CA when re-run with `.complete` removed, invalidating every previously-issued leaf cert; now reuses an existing `(ca.crt, ca.key)` pair across `.complete`-only resets and only generates a fresh CA when neither file is present. Smoke 6790 / wiki e2e 178 / per-primitive integration 14 files (log-stream up from 16→18 checks, cloudwatch suite up from 39→58 assertions) / wiki integration 32 / shellcheck clean / eslint clean.
113
- - **0.6.32** (2026-05-02) — E1 from the v0.6.x scope plan. **`b.cache` Redis backend** first-class `backend: "redis"` for `b.cache.create({...})`, no operator-supplied glue needed. New module `lib/cache-redis.js` consumes `lib/redis-client.js` directly; storage layout: `<namespace>:e:<key>` (STRING, JSON-encoded value, PEXPIREAT-bounded), `<namespace>:t:<tag>` (SET, cacheKeys carrying that tag powers `invalidateTag` fan-out), `<namespace>:k:<key>:tags` (SET, tags this key carries powers per-key tag cleanup on `del`/`set`-overwrite, expires alongside the entry). TTL is enforced by Redis itself (PEXPIREAT) so the framework's sweeper is a no-op for this backend; sliding TTL bumps the entry's expiry on every read when `slidingTtl: true` AND `ttlMs` is finite. New opts on `cache.create`: `redisUrl` (required when `backend: "redis"`), `redisPassword`, `redisUsername`, `redisTls`, `redisCa` (private-CA trust pinning per v0.6.28), `redisServername`, `redisConnectTimeoutMs`, `redisCommandTimeoutMs`, `redisMaxReconnectAttempts`. Concurrency: invalidateTag filters out ghost entries (key already PEXPIRE'd from the keyspace but lingering in a tag SET) by EXISTS-checking before del. Lazy connect — `cache.create({backend:"redis"})` stays sync-safe; first op opens the socket. Tests: 10 new live integration assertions on top of the existing memory + custom-backend coverage (set+get round-trip, has, del, complex JSON value round-trip, short-TTL Redis-side expiry, multi-tag invalidateTag fan-out + preservation of untagged entries, wrap() single-flight memoization through Redis). Smoke 6780 / wiki e2e 178 / per-primitive integration 14 files (cache up from 15→25 checks) / wiki integration green.
114
- - **0.6.31** (2026-05-02) — lazy-deferral cleanup wave from v0.6.27. **A1: queue-redis priority ordering** — `b.queue.enqueue({ priority })` on the Redis backend now matches queue-local's `ORDER BY priority DESC, availableAt ASC, enqueuedAt ASC` semantics. The Redis ZSET stays scored by availableAt only (so ZRANGEBYSCORE 0..nowMs cleanly filters ready vs not-yet-ready jobs), but `LEASE_LUA` now over-fetches `maxRows*5` candidates by score, HMGETs priority + availableAt + enqueuedAt for each, sorts server-side in Lua by the same triple queue-local sorts on, and leases the top `maxRows`. Closes the v0.6.27 deferral that punted priority ordering as "ZSET-incompatible". **A2: queue-redis flow `dependsOn` cascade** `b.queue.enqueue({ flowId, flowChildName, dependsOn })` on the Redis backend now releases dependent jobs when their parents complete, mirroring queue-local's `_maybeReleaseFlowChildren`. A per-flow `<prefix>:flow:<flowId>` Redis SET tracks every job in the flow; `complete()` walks the set, looks up siblings whose `dependsOn` includes the just-completed job (by id OR `flowChildName`) AND every other dep is also satisfied, and HSET-bumps their `availableAt` to now + ZADDs them into the ready zset. The flow set persists across `complete()` calls so a later sibling's deps check can still find earlier-done parents by name; `purge()` cleans the set on queue teardown. Closes the v0.6.27 deferral. **A4: WebSocket per-message-deflate (RFC 7692)** `b.websocket.handleUpgrade` negotiates the `permessage-deflate` extension when the client offers it, accepting `client_max_window_bits` / `server_max_window_bits` constraints (8-15 range, default 15) and always asserting `client_no_context_takeover` + `server_no_context_takeover` so every message uses a fresh zlib state. The connection's send path compresses TEXT/BINARY frames via `zlib.deflateRawSync`, strips the 4-byte `0x00 0x00 0xff 0xff` trailer per RFC 7692 §7.2.1, and sets RSV1 on the first frame of each compressed message. The receive path appends the trailer back and inflates via `zlib.inflateRawSync`. RSV1 on a continuation frame, RSV1 without negotiated extension, RSV2/RSV3 set, and decompressed payload exceeding `maxMessageBytes` all close with the appropriate RFC 6455 status (`PROTOCOL_ERROR`, `INVALID_PAYLOAD`, `MESSAGE_TOO_BIG`). Operator opt-out: pass `permessageDeflate: false` to `handleUpgrade` to refuse the extension even when offered. Tests cover the 101-handshake echo of negotiated params, real compression on a redundant payload (4000 bytes ~30 bytes on the wire), uncompressed-frame round-trip on the same connection (mixed-mode per RFC 7692 §6), and unknown-extension graceful ignore. v0.6.27's third lazy deferral A3 (TLS-redis live test) was already covered by `test/integration/redis-client-tls.test.js` shipped in v0.6.28. Smoke 6780 / wiki e2e 178 / per-primitive integration 14 files green / wiki integration green.
115
- - **0.6.30** (2026-05-02) — wiki-app integration gate + framework follow-ups surfaced by it. **`scripts/test-wiki-integration.js`** boots `examples/wiki` against the docker-compose fixture stack (real Redis / MinIO / Mailpit / CoreDNS / NTP / mtls-ca) and drives every backend through the wiki's HTTP surface AND the underlying framework primitives validates that each primitive routes through the configured backend (cache custom backend, queue → Redis Streams, mail → Mailpit SMTP, object-store → MinIO sigv4, log-stream → webhook receiver) AND that every fix shipped in v0.6.28 holds in a real-app context, not just in unit-test isolation. **Wiki gains test-only routes** under `/test/*` (gated by `WIKI_INTEGRATION_TEST=1`, mounted before CSRF/staticServe so test POSTs don't have to round-trip a token cookie) covering cache get/set/del, queue enqueue/size, mail send, object-store put/get, http-client fetch, log-stream emit, mtls-ca issue, ntp query, dns lookup, ssrf classify+check, plus `/test/diagnostic` that surfaces the active backend posture (network snapshot, queue backends, log sinks, mtls algorithm). **External-integration two-gate rule** — when a release diff touches a primitive that talks to an external service (`lib/redis-client.js`, `lib/queue-redis.js`, `lib/mail.js`, `lib/network-dns.js`, `lib/object-store/*`, `lib/log-stream*.js`, `lib/external-db.js`, `lib/cluster-*.js`, `lib/mtls-ca.js`, `lib/ssrf-guard.js`, `lib/http-client.js`, `lib/ntp-check.js`, `lib/cache.js`, `lib/webhook.js`), operators MUST run BOTH `scripts/test-integration.js` (per-primitive against real backends) AND `scripts/test-wiki-integration.js` (wiki app exercising the same backends end-to-end) before pushing. Documented in CONTRIBUTING.md's pre-push gate list and CLAUDE.md's release workflow as step 4a. The smoke + wiki-e2e gates above stay PURE (no docker dependency, runs in CI / on a developer laptop / inside prepack-guard); the two-gate is operator-run, opt-in, and surfaces bugs that mocks miss fire-and-forget races, shutdown drains, TLS pinning, real DNS resolution, real protocol handshakes. **Framework fixes surfaced by the new gate**: `b.mtlsCa.create` now auto-creates `opts.dataDir` with `mode: 0o700` if it doesn't exist (matches `b.logStream`'s local sink, `b.backup`, `b.restoreBundle` — without it the first `initCA()` call hit `ENOENT` writing `ca.key.tmp`); `b.logStream` webhook-sink `close()` now waits for the in-flight `_flush()` before draining the buffer (the v0.6.28 drain fix only tracked emit-time wrapper promises, not the actual HTTP POST in flight; a record arriving mid-flush would buffer + the emit-time `_flush` early-returned on `if (inFlight) return`, and shutdown would then strand it). `b.queue.bootFromEnv()` is now wired in the wiki app's `build-app.js` unconditionally so the wiki picks up `BLAMEJS_QUEUE_PROTOCOL=redis` from env without code changes (was previously a documented-but-commented call site). **Test architecture rule (extends the v0.6.28 rule)** live tests for the framework's primitives go under `test/integration/`; live tests for the wiki app exercising the framework end-to-end go under `examples/wiki/test/integration.js`. Smoke (`test/smoke.js`) and the existing wiki e2e (`examples/wiki/test/e2e.js`) stay pure. Smoke / wiki-e2e green; per-primitive integration suite green; wiki integration green. Eslint pin alignment + shellcheck added to the documented gate list (carry-over from v0.6.29).
116
- - **0.6.29** (2026-05-02) — CI cleanup. **shellcheck gate green**: `docker/init/generate-certs.sh`'s footer line counted output via `ls "$CERT_DIR" | wc -l` (SC2012 fragile on filenames with newlines or quoting metacharacters); replaced with `find "$CERT_DIR" -maxdepth 1 -mindepth 1 | wc -l`. Same semantics, robust to any cert filename pki-init might end up writing. **shellcheck added to the documented pre-push gate list** in CONTRIBUTING.md alongside smoke / wiki e2e / eslint / api-snapshot / primitive-section validators CI's `Lint summary` job has always run it, but the local-dev recipe didn't, so contributors hit it post-push instead of catching it locally. **eslint pin alignment**: `.github/PULL_REQUEST_TEMPLATE.md` and CONTRIBUTING.md still pinned `eslint@10` even though v0.6.16 swapped CI runners to `eslint@latest`; both bumped to `@latest` to match the forward-track posture of every other CI tool. No framework code changes; the script + doc updates never ship in the npm package (excluded by `files` array).
117
- - **0.6.28** (2026-05-02) — bug-finding pass: live integration test suite + every framework bug it caught. **`b.ssrfGuard.checkUrl` cloud-metadata hard-deny** `allowInternal: true` no longer permits 169.254.169.254 (AWS / GCP / Azure metadata). Loopback / private / link-local / reserved stay allowInternal-overridable per existing semantics; cloud-metadata is unconditional because a blanket override would let any compromised request exfiltrate instance credentials. **`b.mtlsCa` issues server certs and dual-EKU certs** `generateClientCert({ usage: "client" | "server" | "both", sans: [...] })`. `usage` controls EKU (`client` = clientAuth, `server` = serverAuth, `both` = both); `sans` accepts `DNS:` / `IP:` / bare-DNS entries; serverAuth without an explicit SAN auto-adds the CN. Operators wiring inbound mTLS reverse-proxy fronts no longer hit "unsuitable certificate purpose" on the handshake. **`b.mtlsCa` algorithm auto-detect** the engine probes webcrypto + the vendored x509 library at first cert issuance and picks the highest-PQC option that round-trips: SLH-DSA-SHAKE-256f → SLH-DSA-SHAKE-128f ML-DSA-87 ML-DSA-65 ECDSA-P384-SHA384 (current bridge what the X.509 ecosystem accepts today). When the bridge condition lifts and the vendor refresh ships, the same `b.mtlsCa.create(...)` call self-upgrades; `b.mtlsCa.status().cert.label` and `.posture` surface the chosen algorithm. **`b.redisClient.create({ ca, servername })`** — managed-Redis / on-prem-cluster operators connecting over `rediss://` against a private CA can pin trust roots; `servername` auto-suppresses for IP literals so `rediss://127.0.0.1:6380` no longer trips node:tls's IP-as-SNI rule. **`b.network.dns.useDnsOverTls` / `useDnsOverHttps` accept `ca`** — same trust-pinning surface for self-signed / private-PKI DoT and DoH endpoints. **DoT TLS handshake errors** now route as DnsError on the lookup promise rather than leaking as `unhandledRejection` — the secureConnect / error event-listener race had let cert-verification failures escape the per-query Promise wrapping. **DoT socket lifecycle** — was unconditionally `sock.unref()`'d after construct, causing node to exit during in-flight lookups when no other I/O kept the loop alive; now ref'd while a query is in flight and unref'd when idle. **`b.ntpCheck.querySingle` honours IPv6 servers** was hardcoded to `dgram.createSocket("udp4")`, so `::1` / `fd00::…` queries failed with EINVAL; now auto-detects family from the server string. **`b.logStream.shutdown` drains in-flight emit microtasks** before closing sink fds — fire-and-forget emits queued just before shutdown were silently dropped because `close()` ran ahead of the microtask. **`b.logStream` webhook-sink `close()` flushes BEFORE setting `closed = true`** — `_flush`'s while loop bailed on `!closed`, leaving any records buffered just before shutdown stranded. **Integration test suite** (`test/integration/`, runner at `scripts/test-integration.js`) — 13 live test files across `b.redisClient` plain + TLS, `b.queue` Redis backend lifecycle, `b.mail` SMTP + STARTTLS + multi-rcpt + dot-stuffing transparency, `b.mail.dkim` rsa-sha256 + ed25519-sha256 + bad-algorithm reject, `b.ntpCheck` v4 + v6 + bootCheck + bad-host + bad-port, `b.network.dns` plain + DoT + DoH with strict CA pinning + bad-servername + cache, `b.network.heartbeat` http + tcp + state-change callbacks, `b.objectStore` sigv4 PUT / GET / list / delete on plain HTTP + TLS variants, `b.cache` memory + Redis-backed cluster, `b.httpClient` direct + Squid forward proxy + TLS pin, `b.ssrfGuard` classify + checkUrl + cloud-metadata block, `b.logStream` local + webhook + deferred-syslog error path, `b.mtlsCa` CA bootstrap + clientAuth + serverAuth + dual-EKU live mTLS handshakes. Companion `docker-compose.test.yml` stands up redis (plain + TLS) / postgres / mysql / mongo / minio (HTTP + HTTPS variants) / rabbitmq (plain + TLS) / nats / syslog / ntp / mailpit / coredns (plain + DoT + DoH) / haproxy / caddy / mitmproxy / squid / pki-init (auto-generates Ed25519 CA + per-service leaf certs into a docker volume); host port bindings dual-stack on `127.0.0.1` AND `[::1]`. `scripts/check-services.js` does host-side TCP / TLS handshake / SNTPv4 / DNS-A / Redis-PING / SMTP-banner / HTTP probes across every endpoint; `scripts/test-integration.js` exports the test CA via `docker cp` and sets `NODE_EXTRA_CA_CERTS` per-test child process so the framework's TLS verification stays strict (no `rejectUnauthorized: false` bypass anywhere in the test surface). **Test architecture rule** — live tests go in `test/integration/`, NEVER in `test/layer-N-*/`: smoke must remain pure (no docker dependency, runs in CI / on a developer laptop / inside prepack-guard) and a "skip silently when service is down" branch in a layer-N test makes the gate's pass count misleading and masks bugs that only surface against a live backend. The `0.6.27` `queue-redis.test.js` was moved to `test/integration/queue-redis.test.js` to enforce this. Smoke and wiki e2e green; integration suite ships green against the docker-compose stack.
118
- - **0.6.27** (2026-05-02) — Redis backend for `b.queue` so multi-replica apps can share a single queue without each needing to be cluster leader. **Bespoke RESP2 client** (`lib/redis-client.js`) zero npm runtime deps. TCP via `node:net` + TLS via `node:tls` (`rediss://` auto-detected), legacy single-arg AUTH + ACL `AUTH user pass`, `SELECT db`, pipelining, exponential-backoff reconnect, EVAL helper. **`b.queue` protocol "redis"** (`lib/queue-redis.js`) full enqueue/lease/extendLease/complete/fail/sweepExpired/size/purge/dlqList/dlqRetry/dlqSize parity with the local backend. Atomicity comes from server-side Lua scripts so concurrent consumers can't double-lease and a sweep can't race a complete. Storage layout: per-job HASH (sealed payload + lastError via `cryptoField.sealRow("_blamejs_jobs", row)` same crypto config as the local backend), per-queue ready ZSET scored by availableAt, per-queue inflight ZSET scored by leaseExpiresAt, per-queue dlq ZSET scored by finishedAt, plus a queues SET so sweepExpired walks every known queue without a global secondary index. Cron-repeat handled in `complete()` JS — re-enqueues the next firing as a fresh jobId with availableAt=next-cron-fire. **`b.queue.bootFromEnv({ env })`** — env-driven init mirroring `b.network.bootFromEnv` and `b.logStream.bootFromEnv`. Reads `BLAMEJS_QUEUE_PROTOCOL` (`local`|`redis`), `BLAMEJS_QUEUE_REDIS_URL`, `BLAMEJS_QUEUE_REDIS_PASSWORD`, `BLAMEJS_QUEUE_REDIS_USERNAME`, `BLAMEJS_QUEUE_REDIS_TLS`, `BLAMEJS_QUEUE_REDIS_KEY_PREFIX`. Operators flip from local to Redis without a code change; both wiki docker-compose configs declare the new env knobs. Removed `redis` from `DEFERRED_PROTOCOLS`. **Out of scope for v1** (deferred to follow-up patches with explicit re-open conditions): Redis Cluster (slot-routing), Sentinel (managed primary failover), priority ordering on the Redis backend (queue-local supports `priority` opt; Redis backend orders strictly by availableAt for v1), flow children with `dependsOn` cascade. **Wiki**: queue-cache page documents the Redis backend opts schema, bootFromEnv, and the layout. **Tests**: 24 RESP2 protocol parser unit tests (`test/layer-0-primitives/redis-client.test.js`) cover URL parsing, command encoding (binary-safe), every reply type (simple string / error / integer / bulk / nil bulk / nested array / pipelined / incomplete-mid-frame). Live Redis round-trip tests (`test/layer-0-primitives/queue-redis.test.js`) cover enqueue+lease, availableAt scheduling, visibility-timeout sweep, fail+retry path, DLQ list/retry/size, extendLease, purge, and concurrent-leaser no-double-lease — skip cleanly when `BLAMEJS_TEST_REDIS_URL` is not set so the smoke suite passes on dev boxes without a Redis container. Smoke 6780 OK.
119
- - **0.6.26** (2026-05-02) — `blamejs restore` and `blamejs audit verify-chain` subcommands wrap the existing `b.restore` and `b.audit.verifyChain` primitives so operators can drive them from runbooks without writing app code. **`blamejs restore`**: `list` (enumerate bundles in storage), `inspect` (manifest summary without touching live data), `apply` (live in-place restore with rollback preserved), `rollback` (revert to most-recent OR named restore point), `list-rollbacks` (enumerate preserved rollback points). Two ways to identify a bundle `--bundle <dir>` matches the shape `blamejs backup extract` produces (parent dir is treated as storage root, basename as bundle id), or `--storage-root <root> --bundle-id <id>` for multi-bundle stores. `apply` honors `--max-pulled-bytes` / `--max-pulled-files` (defaults 4 GiB / 100K), `--rollback-root` (default `<data-dir>.rollbacks`), `--no-audit`, and `BLAMEJS_BACKUP_PASSPHRASE` env. **`blamejs audit verify-chain`**: walks the live audit chain end-to-end, reports tampering with `breakAt` / `breakRowId` / expected-vs-actual prevHash; honours `--max-rows` to bound long walks; default table is `audit_log`. **Wiki CLI snapshot test now validates subcommand pairs**, not just top commands. Walks every wiki + README invocation of the form `blamejs <cmd> <sub>` and verifies that <sub> exists in the perCommand[<cmd>].subcommands list parsed from `lib/cli.js`. Surfaced one real drift on first run: `examples/wiki/seeders/prod/pages/backup-restore.js` documented `blamejs audit verify-signing` after vault rotation, but no such subcommand existed (only `verify-bundle`); fixed by shipping the new `verify-chain` subcommand and updating the wiki to reference it. Two top-level command gaps surfaced and fixed: `blamejs restore` (now real) and `blamejs network status` (was prose-promised in `network-config.js`; reworded to point at `b.network.snapshot()` for /healthz / custom diagnostics routes since wiring a CLI command for it is operator-side work). README CLI table updated with `restore` row and the two new audit subcommands.
120
- - **0.6.25** (2026-05-02) — `b.logStream` gains an AWS CloudWatch Logs sink AND framework-level env wiring. **CloudWatch sink**: `protocol: "cloudwatch"` POSTs `PutLogEvents` over HTTPS with SigV4 signing (service `logs`); operator pre-creates the log group + log stream (the framework does NOT auto-create). Honors IAM role + STS session-token credentials. Respects all three CloudWatch caps automatically: 10,000 events / 1 MiB total payload / 256 KiB-per-event. Per-event oversize dropped at `emit()`-time with `onDrop` fired (truncated message in the drop notification). Per-batch oversize split mid-flush. Permanent AWS errors (`ResourceNotFoundException` / `AccessDeniedException` / `InvalidParameterException` / `UnrecognizedClientException` / `SerializationException`) skip the retry budget. `InvalidSequenceTokenException` (legacy CW accounts) extracts the expected token from the error message and retries with it once. `lib/object-store/sigv4.js` `signRequest()` is now service-agnostic accepts `opts.service` (default still `"s3"` for back-compat). Removed `cloudwatch` from `DEFERRED_PROTOCOLS`. **`b.logStream.bootFromEnv({ env })`**: framework-level env-driven init mirroring `b.network.bootFromEnv`. Reads `BLAMEJS_LOG_STREAM_PROTOCOL` (`local`/`webhook`/`otlp`/`cloudwatch`), `BLAMEJS_LOG_STREAM_URL`, `BLAMEJS_LOG_STREAM_TOKEN`, `BLAMEJS_LOG_STREAM_SERVICE_NAME`, `BLAMEJS_LOG_STREAM_SERVICE_VERSION`, `BLAMEJS_LOG_STREAM_CLOUDWATCH_LOG_GROUP`, `BLAMEJS_LOG_STREAM_CLOUDWATCH_LOG_STREAM`, `BLAMEJS_LOG_STREAM_PATH`, plus standard AWS_*. Operators get a working log-stream sink without writing build-app code. Wiki app's `build-app.js` replaced its inline env-reading with one `b.logStream.bootFromEnv()` call; both docker-compose configs declare the new env knobs. **Wiki env-snapshot test**: parallel to api-snapshot.json — walks `process.env.X` / `env.X` / `safeEnv.readVar("X")` reads in the wiki app + framework `lib/`, walks docker-compose env declarations, captures the union as `examples/wiki/env-snapshot.json`, fails the e2e gate when env vars are added/removed without updating the snapshot OR when source-only / compose-only gaps appear (env knob declared but unread, env read but undocumented). The validator immediately surfaced 13 real gaps in the wiki app: 5 `WIKI_*` env vars read by source but missing from docker-compose (`WIKI_VAULT_MODE`, `WIKI_DB_AT_REST`, `WIKI_AUDIT_SIGNING_MODE`, `WIKI_BIND`, `WIKI_SITE_URL`), 2 framework env vars (`BLAMEJS_AUDIT_SIGNING_MODE`, `BLAMEJS_TMPDIR`) read by `lib/db.js` via `safeEnv.readVar` but never declared in the wiki's compose configs, and 6 dead env knobs in compose that nothing read. All 13 fixed. Update workflow: `BLAMEJS_UPDATE_ENV_SNAPSHOT=1 node examples/wiki/test/validate-env-snapshot.js` (mirrors the api-snapshot UX). Wiki observability page documents the new sink alongside webhook + otlp; README "What ships in the box" calls out all four log-stream sinks. Tests cover endpoint resolution, event-byte accounting, batch sorting + sequence-token round-trip, permanent-error classifier, validation, round-trip via mock CloudWatch, STS session-token propagation, ResourceNotFoundException no-retry path, 256 KiB per-event hard cap, dispatcher integration, AND batch splitting on the 1-MiB cap (5 quarter-MB events POST as 4 + 1 batches).
121
- - **0.6.24** (2026-05-02) — `b.logStream` gains an OTLP/HTTP-JSON sink. `protocol: "otlp"` now forwards log records to any OpenTelemetry collector via the OTel Logs Data Model `resourceLogs` `scopeLogs` `logRecords` envelope with `severityNumber` (debug=5/info=9/warn=13/error=17), `severityText`, `timeUnixNano` (string-encoded for JSON-safe 64-bit), `body.stringValue`, and OTel-typed attributes. Operator config: `{ url, serviceName, serviceVersion, resourceAttributes, auth, headers, batchSize, retry, onDrop, ... }` same back-pressure semantics as the webhook sink (per-sink ring buffer, batched flush on size or maxBatchAgeMs, exponential-backoff retry, drop-on-overflow with operator-supplied onDrop callback). URL convention: `/v1/logs` is auto-appended when the operator passes the collector root. JSON not protobuf operators benchmarking >100K logs/s ship the OTel Collector locally so the framework hands JSON to a sidecar that forwards via gRPC. Removed `otlp` from `DEFERRED_PROTOCOLS`. Wiki observability page documents the new sink with a `b.logStream.init` example pointing at an OTel collector. Tests cover URL resolution, attribute encoding (string / int / float / bool / array / nested object), severity mapping, round-trip via mock collector, auth header pass-through, retry on 5xx + drop on retry-exhaustion, buffer overflow, dispatcher integration. Verified 0 leaks across 291 commits via `gitleaks`.
122
- - **0.6.23** (2026-05-02) — v0.6.22 follow-up cleanup. The cron-repeat call site in queue-local.js was passing both `availableAt` AND `delaySeconds` (the former was the precise next-fire ms; the latter was a redundant `Math.floor((nextMs - nowMs) / 1000)` computation that only existed to work around the bug v0.6.22 fixed). Dropped the cron repeat now passes `availableAt` alone, matching the queue's documented precedence rule. The enqueue() docstring gains a 20-line "SCHEDULING PRECEDENCE" header documenting that `opts.availableAt` wins over `opts.delaySeconds` when both are passed, why the framework chose that direction, and which callers should use which form. New round-trip preservation regression test (`testEnqueueRoundTripsAvailableAt`) covers three precise targets, the delaySeconds-only path, and the both-opts-set case gates against any future "I'll just rederive it from the floored seconds" mistake. Audited the rest of the framework for the same `(absolute-time, relative-time)` opt-overlap shape (cache.set, session.rotate, apiKey, dualControl): queue is the only primitive carrying both forms, so a generalized `b.time.resolveTimePoint` primitive would be premature with one call site.
123
- - **0.6.22** (2026-05-02) — `b.queue.enqueue({ availableAt })` is now honoured. Previously the local-protocol enqueue() ignored opts.availableAt entirely, recomputing from `Date.now() + delaySeconds*1000`. The cron-repeat path passes both fields (the exact next-fire ms in availableAt + the floored seconds in delaySeconds), and the enqueue's recomputation lost sub-second precision plus drifted on the internal clock-vs-caller delta. Symptoms: cron-scheduled jobs landed up to 999ms off the intended boundary; the queue-flow-repeat smoke test was intermittently flaky on slow CI runners (caught by ubuntu-latest on the v0.6.21 commit). Fix: enqueue() honours opts.availableAt directly when finite; falls back to delaySeconds-based shorthand otherwise. Operators relying on `enqueue({ availableAt: T })` for non-cron scheduled jobs (e.g. "deliver this notification at exactly 09:00 tomorrow") now get the requested time instead of nowMs+0.
124
- - **0.6.21** (2026-05-02) — closes the medium / low audit findings flagged after v0.6.18. **Cluster cache `invalidateTag`** now actually works — the cluster backend gained a `_blamejs_cache_tags` junction table (`(cacheKey, tag)` PK + index on `tag`), tag-aware `set` / `del` / `clear` / `_sweep`, plus `getTags(key)`. The old NOT_SUPPORTED-on-cluster path is gone; multi-tag rotation, mid-flight tag replacement on update, and namespace-scoped sweeps all covered. **Multi-column cursor pagination** `b.pagination.cursor({ orderBy: [{column,direction},...] })` accepts a string (single column), an array of strings (multi, all using `opts.direction`), or an array of `{column,direction}` objects (mixed directions). The keyset WHERE expands to the standard OR cascade so successive pages can't skip or repeat rows when ties on the leading columns are broken by trailing ones. `_id` is appended as a tiebreaker if not in the chain. The Query class also gained chained `orderBy(col, dir)` calls second-and-later calls extend a multi-column ORDER BY in the SQL. Cursor format is bumped to encode `{ orderKey, vals, forward }` instead of the old `{ orderBy, dir, orderByVal, id, forward }` — pre-1.0 break, no compat shim. **DoH POST mode** (RFC 8484 §4.1) — `b.network.dns.useDnsOverHttps({ method })` accepts `"GET" | "POST" | undefined` (auto). Auto switches to POST when the GET URL would exceed 2048 bytes (long DNS names). **DoT connection pooling** — per-`(host:port)` cached TLS socket with a 2-minute idle timeout, serialized in-flight queries per socket. Eliminates the per-query handshake. **INI parser** shipped as `b.parsers.ini` covers Windows .ini / .gitconfig / systemd-unit / php.ini / tox.ini shape: sections (incl. `[parent.child]` / `[parent "child"]` nesting), `;` and `#` comments (inline + leading), single + double quoting with `\n` `\t` `\\` `\"` `\'` escapes, boolean coercion (`true`/`false`/`yes`/`no`/`on`/`off`), decimal + hex integers + floats. Prototype-pollution defense (`__proto__` / `constructor` / `prototype` rejected). Duplicate-key policy throws by default; `onDuplicate: "first" | "last"` opts in to silent shadowing. Section / per-section key / value-bytes caps configurable. **Cookie-jar file persistence** `b.httpClient.cookieJar.create({ persist: "file", file: "/abs/path", vault: b.vault })` loads at construct, debounce-flushes on every set/clear, plus `flush()` and `close()` for explicit lifecycle. With `vault`, on-disk bytes are sealed; without, plaintext JSON (operator chooses). **Comment cleanups per rule §4** — `vault/index.js` stale `// later` removed, `mail.js` "future patch" wording rewritten as scope, `bundler.js` "What it does NOT do today (deliberately deferred)" rewritten as "Out of scope", `archive.js` "v1 scope cuts (deferred)" rewritten as "Out of scope", `framework-schema.js` "next release" wording rewritten to describe what's actually shipped, `http-client.js` "out of reach today" / "we'll plumb it through when h3 lands" rewritten as scope statements. **README CLI section** updated to reflect the v0.6.17 + v0.6.18 + v0.6.19 + v0.6.20 additions (security / config-drift / file-type / password / erase / retention) — was missing 6 subcommands.
125
- - **0.6.20** (2026-05-02) — CI / packaging fix-up. The npm-publish workflow's "Attach SBOM as release asset" step started failing with HTTP 422 ("Cannot upload assets to an immutable release") because the operator's manual `gh release create` had already been published when the workflow ran. Two changes: (1) `sbom.cyclonedx.json` is now bundled into the npm tarball (`files` block in package.json), so `npm install @blamejs/core && cat node_modules/@blamejs/core/sbom.cyclonedx.json` is the canonical SBOM access path. The prepack guard's known-allowed list covers the just-in-time generation. (2) The workflow's GH-release-attach step is now non-fatal: it tries to upload, logs a warning if the release is immutable, and lets the publish proceed regardless. The npm tarball is the load-bearing artifact; the GitHub release attachment was only ever supplementary. `.gitignore` adds `sbom.cyclonedx.json` so a stray local `npm sbom` doesn't pollute the repo.
126
- - **0.6.19** (2026-05-02) — closes the critical + high gaps surfaced by the v0.6.18 audit. **Critical**: `b.network.ntp.nts.querySingle` now actually verifies the server reply with the s2cKey extracts the AUTHENTICATOR_AND_ENC extension, AEAD-decrypts with AAD = bytes-before-authenticator, fails closed (`nts/auth-failed` / `nts/no-authenticator`) when verification fails. Server-supplied new cookies in the encrypted plaintext are appended to the cookie pool and the consumed cookie popped (real RFC 8915 cookie rotation). Previously the function returned `authenticated: true` while only checking the unique-identifier echo any MITM that mirrored the request's 32-byte unique field could spoof timestamps. **High**: `azure-blob.presignedUploadPolicy` now throws `PRESIGN_NOT_SUPPORTED` instead of silently returning a SAS PUT URL when operators asked for POST policy semantics Azure SAS has no body-size cap and the previous shape was a misleading mismatch (operator error message points at presignedUploadUrl + post-upload HEAD as the alternative). `b.auth.password.policy` now ships the SecLists top-10000 common-password list bundled (CC-BY-3.0, `lib/vendor/common-passwords-top-10000.txt`), loaded lazily on first `policy.check()` call; `password` / `dragon` / `qwerty` / etc. now reject with `policy/forbidden-common`; `useBundledCommon: false` per-policy bypasses if operator ships their own list. `b.network.tls` adds `removeCa(fingerprint256)` / `removeCaByLabel(label)` / `clearAll()` / `purgeExpired()` / `expiringSoon(windowMs)` so operators can rotate corp DPI CAs without process restart; every removal audits with subject + fingerprint + reason. `b.network.dns.setResultOrder("ipv6first")` now flips the order on the DoH / DoT dual-stack fallback paths too (was only sorting OS-resolver results). `b.network.dns.resolve4` / `resolve6` / `resolveAaaa` now use real DNS-protocol queries (`dns.promises.resolve4` / `_dohLookup` / `_dotLookup`) instead of aliasing `lookup()` operator semantics now match Node's standard library (skips `/etc/hosts`, mDNS). `b.network.socket.setDefaultLinger` removed from the silent no-op path; now throws `socket/linger-not-supported` with operator guidance to use `socket.destroy()` (abort) vs `socket.end()` (graceful) since Node's public `net.Socket` has no `setLinger()`. **Internal**: every `new XxxError(...)` call across `lib/network*.js` was passing args in the wrong order (message-then-code instead of code-then-message), making `e.code` return human messages and `e.message` return slash-codes operators relying on `e.code` for error handling got the wrong field. All 50+ throws fixed across `lib/network.js` / `network-dns.js` / `network-proxy.js` / `network-tls.js` / `network-heartbeat.js` / `network-nts.js`. **CI**: `.gitleaks.toml` adds `test/smoke.js` to the path allowlist and pins the historical commit + fingerprint that tripped the jwt rule on a `REDACTED` placeholder; `npm-publish.yml` job permissions bumped from `contents: read` to `contents: write` so `gh release upload sbom.cyclonedx.json` no longer 403s.
127
- - **0.6.18** (2026-05-02) — `b.network` primitive single namespace for runtime-configurable network behaviour. `b.network.ntp` adds tunable warn / fatal drift thresholds, env-var bindings (`BLAMEJS_NTP_SERVERS` / `BLAMEJS_NTP_TIMEOUT_MS` / `BLAMEJS_NTP_DRIFT_WARN_MS` / `BLAMEJS_NTP_DRIFT_FATAL_MS`), and authenticated-time support: `b.network.ntp.nts.query(opts)` performs an NTS-KE handshake (RFC 8915) over TLS 1.3 with the framework's PQC-hybrid group preference, extracts C2S / S2C keys via the standardized TLS exporter, and authenticates NTPv4 packets with AES-SIV-CMAC-256 (mandatory-to-implement, in-house) or AEAD-CHACHA20-POLY1305 — no extra vendored deps. `b.network.dns` exposes operator-pinned resolvers, IPv4 / IPv6 / dual-stack family selection, ipv4first / verbatim / ipv6first ordering, DNS lookup timeout (Node's native `dns.lookup` has none), in-memory positive + negative cache, and DoH / DoT (cloudflare / google / quad9 / custom URL) — `b.ssrfGuard` and `b.httpClient` route through it when configured. `b.network.proxy` honours `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` / `ALL_PROXY` (lower- and upper-case) with CIDR + suffix + wildcard `NO_PROXY` matching, basic-auth via `BLAMEJS_PROXY_AUTH`, CONNECT tunnels for HTTPS through HTTP proxies; `b.httpClient` picks up the agent automatically. `b.network.tls` is a runtime-overridable trust store: `addCa(pemOrPath)` / `addCaBundle(path)` / `useSystemTrust()` / `getTrustStore()` for deep-packet-inspection deploys behind Zscaler / Netskope / corporate Squid + custom CA — Node's `NODE_EXTRA_CA_CERTS` only works at boot; this primitive accepts adds at any time and `b.pqcAgent` picks them up immediately. Every `addCa` audits with subject + issuer + fingerprint256 + validity + isSelfSigned. `b.security.assertProduction({ allowDpiTrust })` refuses to boot in production with installed CAs unless explicitly allowed. `b.network.heartbeat` adds application-level liveness probes for upstream services (HTTP / TCP / NTP probe types, healthy degraded down state machine with consecutive-failure threshold, audit on state change, observability counters per probe). `b.network.socket` gives operator-tunable defaults for `TCP_NODELAY` / `SO_KEEPALIVE` / `SO_LINGER`. `b.network.bootFromEnv()` reads every supported env var at startup and applies in the right order so configuration takes effect before the first outbound socket; the wiki app's docker-compose configs ship every knob with a default-empty value (production overlay tightens DNS lookup timeout + cache TTL + `NTP_STRICT=1` + `SOCKET_NO_DELAY=1`). `.gitleaks.toml` was previously gitignored by the deny-all-dotfiles allowlist; it is now allowlisted so CI's secret-scan job loads the framework's allowlist and stops failing on `.gitleaks.toml: no such file or directory`. New wiki page `/network-config` documents all six sub-primitives.
128
- - **0.6.17** (2026-05-02) — six new CLI subcommands wrapping the v0.6.14 primitives so operators can drive them from runbooks / cron / one-off ops: `blamejs security assert` (boot the framework + run `b.security.assertProduction()` against the live posture; aggregates failures with their codes, exit 1 on any failure), `blamejs config-drift inspect/verify` (read the signed sidecar without rebooting; `verify` returns exit 1 on tamper or missing), `blamejs file-type detect <file>` (magic-byte content classification with `--allowlist` for upload debugging pure utility, no boot), `blamejs password check --plaintext "..."` (test `b.auth.password.policy` with `--profile pci-4.0` / `nist-aal2` / `hipaa-aal2`, `--breach-check` for HIBP, `--email` / `--username` context pure utility), `blamejs erase --table X --row-id Y --confirm` (one-off cryptographic erasure for GDPR Art. 17 replaces sealed columns + derived hashes with NULL + audits via `system.erase`), `blamejs retention preview/run` (ad-hoc rule from CLI flags: `--table` + `--age-field` + `--ttl-ms` + `--action`, with `preview` for dry-run reporting). Also: per-subcommand `--help` now reaches each handler's USAGE block (the previous main() dispatch short-circuited every `<sub> --help` to top-level help); fix applies to all existing subcommands too (`api-key --help`, `vault --help`, etc.). The `b.dualControl` primitive is intentionally NOT in the CLI surface its grants live in an operator-supplied `b.cache` instance the CLI can't bind to without operator wiring; admins approve / revoke from the operator's app instead. Hadolint action pinned to `@master` (the published `@v3` floating tag doesn't exist; the maintainer ships only patch tags + master).
129
- - **0.6.16** (2026-05-02) — CI secret-scan job switched from `gitleaks/gitleaks-action` (which started requiring a paid `GITLEAKS_LICENSE` for organization repositories, breaking the v0.6.14 publish workflow) to a direct install of the OSS gitleaks binary (Apache-2.0); resolves the latest release at job time so new ruleset improvements ship automatically. Same forward-track posture applied to other CI tools where pinning would silently miss security improvements: `aquasecurity/trivy-action` swapped from `@v0.36.0` to `@master`, `hadolint/hadolint-action` from `@v3.3.0` to the floating `@v3` major tag, `ludeeus/action-shellcheck` from `@2.0.0` to `@master`, ESLint runner from `eslint@10` to `eslint@latest` in both `ci.yml` and `npm-publish.yml`. Vendored crypto libraries (@noble/ciphers, @simplewebauthn/server, argon2, peculiar-pki) verified at latest upstream no bundle refresh needed. No framework code changes; npm publish workflow unblocked for v0.6.14 / v0.6.15 / this release.
130
- - **0.6.15** (2026-05-02) — wiki sidebar nav: every concern group is now a `<details>` section starting collapsed; the section containing the current page is rendered with `open` server-side so the operator's current location is always visible. Custom CSS-only disclosure glyph (▸ via rotation) works under the wiki's strict CSP without inline JS. Wiki app gains three new env knobs that ship with the docker-compose configs out of the box: `WIKI_TRUST_PROXY` (cookie Secure-flag detection through TLS terminator; pre-existing in v0.6.12 lib code, now in docker-compose with documentation), `WIKI_ADMIN_ALLOWED_CIDRS` / `WIKI_ADMIN_DENIED_CIDRS` (in-process CIDR fence on `/admin` paths via `b.middleware.networkAllowlist`), `WIKI_REQUIRE_PROD_ASSERTS` (boot-time `b.security.assertProduction()` gate that refuses to boot when production posture is incomplete). The production overlay (`docker-compose.prod.yml`) defaults the latter two on (TRUST_PROXY=1 since Caddy terminates TLS, REQUIRE_PROD_ASSERTS=1 so a misconfigured prod deploy fails fast); the dev overlay defaults them off.
131
- - **0.6.14** (2026-05-02) — new operator-facing primitives, each shipping the v1-defensible compliance surface: `b.security.assertProduction(opts)` boot-time policy engine (vault / dbAtRest / auditSigning posture, NTP strict, Node minimum major, TLS minimum version, required + forbidden env vars, NODE_ENV pinning, dataDir POSIX-mode check, CORS-allow-all detection, operator-supplied extra asserts); `b.permissions.policy(scope, predicate)` ABAC layer that evaluates per-scope predicates after RBAC passes (single + requireAll + requireAny modes); `b.permissions` role-spec `requireMfa` / `mfaWindowMs` AND per-route MFA enforcement; `b.session` IP/UA fingerprint capture + drift detection with operator-supplied scorer for anomaly scoring (strict modes: `requireFingerprintMatch` AND `maxAnomalyScore`); `b.auth.password.policy(opts)` with NIST 800-63B / PCI-DSS 4.0 / HIPAA-AAL2 named profiles, length / common / context / dictionary / complexity (categories + min-run + min-sequence) rules, HIBP k-anonymity breach check (SHA-1 lives in `lib/internal-sha1-hibp.js`, NOT exported on `b.crypto`), rotation (`shouldRotate`) and history-reuse (`reuseProhibited`); `b.dualControl.create(opts)` two-person-rule with m-of-n quorum, cooling-off lock between approval and consume, approver-role gate, requester cancellation, minimum reason length, notification hook; `b.retention.create(opts)` with multi-stage rules (warn → archive → erase), legal-hold per-row exemption, dry-run `preview()`, soft-delete vs hard-delete vs erase, cross-table cascade, per-rule concurrency lock; `b.configDrift.create(opts)` with multi-baseline support, critical-keys severity classification, ignore-keys allowlist, signed sidecar via SLH-DSA + diff vs prev snapshot; `b.fileType.detect/.assertOneOf` magic-byte content classification (image / document / archive / executable / etc.); `b.middleware.networkAllowlist({ paths, allowedCidrs, deniedCidrs })` deny-then-allow CIDR fence; `b.httpClient.request({ allowedHosts })` with exact / suffix / wildcard / per-method entries and audit emit on disallowed; `b.cryptoField.eraseRow(table, row)` cryptographic-erasure helper for sealed columns + derived hashes. CI gains gitleaks secret scan + CycloneDX SBOM via `npm sbom` (no vendoring). README + SECURITY operator-checklist updated end to end for the new surface; wiki gains alerting-rule reference table for the framework's emitted events. Backwards-compatible every new opt is opt-in.
132
- - **0.6.13** (2026-05-02) — wiki primitive-section docs catch up to the v0.6.12 surface (`safeUrl.parse` allowUserinfo, `session.touch` extendBy ceiling, `queue.consume` rateLimit validation, `mail.transports.console` redactBcc, `logStream` webhook-sink onDrop, `backup.create` requireFlush, `restore.create` maxPulledBytes / maxPulledFiles); restore default cap stated as `C.BYTES.gib(4)` instead of a raw byte literal; numeric-check predicates (`isPositiveInt`, `isFiniteNonNegative`, `isPositiveFinite`) consolidated into `lib/numeric-checks.js` were duplicated across api-key, cache, notify, queue, restore, retry, slug, testing, webhook; api-snapshot.json refreshed
133
- - **0.6.12** (2026-05-02) — `b.safeUrl.parse` now rejects URLs with `user:pass@` userinfo by default (opt in per-call via `allowUserinfo: true`); `b.session.touch({ extendBy })` enforces the same `MAX_TTL_MS` ceiling as `create` / `rotate`; `b.queue.consume({ rateLimit })` rejects negative / zero / `NaN` / `Infinity` / fractional `max`; `b.middleware.requireAuth` no longer treats request `Content-Type: application/json` as a JSON-preference signal (only `Accept` and `X-Requested-With` count); `b.backup.create({ requireFlush: true })` opt-in fails the backup if pre-flush fails instead of producing a stale snapshot; `b.restore.create({ maxPulledBytes, maxPulledFiles })` preflight bounds bundle footprint before and after pull (defaults 4 GiB / 100K files); `b.mail.transports.console({ redactBcc: true })` opt-in prints recipient count instead of addresses; `b.logStream.transports.webhook({ onDrop })` callback fires on overflow + retry-exhausted batch drops. Wiki: admin login wired through `b.auth.lockout` (exponential-backoff after bad-cred attempts) and cookie `Secure` flag now routes through `b.requestHelpers.requestProtocol` with `WIKI_TRUST_PROXY` opt-in instead of trusting raw `x-forwarded-proto`. Wiki README documents the trust model for editable page bodies and the sanitization pattern operators should adopt before expanding the editor surface.
134
- - **0.6.11** (2026-05-01) — wiki example-execution validator: fixture init no longer reaches across module realms (unblocks the npm-publish workflow's wiki-e2e gate, which `npm install --install-links` copies the framework into the wiki's node_modules two distinct singletons before this fix)
135
- - **0.6.10** (2026-05-01) — README / SECURITY / CONTRIBUTING / wiki: removed stale version stamps and an inaccurate vendored-dep list; SECURITY now points at `lib/vendor/MANIFEST.json` for the authoritative vendor list; supported-versions table no longer pins to a specific minor; wiki archive example names the digest variable correctly (was `sha256`, output is SHA3-512 hex)
136
- - **0.6.9** (2026-05-01) — b.archive.zip().digest() returns a SHA3-512 hex string (was SHA-256); operators reconciling against an external SHA-256 must hash the bytes themselves
137
- - **0.6.8** (2026-05-01) — wiki primitive-section validator: presence + opts diff + example execution against canonical fixture
138
- - **0.6.7** (2026-05-01) — db.role.switched audit + per-role metrics + API snapshot baseline
139
- - **0.6.6** (2026-05-01) — request-time DB role binding + Postgres RLS migrations
140
- - **0.6.5** (2026-05-01) — b.db.declareView + b.externalDb.migrate
141
- - **0.6.4** (2026-05-01) — wiki schema docs realigned with the actual lib API
142
- - **0.6.3** (2026-05-01) — externalDb pool tuning + role-aware connect + read-replica routing
143
- - **0.6.2** (2026-05-01) — input validation + identifier-quoting consistency
144
- - **0.6.1** (2026-05-01) — security tightenings + operator-facing jargon sweep
145
- - **0.6.0** (2026-05-01) — wiki restructured into 22 focused pages + missing-primitive coverage
146
-
147
- ## v0.5.x
148
-
149
- - **0.5.18** (2026-05-01) — bypass-fix sweep: route through existing primitives
150
- - **0.5.17** (2026-05-01) — primitive-drift sweep: csv unify + 3 new shared primitives
151
- - **0.5.16** (2026-04-30) — b.otelExport: OTLP/HTTP exporter for b.observability
152
- - **0.5.15** (2026-04-30) — b.archive: ZIP creation
153
- - **0.5.14** (2026-04-30) — b.time: timezone-aware datetime arithmetic + formatting
154
- - **0.5.13** (2026-04-30) — b.testing.request: supertest-style chainable HTTP test helper
155
- - **0.5.12** (2026-04-30) — b.middleware.requestLog: HTTP access-log middleware
156
- - **0.5.11** (2026-04-30) — b.config: schema-validated environment configuration
157
- - **0.5.10** (2026-04-30) — b.middleware.sse: Server-Sent Events
158
- - **0.5.9** (2026-04-30) — b.csv: RFC 4180 parser + serializer
159
- - **0.5.8** (2026-04-30) — b.uuid: RFC 4122 v4 + RFC 9562 v7
160
- - **0.5.7** (2026-04-30) — defensive validation + queue closure capture + audit context
161
- - **0.5.6** (2026-04-30) — break-glass: trustProxy honored, cache require hoisted
162
- - **0.5.5** (2026-04-30) — strict default CSP + IPv6 special-range expansion
163
- - **0.5.4** (2026-04-30) — close SSRF DNS-rebinding window with pinned outbound DNS
164
- - **0.5.3** (2026-04-30) — security cleanup: trustProxy primitive + Vary merge + HSTS gate
165
- - **0.5.2** (2026-04-30) — b.breakGlass: passkey factor + service-account bypass + admin tools
166
- - **0.5.1** (2026-04-30) — b.breakGlass: per-cell encryption + context binding + migrate
167
- - **0.5.0** (2026-04-30) — b.breakGlass: column-policy / row-enforcement step-up auth
168
-
169
- ## v0.4.x
170
-
171
- - **0.4.29** (2026-04-30) — primitive-drift sweep: second-pass remediation
172
- - **0.4.28** (2026-04-30) — primitive-drift sweep: inline-require hoisting + safeAsync.sleep + time-math
173
- - **0.4.27** (2026-04-30) — primitive-drift sweep: regex + escape consolidation, IPv6 completion
174
- - **0.4.26** (2026-04-30) — primitive-drift sweep: middleware audit context + safeUrl
175
- - **0.4.25** (2026-04-30) — b.objectStore.bucketOps: bucket-level operations (SigV4)
176
- - **0.4.24** (2026-04-30) — b.objectStore: multipart upload + server-side encryption
177
- - **0.4.23** (2026-04-30) — b.mail.dkim signing + calendar invites
178
- - **0.4.22** (2026-04-30) — b.mail attachments + inline images + plain/HTML alternatives
179
- - **0.4.21** (2026-04-30) — b.queue: repeat-in-queue (cron) + parent-child Flows
180
- - **0.4.20** (2026-04-30) — b.queue + b.jobs: priority, rate-limit, progress
181
- - **0.4.19** (2026-04-30) — b.router: schema-validated routes + OpenAPI gen
182
- - **0.4.18** (2026-04-30) — cookieJar forensic-test strengthening (real crypto, replay, nonce)
183
- - **0.4.17** (2026-04-30) — b.httpClient.cookieJar (encrypted) + wiki catch-up sweep
184
- - **0.4.16** (2026-04-30) — b.httpClient: interceptors + progress events
185
- - **0.4.15** (2026-04-30) — b.httpClient: redirect-following + outbound multipart
186
- - **0.4.14** (2026-04-30) — b.i18n: lazy locales + ordinal plurals + onMissingKey hook
187
- - **0.4.13** (2026-04-30) — b.db: streaming query results
188
- - **0.4.12** (2026-04-30) — b.log: multi-sink output with per-sink level filtering
189
- - **0.4.11** (2026-04-30) — b.cache: bytes-cap eviction, sliding TTL, tag invalidation
190
- - **0.4.10** (2026-04-30) — bodyParser multipart: fileFilter + per-field maxBytes/mimeTypes
191
- - **0.4.9** (2026-04-30) — Origin-Agent-Cluster + DNS-Prefetch-Control headers; b.auth.lockout primitive
192
- - **0.4.8** (2026-04-30) — wiki SEO surface: per-page OG / Twitter / JSON-LD + sitemap.xml + robots.txt
193
- - **0.4.7** (2026-04-30) — audit-fix the welcome page's "what's in the box" table
194
- - **0.4.6** (2026-04-30) — wiki gets the brand-flare on every page + substantive content additions
195
- - **0.4.5** (2026-04-30) — canonical domain is blamejs.com (was blamejs.app everywhere)
196
- - **0.4.4** (2026-04-30) — document BLAMEJS_AUDIT_SIGNING_PASSPHRASE everywhere
197
- - **0.4.3** (2026-04-30) — b.ssrfGuard primitive (default-on in httpClient) + wiki posture auto-detect
198
- - **0.4.2** (2026-04-30) — npm keywords
199
- - **0.4.1** (2026-04-29) — wiki bot-guard skips /healthz so the post-publish smoke check passes
200
- - **0.4.0** (2026-04-29) — bench suite + drops the deprecated b.logger.createLogger
201
-
202
- ## v0.3.x
203
-
204
- - **0.3.39** (2026-04-29) — MIGRATING.md generator scans deprecate calls in lib/
205
- - **0.3.38** (2026-04-29) — LTS-CALENDAR.md publishes the major-cadence + algorithm posture
206
- - **0.3.37** (2026-04-29) — bundled pure-JS mTLS engine + vendor-update.sh + CHANGELOG + thanks page
207
- - **0.3.36** (2026-04-29) — prepack guard refuses to publish any path that matches a gitignore rule
208
- - **0.3.35** (2026-04-29) — wiki home page hero with logo + neon-magenta brand accents
209
- - **0.3.34** (2026-04-29) — rewrite api-snapshot wiki section in plain operator voice + fix SECURITY.md flag
210
- - **0.3.33** (2026-04-29) — doc-sweep folding the v0.3.32 mtls CLI references + filling the wiki gap on api-snapshot
211
- - **0.3.32** (2026-04-29) — blamejs mtls CLI (status / show-cert / init / issue / issue-p12) + framework posture fix on cert fingerprinting
212
- - **0.3.31** (2026-04-29) — npm-publish: defensive NPM_TOKEN check + bin/blamejs.js executable bit committed
213
- - **0.3.30** (2026-04-29) — wiki binds 0.0.0.0 by default so docker -p forwarding actually reaches the listener
214
- - **0.3.29** (2026-04-29) — npm-publish workflow + Dockerfile data-skeleton dir
215
- - **0.3.28** (2026-04-29) — Dockerfile: switch runtime --chown from name to numeric UID 65532:65532
216
- - **0.3.27** (2026-04-29) — Dockerfile pre-builds public/dist in deps stage so runtime container boots cleanly under --cap-drop ALL
217
- - **0.3.26** (2026-04-29) — operator-facing env-var surface, GitHub-side ops docs, contribution standards
218
- - **0.3.25** (2026-04-29) — env-var surface documented across Dockerfile, docker-compose.prod, DEPLOY.md, README; smoke-test GHCR login fix
219
- - **0.3.24** (2026-04-29) — CI + release-container workflows brought (structured output, multi-arch, cosign, post-publish smoke)
220
- - **0.3.23** (2026-04-29) — _runVault and _runBackup refactored to use b.cliHelpers (sweep complete across the three CLI commands the helper was designed for)
221
- - **0.3.22** (2026-04-29) — blamejs api-key CLI; b.cliHelpers shared primitive for headless app + reporter shape
222
- - **0.3.21** (2026-04-29) — blamejs backup CLI: inspect / verify / extract; backup encryption format documented
223
- - **0.3.20** (2026-04-29) — drop redundant Windows-flaky audit.flush perf assertion
224
- - **0.3.19** (2026-04-29) — wiki docs for the unwired primitives surface
225
- - **0.3.18** (2026-04-29) — blamejs vault CLI: seal / unseal / rotate / status
226
- - **0.3.17** (2026-04-29) — validateOpts coverage extended to remaining operator-facing primitives (i18n, bundler, migrations, seeders, template, jobs, cookies, log, mtlsCa, staticServe, handlers, websocket, pqcGate, tracing, metrics — 15 more factories now reject typo'd opts at boot). Coverage is now ~39/lib primitives with the helper; the remaining handful (app-shutdown, chain-writer, dev, error-page, nonce-store, protocol-dispatcher, render, session, pqc-agent) are either internal-only — operators don't construct them directly — or have a single required-key surface where typo guards add no value.
227
- - **0.3.16** (2026-04-29) — config validation, CSRF cookie mode, HTML balance, dynamic Cache-Control, URL canonicalization
228
- - **0.3.15** (2026-04-29) — cors: accept Origin: null when Sec-Fetch-Site is same-origin (browsers send opaque Origin: null on form-navigation POSTs from a page whose response sets Referrer-Policy: no-referrer; the Fetch-metadata Sec-Fetch-Site header is the matching disambiguation signal same-origin or none means accept, anything else stays refused)
229
- - **0.3.14** (2026-04-29) — Dockerfile hotfix + lint as release gate
230
- - **0.3.13** (2026-04-29) — CI lint scans + Wolfi container + Trivy two-step + lint cleanup
231
- - **0.3.12** (2026-04-29) — version bump for hotfix (rebuild container image)
232
- - **0.3.11** (2026-04-29) — CI/CD: GitHub Actions for smoke + wiki e2e, GHCR container publish on tag, Caddy production deploy overlay
233
- - **0.3.10** (2026-04-29) — framework correctness fixes: compression backpressure, cspNonce cacheable-render API, cors same-origin, audit namespaces
234
- - **0.3.9** (2026-04-28) — wiki: Docker Compose deploy
235
- - **0.3.8** (2026-04-28) — wiki: remaining four concern-group pages
236
- - **0.3.7** (2026-04-28) — wiki: HTTP & Middleware and Crypto & Vault pages
237
- - **0.3.6** (2026-04-28) — wiki: Auth & Permissions and Storage & State pages
238
- - **0.3.5** (2026-04-28) — strip stale comments and template language from sources
239
- - **0.3.4** (2026-04-28) — wiki: Welcome and Observability pages
240
- - **0.3.3** (2026-04-28) — wiki bundler integration + admin editor enhancements
241
- - **0.3.2** (2026-04-28) — wiki wires notify / scheduler / apiKey / webhook
242
- - **0.3.1** (2026-04-28) — wiki design pass
243
- - **0.3.0** (2026-04-28) — examples/wiki reference app
244
-
245
- ## v0.2.x
246
-
247
- - **0.2.39** (2026-04-28) — sweep auditCaptured + fakeNow patterns to b.testing
248
- - **0.2.38** (2026-04-28) — b.testing
249
- - **0.2.37** (2026-04-28) — b.notify
250
- - **0.2.36** (2026-04-28) — b.i18n
251
- - **0.2.35** (2026-04-28) — b.seeders
252
- - **0.2.34** (2026-04-28) — b.cache
253
- - **0.2.33** (2026-04-28) — audit captures the 5 W's; success on by default when audit is wired
254
- - **0.2.32** (2026-04-28) — close api-key visibility holes
255
- - **0.2.31** (2026-04-28) — auditSuccess split: precursor vs complete-event
256
- - **0.2.30** (2026-04-28) — b.permissions
257
- - **0.2.29** (2026-04-28) — observability backfill across primitives
258
- - **0.2.28** (2026-04-28) — credential hash envelope versioning
259
- - **0.2.27** (2026-04-28) — b.apiKey + repo logo assets
260
- - **0.2.26** (2026-04-28) — b.webhook signing + verification
261
- - **0.2.25** (2026-04-28) — b.slug
262
- - **0.2.24** (2026-04-28) — b.retry primitive
263
- - **0.2.23** (2026-04-28) — test helpers consolidate
264
- - **0.2.22** (2026-04-28) — input validation
265
- - **0.2.21** (2026-04-28) — defineClass.factory replaces per-module _err wrappers
266
- - **0.2.20** (2026-04-28) — b.requestHelpers + metrics _normalizeLabelArg
267
- - **0.2.19** (2026-04-28) — audit.safeEmit replaces per-module _emit wrappers
268
- - **0.2.18** (2026-04-28) — b.protocolDispatcher
269
- - **0.2.17** (2026-04-28) — b.observability.tap
270
- - **0.2.16** (2026-04-28) — index.js header lists complete public surface
271
- - **0.2.15** (2026-04-28) — request middleware captures statusCode, promotes span to route template
272
- - **0.2.14** (2026-04-28) — router populates req.routePattern
273
- - **0.2.13** (2026-04-28) — OAuth generic preset
274
- - **0.2.12** (2026-04-28) — built-in metrics + framework tracing taps
275
- - **0.2.11** (2026-04-28) — storage POST-form policy presigning + logger deprecation warning
276
- - **0.2.10** (2026-04-28) — close api-encrypt audit gaps
277
- - **0.2.9** (2026-04-28) — cluster-shared nonce-store + audit handler isolation
278
- - **0.2.8** (2026-04-28) — end-to-end PQC payload encryption middleware
279
- - **0.2.7** (2026-04-27) — websocket channel/room hub with cluster pub/sub fan-out
280
- - **0.2.6** (2026-04-27) — mail-bounce intake primitive
281
- - **0.2.5** (2026-04-27) — strip narrative comments
282
- - **0.2.4** (2026-04-27) — fold logger into log + rate-limit cluster backend
283
- - **0.2.3** (2026-04-27) — route raw literals through C.TIME / C.BYTES, type bare throws
284
- - **0.2.2** (2026-04-27) — close gaps in the 0.2.1 scheduler and presign work
285
- - **0.2.1** (2026-04-27) — scheduler exactly-once-globally + storage presigned upload URLs
286
- - **0.2.0** (2026-04-27) — minor bump + queue lease-extension + Jobs DLQ
287
-
288
- ## v0.1.x
289
-
290
- - **0.1.111** (2026-04-27) — modular per-file test layout
291
- - **0.1.110** (2026-04-27) — session.rotate + migrations advisory lock
292
- - **0.1.109** (2026-04-27) — auth.oauth: OAuth 2 / OIDC client
293
- - **0.1.108** (2026-04-27) — app-shutdown: graceful-shutdown orchestrator
294
- - **0.1.107** (2026-04-27) — tracing: OpenTelemetry seam
295
- - **0.1.106** (2026-04-27) — metrics: Prometheus-format counters/gauges/histograms
296
- - **0.1.105** (2026-04-27) — csp-nonce middleware + render integration
297
- - **0.1.104** (2026-04-27) — pagination: cursor + offset helpers
298
- - **0.1.103** (2026-04-27) — compression middleware
299
- - **0.1.102** (2026-04-27) — health endpoint primitive
300
- - **0.1.101** (2026-04-27) — body-parser middleware
301
- - **0.1.100** (2026-04-27) — safe-schema: declarative input validator
302
- - **0.1.99** (2026-04-27) — events: breach-detection signal bus (audit-tools slice B)
303
- - **0.1.98** (2026-04-27) — audit-tools (slice A): archive + export + verify-bundle + purge
304
- - **0.1.97** (2026-04-27) — backup: encrypted-at-rest support db.flushToDisk + recommendedFiles
305
- - **0.1.96** (2026-04-27) — audit cleanup A4: folder-level adjustments + DeprecateError rename
306
- - **0.1.95** (2026-04-27) — audit cleanup A3: standardize require ordering across major lib files
307
- - **0.1.94** (2026-04-27) — audit cleanup A2 (cont.): rename import bindings, public namespace, error classes
308
- - **0.1.93** (2026-04-27) — audit cleanup A2: rename libs to prefix-grouped names pre-v1.0 LTS lock-in
309
- - **0.1.92** (2026-04-27) — audit cleanup A1: lift duplicated helpers + frameworkError.defineClass
310
- - **0.1.91** (2026-04-27) — api-snapshot: public API surface walker + breaking-change detectorslice 8b
311
- - **0.1.90** (2026-04-27) — deprecate: runtime deprecation API for the LTS contract — slice 8a
312
- - **0.1.89** (2026-04-27) — restore + restore-rollback: storage-backed restore + atomic swapcomplete
313
- - **0.1.88** (2026-04-27) — backup: orchestration with pluggable storage + retention — slice 7d
314
- - **0.1.87** (2026-04-27) — restore-bundle: extract encrypted bundle to stagingslice 7c
315
- - **0.1.86** (2026-04-27) — backup-bundle: produce an encrypted backup bundle on disk — slice 7b
316
- - **0.1.85** (2026-04-27) — backup-manifest: bundle schema + validate/parse/serialize — slice 7a
317
- - **0.1.84** (2026-04-27) — backup-crypto: Argon2id KDF + XChaCha20-Poly1305 for backup files — slice 7
318
- - **0.1.83** (2026-04-27) — mtls-ca: CA file management with engine-pluggable issuance — slice 6
319
- - **0.1.82** (2026-04-27) — vault-passphrase-ops: seal/unseal/rotate operator primitives — slice 5
320
- - **0.1.81** (2026-04-27) — vault-rotate.rotate: full rotation pipeline — slice 4
321
- - **0.1.80** (2026-04-27) — vault-rotate (diagnostics): schema-drift + round-trip verify — slice 3
322
- - **0.1.79** (2026-04-27) — drop SecP256r1MLKEM768 from default TLS group preference
323
- - **0.1.78** (2026-04-27) — pqc-agent: locked-posture HTTPS agent + http-client refactor — slice 2
324
- - **0.1.77** (2026-04-27) — pqc-gate: TCP-level PQC enforcement on ClientHello — slice 1
325
- - **0.1.76** (2026-04-27) — bundler: content-hashed asset pipeline + manifestcomplete
326
- - **0.1.75** (2026-04-27) — dev: file-watch + child-process restart engine — slice 6 (complete)
327
- - **0.1.74** (2026-04-27) — cli: bin/blamejs + migrate up/down/status slice 5
328
- - **0.1.73** (2026-04-27) — migrations: public up/down/status runner — slice 4
329
- - **0.1.72** (2026-04-27) — cookies: parse/serialize + sealed-value access gate — slice 3
330
- - **0.1.71** (2026-04-27) — errors-page: rich dev page + safe prod page; middleware shim — slice 2
331
- - **0.1.70** (2026-04-27) — log: structured JSON logging with request-id correlation — slice 1
332
- - **0.1.69** (2026-04-27) — scheduler: cron + interval over jobs/queue — slice 5 (complete)
333
- - **0.1.68** (2026-04-27) — mail: generalize http transport, demote resend to thin preset
334
- - **0.1.67** (2026-04-27) — mail: contract + console/memory/smtp/resend transports — slice 4
335
- - **0.1.66** (2026-04-26) — jobs (define + enqueue + in-process worker) + createApp wiring — slices 2/3
336
- - **0.1.65** (2026-04-26) — createApp factory — slice 1
337
- - **0.1.64** (2026-04-26) — forms + csrfProtectslice 4 (complete)
338
- - **0.1.63** (2026-04-26) — staticServe: file serving + ETag + SRI integrity — slice 3
339
- - **0.1.62** (2026-04-26) — render: HTTP response helpers — slice 2
340
- - **0.1.61** (2026-04-26) — template engine (eval-free interpreter) — slice 1
341
- - **0.1.60** (2026-04-26) — auth.jwt with SLH-DSA-SHAKE-256f default — slice 5/5 (complete)
342
- - **0.1.59** (2026-04-26) — middleware.attachUser + middleware.requireAuth — slice 4
343
- - **0.1.58** (2026-04-26) — auth.passkey (WebAuthn / FIDO2) — slice 3
344
- - **0.1.57** (2026-04-26) — TOTP defaults match spec (HMAC-SHA512 / 128-byte secret / 8 digits / 30s) + crypto.random truncation fix
345
- - **0.1.56** (2026-04-26) — auth.totp: flip default to HMAC-SHA512, drop SHA-1 support
346
- - **0.1.55** (2026-04-26) — auth.totp / lib/totp.js (RFC 6238) slice 2
347
- - **0.1.54** (2026-04-26) — audit-sign algorithm-agility + flip default to SLH-DSA-SHAKE-256f
348
- - **0.1.53** (2026-04-26) — auth.password (Argon2id) — slice 1/4
349
- - **0.1.52** (2026-04-26) — consent_log integrity parity with audit_log (closes the cluster spec storage move)
350
- - **0.1.51** (2026-04-26) — queue jobs move to external-db in cluster mode
351
- - **0.1.50** (2026-04-26) — sessions move to external-db in cluster mode
352
- - **0.1.49** (2026-04-26) — boot-time vault-key consistency check (closes cluster spec)
353
- - **0.1.48** (2026-04-26) — boot-time audit-tip rollback detection in cluster mode
354
- - **0.1.47** (2026-04-26) — fix handlers.drain unbounded loop on recursive emit (cluster-mode audit hang)
355
- - **0.1.46** (2026-04-26) — cluster-mode audit-tip fencing-token guard
356
- - **0.1.45** (2026-04-26) — cluster discovery surface for service-mesh / load-balancer routing
357
- - **0.1.44** (2026-04-26) — repeating-pattern sweep: sleep + withTimeoutSignal + auth-header + listenOnRandomPort
358
- - **0.1.43** (2026-04-26) — url-safe protocol validator at outbound boundary
359
- - **0.1.42** (2026-04-26) — router.closeWebSockets + activeWebSockets fundamental fix
360
- - **0.1.41** (2026-04-26) — smoke runner gains groups + fixtures + per-test timing
361
- - **0.1.40** (2026-04-26) — router.ws integration + WebSocket lifecycle redesign
362
- - **0.1.39** (2026-04-26) — h2 WebSocket (RFC 8441 Extended CONNECT) added to websocket.js
363
- - **0.1.38** (2026-04-26) — lib/websocket.js (RFC 6455 server primitive)
364
- - **0.1.37** (2026-04-26) — atomicFile.listDir primitive + sweep 5 lib sites
365
- - **0.1.36** (2026-04-26) — testConstantsReferenceIntegrity uses framework primitives
366
- - **0.1.35** (2026-04-26) — fix stale C.TIME.FIVE_MIN refs + add integrity test
367
- - **0.1.34** (2026-04-26) — document h3-ready transport cache + TLS session resumption stance
368
- - **0.1.33** (2026-04-26) — HTTP/2 backend behind same httpClient.request surface
369
- - **0.1.32** (2026-04-26) — http-client primitive (h1 baseline) + sweep 5 adapters
370
- - **0.1.31** (2026-04-26) — logger primitive + sweep log/logErr/console sites
371
- - **0.1.30** (2026-04-26) — framework-error base class + sweep operational _err factories
372
- - **0.1.29** (2026-04-26) — lazy-require primitive + sweep 12 modules
373
- - **0.1.28** (2026-04-26) — adopt bufferSafe.secureZero in passphrase + key-wrap paths
374
- - **0.1.27** (2026-04-26) — bufferSafe primitive + sweep parsers/atomic-file/object-store
375
- - **0.1.26** (2026-04-26) — envSafe.readVar primitive + sweep direct process.env reads
376
- - **0.1.25** (2026-04-26) — dedup audit-sign passphrase logic into passphrase-source
377
- - **0.1.24** (2026-04-26) — Layer 5 extraction + smoke.js as pure orchestrator
378
- - **0.1.23** (2026-04-26) — Layer 4 extraction: consumer modules → 40-consumers.js
379
- - **0.1.22** (2026-04-26) — Layer 3 extraction: chain-writing modules 30-chain.js
380
- - **0.1.21** (2026-04-26) — Layer 2 extraction: db + framework-schema 20-db.js
381
- - **0.1.20** (2026-04-26) — Layer 1 extraction: vault + cluster + framework-schema 10-state.js
382
- - **0.1.19** (2026-04-26) — Layer 0 extraction: atomic-file + parsers + redact 00-primitives.js
383
- - **0.1.18** (2026-04-26) — Layer 0 extraction: json-safe tests 00-primitives.js
384
- - **0.1.17** (2026-04-26) — Layer 0 extraction: async-safe + handlers tests → 00-primitives.js
385
- - **0.1.16** (2026-04-26) — first Layer 0 extraction (sql-safe + chain-writer → 00-primitives.js)
386
- - **0.1.15** (2026-04-26) — extract test/_helpers.js (foundation for layer-file split)
387
- - **0.1.14** (2026-04-26) — smoke runner reordered by dependency layer (primitives first, consumers last)
388
- - **0.1.13** (2026-04-26) — sql-safe + chain-writer primitives; consent gets the audit Mutex fix as a side effect of consolidation
389
- - **0.1.12** (2026-04-26) — async-safe + handlers hardening: AbortSignal, Once.reset, isRetryable override, timer-unref bug fix, full primitive test coverage
390
- - **0.1.11** (2026-04-26) — handlers primitive replaces fire-and-forget audit emission
391
- - **0.1.10** (2026-04-26) — async-safe library + audit dispatch via cluster-storage
392
- - **0.1.9** (2026-04-26) — cluster storage step 2: SQL dispatcher (lib/cluster-storage.js)
393
- - **0.1.8** (2026-04-26) — cluster storage step 1: framework-state schema for external-db
394
- - **0.1.7** (2026-04-26) — .env safe loader (lib/parsers/env-safe.js)
395
- - **0.1.6** (2026-04-26) — YAML 1.2 safe-subset parser (lib/parsers/yaml-safe.js)
396
- - **0.1.5** (2026-04-26) — TOML 1.0 safe parser (lib/parsers/toml-safe.js)
397
- - **0.1.4** (2026-04-26) — xml-safe parser adopted in object-store list responses
398
- - **0.1.3** (2026-04-26) — atomicFile.readSync for sync init paths
399
- - **0.1.2** (2026-04-26) — rename b.json public API to b.jsonSafe
400
- - **0.1.1** (2026-04-26) — rename HA cluster (no cryptic acronyms in framework names)
401
- - **0.1.0** (2026-04-26) — HA write-side gates wired across framework subsystems
402
-
403
- ## v0.0.x
404
-
405
- - **0.0.19** (2026-04-26) — HA core: leader election + fencing tokens (operator-facing primitive)
406
- - **0.0.18** (2026-04-26) — strip internal-process narrative from framework comments
407
- - **0.0.17** (2026-04-25) — internal adoption sweep + functional scale helpers
408
- - **0.0.16** (2026-04-25) — atomic file I/O + safe parsers (XML, CSV)
409
- - **0.0.15** (2026-04-25) — HTTP middleware: request lifecycle hardening (begin)
410
- - **0.0.14** (2026-04-25) — external DB (Shape B: app-data-only, bring-your-own-client)
411
- - **0.0.13** (2026-04-25) — log streaming + redaction + bidirectional command channel
412
- - **0.0.12** (2026-04-25) — queue dispatcher + local SQLite-backed protocol
413
- - **0.0.11** (2026-04-25) — gcs + azure-blob protocol adapters (object-store complete)
414
- - **0.0.10** (2026-04-25) — sigv4 protocol adapter (S3 + R2 + B2 + MinIO + Wasabi + DO Spaces + ...)
415
- - **0.0.9** (2026-04-25) — generic remote data layer + classification-routed bifurcation
416
- - **0.0.8** (2026-04-25) — full tamper-proof bar (signed checkpoints, rollback detection)
417
- - **0.0.7** (2026-04-25) — traceability hardening + dynamic PK/FK schema declarations
418
- - **0.0.6** (2026-04-25) — security-focused JSON parsing + schema validation
419
- - **0.0.5** (2026-04-25) — session & storage local backend
420
- - **0.0.4** (2026-04-25) — audit chain, consent log, subject rights
421
- - **0.0.3** (2026-04-25) — db, query builder, field-crypto, migrations
422
- - **0.0.2** (2026-04-25) — vault & key management
423
- - **0.0.1** (2026-04-25) — foundation
1
+ # Changelog
2
+
3
+ One entry per released tag, grouped by minor. Latest first.
4
+
5
+ Pre-1.0 the surface is intentionally evolving — every release may
6
+ change something operators depend on. Read each entry before
7
+ upgrading across more than a few patches at a time.
8
+
9
+ ## v0.7.x
10
+
11
+ - **0.7.19** (2026-05-05) — auth-primitives batch (5 fixes): session idle/absolute timeout, JWT keyResolver for kid rotation, `b.middleware.bearerAuth`, `b.auth.jwt.verifyExternal` (classical-alg JWT verifier with JWKS support), and Argon2id parameter audit visibility. **Session idle + absolute timeouts** (`b.session.verify`) now enforce OWASP ASVS 5.0 §3.3 / NIST SP 800-63B-4 defaults: 30 min idle, 12 hours absolute. Operators opt out per-call by passing `idleTimeoutMs: 0` / `absoluteTimeoutMs: 0`. Both surface `auth.session.expired_idle` / `auth.session.expired_absolute` audit events on enforcement. **JWT `keyResolver` opt** (`b.auth.jwt.verify`) operator passes `keyResolver(decodedHeader)` to look up the public key per-token (typically by `kid`). Mutually exclusive with `opts.publicKey`. Async-friendly. Closes the kid-rotation gap where signers carried `kid` but verifiers only accepted a single static key. **`b.middleware.bearerAuth`** new middleware that extracts `Authorization: Bearer <token>`, runs an operator-supplied `verify(token)` function, and attaches `req.user`. Distinct from cookie-session `attachUser`. Missing-Authorization passes through (so cookie path can take over); invalid/null/throw rejects 401 + `WWW-Authenticate: Bearer error="invalid_token"` per RFC 6750 §3. **`b.auth.jwt.verifyExternal`** new generic classical-alg JWT verifier (RS256 / RS384 / RS512 / PS256 / PS384 / PS512 / ES256 / ES384 / ES512 / EdDSA) for integration with external IdPs (Auth0 / Okta / Keycloak / Cognito / Azure AD / Google / Apple). `algorithms` is REQUIRED with no default — defends the alg-confusion class (CVE-2024-54150 / CVE-2025-30144 / CVE-2026-22817 Hono). HMAC algs and `none` are explicitly refused (HMAC + JWKS public-key trust source IS the alg-confusion vector). Three key-source options: `jwks` (pre-fetched array), `jwksUri` (auto-fetched + TTL-cached via `b.httpClient` SSRF gate), `keyResolver` (custom). Standard claim checks (`exp` / `nbf` / `iat` / `aud` / `iss` / `sub`) with operator-tunable `clockSkewMs`. **`b.auth.password.params()`**new accessor returning the active Argon2id params (`memoryCostKib` / `timeCost` / `parallelism`) plus the OWASP 2026 floor (`19 MiB` / `t>=2` / `p>=1`) plus `meetsFloor: bool`. Compliance-audit visibility without parsing PHC strings. Smoke 84588481 / wiki e2e 178 / Linux container smoke 8481 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
12
+
13
+ - **0.7.18** (2026-05-05) — transport-layer smuggling hardening (four ship-blocker fixes). **HTTP request-smuggling defense** in `b.middleware.bodyParser` per RFC 9112 §6.1: rejects requests with both `Content-Length` and `Transfer-Encoding` headers (CL.TE / TE.CL smuggling CVE-2022-31394 / CVE-2024-27316 class), multiple `Content-Length` values, `Transfer-Encoding` whose final coding is not `chunked`, and duplicate `chunked` tokens (TE.TE smuggling). Each rejection responds 400 + `Connection: close` so the upstream proxy doesn't reuse the socket. **Static-serve symlink-escape + filename safety** in `b.staticServe`: `_resolveSafe` now `fs.realpathSync`-es the resolved path (defeats symlink-out-of-root) AND validates the basename through `b.guardFilename` at the balanced profile (rejects path traversal, null-byte, NTFS alternate data streams, UNC paths, RTLO bidi, overlong UTF-8, Windows reserved device names, double-extension; balanced profile chosen over strict so legitimate operator-deposited shell-exec extensions like `.exe`/`.bin` remain serveable). **Outbound SMTP smuggling defense** in `b.mail` SMTP transport: every produced RFC 822 wire (post-DKIM-sign) is run through `b.guardEmail.validateMessage` at strict profile before the socket opens; refuses on critical issues bare CR / bare LF + smuggled SMTP verbs (CVE-2023-51764 Postfix / CVE-2023-51765 Sendmail / CVE-2023-51766 Exim / CVE-2026-32178 .NET class) cannot leave the framework even when operator-supplied subject/body/headers contain the pattern. **DKIM `l=` body-length tag forbidden** in `b.mail.dkim.create`: passing `bodyLength` now throws `dkim/l-tag-forbidden` at create-time. M³AAWG / Gmail / Microsoft 365 guidance is "never use l=" it enables append-after-signature attacks where an attacker appends arbitrary content past the signed length and the DKIM signature still validates against the original prefix. The body is always hashed in full. Smoke 84508458 / wiki e2e 178 / Linux container smoke 8458 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
14
+
15
+ - **0.7.17** (2026-05-05) — `b.guardEmail` email content-safety primitive (single-address validation + full RFC 822 / 5322 message validation). Threat catalog grounded in current research (SMTP smuggling CVE-2023-51764 Postfix / CVE-2023-51765 Sendmail / CVE-2023-51766 Exim / CVE-2026-32178 .NET System.Net.Mail; SEC Consult / smtpsmuggling.com class; IDN homograph attacks; CRLF header injection). Surface: `validateAddress(addr, opts)` validates a single address; `validateMessage(rfc822, opts)` validates a full message; `validate(input, opts)` auto-detects; `sanitize(input, opts)` strips character-class threats but throws on critical (smuggling / CRLF injection / multi-@ / mixed-script — no safe sanitization for these); `gate(opts)` returns a `b.gateContract`-shaped gate auto-routed by `b.guardAll` for `message/rfc822` / `message/global`. KIND="content". **Threat catalog**: SMTP smuggling (bare CR / bare LF outside CRLF pairs combined with embedded SMTP verbs `MAIL FROM`/`RCPT TO`/`DATA`/`EHLO`/`HELO`/`RSET`/`QUIT`); CRLF header injection in single-line headers; IDN homograph spoofing (mixed-script Cyrillic / Greek / Armenian / Cherokee codepoints in domains operator opts in via `allowedScripts`); Punycode/IDN flag; display-name spoofing (`"support@apple.com" <attacker@evil>` display contains @-address that doesn't match envelope domain); IP literal addresses (`user@[1.2.3.4]` bypasses DNS/DMARC alignment); RFC 5322 comment syntax in addresses; multiple @ characters; RFC 5321 length caps (local-part 64 / domain 255 / address 320); RFC 5322 line cap (998); BOM injection; bidi/null/control/zero-width chars in addresses + headers. **Profiles** — strict / balanced / permissive (SMTP smuggling + CRLF header injection + multi-@ + null bytes refused at every profile — universal class). **Compliance postures** — hipaa / pci-dss / gdpr / soc2-cc7. **Strict default-on via v0.7.12**: every `b.fileUpload` + `b.staticServe` deploy gets this gate at strict profile automatically. Smoke 8387smoke / wiki e2e / Linux container smoke + wiki e2e green / eslint clean / api-snapshot baseline refreshed.
16
+
17
+ - **0.7.16** (2026-05-05) — `b.guardMarkdown` markdown content-safety primitive. Threat catalog grounded in current research (CVE-2026-30838 CommonMark DisallowedRawHtml whitespace-tag bypass; CVE-2025-9540 Markup Markdown javascript: link XSS; CVE-2025-7969 markdown-it ReDoS; CVE-2025-6493 CodeMirror Markdown catastrophic backtracking; CVE-2025-24981 MDC autolink XSS; CVE-2026-33500 AVideo Parsedown link bypass; GHSA-gwjh-c548-f787 NuGetGallery autolink XSS; Joplin GHSA-hff8-hjwv-j9q7 RCE via untrusted markdown link). Surface: `validate(input, opts)` returns `{ ok, issues }`; `sanitize(input, opts)` strips character-class threats but throws on critical (dangerous tags / dangerous URL schemes / DOCTYPE / code-fence injection / image scheme / autolink scheme — no safe sanitization for these); `gate(opts)` returns a `b.gateContract`-shaped gate auto-routed by `b.guardAll` for `text/markdown` / `text/x-markdown` / `text/x-gfm`. KIND="content". Source-level discipline runs BEFORE any downstream renderer (marked / markdown-it / commonmark / remark / parsedown) sees the input, the same shape that catches `__proto__` in JSON before parse. **Threat catalog**: dangerous tags (script/iframe/object/embed/form/input/meta/link/base/svg/math/video/audio/style/template/portal/marquee refused at every profile, whitespace-tolerant per CVE-2026-30838); dangerous URL schemes in inline links + images + autolinks + reference-link definitions (javascript:/vbscript:/livescript:/mocha:/view-source:/data:/jar:/blob:/feed:/tel:/facetime:refused at every profile); HTML-entity scheme bypass (`&#x6A;avascript:` / `&#106;avascript:` decoded BEFORE scheme matching); reference-link smuggling (`[label]: javascript:...` definition smuggle); autolink scheme bypass (`<javascript:alert(1)>`); front-matter YAML/TOML blocks; HTML comments; code-fence language injection (language tag containing `<>"' `` blocks attribute breakout); inline DOCTYPE; catastrophic emphasis runs (`*`/`_` of length 20+CVE-2025-6493 class); bidi/null/control/zero-width chars; total-bytes + line + link + image + autolink + ref-def + list-depth + blockquote-depth caps. **Profiles**strict / balanced / permissive (dangerous tags + dangerous schemes + image schemes + autolink schemes refused at every profile script-tag and javascript: are universal class). **Compliance postures** — hipaa / pci-dss / gdpr / soc2-cc7. **Strict default-on via v0.7.12**: every `b.fileUpload` + `b.staticServe` deploy gets this gate at strict profile automatically. Smoke 83208387 / wiki e2e 178 / Linux container smoke 8387 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
18
+
19
+ - **0.7.15** (2026-05-04) — `b.guardXml` XML content-safety primitive + smoke parallel mode + persistent test output. **`b.guardXml`** threat catalog grounded in current research (CVE-2026-24400 AssertJ XXE; CVE-2025-3225 sitemap parser; CVE-2024-1455 LangChain; CVE-2024-25062 libxml2 use-after-free with DTD + XInclude; CVE-2024-56171 + CVE-2025-24928 + CVE-2025-32415 + CVE-2025-27113 libxml2 family; CVE-2024-8176 libexpat stack overflow via recursive entity expansion). Surface: `validate(input, opts)` returns `{ ok, issues }`; `sanitize(input, opts)` strips character-class threats but throws on critical (DOCTYPE / ENTITY / external — no safe sanitization); `gate(opts)` returns a `b.gateContract`-shaped gate auto-routed by `b.guardAll` for `application/xml` / `text/xml`. KIND="content". **Threat catalog**: DOCTYPE refused unconditionally (XXE / billion-laughs vector); `<!ENTITY>` declarations including parameter entities (`%` prefix); external entity references (`SYSTEM`/`PUBLIC` with file://, http://, etc.); XInclude (`<xi:include>`); `xsi:schemaLocation` schema fetch; processing instructions (skipping standard `<?xml?>` declaration); CDATA sections; XML signature wrapping (audit); bidi/null/control/zero-width; element-count + depth caps; per-attribute-value-length cap. **Profiles** strict / balanced / permissive (DOCTYPE refused at all profile levels billion-laughs class is universal). **Compliance postures**hipaa / pci-dss / gdpr / soc2-cc7. **Smoke parallel mode** new `SMOKE_PARALLEL=N` env var forks Layer 0 test files in parallel batches (each fork is a fresh Node child process for module-state isolation). Layers 1-5 stay sequential because they share db / cluster / vault state. Sanity ceiling 64. Empirical: SMOKE_PARALLEL=64 takes 91s vs sequential 122s (25% faster). **Persistent test output** `test/smoke.js`, `test/layer-0-primitives/codebase-patterns.test.js` (CLI mode), and `examples/wiki/test/e2e.js` now write a tee'd copy of all stdout/stderr to `.test-output/smoke.log` / `.test-output/codebase-patterns.log` / `.test-output/wiki-e2e.log`. Tee semantics original stdout/stderr passthrough preserved so npm exit codes + GitHub Actions annotations work unchanged. `.test-output/` is auto-gitignored via the existing `.* ` catchall and never shipped (npm `files` allowlist explicit). **Family infrastructure** guard-xml shares the same family-ABI shape (resolveProfileAndPosture / aggregateIssues / buildGuardGate / extractBytesAsText / etc.) as the other 6 family members; the v0.7.13 `family-subset` cluster allowlist sustains. Smoke 82648320 / wiki e2e 178 / Linux container smoke 8320 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
20
+
21
+ - **0.7.14** (2026-05-04) — `b.guardYaml` YAML content-safety primitive. Threat catalog grounded in current research (CVE-2026-24009 Docling/PyYAML unsafe load → RCE; CVE-2026-27807 MarkUs alias billion-laughs DoS; CVE-2025-68664 LangChain deserialization → RCE; CVE-2025-61301 + CVE-2025-61303 YAML library DoS family — "Laughter in the Wild" study; CVE-2022-1471 SnakeYAML constructor RCE; CVE-2020-1747 + CVE-2020-14343 PyYAML FullLoader; CVE-2017-18342 PyYAML python/object/apply). **Surface**: `validate(input, opts)` returns `{ ok, issues }`; `parse(input, opts)` refuses on critical and parses through the framework's safe-subset YAML parser; `gate(opts)` returns a `b.gateContract`-shaped gate auto-routed by `b.guardAll` for `application/yaml` / `text/yaml` content. Auto-registers into `b.guardAll` (KIND="content"). **Threat catalog**: deserialization-tag injection (language-specific tags `!!python/` / `!!java.` / `!!ruby/` / `!!perl/` / `!!js/` / `!!cs/` / `!!net/` / `!!system.` plus `!!eval` / `!!exec` / `!!new` / `!!apply`); custom + local user tags; anchor / alias recursion (billion-laughs amplification `&anchor` declares + `*alias` references; alias-amplification ratio ≥8x flagged as alias-explosion regardless of absolute counts); multi-document streams (operators expecting one doc silently get the first one and ignore the rest); Norway problem (YAML 1.1 still default in pyyaml + libyaml in 2026 — unquoted no/yes/y/n/on/off treated as booleans, country code "NO" false); leading-zero octals (`0777` parses as 511); duplicate keys (YAML 1.2 SHOULD-unique; parsers silently last-wins); merge-key chains (`<<: *anchor` anchor-chain DoS); bidi / null / control / zero-width chars in scalar values; total-size + node-count + anchor + alias-depth + document-count + scalar-length + depth caps. **Profiles** — strict (refuse every threat; 2 MiB cap; depth 8; 16 anchors; 1 alias depth; 1 document; 1024 nodes); balanced (audit most threats; allow YAML 1.2 core tags `!!str` / `!!int` / `!!seq` / `!!map` / etc.; refuse alias-explosion regardless; 8 MiB cap; 64 anchors); permissive (audit most; refuse only null bytes + alias-explosion; 64 MiB cap; 1024 anchors). **Compliance postures** — hipaa / pci-dss / gdpr / soc2-cc7 with strict overlays + forensic snapshots. **Refuse-only sanitize discipline**: YAML has no safe sanitization — gate decisions are serve / audit-only / refuse only. **Strict default-on via v0.7.12**: every `b.fileUpload` + `b.staticServe` deploy gets this gate at strict profile automatically. Smoke 81918264 / wiki e2e 178 (210 runtime examples clean) / Linux container smoke 8264 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
22
+
23
+ - **0.7.13** (2026-05-04) — `b.guardJson` JSON content-safety primitive. Threat catalog grounded in current research (CVE-2025-55182 React/Next.js Server Functions deserialization RCE; CVE-2025-57820 + CVE-2026-30226 Svelte devalue prototype pollution; CVE-2026-35209 defu; CVE-2026-28794 @orpc/client; CVE-2025-13465 Lodash; CVE-2025-25014 Kibana RCE; CVE-2024-38984 json-override; CVE-2022-42743 deep-parse-json; GHSA-9c47-m6qq-7p4h JSON5). **Surface**: `validate(input, opts)` returns `{ ok, issues }`; `parse(input, opts)` returns the cleaned tree (composes `b.safeJson.parse` with the source-level pre-parse pollution scan); `gate(opts)` returns a `b.gateContract`-shaped gate auto-routed by `b.guardAll` for `application/json` / `application/ld+json` / `application/vnd.api+json` content. Auto-registers into `b.guardAll` (KIND="content"). **Threat catalog** — source-level prototype-pollution detection (catches `__proto__` / `constructor` / `prototype` keys BEFORE parse — JSON.parse silently routes `__proto__` through the prototype setter so a post-parse Object.keys walk misses the most dangerous key in the catalog); duplicate-key detection (RFC 8259 SHOULD-unique violation; JSON.parse silently last-wins, letting attackers smuggle duplicate-key payloads past validation that ran on the first occurrence Bishop Fox JSON-interoperability research); NaN / Infinity / -Infinity / undefined refusal (RFC 8259 forbids; JSON5 / lenient libraries accept); comment refusal (single-line and block); trailing-comma refusal; JSON5 syntax refusal (single-quoted keys, hex literals, unquoted keys); BOM injection (leading + mid-stream); bidi / null / control / zero-width chars; numeric precision-loss (integers above `Number.MAX_SAFE_INTEGER`); top-level-key allowlist; depth + breadth + array-length + string-length + node-count caps. **Profiles** strict (refuse every threat; 2 MiB doc cap; depth 8; 256 keys/object; 1024 array items) / balanced (strip pollution + BOM + bidi + control + null + zero-width; audit duplicates + comments + trailing commas + JSON5 + numeric precision; refuse NaN; 8 MiB cap; depth 32) / permissive (audit most; refuse only null bytes outright; 64 MiB cap; depth 64). **Compliance postures** hipaa / pci-dss / gdpr / soc2-cc7 with strict overlays + forensic snapshots. **Strict default-on via v0.7.12**: every `b.fileUpload` + `b.staticServe` deploy gets this gate at strict profile automatically — no explicit wiring required. **Family infrastructure**: `KNOWN_CLUSTERS` allowlist gains `mode: "family-subset"` matcher (collapses 6+ exact-match cluster entries into one subset entry; sustainable as the family grows beyond 6 guards). Smoke 81088191 / wiki e2e 178 (210 runtime examples clean) / Linux container smoke 8191 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
24
+
25
+ - **0.7.12** (2026-05-04) — `b.fileUpload` and `b.staticServe` ship with `b.guardAll` wired ON by default at strict profile. Defense-in-depth applied automatically: every operator-supplied bytes path runs through the full guard-* family without any explicit wiring. **`b.fileUpload`**: `contentSafety` defaults to `b.guardAll.byExtension({ profile: "strict" })` (every shipped guard's full threat catalog refused dangerous tags, event handlers, dangerous URL schemes, formula injection, DOCTYPE, SVGZ, animation-href hijack, zip-slip, ratio bombs); new `filenameSafety` opt defaults to `b.guardFilename.gate({ profile: "strict" })` (path traversal + null-byte truncation + Windows reserved names + NTFS ADS + RTLO bidi + overlong UTF-8 + shell-exec extensions + double-extension + reserved chars). **`b.staticServe`**: `contentSafety` defaults to `b.guardAll.byExtension({ profile: "strict" })`. **Explicit opt-out**: `contentSafety: null` / `filenameSafety: null` skips the gate AND emits an audit row at `create()` time (`fileUpload.contentSafety.disabled` / `fileUpload.filenameSafety.disabled` / `staticServe.contentSafety.disabled`) with operator-supplied `contentSafetyDisabledReason` / `filenameSafetyDisabledReason` metadata so a security review can reconstruct which deploys disabled the default-on protection AND why. **filenameSafety wiring**: runs in `fileUpload.finalize` BEFORE `contentSafety` (a refused filename obviates body validation); honours sanitize action (replaces `metadata.filename` with the cleaned form so downstream code sees the safe name). **Explicit operator wiring still works**: passing `contentSafety: { ".csv": gate }` or `filenameSafety: gate` uses the operator-supplied object as-is (no default-on override). **Migration**: this is a behavior change. Code that called `b.fileUpload.create({ stagingDir, onFinalize })` previously had no content / filename safety; after v0.7.12 the same call has both gates wired at strict profile. If existing routes depended on accepting hostile content (raw-bytes uploads, executable-extension artifacts), pass `contentSafety: null` and/or `filenameSafety: null` with an operator-supplied reason at `create()` time. Smoke 8101 8108 / wiki e2e 178 / Linux container smoke 8108 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
26
+
27
+ - **0.7.11** (2026-05-04) — adaptive guard-* family integration harness (`test/layer-5-integration/guard-host-integration.test.js`). The harness discovers every guard primitive registered in `b.guardAll.allGuards()` (registered content guards + standalone non-content guards) and exercises gate decisions through the appropriate host wiring per guard's `KIND`. **Adaptive design** adding a new guard automatically picks up the harness without touching the test file. Each guard exports `KIND` (`"content"` / `"entries"` / `"filename"`) and `INTEGRATION_FIXTURES` with kind-appropriate sample payloads (benign + hostile). The harness iterates `b.guardAll.allGuards()`, dispatches per kind, and per-guard runs: direct gate (benign serve; hostile not serve); `b.guardAll.gate` contentTypeMux dispatch; `b.guardAll.gate({ exceptFor })` opt-out path with audit-row verification of the skipped roster; `b.staticServe.create({ contentSafety })` GET round-trip (benign 2xx, hostile 4xx); `b.fileUpload.create({ contentSafety })` chunk finalize round-trip (benign succeeds, hostile throws content-safety error); audit chain captures host-level rows. **Family additions** `lib/guard-all.js` gains `STANDALONE_GUARDS` array (currently `[guardFilename]`) and `allGuards()` aggregator that returns `GUARDS.concat(STANDALONE_GUARDS)`. Each guard module exports `KIND` + `INTEGRATION_FIXTURES`. **Tests run in-process** no docker / network / fixture-archive dependencies; the harness completes in ~100ms regardless of guard count, so the gate runs on every smoke. Smoke 8060 8101 / wiki e2e 178 / Linux container smoke 8101 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed. **Future-proofing** when v0.7.12+ adds guard-link / guard-mime / guard-yaml / etc., they ship with `KIND` + `INTEGRATION_FIXTURES` and the harness picks them up automatically. The on-by-default wiring slice (next) can flip the default knowing every shipped guard already has end-to-end host coverage.
28
+
29
+ - **0.7.10** (2026-05-04) — `b.guardArchive` archive content-safety primitive. Threat catalog grounded in current archive-extraction CVE research: CVE-2025-3445 mholt/archiver Zip Slip; CVE-2025-32779 EDDI Zip Slip; CVE-2025-62156 Argo Workflows Zip Slip; CVE-2025-66945 Zdir Pro path traversal; CVE-2025-45582 GNU Tar two-step symlink bypass; CVE-2025-11001/11002 7-Zip symlink + directory traversal RCE; CVE-2025-4138/4517 Python tarfile extraction-filter symlink bypass; CVE-2025-10854 txtai; CVE-2025-12060 Keras; CVE-2026-26960 node-tar hardlink-via-symlink-chain. **Surface**: `validateEntries(entries, opts)` validates an operator-supplied entry-list (`{ name, size, compressedSize, isSymlink, isHardlink, linkTarget, isDirectory, isEncrypted, attrs }`); `inspectMagic(buffer)` reads first bytes and returns detected format (zip / gzip / bzip2 / xz / 7z / rar4 / rar5 / lzma / zstd / tar via "ustar" magic at byte 257); `checkExtractionPath(entryName, extractionRoot)` single-entry helper; `gate(opts)` returns a `b.gateContract`-shaped gate. Auto-registers into `b.guardAll` (NAME / MIME_TYPES / EXTENSIONS / shared profiles + postures all conformant). **Scope discipline**: validates archive METADATA, not bytes. Operators use their archive library to enumerate entries, then validate the list before extracting. The framework's no-deps rule argues against shipping a parser for every format (zip / tar / gzip / bzip2 / 7z / rar / zstd...). **Threat catalog**: zip-slip path traversal (CVE-2025-3445 class); absolute-path entries (`/etc/passwd`, drive-letter prefix); symlink + hardlink escape (CVE-2026-26960 class); per-entry compression-ratio bombs (default 100:1 strict, 1000:1 permissive); aggregate compression-ratio bomb (sum-of-uncompressed / sum-of-compressed); total-size + entry-count + per-entry-size caps; nested-archive detection via extension catalog (.zip / .jar / .tar.gz / .tgz / .tar.bz2 / .gz / .bz2 / .xz / .7z / .rar / .zst / etc.) with maxNestedDepth; per-entry-name validation composing `b.guardFilename` (path traversal / null-byte / Windows reserved names / NTFS ADS / RTLO bidi / overlong UTF-8 / shell-exec / double-extension); duplicate entry names (silent overwrite vector); case-insensitive collisions on Windows / macOS; encryption-claim mismatch (mixed encrypted + unencrypted); sparse tar entries; format-claim mismatch via magic-byte detection. **Profiles**: strict (every threat refused; 100 entries; 100 MiB total; 100:1 ratio cap), balanced (symlinks within root allowed; no hardlinks; nested-depth 2; 10000 entries; 1 GiB total; 100:1/1000:1 ratios), permissive (symlinks + hardlinks within root; depth 4; 100000 entries; 10 GiB total; 1000:1 ratios). **Compliance postures**: hipaa / pci-dss / gdpr / soc2-cc7 with strict overlays + forensic snapshots. **Refuse-only sanitize discipline**: archive content has no safe sanitization gate decisions are serve / audit-only / refuse only (no sanitize action). **Tests** — 102 new layer-0 assertions covering surface, registry parity, zip-slip detection (3 forms), absolute-path detection (3 forms), symlink reject (strict), symlink escape (balanced), hardlink reject + escape, compression-ratio bomb (per-entry), aggregate-ratio bomb, total-size cap, entry-count cap, nested-archive (.zip / .tar.gz), duplicate-entry-name, case-insensitive collision, encryption-claim mismatch, sparse entry, magic-byte detection (8 formats including tar via offset-257 ustar), checkExtractionPath helper, clean-archive + warn-only handling, gate decision shapes (clean / refuse / no-entry-list), profile + posture vocabulary. Smoke 79588060 / wiki e2e 178 (210 runtime examples clean) / Linux container smoke 8060 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
30
+
31
+ - **0.7.9** (2026-05-04) — `b.guardFilename` filename content-safety primitive. **Standalone primitive** does NOT register into `b.guardAll`'s content-type-routed dispatch (filename is a different axis from content-bytes; operators apply both filename safety on the upload's name plus content safety on the body). Threat catalog grounded in OWASP Path Traversal + WSTG file-inclusion testing guides; CWE-22 / CWE-23 / CWE-35 / CWE-73 / CWE-78 / CWE-434; PortSwigger File-path-traversal series (null-byte bypass + extension validation); Memento-RTLO + RTL-Spiegel file-name spoofing reports (CVE-2021-42574 in filename context); Kevin Boone overlong UTF-8 sequence write-up. **Threat catalog** path traversal (raw + percent-encoded `%2e%2e` + double-encoded `%252e%252e` + UTF-8 overlong `0xC0 0xAE` for dot and `0xC0 0xAF` for slash); null-byte truncation; Windows reserved device names (CON / PRN / AUX / NUL / COM1-9 / LPT1-9 / CLOCK$ / CONFIG$, with and without extensions, case-insensitive); NTFS alternate data streams (`name:stream`); leading/trailing whitespace + trailing dots (Windows silently strips them); Unicode bidi / RTLO file-name spoofing; zero-width / homoglyph chars; reserved characters (Windows < > : " | ? *); UNC paths; path separators in leaf-name; length caps (strict 64-byte / balanced+permissive 255-byte); multi-dot policy (strict requires single dot, balanced+permissive allow .tar.gz); extension allowlist (catches double-extension bypass: `file.jpg.exe` matches `.exe` against the allowlist); shell-shortcut + executable extensions (.exe / .bat / .vbs / .ps1 / .lnk / .scr / .dll / .so / .dmg / .msi / etc); overlong UTF-8 detection at the Buffer level (RFC 3629 §3 prohibits non-shortest-form). **Profiles** — strict (ASCII-only, single dot, single leaf, 64-byte cap, every threat class refused), balanced (Unicode NFC-normalized, multi-dot allowed, 255-byte cap, refuses dangerous classes + strips zero-width + audits homoglyphs), permissive (multi-component paths up to 16 components, reserved-name audited not refused for non-Windows targets). **Compliance postures** hipaa / pci-dss / gdpr / soc2-cc7 with appropriate strict overlays + ASCII-only + forensic snapshots. **Sanitize discipline** strips leading/trailing whitespace + trailing dots, NFC-normalizes Unicode, replaces reserved chars with underscore (under strip policy), prepends underscore to Windows reserved device names. ALWAYS THROWS on path-traversal / null-byte / NTFS ADS / UNC / overlong UTF-8 these have no safe sanitization, the only correct response is reject. **New shared helpers** `gateContract.badInputResultIfNotStringOrBuffer(input)` and `gateContract.aggregateIssues(issues)` extracted across guard-svg / guard-filename validate paths that need raw-Buffer input pre-conversion. Both registered in KNOWN_ANTIPATTERNS so future re-implementations fail the n=1 gate. **Tests** — 79 new layer-0 assertions covering surface, path traversal (5 forms), percent-encoded traversal (3 forms), null-byte truncation, every Windows reserved name (12 cases including extensions), NTFS ADS, leading/trailing strip, RTLO bidi spoofing, reserved chars (7 cases), UNC paths, path separators in leaf, length cap, single-dot policy, extension allowlist, shell-exec extensions (11 cases), double-extension bypass, overlong UTF-8 at buffer level, ASCII-only enforcement, sanitize round-trip + throw-on-traversal/null-byte, gate decision shapes, profile + posture vocabulary. Smoke 78797958 / wiki e2e 178 (206 runtime examples clean) / Linux container smoke 7958 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
32
+
33
+ - **0.7.8** (2026-05-04) — `b.guardSvg` SVG content-safety primitive + guard-* family helper extraction. **`b.guardSvg`** ships full v1 scope grounded in current SVG attack-surface research (Fortinet anatomy of SVG attack surface; Angular GHSA-jrmj-c5cx-3cw6 + GHSA-v4hv-rgfq-gp49 SVG animation/href XSS; SVGO CVE-2026-29074 billion laughs DoS; siyuan-note GHSA-5hc8-qmg8-pw27 animate-element sanitizer bypass; cure53/DOMPurify issue #233 xlink:href filtering; insertScript SVG fun-time series). Threat catalog: dangerous tags (script / foreignObject / handler / iframe / embed / object / audio / video / animation family); SMIL animation attributeName allowlist enforcement (recent CVE class `<animate attributeName="href" to="javascript:..."/>` is the published bypass shape, refused regardless of broader animation policy); on* + SMIL event-handler attributes (onclick / onerror / onload / onbegin / onend / onrepeat); href + xlink:href dangerous URL schemes (javascript / vbscript / data outside image-context / file / mhtml / view-source entity-encoded form too); cross-origin `<use>` external refs (SSRF + XSS chain); XML DOCTYPE declarations refused unconditionally (defends billion-laughs entity expansion + XXE); custom `<!ENTITY>` declarations; CDATA + processing instructions; SVGZ compressed payloads (gzip magic bytes 0x1F 0x8B refused at gate level operator must ungzip first); CSS injection in style attributes; <use>-recursion DoS via maxUseDepth; total-document size + element-count + attribute-count caps. **Profiles**: strict (minimal text+shapes; no animation; no external refs), balanced (adds <use> + <image> with same-origin or http(s); allowExternalRefs=true), permissive (adds animation with attributeName allowlist still enforced). **Compliance postures**: hipaa / pci-dss / gdpr / soc2-cc7 with strict overlays + forensic snapshots. **Guard-* family helper extraction (audit-existing-code rule §8 sweep)** five new shared helpers landed in `lib/codepoint-class.js` and `lib/gate-contract.js` and the existing guards refactored to consume them: `codepointClass.detectCharThreats(text, opts, codePrefix)`, `codepointClass.assertNoCharThreats(text, opts, errorFactory, codePrefix)`, `codepointClass.applyCharStripPolicies(text, opts)`, `gateContract.resolveProfileAndPosture(opts, cfg)`, `gateContract.runIssueValidator(input, opts, detector)`, `gateContract.buildGuardGate(name, opts, check)`, `gateContract.extractBytesAsText(ctx)`, `gateContract.lookupCompliancePosture(name, postures, errorFactory, codePrefix)`, `gateContract.makeRulePackLoader(errorClass, codePrefix)`, `gateContract.makeProfileBuilder(profiles)`, `numericBounds.requireAllPositiveFiniteIntIfPresent(opts, names, labelPrefix, errorClass, code)`. Pre-existing call sites in `lib/csv.js` and `lib/file-upload.js` migrated in the same patch per the no-future-patch-deferrals rule. New shared module `lib/codepoint-class.js` centralizes BIDI / C0_CTRL / ZERO_WIDTH range tables plus the `_hex4` / `_charClass` / `_fromCp` rendering helpers the codepoint catalog has a single source of truth and future guard-* slices consume the shared module instead of redeclaring the tables. Each helper extraction registers its inline-shape in `KNOWN_ANTIPATTERNS` (the codebase-patterns gate fires at n=1 if any future code re-introduces the pattern). **Tests** — 83 new layer-0 assertions covering surface, registry parity, dangerous-tag detection, on* handler family, dangerous URL schemes (entity-encoded form too), animation attributeName href hijack, cross-origin <use> refusal, DOCTYPE / <!ENTITY> rejection, CDATA + processing-instruction policy, SVGZ magic-byte detection, CSS injection, bidi/control/null-byte detection, depth/size caps, sanitize round-trips, gate decision shapes, profile + posture vocabulary. Smoke 77967879 / wiki e2e 178 (203 runtime examples clean) / Linux container smoke 7879 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
34
+
35
+ - **0.7.7** (2026-05-04) — `b.guardHtml` HTML content-safety primitive ships full v1 scope. Threat catalog grounded in current sanitizer research (DOMPurify CVE series, OWASP XSS / DOM-Clobbering / HTML5 Security cheat sheets, PortSwigger and Sonar mXSS write-ups, html5sec.org). Surface: `validate(input, opts)` `{ ok, issues }`; `sanitize(input, opts)` cleaned HTML string; `escapeText(value)` and `escapeAttr(value)` always-correct entity escapers; `gate(opts)` returns a `b.gateContract`-shaped gate; `buildProfile`, `compliancePosture`, `loadRulePack`. Auto-registers into `b.guardAll` (NAME / MIME_TYPES / EXTENSIONS / shared profiles + postures all conformant). **Threat catalog covered** — dangerous tags (script / style / link / meta / base / iframe / object / embed / applet / form / input / button / frame / frameset / marquee / blink / plaintext / xmp / audio / video / source / track / math / svg / template / noscript / portal / dialog / keygen / menuitem / command); every `on*` event-handler attribute (caught family-wide via `/^on[a-z]/` regex covers the entire HTML5 event-handler family without a manual list that rots when WHATWG specs a new event); form-override attributes (formaction / formmethod / formenctype / formtarget / formnovalidate, CWE-1021); iframe `srcdoc`; custom-element `is`; CSP-bypass-shaped attributes (nonce / integrity / crossorigin / http-equiv / manifest); URL-scheme allowlist with entity-decode pre-pass (defends `&#x6A;avascript:` and decimal-entity bypasses); CSS injection in style attribute values (expression / behavior: / -moz-binding / javascript:/vbscript: inside url() / @import / @namespace); DOM clobbering (id/name attribute values matching well-known JS globals on form/input/button/a/img/iframe/object/embed/select/textarea); mXSS hints (svg/math namespace-context-shift parents); IE conditional comments; Unicode bidi override (CVE-2021-42574 Trojan Source) / C0 control chars / null bytes / zero-width chars; tag-depth + attribute-count + per-attribute-value-size + total-document-size DoS caps. **Profiles** — strict (minimal text-formatting allowlist; reject every threat class) / balanced (links + images + tables + semantic markup; strip rather than reject for character-class threats; data:image/* on `<img>`) / permissive (every tag NOT in the dangerous-tag denylist). **Compliance postures** — hipaa / pci-dss / gdpr / soc2-cc7 with appropriate strict/balanced overlays + forensic-snippet sizing (256 / 256 / 128 / 512 bytes). **Sanitizer discipline** drops dangerous tags AND their text-content body (script/style/iframe/object/embed/applet/template/svg/math etc. body parsed as code in the host); strips on* attributes, dangerous URL schemes, CSS injection patterns, DOM-clobbering values, doctypes, CDATA, comments. Documented operator-facing trade-off: for hostile sources, validate+reject is the strong path; sanitize is best-effort and operators displaying untrusted HTML should additionally serve under a strict CSP. **Codepoint-table programmatic regex pattern** — same as guard-csv: BIDI_RANGES / C0_CTRL_RANGES / ZERO_WIDTH_RANGES literal numeric tables compiled into character classes via `_charClass` + `\\uXXXX` escapes at module load. Source file never embeds attack chars themselves. **Tests** — 134 new layer-0 assertions covering surface, registry parity, every dangerous tag in the denylist, 15 representative on* handlers, 9 dangerous attributes, 10 dangerous URL schemes (entity-encoded form too), 5 CSS injection patterns, 8 DOM clobber globals, mXSS hints, IE conditional comments, bidi/control/null detection, depth/size caps, sanitize round-trips, escape correctness, gate decision shapes (clean/refuse/sanitize), profile + posture vocabulary. Smoke 76627796 / wiki e2e 178 (200 runtime examples clean) / Linux container smoke 7796 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
36
+
37
+ - **0.7.6** (2026-05-04) — `b.guardAll` registry + aggregator for the guard-* family. Security-on-by-default: every shipped guard is ON unless the operator opts OUT specifically with an audited reason. **Surface** `b.guardAll.gate(opts)` returns a single `b.gateContract`-shaped gate that routes by `Content-Type` to the right registered guard; `b.guardAll.byExtension(opts)` and `b.guardAll.byContentType(opts)` return ready-made gate maps for direct drop-in to `b.staticServe.create({ contentSafety: ... })` and any host primitive that dispatches on extension or mime; `b.guardAll.list()` returns the registered roster (operator audit aid); `b.guardAll.GUARDS` / `SHARED_PROFILES` / `SHARED_POSTURES` exposed as readonly registry exports. **Opt-out** `exceptFor: { csv: { reason: "no CSV emission in this app" } }` requires a non-empty `reason` string per opted-out guard (throws at gate-creation time, not silently); `override: { csv: { profile: "email-attachment" } }` reaches per-guard extension profiles that aren't in the shared vocabulary. **Audit emission** every gate / byExtension / byContentType call with `opts.audit` wired emits `guardAll.gate.created` exactly once at gate-creation time, recording the full `active` + `skipped` roster (each skipped entry carries its `reason`). **Registry contract** every primitive registered into `b.guardAll` MUST export `NAME` (string), `MIME_TYPES` (frozen array), `EXTENSIONS` (frozen array), `PROFILES` containing the shared vocabulary (`strict` / `balanced` / `permissive`), `COMPLIANCE_POSTURES` containing the shared regulatory shapes (`hipaa` / `pci-dss` / `gdpr` / `soc2-cc7`), and `gate(opts)` returning a `b.gateContract`-shaped gate. The parity check at module load throws `GuardAllError` with a multi-line failure list if any registered guard is missing the contract this is the framework's mechanism for keeping every future guard slice conformant. Duplicate `NAME` / `MIME_TYPE` / `EXTENSION` across guards is also caught at module load. **Adjacent** — `b.guardCsv` now exports `NAME` / `MIME_TYPES` / `EXTENSIONS` for registry compliance. Wiki harness gains `fs` binding for examples that mkdir staging dirs (joins the existing `path` / `os` bindings). **Tests** — 56 new layer-0 assertions: surface, registry parity (every member declares the full contract), default-on (no `exceptFor` every guard active), `exceptFor` reason validation (missing / blank / non-object / unknown name throws), `override` shape validation, profile-vocabulary enforcement (per-guard extensions like csv's `email-attachment` rejected at the aggregator), posture-vocabulary enforcement, audit creation roster, dispatch correctness (text/csv routes to csv guard, unrelated mime bypasses), byExtension / byContentType output shape. Smoke 76067662 / wiki e2e 178 (196 runtime examples clean) / Linux container smoke 7662 / Linux container wiki e2e 178 / eslint clean / shellcheck clean / api-snapshot baseline refreshed.
38
+
39
+ - **0.7.5** (2026-05-04) — `b.gateContract` foundation + `b.guardCsv` content-safety primitive. **BREAKING (pre-v1):** `b.csv.stringify` no longer accepts `preventFormulaInjection` / `formulaPrefixChars` opts the partial single-mode defense it offered (only `=`/`+`/`-`/`@`/TAB/CR ASCII prefixes, missing LF / `|` / full-width formula chars) was a false-confidence path. `b.csv` is now documented as trusted-source-only emission (RFC 4180 quoting + anti-DoS bounds); any user-supplied cells route through `b.guardCsv.serialize` / `.gate` for the full threat catalog. The `b.guardCsv.parse` / `.stringify` re-exports are also removed — operators use `b.csv.parse` / `.stringify` directly for pure parsing. New `guard-*` family naming complements the existing `safe-*` parser family (safeJson / safeUrl / safeBuffer / safeSql / safeAsync are "parser/validator with safer defaults"; guard-* are content-safety gates that run inside the request lifecycle). **`b.gateContract`** provides the uniform composition contract every guard-* primitive implements: `defineGate / runGate / composeGates / multiplexGates / contentTypeMux / shadowMode / canaryGate / cachingGate / workerThreadGate / buildProfile / composeHooks / summarizeIssues / validateGateShape`, plus mode posture (enforce / warn-only / shadow / audit-only / log-only / canary), hook system (beforeCheck / afterCheck / onIssue / onSanitize / onRefuse / onAudit), forensic snapshot store integration, decision cache, and runtime cap via `safeAsync.withTimeout`. **`b.guardCsv`** ships full v1-defensible scope: `serialize / validate / sanitize / escapeCell / detect / parse / stringify / schema / gate / buildProfile / compliancePosture / loadRulePack`. Threat catalog covered — formula injection (5 modes: prefix-tab / prefix-quote / wrap-with-quotes-and-prefix / reject / allowlist) with all 8 ASCII triggers (`= + - @ TAB CR LF |`) plus full-width variants (U+FF1D / U+FF0B / U+FF0D / U+FF20) per OWASP locale catalog, dangerous-function denylist (WEBSERVICE / HYPERLINK / IMAGE / IMPORT* / RTD / DDE / CALL / GOOGLEFINANCE / GOOGLETRANSLATE) surfaced as critical regardless of broader formula policy, Unicode bidi override (CVE-2021-42574 Trojan Source), homoglyph mixed-script detection, C0 control chars, null bytes, BOM mid-stream injection, zero-width chars, dialect ambiguity, CSV-bombs (per-cell / total / sanitize-amplification caps), numeric precision loss above `Number.MAX_SAFE_INTEGER`, trailing-whitespace exfiltration policy, PII redaction (composes `b.redact`), and schema-bound serializer with type / regex / range / nullable validation. Profiles: `strict` (OWASP-recommended `prefix-tab` default — Excel-resistant) / `balanced` / `permissive` / `email-attachment`; compliance postures: `hipaa` / `pci-dss` / `gdpr` / `soc2-cc7`. **Threat-detection regex literals are composed programmatically from numeric codepoint range tables** (`BIDI_RANGES` / `C0_CTRL_RANGES` / `ZERO_WIDTH_RANGES` / `HOMOGLYPH_RANGES` / `FORMULA_PREFIX_CPS`) `_charClass(ranges)` renders into a regex character class via `\uXXXX` escapes at module load. Source file never embeds the attack characters themselves, only their codepoint numbers; the detector composes the way an attacker would compose the payload. **Adjacent integrations** — `b.staticServe` gains `contentSafety: { ".csv": gate, ... }` extension-keyed gate map (runs after MIME allowlist / before headers; `sanitize` decision replaces body buffer, `refuse` returns 415); `b.fileUpload` gains `contentSafety: gate` running before `onFinalize` (`refuse` throws `CONTENT_SAFETY_REFUSED`, `sanitize` replaces `bodyBuffer`). **Validation primitive** — `validateOpts.optionalPlainObject(value, label, ErrorClass, code, description)` consolidates the `if (typeof !== "object" || Array.isArray) throw` cascade across api-key / db-declare-view / static / file-upload; registered in `KNOWN_ANTIPATTERNS` so future re-implementations fail the n=1 gate. **Tests** — 47 new layer-0 assertions covering surface (gateContract + guardCsv), every formula prefix per policy, full-width formula prefixes, dangerous-function denylist, bidi reject + strip, control-char + null byte, numeric precision, CSV-bombs, dialect detection, sanitize amplification cap, schema type drift / nullable / regex, gate clean serve / refuse / sanitize, operator rules, forensic snapshot, audit emission. Smoke 7485 → 7614 / wiki e2e 178 / Linux container smoke 7614 (156s) / Linux container wiki e2e 178 / eslint clean / shellcheck clean / api-snapshot baseline refreshed for new public surface.
40
+
41
+ - **0.7.4** (2026-05-04) — CI gate alignment, wiki cross-platform fix, redis-client opts forwarding consolidation. Two CI-only gates (`scripts/check-api-snapshot.js` for public-API drift detection; Linux-container wiki e2e for cross-platform example execution) added to the local Release Workflow, plus `api-snapshot.json` baseline refreshed for the first time since v0.6.17 (40+ patches of accumulated drift; `b.objectStoreRetry` removal in v0.7.0 was the BREAKING entry). The wiki file-upload example previously hardcoded `stagingDir: "/var/lib/myapp/uploads"` which mkdir-passed on Windows but failed `EACCES` on Linux/macOS CI runnersreplaced with a real runnable example using `path.join(os.tmpdir(), "myapp-uploads-" + Date.now())` and a self-contained `b.fileUpload.create` + `uploads.close()` round-trip; the Usage block likewise instantiates a real `b.router.create()` + uploads pair. `examples/wiki/test/run-example.js` harness gains `path` + `os` Node built-ins as bindings so wiki examples can write idiomatic `path.join(os.tmpdir(), ...)` without falling foul of the harness's `var X = require(...)` line stripper. **`redisClient.pickClientOpts(cfg, prefix?)`** new helper that returns the standard 9-key opts bag (`url / password / username / tls / ca / servername / connectTimeoutMs / commandTimeoutMs / maxReconnectAttempts`); `cache-redis` / `pubsub-redis` / `queue-redis` / `cache` redis-backend creator all migrated to it, eliminating ~40 lines of duplicate inline forwarding across the four files. The new `inline-redis-client-opts-forwarding` `KNOWN_ANTIPATTERNS` entry catches future re-implementations at n=1. **`testNoDuplicateCodeBlocks`** + **`testNoUnresolvedMarkers`** + **`testNoTierTerminologyInLib`** detector tuning: `MIN_DISTINCT_FILES` lowered 3 2 / `MIN_DISTINCT_TOKENS` lowered 6 5 (more aggressive duplicate detection); `defer` added to forbidden-marker scan; `Tier 1/2/3` added to forbidden-tier-terminology scan. `lib/log-stream-syslog.js`'s socket error-handler comment reworded from "defer to 'close' for reconnect" to "reconnect handled in the close listener" same behavior, no marker false-positive. **Tests** Linux-container wiki e2e green (188 examples ran clean, 0 failures, 0 symbol drift); `node scripts/check-api-snapshot.js` reports `[api-snapshot] no changes`. Smoke 7485 / wiki e2e 178 / Linux container smoke 7485 / Linux container wiki e2e 178 / eslint clean / shellcheck clean.
42
+
43
+ - **0.7.3** (2026-05-04) — `b.middleware.apiEncrypt` per-session keying mode (opt-in). Per-request keying stays the default every existing test passes unchanged. Operators opt in by passing `keying: "per-session"`; the first request in a session carries the bootstrap envelope `{ _ek, _ct, _ts, _nonce, _sid, _ctr }` and the server stores the session key keyed by `_sid` (UUID v4); subsequent requests in the same session omit `_ek` and `_nonce` entirely, sending only `{ _ct, _ts, _sid, _ctr }`. The monotonic `_ctr` replaces the nonce for replay defense server rejects any request with `_ctr <= last_seen_ctr` for that session. Responses carry `{ _ct, _sid, _ctr }`; the client helper validates the response counter is strictly increasing so a tampered / replayed response surfaces as `CLIENT_RESPONSE_REPLAY` instead of silently propagating stale data. Sessions expire on `sessionTtlMs` (default 15 minutes) OR after `sessionMaxResponses` (default 1024); both surface as 401 (`session-expired` / `session-rotation-required`) and the client helper's `resetSession()` re-bootstraps. Operator-supplied `sessionStore` is a `{ get, set, delete }`-shaped handle (b.cache shape-compatible) so multi-replica deploys share session state; default is an in-memory map for single-process apps. Wire format trade-off: per-session amortizes ~1.6 KB of KEM material across the session for high-throughput / mobile / metered-network apps; per-request stays strictly safer if the session storage gets compromised. Audit emits `apiEncrypt.session.created` / `.expired` / `.rotated` / `.replay_rejected` with actor context; observability counters track sessions established / replay-rejected / expired / unknown / rotated. The framework HTTP-client helper `b.httpClient.encrypted({ keying: "per-session" })` threads the same posture through service-to-service callers. **Tests** — 13 new layer-0 assertions covering default-keying invariant, opt validation (bad keying value / TTL / store shape), bootstrap-and-reuse round-trip (KEM amortization confirmed by inspecting subsequent envelope shape), unknown-sid 401, counter-replay 400, TTL-expiry 401, max-responses-rotation 401, response counter monotonic check, sessionInfo / resetSession client API, observability emission. Smoke 74457485 / wiki e2e 178 / Linux container smoke 7485 (147s) / eslint clean / shellcheck clean.
44
+
45
+ - **0.7.2** (2026-05-04) — Symmetric upload + download primitives ship with full v1-defensible feature sets. **`b.fileUpload`** chunked upload primitive: operators wire HTTP routes for per-chunk PUT (`fileUpload.acceptChunk`) and finalize POST (`fileUpload.finalize`); the framework owns the chunk lifecycle (per-chunk SHA3-512, atomic chunk write to `<stagingDir>/<uploadId>/<index>`, reassemble in manifest order, verify total SHA3-512) and hands the assembled buffer to an operator-supplied `onFinalize` callback. Surface includes `init` / `acceptChunk` / `finalize` / `status` / `list` / `cancelUpload` / `purgeIncomplete`. v1 features: per-actor active-upload quota, total staging-bytes quota, MIME magic-byte allowlist (composes `b.fileType`), `onChunk` operator hook, idle-upload timeout, stream reassembly above `maxStreamReassemblyBytes` (sequential async iterator over chunk files instead of `Buffer.concat`), permissions integration, metadata stash with cap, path-safe upload-ID regex, mode 0o700 staging dir, idempotent re-PUT, force-cancel, audit emission with 5-W actor context, observability counters. **`b.staticServe`** download primitive expanded with the symmetric feature set: permissions gate (403), compliance retention gate (451), force-revoke instance method + revoke-store opt (404), MIME magic-byte allowlist (415), RFC 7233 single-range support (multi-range refused with 416), full conditional-request set (`If-None-Match` / `If-Match` / `If-Modified-Since` / `If-Unmodified-Since`), per-actor + global bandwidth quotas via `b.cache` cluster-shared token-buckets (429 + Retry-After), per-actor concurrency cap (429), `onServe` per-request operator hook, `maxIdleMs` stalled-stream timeout, cancellation propagation (client disconnect destroys file stream + releases concurrency slot), audit + observability emission with 5-W actor context. ETag is now SHA3-512-truncated for PQC posture; the SRI integrity helper keeps SHA-384 because the W3C subresource-integrity spec only allows sha256/sha384/sha512. **Object-store backends** (sigv4 / gcs / azure-blob) gain a new `getResponse(key, opts?)` method that forwards `range` / `ifNoneMatch` / `ifMatch` / `ifModifiedSince` / `ifUnmodifiedSince` opts to the backend's protocol-specific headers and returns `{ statusCode, body, etag, lastModified, contentRange, size, contentType }` — operators wiring object-store-backed download routes route conditional + range from the client request straight through. Existing `get(key)` returns just the body for back-compat. **`HTTP_STATUS`** constants extended with `PARTIAL_CONTENT` / `RANGE_NOT_SATISFIABLE` / `PRECONDITION_FAILED` / `UNAVAILABLE_FOR_LEGAL_REASONS`. **New helpers** — `validateOpts.optionalNonEmptyStringArray(value, label, ErrorClass, code?)` and `validateOpts.optionalObjectWithMethod(value, method, label, ErrorClass, code?, description?)` consolidate the recurring "string-array" and "duck-typed handle" inline cascades across api-key / file-upload / seeders / notify / webhook / db-role-for. Both registered in `KNOWN_ANTIPATTERNS` so future re-implementations fail the n=1 gate. **Tests** — 28 new layer-0 fileUpload assertions (init / acceptChunk / finalize / status / list / cancel / quotas / MIME allowlist / onChunk / idle / stream reassembly / permissions) plus 24 new staticServe assertions (range / suffix-range / open-end-range / unsatisfiable-range / multi-range refused / acceptRanges off / If-Match / If-Modified-Since / If-Unmodified-Since / permissions / retention / revoke / MIME allowlist / onServe / audit / stats / invalidateMeta / quotas / concurrency cap). Wiki page `examples/wiki/seeders/prod/pages/file-upload.js` added; routing.js section for `b.staticServe` rewritten to document the v1 surface. README "what ships in the box" Communication + Routing bullets updated. Smoke 73417445 / wiki e2e 178 / Linux container smoke 7445 (146s) / eslint clean / shellcheck clean.
46
+
47
+ - **0.7.1** (2026-05-04) — `b.websocket` route opts gain `handshakeGuid` to override the RFC 6455 §1.3 magic string used in the `Sec-WebSocket-Accept` derivation. Default stays `258EAFA5-E914-47DA-95CA-C5AB0DC85B11`. Operators with closed-ecosystem clients running their own GUID (typical for migrations from frameworks that customize the handshake to namespace their own client family) drop `handshakeGuid: "<their-uuid>"` into the `router.ws(path, handler, opts)` opts and clients keep working unchanged. Throws at upgrade time if the override is malformed UUID-shape regex with a 64-char length cap before the regex test, so a typo produces a clear error instead of silently producing a `Sec-WebSocket-Accept` the client can't match. **Tests** — 3 new layer-0 assertions in `test/00-primitives.js` (custom GUID produces different accept key, empty / null falls back to RFC default, malformed handshakeGuid rejected at config time). Smoke 7338 7341 / wiki e2e 178 / Linux container smoke 7341 (145s) / eslint clean / shellcheck clean.
48
+
49
+ - **0.7.0** (2026-05-04) — Codebase-patterns hardening sweep + primitive consolidation. The duplicate-block detector in `test/layer-0-primitives/codebase-patterns.test.js` ran at MIN_DISTINCT_FILES=3 and surfaced ~50 inline-shape clusters that had proliferated across lib/ — the kind of soft drift that's invisible at higher thresholds and hides re-introduction of bug classes the framework already swept once. **New primitive families** consolidated those clusters down: opts validation (`validateOpts.requireObject` / `auditShape` / `observabilityShape` / `applyDefaults` / `optional{Boolean,Function,PositiveInt,FiniteNonNegative,PositiveFinite,NonEmptyString}` / `requireNonEmptyString` / `makeAuditEmitter`), async coordination (`safeAsync.safeInvoke` / `makeDropCallback` / `makeScheduledFlush`), and SQL execution (`dbSchema.runInTransaction` / `runSqlOnHandle`). Plus `numericBounds.requireXFiniteIntIfPresent` for opt-time numeric-shape gates that throw via the caller's framework-error class, `log.makeViaOrFallback` for operator-log routing with per-module fallback, `observability.safeEvent` for hot-path drop-silent emission, `safeBuffer.HEX_RE`/`CRLF_RE`/`TRAILING_HSPACE_RE` regex constants + `isHex` / `hasCrlf` / `stripCrlf` / `stripTrailingHspace` helpers, `time.toIso8601NoMs`, `migrationFiles.MIGRATION_FILE_RE` / `isMigrationFileName` (shared filename grammar across migrations / seeders / external-db-migrate), and `lib/object-store/http-request.js` as a shared HTTP request helper across azure-blob / gcs / sigv4 / http-put. Roughly 80 inline call sites across 50+ files refactored to route through the new primitives; ~1500 lines of duplicate inline shapes eliminated. **Catalog gate** — `KNOWN_ANTIPATTERNS` in the codebase-patterns test now has 24 entries firing at n=1, so future code re-introducing any of the registered inline shapes (even one new file) fails the gate immediately. Pre-v0.7.0 the duplicate-block detector required n>=3 to fire; the catalog closes the gap by registering each extracted primitive's inline shape so it can't drift back in. **Cluster allowlist** — `KNOWN_CLUSTERS` allowlist (n>=3 detector) has 25 entries with documented structural reasons (parser error class signatures don't fit the framework's `(code, message)` contract; framework-convention shapes like middleware factories; future consolidation candidates). **Cleanup** — deleted dead re-export shims `lib/object-store/retry.js` (re-exported `lib/retry.js`) and `lib/auth/totp.js` (re-exported `lib/totp.js`); both fit the rule "pre-v1 frameworks have no operators to compatibly upgrade — every legacy fallback is dead code." Renamed `lib/internal-sha1-hibp.js` → `lib/framework-sha1-hibp.js` to match the lib naming convention (the `internal-` prefix wasn't in the convention's five-bucket list; `framework-` is the canonical "restricted-use" bucket alongside `framework-error.js` / `framework-schema.js`). **No operator-facing API breakage** — `b.objectStoreRetry` was removed from the public surface (operators use `b.retry` directly, which has been the canonical primitive since v0.2.24). **Eslint config tightened** — added `eqeqeq` (with the `null` exception for the `== null` null-or-undefined idiom), `no-throw-literal`, `no-promise-executor-return`, `default-case`, `no-loss-of-precision`. The previous config was hiding 11 real errors: 8 sites of `return resolve()` / `return done()` inside `new Promise(executor)` (return value silently discarded) across app / dev / http-client / mail / router / wiki/integration / 00-primitives, plus a missing default-case + 2 intentional template-language `==` / `!=` operators in template.js's binary-op evaluator (now allowed via inline `// eslint-disable-next-line eqeqeq -- template language operator`). Also adds `structuredClone` to the Node-globals list so db.js's deep-clone calls don't trip `no-undef`. **Release-workflow gate added** — Linux container smoke `docker run --rm -v "/$(pwd):/blamejs" -w //blamejs node:24-alpine node test/smoke.js` is now part of the release flow. Catches lingering-handle bugs that pass on Windows / macOS but hang or error on Linux CI. **Tests** — smoke 7338 / Linux container smoke 7338 (148s) / wiki e2e 178 / per-primitive integration 16 files / wiki integration green / eslint clean / shellcheck clean.
50
+
51
+ ## v0.6.x
52
+
53
+ - **0.6.70** (2026-05-03) — `lib/numeric-bounds` extended to 3 more sites the v0.6.69 sweep didn't cover. **`lib/middleware/csp-nonce.js` `nonceBytes`** pre-fix the `typeof === "number"` check accepted `Infinity` / `NaN` (both bypass `< MIN_NONCE_BYTES` because every comparison-with-NaN-or-Infinity is false), then crashed per-request inside `crypto.generateBytes(Infinity)` with `ERR_OUT_OF_RANGE`. The DoS-shape: an operator typo at boot took down every CSP-protected route at request time. Now rejects at create() with `csp-nonce/bad-nonce-bytes`. **`lib/migrations.js` and `lib/external-db-migrate.js` `staleAfterMs`**both used the `> 0` guard which silently accepted `Infinity`. `(Date.now() - lockedAt) > Infinity` is always false `staleAfterMs: Infinity` was identical to the `0` default ("never replace") but obscured the typo. Operators wanting "never expire" now pass `0` explicitly; everything else (Infinity / NaN / fractional / negative / non-number) rejects with a clear message. **Tests** — 6 new layer-0 assertions extending `test/layer-0-primitives/numeric-bounds.test.js` (3 csp-nonce reject paths + 3 migration locks already covered indirectly via existing test/integration suite). Smoke 72937296 / wiki e2e 178 / Linux 85.9s / eslint clean / shellcheck clean.
54
+
55
+ - **0.6.69** (2026-05-03) — `lib/numeric-bounds.js` shared validator applied across every numeric-opt site that previously accepted `Infinity` / `NaN` and silently bypassed its OOM / size / depth cap. Pre-fix the recurring pattern `typeof opts.X === "number" && opts.X > 0` was vulnerable everywhere it shipped: `Infinity > Infinity` is false, `byteLength > NaN` is false, `Number.isFinite()` was missing. Operators with env-var coercions (`Number(process.env.MAX_X || "")` produces `NaN` for missing values, `Infinity` for typo'd ones) silently lost their cap. **The unified fix:** new `lib/numeric-bounds.js` exposes `isPositiveFiniteInt(value)` + `shape(value)` (the latter formats `"number Infinity"` / `"number NaN"` / `"string \"100\""` so the actual coercion is visible — `JSON.stringify` collapses Infinity/NaN to `"null"` and hides the typo). Each call site uses its own framework-error class (constructors split between `(message, code)` and `(code, message)` conventions across the codebase) but routes through the shared predicate. **Sites swept:** `lib/safe-buffer.js` (`boundedChunkCollector`, `toBuffer`, `normalizeText` — the v0.6.57 fix only covered the first), `lib/atomic-file.js` (refactored from v0.6.68's inline check), `lib/csv.js` (parse `maxBytes` was operator-overridable to Infinity → multi-megabyte CSV bodies through unbounded), `lib/safe-url.js` (`maxUrlLength` opt was the v0.6.62 fix's escape hatch now also bounded), `lib/mail-bounce.js` (`maxBytes` for inbound webhook body — DoS-shape on bounce intake). **Tests** — new `test/layer-0-primitives/numeric-bounds.test.js` with 35 assertions (12 helper + 9 reject-Infinity at each consumer + 7 accept-legit smoke calls + 7 helper-shape format). Smoke 72587293 / wiki e2e 178 / Linux 86.0s / eslint clean / shellcheck clean.
56
+
57
+ - **0.6.68** (2026-05-03) — `b.atomicFile.read` / `readSync` enforce strict `maxBytes` validation. Same bug class as v0.6.57's `safeBuffer.boundedChunkCollector` fix: pre-fix the size check was `if (stat.size > opts.maxBytes)`, which silently accepted `Infinity` (`stat.size > Infinity` always false reads any file regardless of size, defeating the OOM cap). Operators with `Number(env.MAX_READ_BYTES || "")` coercion bugs got `NaN` on missing values and unbounded reads on Infinity-typo'd values. New `_validateMaxBytes` runs before the size check and rejects `Infinity` / `NaN` / non-integer / negative / zero / non-number with `atomic-file/bad-opt`. Real-world consumers (vault.initPlaintext, audit-sign, db.loadOrCreateDbKey, etc.) all pass real positive integers via `C.BYTES.*` helpers and are unaffected. **Tests** — 7 new layer-0 boot-validation assertions in `test/00-primitives.js` (Infinity, NaN, 0, -1, 3.5, "100", null all reject). Smoke 7251 → 7258 / wiki e2e 178 / Linux 86.0s / eslint clean / shellcheck clean.
58
+
59
+ - **0.6.67** (2026-05-03) — canonical-JSON walker extracted to `lib/canonical-json.js` and applied to `lib/audit-tools.js` + `lib/config-drift.js`. Same bug class as v0.6.60 (pagination) and v0.6.66 (audit-chain) lived in two more sites: `audit-tools._canonicalize` (used for backup-bundle JSONL serialisation, requires byte-equivalence with audit-chain) and `config-drift._stableStringify` (used to hash framework config for post-boot tamper detection). Both still had the silent-data-loss walk Date `{}`, Buffer`{"0":97,…}`, Map / Set `{}`, BigInt throws, circular references stack-overflow. New `lib/canonical-json.js` exposes `stringify(value, opts?)` with `opts.bufferAs = "hex" | "reject"` (default `"hex"` for crypto / config / audit; `"reject"` for pagination's strict-cursor policy). All four call sites audit-chain, audit-tools, config-drift, pagination now route through it; the four near-identical inline walkers collapse to one. **Stored-row + bundle compatibility** preserved: existing audit rows verify identically (their `metadata` columns are JSON strings); backup bundles produced pre-v0.6.67 round-trip correctly because byte output for plain types is unchanged. **Tests** — 17 layer-0 audit-chain assertions from v0.6.66 cover the unified walker; existing pagination tests cover `bufferAs: "reject"`. Smoke 7251 / wiki e2e 178 / Linux 86.3s / eslint clean / shellcheck clean.
60
+
61
+ - **0.6.66** (2026-05-03) — `b.auditChain.canonicalize` deep-walks values and rejects non-plain types instead of silently round-tripping garbage. Same bug class as the v0.6.60 pagination canonicalize fix: pre-fix the walker did `Object.keys(row).map(...)` and JSON.stringify'd whatever fell through, which silently encoded **Map / Set / RegExp** as `{}`, **Symbol / function** as missing keys, **BigInt** as a thrown `Do not know how to serialize` mid-emit (DoS-shape on any operator routing bigint IDs into audit metadata), and **circular references** as the unwrapped JSON.stringify error message. The post-fix walker: BigInt decimal string (no data loss for large account IDs / 64-bit counters from external-DB drivers); Date → ISO string (already worked via JSON.stringify but now explicit before the recursive walk); Map / Set / RegExp / Symbol / function reject with a clear constructor-name message; circular refs detected via WeakSet and rejected as a framework error. Walks recursively so nested structures (`{ a: [BigInt(1), BigInt(2)] }`, `{ a: { b: Uint8Array(…) } }`) all serialise correctly. **Stored-row compat** — existing audit rows verify identically because their stored `metadata` columns are already JSON strings; the change only affects EMIT-time serialisation. **Tests** 17 new layer-0 assertions in `test/00-primitives.js`. Smoke 72347251 / wiki e2e 178 / Linux 86.1s / eslint clean / shellcheck clean.
62
+
63
+ - **0.6.65** (2026-05-03) — `b.scheduler.parseCron` rejects step values exceeding the field's range. Pre-fix `*/99999 * * * *` was silently accepted and degenerated to "minute 0 of every hour" (because the for-loop adding values stopped at the first iteration when `step > range`); an operator typing the typo got a once-per-hour schedule when they probably meant once per N minutes. Now `_parseCronField` rejects with `scheduler/invalid-cron` when `step > (range.max - range.min + 1)`. The bound is inclusive so `*/60 * * * *` (= "minute 0 of every hour" written with a redundant explicit step) still accepts. **Tests** — 4 new layer-0 assertions: `*/60` accepts, `*/61` rejects, `*/99999` rejects (the silent-degenerate case), `* */25 * * *` rejects (step > 24-hour range). Smoke 72307234 / wiki e2e 178 / Linux 86.9s / eslint clean / shellcheck clean.
64
+
65
+ - **0.6.64** (2026-05-03) — `b.credentialHash.verify` now enforces the same 16-byte minimum payload that `hash()` enforces. Pre-fix the asymmetry was a real risk: `hash()` refused to *create* a hash shorter than 16 bytes (`length < 16` rejected with `credential-hash/bad-opt`), but `verify()` silently accepted any payload length, including 1-byte payloads where the collision space is **256** — a hand-crafted envelope `{ magic, algoId, SHAKE256(targetPassword, 1)[0] }` would verify against the target password in microseconds. A storage bug or attacker tampering that truncated the stored envelope produced a verifiable but catastrophically weak hash; an attacker with DB write access who couldn't replace the hash outright could *truncate* it to a nearly-cleartext-equivalent value. Both ends now refuse short payloads symmetrically via the new `SHAKE256_MIN_LENGTH = 16` constant; verify returns false (with `payload-too-short` observability label) when the envelope's payload is < 16 bytes. Legitimate truncation that keeps the payload ≥ 16 bytes still verifies (the framework's design intentionally supports XOF variable-length digests via `params: { length: N }`); the bound only refuses hashes too short to provide meaningful collision resistance. **Tests** — 4 new layer-0 assertions on `b.credentialHash.verify` (1-byte attack envelope rejected, 15-byte rejected, 16-byte accepts, hash() rejects length 15). Smoke 72267230 / wiki e2e 178 / Linux 86.5s / eslint clean / shellcheck clean.
66
+
67
+ - **0.6.63** (2026-05-03) — `b.parsers.toml.parse` enforces `maxDepth` on dotted-key paths. Pre-fix the parser walked arbitrarily deep table headers (`[a.b.c.d.e…]`) without applying the existing `maxDepth` (which only ran inside `_parseValue` for inline tables / arrays); a 10,000-segment path built a tree deep enough to stack-overflow the recursive `_normalize` walker post-parse. An attacker submitting a malicious TOML config to `b.config` would crash the framework at boot or whenever the file was reloaded. Now `_parseDottedKey` rejects a path with more than `maxDepth` segments via the same `toml/too-deep` error code already used for value-side depth. **Tests** — 4 new layer-0 assertions: 99-segment path under default depth accepts, 150-segment path rejects with `toml/too-deep`, 10K-segment path rejects (no stack overflow), `maxDepth: 1000` opt allows a 500-segment path. Smoke 72227226 / wiki e2e 178 / Linux 85.6s / eslint clean / shellcheck clean.
68
+
69
+ - **0.6.62** (2026-05-03) — URL-length DoS-shape gap closed in two places. **`b.safeSchema.string().url()`** was regex-only with no length cap, accepting arbitrarily long matching strings same DoS class as v0.6.61's `.email()` gap; an operator chaining `.url()` on a request body would feed multi-megabyte URLs into downstream HTTP clients / SSRF gates / log lines. Now rejects with `string/url-too-long` at 8193+ chars. **`b.safeUrl.parse`** had the same gap at the framework's primary URL-validation surface (used by httpClient + ssrfGuard + every operator-supplied URL). The 8 KB cap now applies BEFORE handing the string to Node's `new URL()` parser; operator-supplied URLs feeding `b.httpClient.request({ url })` from request bodies / webhook configs are bounded. Operators with legitimate non-standard use (proxies, tunnels with embedded payloads) override via `opts.maxUrlLength`. Both bounds reference RFC 7230 §3.1.1's 8000-octet recommendation. **Tests** — 4 new layer-0 assertions on `string().url()` (8 KB / 8193 / 100K / error code) + 4 on `safeUrl.parse` (8192 accept / 8193 reject / error code / `maxUrlLength` opt override). Smoke 72157222 / wiki e2e 178 / Linux 85.5s / per-primitive integration 16 files / eslint clean / shellcheck clean.
70
+
71
+ - **0.6.61** (2026-05-03) — `b.safeSchema.string().email()` enforces RFC 5321 §4.5.3.1.3's 254-character cap on the address. Pre-fix the validator was the regex-only `/^[^\s@]+@[^\s@]+\.[^\s@]+$/`, which accepts arbitrarily long matching strings an operator chaining `.email()` on a request body was open to a DoS shape (50 KB email passes validation, downstream DB writes / log lines unbounded). Now `.email()` rejects with `string/email-too-long` at 255+ chars before the regex even runs, with a message naming the RFC. Operators with a legitimate non-RFC reason for longer addresses skip `.email()` and chain `.regex(custom)` directly. **Tests** — 4 new layer-0 assertions: 254-char address accepts, 255 / 500 char addresses reject, error code `string/email-too-long` exposed for operator handler routing. Smoke 7211 → 7215 / wiki e2e 178 / Linux 85.8s / eslint clean / shellcheck clean.
72
+
73
+ - **0.6.60** (2026-05-03) — `b.pagination.encodeCursor` correctness fixes for non-plain types in cursor state. The `_canonicalize` walker did `if (typeof === "object") Object.keys(...)`, which silently lost data on three classes of input: **Date** silently encoded as `{}` (Date instances have no own enumerable keys); **Buffer / Uint8Array** silently encoded as `{"0":97,"1":98,…}` (indexed-key serialisation); **circular references** stack-overflowed instead of throwing a clean error. After encode → decode the operator's data was either gone or unrecognisable. The fix: Date now serialises to its ISO string (matches stdlib `JSON.stringify` semantics round-trips through encode/decode as the ISO string operators expect); Buffer / Uint8Array / Map / Set / RegExp throw `pagination/bad-state` with a message naming the offending constructor and pointing operators at the right conversion (`"convert to a plain primitive (string / number / iso-string) first"`); circular references throw `pagination/bad-state` cleanly via WeakSet cycle detection. **Tests** — 7 new layer-0 assertions: Date round-trip preserved, all 5 non-plain types reject cleanly, circular reference rejects without stack overflow. Smoke 7204→7211 / wiki e2e 178 / Linux 85.3s / eslint clean / shellcheck clean.
74
+
75
+ - **0.6.59** (2026-05-03) — HTTP/2 session teardown deduplication + http-client sweep. v0.6.58's inline `session.close() + session.destroy()` block in `lib/log-stream-otlp-grpc.js` was the right fix for the OTLP-gRPC sink hang, but the same bug class lives in `lib/http-client.js` the h2 transport pool had 5 call sites running the bare `session.close()` (`_resetTransports`, the ALPN-fallback path, the h2 connect-error path, the h2c connect-error path, the idle-timeout handler, and `_resetForTest`) all of which leak the underlying TCP socket on idle / error / fallback paths in exactly the same way. New `lib/http2-teardown.js` exports `tearDownH2Session(session)` which performs the close()-then-destroy() routine; `lib/http-client.js` and `lib/log-stream-otlp-grpc.js` both import it. The OTLP-gRPC v0.6.58 inline block is removed in favour of the shared helper. **Tests** verified in a node:24-alpine docker container: smoke 7204 checks in 85.3 seconds. Wiki e2e 178 / eslint clean / shellcheck clean.
76
+
77
+ - **0.6.58** (2026-05-03) — `b.logStream` OTLP-gRPC sink hang fix that has been silently breaking the npm-publish gate since v0.6.38. The sink's `close()` was calling `session.close()` (HTTP/2 *graceful* closewaits for in-flight streams before freeing the socket) but never `session.destroy()`. The graceful close completed but the underlying TCP socket stayed connected, blocking the test fixture's `server.close()` indefinitely on Linux CI runners. Same bug also added ~120 seconds of lingering latency to every local smoke run on Windows (smoke 196s 76s after fix). The fix calls `session.close()` then `session.destroy()`; by the time we reach close(), all buffered records have been flushed via the awaited `inflightPromise` + final `_doExport`, so destroy() is structurally safe. **Operator impact** — the npm registry has been stuck at v0.6.37 since 2026-05-02; every tag from v0.6.38 v0.6.57 timed out at the publish workflow's smoke step. With this fix the publish should reach `npm publish`. No layer-0 or integration test changes — the existing `log-stream-otlp-grpc.test.js` round-trip was passing locally because tests use small batches and the lingering socket happens to terminate before the test driver's overall timeout, but it was leaving the process unable to exit cleanly until the OS-level TCP timeout fired. Smoke 7204 / wiki e2e 178 / integration 16 files / eslint clean / shellcheck clean.
78
+
79
+ - **0.6.57** (2026-05-03) — `b.safeBuffer.boundedChunkCollector` strict `maxBytes` validation. Pre-fix the validator was `typeof opts.maxBytes === "number" && opts.maxBytes > 0` which silently accepted `Infinity` (defeating the OOM-cap purpose entirelya hostile 10-GB upstream would accumulate fully) and non-integer floats like `3.5` (which set a fractional cap that confused downstream `total + chunk.length > maxBytes` arithmetic). Now requires positive finite integer; everything else throws `buffer/bad-arg` at boot with the offending value in the message. Real-world consumers (`b.httpClient` request cap, `b.atomicFile.read`, `b.parsers.*`, multipart body parser) are unaffected because they pass real positive integers via `C.BYTES.*` helpers; the fix catches operator typos / misconfigured env-var coercions where `Number(env.MAX_BYTES)` produces NaN or Infinity. **Tests** — 7 new layer-0 boot-validation assertions in `test/00-primitives.js` (smoke 71977204). Wiki e2e 178 / eslint clean / shellcheck clean.
80
+
81
+ - **0.6.56** (2026-05-03) — `b.auth.totp.verify` strips whitespace + common separators (`\s`, `-`, `.`, `_`) from the user-supplied code before the timing-safe comparison. Authenticator UIs (Google Authenticator, Authy, Duo, 1Password, etc.) and clipboard paste typically introduce these `"123 456"`, `"123-456"`, `"123.456"`, `" 123456\t"` and the framework was rejecting all of them as if they were real typos. RFC 6238 / NIST 800-63B don't mandate normalisation, but every consumer-facing TOTP implementation strips these because the alternative is a silent operator footgun: a user mashing a code from their phone into a login form that the verifier rejects on the space character. Letters and other non-numeric characters are NOT stripped those are real input errors, not paste artefacts. Comparison stays timing-safe via the framework's `crypto.timingSafeEqual` wrapper. **Tests** 6 new layer-0 paste-form assertions in `test/00-primitives.js` (smoke 7191→7197). Wiki e2e 178 / per-primitive integration 16 files / eslint clean / shellcheck clean.
82
+
83
+ - **0.6.55** (2026-05-03) — two bug fixes. **`b.mail` SMTP transport**: TLS handshake (implicit on port 465 + STARTTLS upgrade) was unconditionally setting `servername: cfg.host`, which Node's `tls.connect` rejects when host is an IP literal `Setting the TLS ServerName to an IP address is not permitted`. Operators with `host: "127.0.0.1"` (or any IPv4/IPv6 literal) hit the error before a real cert-verification or auth issue could surface. Now: SNI is auto-suppressed on IP literals (matching `lib/redis-client.js`'s v0.6.28 convention); operators with private CAs and an IP-only target pass `opts.servername: "expected-cn.example"` explicitly. **`docker/mongo/init-tls.sh`**: SC2015 `id mongodb >/dev/null && chown mongodb:mongodb ... || true` is not if-then-else; the `|| true` would fire if `chown` failed (treating it as if `id` had failed), masking real failures. Rewritten as plain `if id mongodb …; then chown …; fi`. The shellcheck failure was blocking the v0.6.54 npm-publish gate added in this morning's release. Smoke 7191 / wiki e2e 178 / per-primitive integration 16 files / eslint clean / shellcheck clean.
84
+
85
+ - **0.6.54** (2026-05-03) — npm-publish workflow fixes. The npm registry has been stuck at v0.6.37 because every tag from v0.6.38 v0.6.53 timed out at the workflow's 15-minute cap and got cancelled before reaching the `npm publish` step (smoke alone is ~3 min locally / 6-8 min on CI runners; combined with eslint cold-pull + wiki e2e + SBOM generation that exceeded 15 min). Cap raised to 30 minutes, matching `release-container.yml`. The publish gate also now runs `ludeeus/action-shellcheck@master` against every tracked `.sh` (vendor-update.sh + docker init scripts ARE shipped in the npm tarball, so a shell-script regression should fail the publish). Hadolint stays in release-container.yml only because the Dockerfile is container-only and is not part of the npm artifact. No framework code changesrelease-infrastructure-only patch.
86
+
87
+ - **0.6.53** (2026-05-03) — `b.objectStore.bucketOps` SigV4 backend gains audit + observability emissions across the full state-changing surface closes the v0.6.47 compliance audit gap (Object Lock + retention + legal hold were positioned as SEC 17a-4 / FINRA / HIPAA-grade primitives but had zero audit trail; an auditor asking "who set the retention period for this object, and when?" got nothing). New opts on `bucketOps.create`: `audit` (operator passes `b.audit`), `observability` (operator passes `b.observability`), `auditSuccess` (default true; opt-out at extreme volume), `auditFailures` (default true). Audit emissions for every state-changing op: `objectstore.bucket.create / delete / setLifecycle / setCorsRules / setObjectLockConfiguration` and `objectstore.object.setRetention / setLegalHold`. Observability counter on every op including reads (`getObjectLockConfiguration / getRetention / getLegalHold`). Each state-changing call accepts `{ req }` so the audit chain populates the WHO/WHERE/HOW columns (actorIp, actorUserAgent, actorSessionId, requestId, method, route) via `requestHelpers.resolveActorWithOverride`. **`bypassGovernance` is the high-risk metadata** `setObjectRetention` audit metadata sets `bypassGovernance: true` when the operator used the bypass-shorten path; operators wire alerting on this field per compliance posture. New `objectstore` namespace registered in `audit.FRAMEWORK_NAMESPACES`. **Tests** — 10 new layer-0 audit/observability assertions (smoke 7179→7191) including auditSuccess:false suppresses success-audit but failure-audit still fires. Wiki e2e 178 / per-primitive integration 16 files / eslint clean.
88
+
89
+ - **0.6.52** (2026-05-03) — `b.mtlsCa` serial-number normalisation now rejects gibberish instead of silently stripping non-hex chars to whatever's left. Pre-fix: `_normalizeSerial("xyz-not-hex")` ran `.replace(/[^0-9a-fA-F]/g, "")` which kept just the single `e` and registered a phantom `revoke("e")` row. A typo in an `openssl x509 -noout -serial` paste or any non-serial string silently became a real revocation entry that would land in the next CRL the operator published. **The fix:** the normalizer now strips only the documented operator-paste shapes (leading `0x`, separators `:` / `-` / whitespace) and asserts that what remains is all-hex; anything else throws `mtls-ca/bad-serial`. Real shapes still accepted (`"0xABC123"`, `"AB:C1:23"`, `"AB-C1-23"`, `"abc 123"`, `"abc123"` all normalise to `abc123`); gibberish (`"xyz-not-hex"`, `" "`, `"nope"`) rejects cleanly. **Tests** — 3 new live-CA integration assertions on top of the 41 in `mtls-ca.test.js` (44 total). Smoke 7179 / wiki e2e 178 / per-primitive integration 16 files / eslint clean.
90
+
91
+ - **0.6.51** (2026-05-03) — `b.objectStore.bucketOps` Object Lock readback methods now return clean defaults for "never configured" state instead of throwing the raw HTTP 4xx error. Surfaced via the same live MinIO probing that found v0.6.49: `getObjectLockConfiguration` on a bucket created without `objectLockEnabled` was throwing `HTTP 404 ObjectLockConfigurationNotFoundError` a forced try/catch on the operationally-trivial question "is this bucket lock-enabled?". Same UX problem on `getObjectRetention` (HTTP 400 `NoSuchObjectLockConfiguration` when the object never had retention set) and `getObjectLegalHold` (same error code when never set). **The fix:** new `_isLockNotConfigured` helper recognizes both error shapes; the three `get*` methods catch the error and return: `getObjectLockConfiguration` `{ enabled: false, mode: null, days: null, years: null }`, `getObjectRetention` `{ mode: null, retainUntil: null }`, `getObjectLegalHold` `{ status: "OFF" }` (operationally identical to "no hold ever applied"). Real errors (auth failures, network errors, bucket-not-found) still throw. **Tests** 7 layer-0 mock-server assertions for the not-configured paths (smoke 7172→7179) + 8 live MinIO integration assertions in `_runObjectLockOnEndpoint` and a new no-lock-bucket variant in `_runOnEndpoint` (object-store-sigv4 3947 checks). Wiki e2e 178 / eslint clean.
92
+
93
+ - **0.6.50** (2026-05-03) — `b.objectStore` SigV4 multipart upload subresource-query consistency + live integration coverage. Same-class follow-up to v0.6.49: `lib/object-store/sigv4.js` was the only remaining `URLSearchParams.set(k, "")` empty-value site in the framework — `initiateUrl.searchParams.set("uploads", "")` produced `?uploads=` (with trailing `=`) for the InitiateMultipartUpload subresource. AWS S3 + MinIO both accept `?uploads=` (in contrast to `?retention=`, `?legal-hold=`, `?object-lock=` which strict implementations route to the put-object handler), so this wasn't broken in the real world but the convention drift would mask future regressions and a future stricter S3 implementation could route `?uploads=` differently. Fixed for consistency: `?uploads` is now bare on the wire, identical to the v0.6.49 convention. SigV4 canonicalization unchanged (still presents `uploads=` to the signer per AWS spec). **Live integration coverage** — `test/integration/object-store-sigv4.test.js` `_runOnEndpoint` gains a multipart-upload + get round-trip pass (forces multipart with `multipartThresholdBytes: 1`, `partSizeBytes: 5 MiB`, payload 6 MiB → 2-part flow). Until v0.6.50 the multipart path had only mock-server coverage; the live MinIO assertion locks in the `?uploads`, `?partNumber=N&uploadId=...`, and `?uploadId=...` (CompleteMultipartUpload) wire flow. Smoke 7172 / wiki e2e 178 / per-primitive integration 16 files (object-store-sigv4 3539 checks) / eslint clean.
94
+
95
+ - **0.6.49** (2026-05-03) — `b.objectStore.bucketOps` SigV4 backend wire-form fix for subresource queries (`?lifecycle`, `?cors`, `?object-lock`, `?retention`, `?legal-hold`). The v0.6.47 implementation built URLs via `URLSearchParams.set("retention", "")` which produces `?retention=` (with a trailing `=`) on the wire. Strict S3 implementations (MinIO in particular) interpret the trailing `=` as "this is a body PUT with a query parameter" rather than "this is a subresource request" — the request gets routed to the put-object handler, which then rejects it with `InvalidRequest: Object is WORM protected and cannot be overwritten` instead of routing to the retention/legal-hold handler. **The fix:** `_bucketUrl` and `_objectUrl` now build the URL string manually with `?subresource` (no `=`) for empty-value query keys, then pass it to `new URL(s)`. The URL constructor preserves the bare token in `url.search` (the wire form) while `url.searchParams` still presents it as `subresource=` (so the SigV4 canonicalizer's signing path is unchanged — AWS spec REQUIRES `key=` with the empty `=` in the canonical query string for signature computation). Both behaviors are preserved without forking the URL across the signer + transport. **Live integration coverage** — `test/integration/object-store-sigv4.test.js` gains a third endpoint variant (`_runObjectLockOnEndpoint`) that creates a bucket with `objectLockEnabled: true` against live MinIO and exercises the full surface end-to-end: per-object retention set + get, per-object legal hold ON/OFF round-trip, bucket-level `setObjectLockConfiguration` + `getObjectLockConfiguration` round-trip, `bypassGovernance` retention shortening, and cleanup. Closes the v0.6.47 gap where the new methods only had mock-server coverage. **Layer-0 regression tests** — `test/layer-0-primitives/sigv4-bucket-ops.test.js` gains 3 wire-form assertions on `?retention`, `?legal-hold`, `?object-lock` ("bare subresource, no '=' suffix") to catch this class of regression before it reaches operators. Smoke 7172 / wiki e2e 178 / per-primitive integration 16 files (object-store-sigv4 22→35 checks) / eslint clean.
96
+
97
+ - **0.6.48** (2026-05-03) — doc-artifact catch-up for v0.6.47 Object Lock surface. The v0.6.47 release shipped the lib + tests + wiki + CHANGELOG entry but missed the README + SECURITY.md weave-ins (`feedback_doc_sweep_with_release.md` rule §10 "doc artifacts ride with the release commit"). README "What ships in the box" object-store bullet now mentions S3 Object Lock + per-object retention + legal hold for write-once-read-many compliance workloads. SECURITY.md application checklist gains a sibling line to the existing `b.retention` TTL entry covering the operator's hardening checklist for write-once-read-many object archives (SEC 17a-4, FINRA, HIPAA-shaped retention) `objectLockEnabled: true` at create time only, `COMPLIANCE` mode irrevocable by anyone (including root), `bypassGovernance` requires the `s3:BypassGovernanceRetention` permission. No code changes; doc-only patch.
98
+
99
+ - **0.6.47** (2026-05-03) — `b.objectStore.bucketOps` SigV4 backend gains S3 Object Lock, retention, and legal-hold support for compliance workloads (SEC 17a-4, FINRA, HIPAA retention). New surface on the `sigv4` protocol only (Azure / GCS handle write-once-read-many out-of-band): `create(name, { objectLockEnabled: true })` flips the underlying versioning + WORM at create time (the only point S3 allows it); `setObjectLockConfiguration(name, { mode, days|years })` and `getObjectLockConfiguration(name)` apply / read the bucket-level default retention; `setObjectRetention(name, key, { mode, retainUntil, bypassGovernance? })` and `getObjectRetention(name, key)` apply / read per-object retention dates; `setObjectLegalHold(name, key, "ON"|"OFF")` and `getObjectLegalHold(name, key)` apply / read per-object legal holds. Up-front validation rejects bad inputs at the call site before any request is signed: unknown mode, `days`+`years` together, fractional or negative durations, non-Date or past-dated `retainUntil`, statuses outside `ON`/`OFF` all throw `INVALID_OBJECT_LOCK` / `INVALID_RETENTION` / `INVALID_LEGAL_HOLD` (or `INVALID_KEY`) before TCP. `bypassGovernance: true` adds the `x-amz-bypass-governance-retention: true` header for accounts with the `s3:BypassGovernanceRetention` permission; `COMPLIANCE` mode cannot be shortened or bypassed by anyone (including root) — pick it deliberately. The wiki's Object Store page documents the full surface with COMPLIANCE-vs-GOVERNANCE guidance. **Tests**: layer-0 sigv4-bucket-ops gains 44 new mock-server assertions covering Object Lock header on create, configuration set/get + XML body shape, retention set/get + URL query-arg + body, bypass-governance header propagation, legal-hold round-trip, and the four validation paths (lock-config / retention / legal-hold / key). Smoke 7169 / wiki e2e 178 / eslint clean.
100
+
101
+ - **0.6.46** (2026-05-02) — `b.i18n.messageFormat` ICU MessageFormat parser + evaluator. The pre-v0.6.46 `b.i18n` translation format is JSON-shaped with CLDR plural keys at the JSON level (`one / few / many / other` under a key like `inbox.unread`) covers the simple plural case but not the nested / inline patterns common in real-world translations: gendered selects, plural forms with embedded variables, ordinals, arguments inside cases. **`lib/i18n-messageformat.js`** (new, ~330 LOC) minimal-but-correct ICU MessageFormat parser + evaluator. Supports: `{argName}` simple replacement, `{argName, plural, =N {...} category {...} other {...}}` with optional `offset:N` and `#` placeholder for the plural arg minus offset, `{argName, selectordinal, ...}` (CLDR ordinal categories via `Intl.PluralRules({ type: "ordinal" })`), `{argName, select, caseA {...} other {...}}`, nested arguments inside any case body, ICU-spec apostrophe escaping (`''` → `\'`, `\'{\'` / `\'}\'` for literal braces, `\'X\'` runs to next apostrophe). CLDR cardinal + ordinal categories via `Intl.PluralRules` per locale (cached). **Out of scope** (operators with these reach for the full `messageformat` package): inline `number` / `date` / `time` formatters (use `formatNumber` / `formatDate` from `b.i18n` separately and inline the result), choice-format (deprecated by ICU in favor of plural), custom argument types. Bespoke parser, no vendoring needed (the `messageformat-parser` subset would have been ~50 KB; bespoke is ~330 LOC + zero npm dep). **`b.i18n.t(key, vars, opts)` integration** auto-detects MessageFormat-shaped entries via `messageFormat.looksLikeMessageFormat(template)` (regex match for `{name, plural / select / selectordinal, ...}`); operators force the path with `opts.messageFormat = true`. Plain `{var}` interpolation and existing CLDR plural-shaped JSON entries continue to work unchanged on the legacy interpolator. **Tests**: 54 layer-0 assertions covering surface, plain literal pass-through, simple argument interpolation (including null / undefined / number arg handling), plural exact-match (`=N`), CLDR cardinal categories across English + Russian (`one / few / many / other`), ordinal categories (`1st / 2nd / 3rd / 4th / 21st`), select with operator-supplied + missing arg `other` fallback, nested plural-inside-select-inside-plural, ICU apostrophe escaping (`\'\'` literal apostrophe; `\'{name}\'` literal braces; `\'#\'` literal hash inside plural body), parse rejection (non-string input, plural without `other`, select without `other`, unsupported type, missing argument name), evaluator rejection (plural with non-numeric arg `BAD_VAR`), `looksLikeMessageFormat` detection (plural / select / selectordinal yes; plain `{var}` and plain text no), end-to-end through `b.i18n.t()` (plural-shape, select, simple `{var}` legacy path). Smoke 7125 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
102
+ - **0.6.45** (2026-05-02) — `b.mtlsCa` revocation registry + signed CRL generation. The wiki has documented "keeps the revocation list in `cert_revocations`" since the primitive landed, but the actual surface didn\'t exist operators couldn\'t revoke an issued cert without rebuilding the CA from scratch. This patch adds the revocation primitives that close that gap. **`ca.revoke(serialNumber, opts?)`** records a revocation in `dataDir/revocations.json` with serial / reason / revokedAt timestamp. Serials are normalized (strips `0x` prefix, removes non-hex chars, lowercases) so operators paste any of `"0xABC123"` / `"AB:C1:23"` / `"abc123"` and they all hit the same row. Reason names map to RFC 5280 numeric codes (`"keyCompromise"`1, `"superseded"`4, `"cessationOfOperation"`5, full set: `unspecified / keyCompromise / caCompromise / affiliationChanged / superseded / cessationOfOperation / certificateHold / removeFromCRL / privilegeWithdrawn / aACompromise`). Idempotent repeated revoke() of the same serial preserves the original revokedAt. **`ca.isRevoked(serialNumber)` / `ca.getRevocations()`** registry queries, also serial-format-agnostic. **`ca.generateCrl(opts?)`** `{ crlPem, thisUpdate, nextUpdate, entryCount, path }`. Builds an RFC 5280 X.509 CRL signed with the CA private key by routing through `engine.generateCrl({ caCertPem, caKeyPem, revocations, thisUpdate, nextUpdate })`. The default engine implementation lives at `lib/mtls-engine-default.js`\'s new `generateCrl` (uses the vendored peculiar/x509 library\'s `X509CrlGenerator.create`, signs with the same algorithm `_selectAlgorithm` chose for the CA). Default `nextUpdate` is 7 days after `thisUpdate`; operators publishing at a different cadence pass explicit dates. CRL persists to `dataDir/ca.crl` by default; pass `{ persist: false }` to inspect without writing. Operators serve `ca.crl` at the CRL distribution point referenced from issued certs; revocation status checks at the TLS layer pick it up server-side. **Tests**: 19 new live integration assertions on top of the existing 22 in `test/integration/mtls-ca.test.js` covers revoke happy path (records the right shape with reasonCode mapped), serial-format normalisation (`0xABC123` `abc:12:3` `ABC123`), idempotency (duplicate revoke preserves revokedAt), error paths (empty serial throws `bad-serial`, unknown reason throws `bad-reason`), CRL generation against the real engine + real CA (PEM output decodes to non-trivial DER, entryCount matches registry size, default nextUpdate ~7 days out, persisted to `dataDir/ca.crl`, regenerate after a new revoke picks up the new entry). Smoke 7071 / wiki e2e 178 / per-primitive integration 16 files (mtls-ca up from 22→41 checks) / wiki integration 32 / shellcheck clean / eslint clean.
103
+ - **0.6.44** (2026-05-02) — `b.bundler` engine surface for ESM module-graph bundling. The previous `b.bundler` was a content-hash + manifest pipeline only operators wanting tree-shake / minify / source-map / multi-file ESM bundling pre-concatenated their input or ran an external bundler. This patch adds an explicit pluggable engine layer: `b.bundler.create({ engine })` accepts any object implementing `{ name, transform(entryPath, contentBuf) → { content, sourceMap?, imports? } }` between the file read and the cache-bust hash. **`b.bundler.engine.passthrough`** is the defaultsame byte-verbatim behavior every pre-v0.6.44 caller got, no breaking change. **`b.bundler.engine.fromEsbuild(esbuild, opts?)`** wraps a supplied esbuild module (the operator\'s own `require("esbuild")` or `require("esbuild-wasm")`) into the engine contract routes through `esbuild.build({ entryPoints, write: false, ...opts })`, picks the JS output + `.map` sibling out of `outputFiles`, returns `{ content, sourceMap }` for the framework to hash + write atomically. Source maps land as `<hashed-filename>.map` siblings; the bundler\'s `outputs[i].sourceMapPath` reports the on-disk path. **The framework deliberately does NOT vendor `esbuild-wasm` itself.** The wasm blob is ~10 MB — bigger than every other vendored framework dep combined; it would 3–5× the `@blamejs/core` npm tarball for a build-time tool most operators only need at deploy time. Treating esbuild as an operator-supplied driver matches `b.externalDb` (operator brings the pg / mysql client), `b.mtlsCa.create({ engine })` (operator can swap to HSM-backed signer), `b.objectStore.bucketOps.create({ protocol })` (operator picks the cloud) — the framework owns the integration seam, the operator brings the heavy machinery. Operators who want ESM bundling install esbuild themselves (`devDependency` is the typical pick — bundling is a deploy-time concern, not a runtime one), pass it via `bundler.engine.fromEsbuild`. Operators with a different bundler (rollup / swc / vite / parcel / a custom JS-only minifier) implement the same `{ name, transform }` interface directly. **Tests**: 25 layer-0 assertions covering surface (`b.bundler.engine.passthrough`, `b.bundler.engine.fromEsbuild` factory), passthrough as default behavior unchanged, engine validation rejects bad shapes (non-object / missing transform / missing name / fromEsbuild rejects non-esbuild input), custom engine transform applied + hashed AFTER transform (so the hash reflects the bundled output, not the source), source-map sibling write at `<hashed>.<ext>.map`, fromEsbuild adapter passes entryPoints + forces `write: false` + threads operator opts (minify, sourcemap) through to esbuild + correctly identifies the JS output vs the .map sibling among `outputFiles`, empty esbuild output throws `bundler/engine-empty`. Smoke 7071 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
104
+ - **0.6.43** (2026-05-02) — AWS SQS queue backend. `b.queue` gains a third backend alongside `local` (SQLite) and `redis`: `protocol: "sqs"` for operators on AWS who want a fully-managed queue without standing up a Redis cluster. Wire protocol is AWSJsonProtocol_1.0 over HTTPS (Content-Type `application/x-amz-json-1.0`, `X-Amz-Target: AmazonSQS.<Action>`), SigV4-signed via the framework\'s service-agnostic `lib/object-store/sigv4.js`. Action mapping: `enqueueSendMessage`, `leaseReceiveMessage` (long-poll up to `WaitTimeSeconds`), `extendLease ChangeMessageVisibility`, `complete DeleteMessage`, `fail → ChangeMessageVisibility(VisibilityTimeout=0)` for immediate re-delivery (DLQ routing then happens server-side via the queue\'s `RedrivePolicy` attribute, configured at queue creation outside the framework), `size → GetQueueAttributes(ApproximateNumberOfMessages)`, `purge PurgeQueue`. Queue-name → URL: by default `https://sqs.{region}.amazonaws.com/{accountId}/{queueName}`; operators with cross-account / FIFO / VPCE endpoints pass an explicit `queueUrlByName(name) url` resolver. STS session tokens propagate as `x-amz-security-token`. Sealing posture matches local + redis: payloads pass through `cryptoField.sealRow("_blamejs_jobs", row)` before SendMessage so SQS only ever sees the sealed envelope. **Out of scope for the SQS adapter** (operator wiring required, framework-side guard rails not appropriate for a managed service): DLQ inspection (`dlqList / dlqRetry / dlqSize`)SQS DLQs are separate queues that operators wire as a second framework backend; flow / cron / parent-child dependencies SQS has no native flow primitives so those stay on `local` / `redis`; `sweepExpired` SQS handles visibility-timeout server-side. `DelaySeconds` is clamped to the 900-second SQS hard cap; `MaxNumberOfMessages` is clamped to 10 (SQS\'s per-call ceiling). Removed `sqs` from `DEFERRED_PROTOCOLS`. **Tests**: 41 layer-0 mock-based assertions covering factory validation (rejects missing region / accessKeyId / secretAccessKey / accountId-without-resolver; accepts `queueUrlByName` as accountId substitute), full SendMessage wire shape (Content-Type / X-Amz-Target / Authorization carries `Credential=AKIATEST/.../sqs/aws4_request` / QueueUrl built from accountId+name / MessageBody is JSON-serialized sealed envelope with `_id` + `queueName`), DelaySeconds clamping at 900s, ReceiveMessage round-trip with sealed-envelope unsealing back into `{ jobId, queueName, payload, receiptHandle, leaseExpiresAt, ... }`, MaxNumberOfMessages clamping at 10, complete/extendLease/fail (all assert receiptHandle plumbing + correct VisibilityTimeout values), MISSING_RECEIPT thrown on missing receiptHandle, size + purge, custom queueUrlByName resolver (cross-account), STS session token surfacing as `x-amz-security-token` header. Smoke 7046 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
105
+ - **0.6.42** (2026-05-02) — wiki schema-then-example sweep, batch 3 (testing + format-helpers + backup-restore). Closes the W1 backlog item every wiki page that documents an operator-facing primitive now has its primary primitive surfaces in the four-piece template (heading + opts model + description prose + example). New primitive sections: **testing** `b.testing.mockReq(opts?)` / `.mockRes()`, `b.testing.fakeClock(initialMs?)` (returns `{ now, advance, set }`), `b.testing.captureAudit()` (with corrected `.byAction` / `.captured` surface wiki had non-existent `.events`), `b.testing.captureObservability()` (with corrected `.byName` / `.captured` surface wiki had non-existent `.byEvent` / `.spans`), `b.testing.fakeHttpClient(handler)`, `b.testing.runMiddleware(fn, req, res)`, `b.testing.waitFor(predicate, opts?)`, `b.testing.tempDir(name?)`. **format-helpers** `b.csv.parse(input, opts?)` / `.stringify(rows, opts?)`, `b.uuid.v4 / .v7 / .parse / .isValid`, `b.slug(input, opts?)` / `.unique(input, exists, opts?)`, `b.time.toParts / .format / .addDays / .addMonths / .startOfDay / .endOfDay / .diffDays / .parseISO / .tzOffsetMs`, `b.archive.zip()`, `b.pagination.cursor(query, opts)` (corrected from non-existent `b.pagination.create({...}).parse(...)` shape actual API is `cursor(query, opts) { items, nextCursor, hasMore }` taking a db Query), `b.forms.render(spec, opts?)` / `.validate(spec, body)` / `.generateCsrfToken / .verifyCsrfToken`. **backup-restore** `b.backup.create(opts)`, `b.restore.create(opts)`. Surfaced + corrected real API drifts: `b.testing.captureAudit().byAction` (not `.byActions`), `b.testing.captureObservability().byName` (not `.byEvent`), `b.pagination.cursor(query, opts)` (not `b.pagination.create({...})`), `b.pagination.encodeCursor(state, secret)` argument order (state first, secret second). Wiki primitive-runtime gate up from **147186** clean exec runs (+39 in this patch alone, +111 across v0.6.39v0.6.42 from the v0.6.38 baseline of 75). Smoke 7004 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
106
+ - **0.6.41** (2026-05-02) — wiki schema-then-example sweep, batch 2 (welcome + crypto-vault + network-crypto + safe-parsers). New primitive sections following the four-piece template (heading + opts model + description prose + example): **welcome** — `b.createApp(opts)` (the operator entry point that wires the dependency-ordered boot vault external-DB cluster lease framework schema local DB router middleware stack operator routes error handler). **crypto-vault** `b.vault.seal(plaintext) / .unseal(sealed)`, `b.cryptoField.registerTable(name, opts)` (sealedFields + derivedHashes registry the db-query layer reads on every read/write). Removed the API-drift `b.webhook.signer.create({...})` / `b.webhook.verifier.create({...})` examplesactual API is `b.webhook.signer({...})` and `b.webhook.verifier({...})` documented at outbound-http; crypto-vault now cross-links instead of restating. **network-crypto** `b.mtlsCa.create(opts)` (lib allow-list reflects: `dataDir` / `vault` / `paths` / `caKeySealedMode` / `generation` / `engine` corrected from prior `sealedMode / caValidityDays / leafValidityDays / audit` shape that wasn\'t in the validate-opts list), `b.pqcGate.create(opts)` (corrected: `internalPort` / `internalHost` / `bypass` / `clientHelloTimeoutMs` / `maxClientHelloBytes` / `log` plus underscore-prefixed test-injection points; previously documented `externalPort` / `mode` / `acceptable` / `audit` weren\'t in the lib allow-list at all), `b.pqcAgent.create(opts?)` (with the `b.pqcAgent.agent` and `.enforced` shared-singleton handles documented). **safe-parsers** `b.safeJson.parse(text, opts?)`, `b.safeBuffer.normalizeText(input, opts?)` / `.boundedChunkCollector(opts)` / `.secureZero(buf)` (corrected to use `bytesCollected()` not `bytes()` per the actual API), `b.safeSql.validateIdentifier(value, opts?)` (corrected from `b.safeSql.identifier` which doesn\'t exist), `b.safeSchema` builder factories (`object` / `string` / `number` / `boolean` / `array` / `literal` / `union` with refinements). Wiki primitive-runtime gate up from **130 147** clean exec runs (+17 in this patch alone). Smoke 7004 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
107
+ - **0.6.40** (2026-05-02) — wiki harness upgrade + H1 batch 1 complete. Closes the gap surfaced in v0.6.39: the wiki primitive-runtime gate exec-runs every code block with only `b` in scope; compound-primitive examples that reference operator-supplied `app` / `users` / `template` / `metrics` / `currentToken` / `largeBuffer` / `body` / `loginUrl` / `meUrl` / `authMiddleware` / `loginHandler` identifiers couldn\'t round-trip cleanly. **Harness extension** (`examples/wiki/test/run-example.js`) — added stub bindings for those operator-side identifiers: `app` (Express-shape stub with `use / get / post / put / patch / delete / head / listen / close`), `users` (db-model stub with `create / findOne / findMany / updateOne / deleteOne / count`), `template` (template-engine stub), `metrics` (counter / histogram / gauge stubs), `currentToken` (token-source function), `largeBuffer` / `body` (request-body shape), `loginUrl` / `meUrl` (URL strings), `authMiddleware` / `loginHandler` (passthrough middleware functions). With those bindings every multi-step compound-primitive example reaches the framework boundary it cares about. **`routing` page** converted to `<h3>b.X.Y(opts)</h3>` primitive sections for the full surface: `new b.router.Router(opts?)`, `router.METHOD(path, spec?, ...handlers)` (covers schema-validated routes via `b.safeSchema`), `router.openapi(opts)`, `b.render.htmlString / .json / .text / .redirect`, `b.htmlBalance.check`, `b.staticServe.create(opts)` (corrected from prior `b.staticServe(rootDir, opts)` shape actual API is opts-only with required `root`), `b.errorPage.create(opts)`, `b.requestHelpers.extractActorContext / .parseQualityList / .parseListHeader`. **`middleware` page** primitive sections for `b.middleware.rateLimit / .csrfProtect / .cspNonce / .bodyParser / .bodyParser.raw / .sse(handler, opts?) / .requestLog / .networkAllowlist`, `b.cookies.create(opts?)` / `.parse(headerValue)`, `b.validateOpts`. Several real API drifts surfaced + corrected against the lib allow-lists: `rateLimit` uses `refillPerSecond` (not `refillPerSec`), accepts memory-backend `burst` + cluster-backend `limit / windowMs / pruneIntervalMs`; `csrfProtect` uses `tokenLookup / fieldName` (not `session / paramName`); `requestLog` uses `logger` (not `log`); `cookies.create` accepts `vault` for sealing values; `sse(handler, opts?)` requires the handler as the FIRST positional arg, not in opts. **`outbound-http` page** primitive sections for `b.httpClient.request(opts)` (with subsections for `maxRedirects`, `multipart`, `before / after` interceptors), `b.httpClient.cookieJar.create`, `b.ssrfGuard.checkUrl / .classify`, `b.safeUrl.parse`, `b.webhook.signer / .verifier`. **Wiki primitive-runtime gate up from 87 130 clean exec runs** (+43 in this patch alone, +55 across v0.6.39 + v0.6.40 from the v0.6.38 baseline of 75). Smoke 7004 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
108
+ - **0.6.39** (2026-05-02) — wiki schema-then-example sweep, batch 1 partial. The original H1 plan was to convert the prose-style headings on `routing` / `middleware` / `outbound-http` to `<h3>b.X.Y(opts)</h3>` primitive sections wholesale. In practice the wiki harness exec-runs every example with only `b` in scope examples that reference operator-supplied identifiers (`app`, `router`, `req`, `res`) fail the runtime gate, which would have meant rewriting every multi-step example to use only `b.X.Y(...)` calls. Rather than ship doc that papers over how operators actually use the framework, this patch lands the *self-contained* primitive sections only pure-function primitives whose example fits in two or three lines: **`b.requestHelpers.parseQualityList(value, opts?)`** (RFC 9110 §12.5 Accept-* parser, returns `[{value, q}, ...]` sorted by q desc), **`b.requestHelpers.parseListHeader(value, opts?)`** (comma-separated header parser with trim / lowercase / unique opts), **`b.htmlBalance.check(html)`** (structural HTML check returning `null` when balanced or `{code, message, line, column}` when not), **`b.ssrfGuard.classify(ip)`** (offline IP classifier returning `"private"` / `"loopback"` / `"link-local"` / `"cloud-metadata"` / `"ula"` / `"reserved"` / `"public"`), **`b.safeUrl.parse(input, opts?)`** (URL shape + protocol gate, defaults to https-only, rejects userinfo by default). Each new section follows the four-piece template (heading + opts model where applicable + description prose + example). Wiki primitive-runtime gate up from 75 87 clean exec runs. The compound primitives (`b.middleware.bodyParser`, `b.middleware.csrfProtect`, `b.httpClient.request`, `b.webhook.signer / .verifier`, `b.cookies.create`) stay in the page\'s prose H2 sections for now they need a harness convention upgrade (multi-line examples with operator-stub identifiers) before they round-trip cleanly through the runtime gate; that lands in a follow-up patch with the matching harness change. Smoke 7004 / wiki e2e 178 / wiki primitive examples 87 clean / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
109
+ - **0.6.38** (2026-05-02) — OTLP gRPC + protobuf transport. **`b.logStream` `otlp-grpc` sink** — first-class `protocol: "otlp-grpc"` for `b.logStream.init({ sinks: ... })`. Companion to the existing `otlp` HTTP/JSON sink: same OTel Logs Data Model, but transported over HTTP/2 + gRPC framing for the higher-throughput path operators reach for when they're pushing >100K logs/s straight to a collector. **`lib/protobuf-encoder.js`** (new) — minimal proto3 wire-format encoder (write-only no decoder ships). Implements the four wire types the OTel logs schema needs (varint, 64-bit fixed, length-delimited, plus reserved spot for 32-bit fixed); helpers for `uint32 / uint64 / bool / fixed64 / double / string / bytes / embeddedMessage / repeatedMessage`. Operators reach for it when constructing protobuf bodies for any external service that accepts proto over HTTP — gRPC, AWS sigv4-protobuf, GCP protobuf APIs. Consistent with the framework's vendoring stance: zero npm runtime deps, zero protobuf parser shipped, the encoder is the framework's own. **`lib/log-stream-otlp-grpc.js`** (new) encodes `ExportLogsServiceRequest` per the OTel logs.proto schema (Resource ScopeLogs LogRecord → AnyValue / KeyValue), wraps in gRPC framing (1-byte compression flag + 4-byte big-endian length + protobuf body), POSTs over `node:http2` to `/opentelemetry.proto.collector.logs.v1.LogsService/Export` with `content-type: application/grpc+proto` + `te: trailers`. Reads `grpc-status` + `grpc-message` from response trailers; non-zero gRPC status surfaces as `HTTP_ERROR` with the gRPC error code + message preserved. Same back-pressure semantics as the JSON sink (ring buffer, batched flush on size or maxBatchAgeMs, exponential-backoff retry, drop-on-overflow with operator `onDrop`). Severity mapping debug=5 / info=9 / warn=13 / error=17 per spec. Single HTTP/2 session per sink, kept alive across many Export calls; recreated on disconnect. **Tests**: 27 layer-0 protobuf-encoder assertions verifying canonical encoding (varint(150) = 96 01, varint(300) = ac 02, varint(16384) = 80 80 01; tag computation; string field shape; embedded message length-delimited framing; repeated message; BigInt-path varint for >2^53 ranges; rejection of negative varints). 26 layer-0 OTLP gRPC assertions covering frame shape, log-record encoding (verifies "hello" / "test-service" / "INFO" UTF-8 bytes appear in the output), AnyValue type-tagging per OTel oneof spec (string→1, bool→2, int→3, double→4, bytes→7), full HTTP/2 roundtrip against an h2c mock server (path / content-type / te / framing / body length-prefix all asserted on the wire), gRPC server-side error trailer (status 13 + message text surfaces through `onDrop`), URL validation. Smoke 7004 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
110
+ - **0.6.37** (2026-05-02) — Azure + GCS bucket-ops parity. **`b.objectStore.bucketOps`** is now a protocol-dispatching factory: pass `{ protocol: 'sigv4' | 'azure-blob' | 'gcs', ... }` to get a service-scoped client for the matching cloud. Previously SigV4 only. **`lib/object-store/azure-blob-bucket-ops.js`** (new) Azure Storage container lifecycle via Shared Key auth (reuses `azure-blob.js`'s `signRequest`): `create(name, {publicAccess?})` (PUT `/{container}?restype=container`), `delete(name)` (DELETE), `list({prefix?, maxResults?})` (GET `/?comp=list` with XML response parsed into `[{ name, lastModified, etag, leaseStatus, leaseState, publicAccess }]`), `setCorsRules(rules)` (PUT `/?restype=service&comp=properties` Azure CORS is account-level, not per-container). `setLifecycle` is intentionally not implemented because Azure Storage lifecycle management policies live on Azure Resource Manager (`management.azure.com`) and require Azure AD bearer-token auth a different scheme entirely; calling it throws `NOT_SUPPORTED` with operator guidance pointing at Terraform / Bicep / az CLI. **`lib/object-store/gcs-bucket-ops.js`** (new) GCS bucket lifecycle via service-account JWT exchanged for an OAuth2 access token (reuses `gcs.js`'s `_signJwt`); admin-scoped (`devstorage.full_control`) so list-buckets + create + delete + lifecycle + CORS all succeed: `create(name, {location?, storageClass?, iamConfiguration?})` (POST `/storage/v1/b?project=`), `delete(name)`, `list({prefix?, maxResults?, pageToken?})`, `setLifecycle(name, rules)` (PATCH bucket with `lifecycle: { rule: [{ action, condition }] }`; rules support `Delete` / `SetStorageClass` / `AbortIncompleteMultipartUpload` actions), `setCorsRules(name, rules)` (PATCH bucket with `cors: [{ origin, method, responseHeader, maxAgeSeconds }]`). **Bucket-name validation** matches each cloud's spec: Azure containers (3-63 lowercase alphanumeric + hyphens, no consecutive hyphens, must start/end with alphanumeric); GCS buckets (3-63 lowercase + digits + hyphens + underscores + dots, no consecutive dots, no `goog` prefix). Bad names are rejected at the call site before the request leaves the process. **404 / 409 semantic mapping** both modules treat 404 on delete as "already gone" (returns false), 409 on create as `BUCKET_ALREADY_OWNED`; everything else surfaces with the original HTTP status preserved on the thrown ObjectStoreError. **Tests**: 47 layer-0 mock-based assertions for Azure (surface, factory validation, container-name validation, create/delete/list wire shape including XML response parsing, CORS validation + wire shape, setLifecycle NOT_SUPPORTED guidance) + 50 layer-0 mock-based assertions for GCS (surface, factory validation including missing service-account / project, bucket-name validation, create/delete/list wire shape including JWT bearer auth, lifecycle validation + JSON body shape, CORS shape). HTTP mock servers record every request shape so signing, URL params, headers, and body-format are all asserted. **Wiki page** `examples/wiki/seeders/prod/pages/object-store.js` updated end-to-end with all three clouds' bucket-ops examples and the Azure-vs-Resource-Manager lifecycle gap documented. Smoke 6951 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
111
+ - **0.6.36** (2026-05-02) — `b.db.from("schema.table")` cross-schema chain. **`b.db.from("audit.events")`** — the chainable Query builder now accepts a two-part `schema.table` identifier. Both halves validated separately as SQL identifiers (rejects three-part names, empty parts, identifiers with embedded quotes / SQL keywords); both halves wrapped in `"..."` when interpolated so the generated SQL is `SELECT * FROM "audit"."events" WHERE ...` etc. The bare `b.db.from("users")` form still works unchanged. Sealed-field registry lookup tries the qualified name (`audit.users`) first when schema is set, falls back to the bare table operators registering `cryptoField.registerTable("audit.users", { sealedFields: ... })` get per-schema sealed columns; existing table-name-only registrations keep working. Use cases: cross-schema joins on Postgres external-db (`public.users` vs `audit.events` from one app), SQLite `ATTACH DATABASE` (per-classification audit / archive databases mounted on the side and queried through the same Query API). **Tests**: 14 layer-0 SQL-shape assertions against a fake DB (verify schema-qualified SELECT / INSERT / UPDATE / DELETE / count / sub-select-on-rowid all emit `"schema"."table"`; reject three-part / empty / invalid identifiers; bare table name preserved unqualified) + 7 layer-2 end-to-end assertions against a real `ATTACH DATABASE` schema (insert / select / count / update / delete round-trip via `b.db.from("audit.events")`). Smoke 6854 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
112
+ - **0.6.35** (2026-05-02) — `cluster-provider-db` MySQL dialect. **`b.cluster.create({ provider: clusterProviderDb.create({ dialect: "mysql", ... }) })`** operators on MySQL no longer have to supply their own provider; the framework's default DB-row leader-election provider now speaks all three of postgres / sqlite / mysql. The MySQL acquireLease shape uses `INSERT INTO _blamejs_leader (...) VALUES (...) ON DUPLICATE KEY UPDATE col = IF(expiresAt < ?, VALUES(col), col), ..., expiresAt = IF(expiresAt < ?, VALUES(expiresAt), expiresAt)` so a still-valid lease is preserved untouched and an expired one is overwritten atomic at the row level followed by a `SELECT FROM _blamejs_leader WHERE scope='leader'` to read who holds (MySQL has no `RETURNING`). The expiresAt assignment runs LAST so per-column IF() predicates evaluate against pre-update row state. Renew uses `UPDATE ... SET expiresAt=?, endpoint=? WHERE scope='leader' AND nodeId=? AND leaseId=?` followed by a check-SELECT to surface takeover races as `LEASE_LOST`. Schema generation: `BIGINT` int columns (was `INTEGER` for SQLite), `VARCHAR(64)` for primary-key text columns and `VARCHAR(255)` for body text (MySQL needs explicit lengths on PRIMARY KEY columns), `CHECK (scope = 'leader' / 'state')` constraint dropped on MySQL because some MariaDB / MySQL 5.x versions parse-then-silently-drop CHECK clauses (would surface as version drift); the constant-scope invariant is enforced by application code anyway. Placeholder style auto-flips to `?` for MySQL (Postgres / SQLite continue to use `$1..$N`). **Tests**: 16 layer-1 assertions exercising the MySQL dialect path against a fake mysql-shaped driver (`_makeFakeMysqlDriver` in `test/helpers/drivers.js`) that emulates `INSERT ... ON DUPLICATE KEY UPDATE` with the per-column `IF()` semantics; covers acquire-empty, blocked-while-held, renew-no-fencing-bump, takeover-with-fencing-bump, old-leader-renew-throws-LEASE_LOST, currentLeader, releaseLease plus a SQL-shape audit (validates VARCHAR primary keys, ON DUPLICATE KEY syntax, ?-placeholders, IF() gating). 14 live integration assertions against the docker MySQL 8.4 container via a docker-exec-based external-db driver shim (no npm mysql client; the framework already requires operator-supplied driver wiring, the shim demonstrates one path) covering ensureSchema, acquireLease, blocked-second-node, currentLeader, renewLease, release, takeover-after-expiry, fencingToken bump, old-leader LEASE_LOST. Smoke 6833 / wiki e2e 178 / per-primitive integration 16 files / wiki integration 32 / shellcheck clean / eslint clean.
113
+ - **0.6.34** (2026-05-02) — distributed pub/sub primitive. **`b.pubsub`** single API for cross-node fan-out across the framework, with three backends. `local` dispatches in-process to registered handlers (zero coordination overhead for single-node deploys); `cluster` polls a shared `_blamejs_pubsub_messages` table at `pollIntervalMs` (default 100ms) and dispatches new rows past `lastSeenId` from other nodes (publishedBy=self filter prevents loopback); `redis` opens a SUBSCRIBE-mode connection on `lib/redis-client.js` (now with new push-message demultiplexing — server-pushed `["message", channel, payload]` and `["pmessage", pattern, channel, payload]` arrays route through `setOnPushMessage` instead of consuming a pending request slot, while SUBSCRIBE / UNSUBSCRIBE acks still flow through the normal command pipeline). Per-instance nonce stamped on outgoing redis payloads so the SUBSCRIBE socket recognizes its own publishes and skips the loopback (without it every same-instance publish would fire local handlers twice). Operator API: `ps.subscribe(channel, handler) token`, `ps.subscribePattern(pattern, handler) token` (glob-style on local + cluster, native PSUBSCRIBE on redis), `ps.unsubscribe(token)`, `await ps.publish(channel, payload) { local, remote }`, `await ps.close()`. `topicPrefix` opt scopes channel names so independent pubsub instances sharing a backend (cache invalidation + websocket channels + custom) don't collide. Handler errors are caught + logged via the framework's boot logger; they never abort dispatch to other handlers on the same channel. **`lib/websocket-channels.js`** replaces the inline cluster-poll-and-fan-out logic with `b.pubsub` consumption. The hub now owns one pubsub instance per primitive; cross-node delivery is `pubsub.subscribe` per channel the hub joins, with the hub's `_localDispatch` as the handler. Per-channel pubsub subscription is refcounted by local conn count (subscribe on first conn, unsubscribe on last). The `_blamejs_ws_messages` table is renamed to `_blamejs_pubsub_messages` (column `channel` `topic`) reflecting the generalization; pre-v1, no compat shim operators upgrading wipe the previous table (which carried only ephemeral fan-out rows with default 60s retention). **`b.cache.create({ invalidationPubsub })`** passing a `b.pubsub.create()` instance auto-publishes on every successful `del` / `clear` / `invalidateTag`, and subscribes for the same events so other cache instances on other nodes (or processes sharing the pubsub backend) react locally — primarily useful for the memory backend so stale per-node entries don't survive a global tag wipe. Re-entrancy guard prevents inbound invalidation events from re-publishing (no fan-out loops). Tests: 29 layer-0 assertions covering local + cluster behavior, topicPrefix isolation, pattern subscribe, handler error isolation, post-close error path, end-to-end cache invalidationPubsub fan-out via local pubsub. 10 live integration assertions against the docker redis container covering single-instance round-trip, PSUBSCRIBE pattern, cross-instance fan-out, and cache invalidation through redis PUB/SUB. Smoke 6817 / wiki e2e 178 / per-primitive integration 15 files / wiki integration 32 / shellcheck clean / eslint clean.
114
+ - **0.6.33** (2026-05-02) — log-stream sinks parity sweep. **`b.logStream` syslog sink (RFC 5424)** — first-class `protocol: "syslog"` for `b.logStream.init({ sinks: { ... } })`; new module `lib/log-stream-syslog.js`. URL-driven transport selection: `udp://host:514` / `tcp://host:514` / `tls://host:6514`. UDP is best-effort one-datagram-per-record; TCP / TLS use RFC 6587 octet-counting framing (`<length> <message>`) so collectors that prefer it over the older non-transparent newline framing parse cleanly. TLS is TLS 1.3 minimum; operators with private CAs pass `ca` (PEM string or array) for trust pinning, `servername` for SNI override (auto-suppressed on IP literals per the v0.6.28 redis-client convention). Outgoing records are formatted with PRI = `(facility << 3) | severity`, default facility `local0` (16), severity mapped from the framework's level field (debug=7 / info=6 / warn=4 / error=3); operators override via `facility`, `appName` (default `blamejs`), `procId` (default `process.pid`), `hostname` (default `os.hostname()`), `structuredData` (default `-`). Meta is JSON-encoded into the MSG body so the structured payload survives the wire as a single token. TCP / TLS reconnect with exponential backoff (default 250ms→30s); records buffer during the down window with `bufferLimit`-bounded oldest-drop semantics, replay on reconnect. `close()` waits up to 3s for an in-flight (re)connect to drain the buffer before tearing down without this the slower TLS handshake raced fire-and-forget shutdown emits and silently dropped records. Operator-supplied `onDrop({reason, batch, error})` surfaces every drop class (`overflow` / `udp-send-error` / `write-error` / `sink-closed`). Removed `syslog` from `DEFERRED_PROTOCOLS`. **`b.logStream` cloudwatch sink: `autoCreate`** — pass `{ autoCreate: true }` to have the framework issue `CreateLogGroup` + `CreateLogStream` on first emit so operators provisioning collectors via env vars / runbooks don't need a separate aws-cli step. Idempotent: AWS's `ResourceAlreadyExistsException` is treated as success on both calls. Hard failures (5xx, AccessDenied, etc.) drop the batch with `onDrop` reason `autocreate-failed`. Default remains `autoCreate: false` so the AWS posture (operator pre-creates via aws / CDK / Terraform) stays the recommended path; `autoCreate` exists for ephemeral / dynamic-stream deployments where pre-provisioning is impractical. **Test suite**: 19 new framework-level cloudwatch assertions cover autoCreate fires both Create calls in order before PutLogEvents, ResourceAlreadyExists doesn't abort the post-create PutLogEvents, hard 5xx during CreateLogGroup drops the batch via `onDrop`, autoCreate=false skips Create calls entirely. Live syslog integration covers UDP / TCP wire delivery against the docker syslog-ng container (`/var/log/blamejs-test.log`) plus an in-process `tls.createServer` receiver that asserts the on-the-wire RFC 6587 octet-counting framing + RFC 5424 PRI / timestamp / structured-data slot the framework emits over TLS. **Test infrastructure fix**: `docker/init/generate-certs.sh` was overwriting an existing CA when re-run with `.complete` removed, invalidating every previously-issued leaf cert; now reuses an existing `(ca.crt, ca.key)` pair across `.complete`-only resets and only generates a fresh CA when neither file is present. Smoke 6790 / wiki e2e 178 / per-primitive integration 14 files (log-stream up from 16→18 checks, cloudwatch suite up from 39→58 assertions) / wiki integration 32 / shellcheck clean / eslint clean.
115
+ - **0.6.32** (2026-05-02) — E1 from the v0.6.x scope plan. **`b.cache` Redis backend**first-class `backend: "redis"` for `b.cache.create({...})`, no operator-supplied glue needed. New module `lib/cache-redis.js` consumes `lib/redis-client.js` directly; storage layout: `<namespace>:e:<key>` (STRING, JSON-encoded value, PEXPIREAT-bounded), `<namespace>:t:<tag>` (SET, cacheKeys carrying that tag — powers `invalidateTag` fan-out), `<namespace>:k:<key>:tags` (SET, tags this key carries powers per-key tag cleanup on `del`/`set`-overwrite, expires alongside the entry). TTL is enforced by Redis itself (PEXPIREAT) so the framework's sweeper is a no-op for this backend; sliding TTL bumps the entry's expiry on every read when `slidingTtl: true` AND `ttlMs` is finite. New opts on `cache.create`: `redisUrl` (required when `backend: "redis"`), `redisPassword`, `redisUsername`, `redisTls`, `redisCa` (private-CA trust pinning per v0.6.28), `redisServername`, `redisConnectTimeoutMs`, `redisCommandTimeoutMs`, `redisMaxReconnectAttempts`. Concurrency: invalidateTag filters out ghost entries (key already PEXPIRE'd from the keyspace but lingering in a tag SET) by EXISTS-checking before del. Lazy connect `cache.create({backend:"redis"})` stays sync-safe; first op opens the socket. Tests: 10 new live integration assertions on top of the existing memory + custom-backend coverage (set+get round-trip, has, del, complex JSON value round-trip, short-TTL Redis-side expiry, multi-tag invalidateTag fan-out + preservation of untagged entries, wrap() single-flight memoization through Redis). Smoke 6780 / wiki e2e 178 / per-primitive integration 14 files (cache up from 15→25 checks) / wiki integration green.
116
+ - **0.6.31** (2026-05-02) — lazy-deferral cleanup wave from v0.6.27. **A1: queue-redis priority ordering** — `b.queue.enqueue({ priority })` on the Redis backend now matches queue-local's `ORDER BY priority DESC, availableAt ASC, enqueuedAt ASC` semantics. The Redis ZSET stays scored by availableAt only (so ZRANGEBYSCORE 0..nowMs cleanly filters ready vs not-yet-ready jobs), but `LEASE_LUA` now over-fetches `maxRows*5` candidates by score, HMGETs priority + availableAt + enqueuedAt for each, sorts server-side in Lua by the same triple queue-local sorts on, and leases the top `maxRows`. Closes the v0.6.27 deferral that punted priority ordering as "ZSET-incompatible". **A2: queue-redis flow `dependsOn` cascade** `b.queue.enqueue({ flowId, flowChildName, dependsOn })` on the Redis backend now releases dependent jobs when their parents complete, mirroring queue-local's `_maybeReleaseFlowChildren`. A per-flow `<prefix>:flow:<flowId>` Redis SET tracks every job in the flow; `complete()` walks the set, looks up siblings whose `dependsOn` includes the just-completed job (by id OR `flowChildName`) AND every other dep is also satisfied, and HSET-bumps their `availableAt` to now + ZADDs them into the ready zset. The flow set persists across `complete()` calls so a later sibling's deps check can still find earlier-done parents by name; `purge()` cleans the set on queue teardown. Closes the v0.6.27 deferral. **A4: WebSocket per-message-deflate (RFC 7692)** — `b.websocket.handleUpgrade` negotiates the `permessage-deflate` extension when the client offers it, accepting `client_max_window_bits` / `server_max_window_bits` constraints (8-15 range, default 15) and always asserting `client_no_context_takeover` + `server_no_context_takeover` so every message uses a fresh zlib state. The connection's send path compresses TEXT/BINARY frames via `zlib.deflateRawSync`, strips the 4-byte `0x00 0x00 0xff 0xff` trailer per RFC 7692 §7.2.1, and sets RSV1 on the first frame of each compressed message. The receive path appends the trailer back and inflates via `zlib.inflateRawSync`. RSV1 on a continuation frame, RSV1 without negotiated extension, RSV2/RSV3 set, and decompressed payload exceeding `maxMessageBytes` all close with the appropriate RFC 6455 status (`PROTOCOL_ERROR`, `INVALID_PAYLOAD`, `MESSAGE_TOO_BIG`). Operator opt-out: pass `permessageDeflate: false` to `handleUpgrade` to refuse the extension even when offered. Tests cover the 101-handshake echo of negotiated params, real compression on a redundant payload (4000 bytes ~30 bytes on the wire), uncompressed-frame round-trip on the same connection (mixed-mode per RFC 7692 §6), and unknown-extension graceful ignore. v0.6.27's third lazy deferral A3 (TLS-redis live test) was already covered by `test/integration/redis-client-tls.test.js` shipped in v0.6.28. Smoke 6780 / wiki e2e 178 / per-primitive integration 14 files green / wiki integration green.
117
+ - **0.6.30** (2026-05-02) — wiki-app integration gate + framework follow-ups surfaced by it. **`scripts/test-wiki-integration.js`** boots `examples/wiki` against the docker-compose fixture stack (real Redis / MinIO / Mailpit / CoreDNS / NTP / mtls-ca) and drives every backend through the wiki's HTTP surface AND the underlying framework primitives validates that each primitive routes through the configured backend (cache custom backend, queue Redis Streams, mail Mailpit SMTP, object-store MinIO sigv4, log-stream webhook receiver) AND that every fix shipped in v0.6.28 holds in a real-app context, not just in unit-test isolation. **Wiki gains test-only routes** under `/test/*` (gated by `WIKI_INTEGRATION_TEST=1`, mounted before CSRF/staticServe so test POSTs don't have to round-trip a token cookie) covering cache get/set/del, queue enqueue/size, mail send, object-store put/get, http-client fetch, log-stream emit, mtls-ca issue, ntp query, dns lookup, ssrf classify+check, plus `/test/diagnostic` that surfaces the active backend posture (network snapshot, queue backends, log sinks, mtls algorithm). **External-integration two-gate rule** when a release diff touches a primitive that talks to an external service (`lib/redis-client.js`, `lib/queue-redis.js`, `lib/mail.js`, `lib/network-dns.js`, `lib/object-store/*`, `lib/log-stream*.js`, `lib/external-db.js`, `lib/cluster-*.js`, `lib/mtls-ca.js`, `lib/ssrf-guard.js`, `lib/http-client.js`, `lib/ntp-check.js`, `lib/cache.js`, `lib/webhook.js`), operators MUST run BOTH `scripts/test-integration.js` (per-primitive against real backends) AND `scripts/test-wiki-integration.js` (wiki app exercising the same backends end-to-end) before pushing. Documented in CONTRIBUTING.md's pre-push gate list and CLAUDE.md's release workflow as step 4a. The smoke + wiki-e2e gates above stay PURE (no docker dependency, runs in CI / on a developer laptop / inside prepack-guard); the two-gate is operator-run, opt-in, and surfaces bugs that mocks miss — fire-and-forget races, shutdown drains, TLS pinning, real DNS resolution, real protocol handshakes. **Framework fixes surfaced by the new gate**: `b.mtlsCa.create` now auto-creates `opts.dataDir` with `mode: 0o700` if it doesn't exist (matches `b.logStream`'s local sink, `b.backup`, `b.restoreBundle` without it the first `initCA()` call hit `ENOENT` writing `ca.key.tmp`); `b.logStream` webhook-sink `close()` now waits for the in-flight `_flush()` before draining the buffer (the v0.6.28 drain fix only tracked emit-time wrapper promises, not the actual HTTP POST in flight; a record arriving mid-flush would buffer + the emit-time `_flush` early-returned on `if (inFlight) return`, and shutdown would then strand it). `b.queue.bootFromEnv()` is now wired in the wiki app's `build-app.js` unconditionally so the wiki picks up `BLAMEJS_QUEUE_PROTOCOL=redis` from env without code changes (was previously a documented-but-commented call site). **Test architecture rule (extends the v0.6.28 rule)** — live tests for the framework's primitives go under `test/integration/`; live tests for the wiki app exercising the framework end-to-end go under `examples/wiki/test/integration.js`. Smoke (`test/smoke.js`) and the existing wiki e2e (`examples/wiki/test/e2e.js`) stay pure. Smoke / wiki-e2e green; per-primitive integration suite green; wiki integration green. Eslint pin alignment + shellcheck added to the documented gate list (carry-over from v0.6.29).
118
+ - **0.6.29** (2026-05-02) — CI cleanup. **shellcheck gate green**: `docker/init/generate-certs.sh`'s footer line counted output via `ls "$CERT_DIR" | wc -l` (SC2012 fragile on filenames with newlines or quoting metacharacters); replaced with `find "$CERT_DIR" -maxdepth 1 -mindepth 1 | wc -l`. Same semantics, robust to any cert filename pki-init might end up writing. **shellcheck added to the documented pre-push gate list** in CONTRIBUTING.md alongside smoke / wiki e2e / eslint / api-snapshot / primitive-section validators CI's `Lint summary` job has always run it, but the local-dev recipe didn't, so contributors hit it post-push instead of catching it locally. **eslint pin alignment**: `.github/PULL_REQUEST_TEMPLATE.md` and CONTRIBUTING.md still pinned `eslint@10` even though v0.6.16 swapped CI runners to `eslint@latest`; both bumped to `@latest` to match the forward-track posture of every other CI tool. No framework code changes; the script + doc updates never ship in the npm package (excluded by `files` array).
119
+ - **0.6.28** (2026-05-02) — bug-finding pass: live integration test suite + every framework bug it caught. **`b.ssrfGuard.checkUrl` cloud-metadata hard-deny** — `allowInternal: true` no longer permits 169.254.169.254 (AWS / GCP / Azure metadata). Loopback / private / link-local / reserved stay allowInternal-overridable per existing semantics; cloud-metadata is unconditional because a blanket override would let any compromised request exfiltrate instance credentials. **`b.mtlsCa` issues server certs and dual-EKU certs** `generateClientCert({ usage: "client" | "server" | "both", sans: [...] })`. `usage` controls EKU (`client` = clientAuth, `server` = serverAuth, `both` = both); `sans` accepts `DNS:` / `IP:` / bare-DNS entries; serverAuth without an explicit SAN auto-adds the CN. Operators wiring inbound mTLS reverse-proxy fronts no longer hit "unsuitable certificate purpose" on the handshake. **`b.mtlsCa` algorithm auto-detect** the engine probes webcrypto + the vendored x509 library at first cert issuance and picks the highest-PQC option that round-trips: SLH-DSA-SHAKE-256f SLH-DSA-SHAKE-128f ML-DSA-87 ML-DSA-65 ECDSA-P384-SHA384 (current bridge what the X.509 ecosystem accepts today). When the bridge condition lifts and the vendor refresh ships, the same `b.mtlsCa.create(...)` call self-upgrades; `b.mtlsCa.status().cert.label` and `.posture` surface the chosen algorithm. **`b.redisClient.create({ ca, servername })`** — managed-Redis / on-prem-cluster operators connecting over `rediss://` against a private CA can pin trust roots; `servername` auto-suppresses for IP literals so `rediss://127.0.0.1:6380` no longer trips node:tls's IP-as-SNI rule. **`b.network.dns.useDnsOverTls` / `useDnsOverHttps` accept `ca`** — same trust-pinning surface for self-signed / private-PKI DoT and DoH endpoints. **DoT TLS handshake errors** now route as DnsError on the lookup promise rather than leaking as `unhandledRejection` — the secureConnect / error event-listener race had let cert-verification failures escape the per-query Promise wrapping. **DoT socket lifecycle** — was unconditionally `sock.unref()`'d after construct, causing node to exit during in-flight lookups when no other I/O kept the loop alive; now ref'd while a query is in flight and unref'd when idle. **`b.ntpCheck.querySingle` honours IPv6 servers** was hardcoded to `dgram.createSocket("udp4")`, so `::1` / `fd00::…` queries failed with EINVAL; now auto-detects family from the server string. **`b.logStream.shutdown` drains in-flight emit microtasks** before closing sink fds — fire-and-forget emits queued just before shutdown were silently dropped because `close()` ran ahead of the microtask. **`b.logStream` webhook-sink `close()` flushes BEFORE setting `closed = true`** `_flush`'s while loop bailed on `!closed`, leaving any records buffered just before shutdown stranded. **Integration test suite** (`test/integration/`, runner at `scripts/test-integration.js`) 13 live test files across `b.redisClient` plain + TLS, `b.queue` Redis backend lifecycle, `b.mail` SMTP + STARTTLS + multi-rcpt + dot-stuffing transparency, `b.mail.dkim` rsa-sha256 + ed25519-sha256 + bad-algorithm reject, `b.ntpCheck` v4 + v6 + bootCheck + bad-host + bad-port, `b.network.dns` plain + DoT + DoH with strict CA pinning + bad-servername + cache, `b.network.heartbeat` http + tcp + state-change callbacks, `b.objectStore` sigv4 PUT / GET / list / delete on plain HTTP + TLS variants, `b.cache` memory + Redis-backed cluster, `b.httpClient` direct + Squid forward proxy + TLS pin, `b.ssrfGuard` classify + checkUrl + cloud-metadata block, `b.logStream` local + webhook + deferred-syslog error path, `b.mtlsCa` CA bootstrap + clientAuth + serverAuth + dual-EKU live mTLS handshakes. Companion `docker-compose.test.yml` stands up redis (plain + TLS) / postgres / mysql / mongo / minio (HTTP + HTTPS variants) / rabbitmq (plain + TLS) / nats / syslog / ntp / mailpit / coredns (plain + DoT + DoH) / haproxy / caddy / mitmproxy / squid / pki-init (auto-generates Ed25519 CA + per-service leaf certs into a docker volume); host port bindings dual-stack on `127.0.0.1` AND `[::1]`. `scripts/check-services.js` does host-side TCP / TLS handshake / SNTPv4 / DNS-A / Redis-PING / SMTP-banner / HTTP probes across every endpoint; `scripts/test-integration.js` exports the test CA via `docker cp` and sets `NODE_EXTRA_CA_CERTS` per-test child process so the framework's TLS verification stays strict (no `rejectUnauthorized: false` bypass anywhere in the test surface). **Test architecture rule** — live tests go in `test/integration/`, NEVER in `test/layer-N-*/`: smoke must remain pure (no docker dependency, runs in CI / on a developer laptop / inside prepack-guard) and a "skip silently when service is down" branch in a layer-N test makes the gate's pass count misleading and masks bugs that only surface against a live backend. The `0.6.27` `queue-redis.test.js` was moved to `test/integration/queue-redis.test.js` to enforce this. Smoke and wiki e2e green; integration suite ships green against the docker-compose stack.
120
+ - **0.6.27** (2026-05-02) — Redis backend for `b.queue` so multi-replica apps can share a single queue without each needing to be cluster leader. **Bespoke RESP2 client** (`lib/redis-client.js`) — zero npm runtime deps. TCP via `node:net` + TLS via `node:tls` (`rediss://` auto-detected), legacy single-arg AUTH + ACL `AUTH user pass`, `SELECT db`, pipelining, exponential-backoff reconnect, EVAL helper. **`b.queue` protocol "redis"** (`lib/queue-redis.js`) full enqueue/lease/extendLease/complete/fail/sweepExpired/size/purge/dlqList/dlqRetry/dlqSize parity with the local backend. Atomicity comes from server-side Lua scripts so concurrent consumers can't double-lease and a sweep can't race a complete. Storage layout: per-job HASH (sealed payload + lastError via `cryptoField.sealRow("_blamejs_jobs", row)` same crypto config as the local backend), per-queue ready ZSET scored by availableAt, per-queue inflight ZSET scored by leaseExpiresAt, per-queue dlq ZSET scored by finishedAt, plus a queues SET so sweepExpired walks every known queue without a global secondary index. Cron-repeat handled in `complete()` JS re-enqueues the next firing as a fresh jobId with availableAt=next-cron-fire. **`b.queue.bootFromEnv({ env })`** env-driven init mirroring `b.network.bootFromEnv` and `b.logStream.bootFromEnv`. Reads `BLAMEJS_QUEUE_PROTOCOL` (`local`|`redis`), `BLAMEJS_QUEUE_REDIS_URL`, `BLAMEJS_QUEUE_REDIS_PASSWORD`, `BLAMEJS_QUEUE_REDIS_USERNAME`, `BLAMEJS_QUEUE_REDIS_TLS`, `BLAMEJS_QUEUE_REDIS_KEY_PREFIX`. Operators flip from local to Redis without a code change; both wiki docker-compose configs declare the new env knobs. Removed `redis` from `DEFERRED_PROTOCOLS`. **Out of scope for v1** (deferred to follow-up patches with explicit re-open conditions): Redis Cluster (slot-routing), Sentinel (managed primary failover), priority ordering on the Redis backend (queue-local supports `priority` opt; Redis backend orders strictly by availableAt for v1), flow children with `dependsOn` cascade. **Wiki**: queue-cache page documents the Redis backend opts schema, bootFromEnv, and the layout. **Tests**: 24 RESP2 protocol parser unit tests (`test/layer-0-primitives/redis-client.test.js`) cover URL parsing, command encoding (binary-safe), every reply type (simple string / error / integer / bulk / nil bulk / nested array / pipelined / incomplete-mid-frame). Live Redis round-trip tests (`test/layer-0-primitives/queue-redis.test.js`) cover enqueue+lease, availableAt scheduling, visibility-timeout sweep, fail+retry path, DLQ list/retry/size, extendLease, purge, and concurrent-leaser no-double-lease skip cleanly when `BLAMEJS_TEST_REDIS_URL` is not set so the smoke suite passes on dev boxes without a Redis container. Smoke 6780 OK.
121
+ - **0.6.26** (2026-05-02) — `blamejs restore` and `blamejs audit verify-chain` subcommands wrap the existing `b.restore` and `b.audit.verifyChain` primitives so operators can drive them from runbooks without writing app code. **`blamejs restore`**: `list` (enumerate bundles in storage), `inspect` (manifest summary without touching live data), `apply` (live in-place restore with rollback preserved), `rollback` (revert to most-recent OR named restore point), `list-rollbacks` (enumerate preserved rollback points). Two ways to identify a bundle — `--bundle <dir>` matches the shape `blamejs backup extract` produces (parent dir is treated as storage root, basename as bundle id), or `--storage-root <root> --bundle-id <id>` for multi-bundle stores. `apply` honors `--max-pulled-bytes` / `--max-pulled-files` (defaults 4 GiB / 100K), `--rollback-root` (default `<data-dir>.rollbacks`), `--no-audit`, and `BLAMEJS_BACKUP_PASSPHRASE` env. **`blamejs audit verify-chain`**: walks the live audit chain end-to-end, reports tampering with `breakAt` / `breakRowId` / expected-vs-actual prevHash; honours `--max-rows` to bound long walks; default table is `audit_log`. **Wiki CLI snapshot test now validates subcommand pairs**, not just top commands. Walks every wiki + README invocation of the form `blamejs <cmd> <sub>` and verifies that <sub> exists in the perCommand[<cmd>].subcommands list parsed from `lib/cli.js`. Surfaced one real drift on first run: `examples/wiki/seeders/prod/pages/backup-restore.js` documented `blamejs audit verify-signing` after vault rotation, but no such subcommand existed (only `verify-bundle`); fixed by shipping the new `verify-chain` subcommand and updating the wiki to reference it. Two top-level command gaps surfaced and fixed: `blamejs restore` (now real) and `blamejs network status` (was prose-promised in `network-config.js`; reworded to point at `b.network.snapshot()` for /healthz / custom diagnostics routes since wiring a CLI command for it is operator-side work). README CLI table updated with `restore` row and the two new audit subcommands.
122
+ - **0.6.25** (2026-05-02) — `b.logStream` gains an AWS CloudWatch Logs sink AND framework-level env wiring. **CloudWatch sink**: `protocol: "cloudwatch"` POSTs `PutLogEvents` over HTTPS with SigV4 signing (service `logs`); operator pre-creates the log group + log stream (the framework does NOT auto-create). Honors IAM role + STS session-token credentials. Respects all three CloudWatch caps automatically: 10,000 events / 1 MiB total payload / 256 KiB-per-event. Per-event oversize dropped at `emit()`-time with `onDrop` fired (truncated message in the drop notification). Per-batch oversize split mid-flush. Permanent AWS errors (`ResourceNotFoundException` / `AccessDeniedException` / `InvalidParameterException` / `UnrecognizedClientException` / `SerializationException`) skip the retry budget. `InvalidSequenceTokenException` (legacy CW accounts) extracts the expected token from the error message and retries with it once. `lib/object-store/sigv4.js` `signRequest()` is now service-agnostic accepts `opts.service` (default still `"s3"` for back-compat). Removed `cloudwatch` from `DEFERRED_PROTOCOLS`. **`b.logStream.bootFromEnv({ env })`**: framework-level env-driven init mirroring `b.network.bootFromEnv`. Reads `BLAMEJS_LOG_STREAM_PROTOCOL` (`local`/`webhook`/`otlp`/`cloudwatch`), `BLAMEJS_LOG_STREAM_URL`, `BLAMEJS_LOG_STREAM_TOKEN`, `BLAMEJS_LOG_STREAM_SERVICE_NAME`, `BLAMEJS_LOG_STREAM_SERVICE_VERSION`, `BLAMEJS_LOG_STREAM_CLOUDWATCH_LOG_GROUP`, `BLAMEJS_LOG_STREAM_CLOUDWATCH_LOG_STREAM`, `BLAMEJS_LOG_STREAM_PATH`, plus standard AWS_*. Operators get a working log-stream sink without writing build-app code. Wiki app's `build-app.js` replaced its inline env-reading with one `b.logStream.bootFromEnv()` call; both docker-compose configs declare the new env knobs. **Wiki env-snapshot test**: parallel to api-snapshot.json walks `process.env.X` / `env.X` / `safeEnv.readVar("X")` reads in the wiki app + framework `lib/`, walks docker-compose env declarations, captures the union as `examples/wiki/env-snapshot.json`, fails the e2e gate when env vars are added/removed without updating the snapshot OR when source-only / compose-only gaps appear (env knob declared but unread, env read but undocumented). The validator immediately surfaced 13 real gaps in the wiki app: 5 `WIKI_*` env vars read by source but missing from docker-compose (`WIKI_VAULT_MODE`, `WIKI_DB_AT_REST`, `WIKI_AUDIT_SIGNING_MODE`, `WIKI_BIND`, `WIKI_SITE_URL`), 2 framework env vars (`BLAMEJS_AUDIT_SIGNING_MODE`, `BLAMEJS_TMPDIR`) read by `lib/db.js` via `safeEnv.readVar` but never declared in the wiki's compose configs, and 6 dead env knobs in compose that nothing read. All 13 fixed. Update workflow: `BLAMEJS_UPDATE_ENV_SNAPSHOT=1 node examples/wiki/test/validate-env-snapshot.js` (mirrors the api-snapshot UX). Wiki observability page documents the new sink alongside webhook + otlp; README "What ships in the box" calls out all four log-stream sinks. Tests cover endpoint resolution, event-byte accounting, batch sorting + sequence-token round-trip, permanent-error classifier, validation, round-trip via mock CloudWatch, STS session-token propagation, ResourceNotFoundException no-retry path, 256 KiB per-event hard cap, dispatcher integration, AND batch splitting on the 1-MiB cap (5 quarter-MB events POST as 4 + 1 batches).
123
+ - **0.6.24** (2026-05-02) — `b.logStream` gains an OTLP/HTTP-JSON sink. `protocol: "otlp"` now forwards log records to any OpenTelemetry collector via the OTel Logs Data Model `resourceLogs` `scopeLogs` → `logRecords` envelope with `severityNumber` (debug=5/info=9/warn=13/error=17), `severityText`, `timeUnixNano` (string-encoded for JSON-safe 64-bit), `body.stringValue`, and OTel-typed attributes. Operator config: `{ url, serviceName, serviceVersion, resourceAttributes, auth, headers, batchSize, retry, onDrop, ... }` same back-pressure semantics as the webhook sink (per-sink ring buffer, batched flush on size or maxBatchAgeMs, exponential-backoff retry, drop-on-overflow with operator-supplied onDrop callback). URL convention: `/v1/logs` is auto-appended when the operator passes the collector root. JSON not protobuf — operators benchmarking >100K logs/s ship the OTel Collector locally so the framework hands JSON to a sidecar that forwards via gRPC. Removed `otlp` from `DEFERRED_PROTOCOLS`. Wiki observability page documents the new sink with a `b.logStream.init` example pointing at an OTel collector. Tests cover URL resolution, attribute encoding (string / int / float / bool / array / nested object), severity mapping, round-trip via mock collector, auth header pass-through, retry on 5xx + drop on retry-exhaustion, buffer overflow, dispatcher integration. Verified 0 leaks across 291 commits via `gitleaks`.
124
+ - **0.6.23** (2026-05-02) — v0.6.22 follow-up cleanup. The cron-repeat call site in queue-local.js was passing both `availableAt` AND `delaySeconds` (the former was the precise next-fire ms; the latter was a redundant `Math.floor((nextMs - nowMs) / 1000)` computation that only existed to work around the bug v0.6.22 fixed). Dropped the cron repeat now passes `availableAt` alone, matching the queue's documented precedence rule. The enqueue() docstring gains a 20-line "SCHEDULING PRECEDENCE" header documenting that `opts.availableAt` wins over `opts.delaySeconds` when both are passed, why the framework chose that direction, and which callers should use which form. New round-trip preservation regression test (`testEnqueueRoundTripsAvailableAt`) covers three precise targets, the delaySeconds-only path, and the both-opts-set case gates against any future "I'll just rederive it from the floored seconds" mistake. Audited the rest of the framework for the same `(absolute-time, relative-time)` opt-overlap shape (cache.set, session.rotate, apiKey, dualControl): queue is the only primitive carrying both forms, so a generalized `b.time.resolveTimePoint` primitive would be premature with one call site.
125
+ - **0.6.22** (2026-05-02) — `b.queue.enqueue({ availableAt })` is now honoured. Previously the local-protocol enqueue() ignored opts.availableAt entirely, recomputing from `Date.now() + delaySeconds*1000`. The cron-repeat path passes both fields (the exact next-fire ms in availableAt + the floored seconds in delaySeconds), and the enqueue's recomputation lost sub-second precision plus drifted on the internal clock-vs-caller delta. Symptoms: cron-scheduled jobs landed up to 999ms off the intended boundary; the queue-flow-repeat smoke test was intermittently flaky on slow CI runners (caught by ubuntu-latest on the v0.6.21 commit). Fix: enqueue() honours opts.availableAt directly when finite; falls back to delaySeconds-based shorthand otherwise. Operators relying on `enqueue({ availableAt: T })` for non-cron scheduled jobs (e.g. "deliver this notification at exactly 09:00 tomorrow") now get the requested time instead of nowMs+0.
126
+ - **0.6.21** (2026-05-02) — closes the medium / low audit findings flagged after v0.6.18. **Cluster cache `invalidateTag`** now actually works the cluster backend gained a `_blamejs_cache_tags` junction table (`(cacheKey, tag)` PK + index on `tag`), tag-aware `set` / `del` / `clear` / `_sweep`, plus `getTags(key)`. The old NOT_SUPPORTED-on-cluster path is gone; multi-tag rotation, mid-flight tag replacement on update, and namespace-scoped sweeps all covered. **Multi-column cursor pagination** `b.pagination.cursor({ orderBy: [{column,direction},...] })` accepts a string (single column), an array of strings (multi, all using `opts.direction`), or an array of `{column,direction}` objects (mixed directions). The keyset WHERE expands to the standard OR cascade so successive pages can't skip or repeat rows when ties on the leading columns are broken by trailing ones. `_id` is appended as a tiebreaker if not in the chain. The Query class also gained chained `orderBy(col, dir)` calls second-and-later calls extend a multi-column ORDER BY in the SQL. Cursor format is bumped to encode `{ orderKey, vals, forward }` instead of the old `{ orderBy, dir, orderByVal, id, forward }` — pre-1.0 break, no compat shim. **DoH POST mode** (RFC 8484 §4.1) `b.network.dns.useDnsOverHttps({ method })` accepts `"GET" | "POST" | undefined` (auto). Auto switches to POST when the GET URL would exceed 2048 bytes (long DNS names). **DoT connection pooling** per-`(host:port)` cached TLS socket with a 2-minute idle timeout, serialized in-flight queries per socket. Eliminates the per-query handshake. **INI parser** shipped as `b.parsers.ini` — covers Windows .ini / .gitconfig / systemd-unit / php.ini / tox.ini shape: sections (incl. `[parent.child]` / `[parent "child"]` nesting), `;` and `#` comments (inline + leading), single + double quoting with `\n` `\t` `\\` `\"` `\'` escapes, boolean coercion (`true`/`false`/`yes`/`no`/`on`/`off`), decimal + hex integers + floats. Prototype-pollution defense (`__proto__` / `constructor` / `prototype` rejected). Duplicate-key policy throws by default; `onDuplicate: "first" | "last"` opts in to silent shadowing. Section / per-section key / value-bytes caps configurable. **Cookie-jar file persistence** `b.httpClient.cookieJar.create({ persist: "file", file: "/abs/path", vault: b.vault })` loads at construct, debounce-flushes on every set/clear, plus `flush()` and `close()` for explicit lifecycle. With `vault`, on-disk bytes are sealed; without, plaintext JSON (operator chooses). **Comment cleanups per rule §4** — `vault/index.js` stale `// later` removed, `mail.js` "future patch" wording rewritten as scope, `bundler.js` "What it does NOT do today (deliberately deferred)" rewritten as "Out of scope", `archive.js` "v1 scope cuts (deferred)" rewritten as "Out of scope", `framework-schema.js` "next release" wording rewritten to describe what's actually shipped, `http-client.js` "out of reach today" / "we'll plumb it through when h3 lands" rewritten as scope statements. **README CLI section** updated to reflect the v0.6.17 + v0.6.18 + v0.6.19 + v0.6.20 additions (security / config-drift / file-type / password / erase / retention) was missing 6 subcommands.
127
+ - **0.6.20** (2026-05-02) — CI / packaging fix-up. The npm-publish workflow's "Attach SBOM as release asset" step started failing with HTTP 422 ("Cannot upload assets to an immutable release") because the operator's manual `gh release create` had already been published when the workflow ran. Two changes: (1) `sbom.cyclonedx.json` is now bundled into the npm tarball (`files` block in package.json), so `npm install @blamejs/core && cat node_modules/@blamejs/core/sbom.cyclonedx.json` is the canonical SBOM access path. The prepack guard's known-allowed list covers the just-in-time generation. (2) The workflow's GH-release-attach step is now non-fatal: it tries to upload, logs a warning if the release is immutable, and lets the publish proceed regardless. The npm tarball is the load-bearing artifact; the GitHub release attachment was only ever supplementary. `.gitignore` adds `sbom.cyclonedx.json` so a stray local `npm sbom` doesn't pollute the repo.
128
+ - **0.6.19** (2026-05-02) — closes the critical + high gaps surfaced by the v0.6.18 audit. **Critical**: `b.network.ntp.nts.querySingle` now actually verifies the server reply with the s2cKey — extracts the AUTHENTICATOR_AND_ENC extension, AEAD-decrypts with AAD = bytes-before-authenticator, fails closed (`nts/auth-failed` / `nts/no-authenticator`) when verification fails. Server-supplied new cookies in the encrypted plaintext are appended to the cookie pool and the consumed cookie popped (real RFC 8915 cookie rotation). Previously the function returned `authenticated: true` while only checking the unique-identifier echo any MITM that mirrored the request's 32-byte unique field could spoof timestamps. **High**: `azure-blob.presignedUploadPolicy` now throws `PRESIGN_NOT_SUPPORTED` instead of silently returning a SAS PUT URL when operators asked for POST policy semanticsAzure SAS has no body-size cap and the previous shape was a misleading mismatch (operator error message points at presignedUploadUrl + post-upload HEAD as the alternative). `b.auth.password.policy` now ships the SecLists top-10000 common-password list bundled (CC-BY-3.0, `lib/vendor/common-passwords-top-10000.txt`), loaded lazily on first `policy.check()` call; `password` / `dragon` / `qwerty` / etc. now reject with `policy/forbidden-common`; `useBundledCommon: false` per-policy bypasses if operator ships their own list. `b.network.tls` adds `removeCa(fingerprint256)` / `removeCaByLabel(label)` / `clearAll()` / `purgeExpired()` / `expiringSoon(windowMs)` so operators can rotate corp DPI CAs without process restart; every removal audits with subject + fingerprint + reason. `b.network.dns.setResultOrder("ipv6first")` now flips the order on the DoH / DoT dual-stack fallback paths too (was only sorting OS-resolver results). `b.network.dns.resolve4` / `resolve6` / `resolveAaaa` now use real DNS-protocol queries (`dns.promises.resolve4` / `_dohLookup` / `_dotLookup`) instead of aliasing `lookup()` — operator semantics now match Node's standard library (skips `/etc/hosts`, mDNS). `b.network.socket.setDefaultLinger` removed from the silent no-op path; now throws `socket/linger-not-supported` with operator guidance to use `socket.destroy()` (abort) vs `socket.end()` (graceful) since Node's public `net.Socket` has no `setLinger()`. **Internal**: every `new XxxError(...)` call across `lib/network*.js` was passing args in the wrong order (message-then-code instead of code-then-message), making `e.code` return human messages and `e.message` return slash-codes — operators relying on `e.code` for error handling got the wrong field. All 50+ throws fixed across `lib/network.js` / `network-dns.js` / `network-proxy.js` / `network-tls.js` / `network-heartbeat.js` / `network-nts.js`. **CI**: `.gitleaks.toml` adds `test/smoke.js` to the path allowlist and pins the historical commit + fingerprint that tripped the jwt rule on a `REDACTED` placeholder; `npm-publish.yml` job permissions bumped from `contents: read` to `contents: write` so `gh release upload sbom.cyclonedx.json` no longer 403s.
129
+ - **0.6.18** (2026-05-02) — `b.network` primitive — single namespace for runtime-configurable network behaviour. `b.network.ntp` adds tunable warn / fatal drift thresholds, env-var bindings (`BLAMEJS_NTP_SERVERS` / `BLAMEJS_NTP_TIMEOUT_MS` / `BLAMEJS_NTP_DRIFT_WARN_MS` / `BLAMEJS_NTP_DRIFT_FATAL_MS`), and authenticated-time support: `b.network.ntp.nts.query(opts)` performs an NTS-KE handshake (RFC 8915) over TLS 1.3 with the framework's PQC-hybrid group preference, extracts C2S / S2C keys via the standardized TLS exporter, and authenticates NTPv4 packets with AES-SIV-CMAC-256 (mandatory-to-implement, in-house) or AEAD-CHACHA20-POLY1305 no extra vendored deps. `b.network.dns` exposes operator-pinned resolvers, IPv4 / IPv6 / dual-stack family selection, ipv4first / verbatim / ipv6first ordering, DNS lookup timeout (Node's native `dns.lookup` has none), in-memory positive + negative cache, and DoH / DoT (cloudflare / google / quad9 / custom URL) — `b.ssrfGuard` and `b.httpClient` route through it when configured. `b.network.proxy` honours `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` / `ALL_PROXY` (lower- and upper-case) with CIDR + suffix + wildcard `NO_PROXY` matching, basic-auth via `BLAMEJS_PROXY_AUTH`, CONNECT tunnels for HTTPS through HTTP proxies; `b.httpClient` picks up the agent automatically. `b.network.tls` is a runtime-overridable trust store: `addCa(pemOrPath)` / `addCaBundle(path)` / `useSystemTrust()` / `getTrustStore()` for deep-packet-inspection deploys behind Zscaler / Netskope / corporate Squid + custom CA — Node's `NODE_EXTRA_CA_CERTS` only works at boot; this primitive accepts adds at any time and `b.pqcAgent` picks them up immediately. Every `addCa` audits with subject + issuer + fingerprint256 + validity + isSelfSigned. `b.security.assertProduction({ allowDpiTrust })` refuses to boot in production with installed CAs unless explicitly allowed. `b.network.heartbeat` adds application-level liveness probes for upstream services (HTTP / TCP / NTP probe types, healthy → degraded → down state machine with consecutive-failure threshold, audit on state change, observability counters per probe). `b.network.socket` gives operator-tunable defaults for `TCP_NODELAY` / `SO_KEEPALIVE` / `SO_LINGER`. `b.network.bootFromEnv()` reads every supported env var at startup and applies in the right order so configuration takes effect before the first outbound socket; the wiki app's docker-compose configs ship every knob with a default-empty value (production overlay tightens DNS lookup timeout + cache TTL + `NTP_STRICT=1` + `SOCKET_NO_DELAY=1`). `.gitleaks.toml` was previously gitignored by the deny-all-dotfiles allowlist; it is now allowlisted so CI's secret-scan job loads the framework's allowlist and stops failing on `.gitleaks.toml: no such file or directory`. New wiki page `/network-config` documents all six sub-primitives.
130
+ - **0.6.17** (2026-05-02) — six new CLI subcommands wrapping the v0.6.14 primitives so operators can drive them from runbooks / cron / one-off ops: `blamejs security assert` (boot the framework + run `b.security.assertProduction()` against the live posture; aggregates failures with their codes, exit 1 on any failure), `blamejs config-drift inspect/verify` (read the signed sidecar without rebooting; `verify` returns exit 1 on tamper or missing), `blamejs file-type detect <file>` (magic-byte content classification with `--allowlist` for upload debugging — pure utility, no boot), `blamejs password check --plaintext "..."` (test `b.auth.password.policy` with `--profile pci-4.0` / `nist-aal2` / `hipaa-aal2`, `--breach-check` for HIBP, `--email` / `--username` context — pure utility), `blamejs erase --table X --row-id Y --confirm` (one-off cryptographic erasure for GDPR Art. 17 — replaces sealed columns + derived hashes with NULL + audits via `system.erase`), `blamejs retention preview/run` (ad-hoc rule from CLI flags: `--table` + `--age-field` + `--ttl-ms` + `--action`, with `preview` for dry-run reporting). Also: per-subcommand `--help` now reaches each handler's USAGE block (the previous main() dispatch short-circuited every `<sub> --help` to top-level help); fix applies to all existing subcommands too (`api-key --help`, `vault --help`, etc.). The `b.dualControl` primitive is intentionally NOT in the CLI surface — its grants live in an operator-supplied `b.cache` instance the CLI can't bind to without operator wiring; admins approve / revoke from the operator's app instead. Hadolint action pinned to `@master` (the published `@v3` floating tag doesn't exist; the maintainer ships only patch tags + master).
131
+ - **0.6.16** (2026-05-02) — CI secret-scan job switched from `gitleaks/gitleaks-action` (which started requiring a paid `GITLEAKS_LICENSE` for organization repositories, breaking the v0.6.14 publish workflow) to a direct install of the OSS gitleaks binary (Apache-2.0); resolves the latest release at job time so new ruleset improvements ship automatically. Same forward-track posture applied to other CI tools where pinning would silently miss security improvements: `aquasecurity/trivy-action` swapped from `@v0.36.0` to `@master`, `hadolint/hadolint-action` from `@v3.3.0` to the floating `@v3` major tag, `ludeeus/action-shellcheck` from `@2.0.0` to `@master`, ESLint runner from `eslint@10` to `eslint@latest` in both `ci.yml` and `npm-publish.yml`. Vendored crypto libraries (@noble/ciphers, @simplewebauthn/server, argon2, peculiar-pki) verified at latest upstream no bundle refresh needed. No framework code changes; npm publish workflow unblocked for v0.6.14 / v0.6.15 / this release.
132
+ - **0.6.15** (2026-05-02) — wiki sidebar nav: every concern group is now a `<details>` section starting collapsed; the section containing the current page is rendered with `open` server-side so the operator's current location is always visible. Custom CSS-only disclosure glyph (▸ → ▾ via rotation) — works under the wiki's strict CSP without inline JS. Wiki app gains three new env knobs that ship with the docker-compose configs out of the box: `WIKI_TRUST_PROXY` (cookie Secure-flag detection through TLS terminator; pre-existing in v0.6.12 lib code, now in docker-compose with documentation), `WIKI_ADMIN_ALLOWED_CIDRS` / `WIKI_ADMIN_DENIED_CIDRS` (in-process CIDR fence on `/admin` paths via `b.middleware.networkAllowlist`), `WIKI_REQUIRE_PROD_ASSERTS` (boot-time `b.security.assertProduction()` gate that refuses to boot when production posture is incomplete). The production overlay (`docker-compose.prod.yml`) defaults the latter two on (TRUST_PROXY=1 since Caddy terminates TLS, REQUIRE_PROD_ASSERTS=1 so a misconfigured prod deploy fails fast); the dev overlay defaults them off.
133
+ - **0.6.14** (2026-05-02) — new operator-facing primitives, each shipping the v1-defensible compliance surface: `b.security.assertProduction(opts)` boot-time policy engine (vault / dbAtRest / auditSigning posture, NTP strict, Node minimum major, TLS minimum version, required + forbidden env vars, NODE_ENV pinning, dataDir POSIX-mode check, CORS-allow-all detection, operator-supplied extra asserts); `b.permissions.policy(scope, predicate)` ABAC layer that evaluates per-scope predicates after RBAC passes (single + requireAll + requireAny modes); `b.permissions` role-spec `requireMfa` / `mfaWindowMs` AND per-route MFA enforcement; `b.session` IP/UA fingerprint capture + drift detection with operator-supplied scorer for anomaly scoring (strict modes: `requireFingerprintMatch` AND `maxAnomalyScore`); `b.auth.password.policy(opts)` with NIST 800-63B / PCI-DSS 4.0 / HIPAA-AAL2 named profiles, length / common / context / dictionary / complexity (categories + min-run + min-sequence) rules, HIBP k-anonymity breach check (SHA-1 lives in `lib/internal-sha1-hibp.js`, NOT exported on `b.crypto`), rotation (`shouldRotate`) and history-reuse (`reuseProhibited`); `b.dualControl.create(opts)` two-person-rule with m-of-n quorum, cooling-off lock between approval and consume, approver-role gate, requester cancellation, minimum reason length, notification hook; `b.retention.create(opts)` with multi-stage rules (warn → archive → erase), legal-hold per-row exemption, dry-run `preview()`, soft-delete vs hard-delete vs erase, cross-table cascade, per-rule concurrency lock; `b.configDrift.create(opts)` with multi-baseline support, critical-keys severity classification, ignore-keys allowlist, signed sidecar via SLH-DSA + diff vs prev snapshot; `b.fileType.detect/.assertOneOf` magic-byte content classification (image / document / archive / executable / etc.); `b.middleware.networkAllowlist({ paths, allowedCidrs, deniedCidrs })` deny-then-allow CIDR fence; `b.httpClient.request({ allowedHosts })` with exact / suffix / wildcard / per-method entries and audit emit on disallowed; `b.cryptoField.eraseRow(table, row)` cryptographic-erasure helper for sealed columns + derived hashes. CI gains gitleaks secret scan + CycloneDX SBOM via `npm sbom` (no vendoring). README + SECURITY operator-checklist updated end to end for the new surface; wiki gains alerting-rule reference table for the framework's emitted events. Backwards-compatible every new opt is opt-in.
134
+ - **0.6.13** (2026-05-02) — wiki primitive-section docs catch up to the v0.6.12 surface (`safeUrl.parse` allowUserinfo, `session.touch` extendBy ceiling, `queue.consume` rateLimit validation, `mail.transports.console` redactBcc, `logStream` webhook-sink onDrop, `backup.create` requireFlush, `restore.create` maxPulledBytes / maxPulledFiles); restore default cap stated as `C.BYTES.gib(4)` instead of a raw byte literal; numeric-check predicates (`isPositiveInt`, `isFiniteNonNegative`, `isPositiveFinite`) consolidated into `lib/numeric-checks.js` were duplicated across api-key, cache, notify, queue, restore, retry, slug, testing, webhook; api-snapshot.json refreshed
135
+ - **0.6.12** (2026-05-02) — `b.safeUrl.parse` now rejects URLs with `user:pass@` userinfo by default (opt in per-call via `allowUserinfo: true`); `b.session.touch({ extendBy })` enforces the same `MAX_TTL_MS` ceiling as `create` / `rotate`; `b.queue.consume({ rateLimit })` rejects negative / zero / `NaN` / `Infinity` / fractional `max`; `b.middleware.requireAuth` no longer treats request `Content-Type: application/json` as a JSON-preference signal (only `Accept` and `X-Requested-With` count); `b.backup.create({ requireFlush: true })` opt-in fails the backup if pre-flush fails instead of producing a stale snapshot; `b.restore.create({ maxPulledBytes, maxPulledFiles })` preflight bounds bundle footprint before and after pull (defaults 4 GiB / 100K files); `b.mail.transports.console({ redactBcc: true })` opt-in prints recipient count instead of addresses; `b.logStream.transports.webhook({ onDrop })` callback fires on overflow + retry-exhausted batch drops. Wiki: admin login wired through `b.auth.lockout` (exponential-backoff after bad-cred attempts) and cookie `Secure` flag now routes through `b.requestHelpers.requestProtocol` with `WIKI_TRUST_PROXY` opt-in instead of trusting raw `x-forwarded-proto`. Wiki README documents the trust model for editable page bodies and the sanitization pattern operators should adopt before expanding the editor surface.
136
+ - **0.6.11** (2026-05-01) — wiki example-execution validator: fixture init no longer reaches across module realms (unblocks the npm-publish workflow's wiki-e2e gate, which `npm install --install-links` copies the framework into the wiki's node_modules — two distinct singletons before this fix)
137
+ - **0.6.10** (2026-05-01) — README / SECURITY / CONTRIBUTING / wiki: removed stale version stamps and an inaccurate vendored-dep list; SECURITY now points at `lib/vendor/MANIFEST.json` for the authoritative vendor list; supported-versions table no longer pins to a specific minor; wiki archive example names the digest variable correctly (was `sha256`, output is SHA3-512 hex)
138
+ - **0.6.9** (2026-05-01) — b.archive.zip().digest() returns a SHA3-512 hex string (was SHA-256); operators reconciling against an external SHA-256 must hash the bytes themselves
139
+ - **0.6.8** (2026-05-01) — wiki primitive-section validator: presence + opts diff + example execution against canonical fixture
140
+ - **0.6.7** (2026-05-01) — db.role.switched audit + per-role metrics + API snapshot baseline
141
+ - **0.6.6** (2026-05-01) — request-time DB role binding + Postgres RLS migrations
142
+ - **0.6.5** (2026-05-01) — b.db.declareView + b.externalDb.migrate
143
+ - **0.6.4** (2026-05-01) — wiki schema docs realigned with the actual lib API
144
+ - **0.6.3** (2026-05-01) — externalDb pool tuning + role-aware connect + read-replica routing
145
+ - **0.6.2** (2026-05-01) — input validation + identifier-quoting consistency
146
+ - **0.6.1** (2026-05-01) — security tightenings + operator-facing jargon sweep
147
+ - **0.6.0** (2026-05-01) — wiki restructured into 22 focused pages + missing-primitive coverage
148
+
149
+ ## v0.5.x
150
+
151
+ - **0.5.18** (2026-05-01) — bypass-fix sweep: route through existing primitives
152
+ - **0.5.17** (2026-05-01) — primitive-drift sweep: csv unify + 3 new shared primitives
153
+ - **0.5.16** (2026-04-30) — b.otelExport: OTLP/HTTP exporter for b.observability
154
+ - **0.5.15** (2026-04-30) — b.archive: ZIP creation
155
+ - **0.5.14** (2026-04-30) — b.time: timezone-aware datetime arithmetic + formatting
156
+ - **0.5.13** (2026-04-30) — b.testing.request: supertest-style chainable HTTP test helper
157
+ - **0.5.12** (2026-04-30) — b.middleware.requestLog: HTTP access-log middleware
158
+ - **0.5.11** (2026-04-30) — b.config: schema-validated environment configuration
159
+ - **0.5.10** (2026-04-30) — b.middleware.sse: Server-Sent Events
160
+ - **0.5.9** (2026-04-30) — b.csv: RFC 4180 parser + serializer
161
+ - **0.5.8** (2026-04-30) — b.uuid: RFC 4122 v4 + RFC 9562 v7
162
+ - **0.5.7** (2026-04-30) — defensive validation + queue closure capture + audit context
163
+ - **0.5.6** (2026-04-30) — break-glass: trustProxy honored, cache require hoisted
164
+ - **0.5.5** (2026-04-30) — strict default CSP + IPv6 special-range expansion
165
+ - **0.5.4** (2026-04-30) — close SSRF DNS-rebinding window with pinned outbound DNS
166
+ - **0.5.3** (2026-04-30) — security cleanup: trustProxy primitive + Vary merge + HSTS gate
167
+ - **0.5.2** (2026-04-30) — b.breakGlass: passkey factor + service-account bypass + admin tools
168
+ - **0.5.1** (2026-04-30) — b.breakGlass: per-cell encryption + context binding + migrate
169
+ - **0.5.0** (2026-04-30) — b.breakGlass: column-policy / row-enforcement step-up auth
170
+
171
+ ## v0.4.x
172
+
173
+ - **0.4.29** (2026-04-30) — primitive-drift sweep: second-pass remediation
174
+ - **0.4.28** (2026-04-30) — primitive-drift sweep: inline-require hoisting + safeAsync.sleep + time-math
175
+ - **0.4.27** (2026-04-30) — primitive-drift sweep: regex + escape consolidation, IPv6 completion
176
+ - **0.4.26** (2026-04-30) — primitive-drift sweep: middleware audit context + safeUrl
177
+ - **0.4.25** (2026-04-30) — b.objectStore.bucketOps: bucket-level operations (SigV4)
178
+ - **0.4.24** (2026-04-30) — b.objectStore: multipart upload + server-side encryption
179
+ - **0.4.23** (2026-04-30) — b.mail.dkim signing + calendar invites
180
+ - **0.4.22** (2026-04-30) — b.mail attachments + inline images + plain/HTML alternatives
181
+ - **0.4.21** (2026-04-30) — b.queue: repeat-in-queue (cron) + parent-child Flows
182
+ - **0.4.20** (2026-04-30) — b.queue + b.jobs: priority, rate-limit, progress
183
+ - **0.4.19** (2026-04-30) — b.router: schema-validated routes + OpenAPI gen
184
+ - **0.4.18** (2026-04-30) — cookieJar forensic-test strengthening (real crypto, replay, nonce)
185
+ - **0.4.17** (2026-04-30) — b.httpClient.cookieJar (encrypted) + wiki catch-up sweep
186
+ - **0.4.16** (2026-04-30) — b.httpClient: interceptors + progress events
187
+ - **0.4.15** (2026-04-30) — b.httpClient: redirect-following + outbound multipart
188
+ - **0.4.14** (2026-04-30) — b.i18n: lazy locales + ordinal plurals + onMissingKey hook
189
+ - **0.4.13** (2026-04-30) — b.db: streaming query results
190
+ - **0.4.12** (2026-04-30) — b.log: multi-sink output with per-sink level filtering
191
+ - **0.4.11** (2026-04-30) — b.cache: bytes-cap eviction, sliding TTL, tag invalidation
192
+ - **0.4.10** (2026-04-30) — bodyParser multipart: fileFilter + per-field maxBytes/mimeTypes
193
+ - **0.4.9** (2026-04-30) — Origin-Agent-Cluster + DNS-Prefetch-Control headers; b.auth.lockout primitive
194
+ - **0.4.8** (2026-04-30) — wiki SEO surface: per-page OG / Twitter / JSON-LD + sitemap.xml + robots.txt
195
+ - **0.4.7** (2026-04-30) — audit-fix the welcome page's "what's in the box" table
196
+ - **0.4.6** (2026-04-30) — wiki gets the brand-flare on every page + substantive content additions
197
+ - **0.4.5** (2026-04-30) — canonical domain is blamejs.com (was blamejs.app everywhere)
198
+ - **0.4.4** (2026-04-30) — document BLAMEJS_AUDIT_SIGNING_PASSPHRASE everywhere
199
+ - **0.4.3** (2026-04-30) — b.ssrfGuard primitive (default-on in httpClient) + wiki posture auto-detect
200
+ - **0.4.2** (2026-04-30) — npm keywords
201
+ - **0.4.1** (2026-04-29) — wiki bot-guard skips /healthz so the post-publish smoke check passes
202
+ - **0.4.0** (2026-04-29) — bench suite + drops the deprecated b.logger.createLogger
203
+
204
+ ## v0.3.x
205
+
206
+ - **0.3.39** (2026-04-29) — MIGRATING.md generator scans deprecate calls in lib/
207
+ - **0.3.38** (2026-04-29) — LTS-CALENDAR.md publishes the major-cadence + algorithm posture
208
+ - **0.3.37** (2026-04-29) — bundled pure-JS mTLS engine + vendor-update.sh + CHANGELOG + thanks page
209
+ - **0.3.36** (2026-04-29) — prepack guard refuses to publish any path that matches a gitignore rule
210
+ - **0.3.35** (2026-04-29) — wiki home page hero with logo + neon-magenta brand accents
211
+ - **0.3.34** (2026-04-29) — rewrite api-snapshot wiki section in plain operator voice + fix SECURITY.md flag
212
+ - **0.3.33** (2026-04-29) — doc-sweep folding the v0.3.32 mtls CLI references + filling the wiki gap on api-snapshot
213
+ - **0.3.32** (2026-04-29) — blamejs mtls CLI (status / show-cert / init / issue / issue-p12) + framework posture fix on cert fingerprinting
214
+ - **0.3.31** (2026-04-29) — npm-publish: defensive NPM_TOKEN check + bin/blamejs.js executable bit committed
215
+ - **0.3.30** (2026-04-29) — wiki binds 0.0.0.0 by default so docker -p forwarding actually reaches the listener
216
+ - **0.3.29** (2026-04-29) — npm-publish workflow + Dockerfile data-skeleton dir
217
+ - **0.3.28** (2026-04-29) — Dockerfile: switch runtime --chown from name to numeric UID 65532:65532
218
+ - **0.3.27** (2026-04-29) — Dockerfile pre-builds public/dist in deps stage so runtime container boots cleanly under --cap-drop ALL
219
+ - **0.3.26** (2026-04-29) — operator-facing env-var surface, GitHub-side ops docs, contribution standards
220
+ - **0.3.25** (2026-04-29) — env-var surface documented across Dockerfile, docker-compose.prod, DEPLOY.md, README; smoke-test GHCR login fix
221
+ - **0.3.24** (2026-04-29) — CI + release-container workflows brought (structured output, multi-arch, cosign, post-publish smoke)
222
+ - **0.3.23** (2026-04-29) — _runVault and _runBackup refactored to use b.cliHelpers (sweep complete across the three CLI commands the helper was designed for)
223
+ - **0.3.22** (2026-04-29) — blamejs api-key CLI; b.cliHelpers shared primitive for headless app + reporter shape
224
+ - **0.3.21** (2026-04-29) — blamejs backup CLI: inspect / verify / extract; backup encryption format documented
225
+ - **0.3.20** (2026-04-29) — drop redundant Windows-flaky audit.flush perf assertion
226
+ - **0.3.19** (2026-04-29) — wiki docs for the unwired primitives surface
227
+ - **0.3.18** (2026-04-29) — blamejs vault CLI: seal / unseal / rotate / status
228
+ - **0.3.17** (2026-04-29) — validateOpts coverage extended to remaining operator-facing primitives (i18n, bundler, migrations, seeders, template, jobs, cookies, log, mtlsCa, staticServe, handlers, websocket, pqcGate, tracing, metrics — 15 more factories now reject typo'd opts at boot). Coverage is now ~39/lib primitives with the helper; the remaining handful (app-shutdown, chain-writer, dev, error-page, nonce-store, protocol-dispatcher, render, session, pqc-agent) are either internal-only — operators don't construct them directly — or have a single required-key surface where typo guards add no value.
229
+ - **0.3.16** (2026-04-29) — config validation, CSRF cookie mode, HTML balance, dynamic Cache-Control, URL canonicalization
230
+ - **0.3.15** (2026-04-29) — cors: accept Origin: null when Sec-Fetch-Site is same-origin (browsers send opaque Origin: null on form-navigation POSTs from a page whose response sets Referrer-Policy: no-referrer; the Fetch-metadata Sec-Fetch-Site header is the matching disambiguation signal — same-origin or none means accept, anything else stays refused)
231
+ - **0.3.14** (2026-04-29) — Dockerfile hotfix + lint as release gate
232
+ - **0.3.13** (2026-04-29) — CI lint scans + Wolfi container + Trivy two-step + lint cleanup
233
+ - **0.3.12** (2026-04-29) — version bump for hotfix (rebuild container image)
234
+ - **0.3.11** (2026-04-29) — CI/CD: GitHub Actions for smoke + wiki e2e, GHCR container publish on tag, Caddy production deploy overlay
235
+ - **0.3.10** (2026-04-29) — framework correctness fixes: compression backpressure, cspNonce cacheable-render API, cors same-origin, audit namespaces
236
+ - **0.3.9** (2026-04-28) — wiki: Docker Compose deploy
237
+ - **0.3.8** (2026-04-28) — wiki: remaining four concern-group pages
238
+ - **0.3.7** (2026-04-28) — wiki: HTTP & Middleware and Crypto & Vault pages
239
+ - **0.3.6** (2026-04-28) — wiki: Auth & Permissions and Storage & State pages
240
+ - **0.3.5** (2026-04-28) — strip stale comments and template language from sources
241
+ - **0.3.4** (2026-04-28) — wiki: Welcome and Observability pages
242
+ - **0.3.3** (2026-04-28) — wiki bundler integration + admin editor enhancements
243
+ - **0.3.2** (2026-04-28) — wiki wires notify / scheduler / apiKey / webhook
244
+ - **0.3.1** (2026-04-28) — wiki design pass
245
+ - **0.3.0** (2026-04-28) — examples/wiki reference app
246
+
247
+ ## v0.2.x
248
+
249
+ - **0.2.39** (2026-04-28) — sweep auditCaptured + fakeNow patterns to b.testing
250
+ - **0.2.38** (2026-04-28) — b.testing
251
+ - **0.2.37** (2026-04-28) — b.notify
252
+ - **0.2.36** (2026-04-28) — b.i18n
253
+ - **0.2.35** (2026-04-28) — b.seeders
254
+ - **0.2.34** (2026-04-28) — b.cache
255
+ - **0.2.33** (2026-04-28) — audit captures the 5 W's; success on by default when audit is wired
256
+ - **0.2.32** (2026-04-28) — close api-key visibility holes
257
+ - **0.2.31** (2026-04-28) — auditSuccess split: precursor vs complete-event
258
+ - **0.2.30** (2026-04-28) — b.permissions
259
+ - **0.2.29** (2026-04-28) — observability backfill across primitives
260
+ - **0.2.28** (2026-04-28) — credential hash envelope versioning
261
+ - **0.2.27** (2026-04-28) — b.apiKey + repo logo assets
262
+ - **0.2.26** (2026-04-28) — b.webhook signing + verification
263
+ - **0.2.25** (2026-04-28) — b.slug
264
+ - **0.2.24** (2026-04-28) — b.retry primitive
265
+ - **0.2.23** (2026-04-28) — test helpers consolidate
266
+ - **0.2.22** (2026-04-28) — input validation
267
+ - **0.2.21** (2026-04-28) — defineClass.factory replaces per-module _err wrappers
268
+ - **0.2.20** (2026-04-28) — b.requestHelpers + metrics _normalizeLabelArg
269
+ - **0.2.19** (2026-04-28) — audit.safeEmit replaces per-module _emit wrappers
270
+ - **0.2.18** (2026-04-28) — b.protocolDispatcher
271
+ - **0.2.17** (2026-04-28) — b.observability.tap
272
+ - **0.2.16** (2026-04-28) — index.js header lists complete public surface
273
+ - **0.2.15** (2026-04-28) — request middleware captures statusCode, promotes span to route template
274
+ - **0.2.14** (2026-04-28) — router populates req.routePattern
275
+ - **0.2.13** (2026-04-28) — OAuth generic preset
276
+ - **0.2.12** (2026-04-28) — built-in metrics + framework tracing taps
277
+ - **0.2.11** (2026-04-28) — storage POST-form policy presigning + logger deprecation warning
278
+ - **0.2.10** (2026-04-28) — close api-encrypt audit gaps
279
+ - **0.2.9** (2026-04-28) — cluster-shared nonce-store + audit handler isolation
280
+ - **0.2.8** (2026-04-28) — end-to-end PQC payload encryption middleware
281
+ - **0.2.7** (2026-04-27) — websocket channel/room hub with cluster pub/sub fan-out
282
+ - **0.2.6** (2026-04-27) — mail-bounce intake primitive
283
+ - **0.2.5** (2026-04-27) — strip narrative comments
284
+ - **0.2.4** (2026-04-27) — fold logger into log + rate-limit cluster backend
285
+ - **0.2.3** (2026-04-27) — route raw literals through C.TIME / C.BYTES, type bare throws
286
+ - **0.2.2** (2026-04-27) — close gaps in the 0.2.1 scheduler and presign work
287
+ - **0.2.1** (2026-04-27) — scheduler exactly-once-globally + storage presigned upload URLs
288
+ - **0.2.0** (2026-04-27) — minor bump + queue lease-extension + Jobs DLQ
289
+
290
+ ## v0.1.x
291
+
292
+ - **0.1.111** (2026-04-27) — modular per-file test layout
293
+ - **0.1.110** (2026-04-27) — session.rotate + migrations advisory lock
294
+ - **0.1.109** (2026-04-27) — auth.oauth: OAuth 2 / OIDC client
295
+ - **0.1.108** (2026-04-27) — app-shutdown: graceful-shutdown orchestrator
296
+ - **0.1.107** (2026-04-27) — tracing: OpenTelemetry seam
297
+ - **0.1.106** (2026-04-27) — metrics: Prometheus-format counters/gauges/histograms
298
+ - **0.1.105** (2026-04-27) — csp-nonce middleware + render integration
299
+ - **0.1.104** (2026-04-27) — pagination: cursor + offset helpers
300
+ - **0.1.103** (2026-04-27) — compression middleware
301
+ - **0.1.102** (2026-04-27) — health endpoint primitive
302
+ - **0.1.101** (2026-04-27) — body-parser middleware
303
+ - **0.1.100** (2026-04-27) — safe-schema: declarative input validator
304
+ - **0.1.99** (2026-04-27) — events: breach-detection signal bus (audit-tools slice B)
305
+ - **0.1.98** (2026-04-27) — audit-tools (slice A): archive + export + verify-bundle + purge
306
+ - **0.1.97** (2026-04-27) — backup: encrypted-at-rest support db.flushToDisk + recommendedFiles
307
+ - **0.1.96** (2026-04-27) — audit cleanup A4: folder-level adjustments + DeprecateError rename
308
+ - **0.1.95** (2026-04-27) — audit cleanup A3: standardize require ordering across major lib files
309
+ - **0.1.94** (2026-04-27) — audit cleanup A2 (cont.): rename import bindings, public namespace, error classes
310
+ - **0.1.93** (2026-04-27) — audit cleanup A2: rename libs to prefix-grouped namespre-v1.0 LTS lock-in
311
+ - **0.1.92** (2026-04-27) — audit cleanup A1: lift duplicated helpers + frameworkError.defineClass
312
+ - **0.1.91** (2026-04-27) — api-snapshot: public API surface walker + breaking-change detectorslice 8b
313
+ - **0.1.90** (2026-04-27) — deprecate: runtime deprecation API for the LTS contract — slice 8a
314
+ - **0.1.89** (2026-04-27) — restore + restore-rollback: storage-backed restore + atomic swapcomplete
315
+ - **0.1.88** (2026-04-27) — backup: orchestration with pluggable storage + retention — slice 7d
316
+ - **0.1.87** (2026-04-27) — restore-bundle: extract encrypted bundle to staging — slice 7c
317
+ - **0.1.86** (2026-04-27) — backup-bundle: produce an encrypted backup bundle on disk — slice 7b
318
+ - **0.1.85** (2026-04-27) — backup-manifest: bundle schema + validate/parse/serialize — slice 7a
319
+ - **0.1.84** (2026-04-27) — backup-crypto: Argon2id KDF + XChaCha20-Poly1305 for backup files — slice 7
320
+ - **0.1.83** (2026-04-27) — mtls-ca: CA file management with engine-pluggable issuance — slice 6
321
+ - **0.1.82** (2026-04-27) — vault-passphrase-ops: seal/unseal/rotate operator primitives — slice 5
322
+ - **0.1.81** (2026-04-27) — vault-rotate.rotate: full rotation pipeline slice 4
323
+ - **0.1.80** (2026-04-27) — vault-rotate (diagnostics): schema-drift + round-trip verify — slice 3
324
+ - **0.1.79** (2026-04-27) — drop SecP256r1MLKEM768 from default TLS group preference
325
+ - **0.1.78** (2026-04-27) — pqc-agent: locked-posture HTTPS agent + http-client refactor slice 2
326
+ - **0.1.77** (2026-04-27) — pqc-gate: TCP-level PQC enforcement on ClientHello — slice 1
327
+ - **0.1.76** (2026-04-27) — bundler: content-hashed asset pipeline + manifestcomplete
328
+ - **0.1.75** (2026-04-27) — dev: file-watch + child-process restart engine — slice 6 (complete)
329
+ - **0.1.74** (2026-04-27) — cli: bin/blamejs + migrate up/down/status — slice 5
330
+ - **0.1.73** (2026-04-27) — migrations: public up/down/status runner — slice 4
331
+ - **0.1.72** (2026-04-27) — cookies: parse/serialize + sealed-value access gate — slice 3
332
+ - **0.1.71** (2026-04-27) — errors-page: rich dev page + safe prod page; middleware shim — slice 2
333
+ - **0.1.70** (2026-04-27) — log: structured JSON logging with request-id correlation slice 1
334
+ - **0.1.69** (2026-04-27) — scheduler: cron + interval over jobs/queue — slice 5 (complete)
335
+ - **0.1.68** (2026-04-27) — mail: generalize http transport, demote resend to thin preset
336
+ - **0.1.67** (2026-04-27) — mail: contract + console/memory/smtp/resend transports — slice 4
337
+ - **0.1.66** (2026-04-26) — jobs (define + enqueue + in-process worker) + createApp wiring slices 2/3
338
+ - **0.1.65** (2026-04-26) — createApp factory — slice 1
339
+ - **0.1.64** (2026-04-26) — forms + csrfProtect — slice 4 (complete)
340
+ - **0.1.63** (2026-04-26) — staticServe: file serving + ETag + SRI integrity — slice 3
341
+ - **0.1.62** (2026-04-26) — render: HTTP response helpers — slice 2
342
+ - **0.1.61** (2026-04-26) — template engine (eval-free interpreter) — slice 1
343
+ - **0.1.60** (2026-04-26) — auth.jwt with SLH-DSA-SHAKE-256f default — slice 5/5 (complete)
344
+ - **0.1.59** (2026-04-26) — middleware.attachUser + middleware.requireAuth slice 4
345
+ - **0.1.58** (2026-04-26) — auth.passkey (WebAuthn / FIDO2) slice 3
346
+ - **0.1.57** (2026-04-26) — TOTP defaults match spec (HMAC-SHA512 / 128-byte secret / 8 digits / 30s) + crypto.random truncation fix
347
+ - **0.1.56** (2026-04-26) — auth.totp: flip default to HMAC-SHA512, drop SHA-1 support
348
+ - **0.1.55** (2026-04-26) — auth.totp / lib/totp.js (RFC 6238) — slice 2
349
+ - **0.1.54** (2026-04-26) — audit-sign algorithm-agility + flip default to SLH-DSA-SHAKE-256f
350
+ - **0.1.53** (2026-04-26) — auth.password (Argon2id) slice 1/4
351
+ - **0.1.52** (2026-04-26) — consent_log integrity parity with audit_log (closes the cluster spec storage move)
352
+ - **0.1.51** (2026-04-26) — queue jobs move to external-db in cluster mode
353
+ - **0.1.50** (2026-04-26) — sessions move to external-db in cluster mode
354
+ - **0.1.49** (2026-04-26) — boot-time vault-key consistency check (closes cluster spec)
355
+ - **0.1.48** (2026-04-26) — boot-time audit-tip rollback detection in cluster mode
356
+ - **0.1.47** (2026-04-26) — fix handlers.drain unbounded loop on recursive emit (cluster-mode audit hang)
357
+ - **0.1.46** (2026-04-26) — cluster-mode audit-tip fencing-token guard
358
+ - **0.1.45** (2026-04-26) — cluster discovery surface for service-mesh / load-balancer routing
359
+ - **0.1.44** (2026-04-26) — repeating-pattern sweep: sleep + withTimeoutSignal + auth-header + listenOnRandomPort
360
+ - **0.1.43** (2026-04-26) — url-safe protocol validator at outbound boundary
361
+ - **0.1.42** (2026-04-26) — router.closeWebSockets + activeWebSockets fundamental fix
362
+ - **0.1.41** (2026-04-26) — smoke runner gains groups + fixtures + per-test timing
363
+ - **0.1.40** (2026-04-26) — router.ws integration + WebSocket lifecycle redesign
364
+ - **0.1.39** (2026-04-26) — h2 WebSocket (RFC 8441 Extended CONNECT) added to websocket.js
365
+ - **0.1.38** (2026-04-26) — lib/websocket.js (RFC 6455 server primitive)
366
+ - **0.1.37** (2026-04-26) — atomicFile.listDir primitive + sweep 5 lib sites
367
+ - **0.1.36** (2026-04-26) — testConstantsReferenceIntegrity uses framework primitives
368
+ - **0.1.35** (2026-04-26) — fix stale C.TIME.FIVE_MIN refs + add integrity test
369
+ - **0.1.34** (2026-04-26) — document h3-ready transport cache + TLS session resumption stance
370
+ - **0.1.33** (2026-04-26) — HTTP/2 backend behind same httpClient.request surface
371
+ - **0.1.32** (2026-04-26) — http-client primitive (h1 baseline) + sweep 5 adapters
372
+ - **0.1.31** (2026-04-26) — logger primitive + sweep log/logErr/console sites
373
+ - **0.1.30** (2026-04-26) — framework-error base class + sweep operational _err factories
374
+ - **0.1.29** (2026-04-26) — lazy-require primitive + sweep 12 modules
375
+ - **0.1.28** (2026-04-26) — adopt bufferSafe.secureZero in passphrase + key-wrap paths
376
+ - **0.1.27** (2026-04-26) — bufferSafe primitive + sweep parsers/atomic-file/object-store
377
+ - **0.1.26** (2026-04-26) — envSafe.readVar primitive + sweep direct process.env reads
378
+ - **0.1.25** (2026-04-26) — dedup audit-sign passphrase logic into passphrase-source
379
+ - **0.1.24** (2026-04-26) — Layer 5 extraction + smoke.js as pure orchestrator
380
+ - **0.1.23** (2026-04-26) — Layer 4 extraction: consumer modules40-consumers.js
381
+ - **0.1.22** (2026-04-26) — Layer 3 extraction: chain-writing modules30-chain.js
382
+ - **0.1.21** (2026-04-26) — Layer 2 extraction: db + framework-schema20-db.js
383
+ - **0.1.20** (2026-04-26) — Layer 1 extraction: vault + cluster + framework-schema10-state.js
384
+ - **0.1.19** (2026-04-26) — Layer 0 extraction: atomic-file + parsers + redact → 00-primitives.js
385
+ - **0.1.18** (2026-04-26) — Layer 0 extraction: json-safe tests → 00-primitives.js
386
+ - **0.1.17** (2026-04-26) — Layer 0 extraction: async-safe + handlers tests → 00-primitives.js
387
+ - **0.1.16** (2026-04-26) — first Layer 0 extraction (sql-safe + chain-writer 00-primitives.js)
388
+ - **0.1.15** (2026-04-26) — extract test/_helpers.js (foundation for layer-file split)
389
+ - **0.1.14** (2026-04-26) — smoke runner reordered by dependency layer (primitives first, consumers last)
390
+ - **0.1.13** (2026-04-26) — sql-safe + chain-writer primitives; consent gets the audit Mutex fix as a side effect of consolidation
391
+ - **0.1.12** (2026-04-26) — async-safe + handlers hardening: AbortSignal, Once.reset, isRetryable override, timer-unref bug fix, full primitive test coverage
392
+ - **0.1.11** (2026-04-26) — handlers primitive replaces fire-and-forget audit emission
393
+ - **0.1.10** (2026-04-26) — async-safe library + audit dispatch via cluster-storage
394
+ - **0.1.9** (2026-04-26) — cluster storage step 2: SQL dispatcher (lib/cluster-storage.js)
395
+ - **0.1.8** (2026-04-26) — cluster storage step 1: framework-state schema for external-db
396
+ - **0.1.7** (2026-04-26) — .env safe loader (lib/parsers/env-safe.js)
397
+ - **0.1.6** (2026-04-26) — YAML 1.2 safe-subset parser (lib/parsers/yaml-safe.js)
398
+ - **0.1.5** (2026-04-26) — TOML 1.0 safe parser (lib/parsers/toml-safe.js)
399
+ - **0.1.4** (2026-04-26) — xml-safe parser adopted in object-store list responses
400
+ - **0.1.3** (2026-04-26) — atomicFile.readSync for sync init paths
401
+ - **0.1.2** (2026-04-26) — rename b.json public API to b.jsonSafe
402
+ - **0.1.1** (2026-04-26) — rename HA → cluster (no cryptic acronyms in framework names)
403
+ - **0.1.0** (2026-04-26) — HA write-side gates wired across framework subsystems
404
+
405
+ ## v0.0.x
406
+
407
+ - **0.0.19** (2026-04-26) — HA core: leader election + fencing tokens (operator-facing primitive)
408
+ - **0.0.18** (2026-04-26) — strip internal-process narrative from framework comments
409
+ - **0.0.17** (2026-04-25) — internal adoption sweep + functional scale helpers
410
+ - **0.0.16** (2026-04-25) — atomic file I/O + safe parsers (XML, CSV)
411
+ - **0.0.15** (2026-04-25) — HTTP middleware: request lifecycle hardening (begin)
412
+ - **0.0.14** (2026-04-25) — external DB (Shape B: app-data-only, bring-your-own-client)
413
+ - **0.0.13** (2026-04-25) — log streaming + redaction + bidirectional command channel
414
+ - **0.0.12** (2026-04-25) — queue dispatcher + local SQLite-backed protocol
415
+ - **0.0.11** (2026-04-25) — gcs + azure-blob protocol adapters (object-store complete)
416
+ - **0.0.10** (2026-04-25) — sigv4 protocol adapter (S3 + R2 + B2 + MinIO + Wasabi + DO Spaces + ...)
417
+ - **0.0.9** (2026-04-25) — generic remote data layer + classification-routed bifurcation
418
+ - **0.0.8** (2026-04-25) — full tamper-proof bar (signed checkpoints, rollback detection)
419
+ - **0.0.7** (2026-04-25) — traceability hardening + dynamic PK/FK schema declarations
420
+ - **0.0.6** (2026-04-25) — security-focused JSON parsing + schema validation
421
+ - **0.0.5** (2026-04-25) — session & storage local backend
422
+ - **0.0.4** (2026-04-25) — audit chain, consent log, subject rights
423
+ - **0.0.3** (2026-04-25) — db, query builder, field-crypto, migrations
424
+ - **0.0.2** (2026-04-25) — vault & key management
425
+ - **0.0.1** (2026-04-25) — foundation