@blamejs/blamejs-shop 0.4.32 → 0.4.37

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 (65) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +1 -1
  3. package/lib/asset-manifest.json +1 -1
  4. package/lib/vendor/MANIFEST.json +72 -52
  5. package/lib/vendor/blamejs/.github/workflows/ci.yml +12 -12
  6. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +37 -5
  7. package/lib/vendor/blamejs/.github/workflows/release-container.yml +2 -2
  8. package/lib/vendor/blamejs/CHANGELOG.md +6 -0
  9. package/lib/vendor/blamejs/MIGRATING.md +12 -0
  10. package/lib/vendor/blamejs/README.md +5 -2
  11. package/lib/vendor/blamejs/SECURITY.md +4 -2
  12. package/lib/vendor/blamejs/api-snapshot.json +137 -2
  13. package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +1 -0
  14. package/lib/vendor/blamejs/index.js +4 -0
  15. package/lib/vendor/blamejs/lib/archive-read.js +2 -1
  16. package/lib/vendor/blamejs/lib/archive-tar-read.js +2 -1
  17. package/lib/vendor/blamejs/lib/atomic-file.js +5 -0
  18. package/lib/vendor/blamejs/lib/audit.js +2 -0
  19. package/lib/vendor/blamejs/lib/auth/elevation-grant.js +6 -2
  20. package/lib/vendor/blamejs/lib/auth/oauth.js +13 -0
  21. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +5 -2
  22. package/lib/vendor/blamejs/lib/cli.js +8 -1
  23. package/lib/vendor/blamejs/lib/compliance.js +4 -0
  24. package/lib/vendor/blamejs/lib/config-drift.js +2 -1
  25. package/lib/vendor/blamejs/lib/credential-hash.js +9 -0
  26. package/lib/vendor/blamejs/lib/db.js +15 -2
  27. package/lib/vendor/blamejs/lib/dsa.js +482 -0
  28. package/lib/vendor/blamejs/lib/framework-error.js +14 -0
  29. package/lib/vendor/blamejs/lib/http-client.js +5 -2
  30. package/lib/vendor/blamejs/lib/local-db-thin.js +3 -2
  31. package/lib/vendor/blamejs/lib/log-stream-local.js +1 -1
  32. package/lib/vendor/blamejs/lib/log-stream-otlp-grpc.js +9 -2
  33. package/lib/vendor/blamejs/lib/log-stream-otlp.js +16 -7
  34. package/lib/vendor/blamejs/lib/middleware/clear-site-data.js +36 -11
  35. package/lib/vendor/blamejs/lib/mtls-ca.js +2 -2
  36. package/lib/vendor/blamejs/lib/observability.js +3 -2
  37. package/lib/vendor/blamejs/lib/pipl-cn.js +377 -0
  38. package/lib/vendor/blamejs/lib/restore-rollback.js +5 -5
  39. package/lib/vendor/blamejs/lib/retention.js +16 -2
  40. package/lib/vendor/blamejs/lib/scheduler.js +12 -0
  41. package/lib/vendor/blamejs/lib/self-update.js +1 -1
  42. package/lib/vendor/blamejs/lib/session.js +64 -0
  43. package/lib/vendor/blamejs/lib/ssrf-guard.js +25 -7
  44. package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +3 -3
  45. package/lib/vendor/blamejs/lib/watcher.js +8 -0
  46. package/lib/vendor/blamejs/package.json +2 -2
  47. package/lib/vendor/blamejs/release-notes/v0.15.7.json +43 -0
  48. package/lib/vendor/blamejs/release-notes/v0.15.8.json +48 -0
  49. package/lib/vendor/blamejs/release-notes/v0.15.9.json +58 -0
  50. package/lib/vendor/blamejs/scripts/gen-migrating.js +16 -0
  51. package/lib/vendor/blamejs/scripts/generate-ssdf-attestation.js +338 -0
  52. package/lib/vendor/blamejs/test/00-primitives.js +51 -0
  53. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-rename-retry.test.js +70 -0
  54. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +250 -3
  55. package/lib/vendor/blamejs/test/layer-0-primitives/credential-hash.test.js +18 -0
  56. package/lib/vendor/blamejs/test/layer-0-primitives/db-init-extensions.test.js +32 -0
  57. package/lib/vendor/blamejs/test/layer-0-primitives/dsa.test.js +169 -0
  58. package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +40 -1
  59. package/lib/vendor/blamejs/test/layer-0-primitives/pipl-cn.test.js +172 -0
  60. package/lib/vendor/blamejs/test/layer-0-primitives/retention-floor.test.js +59 -0
  61. package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-canonicalize.test.js +64 -11
  62. package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-watchdog-stale-settle.test.js +71 -0
  63. package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +57 -0
  64. package/lib/vendor/blamejs/test/layer-0-primitives/watcher.test.js +7 -3
  65. package/package.json +2 -2
@@ -57,10 +57,10 @@ jobs:
57
57
  with:
58
58
  persist-credentials: false
59
59
 
60
- - name: Set up Node 24.14.1 LTS
60
+ - name: Set up Node 24.16.0 LTS
61
61
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
62
62
  with:
63
- node-version: '24.14.1'
63
+ node-version: '24.16.0'
64
64
 
65
65
  - name: Show Node + npm version
66
66
  run: node --version && npm --version
@@ -142,10 +142,10 @@ jobs:
142
142
  with:
143
143
  persist-credentials: false
144
144
 
145
- - name: Set up Node 24.14.1 LTS
145
+ - name: Set up Node 24.16.0 LTS
146
146
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
147
147
  with:
148
- node-version: '24.14.1'
148
+ node-version: '24.16.0'
149
149
 
150
150
  - name: Resolve wiki framework dep (file:../..)
151
151
  working-directory: examples/wiki
@@ -164,10 +164,10 @@ jobs:
164
164
  with:
165
165
  persist-credentials: false
166
166
 
167
- - name: Set up Node 24.14.1 LTS
167
+ - name: Set up Node 24.16.0 LTS
168
168
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
169
169
  with:
170
- node-version: '24.14.1'
170
+ node-version: '24.16.0'
171
171
 
172
172
  - name: Compare against committed baseline
173
173
  # Fails on breaking changes (removed methods, type changes).
@@ -191,7 +191,7 @@ jobs:
191
191
  - name: Set up Node
192
192
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
193
193
  with:
194
- node-version: 24.14.1
194
+ node-version: 24.16.0
195
195
  - name: Run vendor-currency check
196
196
  # Hits registry.npmjs.org/<pkg>/latest for each npm-mapped entry
197
197
  # (vendored bundles plus per-component versions on meta-bundles
@@ -216,7 +216,7 @@ jobs:
216
216
  - name: Set up Node
217
217
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
218
218
  with:
219
- node-version: 24.14.1
219
+ node-version: 24.16.0
220
220
  - name: Run actions-currency check
221
221
  # Resolves each pinned `uses: owner/repo@<sha> # vX.Y.Z` against
222
222
  # the GitHub API (releases/latest, falling back to highest
@@ -295,10 +295,10 @@ jobs:
295
295
  with:
296
296
  persist-credentials: false
297
297
 
298
- - name: Set up Node 24.14.1 LTS
298
+ - name: Set up Node 24.16.0 LTS
299
299
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
300
300
  with:
301
- node-version: '24.14.1'
301
+ node-version: '24.16.0'
302
302
 
303
303
  - name: Install wiki deps (links @blamejs/core for the opts probe)
304
304
  # opts-resolver.js requires @blamejs/core at top-of-file; the
@@ -326,10 +326,10 @@ jobs:
326
326
  with:
327
327
  persist-credentials: false
328
328
 
329
- - name: Set up Node 24.14.1 LTS
329
+ - name: Set up Node 24.16.0 LTS
330
330
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
331
331
  with:
332
- node-version: '24.14.1'
332
+ node-version: '24.16.0'
333
333
 
334
334
  - name: Run ESLint (max-warnings 0)
335
335
  # Capture full output so the PR comment / step summary can show
@@ -99,10 +99,10 @@ jobs:
99
99
  with:
100
100
  persist-credentials: false
101
101
 
102
- - name: Set up Node 24.14.1 LTS
102
+ - name: Set up Node 24.16.0 LTS
103
103
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
104
104
  with:
