@blamejs/blamejs-shop 0.4.31 → 0.4.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +392 -278
- package/lib/vendor/blamejs/.github/workflows/ci.yml +34 -3
- package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +21 -4
- package/lib/vendor/blamejs/.gitignore +6 -0
- package/lib/vendor/blamejs/CHANGELOG.md +26 -0
- package/lib/vendor/blamejs/MIGRATING.md +43 -0
- package/lib/vendor/blamejs/README.md +8 -6
- package/lib/vendor/blamejs/SECURITY.md +19 -3
- package/lib/vendor/blamejs/api-snapshot.json +2190 -664
- package/lib/vendor/blamejs/docker/caddy/localstack.Caddyfile +19 -0
- package/lib/vendor/blamejs/docker/init/generate-certs.sh +1 -1
- package/lib/vendor/blamejs/docker/otel/config.yaml +42 -0
- package/lib/vendor/blamejs/docker/otel/export/.gitkeep +0 -0
- package/lib/vendor/blamejs/docker/postgres/initdb/10-replication.sh +15 -0
- package/lib/vendor/blamejs/docker/postgres/replica-entrypoint.sh +38 -0
- package/lib/vendor/blamejs/docker/toxiproxy/toxiproxy.json +14 -0
- package/lib/vendor/blamejs/docker-compose.test.yml +209 -0
- package/lib/vendor/blamejs/examples/wiki/lib/page-generator.js +132 -0
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +221 -61
- package/lib/vendor/blamejs/examples/wiki/lib/source-doc-parser.js +144 -9
- package/lib/vendor/blamejs/examples/wiki/test/e2e.js +99 -0
- package/lib/vendor/blamejs/fuzz/guard-sql.fuzz.js +36 -0
- package/lib/vendor/blamejs/index.js +4 -0
- package/lib/vendor/blamejs/lib/agent-envelope-mac.js +104 -0
- package/lib/vendor/blamejs/lib/agent-event-bus.js +105 -4
- package/lib/vendor/blamejs/lib/agent-posture-chain.js +8 -42
- package/lib/vendor/blamejs/lib/ai-content-detect.js +9 -10
- package/lib/vendor/blamejs/lib/api-key.js +158 -77
- package/lib/vendor/blamejs/lib/atomic-file.js +62 -4
- package/lib/vendor/blamejs/lib/audit-chain.js +47 -11
- package/lib/vendor/blamejs/lib/audit-sign.js +77 -2
- package/lib/vendor/blamejs/lib/audit-tools.js +79 -51
- package/lib/vendor/blamejs/lib/audit.js +259 -123
- package/lib/vendor/blamejs/lib/auth/oauth.js +53 -9
- package/lib/vendor/blamejs/lib/auth/openid-federation.js +108 -47
- package/lib/vendor/blamejs/lib/auth/saml.js +6 -8
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +31 -5
- package/lib/vendor/blamejs/lib/backup/index.js +45 -10
- package/lib/vendor/blamejs/lib/break-glass.js +355 -147
- package/lib/vendor/blamejs/lib/cache.js +174 -105
- package/lib/vendor/blamejs/lib/chain-writer.js +38 -16
- package/lib/vendor/blamejs/lib/cli.js +19 -14
- package/lib/vendor/blamejs/lib/cluster-provider-db.js +130 -104
- package/lib/vendor/blamejs/lib/cluster-storage.js +119 -22
- package/lib/vendor/blamejs/lib/cluster.js +119 -71
- package/lib/vendor/blamejs/lib/codepoint-class.js +23 -0
- package/lib/vendor/blamejs/lib/compliance.js +206 -4
- package/lib/vendor/blamejs/lib/consent.js +82 -29
- package/lib/vendor/blamejs/lib/constants.js +27 -11
- package/lib/vendor/blamejs/lib/crypto-field.js +916 -156
- package/lib/vendor/blamejs/lib/db-declare-row-policy.js +35 -22
- package/lib/vendor/blamejs/lib/db-file-lifecycle.js +3 -2
- package/lib/vendor/blamejs/lib/db-query.js +882 -260
- package/lib/vendor/blamejs/lib/db-schema.js +228 -44
- package/lib/vendor/blamejs/lib/db.js +249 -99
- package/lib/vendor/blamejs/lib/dsr.js +385 -55
- package/lib/vendor/blamejs/lib/error-page.js +14 -1
- package/lib/vendor/blamejs/lib/external-db-migrate.js +239 -137
- package/lib/vendor/blamejs/lib/external-db.js +549 -34
- package/lib/vendor/blamejs/lib/file-upload.js +52 -7
- package/lib/vendor/blamejs/lib/framework-error.js +20 -1
- package/lib/vendor/blamejs/lib/framework-files.js +73 -0
- package/lib/vendor/blamejs/lib/framework-schema.js +695 -394
- package/lib/vendor/blamejs/lib/gate-contract.js +659 -1
- package/lib/vendor/blamejs/lib/guard-agent-registry.js +26 -44
- package/lib/vendor/blamejs/lib/guard-all.js +1 -0
- package/lib/vendor/blamejs/lib/guard-auth.js +42 -112
- package/lib/vendor/blamejs/lib/guard-cidr.js +33 -154
- package/lib/vendor/blamejs/lib/guard-csv.js +46 -113
- package/lib/vendor/blamejs/lib/guard-domain.js +34 -157
- package/lib/vendor/blamejs/lib/guard-dsn.js +27 -43
- package/lib/vendor/blamejs/lib/guard-email.js +47 -69
- package/lib/vendor/blamejs/lib/guard-envelope.js +19 -32
- package/lib/vendor/blamejs/lib/guard-event-bus-payload.js +24 -42
- package/lib/vendor/blamejs/lib/guard-event-bus-topic.js +25 -43
- package/lib/vendor/blamejs/lib/guard-filename.js +42 -106
- package/lib/vendor/blamejs/lib/guard-graphql.js +42 -123
- package/lib/vendor/blamejs/lib/guard-html.js +53 -108
- package/lib/vendor/blamejs/lib/guard-idempotency-key.js +24 -42
- package/lib/vendor/blamejs/lib/guard-image.js +46 -103
- package/lib/vendor/blamejs/lib/guard-imap-command.js +18 -32
- package/lib/vendor/blamejs/lib/guard-jmap.js +16 -30
- package/lib/vendor/blamejs/lib/guard-json.js +38 -108
- package/lib/vendor/blamejs/lib/guard-jsonpath.js +38 -171
- package/lib/vendor/blamejs/lib/guard-jwt.js +49 -179
- package/lib/vendor/blamejs/lib/guard-list-id.js +25 -41
- package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +27 -43
- package/lib/vendor/blamejs/lib/guard-mail-compose.js +24 -42
- package/lib/vendor/blamejs/lib/guard-mail-move.js +26 -44
- package/lib/vendor/blamejs/lib/guard-mail-query.js +28 -46
- package/lib/vendor/blamejs/lib/guard-mail-reply.js +24 -42
- package/lib/vendor/blamejs/lib/guard-mail-sieve.js +24 -42
- package/lib/vendor/blamejs/lib/guard-managesieve-command.js +17 -31
- package/lib/vendor/blamejs/lib/guard-markdown.js +37 -104
- package/lib/vendor/blamejs/lib/guard-message-id.js +26 -45
- package/lib/vendor/blamejs/lib/guard-mime.js +39 -151
- package/lib/vendor/blamejs/lib/guard-oauth.js +54 -135
- package/lib/vendor/blamejs/lib/guard-pdf.js +45 -101
- package/lib/vendor/blamejs/lib/guard-pop3-command.js +21 -31
- package/lib/vendor/blamejs/lib/guard-posture-chain.js +24 -42
- package/lib/vendor/blamejs/lib/guard-regex.js +33 -107
- package/lib/vendor/blamejs/lib/guard-saga-config.js +24 -42
- package/lib/vendor/blamejs/lib/guard-shell.js +42 -172
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +48 -54
- package/lib/vendor/blamejs/lib/guard-snapshot-envelope.js +24 -42
- package/lib/vendor/blamejs/lib/guard-sql.js +1491 -0
- package/lib/vendor/blamejs/lib/guard-stream-args.js +24 -43
- package/lib/vendor/blamejs/lib/guard-svg.js +47 -65
- package/lib/vendor/blamejs/lib/guard-template.js +35 -172
- package/lib/vendor/blamejs/lib/guard-tenant-id.js +26 -45
- package/lib/vendor/blamejs/lib/guard-time.js +32 -154
- package/lib/vendor/blamejs/lib/guard-trace-context.js +25 -44
- package/lib/vendor/blamejs/lib/guard-uuid.js +32 -153
- package/lib/vendor/blamejs/lib/guard-xml.js +38 -113
- package/lib/vendor/blamejs/lib/guard-yaml.js +51 -163
- package/lib/vendor/blamejs/lib/http-client.js +37 -9
- package/lib/vendor/blamejs/lib/inbox.js +120 -107
- package/lib/vendor/blamejs/lib/legal-hold.js +121 -50
- package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +47 -31
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +32 -18
- package/lib/vendor/blamejs/lib/mail-auth.js +236 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +2 -6
- package/lib/vendor/blamejs/lib/mail-dkim.js +1 -0
- package/lib/vendor/blamejs/lib/mail-greylist.js +2 -6
- package/lib/vendor/blamejs/lib/mail-helo.js +2 -6
- package/lib/vendor/blamejs/lib/mail-journal.js +85 -64
- package/lib/vendor/blamejs/lib/mail-rbl.js +2 -6
- package/lib/vendor/blamejs/lib/mail-scan.js +2 -6
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +117 -12
- package/lib/vendor/blamejs/lib/mail-server-mx.js +276 -7
- package/lib/vendor/blamejs/lib/mail-spam-score.js +2 -6
- package/lib/vendor/blamejs/lib/mail-store.js +293 -154
- package/lib/vendor/blamejs/lib/mail.js +8 -4
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +71 -25
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +19 -8
- package/lib/vendor/blamejs/lib/middleware/dpop.js +10 -1
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +17 -7
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +75 -51
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +102 -32
- package/lib/vendor/blamejs/lib/middleware/security-headers.js +21 -5
- package/lib/vendor/blamejs/lib/migrations.js +108 -66
- package/lib/vendor/blamejs/lib/network-heartbeat.js +7 -0
- package/lib/vendor/blamejs/lib/network-proxy.js +24 -1
- package/lib/vendor/blamejs/lib/nonce-store.js +31 -9
- package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +9 -4
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +57 -3
- package/lib/vendor/blamejs/lib/object-store/gcs.js +4 -1
- package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -2
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +38 -6
- package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +9 -1
- package/lib/vendor/blamejs/lib/observability.js +124 -0
- package/lib/vendor/blamejs/lib/otel-export.js +12 -3
- package/lib/vendor/blamejs/lib/outbox.js +184 -83
- package/lib/vendor/blamejs/lib/parsers/safe-xml.js +47 -7
- package/lib/vendor/blamejs/lib/pqc-agent.js +44 -0
- package/lib/vendor/blamejs/lib/pubsub-cluster.js +42 -20
- package/lib/vendor/blamejs/lib/queue-local.js +225 -140
- package/lib/vendor/blamejs/lib/queue-redis.js +9 -1
- package/lib/vendor/blamejs/lib/queue-sqs.js +6 -0
- package/lib/vendor/blamejs/lib/queue.js +7 -0
- package/lib/vendor/blamejs/lib/redact.js +68 -11
- package/lib/vendor/blamejs/lib/redis-client.js +160 -31
- package/lib/vendor/blamejs/lib/request-helpers.js +7 -0
- package/lib/vendor/blamejs/lib/retention.js +101 -40
- package/lib/vendor/blamejs/lib/router.js +212 -5
- package/lib/vendor/blamejs/lib/safe-dns.js +29 -45
- package/lib/vendor/blamejs/lib/safe-ical.js +18 -33
- package/lib/vendor/blamejs/lib/safe-icap.js +27 -43
- package/lib/vendor/blamejs/lib/safe-sieve.js +21 -40
- package/lib/vendor/blamejs/lib/safe-sql.js +212 -3
- package/lib/vendor/blamejs/lib/safe-url.js +170 -3
- package/lib/vendor/blamejs/lib/safe-vcard.js +18 -33
- package/lib/vendor/blamejs/lib/scheduler.js +35 -12
- package/lib/vendor/blamejs/lib/seeders.js +122 -74
- package/lib/vendor/blamejs/lib/session-stores.js +42 -14
- package/lib/vendor/blamejs/lib/session.js +175 -77
- package/lib/vendor/blamejs/lib/sql.js +3842 -0
- package/lib/vendor/blamejs/lib/sse.js +26 -0
- package/lib/vendor/blamejs/lib/ssrf-guard.js +151 -4
- package/lib/vendor/blamejs/lib/static.js +177 -34
- package/lib/vendor/blamejs/lib/subject.js +96 -49
- package/lib/vendor/blamejs/lib/vault/index.js +3 -2
- package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +3 -2
- package/lib/vendor/blamejs/lib/vault/rotate.js +168 -108
- package/lib/vendor/blamejs/lib/vault-aad.js +6 -0
- package/lib/vendor/blamejs/lib/vendor-data.js +2 -0
- package/lib/vendor/blamejs/lib/websocket.js +35 -5
- package/lib/vendor/blamejs/lib/worker-pool.js +11 -0
- package/lib/vendor/blamejs/package.json +2 -2
- package/lib/vendor/blamejs/release-notes/v0.14.x.json +1503 -0
- package/lib/vendor/blamejs/release-notes/v0.15.0.json +77 -0
- package/lib/vendor/blamejs/release-notes/v0.15.1.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.2.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.3.json +39 -0
- package/lib/vendor/blamejs/release-notes/v0.15.4.json +39 -0
- package/lib/vendor/blamejs/release-notes/v0.15.5.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.6.json +59 -0
- package/lib/vendor/blamejs/scripts/check-services.js +21 -0
- package/lib/vendor/blamejs/scripts/gen-migrating.js +51 -0
- package/lib/vendor/blamejs/scripts/release.js +398 -38
- package/lib/vendor/blamejs/test/00-primitives.js +117 -0
- package/lib/vendor/blamejs/test/10-state.js +140 -14
- package/lib/vendor/blamejs/test/20-db.js +65 -2
- package/lib/vendor/blamejs/test/helpers/db.js +9 -0
- package/lib/vendor/blamejs/test/helpers/drivers.js +27 -15
- package/lib/vendor/blamejs/test/helpers/services.js +21 -0
- package/lib/vendor/blamejs/test/integration/audit-actor-binding-pg.test.js +246 -0
- package/lib/vendor/blamejs/test/integration/audit-chain-external-db.test.js +517 -0
- package/lib/vendor/blamejs/test/integration/audit-stack-mysql.test.js +639 -0
- package/lib/vendor/blamejs/test/integration/audit-stack-postgres.test.js +832 -0
- package/lib/vendor/blamejs/test/integration/backup-restore-objectstore.test.js +453 -0
- package/lib/vendor/blamejs/test/integration/data-layer-cluster-mysql.test.js +649 -0
- package/lib/vendor/blamejs/test/integration/data-layer-cluster-pg.test.js +770 -0
- package/lib/vendor/blamejs/test/integration/data-layer-mysql-privacy.test.js +630 -0
- package/lib/vendor/blamejs/test/integration/data-layer-mysql.test.js +610 -0
- package/lib/vendor/blamejs/test/integration/data-layer-pg.test.js +577 -0
- package/lib/vendor/blamejs/test/integration/data-layer-postgres.test.js +771 -0
- package/lib/vendor/blamejs/test/integration/db-layer-mysql.test.js +549 -0
- package/lib/vendor/blamejs/test/integration/db-layer-postgres.test.js +598 -0
- package/lib/vendor/blamejs/test/integration/distributed-scheduler-fencing-pg.test.js +602 -0
- package/lib/vendor/blamejs/test/integration/external-db-postgres.test.js +576 -0
- package/lib/vendor/blamejs/test/integration/framework-schema-mysql.test.js +353 -0
- package/lib/vendor/blamejs/test/integration/log-stream-cloudwatch.test.js +224 -0
- package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +142 -17
- package/lib/vendor/blamejs/test/integration/network-heartbeat.test.js +25 -10
- package/lib/vendor/blamejs/test/integration/object-store-azure.test.js +101 -0
- package/lib/vendor/blamejs/test/integration/object-store-gcs.test.js +239 -0
- package/lib/vendor/blamejs/test/integration/object-store-sigv4.test.js +35 -16
- package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +291 -0
- package/lib/vendor/blamejs/test/integration/pubsub.test.js +14 -0
- package/lib/vendor/blamejs/test/integration/queue-sqs.test.js +322 -0
- package/lib/vendor/blamejs/test/integration/redis-reconnect-toxiproxy.test.js +300 -0
- package/lib/vendor/blamejs/test/integration/sql-fts5-catalog-sqlite.test.js +154 -0
- package/lib/vendor/blamejs/test/integration/tls-classical-downgrade-audit.test.js +71 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-event-bus.test.js +175 -12
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-exclusive-temp.test.js +216 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-checkpoint-false-rollback.test.js +203 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-query-self-log.test.js +126 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-safeemit-redacts-secrets.test.js +196 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-signing-key-rotation.test.js +197 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-verifybundle-tamper.test.js +209 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/azure-blob-key-encoding.test.js +121 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-residency-posture.test.js +168 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-scheduletest-drill.test.js +318 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +233 -7
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +1120 -14
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance.test.js +229 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +24 -7
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-dual-read-migrate.test.js +165 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-per-row-key.test.js +350 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-unseal-rate-cap.test.js +27 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-upgrade-dialect.test.js +76 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-interop-oracles.test.js +392 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csrf-protect.test.js +159 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +180 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-cross-schema.test.js +5 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-sealed-field-in.test.js +101 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +38 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-reconcile-emittable.test.js +127 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-stream-and-payload-shape.test.js +267 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-worm.test.js +150 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/defineguard-default-gate-posture-caps.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-replaystore-required.test.js +46 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +218 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/erase-posture-vacuum.test.js +210 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +4 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-migrate.test.js +48 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +237 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +20 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/file-upload-content-safety-skip-audit.test.js +193 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-csv.test.js +90 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +85 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/idempotency-key.test.js +10 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/inbox.test.js +15 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/legal-hold.test.js +146 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +189 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-journal.test.js +3 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-jmap.test.js +123 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +207 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-store.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +43 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otel-export.test.js +133 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +101 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/outbox-inflight-reaper.test.js +136 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/parsers-standalone.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/passkey-real-vectors.test.js +429 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pqc-agent-curve.test.js +21 -11
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +40 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redact-dlp.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +113 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retention-dryrun-no-vacuum.test.js +99 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router-use-path-scope.test.js +255 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-canonicalize.test.js +309 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-xml.test.js +143 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js +287 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc-ecdsa-p1363.test.js +79 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +50 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +31 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +45 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +49 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +595 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sse-backpressure.test.js +91 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ssrf-guard.test.js +69 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +194 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/websocket-extension-header.test.js +88 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/worker-pool-recycle-race.test.js +66 -0
- package/lib/vendor/blamejs/test/layer-1-state/api-key.test.js +84 -0
- package/lib/vendor/blamejs/test/layer-5-integration/external-db-residency.test.js +638 -0
- package/lib/vendor/blamejs/test/layer-5-integration/guard-host-integration.test.js +21 -0
- package/lib/vendor/blamejs/test/smoke.js +79 -21
- package/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.0.json +0 -43
- package/lib/vendor/blamejs/release-notes/v0.14.1.json +0 -60
- package/lib/vendor/blamejs/release-notes/v0.14.10.json +0 -54
- package/lib/vendor/blamejs/release-notes/v0.14.11.json +0 -72
- package/lib/vendor/blamejs/release-notes/v0.14.12.json +0 -95
- package/lib/vendor/blamejs/release-notes/v0.14.13.json +0 -52
- package/lib/vendor/blamejs/release-notes/v0.14.14.json +0 -31
- package/lib/vendor/blamejs/release-notes/v0.14.16.json +0 -45
- package/lib/vendor/blamejs/release-notes/v0.14.17.json +0 -57
- package/lib/vendor/blamejs/release-notes/v0.14.18.json +0 -127
- package/lib/vendor/blamejs/release-notes/v0.14.19.json +0 -61
- package/lib/vendor/blamejs/release-notes/v0.14.2.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.20.json +0 -73
- package/lib/vendor/blamejs/release-notes/v0.14.21.json +0 -98
- package/lib/vendor/blamejs/release-notes/v0.14.22.json +0 -91
- package/lib/vendor/blamejs/release-notes/v0.14.3.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.4.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.5.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.6.json +0 -60
- package/lib/vendor/blamejs/release-notes/v0.14.7.json +0 -77
- package/lib/vendor/blamejs/release-notes/v0.14.8.json +0 -27
- package/lib/vendor/blamejs/release-notes/v0.14.9.json +0 -40
|
@@ -0,0 +1,1503 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-consolidated-schema.json",
|
|
3
|
+
"minor": "0.14",
|
|
4
|
+
"releases": [
|
|
5
|
+
{
|
|
6
|
+
"version": "0.14.27",
|
|
7
|
+
"date": "2026-06-06",
|
|
8
|
+
"headline": "Security-hardening sweep: exclusive temp creation, request path confinement, prototype-safe parsing, cross-tenant authentication, telemetry and error redaction, and posture enforcement floors",
|
|
9
|
+
"summary": "A broad security-hardening release closing classes of bug across the request, storage, identity, and data-governance surfaces. Files that stage data before an atomic rename (the atomic-write substrate, the HTTP client's downloads, the static file server's cache) now create those files exclusively and refuse to follow a symlink, so a same-user pre-planted file or symlink can no longer be truncated or written through. The static file server re-confines every request-derived path to its served root at the filesystem call, and its content-safety read is anchored to a single no-follow descriptor. The request body parser, the WebSocket extension parser, and the cookie parser build their maps from key/value pairs instead of attacker-named computed writes, so a field named after an Object prototype member can neither pollute the prototype nor surface inherited members. The SSRF guard compares cloud-metadata addresses canonically; OIDC federation derives a subordinate's policy and keys from the superior-signed statement rather than self-published config; the JMAP listener rejects a cross-tenant account id with accountNotFound before dispatch; the agent event bus authenticates each envelope's tenant and posture with a vault-keyed MAC; and the audit query no longer under-logs concurrent reads. Telemetry attributes exported over OTLP and the 5xx error record written to the signed audit chain are now redacted, closing two egress paths that previously shipped secrets in plaintext. Regulated compliance postures enforce a minimum seal-envelope strength at table registration, warn when a pinned posture has no content-safety overlay or no outbound DLP wired, and expose region-tag normalization helpers. The local queue's Redis backend no longer wedges a caller on a failed connect or double-schedules reconnects, file uploads audit every content-safety skip, the Azure blob backend percent-encodes object keys, the XML parser rejects prototype-poisoning names, and router path-scoped middleware (`use(path, mw)`) works as documented instead of dropping the gate.",
|
|
10
|
+
"sections": [
|
|
11
|
+
{
|
|
12
|
+
"heading": "Security",
|
|
13
|
+
"items": [
|
|
14
|
+
{
|
|
15
|
+
"title": "Staged writes are created exclusively and refuse symlinks",
|
|
16
|
+
"body": "The atomic-write substrate (`b.atomicFile`), the HTTP client's download staging, and the static file server's pre-serve read now open files with `O_CREAT | O_EXCL | O_NOFOLLOW` at owner-only `0o600` (the vault-rotation pipeline adopted the same in v0.14.26). A pre-planted file or a symlink at a staging path is now a hard failure rather than a truncated or followed write (CWE-377 / CWE-379 / CWE-59). The temp+rename atomicity and download streaming semantics are unchanged."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"title": "Static file server re-confines request paths and reads through one no-follow descriptor",
|
|
20
|
+
"body": "Every request-derived path is re-resolved under the served root and refused if it escapes (`startsWith(root + sep)`) at the filesystem call, not only upstream, and the content-safety gate reads size and bytes from a single `O_NOFOLLOW` descriptor so a final-component symlink swap between stat and read cannot redirect it (CWE-22 / CWE-367). Percent-encoded traversal, NUL bytes, and absolute/drive-letter paths are refused before any filesystem access."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"title": "Body, WebSocket, and cookie parsers build maps without attacker-named writes",
|
|
24
|
+
"body": "The request body parser (Content-Type/Content-Disposition params, multipart headers and fields), the WebSocket `Sec-WebSocket-Extensions` parser, and the CSRF cookie parser now collect key/value pairs and materialize them via `Object.fromEntries` onto a null-prototype accumulator with prototype-poisoning keys dropped, instead of a request-keyed computed write. A field or parameter named `__proto__` / `constructor` / `prototype` can no longer pollute the prototype or surface inherited members (CWE-915 / CWE-1321). Parsed object shapes are unchanged for all legitimate input."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"title": "Prototype-safe XML parsing",
|
|
28
|
+
"body": "`b.safeXml` builds its element, attribute, and grouping accumulators with `Object.create(null)` and rejects element/attribute names `__proto__` / `constructor` / `prototype` with `xml/forbidden-name` (CWE-1321), matching the TOML/YAML/INI parsers. The returned tree has a null prototype; consumers using bracket/dot access, `Object.keys`, or `JSON.stringify` are unaffected."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"title": "SSRF guard compares cloud-metadata addresses canonically",
|
|
32
|
+
"body": "The SSRF guard now canonicalizes addresses before comparing against the cloud metadata endpoints, so a non-canonical encoding of a link-local / metadata address can no longer slip past the block to reach an instance metadata service (CWE-918)."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"title": "OIDC federation trusts the superior-signed statement, not self-config",
|
|
36
|
+
"body": "A subordinate entity's `metadata_policy` is now read from the superior's signed entity statement, and trust-chain building verifies each subordinate against attested keys in a two-phase walk rather than the subordinate's self-published JWKS, closing a trust-substitution gap (RFC 9068 / OpenID Federation). A subordinate that publishes no attested keys is refused."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"title": "Cross-tenant authentication on the JMAP listener and agent event bus",
|
|
40
|
+
"body": "The JMAP listener rejects a client-supplied `accountId` outside the actor's permitted set with `accountNotFound` before any method or blob handler runs (RFC 8620 §3.6.1). The agent event bus authenticates each envelope's tenant id and posture with a vault-keyed HMAC and drops a forged or tampered envelope before routing. The audit query no longer suppresses its own `audit.read` self-log under concurrency, so concurrent reads are each recorded (PCI-DSS 10.2)."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"title": "Telemetry and error records are redacted before egress",
|
|
44
|
+
"body": "Span and metric attributes exported over OTLP now pass through a redactor (`b.observability.setRedactor`, defaulting to `b.redact`) before leaving the process, and the 5xx error record written to the durable, signed audit chain now uses the redacting `audit.safeEmit` rather than the raw `audit.emit` — closing two egress paths that previously shipped a secret embedded in a span attribute or an exception message/stack in plaintext (CWE-532). The error response itself is byte-identical."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"title": "Regulated postures enforce a seal-envelope floor",
|
|
48
|
+
"body": "Compliance postures now carry a `sealEnvelopeFloor`; `hipaa` and `pci-dss` require at least AEAD additional-data binding. Registering a table that seals columns under a weaker envelope while such a posture is pinned is refused at configuration time, so a regulated deployment can no longer register a copy-paste-vulnerable plain-sealed table (CWE-311 / CWE-326; 45 CFR 164.312, PCI-DSS v4 Req. 3.5)."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"title": "Redis backend connect/reconnect robustness",
|
|
52
|
+
"body": "The local queue's Redis backend always settles its connect promise (resolve on ready, reject on error/timeout) so a caller can no longer wedge on a failed connect, schedules at most one reconnect per failure (a lost socket fires both error and close), and gives up idempotently — fixing a caller hang and a reconnect storm on a flapping backend (CWE-833 / CWE-400)."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"title": "File-upload skip auditing and Azure object-key encoding",
|
|
56
|
+
"body": "Every content-safety skip path in `b.fileUpload` now emits a `content_safety_skipped` audit naming why the scan was bypassed (opt-out / no gate for the extension / over the reassembly cap), so an unscanned upload is visible in the audit log (CWE-778). The Azure blob backend percent-encodes each object-key path segment before URL interpolation, so a key containing `?` / `#` / spaces can no longer corrupt the request or escape its container path (CWE-20)."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"title": "Router path-scoped middleware works as documented",
|
|
60
|
+
"body": "`router.use(pathPrefix, middleware)` — documented across the security middleware but previously unimplemented (the path string was pushed as the middleware, 500-ing every request, or the gate silently never ran) — now mounts the middleware scoped to a segment-boundary path prefix, preserving registration order and refusing a non-string prefix or non-function middleware at configuration time (CWE-670)."
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"heading": "Detectors",
|
|
66
|
+
"items": [
|
|
67
|
+
{
|
|
68
|
+
"title": "Recurrence guards for the fixed shapes",
|
|
69
|
+
"body": "The pattern catalog gains detectors for the exclusive-temp-create (atomic-file, http-client, static), request-path confinement (static serve + gate), request-keyed map writes (body-parser, WebSocket), prototype-safe XML accumulators, Azure object-key encoding, file-upload skip auditing, OTLP attribute redaction, JMAP account gating, the seal-envelope floor, and the router path-scoped mount. The data-flow and timing shapes (SSRF canonicalization, federation trust-chain, audit self-log concurrency, event-bus envelope MAC, Redis reconnect scheduling, error-record redaction, posture-overlay warnings) are guarded by request-driven tests where a precise regex would false-positive."
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"heading": "Migration",
|
|
75
|
+
"items": [
|
|
76
|
+
{
|
|
77
|
+
"title": "Behavior changes to review",
|
|
78
|
+
"body": "Several hardening changes alter behavior, all fail-closed: (1) the agent event bus now requires a shared vault to authenticate envelopes and refuses to publish/deliver without one — single-process callers without a vault must pass `requireMac: false`; (2) OIDC federation subordinates must publish attested keys (a subordinate relying on self-published JWKS is refused); (3) `b.safeXml` throws `xml/forbidden-name` on element/attribute names `__proto__`/`constructor`/`prototype`; (4) registering a table that seals columns below the pinned regulated posture's seal-envelope floor throws at configuration time — add `{ aad: true }` or a per-row key; (5) OTLP telemetry attributes are now redacted by default — install a domain redactor via `b.observability.setRedactor` if you need different handling (it cannot be disabled outright); (6) `router.use(path, mw)` now path-scopes instead of applying globally or 500-ing. New advisory audit rows appear when a pinned posture has no content-safety overlay or no outbound DLP wired; `b.compliance.set` does not auto-install outbound DLP (call `b.redact.installForPosture` with your client/mail/webhook handles)."
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"version": "0.14.26",
|
|
86
|
+
"date": "2026-06-06",
|
|
87
|
+
"headline": "Break-glass IP and session pins fail closed, DSR ticket PII is sealed and erasable, and queue jobs are sealed at rest from the first write",
|
|
88
|
+
"summary": "Break-glass grant pins (`pinIp` / `sessionPin`, documented default-ON) were enforced only when the grant had captured a binding at mint time; a grant minted without an IP or session was redeemable from anywhere, so the pin failed open exactly when it mattered. Pins now fail closed: a grant carrying no binding is refused at redemption, and the redeeming client IP falls back to `req.ip` when not threaded explicitly. The Data Subject Request ticket store kept the subject's identifiers and the raw request body in plaintext, so an erasure request could not destroy the very PII it was processing; those columns are now sealed under the vault with `(table, rowId)` additional-data binding, erasure purges the ticket on completion, and an in-place schema upgrade seals existing stores. Queue jobs were sealed at rest only after a table had been registered, which did not happen until the first explicit registration — the queue now self-registers its job table on `init`, so jobs are sealed from the first write. Rounding out the bundle: the OAuth back-channel-logout `logout_token` is parsed under a byte ceiling, the SD-JWT-VC holder key-binding JWT signs with an algorithm asserted against the holder key's type (an RSA or OKP holder no longer mints a self-invalid token), and a pushed authorization request carrying a signed request object emits `authorization_details` as a native array.",
|
|
89
|
+
"sections": [
|
|
90
|
+
{
|
|
91
|
+
"heading": "Security",
|
|
92
|
+
"items": [
|
|
93
|
+
{
|
|
94
|
+
"title": "Break-glass IP and session pins fail closed",
|
|
95
|
+
"body": "`b.breakGlass` grants document `pinIp` and `sessionPin` as default-ON, and grant minting captures the issuing IP and session at that time. Redemption now refuses a grant whose binding was never captured (`pinIp` on but no IP recorded, or `sessionPin` on but no session) instead of treating the absent binding as 'nothing to check' and allowing the redemption from any origin. The redeeming client IP is resolved from the redemption request and falls back to `req.ip` when the caller does not thread an explicit address. The one-time-code replay floor is keyed per `(actor, secret)` so a code consumed under one actor cannot be replayed under another, and the accepted TOTP step is reserved atomically as part of acceptance, so two concurrent grants presenting the same in-window code cannot both pass."
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"title": "DSR ticket store seals subject identifiers and request payload, and erasure purges the ticket",
|
|
99
|
+
"body": "`b.dsr` with the database-backed ticket store now seals the data subject's identifiers and the raw request payload via `b.cryptoField.registerTable` with `(table, rowId)` additional-data binding, so the PII a request processes is encrypted at rest and is destroyed when the ticket's row key is shredded. An erasure request purges its own ticket on completion rather than leaving the subject's identifiers behind. Existing ticket stores are upgraded in place on the next `init` — sealed columns are added via `ALTER TABLE ADD COLUMN` and the legacy `payload NOT NULL` constraint relaxes to `DEFAULT ''` so the add succeeds without data loss. Wrapped ticket keys are re-sealed under the new root on vault keypair rotation (`b.dsr.reseal`), so rotation does not strand tickets. Rows written before the upgrade (plaintext subject, no lookup hash) are backfilled on the next `init` — their hashes are computed and their subject columns sealed — so a subject lookup still finds them and an erasure request can purge them."
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"title": "Queue jobs are sealed at rest from the first write",
|
|
103
|
+
"body": "The local queue backend self-registers its job table for sealing on `init` rather than on first explicit registration, so a job persisted before any other code touched the seal table is encrypted at rest instead of written in plaintext. `b.cryptoField.sealRow` is a silent no-op against an unregistered table; the self-register on `init` closes that fail-open window for the queue's own rows."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"title": "OAuth back-channel logout is bounded; SD-JWT-VC holder binding matches the holder key type",
|
|
107
|
+
"body": "The OAuth back-channel-logout endpoint parses the `logout_token` under an explicit byte ceiling, so an unauthenticated caller cannot exhaust memory with an oversized body. The SD-JWT-VC holder key-binding JWT now signs with an algorithm asserted against the holder key's type, so a holder presenting an RSA or OKP key no longer mints a key-binding JWT that fails its own verification. A pushed authorization request (PAR) that carries a signed request object emits `authorization_details` as a native JSON array per RFC 9396, not a JSON-encoded string."
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"title": "Vault keypair rotation writes its staging files with exclusive, no-follow create",
|
|
111
|
+
"body": "Every file the rotation pipeline writes into its staging directory — the re-encrypted database, the resealed vault and database keys, additional sealed files, the derived-hash material, and the transient plaintext database — is now created with exclusive, symlink-refusing semantics (`O_CREAT | O_EXCL | O_NOFOLLOW`, owner-only `0o600`), and the fsync-by-path step refuses to follow a symlink. A same-user pre-planted file or symlink swap in the staging directory is now a hard failure rather than a followed write, closing a local tamper window during rotation (CWE-377 / CWE-379 / CWE-59) on top of the directory's existing `0o700` owner-only permissions."
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"heading": "Detectors",
|
|
117
|
+
"items": [
|
|
118
|
+
{
|
|
119
|
+
"title": "break-glass-pin-fails-open-on-null-binding",
|
|
120
|
+
"body": "The pattern catalog refuses a break-glass pin comparison guarded by a `grantRow.ip != null &&` (or `sessionId`) short-circuit — a guard that skips enforcement when the captured binding is absent, which is precisely the fail-open. Pin enforcement must refuse a grant with no captured binding, not wave it through."
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"title": "dsr-ticket-store-pii-must-be-sealed",
|
|
124
|
+
"body": "The catalog requires the DSR database ticket store to register its seal table, so the subject identifiers and request payload it holds cannot regress to plaintext-at-rest and remain un-erasable. The queue self-register, the bounded logout parse, and the holder key-type parity are covered by their expanded request-driven tests and the existing fixed-classical-algorithm-default guard."
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"heading": "Migration",
|
|
130
|
+
"items": [
|
|
131
|
+
{
|
|
132
|
+
"title": "Break-glass grants now require a resolvable binding to redeem under default pins",
|
|
133
|
+
"body": "With `pinIp` left at its default (on), a grant is now refused at redemption unless the issuing IP was captured and the redeeming client IP can be resolved. In-tree redemption paths thread the request and resolve the IP automatically. If your redemption path does not surface a client address and you do not intend to bind to IP, set `pinIp: false` (and `sessionPin: false`) explicitly on the grant; the previous behavior silently accepted such grants from any origin."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"title": "DSR ticket stores are sealed in place on next init",
|
|
137
|
+
"body": "An existing database-backed DSR ticket store gains sealed columns via `ALTER TABLE ADD COLUMN` on the next `init`, and the legacy `payload NOT NULL` constraint relaxes to `DEFAULT ''` to permit the in-place add. No data is lost; tickets written before the upgrade remain readable and become sealed as they are rewritten."
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"version": "0.14.25",
|
|
145
|
+
"date": "2026-06-06",
|
|
146
|
+
"headline": "Per-row crypto-shred is real and AAD-bound, and the idempotency fingerprint key is sealed at rest",
|
|
147
|
+
"summary": "The per-row encryption-key feature that backs `b.subject.eraseHard` crypto-shred was both non-functional and, as documented, unsound: the per-row key (`K_row`) was derived deterministically from a salt that is stored in plaintext in the data directory, so an actor with disk access could re-derive it and decrypt 'shredded' residual ciphertext (WAL / replica / backup) — and the key was never actually materialized on insert, so no table ever used it. Per-row keys are now derived from a fresh 32-byte CSPRNG secret stored only in `_blamejs_per_row_keys`, wrapped under the vault with AEAD additional-data binding `(table, rowId)`, and materialized on insert for tables that declare them; destroying the wrapped secret now genuinely renders the row's residual ciphertext undecryptable. The same plaintext-salt class is fixed for the idempotency request-fingerprint HMAC, which now seeds off the sealed-at-rest `vault.getDerivedHashMacKey()`. There is no migration: the per-row-key table was empty in every deployment, so keyed tables are correct from their first write.",
|
|
148
|
+
"sections": [
|
|
149
|
+
{
|
|
150
|
+
"heading": "Security",
|
|
151
|
+
"items": [
|
|
152
|
+
{
|
|
153
|
+
"title": "Per-row crypto-shred: random secret, AAD-bound wrap, materialized on insert",
|
|
154
|
+
"body": "`b.cryptoField.declarePerRowKey` tables now get a per-row key derived from a fresh `b.crypto.generateBytes(32)` secret — never from the plaintext per-deployment salt — so the key has no input recomputable from disk. The secret is stored in `_blamejs_per_row_keys` wrapped via `b.vault.aad.seal` with additional-data bound to `(table, rowId)`, so a wrapped key copied onto another row fails its Poly1305 tag. Sealed columns are encrypted under the per-row key and tagged with a `vault.row:` envelope (`b.cryptoField.isRowSealed`); the residency-tag column is never key-sealed so the write-boundary residency gate can still read it. The key is materialized on insert (it previously never was) and re-derived once per read; `b.subject.eraseHard` and the retention sweep destroy the wrapped secret, after which the row's residual ciphertext reads as absent and cannot be recovered even if the vault root is later compromised. Wrapped keys are re-sealed under the new root on vault keypair rotation, so rotation does not strand keyed rows."
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"title": "Idempotency request-fingerprint HMAC seeds off the sealed MAC key",
|
|
158
|
+
"body": "The `fingerprintSeal` HMAC over the request fingerprint was keyed from the plaintext per-deployment salt, so a disk-access actor could recompute the key and brute-force the low-entropy preimage (method + URL + body) offline. It now seeds off `b.vault.getDerivedHashMacKey()`, which is sealed at rest. Fingerprints cached before the upgrade no longer match, so a replayed request re-executes once (the safe direction)."
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"heading": "Detectors",
|
|
164
|
+
"items": [
|
|
165
|
+
{
|
|
166
|
+
"title": "kdf-key-from-plaintext-derived-hash-salt",
|
|
167
|
+
"body": "The pattern catalog refuses a key-derivation (`kdf(... getDerivedHashSalt() ...)`) that seeds a per-row shred key or a vault-secret-promising keyed-MAC off the plaintext-on-disk derived-hash salt. Such a key is re-derivable by anyone with the data directory, defeating the shred / secrecy it advertises; the correct sources are a CSPRNG secret or the sealed `getDerivedHashMacKey()`. The deterministic salted-SHA3 equality index is a distinct shape and is unaffected."
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"heading": "Migration",
|
|
173
|
+
"items": [
|
|
174
|
+
{
|
|
175
|
+
"title": "No action required",
|
|
176
|
+
"body": "No data migration: the per-row-key table was empty in every deployment, so tables that declare per-row keys are correct from their first write going forward. The only observable change is that the first request after upgrade matching a pre-upgrade idempotency fingerprint re-executes once."
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"version": "0.14.24",
|
|
184
|
+
"date": "2026-06-05",
|
|
185
|
+
"headline": "Per-row data residency enforced at the write boundary, and the long-advertised column-residency gate is wired",
|
|
186
|
+
"summary": "Rows can now carry their own residency tag, and the database write paths enforce it: under a cross-border regulated posture (GDPR / UK-GDPR / DPDP / PIPL / LGPD / APPI / PDPA) a row tagged for one region is refused before it lands on a backend in another. `b.cryptoField.declarePerRowResidency` declares which column carries the tag; local SQLite writes check it against the deployment's `dataResidency` region set, and `b.externalDb.query` / `transaction` DML to a residency-tagged backend takes the tag per call via `rowResidencyTag`. The column-scoped gate (`assertColumnResidency`) — exported and documented since v0.7.27 but never composed into any write path — is now enforced on the same boundary. Unregulated deployments are unaffected: every gate passes with an advisory audit instead of a refusal, so operators can observe before adopting a posture. Note: v0.14.23 was tagged but not published to npm (its publish run timed out in validation); its changes — the MX DATA-phase SPF/DKIM/DMARC gate and the inbound mail authentication pipeline — are included in this package, and the publish-validation timeout is fixed here.",
|
|
187
|
+
"sections": [
|
|
188
|
+
{
|
|
189
|
+
"heading": "Added",
|
|
190
|
+
"items": [
|
|
191
|
+
{
|
|
192
|
+
"title": "`b.cryptoField.declarePerRowResidency` / `getPerRowResidency` — row-scoped residency tags",
|
|
193
|
+
"body": "Declares the plaintext column carrying each row's residency tag plus the whitelist of valid tag values (`{ residencyColumn, allowedTags }`). On INSERT to a declared table the tag is required and must be in `allowedTags`; rows tagged `global` or `unrestricted` pass any backend. The declaration registry mirrors `declareColumnResidency`, and `clearResidencyForTest` now clears both. The tag comes from application logic (the user's declared region) — the framework never infers it from request metadata."
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"title": "Local write gate on `b.db.from(...).insertOne` / `update`",
|
|
197
|
+
"body": "Runs on the plaintext row before sealing, so the tag column stays inspectable when sibling columns seal. Under a cross-border regulated posture, a row whose tag falls outside the deployment's region set (`dataResidency.region` plus `allowedStorageRegions`) is refused with `db-query/row-residency-local-mismatch`; a missing or out-of-whitelist tag refuses with `db-query/row-residency-tag-missing` / `-tag-invalid` regardless of posture. UPDATEs gate when the change set touches the residency column — an update that doesn't move residency is not a transfer. Refusals are typed `DbQueryError`s (new error class) and land in the audit chain (`db.residency.gate.rejected`); unregulated postures emit `db.residency.gate.advisory` and pass."
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"title": "External write gate — `rowResidencyTag` on `b.externalDb.query` / `transaction`",
|
|
201
|
+
"body": "External-db takes raw SQL, not row objects, so the row's tag travels as `opts.rowResidencyTag` (validated at transaction entry too). Under a regulated posture, a write to a residency-tagged backend requires the tag (`RESIDENCY_GATE_REQUIRED`) and refuses a mismatch (`RESIDENCY_TAG_MISMATCH`) before the statement reaches the wire. The gate classifies by what a statement does, not its leading keyword: it resolves the effective verb behind a `WITH` (CTE) or `EXPLAIN ANALYZE` prefix and treats `INSERT`/`UPDATE`/`DELETE`/`MERGE`/`REPLACE`, `CALL`/`EXECUTE`/`DO`, and `COPY ... FROM` as writes — only recognized pure reads (`SELECT`, `SHOW`/`DESCRIBE`/`PRAGMA`, a `COPY ... TO` export, plan-only `EXPLAIN`, and session/transaction statements) pass untagged. A statement whose class can't be resolved, or a multi-statement string that hides a trailing write behind a harmless prefix, is refused (`STATEMENT_UNRESOLVED_REFUSED` / `MULTI_STATEMENT_REFUSED`). Inside `transaction()`, the transaction-level tag applies to every statement and a per-call override on `tx.query(sql, params, opts)` wins for that statement; a refusal rolls the transaction back. Replica reads that carry a tag refuse routing to an incompatible replica (`REPLICA_RESIDENCY_INCOMPATIBLE`) unless the replica is configured `allowCrossBorder: true`, which is audited (`db.residency.replica.cross_border` at read time, `db.residency.replica.cross_border_allowed` at config time). Unrestricted backends are not gated, and the migration runner's own tracking writes are region-neutral so migrations run unaffected on a residency-tagged backend."
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"title": "`b.compliance.isCrossBorderRegulated` — shared posture vocabulary",
|
|
205
|
+
"body": "The cross-border regulated posture set (gdpr, uk-gdpr, dpdp, pipl-cn, lgpd-br, appi-jp, pdpa-sg) now lives on `b.compliance` (`CROSS_BORDER_REGULATED_POSTURES` + the membership helper), one source of truth shared by the local gate, the external gate, and the existing replica-topology boot check."
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"heading": "Fixed",
|
|
211
|
+
"items": [
|
|
212
|
+
{
|
|
213
|
+
"title": "`assertColumnResidency` is now actually enforced",
|
|
214
|
+
"body": "`b.cryptoField.declareColumnResidency` / `assertColumnResidency` shipped in v0.7.27 documenting a write-time gate, but no write path ever called the assertion — column tags were recorded and never checked. The local write paths now run it against the deployment region: a mismatch refuses with `db-query/column-residency-mismatch` under a regulated posture and emits an advisory otherwise. Operators tag columns with the region value their `dataResidency` declares."
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"title": "Cross-border-allowed replica audit event is now recorded",
|
|
218
|
+
"body": "The config-time audit event for a consciously-permitted cross-border replica used a malformed action name that the audit validator silently dropped, so a compliance reviewer saw no record of the `allowCrossBorder` decision in the audit chain. It now emits as `db.residency.replica.cross_border_allowed` and lands like every other audit row."
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"title": "Publish-validation timeout that blocked the v0.14.23 npm release",
|
|
222
|
+
"body": "The v0.14.23 publish run timed out in release validation — the pattern-catalog self-test crossed a per-file watchdog budget as the codebase grew, and the same self-test, which scans the whole library and runs far longer on the slower macOS CI runner, hit the same wall on this package's first CI run. The validation budgets are corrected so a genuinely stuck file still fails fast while the catalog completes. v0.14.23 exists as a signed tag; npm goes 0.14.22 → 0.14.24 carrying both releases' changes."
|
|
223
|
+
}
|
|
224
|
+
]
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"heading": "Detectors",
|
|
228
|
+
"items": [
|
|
229
|
+
{
|
|
230
|
+
"title": "db-query-write-without-residency-gate",
|
|
231
|
+
"body": "Every local write method that seals a row must run the residency gates on the plaintext first — a future write path (upsert, bulk) inherits the requirement automatically."
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"title": "Residency-gates-wired catalog check",
|
|
235
|
+
"body": "The pattern catalog now pins the wiring itself: the local write methods call the gate, the external query/transaction paths call theirs, and `assertColumnResidency` has a real caller — the declared-but-never-enforced class cannot silently reappear."
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"heading": "Migration",
|
|
241
|
+
"items": [
|
|
242
|
+
{
|
|
243
|
+
"title": "No action required unless you adopt the gates",
|
|
244
|
+
"body": "Tables without a residency declaration, deployments without a `dataResidency` region, and unregulated postures behave exactly as before (advisory audit events at most). Adopting: declare per-row residency on mixed-region tables, pass `rowResidencyTag` on external DML, and set a cross-border posture (`b.compliance.set(\"gdpr\")`) to turn refusals on."
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"version": "0.14.23",
|
|
252
|
+
"date": "2026-06-05",
|
|
253
|
+
"headline": "Inbound mail authentication: a DATA-phase SPF/DKIM/DMARC gate on the MX listener and the one-call receiver pipeline",
|
|
254
|
+
"summary": "The MX listener can now refuse policy-failing mail at the wire instead of asking operators to verify after delivery. `b.mail.inbound.verify` is the receiver pipeline — SPF on the envelope identity, DKIM on the message bytes, DMARC policy + alignment on the From-header domain, and the RFC 8601 Authentication-Results header — and the listener's new `guardEnvelope` opt runs it at DATA completion: when the sender's published DMARC policy says reject, the message is refused with 550 before it reaches the agent handoff; accepted mail carries the verdict to the agent and gains the receiver's Authentication-Results header, with any sender-forged header carrying the receiver's name stripped first. Monitor mode annotates without refusing, so operators can observe verdicts on live traffic before enforcing.",
|
|
255
|
+
"sections": [
|
|
256
|
+
{
|
|
257
|
+
"heading": "Added",
|
|
258
|
+
"items": [
|
|
259
|
+
{
|
|
260
|
+
"title": "`b.mail.inbound.verify` — one-call receiver authentication pipeline",
|
|
261
|
+
"body": "Runs the inbound authentication set on a full RFC 5322 message (string or Buffer): SPF (RFC 7208) on the MAIL FROM identity with HELO fallback for the null reverse-path, DKIM (RFC 6376) on every signature, From-header extraction, DMARC (RFC 7489 / DMARCbis) policy discovery + alignment, and — when an `authservId` is supplied — the RFC 8601 Authentication-Results header. Returns `{ spf, dkim, from, dmarc, authResults }` with `dmarc.recommendedAction` carrying the policy disposition. From-header discipline per RFC 7489 §6.6.1: a message with zero From fields, several From fields, or several author addresses in one field returns `permerror` with a reject recommendation instead of picking one of the authors — the header-duplication shape behind the CVE-2024-7208 / CVE-2024-7209 hosted-relay spoofing class. The author parser is quote-aware: a literal `<` or comma inside a quoted display-name (`\"Doe, John\" <j@example.com>`) is one author, not two. A fail verdict computed while SPF or DKIM returned temperror surfaces as temperror (RFC 7489 §6.6.2) — the transiently-failed lookup could have produced the aligned pass, so the caller defers and the sender retries instead of being permanently refused during a DNS blip."
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"title": "MX listener `guardEnvelope` — the DATA-phase authentication gate",
|
|
265
|
+
"body": "`b.mail.server.mx.create({ guardEnvelope: true })` (or a config object: `mode`, `onTemperror`, `authservId`, `dnsLookup`, `maxSignatures`, `clockSkewMs`, `minRsaBits`, `timeoutMs`) runs the pipeline after the SIZE check and before the agent handoff. Enforce mode refuses with 550 5.7.26 (RFC 7372 — multiple authentication checks failed) when DMARC evaluates to fail under a reject policy, 550 5.7.1 on the multi-From shape, and 451 4.7.0 on DNS temperror or pipeline timeout — `onTemperror: \"accept\"` admits unauthenticated mail instead when availability wins. The whole pipeline runs under a wall-clock ceiling (`timeoutMs`, default 20s) so a message stuffed with signatures pointing at slow resolvers cannot pin the connection slot. Accepted messages reach the agent handoff with the verdict as `auth` (including `quarantine: true` when the sender's policy says quarantine — the agent owns foldering) and gain the receiver's Authentication-Results header; any sender-attached header forging the receiver's authserv-id is stripped first (RFC 8601 §5). Monitor mode (the default under the permissive profile) annotates and audits without refusing. New audit events: `mail.server.mx.envelope_verdict` and `mail.server.mx.envelope_error`."
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"heading": "Detectors",
|
|
271
|
+
"items": [
|
|
272
|
+
{
|
|
273
|
+
"title": "ar-header-prepend-without-forged-strip",
|
|
274
|
+
"body": "Any code path that prepends an emitted Authentication-Results header must first strip sender-attached instances claiming the same authserv-id (RFC 8601 §5) — a forged pre-attached verdict would otherwise shadow the computed one for downstream consumers."
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"heading": "Migration",
|
|
280
|
+
"items": [
|
|
281
|
+
{
|
|
282
|
+
"title": "No action required; the gate is opt-in",
|
|
283
|
+
"body": "`guardEnvelope` is off unless wired — an unwired gate is skipped like the sibling HELO / RBL / greylist gates, and existing listeners behave exactly as before. The agent handoff context gains an `auth` field (null when the gate is off). Start with `guardEnvelope: { mode: \"monitor\" }` to observe verdicts on live traffic before switching to enforce."
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
]
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"version": "0.14.22",
|
|
291
|
+
"date": "2026-06-04",
|
|
292
|
+
"headline": "RFC 9101 signed request objects: a JAR request-object builder, a classical JWS signer for external interop, and pushed authorization requests that carry `request=`",
|
|
293
|
+
"summary": "The framework can now mint JWT-Secured Authorization Requests, completing the JAR surface whose verify side (`b.auth.jar.parse`) shipped in v0.12.31 with the builder documented as waiting on a classical signer. `b.auth.jws.sign` is that signer — a compact-JWS producer for RS / PS / ES / EdDSA keys that exists strictly for interop with external ecosystems (authorization servers and relying parties that require classical algorithms); the framework's own tokens stay on the PQC-first signer. `b.auth.jar.build` mints the RFC 9101 request object on top of it, and `pushAuthorizationRequest` composes both so a pushed authorization request can carry the signed `request=` parameter — the FAPI 2.0 message-signing client shape. The OAuth client-attestation builder now composes the same promoted signer internally, with identical wire output.",
|
|
294
|
+
"sections": [
|
|
295
|
+
{
|
|
296
|
+
"heading": "Added",
|
|
297
|
+
"items": [
|
|
298
|
+
{
|
|
299
|
+
"title": "`b.auth.jar.build` — RFC 9101 request-object builder",
|
|
300
|
+
"body": "Mints the JWT-Secured Authorization Request: header `typ: oauth-authz-req+jwt` (RFC 9101 §10.8), `iss` = the client_id and `aud` = the authorization server (§5), `response_type` and `client_id` required as claims (§4), and every authorization parameter carried as a claim. A params object containing `request` or `request_uri` is refused (§4 forbids nesting), reserved-claim collisions are refused, and a `params.client_id` that disagrees with `opts.clientId` is refused. The JWT carries a short `exp` (default 5 minutes, `expiresInMs`-overridable), `nbf`, and a random `jti` for FAPI 2.0 message signing. The signing algorithm derives from the supplied key; `none` is impossible. Round-trips against the existing `b.auth.jar.parse` verifier."
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"title": "`b.auth.jws.sign` — classical compact-JWS signer for external interop",
|
|
304
|
+
"body": "Signs a compact JWS with an RS / PS / ES (P-256/P-384/P-521) / EdDSA key, deriving the algorithm from the key per RFC 7518 §3.1 and refusing `none`, HMAC, and algorithm/key mismatches; a caller-supplied `header.alg` cannot override the derived algorithm (algorithm-substitution closed). This primitive exists for interop with external ecosystems that require classical JOSE — JAR request objects, attestation JWTs, and similar cross-vendor surfaces. It is never the framework-internal token default: `b.auth.jwt` remains the PQC-first signer for the framework's own tokens."
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"title": "Pushed authorization requests can carry a signed request object (RFC 9126 §3)",
|
|
308
|
+
"body": "`pushAuthorizationRequest` accepts a `signedRequestObject` option (`{ key, alg?, kid?, audience?, expiresInMs? }`). When present, the authorization parameters are minted into a JAR request object and the PAR body carries `request=<jwt>` plus only the client-authentication material RFC 9126 allows alongside it; the bare authorization parameters are not duplicated in the form. Absent, the existing plain-form path sends the same key/value set as before."
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"title": "`validateOpts.assignOwnEnumerable` — shared prototype-safe claim merge",
|
|
312
|
+
"body": "Consolidates the own-enumerable key merge with prototype-pollution and reserved-key guards that the request-object builder, the classical signer, and the client-attestation builder all need. Existing call sites compose it; behavior is unchanged."
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
"heading": "Changed",
|
|
318
|
+
"items": [
|
|
319
|
+
{
|
|
320
|
+
"title": "OAuth client-attestation signing composes the promoted classical signer",
|
|
321
|
+
"body": "The attestation builder's private JWS assembly moved to the shared `b.auth.jws.sign` internals. Wire output is identical — same headers, claim order, algorithm selection, and accepted-algorithm set — and the `auth-oauth/attestation-*` error codes are preserved, so operators routing alerts on those codes see no change."
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"title": "Object key-copy sites compose the prototype-safe merge",
|
|
325
|
+
"body": "The long-running-operation status reader, the deny-path response-header merge, the HTTP client's cross-origin redirect header strip, and the trace-log logger wrapper now copy keys through `validateOpts.assignOwnEnumerable` instead of raw bracket-assign loops, so a `__proto__`/`constructor`/`prototype` key in the source object can never graft onto the copy. Behavior is otherwise unchanged."
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"heading": "Security",
|
|
331
|
+
"items": [
|
|
332
|
+
{
|
|
333
|
+
"title": "`jar.parse` returns prototype-safe authorization params (CWE-1321)",
|
|
334
|
+
"body": "A verified request object whose payload carried a `__proto__` claim (JSON.parse materializes it as an own key) previously grafted that claim's value onto the returned `params` object's prototype chain — a signature from a registered-but-malicious client was sufficient. The params object is now built through the prototype-safe merge; `__proto__`/`constructor`/`prototype` claim names are inert and are not copied."
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
"title": "`jws.sign` refuses `b64` and `crit` protected-header members",
|
|
338
|
+
"body": "RFC 7797 `b64: false` changes the JWS signing input (the payload is signed raw, not base64url-encoded) and RFC 7515 §4.1.11 `crit` promises the producer implements every extension it names. The signer always base64url-encodes the payload and implements no header extensions, so passing either member through minted a JWS whose header advertised semantics its signature was not computed under — a compliant verifier derives a different signing input or refuses the critical header. Both members are now refused with `auth-jwt-external/sign-unsupported-header`; unencoded-payload support would land as an explicit feature, not a header pass-through."
|
|
339
|
+
}
|
|
340
|
+
]
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"heading": "Removed",
|
|
344
|
+
"items": [
|
|
345
|
+
{
|
|
346
|
+
"title": "Maintainer planning note removed from the repository",
|
|
347
|
+
"body": "`memory/specs/node-26-map-getorinsert-migration.md` — a maintainer-local planning note that had been committed since v0.11.2 — is gone from the repository (it was never part of the npm package). The Node 26 detector allowlists in the pattern catalog now carry their per-site annotations standalone, and `SECURITY.md` / `.pinact.yaml` no longer reference maintainer-local note paths."
|
|
348
|
+
}
|
|
349
|
+
]
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"heading": "Detectors",
|
|
353
|
+
"items": [
|
|
354
|
+
{
|
|
355
|
+
"title": "raw-key-copy-loop-bypasses-assign-own-enumerable",
|
|
356
|
+
"body": "Refuses raw `out[keys[i]] = src[keys[i]]` bracket-assign copy loops in `lib/` — the shape behind the `jar.parse` finding. Key-copy sites compose `validateOpts.assignOwnEnumerable`; the two genuinely-different bodies (audit-chain hash canonicalization, schema-shape transforms) carry allowlist entries with structural reasons."
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
"title": "jose-header-passthrough-without-b64-crit-refusal",
|
|
360
|
+
"body": "Any caller-supplied JOSE protected-header pass-through must name-refuse `b64`/`crit` before signing."
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
"title": "no-tracked-internal-notes gate",
|
|
364
|
+
"body": "The pattern catalog now refuses any tracked file under `memory/`, `notes/`, or `.scratch*` paths at commit time."
|
|
365
|
+
}
|
|
366
|
+
]
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"heading": "Migration",
|
|
370
|
+
"items": [
|
|
371
|
+
{
|
|
372
|
+
"title": "No action required; everything is additive",
|
|
373
|
+
"body": "The JAR builder, the classical signer, the PAR `signedRequestObject` option, and the shared merge helper are new surface. Existing `jar.parse` callers, attestation flows, and plain PAR requests behave exactly as before."
|
|
374
|
+
}
|
|
375
|
+
]
|
|
376
|
+
}
|
|
377
|
+
]
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
"version": "0.14.21",
|
|
381
|
+
"date": "2026-06-04",
|
|
382
|
+
"headline": "SCIM Bulk forward references, an atomic api-encrypt replay gate, OID4VCI `x5c` proofs, HEAD responses without bodies, and a sweep that makes every accepted option do what its documentation says",
|
|
383
|
+
"summary": "This release closes correctness and conformance gaps across recently shipped standards surfaces, plus a framework-wide sweep for options that were accepted but never read. SCIM `/Bulk` resolves `bulkId` cross-references regardless of operation order; the `apiEncrypt` middleware closes a concurrent-replay window on multi-replica session stores and validates its numeric options at boot; OID4VCI accepts `x5c` holder-key binding; `problemDetails` spreads its documented `extensions` object as RFC 9457 sibling members; `openapiServe` / `asyncapiServe` answer HEAD without a body; and a batch of entry points now throw on mistyped numeric options instead of silently defaulting. Options whose documented behavior was never implemented are now wired; options that could never do anything are removed and refuse as unknown.",
|
|
384
|
+
"sections": [
|
|
385
|
+
{
|
|
386
|
+
"heading": "Fixed",
|
|
387
|
+
"items": [
|
|
388
|
+
{
|
|
389
|
+
"title": "SCIM `/Bulk` resolves `bulkId` cross-references regardless of operation order (RFC 7644 §3.7.2)",
|
|
390
|
+
"body": "Forward references (an operation referencing a resource a later operation creates — the shape Okta and Entra emit) now execute in dependency order; circular references are refused with status 409; a reference to an undeclared `bulkId`, or to an operation that failed, fails that operation with `invalidValue`. References are resolved on BOTH surfaces the spec allows: operation data (`\"value\": \"bulkId:u1\"`) and the operation path (`PATCH /Groups/bulkId:g1` targeting a group created in the same request) — path references order, substitute, and fail exactly like data references. Previously an unresolvable reference passed the literal `bulkId:<id>` token through to your resource adapter as if it were a real id. The Bulk response keeps results in original request order, and `failOnErrors` still short-circuits."
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"title": "OID4VCI `x5c` holder-key binding implemented (RFC 7515 §4.1.6; OID4VCI §8.2.1.1)",
|
|
394
|
+
"body": "The proof-JWT verifier named `x5c` as a valid holder-key binding in its own error message but always refused `x5c`-only proofs. The certificate chain is now shape-validated (standard base64 DER, leaf first), the leaf certificate's public key becomes the holder key at the same self-asserted trust level as an inline `jwk`, and a new optional `validateX5c(chainDerBuffers, header)` hook lets the issuer enforce chain trust (PKI anchoring, EKU checks, attestation-CA allowlists) before the key is accepted."
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
"title": "OID4VCI expired `c_nonce` refuses with a typed error",
|
|
398
|
+
"body": "A wallet whose access token outlived the shorter `c_nonce` TTL hit an untyped `TypeError` from the nonce comparison; issuance now refuses with `auth-oid4vci/c-nonce-expired` so handlers keying on typed codes respond correctly. The refusal direction is unchanged — no credential was ever minted on this path."
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
"title": "OID4VP DCQL numeric claim-path segments must be non-negative integers (OpenID4VP 1.0 §7.1.1)",
|
|
402
|
+
"body": "A query carrying `-1`, `1.5`, `NaN`, or `Infinity` as an array-index segment previously validated and then silently never matched; it now throws at build time, surfacing the malformed query to the verifier author instead of degrading to a silent non-match."
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
"title": "`problemDetails` `extensions` spread as sibling members (RFC 9457 §3.2)",
|
|
406
|
+
"body": "`send` and `create` documented an `extensions` object as the way to attach extension members, but emitted it as a literal nested `extensions` member instead. The keys now land as top-level siblings; reserved fields (`type` / `title` / `status` / `detail` / `instance`) cannot be overridden by an extension key, prototype-pollution-shaped keys are dropped, and a direct top-level key wins on collision."
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
"title": "`cspReport` honors `audit: false`",
|
|
410
|
+
"body": "The documented audit knob was accepted but never read; the `csp.violation` audit row fired unconditionally for every report. `audit: false` now suppresses the row while reports are still normalized and delivered to `onReport`. The default (audit on) is unchanged, and `maxBytes` now throws at config time on a non-positive-integer value instead of silently reverting to 64 KiB."
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"title": "`openapiServe` / `asyncapiServe` HEAD responses carry no body (RFC 9110 §9.3.2)",
|
|
414
|
+
"body": "Both middlewares advertised GET/HEAD but answered HEAD with the full JSON / YAML document as a body. HEAD now returns the GET headers (including `Content-Length`) with an empty body, matching the rest of the framework's document-serving middlewares."
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
"title": "Config-time numeric options throw on bad input across entry points",
|
|
418
|
+
"body": "A mistyped numeric option now throws at `create()` instead of silently becoming the default or garbage downstream: `scimServer` `maxPageSize` (a non-number propagated `NaN` into your `impl.list({ count })` and `ServiceProviderConfig`), `mail.send.deliver` `retry.maxAttempts` / `timeouts.mxLookupMs` / `timeouts.perHostMs`, the `redis` client `db` / `connectTimeoutMs` / `commandTimeoutMs` / `maxReconnectAttempts` (a non-numeric value made the reconnect-cap check false and silently disabled the bound entirely), `pubsub` cluster `pollIntervalMs` / `retentionMs` / `pruneEveryMs`, and SQS queue `visibilityTimeoutSec` / `waitTimeSec` (`0` short-polling stays valid)."
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
"title": "Accepted-but-unread options now do what their documentation says",
|
|
422
|
+
"body": "The db-backed `config` reloader's `audit` knob gates its `config.reload.*` rows; `honeytoken` honors the documented injectable audit sink (`{ audit: yourSink }`) instead of always emitting to the global sink; `dora`'s `observability` knob gates its report counter, and that counter now actually emits (it previously called a method the observability module doesn't export, and the failure was swallowed); flag evaluation-context `tenantKey` sets the tenant axis; the WCAG `aria` / `forms` / `tables` sub-scanners stamp `scopeUrl` on every finding so direct callers can correlate findings to a source document; `safeRedirect`'s documented `base` lets a same-origin absolute URL pass without an explicit allowlist (cross-origin still refused); and object-store bucket operations honor a per-call `actor` override on audit rows."
|
|
423
|
+
}
|
|
424
|
+
]
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
"heading": "Removed",
|
|
428
|
+
"items": [
|
|
429
|
+
{
|
|
430
|
+
"title": "Options that could never do anything now refuse as unknown",
|
|
431
|
+
"body": "The sweep removed accepted-but-impossible option keys: the `securityTxt` / `traceLogCorrelation` / tracer / TLS-RPT `audit` keys (these surfaces emit no audit rows), TLS-RPT `reportingMta` (not an RFC 8460 report field), `dsr.create` `observability` and create-time `verifyContext` (the per-call `process()` option of the same name is unchanged), `breakGlass.init` `now` (a single init-time timestamp cannot coherently override later time reads), WCAG `checkAll`, mail-deploy `compliance`, and bucket-ops `ca` (TLS trust is owned by the PQC agent — use `NODE_EXTRA_CA_CERTS` or `opts.agent`). Passing one of these now throws the standard unknown-option error."
|
|
432
|
+
}
|
|
433
|
+
]
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
"heading": "Security",
|
|
437
|
+
"items": [
|
|
438
|
+
{
|
|
439
|
+
"title": "api-encrypt concurrent-replay window closed (CWE-367)",
|
|
440
|
+
"body": "On a multi-replica session store, two concurrent requests carrying the same valid counter could both pass the monotonic replay check and execute twice — an attacker who captured one encrypted request could replay it concurrently and have a non-idempotent route run twice. The per-session path now claims each `(session, counter)` tuple through the same atomic nonce store the bootstrap path uses; exactly one concurrent request wins and the loser is refused with the standard rejection shape. The claim lives until the session expires (not just the staleness window), so a failed best-effort session write cannot re-open the tuple for late replay. The bootstrap response counter is also persisted correctly on serializing session stores, fixing a response-replay false positive on the second request of a session."
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
"title": "api-encrypt envelope metadata is authenticated (AEAD-bound)",
|
|
444
|
+
"body": "The envelope's plaintext fields — `_ts`, `_nonce`, `_sid`, `_ctr` — were not bound into the ciphertext, so a captured request could be replayed past the staleness window with a rewritten `_ts`, and a captured response could be replayed to the client under a bumped `_ctr` (the client's monotonic check reads the plaintext field). Every request and response envelope now binds its metadata as AEAD associated data on both protocol halves; any rewrite fails authenticated decryption and is refused (server: standard rejection; client: typed `CLIENT_RESPONSE_TAMPERED`). The client also advances its response counter only after authenticated decryption, so a refused forgery cannot poison the monotonic check and block subsequent genuine responses."
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
"title": "api-encrypt numeric options validated at boot",
|
|
448
|
+
"body": "A mistyped `replayWindowMs` (for example the string `\"5m\"`) made the timestamp-staleness comparison always false and silently disabled that replay defense. `replayWindowMs`, `maxDecryptedBytes`, and `pruneIntervalMs` now throw at config time across `create`, `client`, and `httpClient.encrypted`."
|
|
449
|
+
}
|
|
450
|
+
]
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
"heading": "Detectors",
|
|
454
|
+
"items": [
|
|
455
|
+
{
|
|
456
|
+
"title": "Three new codebase-pattern gates",
|
|
457
|
+
"body": "An option key accepted by a validation allowlist must be read by the file that accepts it (an accepted-but-never-read key is an advertised knob with no implementation); entry-point numeric options must validate rather than coerce-or-default (`Number(opts.x) || DEFAULT` swallows exactly the typo the config-time tier exists to surface); and a dispatcher that admits HEAD must suppress the response body somewhere in the same file."
|
|
458
|
+
}
|
|
459
|
+
]
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
"heading": "Migration",
|
|
463
|
+
"items": [
|
|
464
|
+
{
|
|
465
|
+
"title": "Delete removed option keys; everything else is a behavioral fix or additive",
|
|
466
|
+
"body": "If you pass one of the removed keys listed under Removed, delete it — it did nothing before and now throws the standard unknown-option error. Callers passing valid options see conforming behavior with no code change; the new `validateX5c` hook and the per-finding `scopeUrl` stamp are additive."
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
"title": "apiEncrypt middleware and client must upgrade together",
|
|
470
|
+
"body": "Binding the envelope metadata into the AEAD changes what the ciphertext authenticates, so a pre-0.14.21 client cannot talk to a 0.14.21 middleware or vice versa — mixed-version peers fail authenticated decryption and are refused. Both halves ship in this package; a single service upgrading normally is unaffected. If separate services pin different framework versions and speak apiEncrypt to each other, upgrade them together."
|
|
471
|
+
}
|
|
472
|
+
]
|
|
473
|
+
}
|
|
474
|
+
]
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
"version": "0.14.20",
|
|
478
|
+
"date": "2026-06-02",
|
|
479
|
+
"headline": "OAuth Rich Authorization Requests and client attestation, a sealed-field unseal rate cap, DMARC forensic-report parsing, monitor-mode browser-isolation headers, and FedCM / Storage-Access fetch-metadata",
|
|
480
|
+
"summary": "This release extends several standards surfaces the framework already covered in part. The OAuth client gains RFC 9396 Rich Authorization Requests: a typed `authorizationDetails` array is validated before the request and the granted details in the token response are cross-checked, refusing a grant the OP broadened beyond what was asked. The client also gains the OAuth 2.0 Attestation-Based Client Authentication primitives — it can build the `OAuth-Client-Attestation` / `-PoP` JWT pair and verify an inbound attestation. Sealed-field reads gain an opt-in unseal-failure rate cap that throttles a decryption-oracle / brute-force burst against attacker-written sealed columns (CWE-307). The inbound mail stack gains a DMARC forensic (RUF) report parser, the inverse of the aggregate-report parser. On the browser side, the security-headers middleware adds report-only COOP / COEP / Document-Policy variants for safe cross-origin-isolation rollout plus the embedder Require-Document-Policy and Service-Worker-Allowed headers, and the fetch-metadata middleware recognizes the FedCM `webidentity` destination and the Storage-Access request headers first-class. Observability adds a batch of current OpenTelemetry semantic-convention attributes. Every addition is additive or opt-in: an operator who sets no new option, and configures no rate cap, sees prior behavior unchanged.",
|
|
481
|
+
"sections": [
|
|
482
|
+
{
|
|
483
|
+
"heading": "Added",
|
|
484
|
+
"items": [
|
|
485
|
+
{
|
|
486
|
+
"title": "OAuth client: RFC 9396 Rich Authorization Requests (`authorizationDetails`)",
|
|
487
|
+
"body": "The OAuth / OIDC client accepts an `authorizationDetails` array (RFC 9396 §2 — each element a typed object) on the authorization and pushed-authorization-request paths; it is validated at config time (every element must be an object carrying a string `type`) and serialized as the `authorization_details` parameter, with a cap on the serialized payload. When `verifyAuthorizationDetails` is set, the granted `authorization_details` returned in the token response (RFC 9396 §7) is cross-checked against the request, and a grant that exceeds what was requested is refused — defending against an upstream broadened-grant privilege escalation. Without `authorizationDetails`, the request is unchanged."
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
"title": "OAuth 2.0 Attestation-Based Client Authentication",
|
|
491
|
+
"body": "`b.auth.oauth.buildClientAttestation` and `buildClientAttestationPop` mint the `OAuth-Client-Attestation` JWT (a client-backend-signed assertion binding a per-instance key, `typ: oauth-client-attestation+jwt`) and its per-instance `OAuth-Client-Attestation-PoP` proof; `verifyClientAttestation` validates the pair, including the alg allowlist (`ATTESTATION_ALGS`), the audience, and a constant-time `jti` check. This is the wallet / per-device client-authentication shape used in OpenID4VCI and the EUDI Wallet, an alternative to a shared `client_secret`."
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
"title": "`b.cryptoField.configureUnsealRateCap` — sealed-field decryption-oracle throttle",
|
|
495
|
+
"body": "An opt-in per-(actor, table, column) sliding-window cap on sealed-column unseal failures. A DB-write attacker who can place crafted `vault:` / `vault.aad:` bytes in sealed columns can force KEM decapsulation / AEAD verification on attacker-controlled input on every read; past `threshold` failures within `windowMs`, further unseal attempts for that tuple are refused for `cooldownMs` with a typed `CryptoFieldRateError` and a distinct `system.crypto.unseal_rate_exceeded` audit event (CWE-307; OWASP ASVS v5 §2.2.1; NIST SP 800-63B §5.2.2). Default off — with no cap configured, `unsealRow` behaves exactly as before (null the field, emit `system.crypto.unseal_failed`); the cap is count-based and lazily pruned, with no background timer."
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
"title": "`b.mail.dmarc.parseForensicReport` — RFC 6591 forensic (RUF) report parser",
|
|
499
|
+
"body": "Parses a DMARC failure (forensic) report: a `multipart/report; report-type=feedback-report` message (RFC 6591 §3) whose `message/feedback-report` subpart carries the `Feedback-Type: auth-failure` fields, with the required-field set validated (`Auth-Failure` and the other §3.1 fields) and a part-count and byte cap bounding a hostile report. It is the inbound inverse of the aggregate-report path, so an operator ingesting RUF mail has a parser symmetric with the existing aggregate parser and the v0.14.19 aggregate builder."
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
"title": "Monitor-mode cross-origin-isolation and embedder headers",
|
|
503
|
+
"body": "`b.middleware.securityHeaders` gains `coopReportOnly`, `coepReportOnly`, and `documentPolicyReportOnly` — set a policy string to emit the matching `*-Report-Only` header so a UA evaluates and reports violations without enforcing, the safe way to stage a COOP / COEP / Document-Policy rollout (WHATWG HTML cross-origin isolation; W3C Document Policy). `requireDocumentPolicy` emits the embedder `Require-Document-Policy` a parent demands of subframes, and `serviceWorkerAllowed` emits `Service-Worker-Allowed` to broaden a service worker's registration scope (W3C Service Workers). All default off."
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
"title": "fetch-metadata: FedCM `webidentity` and Storage-Access headers",
|
|
507
|
+
"body": "`b.middleware.fetchMetadata` recognizes the `webidentity` `Sec-Fetch-Dest` (a FedCM credentialed request) first-class and adds `deniedDest` — a list of destinations refused outright on the gated methods regardless of site, so a `webidentity` request hitting a route that is not an identity endpoint is refused. `allowStorageAccess` (default true) governs whether a request carrying `Sec-Fetch-Storage-Access: active` / `inactive` (the Storage Access API headers) is allowed, and `strictDest` throws at config time on an `allowedDest` / `deniedDest` value outside the known `Sec-Fetch-Dest` vocabulary, catching a typo at boot."
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
"title": "OpenTelemetry semantic-convention attributes",
|
|
511
|
+
"body": "The observability semantic-convention map gains a batch of current stable attributes: `peer.service`, the `faas.*` function attributes (`name` / `version` / `instance` / `trigger`), `deployment.environment.name`, `telemetry.distro.*`, `otel.scope.*`, and a Kubernetes subset (`k8s.cluster.name` and the node / container / cronjob / daemonset / job / replicaset / statefulset names), so a span or metric carrying these keys is recognized and emitted under the canonical name."
|
|
512
|
+
}
|
|
513
|
+
]
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
"heading": "Fixed",
|
|
517
|
+
"items": [
|
|
518
|
+
{
|
|
519
|
+
"title": "`b.auth.sdJwtVc.holder` signed the key-binding JWT with a non-key-derived algorithm",
|
|
520
|
+
"body": "The holder helper defaulted the key-binding JWT (KB-JWT) signing algorithm to a fixed `ES256` regardless of the holder key type, so a non-EC-P-256 holder key produced a presentation that either could not be built (an Ed25519 or P-384 key) or whose KB-JWT header advertised `ES256` while the signature used the actual key — a self-invalid token a verifier rejects. The algorithm is now derived from the holder key: ES256 / ES384 by EC curve, EdDSA for Ed25519 / Ed448, and ML-DSA-87 / ML-DSA-65 for an ML-DSA key. An RSA holder key, which has no supported KB-JWT algorithm, is refused at `holder.create` with a clear error instead of producing a broken presentation. An EC P-256 holder key, and any explicit `algorithm`, behave exactly as before."
|
|
521
|
+
}
|
|
522
|
+
]
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
"heading": "Security",
|
|
526
|
+
"items": [
|
|
527
|
+
{
|
|
528
|
+
"title": "Throttle a sealed-column decryption oracle (opt-in)",
|
|
529
|
+
"body": "`b.cryptoField.configureUnsealRateCap` lets an operator bound repeated unseal failures against sealed columns, so an attacker who can write crafted bytes into a sealed field cannot hammer the KEM / AEAD verify path indefinitely while only an off-band alert rule notices the burst (CWE-307). It is opt-in because a sensible threshold and window depend on a deployment's legitimate sealed-read volume; the always-on defense (null the field plus an audit event on every unseal failure) is unchanged."
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
"title": "Refuse a broadened OAuth grant",
|
|
533
|
+
"body": "With `verifyAuthorizationDetails` enabled, the OAuth client refuses a token response whose granted `authorization_details` exceed the requested set (RFC 9396 §7), so a compromised or misbehaving authorization server cannot silently widen a client's authorization beyond what the request asked for."
|
|
534
|
+
}
|
|
535
|
+
]
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
"heading": "Migration",
|
|
539
|
+
"items": [
|
|
540
|
+
{
|
|
541
|
+
"title": "No action required; additions are additive or opt-in",
|
|
542
|
+
"body": "The OAuth `authorizationDetails` request parameter, the granted-details cross-check, the client-attestation builders / verifier, the sealed-field unseal rate cap, the DMARC forensic-report parser, the monitor-mode and embedder browser headers, the fetch-metadata FedCM / Storage-Access options, and the new OpenTelemetry attribute keys are all additive or opt-in. A client that passes no `authorizationDetails`, an operator who calls no `configureUnsealRateCap`, and a security-headers / fetch-metadata configuration that sets none of the new options all see prior behavior byte-for-byte unchanged. The one behavior change is the SD-JWT VC holder fix: a `b.auth.sdJwtVc.holder` built with an RSA holder key is now refused at `holder.create` (RSA has no supported KB-JWT algorithm; it previously produced a self-invalid presentation) — switch such a holder to an EC P-256 / P-384, Ed25519, or ML-DSA key. EC P-256 holders and any explicit `algorithm` are unaffected."
|
|
543
|
+
}
|
|
544
|
+
]
|
|
545
|
+
}
|
|
546
|
+
]
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
"version": "0.14.19",
|
|
550
|
+
"date": "2026-06-02",
|
|
551
|
+
"headline": "ZIP64 write completes large-archive support, plus a PKCE-downgrade defense, SCIM Bulk, OID4VCI kid-resolution, SPF macros, DMARC report building, and OpenAPI 3.2",
|
|
552
|
+
"summary": "This release fills out several standards the framework previously read but could not write, or advertised but did not fully implement. The ZIP writer now emits ZIP64, so archives larger than 65535 entries or 4 GiB are produced instead of refused (the reader gained ZIP64 in the previous release; the two now round-trip). The OAuth client refuses an OP whose discovery metadata advertises PKCE methods without S256 — closing a stripped-S256 downgrade. The SCIM server gains opt-in Bulk operations, the OID4VCI issuer accepts a kid-resolution hook for EUDI-Wallet attested-key proofs (and a latent cache-cleanup crash on the issuance path is fixed), the inbound SPF check expands RFC 7208 macros and evaluates the exists mechanism, a DMARC aggregate-report builder is the inverse of the existing parser, and the OpenAPI emitter supports 3.2 with webhooks and a JSON Schema dialect. Every behavior-changing addition is opt-in or additive: classic archives emit byte-for-byte unchanged, OpenAPI still defaults to 3.1.0, SCIM Bulk stays disabled unless configured, and the PKCE refusal only fires for an OP that explicitly advertises a non-S256 method set.",
|
|
553
|
+
"sections": [
|
|
554
|
+
{
|
|
555
|
+
"heading": "Added",
|
|
556
|
+
"items": [
|
|
557
|
+
{
|
|
558
|
+
"title": "`b.archive.zip` writes ZIP64",
|
|
559
|
+
"body": "When an archive exceeds 65535 entries, or any entry's compressed/uncompressed size or local-header offset exceeds 4 GiB, the writer now emits ZIP64 — the classic field carries the `0xFFFF`/`0xFFFFFFFF` sentinel and a ZIP64 extended-information extra field (header id `0x0001`) plus a ZIP64 EOCD record and locator are written (APPNOTE 6.3.10 §4.3.14 / §4.3.15 / §4.4.8 / §4.5.3). Archives within the classic limits emit byte-for-byte unchanged, and `b.archive.read.zip` reads the ZIP64 output transparently. Previously the writer refused past 65535 entries."
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
"title": "`b.middleware.scimServer` opt-in SCIM 2.0 Bulk operations",
|
|
563
|
+
"body": "A `/Bulk` POST endpoint (RFC 7644 §3.7) with `maxOperations` / `maxPayloadSize` caps, `bulkId` cross-reference resolution (§3.7.2), and `failOnErrors` short-circuiting, advertised in `ServiceProviderConfig` when enabled via `opts.bulk`. The request body is read through the bounded stream reader. Bulk stays disabled (and `/Bulk` returns 501) unless `opts.bulk` is set."
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
"title": "`b.auth.oid4vci` accepts a `resolveKid` proof-key hook",
|
|
567
|
+
"body": "An OID4VCI proof JWT that carries a header `kid` without an inline `jwk` (the EUDI-Wallet attested-key shape) can now be verified by supplying `create({ resolveKid(kid, header) })`, which maps the key id to a holder key. The resolved key passes the same alg/key-type cross-check (CVE-2026-22817) as the inline path before import and becomes the credential's `cnf` binding. Without a resolver, a kid-only proof is still refused, exactly as before."
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
"title": "SPF macro expansion + `exists`; DMARC aggregate-report builder",
|
|
571
|
+
"body": "`b.mail.spf.verify` now expands RFC 7208 §7 macros (`%{i}`/`%{s}`/`%{l}`/`%{d}`/`%{o}`/`%{h}`/`%{v}` with the reverse / digit-count / delimiter transformers and `%%`/`%_`/`%-` escapes) and evaluates the `exists` mechanism (§5.7); the §4.6.4 DNS-lookup and void-lookup ceilings still bound every macro-driven query. New `b.mail.dmarc.buildAggregateReport` serializes a DMARC aggregate (RUA) report to RFC 7489 Appendix C XML — the inverse of `parseAggregateReport` — with optional gzip and XML-metacharacter escaping of observed identifiers."
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
"title": "OpenAPI 3.2 — webhooks and `jsonSchemaDialect`",
|
|
575
|
+
"body": "`b.openapi.create({ openapi: \"3.2.0\" })` opts into OpenAPI 3.2; a `webhook(name, method, opts)` builder registers top-level `webhooks` (API-initiated out-of-band operations), and `jsonSchemaDialect` declares the document's default JSON Schema dialect. `parse()` accepts 3.1.x or 3.2.x and validates webhook operations (including dangling-security references) with the same rules as paths. 3.1.0 stays the default emitted version and existing 3.1 documents are unchanged."
|
|
576
|
+
}
|
|
577
|
+
]
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
"heading": "Fixed",
|
|
581
|
+
"items": [
|
|
582
|
+
{
|
|
583
|
+
"title": "OID4VCI issuance crashed on cache cleanup",
|
|
584
|
+
"body": "The pre-authorized-code exchange and credential-issuance paths called `.delete()` on their internal `b.cache` stores, which expose `del()` — so the cleanup step threw and the issuance flow could not complete end-to-end. The three call sites now use `del()`."
|
|
585
|
+
}
|
|
586
|
+
]
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
"heading": "Security",
|
|
590
|
+
"items": [
|
|
591
|
+
{
|
|
592
|
+
"title": "PKCE-downgrade defense for OAuth / OIDC clients",
|
|
593
|
+
"body": "The OAuth client already mandates PKCE-S256 on its own side but never inspected the OP's published `code_challenge_methods_supported`. It now refuses, at `authorizationUrl` / `pushAuthorizationRequest`, an OP whose discovery metadata advertises that field without `\"S256\"` (plain-only / empty) — blocking a stripped-S256 downgrade that would otherwise drive the client into an authorization request the OP claims it cannot verify (RFC 9700 §4.13, RFC 7636). An OP that does not publish the field, and static-endpoint clients that perform no discovery, are unaffected."
|
|
594
|
+
}
|
|
595
|
+
]
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
"heading": "Migration",
|
|
599
|
+
"items": [
|
|
600
|
+
{
|
|
601
|
+
"title": "No action required; additions are additive or opt-in",
|
|
602
|
+
"body": "ZIP64 write is additive — archives within the classic 65535-entry / 4 GiB limits emit identical bytes, and the writer simply no longer refuses larger ones. SCIM Bulk, the OID4VCI `resolveKid` hook, the DMARC report builder, and OpenAPI 3.2 are all opt-in (OpenAPI still defaults to 3.1.0). The SPF `exists` mechanism, previously returning permerror, now evaluates per RFC 7208 — `b.mail.spf.verify` returns a verdict and refuses nothing. The PKCE-downgrade refusal only fires for an OP that explicitly advertises a non-S256 PKCE method set."
|
|
603
|
+
}
|
|
604
|
+
]
|
|
605
|
+
}
|
|
606
|
+
]
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
"version": "0.14.18",
|
|
610
|
+
"date": "2026-06-01",
|
|
611
|
+
"headline": "Advertised endpoints that silently failed now work, plus opt-in response-shaping, transport hardening, and ZIP64 read",
|
|
612
|
+
"summary": "This release closes a set of advertised-but-broken surfaces and adds the extensibility hooks operators kept having to work around. Three endpoints silently failed: the CSP-report endpoint returned 413 to every POST (it never parsed a report), the SCIM server broke on any streamed request body, and both came from misusing the bounded-buffer collector as if it consumed a stream — so this release adds the missing b.safeBuffer.collectStream(stream, opts) primitive and routes both through it. The agent orchestrator's graceful-drain phase never registered (wrong method name), the OpenAPI/AsyncAPI doc endpoints ignored a documented single-origin CORS allowlist, and the EU AI Act Article 50 HTML banner skipped Buffer response bodies. Deny-path refusals gained an RFC 9457 problem-type derived from the problem code (a 429 read about:blank before) and now treat a consumer hook that commits headers as terminal. On top of the fixes, several middlewares gain operator hooks (custom JSON/HTML error formatters, problem+json, refusal callbacks), several entry points gain escape-hatch opts (a configurable Authorization scheme, legacy filename charsets, a submission relay port, an exit-after-phases shutdown), the local job queue can point at an operator-supplied database/table/schema, breach-notification deadlines gain a running escalation clock, the archive reader now reads ZIP64, and external databases gain an opt-in TLS-required posture plus OpenTelemetry db.* trace attributes. Every behavior-changing addition is strictly opt-in — an operator who sets no new option sees no change.",
|
|
613
|
+
"sections": [
|
|
614
|
+
{
|
|
615
|
+
"heading": "Fixed",
|
|
616
|
+
"items": [
|
|
617
|
+
{
|
|
618
|
+
"title": "`b.middleware.cspReport` returned 413 to every POST; `b.middleware.scimServer` broke on streamed bodies",
|
|
619
|
+
"body": "Both read the request body by calling `b.safeBuffer.boundedChunkCollector(req, …)` — but that primitive takes a single options object and returns a push-collector, it does not consume a stream. The call passed the request as the options argument (so `maxBytes` was undefined and it threw), turning every CSP-report POST into a 413 and failing every streamed SCIM request. Both now read the body through the new `b.safeBuffer.collectStream`. A valid CSP report now reaches the parse/audit/`onReport` path and returns 204."
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
"title": "Agent orchestrator graceful-drain phase never registered",
|
|
623
|
+
"body": "`b.middleware.agentOrchestrator` registered its drain phase against a method the shutdown handle does not expose, so the phase silently never ran — connections were not drained on a graceful shutdown. It now registers through the real `addPhase` so the documented drain actually fires."
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
"title": "OpenAPI / AsyncAPI doc endpoints ignored `accessControl: { allowOrigin }`",
|
|
627
|
+
"body": "The documented single-origin CORS form was read by neither serve middleware (only `accessControl: \"public\"` was handled), so an operator passing `{ allowOrigin: \"https://docs.example.com\" }` got no `Access-Control-Allow-Origin` header. The object form now emits a validated origin (passed through `b.safeUrl`, rejecting CR/LF injection, userinfo, and non-http(s) schemes) with `Vary: Origin`."
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
"title": "EU AI Act Article 50 HTML banner skipped Buffer response bodies",
|
|
631
|
+
"body": "In HTML mode the disclosure banner was injected only when the final response chunk was a string; a `res.end(Buffer.from(html))` body (the common path through templating/render) silently received no banner. Buffer bodies are now decoded under the response charset, injected, and re-encoded (with a one-time warning and untouched bytes for charsets without a transcoder)."
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
"title": "Deny-path 429 (and any refusal) emitted `type: about:blank`",
|
|
635
|
+
"body": "The shared deny-path writer documented deriving the RFC 9457 problem-type URI from the refusal's problem code, but only ever read an explicit problem type — so a rate-limit 429 (which supplies a code, not a type) reported `about:blank`. The type is now derived from the problem code (`<base>/<code>`) when no explicit type is given, so a 429 reads `…/rate-limit-exceeded`."
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
"title": "A deny-path `onDeny` hook that commits headers is treated as terminal",
|
|
639
|
+
"body": "The deny-path writer's terminal check looked only at `res.writableEnded`; a response-wrapping consumer whose `onDeny` sent headers without ending the response fell through into a second `writeHead` and hit \"headers already sent\". The writer now also treats `res.headersSent` as terminal, so wrapping responders compose cleanly."
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
"title": "`b.mail.send.deliver` DANE TLSA lookup ignored the configured port",
|
|
643
|
+
"body": "The DANE TLSA query name (`_<port>._tcp.<host>`) was hardcoded to port 25; a delivery configured for a 587/465 relay still queried port 25. The lookup now uses the configured port."
|
|
644
|
+
}
|
|
645
|
+
]
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
"heading": "Added",
|
|
649
|
+
"items": [
|
|
650
|
+
{
|
|
651
|
+
"title": "`b.safeBuffer.collectStream(stream, opts)` — bounded stream-to-Buffer reader",
|
|
652
|
+
"body": "Reads a Node Readable (a request body, an upstream response) fully into one Buffer with the byte cap enforced at every chunk — the streaming sibling of `boundedChunkCollector`, which is a push-collector, not a stream consumer. Resolves the concatenated Buffer on end; rejects (and destroys the stream) the moment a chunk would overflow `maxBytes`."
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
"title": "Operator response-shaping hooks",
|
|
656
|
+
"body": "`b.errorPage` gains `jsonFormatter(info, req)` and `renderHtml(info, req)` overrides plus a `problemDetails: true` RFC 9457 mode (each falls back to the built-in envelope on throw, so the original error is never masked); `b.render.json` gains an `opts.replacer` passthrough (BigInt/Date); `b.middleware.cspReport` gains `onReject(req, res, { status, reason })` for the otherwise-empty-bodied 405/413/400 refusals; `b.static` gains `onError` firing on every refusal path, mirroring `onServe`."
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
"title": "Escape-hatch opts for non-default deployments",
|
|
660
|
+
"body": "`b.appShutdown.create({ exitAfterPhases })` lets a manual `shutdown()` exit after its phases (not only a signal-driven shutdown); `b.middleware.attachUser({ bearerScheme, tokenExtractor })` reads `Token`/`DPoP`/gateway Authorization schemes (RFC 6750 / RFC 9449); `b.middleware.bodyParser` multipart `filenameCharsets` opts iso-8859-1 `filename*` decoding in (RFC 5987); `b.mail.send.deliver.create({ port })` routes through a 587/465 submission relay (RFC 6409 / RFC 8314)."
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
"title": "Local job queue: bring-your-own database, table, and schema",
|
|
664
|
+
"body": "`b.queue.init` local-backend config accepts `db` (an operator-supplied handle), `table`, and `schema`. Table and schema names are validated and quoted as SQL identifiers (refused at init time when unsafe); the sealed `payload`/`lastError` columns stay sealed regardless of the physical table. Defaults are unchanged (`_blamejs_jobs` on the framework/cluster database)."
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
"title": "`b.breach.deadline.createClock` — running breach-notification escalation clock",
|
|
668
|
+
"body": "A detection-to-notification clock that escalates each affected US state's statutory breach-notification deadline (an approaching warning, a passed alert) and accepts per-state filing acknowledgements, complementing the existing static deadline lookup. It composes the incident-report deadline clock, so there is a single timer to start and stop."
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
"title": "`b.archive.read.zip` reads ZIP64",
|
|
672
|
+
"body": "Archives with more than 65535 entries or with sizes/offsets past 4 GiB now decode transparently (APPNOTE 6.3.10 §4.3.14 / §4.3.15 / §4.5.3). Previously such archives were refused outright. The bomb, Zip-Slip, PATH_MAX, and entry-type defenses all continue to apply to the resolved 64-bit values."
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
"title": "External-database opt-in TLS posture and OpenTelemetry `db.*` traces",
|
|
676
|
+
"body": "`b.externalDb` backends accept `requireTls: true` (default off), which refuses a backend at config time unless its declared transport is TLS (`tls` / a truthy `ssl` / `sslmode` `require|verify-ca|verify-full`) — for cardholder data or ePHI (PCI-DSS v4.0 Req 4 / HIPAA §164.312(e)). Query/transaction/read traces now also carry OpenTelemetry database semantic-convention attributes (`db.system`, `db.operation`, `db.statement` (sanitized), `db.name`)."
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
"title": "Opt-in schema-drift detection",
|
|
680
|
+
"body": "`b.db.init({ onDrift })` (and the underlying `reconcile({ onDrift })`) opts into config-vs-live column-drift detection: `\"warn\"` reports columns present in the live database but absent from the declared schema; `\"refuse\"` makes the framework refuse to boot on drift (a strict-schema posture — ISO 27001:2022 A.8.9 / SOC 2 CC8.1). The default (`\"ignore\"`) is unchanged, and drift is never resolved destructively."
|
|
681
|
+
}
|
|
682
|
+
]
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
"heading": "Changed",
|
|
686
|
+
"items": [
|
|
687
|
+
{
|
|
688
|
+
"title": "`b.guardEmail` documents its actual Unicode scope",
|
|
689
|
+
"body": "Domain-side IDN/Punycode and mixed-script confusable detection are supported (and unchanged); the local-part is ASCII atext only (RFC 5321/5322). The documentation previously implied RFC 6531 SMTPUTF8/EAI mailbox names were accepted — they are rejected by default, re-openable behind a future `allowUnicodeLocalPart` opt-in, to keep homograph/confusable exposure bounded to the domain side. No validation behavior changed."
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
"title": "Body-parse 4xx responses send `Connection: close`",
|
|
693
|
+
"body": "Every body-parse rejection (malformed JSON, poisoned key, oversized payload, bad content-length) now closes the connection, matching the chunked-decode path, to deny an upstream proxy reusing a socket whose request stream the parser abandoned mid-body (RFC 9112 §9.6)."
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
"title": "Archive-reader default entry-count cap raised to 1,048,576",
|
|
697
|
+
"body": "To read large (ZIP64) archives, the default `bombPolicy.maxEntries` rises from 65535 to 2^20. An operator who set `maxEntries` explicitly is unaffected; the cap can still be lowered."
|
|
698
|
+
}
|
|
699
|
+
]
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
"heading": "Security",
|
|
703
|
+
"items": [
|
|
704
|
+
{
|
|
705
|
+
"title": "Closed a request-smuggling socket-reuse window on body-parse rejections",
|
|
706
|
+
"body": "Pairing every body-parse 4xx with `Connection: close` stops an upstream proxy from reusing a connection whose request body the parser abandoned mid-stream — a request/response desync vector that previously applied only to the chunked-decode rejections."
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
"title": "Opt-in TLS-required posture for regulated external databases",
|
|
710
|
+
"body": "Set `requireTls: true` on an external-database backend carrying cardholder data or ePHI so a non-TLS (or fallback-permitting `sslmode`) connection is refused at boot rather than silently transmitting in the clear (PCI-DSS v4.0 Req 4 / HIPAA §164.312(e))."
|
|
711
|
+
}
|
|
712
|
+
]
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
"heading": "Detectors",
|
|
716
|
+
"items": [
|
|
717
|
+
{
|
|
718
|
+
"title": "Regression guards for every fixed bug class",
|
|
719
|
+
"body": "The internal pattern gate gains detectors so each fixed mistake cannot return: a bounded-collector call used as a stream consumer, a deny-path writer that does not guard `headersSent`, an `appShutdown` phase registered through a non-existent method, a body-parse error writer missing `Connection: close`, a raw `_blamejs_jobs` table reference bypassing the configured-table quoting, and a breach clock that re-rolls its own timer instead of composing the incident-report clock."
|
|
720
|
+
}
|
|
721
|
+
]
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
"heading": "Migration",
|
|
725
|
+
"items": [
|
|
726
|
+
{
|
|
727
|
+
"title": "No action required; new behavior is opt-in",
|
|
728
|
+
"body": "Every addition is additive or opt-in: `requireTls`, schema-drift `onDrift`, the response-shaping hooks, the new entry-point opts, and the breach clock all default to prior behavior when unset. Two defaults shift in a backward-compatible direction: body-parse 4xx now send `Connection: close`, and the archive reader's default entry-count ceiling rises to 2^20 (lower it via `bombPolicy.maxEntries` if you relied on the old cap). `b.db.reconcile` now returns a drift report object instead of undefined; the framework's own call ignores the return, so existing callers are unaffected."
|
|
729
|
+
}
|
|
730
|
+
]
|
|
731
|
+
}
|
|
732
|
+
]
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
"version": "0.14.17",
|
|
736
|
+
"date": "2026-05-31",
|
|
737
|
+
"headline": "In-session API-encrypt errors stay confidential, and the encrypted client can read them",
|
|
738
|
+
"summary": "When b.middleware.apiEncrypt is active, a normal response is sealed in the { _ct } envelope, but a terminal error that bypassed res.json (an error page, a validation refusal, an RFC 9457 problem+json document, a deny-middleware body) shipped in plaintext on the otherwise-encrypted channel, and the b.httpClient.encrypted client threw on the response shape because it tried to decrypt every reply. This release makes errors symmetric: those four sinks now seal their body in the same envelope a success uses whenever a session is active (via a new req.apiEncryptEncode the middleware installs after a successful decrypt), and the client gains a responseMode: \"passthrough\" that reads a non-2xx — decrypting an in-session error and returning a plaintext one verbatim — instead of throwing. Errors raised before a session exists (a Bearer 401, a handshake rejection, a replay refusal) deliberately stay plaintext and human-readable. Two adjacent streaming fixes ride along: a streamed (responseMode \"stream\") non-2xx now keeps a bounded prefix of the error body on the thrown error instead of draining it, and an SSE channel closed by a transport fault audits as a failure with a reason rather than looking like a clean operator close.",
|
|
739
|
+
"sections": [
|
|
740
|
+
{
|
|
741
|
+
"heading": "Added",
|
|
742
|
+
"items": [
|
|
743
|
+
{
|
|
744
|
+
"title": "`b.httpClient.encrypted` gains `responseMode: \"passthrough\"`",
|
|
745
|
+
"body": "The encrypted client defaults to `responseMode: \"reject\"` (a non-2xx rejects, exactly as before). Set `responseMode: \"passthrough\"` at create time, or per request, to resolve a non-2xx instead: the result is `{ statusCode, headers, body, ok }`, where `body` is the decrypted object when the reply carries an encrypted `_ct` envelope (an in-session error) and the parsed plaintext otherwise (a pre-session error such as a Bearer 401 or a proxy 502). The additive `ok` boolean (status 200–299) is present on every result. This lets a client read an error's status and detail instead of failing on the response shape."
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
"title": "`req.apiEncryptEncode` — the in-session error encoder",
|
|
749
|
+
"body": "`b.middleware.apiEncrypt` installs `req.apiEncryptEncode(obj)` after it successfully decrypts a request body. It returns the same `{ _ct }` (plus `_sid` / `_ctr` in per-session mode) envelope a normal response uses, so a terminal handler that writes its body directly (bypassing the wrapped `res.json`) can keep an error confidential on the encrypted channel. It is present only after a valid decrypt, so every pre-session path has no encoder and its errors stay plaintext."
|
|
750
|
+
}
|
|
751
|
+
]
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
"heading": "Changed",
|
|
755
|
+
"items": [
|
|
756
|
+
{
|
|
757
|
+
"title": "In-session terminal errors are sealed in the encrypted envelope",
|
|
758
|
+
"body": "The error page (JSON branch), the router's schema-validation refusal, `b.problemDetails.respond` (now accepting an optional `req`), and the access-deny middleware now seal their error body via `req.apiEncryptEncode` when a session is active — so an in-session error no longer ships as plaintext while the surrounding traffic is encrypted. A sealed problem+json is labelled `application/json` (the envelope), not `application/problem+json`. With no active session (or if encoding fails) the body stays plaintext and readable, unchanged."
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
"title": "Streamed non-2xx responses preserve a bounded error-body prefix",
|
|
762
|
+
"body": "`b.httpClient.request({ responseMode: \"stream\" })` (and `b.httpClient.downloadStream`, which composes it) previously drained and discarded the body of a `>= 400` response. The rejection is unchanged, but the thrown error now carries a bounded (16 KiB) prefix of the body on `err.body`, so a caller can read the problem+json / error detail. The prefix is collected through `b.safeBuffer.boundedChunkCollector`, so a hostile oversized error body can't accumulate unbounded."
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
"title": "SSE transport-fault close audits as a failure",
|
|
766
|
+
"body": "An SSE channel closed by a transport fault (a stream `error`, a failed heartbeat write) now emits its `sse.channel_closed` audit event with `outcome: \"failure\"` and a `reason`, instead of the `\"success\"` outcome an intentional `channel.close()` records — so an operator's evidence stream can tell a dropped connection from a clean shutdown."
|
|
767
|
+
}
|
|
768
|
+
]
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
"heading": "Security",
|
|
772
|
+
"items": [
|
|
773
|
+
{
|
|
774
|
+
"title": "Error bodies no longer leak in plaintext on an encrypted channel",
|
|
775
|
+
"body": "Before this release, a request that established an apiEncrypt session but then failed (validation error, access denial, server error rendered through the error page or problem-details) returned its error body in plaintext, even though every successful response on the same channel was encrypted — exposing error detail (paths, field names, refusal reasons) to a network observer. In-session errors are now encrypted symmetrically with successes; pre-session errors, which a client must be able to read to recover, remain plaintext by design."
|
|
776
|
+
}
|
|
777
|
+
]
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
"heading": "Migration",
|
|
781
|
+
"items": [
|
|
782
|
+
{
|
|
783
|
+
"title": "Opt into reading non-2xx encrypted errors",
|
|
784
|
+
"body": "No change is required for existing callers — `b.httpClient.encrypted` still defaults to `responseMode: \"reject\"` (throw on non-2xx). To read an error's status and decrypted detail, create the client with `responseMode: \"passthrough\"` (or pass it per request) and branch on the returned `ok` / `statusCode`. The new `ok` field is additive and does not affect existing `{ statusCode, headers, body }` consumers."
|
|
785
|
+
}
|
|
786
|
+
]
|
|
787
|
+
}
|
|
788
|
+
]
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
"version": "0.14.16",
|
|
792
|
+
"date": "2026-05-31",
|
|
793
|
+
"headline": "Connection entry-point ports are validated at config time",
|
|
794
|
+
"summary": "Six connection entry points previously read opts.port with a bare `|| <default>` fallback, silently coercing a string, negative, NaN, or out-of-range port instead of catching the operator's typo. A new b.validateOpts.optionalPort enforces the RFC 6335 §6 wire-valid range and is wired into b.mail.smtpTransport, b.ntpCheck.querySingle, b.networkDns.useDnsOverTls, b.networkNts (KE handshake / query / facade), b.redisClient.create, and createApp().listen — each now throws at construction with a clear message naming the bad value. The app.listen / createApp bind site opts into allowZero so port 0 (the legitimate ephemeral-bind sentinel) still works; the five outbound-connect sites require [1,65535].",
|
|
795
|
+
"sections": [
|
|
796
|
+
{
|
|
797
|
+
"heading": "Added",
|
|
798
|
+
"items": [
|
|
799
|
+
{
|
|
800
|
+
"title": "`b.validateOpts.optionalPort`",
|
|
801
|
+
"body": "A config-time port validator: `optionalPort(value, label, errorClass, code, opts?)` returns an omitted (`undefined` / `null`) port unchanged, and otherwise requires an integer in the RFC 6335 §6 wire-valid range [1,65535] — rejecting a string, negative, NaN, Infinity, fractional, or out-of-range value. Pass `{ allowZero: true }` for a listen-bind site where port 0 is the OS ephemeral-bind sentinel. The thrown message reports the offending shape (so `Infinity` / `\"443\"` stay visible), and routes a caller-supplied typed framework error (or a plain Error when none is given), matching the existing `optionalPositiveFinite` family."
|
|
802
|
+
}
|
|
803
|
+
]
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
"heading": "Changed",
|
|
807
|
+
"items": [
|
|
808
|
+
{
|
|
809
|
+
"title": "Connection entry points reject a malformed port at construction",
|
|
810
|
+
"body": "`b.mail.smtpTransport`, `b.ntpCheck.querySingle`, `b.networkDns.useDnsOverTls`, `b.networkNts.performKeHandshake` / `query` / `querySingle`, `b.redisClient.create`, and `createApp().listen` (plus the `createApp` constructor's default port) now validate `opts.port` and throw synchronously on a non-integer / out-of-range value rather than coercing it through `||` to a default. This is a behavior change for a caller that was passing a non-canonical port (e.g. the string `\"587\"` or a NaN) and relying on the silent fallback — pass an integer in [1,65535] instead (or `0` for an ephemeral `createApp().listen` bind). `b.ntpCheck` gains a typed `NtpCheckError` for this (it had no error class before)."
|
|
811
|
+
}
|
|
812
|
+
]
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
"heading": "Detectors",
|
|
816
|
+
"items": [
|
|
817
|
+
{
|
|
818
|
+
"title": "Connection entry points must compose the port validator",
|
|
819
|
+
"body": "A new check flags a lib connection entry point that reads `opts.port` / `opts.kePort` / `opts.ntpPort` with a `|| <default>` fallback without composing `b.validateOpts.optionalPort` (or the equivalent `numericBounds.isPositiveFiniteInt` + 65535 cap), so an unvalidated port read can't slip back in."
|
|
820
|
+
}
|
|
821
|
+
]
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
"heading": "Migration",
|
|
825
|
+
"items": [
|
|
826
|
+
{
|
|
827
|
+
"title": "Pass an integer port to connection primitives",
|
|
828
|
+
"body": "If you were passing a non-integer or out-of-range `opts.port` to a mail / NTP / NTS / DNS-over-TLS / Redis transport or to `createApp().listen` and relying on the silent `|| default` fallback, that now throws at construction. Pass an integer in [1,65535]; for an ephemeral `createApp().listen` bind, pass `0` (still accepted)."
|
|
829
|
+
}
|
|
830
|
+
]
|
|
831
|
+
}
|
|
832
|
+
]
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
"version": "0.14.14",
|
|
836
|
+
"date": "2026-05-31",
|
|
837
|
+
"headline": "Recognized consent purposes with lawful-basis gating, and a new b.privacy namespace for annual EdTech vendor-review attestations",
|
|
838
|
+
"summary": "Closes the student-data gap where an educational-only consent purpose and an annual third-party vendor-review report were described but never implemented. b.consent gains a recognized-purpose vocabulary: a purpose value matching a recognized key carries lawful-basis constraints that grant() enforces, and the named educational-only purpose (FERPA's school-official exception and California's SOPIPA) refuses a legitimate_interests lawful basis. The new b.privacy namespace ships vendorReview(), a builder for the dated, clause-by-clause annual EdTech third-party / processor review FERPA and SOPIPA expect a school or district to keep — it computes whether every required clause (no targeted advertising, no commercial profiling, no sale of student data, deletion on request, school-official designation, and so on) is attested, names the gaps, and stamps a 365-day re-review clock. Free-form consent purposes keep working unchanged, so the vocabulary is opt-in and additive.",
|
|
839
|
+
"sections": [
|
|
840
|
+
{
|
|
841
|
+
"heading": "Added",
|
|
842
|
+
"items": [
|
|
843
|
+
{
|
|
844
|
+
"title": "`b.consent` recognized-purpose vocabulary + lawful-basis gating",
|
|
845
|
+
"body": "`b.consent.recognizedPurpose(name)` looks up a recognized purpose and `b.consent.listPurposes()` enumerates them. When a `grant({ purpose })` value matches a recognized key, `grant()` enforces that purpose's lawful-basis constraints; the `educational-only` purpose forbids a `legitimate_interests` basis (FERPA 34 CFR 99.31(a)(1) school-official exception; California SOPIPA Cal. B&P 22584; FTC school-authorized COPPA consent 16 CFR 312.5(c)(10)) and marks the data commercial-use-prohibited. The commercial-use prohibition is an operator trust-boundary obligation — `isGranted()` does not re-derive it. Any purpose value NOT in the vocabulary stays free-form and unconstrained, so existing callers are unaffected; the hash-chain column set is unchanged, so `b.consent.verify()` over existing rows is unaffected."
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
"title": "`b.privacy.vendorReview` — annual EdTech vendor-review attestation",
|
|
849
|
+
"body": "A new `b.privacy` namespace whose `vendorReview(opts)` builds the dated third-party / processor review a FERPA school-official arrangement and California SOPIPA expect for every vendor that touches student data. The operator supplies a boolean attestation per clause (educational-purpose-only, no-targeted-advertising, no-commercial-profiling, no-sale-of-student-data, security-safeguards, deletion-on-request, sub-processor-currency, breach-notification, school-official-designation, directory-information-handling); `vendorReview` validates the shape, computes whether every required clause is attested (`attested`) and which are not (`gaps`), and stamps `reviewedAt` plus a 365-day `nextReviewDueAt` re-review clock. `b.privacy.listVendorReviewClauses()` returns the clause set with citations. Operator-feeds-metadata: the frozen report is not framework-persisted — compose it into your retention / audit / export sink. A best-effort `privacy.vendor_review.recorded` audit event fires when an audit sink is wired."
|
|
850
|
+
}
|
|
851
|
+
]
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
"heading": "Detectors",
|
|
855
|
+
"items": [
|
|
856
|
+
{
|
|
857
|
+
"title": "A gated consent purpose must go through `b.consent`",
|
|
858
|
+
"body": "A new check flags any lib code that mints a consent row with a hardcoded `educational-only` purpose literal without composing the recognized-purpose vocabulary — which would record the value while never enforcing its FERPA / SOPIPA lawful-basis constraint."
|
|
859
|
+
}
|
|
860
|
+
]
|
|
861
|
+
}
|
|
862
|
+
]
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
"version": "0.14.13",
|
|
866
|
+
"date": "2026-05-31",
|
|
867
|
+
"headline": "Close advertised-but-missing surface: SRS1 chained forwarding, DCQL array-wildcard claim paths, and in-memory safe-archive extraction",
|
|
868
|
+
"summary": "Three primitives advertised a capability in their documentation or card but refused or omitted it at runtime; this release implements each. b.mail.srs gains srs1Rewrite for the SRS1 double-forward (and multi-hop) case — previously the @intro described SRS1 and create() threw, pointing at a function that was never exported. b.safeArchive gains extractToMemory, the in-memory counterpart to extract for read-only / serverless filesystems — previously the card advertised in-memory extraction but the orchestrator required a destination directory. b.auth.oid4vp.matchDcql now honours a null claims-path segment as the array wildcard the OpenID4VP DCQL spec defines, rather than refusing it as unsupported while the card advertised DCQL. A stale version-pinned wording in a safe-archive error message is corrected. Every change is additive or message-only — no existing caller changes behaviour.",
|
|
869
|
+
"sections": [
|
|
870
|
+
{
|
|
871
|
+
"heading": "Added",
|
|
872
|
+
"items": [
|
|
873
|
+
{
|
|
874
|
+
"title": "`b.mail.srs` SRS1 chained forwarding — `srs1Rewrite`",
|
|
875
|
+
"body": "`b.mail.srs.create(...)` now returns `srs1Rewrite` alongside `rewrite` / `reverse`. `srs1Rewrite(srsAddress)` chains an already-SRS0 (or SRS1) envelope-from for a further forwarding hop: it keeps the original SRS0 body verbatim, prepends the SRS0 originator's domain, and binds the pair with this forwarder's own HMAC-SHA-256 tag — no new timestamp, no repeated original local-part — emitting `SRS1=tag=originator==<SRS0-body>@thisForwarder`. `reverse()` now detects an SRS1 address, verifies this hop's tag and forwarder-domain binding, and unwraps exactly one hop back to the originator's SRS0 so a multi-hop bounce routes straight to the forwarder that can recover the original sender. Typed failure modes: `srs/not-srs0` (input not SRS-encoded), `srs/malformed` (missing the `==` separator), `srs/bad-tag` (tampered), `srs/too-long` (chain exceeds the RFC 5321 256-octet path limit). Implements the Sender Rewriting Scheme SRS1 wire format; the second-hop SPF rationale is RFC 7208 §2.4."
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
"title": "`b.safeArchive.extractToMemory` — in-memory safe extraction",
|
|
879
|
+
"body": "An async generator counterpart to `b.safeArchive.extract` for read-only / serverless filesystems: it resolves the source, sniffs the format, auto-unwraps recipient (`BAWRP`) / passphrase (`BAWPP`) envelopes, and dispatches to the zip / tar / tar.gz reader's in-memory `extractEntries()`, yielding `{ name, bytes, size }` per regular-file entry without ever writing to disk. It takes no `destination`. Every defense the disk path runs applies unchanged: the zip-bomb caps (entry-count / per-entry / total / expansion-ratio), the `b.guardArchive` metadata cascade (Zip-Slip / path-traversal / symlink-escape / encrypted-entry refusal, CVE-2025-3445 class), and the entry-type policy. The disk-only realpath-agreement check (CVE-2025-4517 PATH_MAX TOCTOU defense) is intentionally absent — there is no extraction root — so the archive-level name refusals carry containment. Trusted-stream sources are refused upfront (the adversarial-safe central-directory walk needs random access). gzip magic per RFC 1952 §2.3.1."
|
|
880
|
+
}
|
|
881
|
+
]
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
"heading": "Fixed",
|
|
885
|
+
"items": [
|
|
886
|
+
{
|
|
887
|
+
"title": "OID4VP DCQL `null` claim-path segment now resolves the array wildcard",
|
|
888
|
+
"body": "`b.auth.oid4vp.matchDcql` previously threw `auth-oid4vp/null-path-segment-not-supported` for a `null` claims-path segment while the namespace card advertised DCQL — under-disclosing a legitimate presentation (CWE-863). Per OpenID4VP 1.0 §7.1.1 a `null` segment selects all elements of the array at that depth; the matcher now recurses over array elements with existence semantics (with DCQL value-matching applied to any selected leaf), composed to arbitrary depth. A `null` segment on a non-array node — like an integer index into a non-array, or a string key into an array — is a clean non-match, not a thrown error, because the matcher walks holder credential data rather than operator config. String and integer claim paths are byte-identical to before; only queries that previously threw now succeed or fail cleanly."
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
"title": "safe-archive trusted-stream refusal message no longer cites a stale version",
|
|
892
|
+
"body": "The thrown `safe-archive/trusted-stream-unsupported` message and its comment claimed trusted-stream extraction was \"deferred to v0.12.8 / when the v0.12.8 sequential extract path lands.\" That path shipped long ago — `b.archive.read.zip.fromTrustedStream` and the tar sequential mode exist — so the message now points at them as present capabilities and drops the version-pinned wording. The error code is unchanged."
|
|
893
|
+
}
|
|
894
|
+
]
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
"heading": "Detectors",
|
|
898
|
+
"items": [
|
|
899
|
+
{
|
|
900
|
+
"title": "A primitive may not advertise a capability and then throw an unimplemented stub",
|
|
901
|
+
"body": "A new check flags a bare `not yet supported` / `operator demand TBD` / `not supported in v1` refusal in a lib throw string (comments excluded). A defer is only complete with a written re-open condition; the SRS1 and DCQL stubs that this release implements both carried this bare-defer shape, and the detector keeps it from re-entering."
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
"title": "DCQL `null` path segments must recurse, never refuse",
|
|
905
|
+
"body": "A new check flags the `null path segment not supported` refusal shape in `lib/auth/oid4vp.js`, so the spec-mandated array wildcard cannot be re-stubbed."
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
"title": "`extractToMemory` must stay disk-free",
|
|
909
|
+
"body": "A new check flags any `writeFileSync` / `renameSync` / `mkdirSync` / `createWriteStream` inside the `extractToMemory` generator body, so the read-only / serverless contract cannot regress into a disk write."
|
|
910
|
+
}
|
|
911
|
+
]
|
|
912
|
+
}
|
|
913
|
+
]
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
"version": "0.14.12",
|
|
917
|
+
"date": "2026-05-31",
|
|
918
|
+
"headline": "Vault key rotation re-seals AAD-bound storage under the new root instead of silently orphaning it",
|
|
919
|
+
"summary": "Every AAD-sealed cell derives its key from the live vault root, so rotating the vault keypair changes those keys. `b.vaultRotate.rotate` previously re-sealed only legacy `vault:`-prefixed cells in `db.enc` and skipped `vault.aad:` cells, AAD-bound at-rest files, and operator-supplied AAD stores — leaving them encrypted under the retired keypair while still returning a success result and a passing round-trip verify, so the loss was invisible until the old keypair was discarded and the cells became permanently undecryptable. Rotation now re-seals `db.enc` (preserving its dataDir-bound AAD), `db.key.enc` (location-bound), every `{ aad: true }` table column, and the overflow store under the new root; refuses up front with a fail-closed error when operator-supplied AAD stores (agent idempotency / orchestrator / tenant / snapshot) are reachable unless each has been re-sealed via its module hook and explicitly acknowledged; and the round-trip verify now decrypts AAD-sealed cells under the new root and treats any cell that still opens under the old root as a regression. New explicit-root `b.vault.aad` seal / unseal / reseal primitives carry a cell from the old root to the new one while preserving its AAD tuple; `b.archive.rewrapTenant` re-wraps tenant-scoped archive envelopes; and `b.cluster` can adopt a rotated vault-key fingerprint instead of partitioning the membership during a rolling rotation.",
|
|
920
|
+
"sections": [
|
|
921
|
+
{
|
|
922
|
+
"heading": "Added",
|
|
923
|
+
"items": [
|
|
924
|
+
{
|
|
925
|
+
"title": "`b.vault.aad.sealRoot` / `unsealRoot` / `resealRoot`",
|
|
926
|
+
"body": "Explicit-root variants of the AAD seal / unseal that take a root-keypair JSON (`b.vault.getKeysJson()` output) instead of reading the live vault singleton. `resealRoot(value, aadParts, oldRootJson, newRootJson)` opens a cell under the old root and re-seals it under the new one while preserving the same AAD tuple (`table` / `rowId` / `column` / `schemaVersion`), which is what lets a rotation worker move AAD-bound state across a keypair change without altering the bound context. The default-root `b.vault.aad.seal` / `unseal` behaviour is unchanged."
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
"title": "Per-store AAD re-seal hooks on the agent primitives",
|
|
930
|
+
"body": "`b.agent.idempotency.reseal`, `b.agent.orchestrator.reseal`, `b.agent.snapshot.reseal`, and the `b.agent.tenant` registry / tenant-cell reseal paths re-seal that module's AAD-bound rows from an old root to a new root over the operator's own store. Each module also exposes an `AAD_ROTATION` descriptor naming the store the rotation pipeline cannot reach on its own, so an operator can enumerate exactly what to re-seal before a rotation."
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
"title": "`b.archive.rewrapTenant`",
|
|
934
|
+
"body": "Re-wraps a `recipient: \"tenant\"` archive envelope from an old vault root to a new one for a given `tenantId`, so a keypair rotation does not strand tenant-scoped archives. Opens the blob under the old root + tenantId, refuses a blob that is not a tenant-recipient envelope or that does not open under the supplied old root, and emits a fresh envelope bound to the new root. This is offered alongside the documented re-export path (decrypt with the old keypair, re-archive with the new one) for operators who hold the envelope but not the source."
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
"title": "Cluster vault-key rotation acceptance",
|
|
938
|
+
"body": "A vault-key rotation changes the public-key fingerprint recorded in the canonical cluster-state row, which a peer would otherwise report as `VAULT_KEY_DRIFT`. `b.cluster` configuration gains `acceptVaultKeyRotation: true` to declare the change legitimate — the node adopts the rotated fingerprint and bumps a rotation epoch instead of refusing — and an optional `expectedVaultKeyFp` that narrows acceptance to a single blessed post-rotation fingerprint. The drift guard stays in force whenever a rotation is not declared; supplying `expectedVaultKeyFp` without `acceptVaultKeyRotation` is rejected at configuration time as a misconfiguration."
|
|
939
|
+
}
|
|
940
|
+
]
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
"heading": "Changed",
|
|
944
|
+
"items": [
|
|
945
|
+
{
|
|
946
|
+
"title": "`b.vaultRotate.rotate` refuses when reachable AAD stores are not acknowledged",
|
|
947
|
+
"body": "Because the rotation pipeline walks only `db.enc` and cannot introspect an operator's own AAD-backed stores, it now detects which AAD-store modules are loadable and throws `vault-rotate/external-aad-unresealed` unless `opts.externalAadResealed` is either `true` (you do not use those features) or an array naming every detected store (you have re-sealed each via its hook). This converts a path that previously discarded data and reported success into a fail-closed gate. The error names each store and the hook to call."
|
|
948
|
+
}
|
|
949
|
+
]
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
"heading": "Fixed",
|
|
953
|
+
"items": [
|
|
954
|
+
{
|
|
955
|
+
"title": "Rotation re-seals `vault.aad:` cells and AAD-bound at-rest files",
|
|
956
|
+
"body": "`db.enc` is re-written bound to its dataDir-scoped AAD (it was previously re-written un-bound, silently stripping the at-rest AAD binding on every rotation), `db.key.enc` retains its location-bound AAD, and every `{ aad: true }` table column plus the overflow store is re-sealed under the new root. Previously only `vault:`-prefixed cells were carried across, so AAD-sealed data was left encrypted under the retired keypair and lost once it was discarded."
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
"title": "Round-trip verify no longer reports a false success",
|
|
960
|
+
"body": "`b.vaultRotate.verify` now samples and decrypts AAD-sealed cells under the new root and treats any cell that still decrypts under the old root as a regression, so an incomplete rotation fails verification instead of passing it. The prior verify checked only `vault:` cells and therefore reported `ok` even when AAD-sealed cells had been orphaned."
|
|
961
|
+
}
|
|
962
|
+
]
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
"heading": "Security",
|
|
966
|
+
"items": [
|
|
967
|
+
{
|
|
968
|
+
"title": "A vault key rotation can no longer silently destroy encrypted data",
|
|
969
|
+
"body": "The orphaning path lost agent idempotency / orchestrator / tenant / snapshot state, `{ aad: true }` columns, and tenant archives with no error and a passing verify; the data became unrecoverable the moment the old keypair was retired. Rotation is now fail-closed end to end: it re-seals what it can reach, refuses to proceed past what it cannot until you acknowledge it, and verifies the result under the new root. If you performed a rotation on v0.14.11 or earlier and still hold the retired keypair, re-seal the affected cells under the current root with the explicit-root primitives before discarding it."
|
|
970
|
+
}
|
|
971
|
+
]
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
"heading": "Detectors",
|
|
975
|
+
"items": [
|
|
976
|
+
{
|
|
977
|
+
"title": "AAD-backed store modules must expose a rotation reseal path",
|
|
978
|
+
"body": "A new check flags a module that registers an external `{ aad: true }` store but does not expose an `AAD_ROTATION` descriptor and reseal hook, which would leave its state unreachable by the rotation pipeline."
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
"title": "A root-keyed seal family must ship its reseal",
|
|
982
|
+
"body": "A new check flags adding a `sealRoot` / `unsealRoot` pair without the matching `resealRoot`, since without it a rotated cell cannot be carried from the old root to the new one."
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
"title": "Live-root AAD seals need a reseal path",
|
|
986
|
+
"body": "A new check flags a primitive that AAD-seals under the live vault root without a way to re-seal that state under a new root during rotation."
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
"title": "Tenant archive re-wrapping must compose `b.archive.rewrapTenant`",
|
|
990
|
+
"body": "A new check flags tenant-scoped archive re-wrapping that opens and re-seals a tenant envelope by hand instead of routing through `b.archive.rewrapTenant`."
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
"title": "Cluster vault-key drift needs the rotation-epoch accept gate",
|
|
994
|
+
"body": "A new check flags a cluster vault-key fingerprint comparison that hard-rejects a mismatch without honouring the `acceptVaultKeyRotation` epoch window."
|
|
995
|
+
}
|
|
996
|
+
]
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
"heading": "Migration",
|
|
1000
|
+
"items": [
|
|
1001
|
+
{
|
|
1002
|
+
"title": "Re-seal operator AAD stores before rotating",
|
|
1003
|
+
"body": "Before calling `b.vaultRotate.rotate`, re-seal each AAD-backed store you use via its hook (`b.agent.idempotency.reseal`, `b.agent.orchestrator.reseal`, `b.agent.snapshot.reseal`, the `b.agent.tenant` `AAD_ROTATION` reseal paths) with the old and new root JSON, re-wrap tenant archives with `b.archive.rewrapTenant`, then pass `opts.externalAadResealed` as an array naming each re-sealed store. If you use none of these features, pass `opts.externalAadResealed: true`. Declare the rotation to each cluster node with `acceptVaultKeyRotation: true` so the membership adopts the new fingerprint rather than reporting drift."
|
|
1004
|
+
}
|
|
1005
|
+
]
|
|
1006
|
+
}
|
|
1007
|
+
]
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
"version": "0.14.11",
|
|
1011
|
+
"date": "2026-05-31",
|
|
1012
|
+
"headline": "Defensive LLM model-I/O primitives, C2PA timestamp countersignatures with CAWG identity assertions, and signed EU AI Act GPAI adherence declarations",
|
|
1013
|
+
"summary": "Closes the output side of the LLM trust boundary and hardens content provenance and AI-Act attestation. b.ai.output.sanitize treats model output as untrusted and neutralizes XSS, gates every markdown-image / link and HTML src/href URL against SSRF (the EchoLeak zero-click exfiltration class, CVE-2025-32711), and flags SQL- and command-shaped fragments; b.ai.output.redact strips PII and secret disclosures. b.ai.input.classifyWithSources classifies a prompt together with its retrieval-augmented sources under a stricter, trust-tier-relative threshold, and the new b.ai.prompt namespace assembles prompts with escape-by-default boundaries — untrusted context / user segments are fenced in a per-render crypto-nonce delimiter the content cannot forge and stripped of bidi, control, zero-width, and Unicode-Tags smuggling characters. b.contentCredentials COSE signatures now carry an RFC 3161 timestamp countersignature (C2PA sigTst2, RFC 9921) verified entirely through b.tsa, so a signed manifest stays verifiable after its signing certificate expires, plus a CAWG identity assertion with trust-anchored verification. b.compliance.aiAct.gpai.declareAdherence emits a tamper-evident, ML-DSA-87-signed GPAI Code-of-Practice adherence declaration whose obligation set is derived from the regulation rather than operator-asserted.",
|
|
1014
|
+
"sections": [
|
|
1015
|
+
{
|
|
1016
|
+
"heading": "Added",
|
|
1017
|
+
"items": [
|
|
1018
|
+
{
|
|
1019
|
+
"title": "`b.ai.output.sanitize` and `b.ai.output.redact`",
|
|
1020
|
+
"body": "A new `b.ai.output` namespace that treats LLM output as untrusted before it reaches a browser, a downstream fetcher, a SQL / command sink, or a log. `sanitize(text, opts)` neutralizes active markup via `b.guardHtml`, gates every markdown image / link and HTML `src` / `href` URL through `b.safeUrl.parse` (scheme + credential) and `b.ssrfGuard.classify` (internal / loopback / link-local / cloud-metadata IP-range) so auto-fetch URLs to attacker or internal hosts are neutralized, and flags SQL- and command-shaped fragments rather than silently repairing them. `redact(text, opts)` strips PII and secret disclosures via `b.redact` plus an entity-selectable pass (`pan` / `ssn` / `ein` / `iban` / `jwt` / `aws` / `phi` / `email` / `phone`). Defends OWASP LLM05:2025 Improper Output Handling and LLM02:2025 Sensitive Information Disclosure; the markdown-image URL gate closes the EchoLeak zero-click exfiltration class (CVE-2025-32711, CVSS 9.3)."
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
"title": "`b.ai.input.classifyWithSources`",
|
|
1024
|
+
"body": "Classifies a prompt together with its retrieval-augmented (RAG) sources, applying a stricter, trust-tier-relative threshold to retrieved data. Each source is `{ id, text, trust? }` with `trust` of `trusted` / `internal` / `untrusted` (unset defaults to `untrusted`, fail-closed); untrusted and internal sources escalate to `suspicious` on a single severity-2 signal and to `malicious` on any severity-3, where the direct prompt keeps the baseline threshold. The aggregate verdict is the worst across the prompt and all sources, and every malicious source is reported in `taintedSources`. Defends indirect prompt injection from poisoned context (OWASP LLM01:2025; NIST AI 600-1)."
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
"title": "`b.ai.prompt.template`",
|
|
1028
|
+
"body": "A new `b.ai.prompt` namespace for assembling LLM prompts with escape-by-default boundaries. The `system` segment is operator-trusted; `context` and `user` segments are treated as untrusted (no global opt-out — mark a segment `{ text, trusted: true }` individually). Untrusted segments are wrapped in a per-render, high-entropy delimiter nonce the content cannot forge, with any forged boundary stripped before wrapping (spotlighting / datamarking, Microsoft 2024; NIST AI 100-2e2025), and stripped of bidi overrides (CVE-2021-42574 Trojan Source), C0 controls, zero-width characters, null bytes, and Unicode Tags (U+E0000..U+E007F ASCII-smuggling). Run `b.ai.input.refuseIfMalicious` on the untrusted content as defense in depth."
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
"title": "C2PA RFC 3161 timestamp countersignature and CAWG identity assertion",
|
|
1032
|
+
"body": "`b.contentCredentials.signCose` attaches an RFC 3161 timestamp countersignature (C2PA `sigTst2`, RFC 9921) and `b.contentCredentials.verifyCose` verifies it. Pass `timestamp:{ token }` to embed a TimeStampToken, or `timestamp:{}` to get back the DER `application/timestamp-query` to POST to a timestamp authority. `b.contentCredentials.attachIdentityAssertion` / `verifyIdentityAssertion` add the CAWG Identity Assertion v1.2: a signed creator / organization identity hash-bound to a manifest's referenced assertions, where the `x509` binding reports `verified:true` only when an identity trust anchor is supplied and the leaf chain verifies, and the `identity-claims-aggregator` and self-asserted paths stay `verified:false`."
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
"title": "`b.compliance.aiAct.gpai.declareAdherence` / `verifyAdherence`",
|
|
1036
|
+
"body": "Signed, tamper-evident GPAI Code-of-Practice adherence declarations (Regulation (EU) 2024/1689 Art. 53(1)(a-d); Art. 55 for systemic-risk models under Art. 51(2)). The in-scope obligation set is derived from the classifier, never operator-asserted — a model at or above the 10^25-FLOP systemic-risk threshold that omits the Art. 55 chapter is refused. Each commitment's evidence reference must be a SHA3-512 digest; a malformed hash is rejected so a hollow attestation cannot bind. The declaration ships inside an ML-DSA-87-signed CycloneDX 1.6 ML-BOM via `b.ai.modelManifest`; verify re-canonicalizes before trusting any field and rejects a declaration past its validity window. Cites the GPAI Code of Practice (10 July 2025), Annex XI/XII, and Directive (EU) 2019/790 Art. 4(3)."
|
|
1037
|
+
}
|
|
1038
|
+
]
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
"heading": "Security",
|
|
1042
|
+
"items": [
|
|
1043
|
+
{
|
|
1044
|
+
"title": "Model output is now an untrusted channel by default",
|
|
1045
|
+
"body": "When feeding retrieved documents into an LLM, classify them with `b.ai.input.classifyWithSources` (untrusted sources escalate on a single signal) rather than trusting model input; assemble prompts with `b.ai.prompt.template` so untrusted context / user text is fenced in a per-render crypto-nonce boundary it cannot forge; and pass model output through `b.ai.output.sanitize` / `b.ai.output.redact` before it is rendered, fetched, or logged. Each primitive is on by default and fail-closed — no opt-in flag enables the protection."
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
"title": "Timestamp verification routes only through `b.tsa.verifyToken`",
|
|
1049
|
+
"body": "C2PA `sigTst2` verification performs the full RFC 3161 check (CMS signature over the signed attributes, messageDigest recompute, critical sole `id-kp-timeStamping` EKU) — never a chain-only shortcut — closing the timestamp-validation-bypass class (CVE-2025-52556, CWE-347). Supply `timestampTrustAnchorsPem` to `verifyCose` to check the timestamp certificate chain; `verifyCose` returns `{ valid, reason, claims, alg, timestamp }` and never throws."
|
|
1050
|
+
}
|
|
1051
|
+
]
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
"heading": "Detectors",
|
|
1055
|
+
"items": [
|
|
1056
|
+
{
|
|
1057
|
+
"title": "LLM output URLs must keep the SSRF gate",
|
|
1058
|
+
"body": "A new check requires the output sanitizer to gate every extracted URL through both `b.safeUrl.parse` and `b.ssrfGuard.classify`, so the markdown-image SSRF gate (the EchoLeak class) cannot be silently dropped."
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
"title": "RAG sources must compose `classifyWithSources`",
|
|
1062
|
+
"body": "A new check flags any code that maps `b.ai.input.classify` over a sources array by hand, which would lose the trust-tier-relative threshold for retrieved data."
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
"title": "Prompt boundaries must use a per-render nonce",
|
|
1066
|
+
"body": "A new check flags prompt-assembly that wraps untrusted content in a fixed, guessable literal fence (`<user_input>`, `[DATA]`) instead of a per-render high-entropy delimiter the content cannot forge."
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
"title": "C2PA timestamp verification must route through `b.tsa`",
|
|
1070
|
+
"body": "A new check flags any bespoke certificate-chain-only walk on a timestamp token in place of `b.tsa.verifyToken`, preventing a re-introduction of the timestamp-validation-bypass class."
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
"title": "GPAI adherence declarations must be signed",
|
|
1074
|
+
"body": "A new check flags any code that emits the GPAI Code-of-Practice adherence property without routing it through the `b.ai.modelManifest` signed envelope, keeping the declaration tamper-evident."
|
|
1075
|
+
}
|
|
1076
|
+
]
|
|
1077
|
+
}
|
|
1078
|
+
]
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
"version": "0.14.10",
|
|
1082
|
+
"date": "2026-05-31",
|
|
1083
|
+
"headline": "Full-text-search token hashes move to a keyed MAC; existing mail-store search indexes rebuild automatically on upgrade",
|
|
1084
|
+
"summary": "The mail-store full-text-search index hashed its tokens with a hand-rolled salted-SHA3 derived hash. It now routes through the framework's sealed-column hashing primitive in keyed mode (HMAC-SHAKE256 off the per-deployment MAC key), so a search-index token hash is unforgeable and un-correlatable across deployments without that key — the same posture the sealed-column lookup hashes already use. Because the keyed hash changes the stored token values, a mail-store opened after upgrade detects its index as old-format and rebuilds it once from the sealed message rows. The rebuild runs under a format marker: the index is marked `rebuilding` before it is cleared and only marked current after every row is re-hashed inside an explicit transaction, and search falls back to its cursor path (rather than returning partial hits) whenever the marker is not current — so an interrupted rebuild leaves the old index intact and queryable and retries on the next open, never serving a half-built index. A new `b.cryptoField.computeNamespacedHash` primitive backs the keyed hashing for callers that hash outside the registered-column path.",
|
|
1085
|
+
"sections": [
|
|
1086
|
+
{
|
|
1087
|
+
"heading": "Added",
|
|
1088
|
+
"items": [
|
|
1089
|
+
{
|
|
1090
|
+
"title": "`b.cryptoField.computeNamespacedHash`",
|
|
1091
|
+
"body": "A mode-aware namespaced hash for indexed-lookup callers that hash a value outside the registered-column derived-hash path. `computeNamespacedHash(ns, value, { mode, truncateBytes })` routes through the same engine as `computeDerived` — `salted-sha3` (default) or the keyed `hmac-shake256` — with optional hex truncation. The mail-store full-text index is the first consumer."
|
|
1092
|
+
}
|
|
1093
|
+
]
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
"heading": "Changed",
|
|
1097
|
+
"items": [
|
|
1098
|
+
{
|
|
1099
|
+
"title": "Mail-store full-text index rehashes to a keyed MAC on upgrade",
|
|
1100
|
+
"body": "The full-text-search token hash now uses `b.cryptoField.computeNamespacedHash` in `hmac-shake256` mode instead of a hand-rolled salted-SHA3. The first time a store is opened after upgrade, its index is detected as old-format and rebuilt once from the sealed message rows; subsequent opens are no-ops. Search is unaffected once the rebuild completes. The rebuild requires the vault to be initialized and fails closed (a clear error) at construction if it is not, rather than leaving a stale searchable index."
|
|
1101
|
+
}
|
|
1102
|
+
]
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
"heading": "Security",
|
|
1106
|
+
"items": [
|
|
1107
|
+
{
|
|
1108
|
+
"title": "Keyed, un-correlatable full-text-search token hashes",
|
|
1109
|
+
"body": "A search-index token hash is now a keyed MAC over a per-deployment key, not a static-salted digest — it cannot be forged or correlated across deployments without that key, closing the low-entropy-token correlation gap on the search index. The index remains unrecoverable from a database dump alone, as before."
|
|
1110
|
+
}
|
|
1111
|
+
]
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
"heading": "Detectors",
|
|
1115
|
+
"items": [
|
|
1116
|
+
{
|
|
1117
|
+
"title": "Hand-rolled lookup-hash check covers the split form",
|
|
1118
|
+
"body": "The check that requires sealed-column lookup hashes to compose the framework primitive now also catches the across-lines hand-roll (`var salt = getDerivedHashSalt(); var hex = salt.toString(...); sha3(hex + ns + value)`), not only the single-expression form, so the bypass that the mail-store index used can't reappear."
|
|
1119
|
+
}
|
|
1120
|
+
]
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
"heading": "Migration",
|
|
1124
|
+
"items": [
|
|
1125
|
+
{
|
|
1126
|
+
"title": "Automatic, one-time full-text index rebuild",
|
|
1127
|
+
"body": "No operator action is required: the rebuild runs automatically and idempotently on first open after upgrade, atomically and crash-safe (an interrupted rebuild keeps the old index and retries). The only requirement is that the vault is initialized before the mail-store is constructed. One caveat for shared stores: do not run a pre-upgrade and post-upgrade node against the same backend file concurrently across this format change — the old node would write old-format hashes the new node cannot match. Roll the deployment fully across the upgrade. This re-open condition is lifted once all nodes are on 0.14.10 or later."
|
|
1128
|
+
}
|
|
1129
|
+
]
|
|
1130
|
+
}
|
|
1131
|
+
]
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
"version": "0.14.9",
|
|
1135
|
+
"date": "2026-05-30",
|
|
1136
|
+
"headline": "Corrects EU AI Act doc paths that named an uncallable namespace, plus source-comment hygiene and two new codebase checks",
|
|
1137
|
+
"summary": "A documentation fix and internal hygiene. The `@primitive` / `@signature` / `@example` blocks for the EU AI Act fundamental-rights-impact-assessment and GPAI training-data-summary helpers advertised `b.complianceAiAct.*`, which is undefined — the callable path is `b.compliance.aiAct.*` — so an operator copying the documented call got `undefined is not a function`. The documented paths now match the real surface. Alongside that: a duplicate parser entry in a doc block is removed, version stamps embedded in section-divider comments are stripped, and two codebase checks are added — one that fails the build when a `@primitive` block documents a wholly-unresolvable namespace (the gap that hid the AI Act paths), and one that flags a version stamp left inside a section divider. No exported API, error code, wire format, or runtime behaviour changes.",
|
|
1138
|
+
"sections": [
|
|
1139
|
+
{
|
|
1140
|
+
"heading": "Fixed",
|
|
1141
|
+
"items": [
|
|
1142
|
+
{
|
|
1143
|
+
"title": "EU AI Act helper documentation named an uncallable path",
|
|
1144
|
+
"body": "`b.compliance.aiAct.fundamentalRightsImpactAssessment` and `b.compliance.aiAct.gpai.trainingDataSummary` were documented as `b.complianceAiAct.*` in their `@primitive` / `@signature` / `@example` blocks (and one returned reference string). `b.complianceAiAct` is undefined, so the documented call failed; the documented paths now match the callable surface."
|
|
1145
|
+
}
|
|
1146
|
+
]
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
"heading": "Changed",
|
|
1150
|
+
"items": [
|
|
1151
|
+
{
|
|
1152
|
+
"title": "Source-comment hygiene",
|
|
1153
|
+
"body": "Removed a duplicate `env` entry from the parsers `@module` doc block, and stripped internal version stamps (`vX.Y.Z`) from `// ---- ... ----` section-divider comments across several files, keeping the descriptive label. Comment-only; no behaviour change."
|
|
1154
|
+
}
|
|
1155
|
+
]
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
"heading": "Detectors",
|
|
1159
|
+
"items": [
|
|
1160
|
+
{
|
|
1161
|
+
"title": "`@primitive` reachability covers wrong-namespace paths",
|
|
1162
|
+
"body": "The reachability check previously only flagged a missing leaf on a resolved namespace; a `@primitive` whose entire dotted prefix is unresolvable (the shape that hid the AI Act doc paths) was silently skipped. It now walks each prefix segment and fails the build on any unresolvable one, while preserving the factory-instance-shorthand exemption."
|
|
1163
|
+
},
|
|
1164
|
+
{
|
|
1165
|
+
"title": "Version-stamp-in-divider check",
|
|
1166
|
+
"body": "A new check flags a version stamp (`vX.Y.Z`) left immediately after a section divider's dashes (`// ---- vX.Y.Z ...`) — internal release vocabulary that does not belong in shipped source comments — without matching legitimate `@since` tags or prose version references."
|
|
1167
|
+
}
|
|
1168
|
+
]
|
|
1169
|
+
}
|
|
1170
|
+
]
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
"version": "0.14.8",
|
|
1174
|
+
"date": "2026-05-30",
|
|
1175
|
+
"headline": "Source-comment and codebase-check hygiene, plus a new require-block alignment check; no API or behaviour changes",
|
|
1176
|
+
"summary": "Internal lint and comment cleanup with no operator-facing surface change. Several codebase-check comments and one stub helper name that described behaviour the check no longer has are corrected; an unused lint-suppression class and a set of stale duplicate-cluster qualifiers (functions that were since renamed or extracted) are pruned or re-pointed. Fifty-nine `// allow:` markers that named the byte-size or time-literal check on values those checks no longer flag are removed, and twenty self-negating rationales on markers the time check genuinely fires on are rewritten to say why the value coincidentally matches. A new codebase check holds top-of-file require blocks to consistent `=` column alignment, with the files that currently carry drift listed as a migration allowlist. No exported API, error code, wire format, or runtime behaviour changes.",
|
|
1177
|
+
"sections": [
|
|
1178
|
+
{
|
|
1179
|
+
"heading": "Changed",
|
|
1180
|
+
"items": [
|
|
1181
|
+
{
|
|
1182
|
+
"title": "Lint-suppression and codebase-check comment cleanup",
|
|
1183
|
+
"body": "Corrected codebase-check comments that overstated their check's scope (a duplicate-code threshold described as three files when the advisory threshold is two, a narrowed byte-literal check carrying its pre-narrowing description, and a deferred-scan helper named as though it enforced a guarantee it does not yet provide). Removed an unused lint-suppression class and its one dead in-code marker, and pruned or re-pointed stale duplicate-cluster qualifiers that named functions since renamed or extracted into shared helpers. Removed fifty-nine `// allow:` markers that suppressed nothing, and rewrote twenty self-negating marker rationales (which read \"not seconds\" while sitting on a value the time check fires on) to explain the coincidental match. Source-comment and test hygiene only."
|
|
1184
|
+
}
|
|
1185
|
+
]
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
"heading": "Detectors",
|
|
1189
|
+
"items": [
|
|
1190
|
+
{
|
|
1191
|
+
"title": "Require-block `=` alignment check",
|
|
1192
|
+
"body": "A new codebase check flags a top-of-file require block that mixes its `=` column alignment — a fittable line whose `=` drifts off the column the rest of the block shares. Compact single-space blocks are exempt (only blocks that declare alignment intent are checked), as are destructures and long names physically too wide to reach the column, and blank- or comment-separated tiers align independently. The files that currently carry such drift are an explicit migration allowlist, reflowed over time; new code is held to the rule."
|
|
1193
|
+
}
|
|
1194
|
+
]
|
|
1195
|
+
}
|
|
1196
|
+
]
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
"version": "0.14.7",
|
|
1200
|
+
"date": "2026-05-30",
|
|
1201
|
+
"headline": "Storage and audit-trail hardening: queries are gated to declared columns, raw SQL refuses embedded literals, the database key is bound to its location, sealed-column lookup hashes gain a keyed mode, audit-chain purges can require dual control, and breach deadlines ship a running clock",
|
|
1202
|
+
"summary": "This release tightens the data and audit layers against a set of failure modes that were previously reachable. Database queries are now checked against the columns a table declared in its schema: a reference to an undeclared column fails closed by default instead of silently matching nothing, and the `whereRaw` escape hatch refuses an embedded string literal so values bind through placeholders. The database encryption key is sealed with its purpose, data directory, and key path as additional authenticated data, so a key file cannot be relocated to another deployment and unsealed there; an older key without that binding upgrades itself on first load. Sealed-column equality-lookup hashes can now be computed as a keyed MAC (HMAC-SHAKE256) off a per-deployment key, making the lookup hash unforgeable without that key, while the salted-SHA3 default is unchanged. Purging the tamper-evident audit chain can be placed under a two-authorizer dual-control grant so one operator cannot erase it alone, and database credential-rejection audits now record which relation the rejected credential tried to reach. Finally, breach-notification deadlines get a running clock that raises approaching and passed alerts as each regime's window elapses. One behavior change to note: the column gate defaults to reject — if a service issues queries against columns it did not declare in its schema, set `db.init({ columnGate: \"warn\" })` (audited, allowed) or `\"off\"` while the schema is reconciled.",
|
|
1203
|
+
"sections": [
|
|
1204
|
+
{
|
|
1205
|
+
"heading": "Added",
|
|
1206
|
+
"items": [
|
|
1207
|
+
{
|
|
1208
|
+
"title": "Column-membership gate on every query",
|
|
1209
|
+
"body": "`b.db.from(table)` now checks each referenced column against the table's declared schema. The mode is set with `db.init({ columnGate: \"reject\" | \"warn\" | \"off\" })` (default `reject`), and `query.allowedColumns([...])` narrows a single query to an explicit allowlist that is always enforced. `b.db.getDeclaredColumns(table)` returns a table's declared column names (or `null` for an unknown table). This is defense in depth against typo'd or caller-influenced column names reaching the SQL layer (CWE-89)."
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
"title": "Keyed mode for sealed-column lookup hashes",
|
|
1213
|
+
"body": "Equality-lookup (\"derived\") hashes for sealed columns can be computed as `hmac-shake256` — a keyed MAC over a per-deployment key — instead of the default `salted-sha3`. Set it per table with `cryptoField.registerTable(name, { derivedHashMode: \"hmac-shake256\" })` or per column with `{ from, mode: \"hmac-shake256\" }`. The keyed hash is unforgeable and un-correlatable without the deployment's MAC key, which raises the bar against offline lookup-table attacks on low-entropy sealed values (CWE-916). `b.vault.getDerivedHashMacKey()` exposes the 32-byte per-deployment key; it is created on first use and re-sealed across key rotation automatically."
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
"title": "Dual-control gate on audit-chain purge",
|
|
1217
|
+
"body": "`b.auditTools.purge` accepts `dualControlGrant`. When `audit_log` is placed under dual control, a verified archive and `confirm: true` are no longer sufficient: the purge additionally requires a consumed m-of-n grant whose action is bound to the purge, so a grant minted for another operation cannot be replayed and one operator cannot erase the tamper-evident chain alone (NIST SP 800-53 AU-9, separation of duties)."
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
"title": "Running clock for breach-notification deadlines",
|
|
1221
|
+
"body": "`b.incident.report.createDeadlineClock({ notify, approachThresholds })` tracks open incidents and raises `deadline_approaching` and `deadline_passed` alerts as each regime's window elapses (GDPR 72h, DORA, NIS2, and the rest of the registry). Alerts are deduplicated per incident and stage, suppressed once a submission stage is acknowledged, and the clock can run on an interval or be ticked manually."
|
|
1222
|
+
}
|
|
1223
|
+
]
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
"heading": "Changed",
|
|
1227
|
+
"items": [
|
|
1228
|
+
{
|
|
1229
|
+
"title": "Queries against undeclared columns now fail closed by default",
|
|
1230
|
+
"body": "The column gate defaults to `reject`: a query that references a column the table did not declare throws rather than silently matching nothing. A service that intentionally queries undeclared columns can set `db.init({ columnGate: \"warn\" })` to audit and allow, or `\"off\"` to disable the gate, while its schema is reconciled. Framework-declared columns (including `_id` and derived-hash columns) are always members."
|
|
1231
|
+
}
|
|
1232
|
+
]
|
|
1233
|
+
},
|
|
1234
|
+
{
|
|
1235
|
+
"heading": "Security",
|
|
1236
|
+
"items": [
|
|
1237
|
+
{
|
|
1238
|
+
"title": "Database encryption key bound to its location",
|
|
1239
|
+
"body": "`db.key.enc` is sealed with additional authenticated data over its purpose, resolved data directory, and resolved key path. A sealed key copied to a different deployment or path no longer unseals there — the AEAD authentication fails — which prevents silent key relocation. A legacy key sealed without this binding is detected and re-sealed in the bound format on first load, with no operator action required."
|
|
1240
|
+
},
|
|
1241
|
+
{
|
|
1242
|
+
"title": "`whereRaw` refuses embedded string literals",
|
|
1243
|
+
"body": "`whereRaw(sql, params)` and `WhereBuilder.raw(sql, params)` reject a raw fragment containing a string literal (`'...'`); values must bind through the `params` array. A static, operator-controlled literal can opt in with `{ allowLiterals: true }`. This closes a path where a value concatenated into a raw fragment would reintroduce SQL injection (CWE-89)."
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
"title": "Credential-rejection audits record the attempted relation",
|
|
1247
|
+
"body": "A `db.auth.failed` audit row (SQLSTATE 28000 / 28P01 / 42501) now carries `attemptedTable`, the relation the rejected credential tried to reach, extracted defensively from the statement. Triage can scope the blast radius of a credential-abuse event without correlating back to the raw SQL log (CWE-778)."
|
|
1248
|
+
}
|
|
1249
|
+
]
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
"heading": "Detectors",
|
|
1253
|
+
"items": [
|
|
1254
|
+
{
|
|
1255
|
+
"title": "Audit-purge dual-control gate",
|
|
1256
|
+
"body": "A new check fails the build if a call to `purgeAuditChain` appears in a file that does not also route through the dual-control gate, so a future caller cannot physically delete chain rows without two-authorizer enforcement."
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
"title": "Raw-SQL literal/interpolation guard",
|
|
1260
|
+
"body": "A new check fails the build on a `whereRaw` / `.raw` call whose SQL argument is built by template interpolation or string concatenation, keeping the bound-params discipline enforceable in framework code."
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
"title": "Hand-rolled lookup-hash guard",
|
|
1264
|
+
"body": "A new check fails the build if a sealed-column lookup hash is derived from the per-deployment salt outside the canonical helper, so call sites cannot bypass the keyed-mode and per-column mode policy."
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
"title": "Auth-audit attempted-relation guard",
|
|
1268
|
+
"body": "A new check fails the build if a `db.auth.failed` audit is emitted in a file that does not name `attemptedTable`, so the forensic field cannot be dropped from a future emitter."
|
|
1269
|
+
}
|
|
1270
|
+
]
|
|
1271
|
+
}
|
|
1272
|
+
]
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
"version": "0.14.6",
|
|
1276
|
+
"date": "2026-05-30",
|
|
1277
|
+
"headline": "Access-refusal middleware can return RFC 9457 problem+json or a custom response, and several documented-but-uncallable APIs are now reachable",
|
|
1278
|
+
"summary": "Every access-refusal middleware — the auth gates (bearer, DPoP, mTLS, AAL, bound-key), CSRF, CORS, rate-limit, bot-guard, age-gate, the host and network allowlists, and the method and content-type gates — now accepts two uniform options: `problemDetails: true` returns an RFC 9457 `application/problem+json` body, and `onDeny(req, res, info)` hands the response to the caller. With neither set the refusal is byte-for-byte what it was, so this is a drop-in change that lets a service standardize one error envelope across its API instead of working around each middleware's hardcoded body. Alongside that: `b.middleware.requireBoundKey` is now exported (it was documented and tested but never wired into the middleware surface), `b.middleware.bearerAuth` accepts `requiredScopes` (previously rejected at construction, which made its scope-enforcement path unreachable), API-key refusals send the RFC 6750 challenge code that matches the failure, two documented call paths that named a missing namespace segment are corrected, and the release flow now flags stale GitHub Actions and vendored bundles — with a ready-to-paste pin — before a dependency PR is needed.",
|
|
1279
|
+
"sections": [
|
|
1280
|
+
{
|
|
1281
|
+
"heading": "Added",
|
|
1282
|
+
"items": [
|
|
1283
|
+
{
|
|
1284
|
+
"title": "Uniform `onDeny` and `problemDetails` options on every access-refusal middleware",
|
|
1285
|
+
"body": "Each request-lifecycle middleware that refuses a request now takes `problemDetails: true` to emit an RFC 9457 `application/problem+json` body (composing `b.problemDetails`) and `onDeny(req, res, info)` to take over the response entirely; `info` carries the status, a machine reason, and the middleware-specific fields. The deny-path response headers (`Allow`, `WWW-Authenticate`, `Retry-After`, `Accept`) survive every mode. When neither option is set the response is unchanged. Covers `requireAuth`, `requireAal`, `requireMethods`, `requireContentType`, `requireMtls`, `requireBoundKey`, `bearerAuth`, `dpop`, `csrfProtect`, `fetchMetadata`, `botGuard`, `ageGate`, `hostAllowlist`, `networkAllowlist`, `cors`, `rateLimit`, and `dailyByteQuota` (whose existing `onExceeded` keeps working as an alias of `onDeny`)."
|
|
1286
|
+
}
|
|
1287
|
+
]
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
"heading": "Fixed",
|
|
1291
|
+
"items": [
|
|
1292
|
+
{
|
|
1293
|
+
"title": "`b.middleware.requireBoundKey` is now callable",
|
|
1294
|
+
"body": "The Bearer-API-key middleware was documented (with examples and tests) but never exported on `b.middleware`, so `b.middleware.requireBoundKey(...)` threw `undefined is not a function`. It is now wired into the middleware surface."
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
"title": "`b.middleware.bearerAuth` accepts `requiredScopes`",
|
|
1298
|
+
"body": "The RFC 6750 scope-enforcement path read `opts.requiredScopes`, but the option was rejected at construction with `unknown option`, making the 403 `insufficient_scope` behavior unreachable. `requiredScopes` is now an accepted option."
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
"title": "RFC 6750 challenge codes on API-key refusals",
|
|
1302
|
+
"body": "`b.middleware.requireBoundKey` now sends the `WWW-Authenticate` error code that matches the failure: `insufficient_scope` on a 403 missing-scope, `invalid_token` on an unknown or revoked token, and no error code on a 401 that presented no credentials (RFC 6750 §3). It previously sent `invalid_request` for every refusal."
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
"title": "Corrected two documented call paths",
|
|
1306
|
+
"body": "The compliance and network references named a path that dropped a namespace segment: the conformity-assessment scaffold is at `b.cra.report.conformityAssessment` (not `b.cra.conformityAssessment`), and the per-socket tuning helper is at `b.network.socket.applyToSocket` (not `b.network.applyToSocket`). The documented signatures now match the callable paths."
|
|
1307
|
+
},
|
|
1308
|
+
{
|
|
1309
|
+
"title": "GitHub Actions pins refreshed",
|
|
1310
|
+
"body": "`github/codeql-action` 4.35.5 to 4.36.0, and `docker/login-action`, `docker/setup-buildx-action`, and `docker/setup-qemu-action` to their latest releases."
|
|
1311
|
+
}
|
|
1312
|
+
]
|
|
1313
|
+
},
|
|
1314
|
+
{
|
|
1315
|
+
"heading": "Detectors",
|
|
1316
|
+
"items": [
|
|
1317
|
+
{
|
|
1318
|
+
"title": "`@primitive` reachability gate",
|
|
1319
|
+
"body": "A new check resolves every documented `b.X.Y` primitive against the actual public surface and fails the build when a documented path is not callable (factory-instance shorthands excluded). This is the gate that would have caught the `requireBoundKey` and call-path issues above."
|
|
1320
|
+
},
|
|
1321
|
+
{
|
|
1322
|
+
"title": "Deny-path composition gate",
|
|
1323
|
+
"body": "A new check requires every access-refusal middleware to route its refusal through the shared deny-response writer, so a future middleware cannot reintroduce a hardcoded body that locks callers out of `onDeny` / `problemDetails`."
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
"title": "Actions and vendor currency in the release flow",
|
|
1327
|
+
"body": "The release flow now fails the cut when a SHA-pinned GitHub Action or a vendored bundle is behind its latest upstream release. The actions report prints a ready-to-paste `owner/repo@<sha> # vX.Y.Z` pin and every file and line that uses it, so the bump is copy-paste rather than an after-the-fact dependency PR. Transient registry or API errors stay advisory so a flaky network response does not block an unrelated release."
|
|
1328
|
+
}
|
|
1329
|
+
]
|
|
1330
|
+
}
|
|
1331
|
+
]
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
"version": "0.14.5",
|
|
1335
|
+
"date": "2026-05-30",
|
|
1336
|
+
"headline": "Finished cleaning up the mislabeled byte-literal lint suppressions, with no API or behavior changes",
|
|
1337
|
+
"summary": "A follow-up to the byte-literal lint tightening. The remaining suppression comments that named the byte-literal check on values that are not byte sizes — JSON-RPC error codes, HTTP status codes, octet ranges, day-in-milliseconds constants — are removed, keeping their explanatory text and any correctly-named companion suppression. Every byte-literal suppression that remains is now on genuine 1024-scale byte arithmetic. Source-comment hygiene only.",
|
|
1338
|
+
"sections": [
|
|
1339
|
+
{
|
|
1340
|
+
"heading": "Changed",
|
|
1341
|
+
"items": [
|
|
1342
|
+
{
|
|
1343
|
+
"title": "Remaining mislabeled byte-literal suppressions removed",
|
|
1344
|
+
"body": "The byte-literal lint was previously a check on any multiple-of-8 integer, so suppression comments naming it were scattered across non-byte values. The last of those (in a handful of files, in mixed comment formats) are now removed — their explanatory text is retained as plain comments, and any correctly-named companion suppression is kept. The only byte-literal suppressions that remain are on genuine 1024-scale byte arithmetic. No change to any exported API, error code, wire format, or runtime behavior."
|
|
1345
|
+
}
|
|
1346
|
+
]
|
|
1347
|
+
}
|
|
1348
|
+
]
|
|
1349
|
+
},
|
|
1350
|
+
{
|
|
1351
|
+
"version": "0.14.4",
|
|
1352
|
+
"date": "2026-05-30",
|
|
1353
|
+
"headline": "Removed three pieces of dead code from the SAML, TLS, and JMAP surfaces; no API or behavior changes",
|
|
1354
|
+
"summary": "Cleanup of unreachable code. A reverse signature-algorithm lookup in the SAML verifier was never called — the actual verification path resolves the algorithm through the supported-signature table — so it is removed and a stale comment that referenced it is corrected. A leftover no-op placeholder in the TLS certificate re-encode path (a zero-length slice that was assigned and discarded) is removed, leaving the verbatim extension re-encode it sat next to. An unused JMAP well-known-path constant that existed only to be discarded is removed. None of this changes any exported API, error code, wire format, or runtime behavior.",
|
|
1355
|
+
"sections": [
|
|
1356
|
+
{
|
|
1357
|
+
"heading": "Removed",
|
|
1358
|
+
"items": [
|
|
1359
|
+
{
|
|
1360
|
+
"title": "Unreachable code in SAML, TLS, and JMAP",
|
|
1361
|
+
"body": "Removed `_sigAlgFromUri` from the SAML module (a reverse alg lookup that was never called — the embedded XML-DSig verifier resolves the algorithm via the supported-signature table, and the redirect-binding path uses the forward `_sigAlgUrn`), a discarded zero-length-slice placeholder in the TLS certificate extension re-encode path, and an unused well-known-path constant in the JMAP server. Internal cleanup only — no change to any exported API, error code, wire format, or runtime behavior."
|
|
1362
|
+
}
|
|
1363
|
+
]
|
|
1364
|
+
}
|
|
1365
|
+
]
|
|
1366
|
+
},
|
|
1367
|
+
{
|
|
1368
|
+
"version": "0.14.3",
|
|
1369
|
+
"date": "2026-05-30",
|
|
1370
|
+
"headline": "A codebase check now ensures every lint-suppression marker names a real check, so a typo can't silently disable a guard",
|
|
1371
|
+
"summary": "Source files suppress an individual lint with an `// allow:<class>` comment. If the class is mistyped or stale, the comment suppresses nothing — the check it names does not exist — so the issue it was meant to explain ships unflagged. A new codebase check now verifies every `// allow:<class>` marker names a registered check class and fails if it does not, with the full set of valid classes maintained as an explicit registry. Two markers that named a non-kebab class were corrected as part of this. No runtime, API, or wire-format changes.",
|
|
1372
|
+
"sections": [
|
|
1373
|
+
{
|
|
1374
|
+
"heading": "Detectors",
|
|
1375
|
+
"items": [
|
|
1376
|
+
{
|
|
1377
|
+
"title": "Lint-suppression markers must name a registered check",
|
|
1378
|
+
"body": "A new check flags any `// allow:<class>` suppression comment whose class is not one of the registered check classes — catching typos and stale markers (for example a marker that named a check which was later renamed) that would otherwise silently disable the guard they appear to explain. The valid classes are kept as an explicit registry, so adding a check with a new allow-class is a one-line registration. Source-comment hygiene only — no change to any exported API, error code, wire format, or runtime behavior."
|
|
1379
|
+
}
|
|
1380
|
+
]
|
|
1381
|
+
}
|
|
1382
|
+
]
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
"version": "0.14.2",
|
|
1386
|
+
"date": "2026-05-29",
|
|
1387
|
+
"headline": "Internal lint hygiene: the byte-literal check now flags only genuine byte-scale arithmetic, with no API or behavior changes",
|
|
1388
|
+
"summary": "A no-behavior-change cleanup of the source tree's internal lint markers. The byte-literal lint previously flagged every integer that was a multiple of 8 — which is most numbers — so the source carried a large number of suppression comments on values that were not byte sizes at all (status codes, counts, lengths, radixes, opcodes). The lint now flags only 1024-scale byte arithmetic (the case the C.BYTES.kib/mib/gib helpers exist to replace), and the now-unnecessary suppression comments have been removed while keeping their explanatory text. Several lint-suppression markers that named a check that does not exist were also corrected. None of this changes any API, wire format, or runtime behavior.",
|
|
1389
|
+
"sections": [
|
|
1390
|
+
{
|
|
1391
|
+
"heading": "Changed",
|
|
1392
|
+
"items": [
|
|
1393
|
+
{
|
|
1394
|
+
"title": "Source-tree lint markers cleaned up",
|
|
1395
|
+
"body": "The internal byte-literal lint was tightened to flag only genuine byte-scale (`* 1024`) arithmetic, and the suppression comments it previously required on non-byte integers were removed (their explanatory text is retained as plain comments). A handful of suppression markers that referenced a non-existent check were pointed at the correct one or removed. This is source-comment hygiene only — there is no change to any exported API, error code, wire format, or runtime behavior."
|
|
1396
|
+
}
|
|
1397
|
+
]
|
|
1398
|
+
}
|
|
1399
|
+
]
|
|
1400
|
+
},
|
|
1401
|
+
{
|
|
1402
|
+
"version": "0.14.1",
|
|
1403
|
+
"date": "2026-05-29",
|
|
1404
|
+
"headline": "Correctness fixes: JAR request-object typ enforcement, byte-faithful PGP multipart/signed, and SAML InResponseTo binding",
|
|
1405
|
+
"summary": "A set of correctness fixes across the auth, mail-crypto, TLS, and encoder surfaces. The most important: JWT-secured authorization requests (RFC 9101) now require the request object to carry the registered `oauth-authz-req+jwt` typ, closing a cross-JWT-confusion vector; the PGP multipart/signed wrapper is now assembled byte-faithfully so non-ASCII signed content can't be corrupted; and SAML response verification now returns the InResponseTo of the SubjectConfirmation that actually validated. Two of these change behavior — see Changed — and are bug fixes rather than new features.",
|
|
1406
|
+
"sections": [
|
|
1407
|
+
{
|
|
1408
|
+
"heading": "Security",
|
|
1409
|
+
"items": [
|
|
1410
|
+
{
|
|
1411
|
+
"title": "JAR request objects must carry the registered typ (RFC 9101)",
|
|
1412
|
+
"body": "`b.auth.jar.parse` now requires the request-object JWS header to carry `typ: \"oauth-authz-req+jwt\"` (with or without the `application/` prefix); a request object with an absent or different typ is refused with `auth-jar/bad-typ`. RFC 9101 §10.8 specifies this media type precisely to stop a JWT minted for another purpose (an ID token, an access token, a logout token) and signed by the same client key from being replayed as a request object. The existing `iss` / `aud` / `client_id` bindings already constrained that; this restores the explicit type check as well. This is stricter than before — see Changed."
|
|
1413
|
+
}
|
|
1414
|
+
]
|
|
1415
|
+
},
|
|
1416
|
+
{
|
|
1417
|
+
"heading": "Changed",
|
|
1418
|
+
"items": [
|
|
1419
|
+
{
|
|
1420
|
+
"title": "JAR parsing rejects untyped request objects (breaking)",
|
|
1421
|
+
"body": "Following the typ enforcement above, a request object whose header omits `typ` is now refused. An authorization server whose clients sign request objects without the `oauth-authz-req+jwt` typ must update those clients to set it."
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
"title": "PGP sign() returns multipartSigned as a Buffer (breaking)",
|
|
1425
|
+
"body": "`b.mail.crypto.pgp.sign(...).multipartSigned` is now a Buffer instead of a string. The OpenPGP signature covers the signed-part bytes exactly, and a JS-string round trip through latin1/utf8 could corrupt non-ASCII signed content and break verification, so the RFC 3156 multipart/signed wrapper is now assembled as bytes. Code that wrote the previous string to the wire works unchanged when it writes the Buffer; code that did string operations on the value should treat it as a Buffer (its `indexOf` / `toString` still work)."
|
|
1426
|
+
}
|
|
1427
|
+
]
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
"heading": "Fixed",
|
|
1431
|
+
"items": [
|
|
1432
|
+
{
|
|
1433
|
+
"title": "SAML response verification binds InResponseTo to the validated confirmation",
|
|
1434
|
+
"body": "`verifyResponse` returned the InResponseTo of the first SubjectConfirmationData in the assertion rather than of the SubjectConfirmation that actually passed bearer validation. When a response carried more than one SubjectConfirmation, the returned value could come from a non-validated confirmation. It is now the InResponseTo of the confirmation that validated."
|
|
1435
|
+
},
|
|
1436
|
+
{
|
|
1437
|
+
"title": "checkServerIdentity9525 emits the documented CN-fallback audit code",
|
|
1438
|
+
"body": "A CN-only legacy certificate (a Common Name present, no subjectAltName) is now refused by the exported `b.network.tls.checkServerIdentity9525` with the distinct `tls/pkix-cn-fallback-refused` code its documentation promised, so audit logs can tell a CN-only certificate apart from one carrying neither a SAN nor a CN (which still yields `tls/pkix-san-required`). The accept/refuse outcome is unchanged — both are refused; only the audit granularity improved."
|
|
1439
|
+
},
|
|
1440
|
+
{
|
|
1441
|
+
"title": "OIDC back-channel logout / JARM no longer accept dead override parameters",
|
|
1442
|
+
"body": "`verifyBackchannelLogoutToken` and `parseJarmResponse` passed `acceptedAlgs` / `jwksUri` / `maxClockSkewMs` through to the ID-token verifier, which ignored them and applied the configured (create()-time) values. The pass-throughs are removed so the code no longer reads as if those can be overridden per call; the configured trust anchor was — and remains — what applies."
|
|
1443
|
+
},
|
|
1444
|
+
{
|
|
1445
|
+
"title": "Queue lease failures are logged",
|
|
1446
|
+
"body": "The consumer loop's lease-acquisition error path swallowed the backend error silently; it now logs at debug so a flapping backend that has not yet tripped the circuit breaker is visible."
|
|
1447
|
+
},
|
|
1448
|
+
{
|
|
1449
|
+
"title": "Protobuf and ASN.1 encoders reject out-of-range tags",
|
|
1450
|
+
"body": "The protobuf tag encoder rejected nothing and would have emitted a wrong tag for field numbers at or above 2^28 (where the 32-bit shift overflows); the ASN.1 context-tag writers silently truncated tag numbers above 30 (which require the multi-byte high-tag-number form). Both now throw a RangeError rather than encode silently-wrong output. The values these encoders serve are well within range, so no current caller is affected."
|
|
1451
|
+
},
|
|
1452
|
+
{
|
|
1453
|
+
"title": "oid4vci proofAlgorithms default documented accurately",
|
|
1454
|
+
"body": "The documented default proof-algorithm list now matches the code (`[\"ES256\", \"ES384\", \"EdDSA\"]`); the doc previously omitted EdDSA, which the runtime has always accepted by default."
|
|
1455
|
+
}
|
|
1456
|
+
]
|
|
1457
|
+
}
|
|
1458
|
+
]
|
|
1459
|
+
},
|
|
1460
|
+
{
|
|
1461
|
+
"version": "0.14.0",
|
|
1462
|
+
"date": "2026-05-29",
|
|
1463
|
+
"headline": "Operator-configurable header and field names across SSE, request-id, rate-limit, age-gate, AI-Act disclosure, GraphQL federation, and the HTTP cache",
|
|
1464
|
+
"summary": "This release makes operator-facing identifiers that were hardcoded configurable. The framework already let operators rename most names (CSRF cookie/field, cookie parser, i18n header/query/cookie, mTLS CA name, and so on); this closes the remaining gaps so a custom or framework-specific name is never frozen. Every new option defaults to the value emitted today, so upgrading changes no behavior — these are additive knobs. It also fixes a request-id asymmetry (the response header is now written on the same name the inbound id is read from) and wires an SSE proxy-buffering option whose escape hatch was documented but never implemented.",
|
|
1465
|
+
"sections": [
|
|
1466
|
+
{
|
|
1467
|
+
"heading": "Added",
|
|
1468
|
+
"items": [
|
|
1469
|
+
{
|
|
1470
|
+
"title": "Configurable cache-status header on the HTTP client",
|
|
1471
|
+
"body": "The outbound HTTP client annotated every cached response with a hardcoded `x-blamejs-cache: HIT|MISS|STALE|REVALIDATED` header. `b.httpClient.cache.create` now takes `statusHeader` (default \"x-blamejs-cache\") — pass a custom name (e.g. \"x-cache\") to rename it, or null/false to suppress it entirely. The decision remains available programmatically on `res.cacheStatus`."
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
"title": "Configurable rate-limit header names",
|
|
1475
|
+
"body": "`b.middleware.rateLimit` emitted the de-facto `X-RateLimit-Limit` / `X-RateLimit-Remaining` headers (which are not RFC-pinned). It now accepts `headerPrefix` (default \"X-RateLimit-\") so operators can match the unprefixed IETF-draft `RateLimit-*` names or an upstream gateway's convention; the limit/remaining pair is always built from the same prefix."
|
|
1476
|
+
},
|
|
1477
|
+
{
|
|
1478
|
+
"title": "Configurable age-gate and AI-Act disclosure header names",
|
|
1479
|
+
"body": "`b.middleware.ageGate` now takes `privacyPostureHeader` (default \"X-Privacy-Posture\"; null/false to suppress), and `b.middleware.aiActDisclosure` takes `headerPrefix` (default \"AI-Act-\") that prefixes the emitted Notice / Article / Policy headers. The EU AI Act mandates the disclosure, not the HTTP spelling, so operators matching a downstream convention can rename these."
|
|
1480
|
+
},
|
|
1481
|
+
{
|
|
1482
|
+
"title": "Configurable GraphQL-federation replay-nonce header",
|
|
1483
|
+
"body": "`b.graphqlFederation.guardSdl` read the replay nonce from the Apollo-vendor `x-apollographql-router-nonce` header with no override. It now accepts `nonceHeader` (default unchanged) so an operator fronting the gateway with a non-Apollo router can point the replay check at their own header."
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
"title": "SSE proxy-buffering opt-out",
|
|
1487
|
+
"body": "`b.sse.create` and `b.middleware.sse` set `X-Accel-Buffering: no` (the nginx hint that disables proxy buffering). They now accept `proxyBuffer` (default true) — pass false when not behind nginx, or when buffering is controlled at the load balancer, to suppress the nginx-specific header. The opt-out was previously referenced in the documentation but not implemented."
|
|
1488
|
+
}
|
|
1489
|
+
]
|
|
1490
|
+
},
|
|
1491
|
+
{
|
|
1492
|
+
"heading": "Fixed",
|
|
1493
|
+
"items": [
|
|
1494
|
+
{
|
|
1495
|
+
"title": "Request-id middleware reflects the configured header name",
|
|
1496
|
+
"body": "`b.log.middleware` read the inbound request id from a configurable `headerName` but always wrote the response on the literal `X-Request-Id`. An operator who set a custom `headerName` (e.g. `X-Correlation-Id`) therefore read from one header and emitted another. The response is now written on the same configured name; the default remains `X-Request-Id`, so deployments that did not set `headerName` are unaffected."
|
|
1497
|
+
}
|
|
1498
|
+
]
|
|
1499
|
+
}
|
|
1500
|
+
]
|
|
1501
|
+
}
|
|
1502
|
+
]
|
|
1503
|
+
}
|