105
- node-version: '24.14.1'
105
+ node-version: '24.16.0'
106
106
 
107
107
  - name: Compute version + is_tag flag
108
108
  id: version
@@ -232,11 +232,36 @@ jobs:
232
232
  -r lib/vendor/
233
233
 
234
234
  - name: Pack tarball (the exact bytes that will be published)
235
+ # Reproducible build: npm respects SOURCE_DATE_EPOCH for the mtime
236
+ # stamped into every tar header. Deriving it from the tagged commit's
237
+ # author date makes `npm pack` byte-identical when re-run from the same
238
+ # commit, so an operator can rebuild the tarball from source and match
239
+ # the published sha256.
235
240
  run: |
241
+ SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct)"
242
+ export SOURCE_DATE_EPOCH
243
+ echo "SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH ($(date -u -d "@$SOURCE_DATE_EPOCH" 2>/dev/null || true))"
236
244
  mkdir -p dist
237
245
  npm pack --pack-destination ./dist
238
246
  ls -lh dist/
239
247
 
248
+ - name: Generate SSDF producer self-attestation (NIST SP 800-218 / OMB M-22-18)
249
+ # Generated HERE, in the validate job, BEFORE SLSA subject computation,
250
+ # so ssdf-attestation.json is covered by the SLSA L3 provenance (a
251
+ # subject hash) — a consumer who verifies the provenance also verifies
252
+ # the attestation. Deterministic from version + commit + the tagged
253
+ # commit's author date (SOURCE_DATE_EPOCH).
254
+ env:
255
+ COMMIT: ${{ github.sha }}
256
+ run: |
257
+ set -euo pipefail
258
+ SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct "$COMMIT")"
259
+ export SOURCE_DATE_EPOCH
260
+ node scripts/generate-ssdf-attestation.js \
261
+ --commit "$COMMIT" \
262
+ --out ssdf-attestation.json
263
+ echo "[ssdf-attestation] generated (SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH)"
264
+
240
265
  - name: Compute SLSA subjects (base64-encoded sha256 of every release artifact)
241
266
  # SLSA "subjects" = the artifacts the attestation describes.
242
267
  # We include the tarball + both SBOMs. The cosign sigstore
@@ -254,7 +279,7 @@ jobs:
254
279
  TARBALL=$(basename "$(find dist -maxdepth 1 -name '*.tgz' | head -1)")
255
280
  HASHES=$(
256
281
  (cd dist && sha256sum "$TARBALL") && \
257
- sha256sum sbom.cdx.json sbom.vendored.cdx.json
282
+ sha256sum sbom.cdx.json sbom.vendored.cdx.json ssdf-attestation.json
258
283
  )
259
284
  echo "[slsa-subjects] subjects:"
260
285
  echo "$HASHES"
@@ -269,6 +294,7 @@ jobs:
269
294
  dist/*.tgz
270
295
  sbom.cdx.json
271
296
  sbom.vendored.cdx.json
297
+ ssdf-attestation.json
272
298
  package.json
273
299
  CHANGELOG.md
274
300
  if-no-files-found: error
@@ -361,10 +387,10 @@ jobs:
361
387
  # in under 3 minutes combined. 15-min cap is generous.
362
388
  timeout-minutes: 15
363
389
  steps:
364
- - name: Set up Node 24.14.1 LTS + npm registry auth
390
+ - name: Set up Node 24.16.0 LTS + npm registry auth
365
391
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
366
392
  with:
367
- node-version: '24.14.1'
393
+ node-version: '24.16.0'
368
394
  registry-url: 'https://registry.npmjs.org'
369
395
 
370
396
  - name: Download publish bundle (tarball + SBOMs + CHANGELOG)
@@ -523,6 +549,11 @@ jobs:
523
549
  head -20 release-notes.md
524
550
  echo "---"
525
551
 
552
+ # ssdf-attestation.json is generated in the validate job (covered by the
553
+ # SLSA subjects + carried in the publish-bundle artifact downloaded above),
554
+ # so it is already present in the working directory for the asset bundle
555
+ # below — no separate generate step here.
556
+
526
557
  - name: Create signed release with ALL assets atomically
527
558
  # The workflow is authoritative for release creation as of
528
559
  # v0.11.7. We refuse to overwrite an existing release —
@@ -562,6 +593,7 @@ jobs:
562
593
  sbom.cdx.json sbom.cdx.json.sigstore
563
594
  sbom.vendored.cdx.json sbom.vendored.cdx.json.sigstore
564
595
  "blamejs-${VERSION}.intoto.jsonl"
596
+ ssdf-attestation.json
565
597
  )
566
598
  if [ "${{ steps.pqc-sig.outputs.available }}" = "true" ]; then
567
599
  ASSETS+=("${TARBALL}.mldsa.sig")
@@ -59,10 +59,10 @@ jobs:
59
59
  uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
60
60
  with:
61
61
  persist-credentials: false
62
- - name: Set up Node 24.14.1 LTS
62
+ - name: Set up Node 24.16.0 LTS
63
63
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
64
64
  with:
65
- node-version: '24.14.1'
65
+ node-version: '24.16.0'
66
66
  - name: Run ESLint (max-warnings 0)
67
67
  # Pinned to a known-good eslint version; matches ci.yml /
68
68
  # npm-publish.yml so the three pre-tag gates run identical
@@ -8,6 +8,12 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.15.x
10
10
 
11
+ - v0.15.9 (2026-06-13) — **Raises the Node floor to 24.16, adds SQLite parse-time resource caps, retries Windows rename locks on every atomic write and download, and ships a one-call secure logout that wipes client-side state.** This release moves the engines floor to the current Node 24 LTS patch level and adds three hardening primitives. node:sqlite handles now construct with SQLITE_LIMIT_* caps: a statement over 1 MiB is rejected at parse time (a DoS floor on the raw-SQL surface, complementary to the existing row-count gate) and ATTACH DATABASE is denied. Every final temp-to-destination rename — the file written by an atomic write, a downloaded file, a sealed vault key, a rotated log, an extracted archive entry — now routes through a single retry that rides out a transient Windows lock (antivirus, the search indexer, or a file-sync client briefly holding the destination), instead of surfacing the lock as a hard failure; the retry, previously hand-rolled and unreachable, is now the reusable b.atomicFile.renameWithRetry. And b.session.logout destroys a session and tells the browser to wipe its client-side state in one call: it emits an RFC 9527 Clear-Site-Data header and expires the session cookie before destroying the row, the secure-default logout that previously had to be assembled by hand. **Added:** *b.session.logout — one-call secure logout* — `b.session.logout(res, token, opts?)` destroys the server-side session AND tells the browser to wipe its client-side state in one call: it emits an RFC 9527 Clear-Site-Data response header (cookies + storage + cache + execution contexts by default) and expires the session cookie, then destroys the session row. `b.session.destroy` alone is a store operation with no response object, so it could not wipe the browser's cached pages, storage, or a stale tab still holding the now-revoked cookie — that wiring previously had to be mounted by hand. Pass `cookieName` to match a non-default cookie and `types` to choose the Clear-Site-Data directives. **Changed:** *Node engines floor raised to >=24.16.0* — The minimum supported Node is now 24.16.0 (the current Node 24 LTS patch level), up from 24.14.1. This is an LTS-currency bump — there are no Node CVE fixes between 24.14.1 and 24.16.0 (24.14.1 already carried the CVE-2026-21713 HMAC fix); it keeps the framework on the latest patched LTS and makes the node:sqlite resource-cap hardening below available everywhere. Pre-1.0, operators upgrade across the floor; Node 26 continues to satisfy it. **Fixed:** *b.watcher canonicalizes its root on Windows* — `b.watcher.create` now resolves its `root` to the real long path before watching. On Windows a root with an 8.3 short-name component (the system temp directory commonly resolves to one) made the native recursive backend deliver long-name event paths that no longer prefix-matched the watched root, which could abort the process under a strict libuv fs-event assertion. The watcher now canonicalizes the root (expanding short names and resolving symlinks), so events match the watched directory on Windows. **Security:** *SQLite parse-time statement-size cap* — Every node:sqlite database the framework opens — the main db handle and the CLI's handle — now constructs with a SQLITE_LIMIT_LENGTH cap: a SQL statement over 1 MiB is rejected at parse time. Because the query builder parameterizes every value, the size cap guards the raw-SQL surface (`b.db.runSql`) against an attacker-influenced megaquery the parser would otherwise process (SQLite's default is 1 GB); it is a parse-time DoS floor complementary to the existing row-count gate. Legitimate framework and operator statements are far under the cap. · *Windows rename-lock retry on every atomic rename and download* — On Windows a freshly-written file's destination is briefly held by antivirus, the search indexer, or a file-sync client (Dropbox, OneDrive), surfacing as a transient EPERM / EACCES / EBUSY on rename even though the temp file is fine. `b.atomicFile.writeSync` already retried this, but `b.httpClient.downloadStream` did not — a download into a cloud-synced or AV-scanned directory could fail hard on the lock. The retry is now the reusable `b.atomicFile.renameWithRetry`, and every final temp-to-destination rename in the framework routes through it: downloads, sealed vault keys, CA key/cert writes, log rotation, archive extraction, config-drift sidecars, the self-update binary swap, and restore/rollback moves. A non-transient error still throws immediately; POSIX renames are unaffected. **Detectors:** *Rename-retry, SQLite-limits, and Clear-Site-Data guards* — Three recurrence detectors ship with the fixes: a bare `nodeFs.renameSync` final rename that doesn't route through `atomicFile.renameWithRetry`; a main `DatabaseSync` handle constructed without the SQLITE_LIMIT_LENGTH `limits`; and a hand-rolled Clear-Site-Data header value that skips the shared RFC 9527 builder.
12
+
13
+ - v0.15.8 (2026-06-13) — **Redacts OTLP log-sink attributes to close a secret/PII egress the span fix missed, adds EU DSA and China PIPL cross-border compliance record-builders, ships an SSDF producer self-attestation with every release, and makes the published tarball reproducible.** This release closes a telemetry egress hole, adds two compliance record-builder namespaces, and hardens the release supply chain. The OTLP log sinks (HTTP-JSON and gRPC) shipped a log record's meta attributes and the resource attributes to the collector unredacted — a log line carrying a bearer token, password, or API key reached the wire verbatim (CWE-532). The 0.15.4 fix wired the telemetry redactor into the span and metric exporters but the log sinks were missed; both now run record and resource attributes through the same redactor before serialization. New b.dsa builds the EU Digital Services Act (Reg 2022/2065) records an intermediary or platform must keep — Art. 16 notice-and-action, Art. 17 statement of reasons, and the Art. 15 / 24(3) transparency report. New b.pipl builds the China PIPL cross-border transfer records — an Art. 38/40 assessment that determines whether a CAC security assessment is mandatory (CIIO, important data, or the volume / sensitive-PI thresholds), and an Art. 40 security-assessment certificate. On the supply-chain side, every release now ships ssdf-attestation.json, a machine-readable NIST SP 800-218 / OMB M-22-18 producer self-attestation mapping each secure-development practice to its implementing control, and the published tarball is now packed with SOURCE_DATE_EPOCH so an operator can rebuild it byte-for-byte from the release commit. **Added:** *b.dsa — EU Digital Services Act compliance record-builders* — `b.dsa` builds the dated, frozen records the EU Digital Services Act (Regulation (EU) 2022/2065) requires an online intermediary or platform to keep: `b.dsa.noticeAndAction` (Art. 16) records a notice against a piece of content and computes the action-due window; `b.dsa.statementOfReasons` (Art. 17) records a moderation decision with its legal or contractual ground (exactly one is required), the facts, whether it was automated, and the redress routes offered; `b.dsa.transparencyReport` (Art. 15 / 24(3)) aggregates the period counts into a report with the next-due date. The builders perform no network I/O and emit a best-effort audit event; they map to the `dsa` compliance posture. · *b.pipl — China PIPL cross-border transfer record-builders* — `b.pipl.sccFilingAssessment` builds a PIPL Art. 38/40/55 cross-border transfer assessment and determines the lawful mechanism: it forces a CAC security assessment (over a self-selected standard contract or certification) when the exporter is a critical-information-infrastructure operator, exports important data, handles personal information of more than 1,000,000 individuals, or crosses the cumulative volume / sensitive-PI thresholds. `b.pipl.securityAssessmentCertificate` records an Art. 40 security-assessment self-declaration with a 3-year validity clock. Both return frozen dated records and map to the `pipl-cn` posture. · *SSDF producer self-attestation shipped with every release* — Every release now attaches `ssdf-attestation.json` — a machine-readable NIST SP 800-218 (SSDF v1.1) / OMB M-22-18 producer self-attestation. It maps each secure-software-development practice to its implementing control in the tree (SLSA L3 provenance, SSH-signed tags, vendored zero-runtime-dep supply chain, OSV-Scanner gating, coordinated disclosure) and is deterministic from the release commit. Its sha256 is a subject of the SLSA L3 provenance, so verifying the provenance verifies the attestation has not been tampered with. Downstream consumers who require SSDF supplier-compliance evidence can download it from the release page. **Security:** *OTLP log sinks redact record and resource attributes before export* — `b.logStream`'s OTLP log sinks (HTTP-JSON and gRPC) shipped a log record's `meta` attributes and the sink's resource attributes to the collector UNREDACTED, so a log line whose meta carried a bearer token, password, or API key — or a credential placed in a resource attribute — reached the OTLP wire verbatim (CWE-532). The 0.15.4 change baked the telemetry redactor into the span and metric exporters but its detector was anchored on the span/metric encoder function names, leaving the log sinks uncovered. Both log sinks now run record and resource attributes through `b.observability.redactAttrs` before serialization, the same egress contract the span and metric exporters already hold. · *Reproducible published tarball (SOURCE_DATE_EPOCH)* — The release workflow now exports `SOURCE_DATE_EPOCH` (derived from the tagged commit's author date) before `npm pack`, so the mtime stamped into every tar header is deterministic. An operator can re-pack the package from the same commit and match the published tarball's sha256 byte-for-byte, strengthening the source-to-artifact verification path alongside the existing SLSA L3 provenance and PQC signatures. **Detectors:** *otlp-log-sink-encodes-attrs-without-redactor* — Fires when an OTLP log-sink encoder hands a raw `record.meta` or resource-attribute map to serialization without routing it through `observability.redactAttrs` — the class the span/metric detector could not see because the log sinks carry the OTLP-logs schema function names.
14
+
15
+ - v0.15.7 (2026-06-13) — **Hardens the new URL canonicalizer against an IPv4-mapped allowlist bypass, enforces the OIDC azp authorized-party check, and closes a set of audited correctness gaps in retention, credential rehashing, the scheduler, and SD-JWT key binding.** This release deepens the URL/host canonicalizer shipped in 0.15.6 and clears a batch of audited correctness gaps. The canonicalizer now folds an IPv4-mapped IPv6 address (::ffff:1.2.3.4) to its embedded IPv4 and strips every trailing dot from a host, so a dual-stack peer can no longer slip past an operator's dotted-IPv4 allowlist and host./host.. no longer evade a host comparison. (NAT64 and 6to4 hosts are deliberately kept as IPv6 so canonicalizing then classifying agrees with the SSRF classifier, which treats them as reserved.) On the OIDC side, verifyIdToken now enforces OIDC Core 3.1.3.7: a multi-audience ID token must carry an azp (authorized party) and a present azp must equal the client_id, closing a confused-deputy hole where a token minted for a different client but listing this RP in its audience array verified clean. The rest are audited fixes: retention.complianceFloor now honors the active posture set via applyPosture (the documented inheritance was unimplemented); credentialHash.needsRehash now drives the advertised SHAKE256 length-rotation (raising the output length now triggers a rehash, upgrade-only); the task scheduler no longer lets a run abandoned by its watchdog clobber the next run's state or emit a stale success when its slow promise settles late; and the SD-JWT key-binding JWT compares its audience and nonce in constant time. **Fixed:** *retention.complianceFloor honors the active compliance posture* — `b.retention.complianceFloor` required an explicit posture and never read the active posture set by `applyPosture` (the `b.compliance.set` cascade), so the documented inheritance was unimplemented dead state. It now inherits the active posture when none is passed — `complianceFloor(candidateTtlMs)` uses the active posture, an explicit posture still overrides it — and `applyPosture(null)` now clears the active posture (it was a silent no-op, so `b.compliance.clear` could not reset it). · *credentialHash.needsRehash drives the SHAKE256 length-rotation* — `b.credentialHash.needsRehash` never compared the stored SHAKE256 digest length against the configured length, so raising the output length never triggered a rehash and the advertised length-rotation was a silent no-op. It now flags a digest shorter than the configured/default length for rehash (upgrade-only — a longer-than-target digest is never shortened, matching the Argon2 convention). · *The scheduler no longer lets a watchdog-abandoned run corrupt the next run* — `b.scheduler`'s watchdog force-clears a task's running flag after `maxJobMs` so a hung handler can't lock out future fires, and the next tick re-fires. The original slow promise then settled late and unconditionally overwrote the task's running / lastFinish / lastError state and emitted a `system.scheduler.task.success`|`failure` for a run the watchdog had already given up on — clobbering the new run and double-counting. Each run is now tagged with a generation that the watchdog and every fire bump; a settle whose tag is stale is ignored. **Security:** *The URL canonicalizer folds IPv4-mapped IPv6 addresses to IPv4* — `b.safeUrl.canonicalize` / `b.ssrfGuard.canonicalizeHost` now fold an IPv4-mapped IPv6 address (`::ffff:1.2.3.4`, the `::ffff:0:0/96` block) to its embedded IPv4 dotted form. Previously it canonicalized to an IPv6 string, so a dual-stack peer never unified with an operator's `1.2.3.4` allow/deny entry — an allowlist bypass. Only the IPv4-mapped block folds, because the SSRF classifier maps it to the embedded v4 verdict directly; NAT64 (`64:ff9b::/96`) and 6to4 (`2002::/16`) are deliberately kept as IPv6, since the classifier treats a NAT64 literal as reserved and folding it would turn a blocked verdict into an allowed public IPv4. The host canonicalizer also now strips every trailing dot, so `host`, `host.`, and `host..` collapse to one form. · *verifyIdToken enforces the OIDC azp (authorized party) check* — `b.auth.oauth`'s `verifyIdToken` validated only that its `client_id` was present in the token's `aud`, ignoring `azp`. Per OIDC Core 3.1.3.7, a multi-audience ID token must carry an `azp` and a present `azp` must equal the RP's `client_id`. Without it, a token whose authorized party is a different client — but whose `aud` array also lists this RP — verified clean (a confused-deputy / token-substitution hole). The verifier now rejects a multi-audience token with no `azp` (`auth-oauth/azp-required`) and any token whose `azp` is not the `client_id` (`auth-oauth/azp-mismatch`). A single-audience token with no `azp` (the common case) is unaffected. · *SD-JWT key-binding audience and nonce compare in constant time* — `b.auth.sdJwtVc.verify` compared the key-binding JWT's `aud` and `nonce` with a short-circuiting `!==`, while the adjacent `sd_hash` check already used a constant-time compare. The `nonce` is a verifier-issued replay-defense value, so a non-constant-time compare leaks a matching-prefix timing oracle; both checks now use the constant-time helper.
16
+
11
17
  - v0.15.6 (2026-06-12) — **Closes SAML and OIDC assertion-replay windows, bounds SSE memory under a slow client, restores at-least-once delivery for a crashed outbox publisher, makes sealed-column membership queries work, ships JOSE-conformant SD-JWT signatures, and adds a URL canonicalizer for SSRF-safe comparison.** A security and correctness release. On the identity surface: a SAML Response whose Bearer (or Holder-of-Key) SubjectConfirmation omits NotOnOrAfter is now rejected instead of accepted as fresh-forever, and the OIDC ID-token verifier no longer lets a caller disable expiry validation on a normal token - the exp bypass is restricted to back-channel-logout tokens and bounded by an issued-at freshness floor. SD-JWT-VC ES256/ES384 signatures are now emitted as JOSE raw r||s, so credentials this issuer signs verify in conformant wallets and verifiers. A new b.safeUrl.canonicalize (and b.ssrfGuard.canonicalizeHost) collapses obfuscated host and IP forms to one canonical string so allowlist and SSRF comparisons can't be bypassed by encoding tricks. On the reliability side: server-sent-event channels now cap their per-connection outbound buffer and evict a stalled client instead of growing the heap without bound; the outbox reclaims a job left in-flight by a publisher that crashed mid-delivery; the background worker pool no longer drops a task queued behind one that timed out; a retention preview no longer rewrites the whole database file; an equality / membership query on an encrypted column now hashes each candidate (membership queries previously failed outright); and the on-read re-hash of a legacy lookup digest now runs on Postgres and MySQL handles, not only SQLite. **Fixed:** *SD-JWT-VC ES256 / ES384 signatures are JOSE-conformant* — `b.auth.sdJwtVc` signed and verified ES256 / ES384 credentials with node:crypto's default DER ECDSA encoding instead of the raw r||s (`ieee-p1363`) form JOSE and EUDI wallets require, so a credential this issuer signed was rejected by conformant verifiers and the library rejected conformant holders' key-binding JWTs. The issuer JWT and the holder KB-JWT now both sign and verify with `ieee-p1363`, matching the rest of the framework's JOSE signers. · *Membership queries on an encrypted column now work* — Querying an encrypted (sealed) column with `IN` / `$in` - `b.db.from(table).whereIn("email", [...])` or `b.db.collection(table).find({ email: { $in: [...] } })` - threw, because the sealed-field-to-derived-hash rewrite passed the whole candidate array to the hash lookup as a single value. Each candidate is now hashed individually and matched against both its active keyed digest and its legacy digest, so membership queries on an encrypted column return the right rows, including rows written before the lookup-hash default changed. · *A timing-out background task no longer drops the task queued behind it* — When a `b.workerPool` task timed out or its worker errored, the slot was returned to the idle pool and drained one moment before it was marked for recycling, so a task queued behind it could be dispatched to the worker about to be terminated - and came back as `workerpool/worker-exit` (or hung) instead of running. The slot is now marked recycling before the queue is drained, so the queued task waits for the replacement worker. · *The outbox recovers a job stranded by a crashed publisher* — `b.outbox` claims a row by flipping it to in-flight, but the claim scan only selected pending rows, so a publisher that crashed between claiming a row and recording delivery left that row in-flight forever - the event was silently dropped, breaking at-least-once delivery. The outbox now stamps each claim with a timestamp and, at the start of every poll, returns any in-flight row older than the claim lease (`claimReclaimMs`, default 5 minutes) to the pending pool so it is delivered. An existing outbox table gains the new column automatically. · *A retention preview no longer rewrites the whole database* — Previewing a retention rule with `b.retention` (`run(name, { dryRun: true })` or the `retention preview` command) ran a full database VACUUM for every candidate row under a regulated posture (gdpr / hipaa / and similar), because the per-row erase - which schedules the vacuum - ran before the dry-run check. A preview now computes what it would erase without touching the database; the vacuum runs only on a real erase. · *On-read lookup-digest upgrade runs on Postgres and MySQL* — When `b.cryptoField.unsealRow` re-hashed a legacy lookup digest to the current keyed form and persisted it, the UPDATE was always built for SQLite, so on a Postgres or MySQL handle the durable rewrite quoted identifiers for the wrong dialect and silently no-opped - the legacy digest stayed on disk and the migration never completed off SQLite. The rewrite now uses the handle's own dialect. **Security:** *SAML SubjectConfirmation without NotOnOrAfter is rejected* — `b.auth.saml` SP response verification treated the `NotOnOrAfter` attribute on a Bearer SubjectConfirmationData as optional: a confirmation that omitted it was accepted with no upper bound on the assertion's freshness - a captured assertion replayable indefinitely. SAML 2.0 Web Browser SSO Profile §4.1.4.2 requires Bearer confirmations to carry NotOnOrAfter. `verifyResponse` now rejects a confirmation that is missing NotOnOrAfter, has an unparseable value, or is expired; the Holder-of-Key path (Profile §3.1) is hardened the same way, including the previously-accepted unparseable-value case. · *ID-token expiry can no longer be disabled on a normal token* — `b.auth.oauth`'s `verifyIdToken` honored a `skipExpCheck` option with no constraint, so any caller could verify an expired - or replayed - ID token. That option exists only for OIDC Back-Channel Logout tokens, which carry no `exp`. It is now self-guarding: `verifyIdToken` rejects `skipExpCheck` (`auth-oauth/skip-exp-check-not-allowed`) on any token that lacks the back-channel-logout event claim, and even for a logout token it enforces an `iat` freshness floor (`auth-oauth/logout-token-stale`). The internal logout path is unaffected. · *Server-sent-event channels bound their outbound buffer* — An SSE channel wrote to the response with no regard for backpressure and no cap on buffered bytes, so a single stalled client could make the server buffer events until the heap was exhausted (a memory-exhaustion denial of service). Each channel now tracks its unflushed-byte count and, past a per-channel cap (`maxBufferedBytes`, default 1 MiB), closes the connection and throws `sse/backpressure` - evicting the slow consumer instead of buffering without limit. A client that keeps up is never affected. · *URL and host canonicalizer for SSRF-safe comparison* — New `b.safeUrl.canonicalize(url, opts?)` and `b.ssrfGuard.canonicalizeHost(host)` return the canonical, comparable form of a URL or host: scheme and host lowercased, IDN hosts emitted as their punycode A-label (a confusable / mixed-script host is rejected), every base of an IP literal (decimal, octal, hex, dotted, IPv4-mapped and zero-compressed IPv6) collapsed to one canonical address, default ports stripped, trailing-dot hosts normalized, and path percent-encoding normalized per RFC 3986. Use it to build host allowlists, deduplicate URLs, or compare a fetch target so an allowlist check can't be bypassed by encoding the same address a different way.
12
18
 
13
19
  - v0.15.5 (2026-06-12) — **Legal-hold and subject-restriction PII is sealed at rest, and a guard's compliance-posture forensic and runtime caps are applied on its default gate.** This release closes two data-protection gaps. The legal-hold registry and the subject-restriction flag stored their free-text fields - the legal basis, custodian, ticket citation, and restriction reason that link a data subject to a legal matter - in clear, because those local tables were written through a raw SQL path that bypassed the structured builder's at-rest sealing. Those columns are now sealed on write and unsealed on read, the same way the DSR ticket store already seals subject identifiers. Separately, a content guard built on the standard gate contract and gated with a compliance posture (for example b.guardCidr.gate({ compliancePosture: "hipaa" })) silently dropped that posture's forensic-snapshot cap and the profile's runtime cap, because the default gate passed the caller's raw options straight to the gate builder instead of resolving the profile and posture first - so a regulated-posture refusal carried no forensic evidence. The default gate now resolves the profile and posture before building the gate, matching the hand-written guard gates. **Security:** *Legal-hold and subject-restriction PII is sealed at rest* — `b.legalHold`'s `_blamejs_legal_hold` registry stored the hold reason, custodian, placed-by, and citation in clear, and `b.subject.restrict`'s `_blamejs_subject_restrictions` stored the restriction reason in clear - free text that ties a data subject to a litigation hold or an Art. 18 processing restriction. Those rows were written through a raw `sql.insert` + `prepare().run()` path that bypassed the structured builder's automatic field sealing (the subject-restrictions table even declared the field as sealed, but the raw write never applied it). Both now seal those columns on write and unseal on read through `cryptoField`, so the legal-basis and custodian text is encrypted at rest under the deployment's vault key. Pre-existing plaintext rows continue to read correctly (the unseal path passes through an already-plaintext value). · *A guard's default gate applies its compliance-posture forensic and runtime caps* — A guard built on `b.gateContract.defineGuard` with the standard gate (no bespoke gate) and gated with a compliance posture dropped that posture's `forensicSnippetBytes` cap and the profile's `maxRuntimeMs` cap: the default gate passed the caller's raw options straight to the gate builder, which reads those caps directly, but the values live on the resolved profile and posture, not the raw options. The effect was that a regulated-posture refusal captured no forensic snapshot (the cap defaulted to 0, i.e. disabled) and the check ran without the profile's runtime bound. The default gate now resolves the profile and posture before building the gate - matching the hand-written guard gates - so `gate({ compliancePosture: "hipaa" })` applies the posture's forensic cap and the profile's runtime cap as documented.
@@ -14,6 +14,18 @@ The framework has no `deprecate()`-marked surface awaiting removal.
14
14
 
15
15
  Listed newest-first.
16
16
 
17
+ ### v0.15.7 — `b.auth.oauth verifyIdToken — azp (authorized party) is now enforced`
18
+
19
+ verifyIdToken now applies OIDC Core 3.1.3.7: a multi-audience ID token (aud is an array with more than one entry) MUST carry an azp claim, and a present azp MUST equal the configured client_id. A token whose azp is a different client, or a multi-audience token with no azp, now throws (auth-oauth/azp-mismatch / auth-oauth/azp-required). Previously only `aud contains client_id` was checked, so a token authorized for a different party but also listing this RP verified clean.
20
+
21
+ No change for the common single-audience ID token with no azp. If your IdP issues multi-audience ID tokens, ensure it sets azp to your client_id (it should, per the spec) — otherwise verifyIdToken will now reject them. This is a security fix; a token that fails the new check was authorized for a different client.
22
+
23
+ ### v0.15.7 — `b.safeUrl.canonicalize — IPv4-mapped hosts fold to IPv4`
24
+
25
+ b.safeUrl.canonicalize / b.ssrfGuard.canonicalizeHost now fold an IPv4-mapped IPv6 host (::ffff:1.2.3.4) to its embedded IPv4 dotted form, and strip every trailing dot from a host. In 0.15.6 it canonicalized to an IPv6 string and only one trailing dot was stripped. NAT64 / 6to4 hosts stay IPv6.
26
+
27
+ No code change is needed — this makes a dual-stack / NAT64 peer unify with a dotted-IPv4 allow/deny entry as intended. If you persisted canonical host strings produced by 0.15.6 (e.g. as cache or dedup keys) and compare them against freshly-canonicalized hosts, recompute them: an IPv4-mapped host now yields the dotted IPv4 instead of the bracketed IPv6, and a multi-trailing-dot host yields the bare name.
28
+
17
29
  ### v0.15.6 — `b.auth.sdJwtVc — ES256 / ES384 signatures are now JOSE raw r||s, not DER`
18
30
 
19
31
  `b.auth.sdJwtVc` now signs and verifies ES256 / ES384 with `dsaEncoding: "ieee-p1363"` (raw r||s), the encoding JOSE / JWS and EUDI wallets require. Previously it used node:crypto's default DER ECDSA encoding, so a credential this issuer signed was rejected by conformant verifiers and the library rejected conformant holders' key-binding JWTs. The signature bytes change shape (64 bytes for ES256, 96 for ES384, no leading `0x30` SEQUENCE tag).
@@ -52,7 +52,7 @@ var b = require("@blamejs/core");
52
52
  })();
53
53
  ```
54
54
 
55
- **Requirements:** Node.js 24.14+ (current active LTS, fixes CVE-2026-21713 non-constant-time HMAC compare). Node 26 satisfies the floor and the framework test suite runs cleanly on it today; the floor itself will bump to `>=26.x` when Node 26 promotes to Active LTS. Two Node 26 platform changes operators integrating with blamejs should know about: the new `localStorage` global (the framework's storage backend is `b.backup.diskStorage`; the legacy `b.backup.localStorage` alias was removed in v0.11.20 — update call sites accordingly), and the seed-only ML-KEM / ML-DSA PKCS8 export shape (sealed material from Node 24 re-imports cleanly on Node 26; new material from Node 26 in the seed-only shape). See [SECURITY.md](SECURITY.md#node-26-compatibility) for the details.
55
+ **Requirements:** Node.js 24.16+ (current active LTS line; 24.14.1 fixed CVE-2026-21713 non-constant-time HMAC compare, 24.16 is the current patch level). Node 26 satisfies the floor and the framework test suite runs cleanly on it today; the floor itself will bump to `>=26.x` when Node 26 promotes to Active LTS. Two Node 26 platform changes operators integrating with blamejs should know about: the new `localStorage` global (the framework's storage backend is `b.backup.diskStorage`; the legacy `b.backup.localStorage` alias was removed in v0.11.20 — update call sites accordingly), and the seed-only ML-KEM / ML-DSA PKCS8 export shape (sealed material from Node 24 re-imports cleanly on Node 26; new material from Node 26 in the seed-only shape). See [SECURITY.md](SECURITY.md#node-26-compatibility) for the details.
56
56
 
57
57
  ## What ships in the box
58
58
 
@@ -60,7 +60,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
60
60
 
61
61
  ### Data layer
62
62
 
63
- - **SQLite with sealed-by-default columns** — `b.db`, migrations, seeders, atomic-file writes
63
+ - **SQLite with sealed-by-default columns** — `b.db`, migrations, seeders, atomic-file writes; the db handle constructs with a SQLITE_LIMIT_LENGTH parse-time cap (a >1 MiB statement is rejected) as a DoS floor on the raw-SQL surface
64
64
  - **Chainable query builder** — atomic `.increment(col, delta)`, closure-form `.whereGroup` / top-level `.orWhere` OR composition, `.search(fields, term)` LIKE-OR with safe `%`/`_` ESCAPE handling, `.paginate(opts)` returning `{ items, total, page, totalPages }`; a column-membership gate (`db.init({ columnGate })`, default reject) fails a query closed when it names a column the table never declared, and `whereRaw` refuses an embedded string literal so values bind through placeholders
65
65
  - **Mongo-style document-store facade** — `b.db.collection(name, opts?)` with `$set` / `$inc` / `$unset` / `$eq` / `$ne` / `$gt` / `$gte` / `$lt` / `$lte` / `$in` / `$like`; schemaless-document opts via `overflow: "<col>"` (folds unknown fields into a JSON-text column; rewrites `WHERE` on virtual fields to `JSON_EXTRACT`), `jsonColumns: [...]` (auto-stringify on write + parse via `b.safeJson` on read), `sealedFields: { email: "emailHash" }` (co-locates a `b.cryptoField` sealed-column / derived-hash declaration so plaintext lookups auto-rewrite to hash-column lookups)
66
66
  - **DB lifecycle** — in-memory encrypted snapshot via `b.db.snapshot()`; standalone encrypted-DB-file lifecycle (`b.db.fileLifecycle({ dataDir, vault })` — decrypt-to-tmpfs, periodic re-encrypt flush, graceful shutdown — same envelope as `b.db`, no schema/audit-chain coupling); `db.init` opt-outs `frameworkTables: false` / `auditSigning: false` and path overrides `encryptedDbPath` / `encryptedDbName` / `dbKeyPath`
@@ -86,6 +86,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
86
86
  - Opaque-userId anonymous sessions via `create({ anonymous: true })`
87
87
  - Idle / absolute timeouts, fingerprint drift detection + anomaly scoring, brute-force lockout
88
88
  - Session-fixation rotation (`b.session.rotate`) re-keys the sid-bound device fingerprint to the new id — pass the same `{ req, fingerprintFields }` used at `create` (a fingerprint-bound session rotated without `req` is refused, so the binding can never silently break or false-drift)
89
+ - One-call secure logout (`b.session.logout(res, token)`) destroys the session AND wipes client-side state — emits an RFC 9527 Clear-Site-Data header (cookies + storage + cache) and expires the session cookie before deleting the row
89
90
  - **Authorization** — RBAC + per-role DB binding + role-spec `requireMfa` + per-route MFA freshness window + ABAC predicate registry (`b.permissions`); API keys with rotation (`b.apiKey`)
90
91
  - **Workflow gates** — break-glass column gates with second-factor + audit (`b.breakGlass`); two-person-rule m-of-n approval with cooling-off lock + cancellation (`b.dualControl`)
91
92
  - **Financial / Open Banking** — FAPI 2.0 Final composite posture (PAR + PKCE-S256 + DPoP-or-mTLS + RFC 9207); runtime enforcement helpers `b.fapi2.assertCallback` (refuses missing iss + bare-param under message-signing) and `b.fapi2.assertAuthzRequest` (refuses non-JAR); CFPB §1033 / FDX 6.0 consumer-financial-data-sharing wrapper (`b.fdx`)
@@ -194,6 +195,8 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
194
195
  - **AEDT bias audit** — NYC Local Law 144 bias-audit figures (`b.ai.aedtBiasAudit`): selection / scoring rates and EEOC four-fifths-rule impact ratios across sex, race/ethnicity, and their intersection, with the most-selected group and adverse-impact flags (impact ratio < 0.8) for the annual published summary; sub-2% categories excludable per DCWP §5-301
195
196
  - **Frontier AI protocol** — California SB 53 (Transparency in Frontier AI Act) obligations (`b.ai.frontierModelProtocol`): classify the frontier-model (>10²⁶ training FLOPs) and large-frontier-developer (>$500M revenue) thresholds, enumerate the resulting obligations, check a safety framework for required elements, and build a critical-safety-incident report with the 15-day / 24-hour California OES notification deadline (`.incidentReport`)
196
197
  - **GPAI Code-of-Practice adherence** — signed, tamper-evident EU AI Act Art. 53 / 55 adherence declarations with a regulation-derived obligation set (a systemic-risk model omitting the Art. 55 chapter is refused) and SHA3-512 evidence binding, emitted inside an ML-DSA-87-signed CycloneDX 1.6 ML-BOM and replay-checked on verify (`b.compliance.aiAct.gpai.declareAdherence` / `verifyAdherence`)
198
+ - **EU Digital Services Act records** — dated, frozen builders for the content-governance loop the DSA (Reg 2022/2065) requires: Art. 16 notice-and-action with the action-due window, Art. 17 statement of reasons (exactly one legal-or-contractual ground, redress routes, automated-decision flag), and the Art. 15 / 24(3) transparency report (`b.dsa.noticeAndAction` / `statementOfReasons` / `transparencyReport`)
199
+ - **China PIPL cross-border transfer records** — Art. 38/40/55 assessment that forces a CAC security assessment over a self-selected standard contract when a critical-information-infrastructure operator, important data, or the volume / sensitive-PI thresholds are in scope, plus an Art. 40 security-assessment certificate with a 3-year validity clock (`b.pipl.sccFilingAssessment` / `securityAssessmentCertificate`)
197
200
 
198
201
  ### Compliance regimes
199
202
 
@@ -109,6 +109,8 @@ Re-running `scripts/generate-release-signing-key.js` rotates the key. Rotation u
109
109
 
110
110
  The four trust roots — SLSA L3 npm provenance, Sigstore-keyless SBOM signing, SSH-signed tags (v0.9.7+), ML-DSA-65 release-signing sidecar (v0.11.18+) — are independently verifiable. Tampering with any single root is detected by the others.
111
111
 
112
+ Every release also ships `ssdf-attestation.json` (v0.15.8+) — a machine-readable NIST SP 800-218 (SSDF v1.1) / OMB M-22-18 producer self-attestation mapping each secure-development practice to its implementing control in this tree. Its sha256 is a subject of the SLSA L3 provenance (the `.intoto.jsonl`), so verifying the provenance verifies the attestation has not been tampered with. The published tarball is packed with `SOURCE_DATE_EPOCH` set to the tagged commit's author date, so re-packing from the release commit reproduces the published sha256 byte-for-byte.
113
+
112
114
  ### Verifying SLSA L3 provenance with `slsa-verifier`
113
115
 
114
116
  `gh attestation verify` walks the provenance chain via the GitHub API. For an offline / API-independent verification path, pin `slsa-verifier` v2.7.1 ([slsa-framework/slsa-verifier releases](https://github.com/slsa-framework/slsa-verifier/releases/tag/v2.7.1)):
@@ -340,7 +342,7 @@ This is the minimum-viable security posture for a production deployment. The fra
340
342
  - [ ] For inbound admin paths reachable on the public network: mount `b.middleware.networkAllowlist({ paths: ["/admin"], allowedCidrs: [...] })` as the in-process CIDR fence above the application-layer auth gate
341
343
  - [ ] For outbound integrations: pin destination hosts via `b.httpClient.request({ allowedHosts: ["api.partner.com", ".internal.example.com"] })` so a compromised process can't reach arbitrary upstreams
342
344
  - [ ] For test suites that mount a mock server on `127.0.0.1` to exercise `b.httpClient` / `b.wsClient` against deterministic fixtures: keep the SSRF gate ON in production code, then in tests inject an explicit `allowInternal: true` (per-call) alongside the mock-server URL. The opt-in is loud and audited, the production posture is unchanged, and operators reviewing the test grep see exactly which call sites talk to internal addresses. Cloud-metadata IPs (`169.254.169.254` etc.) stay hard-deny under any `allowInternal` value
343
- - [ ] Before comparing a URL against an allowlist/denylist, building a cache/dedup key from one, or deciding whether a host is internal: canonicalize it with `b.safeUrl.canonicalize(url)` (or `b.ssrfGuard.canonicalizeHost(host)` for the host alone) first. It collapses obfuscated host + IP spellings — decimal / octal / hex / dotted IPv4, IPv4-mapped and zero-compressed IPv6, IDN → punycode (rejecting confusable hosts), default ports, trailing-dot hosts, path percent-encoding — to one comparable string, so `0177.0.0.1`, `0x7f.1`, `2130706433`, and `127.0.0.1` can't slip past a check that only string-matched the literal. Pair it with the `b.ssrfGuard` DNS-resolving gate, which still runs at fetch time
345
+ - [ ] Before comparing a URL against an allowlist/denylist, building a cache/dedup key from one, or deciding whether a host is internal: canonicalize it with `b.safeUrl.canonicalize(url)` (or `b.ssrfGuard.canonicalizeHost(host)` for the host alone) first. It collapses obfuscated host + IP spellings — decimal / octal / hex / dotted IPv4, zero-compressed IPv6, IDN → punycode (rejecting confusable hosts), default ports, trailing-dot hosts (every count), path percent-encoding — to one comparable string, so `0177.0.0.1`, `0x7f.1`, `2130706433`, and `127.0.0.1` can't slip past a check that only string-matched the literal. An IPv4-mapped IPv6 host (`::ffff:1.2.3.4`) folds to its embedded IPv4 so a dual-stack peer unifies with a dotted-IPv4 entry, and credentials are dropped from the canonical form (NAT64 / 6to4 stay IPv6 so canonicalize-then-classify agrees with the SSRF classifier). Pair it with the `b.ssrfGuard` DNS-resolving gate, which still runs at fetch time
344
346
  - [ ] For file-upload routes: gate on magic bytes via `b.fileType.assertOneOf(buffer, ["image", "application/pdf"])` — never trust the client-supplied `Content-Type` alone
345
347
  - [ ] For routes that emit or accept CSV (operator exports, user-supplied uploads, mail attachments, object-store deliverables): wire `b.guardCsv.gate({ profile: "strict" })` into `b.staticServe.create({ contentSafety: { ".csv": gate } })` and `b.fileUpload.create({ contentSafety: gate })` — strict profile applies the OWASP-recommended `prefix-tab` formula-injection mitigation, the dangerous-function denylist (HYPERLINK / WEBSERVICE / IMAGE / IMPORT* / RTD / DDE / CALL), bidi / homoglyph / control / null-byte / BOM detection, dialect-ambiguity refusal, and CSV-bomb size caps; pick `compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2"` instead of (or layered over) the profile when the workload is regulated
346
348
  - [ ] For routes that accept YAML (config uploads, CI/CD pipelines, infra-as-code, document-import flows — ANY operator-supplied YAML the server parses): `b.guardYaml.gate({ profile: "strict" })` is wired by default into `b.fileUpload` + `b.staticServe` as of v0.7.12. For inbound YAML bodies that don't go through those primitives, wire `b.guardYaml.parse(body, { profile: "strict" })` before passing the parsed structure to operator handlers — strict refuses deserialization-tag RCE (defends CVE-2026-24009 Docling/PyYAML, CVE-2022-1471 SnakeYAML, CVE-2017-18342 PyYAML class), billion-laughs alias recursion (CVE-2026-27807 MarkUs class), Norway-problem implicit booleans, multi-document streams, leading-zero octals, duplicate keys, merge-key anchor-chains, bidi/null/control chars. Unlike JSON, YAML's threat surface includes language-specific deserialization triggers — `!!python/object/new:...` / `!!java.util.HashMap` / `!!ruby/object` etc. — which the source-level scan catches before any downstream parser (PyYAML / SnakeYAML / js-yaml) sees them
@@ -466,7 +468,7 @@ CVE classes the framework tracks but does not currently ship a primitive for —
466
468
 
467
469
  ## Node 26 compatibility
468
470
 
469
- Today's `engines.node` floor is `>=24.14.1` and the release container pins `node:24-alpine`. Node 26 satisfies the floor and the framework's test suite runs cleanly on Node 26 today. When Node 26 promotes to Active LTS (target Oct 2026), the framework will bump the floor to `>=26.x` in a dedicated release that ships the queued refactors (Map.getOrInsertComputed sweep, Ed25519 context-parameter adoption, PKCS8 reverse-direction roundtrip test) as one PR.
471
+ Today's `engines.node` floor is `>=24.16.0` and the release container pins `node:24-alpine`. Node 26 satisfies the floor and the framework's test suite runs cleanly on Node 26 today. When Node 26 promotes to Active LTS (target Oct 2026), the framework will bump the floor to `>=26.x` in a dedicated release that ships the queued refactors (Map.getOrInsertComputed sweep, Ed25519 context-parameter adoption, PKCS8 reverse-direction roundtrip test) as one PR.
470
472
 
471
473
  Two Node 26 platform-level changes operators integrating with blamejs should be aware of now:
472
474
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
- "frameworkVersion": "0.15.6",
4
- "createdAt": "2026-06-13T07:00:22.197Z",
3
+ "frameworkVersion": "0.15.9",
4
+ "createdAt": "2026-06-13T22:58:01.805Z",
5
5
  "exports": {
6
6
  "a2a": {
7
7
  "type": "object",
@@ -2358,6 +2358,10 @@
2358
2358
  "type": "function",
2359
2359
  "arity": 2
2360
2360
  },
2361
+ "renameWithRetry": {
2362
+ "type": "function",
2363
+ "arity": 2
2364
+ },
2361
2365
  "write": {
2362
2366
  "type": "function",
2363
2367
  "arity": 3
@@ -15415,6 +15419,100 @@
15415
15419
  }
15416
15420
  }
15417
15421
  },
15422
+ "dsa": {
15423
+ "type": "object",
15424
+ "members": {
15425
+ "DECISIONS": {
15426
+ "type": "instance",
15427
+ "ctorName": "Array"
15428
+ },
15429
+ "DsaError": {
15430
+ "type": "function",
15431
+ "arity": 4
15432
+ },
15433
+ "METRIC_FIELDS": {
15434
+ "type": "instance",
15435
+ "ctorName": "Array"
15436
+ },
15437
+ "NOTICE_TYPES": {
15438
+ "type": "object",
15439
+ "members": {
15440
+ "illegal-content": {
15441
+ "type": "object",
15442
+ "members": {
15443
+ "description": {
15444
+ "type": "primitive",
15445
+ "valueType": "string"
15446
+ },
15447
+ "statementOfReasonsRequired": {
15448
+ "type": "primitive",
15449
+ "valueType": "boolean"
15450
+ }
15451
+ }
15452
+ },
15453
+ "ip-infringement": {
15454
+ "type": "object",
15455
+ "members": {
15456
+ "description": {
15457
+ "type": "primitive",
15458
+ "valueType": "string"
15459
+ },
15460
+ "statementOfReasonsRequired": {
15461
+ "type": "primitive",
15462
+ "valueType": "boolean"
15463
+ }
15464
+ }
15465
+ },
15466
+ "other": {
15467
+ "type": "object",
15468
+ "members": {
15469
+ "description": {
15470
+ "type": "primitive",
15471
+ "valueType": "string"
15472
+ },
15473
+ "statementOfReasonsRequired": {
15474
+ "type": "primitive",
15475
+ "valueType": "boolean"
15476
+ }
15477
+ }
15478
+ },
15479
+ "terms-violation": {
15480
+ "type": "object",
15481
+ "members": {
15482
+ "description": {
15483
+ "type": "primitive",
15484
+ "valueType": "string"
15485
+ },
15486
+ "statementOfReasonsRequired": {
15487
+ "type": "primitive",
15488
+ "valueType": "boolean"
15489
+ }
15490
+ }
15491
+ }
15492
+ }
15493
+ },
15494
+ "REDRESS_OPTIONS": {
15495
+ "type": "instance",
15496
+ "ctorName": "Array"
15497
+ },
15498
+ "listTransparencyMetrics": {
15499
+ "type": "function",
15500
+ "arity": 0
15501
+ },
15502
+ "noticeAndAction": {
15503
+ "type": "function",
15504
+ "arity": 1
15505
+ },
15506
+ "statementOfReasons": {
15507
+ "type": "function",
15508
+ "arity": 1
15509
+ },
15510
+ "transparencyReport": {
15511
+ "type": "function",
15512
+ "arity": 1
15513
+ }
15514
+ }
15515
+ },
15418
15516
  "dsr": {
15419
15517
  "type": "object",
15420
15518
  "members": {
@@ -16341,6 +16439,10 @@
16341
16439
  "type": "function",
16342
16440
  "arity": 4
16343
16441
  },
16442
+ "DsaError": {
16443
+ "type": "function",
16444
+ "arity": 4
16445
+ },
16344
16446
  "ExternalDbError": {
16345
16447
  "type": "function",
16346
16448
  "arity": 4
@@ -16561,6 +16663,10 @@
16561
16663
  "type": "function",
16562
16664
  "arity": 4
16563
16665
  },
16666
+ "PiplError": {
16667
+ "type": "function",
16668
+ "arity": 4
16669
+ },
16564
16670
  "PrivacyError": {
16565
16671
  "type": "function",
16566
16672
  "arity": 4
@@ -45530,6 +45636,31 @@
45530
45636
  "type": "function",
45531
45637
  "arity": 3
45532
45638
  },
45639
+ "pipl": {
45640
+ "type": "object",
45641
+ "members": {
45642
+ "LEGAL_BASES": {
45643
+ "type": "instance",
45644
+ "ctorName": "Array"
45645
+ },
45646
+ "PiplError": {
45647
+ "type": "function",
45648
+ "arity": 4
45649
+ },
45650
+ "RISK_RATINGS": {
45651
+ "type": "instance",
45652
+ "ctorName": "Array"
45653
+ },
45654
+ "sccFilingAssessment": {
45655
+ "type": "function",
45656
+ "arity": 1
45657
+ },
45658
+ "securityAssessmentCertificate": {
45659
+ "type": "function",
45660
+ "arity": 1
45661
+ }
45662
+ }
45663
+ },
45533
45664
  "pqcAgent": {
45534
45665
  "type": "object",
45535
45666
  "members": {
@@ -50641,6 +50772,10 @@
50641
50772
  "type": "function",
50642
50773
  "arity": 1
50643
50774
  },
50775
+ "logout": {
50776
+ "type": "function",
50777
+ "arity": 3
50778
+ },
50644
50779
  "purgeExpired": {
50645
50780
  "type": "function",
50646
50781
  "arity": 0
@@ -53,6 +53,7 @@ var KNOWN_POSTURES = {
53
53
  "sox-404": 1, "sec-cyber": 1, ferpa: 1, glba: 1, ccpa: 1, "modpa": 1,
54
54
  "ca-sopipa": 1, coppa: 1, "coppa-2025": 1,
55
55
  "nydfs-500": 1, "eu-data-act": 1, "eu-ai-act": 1,
56
+ dsa: 1, "pipl-cn": 1,
56
57
  // v0.10.8 — EU AI Act Art. 50 + Art. 11 + AB-853 + CAC + AI governance
57
58
  "eu-ai-act-art-50": 1, "eu-ai-act-art-11": 1,
58
59
  "ca-ab-853": 1, "ca-sb-942": 1,
@@ -89,6 +89,8 @@ audit.export = function (opts) {
89
89
  var auditChain = require("./lib/audit-chain");
90
90
  var consent = require("./lib/consent");
91
91
  var privacy = require("./lib/privacy");
92
+ var dsa = require("./lib/dsa");
93
+ var pipl = require("./lib/pipl-cn");
92
94
  var subject = require("./lib/subject");
93
95
  var session = require("./lib/session");
94
96
  var storage = require("./lib/storage");
@@ -467,6 +469,8 @@ module.exports = {
467
469
  events: events,
468
470
  consent: consent,
469
471
  privacy: privacy,
472
+ dsa: dsa,
473
+ pipl: pipl,
470
474
  subject: subject,
471
475
  session: session,
472
476
  storage: storage,
@@ -42,6 +42,7 @@ var nodeFs = require("node:fs");
42
42
  var C = require("./constants");
43
43
  var lazyRequire = require("./lazy-require");
44
44
  var { defineClass } = require("./framework-error");
45
+ var atomicFile = require("./atomic-file");
45
46
 
46
47
  var ArchiveReadError = defineClass("ArchiveReadError", { alwaysPermanent: true });
47
48
 
@@ -925,7 +926,7 @@ function zip(adapter, opts) {
925
926
  // the rename targets a non-existent path.
926
927
  var tmpPath = resolvedPath + ".__blamejs-archive-read-tmp__";
927
928
  nodeFs.writeFileSync(tmpPath, body);
928
- nodeFs.renameSync(tmpPath, resolvedPath);
929
+ atomicFile.renameWithRetry(tmpPath, resolvedPath);
929
930
  written.push({ name: entry.name, bytesWritten: body.length, path: resolvedPath });
930
931
  bytesExtracted += body.length;
931
932
  }
@@ -14,6 +14,7 @@ var C = require("./constants");
14
14
  var lazyRequire = require("./lazy-require");
15
15
  var safeBuffer = require("./safe-buffer");
16
16
  var archiveTar = require("./archive-tar");
17
+ var atomicFile = require("./atomic-file");
17
18
 
18
19
  var TarError = archiveTar.TarError;
19
20
  var _parseHeader = archiveTar._parseHeader;
@@ -428,7 +429,7 @@ function tar(adapter, opts) {
428
429
  }
429
430
  var tmpPath = resolvedPath + ".__blamejs-archive-tar-tmp__";
430
431
  nodeFs.writeFileSync(tmpPath, body);
431
- nodeFs.renameSync(tmpPath, resolvedPath);
432
+ atomicFile.renameWithRetry(tmpPath, resolvedPath);
432
433
  written.push({ name: entry.name, bytesWritten: body.length, path: resolvedPath });
433
434
  bytesExtracted += body.length;
434
435
  }
@@ -1077,6 +1077,11 @@ module.exports = {
1077
1077
  fsync: fsync,
1078
1078
  fsyncDir: fsyncDir,
1079
1079
  ensureDir: ensureDir,
1080
+ // Atomic rename with a bounded retry on Windows-transient lock errors
1081
+ // (EPERM/EACCES/EBUSY from AV / search indexer / Dropbox / OneDrive briefly
1082
+ // holding the destination). Exposed so any final temp->dest rename routes
1083
+ // through the same retry instead of hand-rolling it (or, worse, omitting it).
1084
+ renameWithRetry: _renameWithRetry,
1080
1085
  copyDirRecursive: copyDirRecursive,
1081
1086
  pathTimestamp: pathTimestamp,
1082
1087
  conflictPath: conflictPath,
@@ -343,6 +343,7 @@ var FRAMEWORK_NAMESPACES = [
343
343
  // (role-switching, RLS-shaped events)
344
344
  "dkim", // b.mail.dkim (DKIM-Signature generation events)
345
345
  "dora", // b.dora (DORA Article 17: dora.incident.classified / reported / draftFinal)
346
+ "dsa", // b.dsa (EU Digital Services Act: dsa.notice.recorded / dsa.sor.recorded / dsa.transparency_report.generated)
346
347
  "dsr", // b.dsr (Data Subject Rights workflow: dsr.ticket.* / dsr.source.*)
347
348
  "dual", // b.dualControl (dual.grant.requested / approved / denied / consumed / expired / self_approval_denied)
348
349
  "mail", // b.mail (b.mail-bounce uses "system.mail.*")
@@ -357,6 +358,7 @@ var FRAMEWORK_NAMESPACES = [
357
358
  "inbox", // b.inbox (inbox.received / handled / handle_failed / swept)
358
359
  "flag", // b.flag (flag.evaluated / flag.evaluation.error / flag.cache.bust)
359
360
  "permissions", // b.permissions
361
+ "pipl", // b.pipl (China PIPL cross-border: pipl.transfer.assessed / pipl.security_assessment.recorded)
360
362
  "pqcagent", // b.pqcAgent (pqcagent.operator_group.accepted)
361
363
  "privacy", // b.privacy (privacy.vendor_review.recorded)
362
364
  "restore", // b.restore