@blamejs/blamejs-shop 0.5.4 → 0.5.6
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 +4 -0
- package/README.md +1 -1
- package/lib/asset-manifest.json +1 -1
- package/lib/dunning.js +29 -4
- package/lib/plan-changes.js +288 -66
- package/lib/storefront.js +420 -0
- package/lib/vendor/MANIFEST.json +1395 -1409
- package/lib/vendor/blamejs/.clusterfuzzlite/Dockerfile +1 -1
- package/lib/vendor/blamejs/.github/dependabot.yml +12 -2
- package/lib/vendor/blamejs/.github/workflows/cflite_batch.yml +3 -3
- package/lib/vendor/blamejs/.github/workflows/cflite_pr.yml +3 -3
- package/lib/vendor/blamejs/.github/workflows/ci.yml +34 -29
- package/lib/vendor/blamejs/.github/workflows/codeql.yml +2 -2
- package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +6 -6
- package/lib/vendor/blamejs/.github/workflows/release-container.yml +8 -8
- package/lib/vendor/blamejs/.github/workflows/scorecard.yml +1 -1
- package/lib/vendor/blamejs/.gitignore +6 -6
- package/lib/vendor/blamejs/CHANGELOG.md +68 -0
- package/lib/vendor/blamejs/CONTRIBUTING.md +16 -0
- package/lib/vendor/blamejs/README.md +2 -1
- package/lib/vendor/blamejs/ROADMAP.md +51 -0
- package/lib/vendor/blamejs/SECURITY.md +52 -1
- package/lib/vendor/blamejs/api-snapshot.json +22 -2
- package/lib/vendor/blamejs/bench/_helpers.js +2 -0
- package/lib/vendor/blamejs/bench/crypto-hash.bench.js +2 -0
- package/lib/vendor/blamejs/bench/crypto-symmetric.bench.js +2 -0
- package/lib/vendor/blamejs/bench/run.js +2 -0
- package/lib/vendor/blamejs/bench/safe-json.bench.js +2 -0
- package/lib/vendor/blamejs/bin/blamejs.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/Dockerfile +1 -1
- package/lib/vendor/blamejs/examples/wiki/lib/auto-site-entries.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/lib/build-app.js +9 -1
- package/lib/vendor/blamejs/examples/wiki/lib/harvest-cli.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/lib/harvest-env-vars.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/lib/harvest-errors.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/lib/harvest-vendored-deps.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/lib/html-entities.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/lib/nav.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/lib/opts-resolver.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/lib/page-generator.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/lib/section.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/lib/source-doc-parser.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/lib/symbol-index.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/migrations/0001-pages-schema.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/package-lock.json +30 -0
- package/lib/vendor/blamejs/examples/wiki/routes/admin.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/routes/integration.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/routes/pages.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/scripts/backfill-module-metadata.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/seeders/prod/0001-default-pages.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/seeders/prod/pages/_index.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/seeders/prod/pages/api.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/server.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/site.config.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/snippets/auth/password-hash.example.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/src/editor.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/src/wiki.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/test/codebase-patterns.test.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/test/e2e.js +23 -0
- package/lib/vendor/blamejs/examples/wiki/test/find-missing-pages.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/test/integration.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/test/validate-cli-snapshot.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/test/validate-env-snapshot.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/test/validate-nav-coverage.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/test/validate-site-coverage.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/test/validate-source-comment-blocks.js +2 -0
- package/lib/vendor/blamejs/examples/wiki/wiki.config.js +2 -0
- package/lib/vendor/blamejs/fuzz/_expected.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-agent-registry.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-csv.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-dsn.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-email.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-envelope.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-event-bus-payload.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-event-bus-topic.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-html.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-idempotency-key.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-imap-command.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-jmap.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-json.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-list-id.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-list-unsubscribe.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-mail-compose.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-mail-move.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-mail-query.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-mail-reply.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-mail-sieve.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-managesieve-command.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-markdown.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-message-id.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-pop3-command.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-posture-chain.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-saga-config.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-smtp-command.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-snapshot-envelope.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-sql.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-stream-args.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-svg.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-tenant-id.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-text.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-trace-context.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-xml.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/guard-yaml.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/package-lock.json +1239 -0
- package/lib/vendor/blamejs/fuzz/package.json +10 -0
- package/lib/vendor/blamejs/fuzz/parsers__safe-ini.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/parsers__safe-toml.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/parsers__safe-xml.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/parsers__safe-yaml.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/safe-archive.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/safe-decompress.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/safe-dns.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/safe-ical.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/safe-icap.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/safe-json.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/safe-jsonpath.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/safe-mime.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/safe-mount-info.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/safe-sieve.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/safe-smtp.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/safe-url.fuzz.js +2 -0
- package/lib/vendor/blamejs/fuzz/safe-vcard.fuzz.js +2 -0
- package/lib/vendor/blamejs/index.js +2 -0
- package/lib/vendor/blamejs/lib/_test/crypto-fixtures.js +2 -0
- package/lib/vendor/blamejs/lib/a2a-tasks.js +2 -0
- package/lib/vendor/blamejs/lib/a2a.js +2 -0
- package/lib/vendor/blamejs/lib/acme.js +7 -1
- package/lib/vendor/blamejs/lib/agent-audit.js +2 -0
- package/lib/vendor/blamejs/lib/agent-envelope-mac.js +2 -0
- package/lib/vendor/blamejs/lib/agent-event-bus.js +2 -0
- package/lib/vendor/blamejs/lib/agent-idempotency.js +2 -0
- package/lib/vendor/blamejs/lib/agent-orchestrator.js +10 -2
- package/lib/vendor/blamejs/lib/agent-posture-chain.js +2 -0
- package/lib/vendor/blamejs/lib/agent-saga.js +2 -0
- package/lib/vendor/blamejs/lib/agent-snapshot.js +2 -0
- package/lib/vendor/blamejs/lib/agent-stream.js +2 -0
- package/lib/vendor/blamejs/lib/agent-tenant.js +11 -2
- package/lib/vendor/blamejs/lib/agent-trace.js +2 -0
- package/lib/vendor/blamejs/lib/ai-adverse-decision.js +2 -0
- package/lib/vendor/blamejs/lib/ai-aedt-bias-audit.js +2 -0
- package/lib/vendor/blamejs/lib/ai-capability.js +2 -0
- package/lib/vendor/blamejs/lib/ai-content-detect.js +2 -0
- package/lib/vendor/blamejs/lib/ai-disclosure.js +2 -0
- package/lib/vendor/blamejs/lib/ai-dp.js +2 -0
- package/lib/vendor/blamejs/lib/ai-frontier-protocol.js +2 -0
- package/lib/vendor/blamejs/lib/ai-input.js +2 -0
- package/lib/vendor/blamejs/lib/ai-model-manifest.js +2 -0
- package/lib/vendor/blamejs/lib/ai-output.js +2 -0
- package/lib/vendor/blamejs/lib/ai-pref.js +2 -0
- package/lib/vendor/blamejs/lib/ai-prompt.js +2 -0
- package/lib/vendor/blamejs/lib/ai-quota.js +2 -0
- package/lib/vendor/blamejs/lib/api-key.js +2 -0
- package/lib/vendor/blamejs/lib/api-snapshot.js +2 -0
- package/lib/vendor/blamejs/lib/app-shutdown.js +2 -0
- package/lib/vendor/blamejs/lib/app.js +2 -0
- package/lib/vendor/blamejs/lib/archive-adapters.js +2 -0
- package/lib/vendor/blamejs/lib/archive-entry-policy.js +2 -0
- package/lib/vendor/blamejs/lib/archive-gz.js +2 -0
- package/lib/vendor/blamejs/lib/archive-read.js +2 -0
- package/lib/vendor/blamejs/lib/archive-tar-read.js +2 -0
- package/lib/vendor/blamejs/lib/archive-tar.js +9 -0
- package/lib/vendor/blamejs/lib/archive-wrap.js +2 -0
- package/lib/vendor/blamejs/lib/archive.js +6 -0
- package/lib/vendor/blamejs/lib/arg-parser.js +2 -0
- package/lib/vendor/blamejs/lib/argon2-builtin.js +2 -0
- package/lib/vendor/blamejs/lib/asn1-der.js +2 -0
- package/lib/vendor/blamejs/lib/asyncapi-bindings.js +2 -0
- package/lib/vendor/blamejs/lib/asyncapi-traits.js +2 -0
- package/lib/vendor/blamejs/lib/asyncapi.js +2 -0
- package/lib/vendor/blamejs/lib/atomic-file.js +2 -0
- package/lib/vendor/blamejs/lib/audit-chain.js +2 -0
- package/lib/vendor/blamejs/lib/audit-daily-review.js +2 -0
- package/lib/vendor/blamejs/lib/audit-emit.js +2 -0
- package/lib/vendor/blamejs/lib/audit-sign.js +2 -0
- package/lib/vendor/blamejs/lib/audit-tools.js +2 -0
- package/lib/vendor/blamejs/lib/audit.js +2 -0
- package/lib/vendor/blamejs/lib/auth/aal.js +2 -0
- package/lib/vendor/blamejs/lib/auth/access-lock.js +2 -0
- package/lib/vendor/blamejs/lib/auth/acr-vocabulary.js +2 -0
- package/lib/vendor/blamejs/lib/auth/ato-kill-switch.js +48 -11
- package/lib/vendor/blamejs/lib/auth/auth-time-tracker.js +2 -0
- package/lib/vendor/blamejs/lib/auth/bot-challenge.js +2 -0
- package/lib/vendor/blamejs/lib/auth/ciba.js +2 -0
- package/lib/vendor/blamejs/lib/auth/dpop.js +2 -0
- package/lib/vendor/blamejs/lib/auth/elevation-grant.js +2 -0
- package/lib/vendor/blamejs/lib/auth/fal.js +2 -0
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +11 -7
- package/lib/vendor/blamejs/lib/auth/jar.js +4 -1
- package/lib/vendor/blamejs/lib/auth/jwt-external.js +16 -3
- package/lib/vendor/blamejs/lib/auth/jwt.js +2 -0
- package/lib/vendor/blamejs/lib/auth/lockout.js +237 -104
- package/lib/vendor/blamejs/lib/auth/oauth.js +98 -17
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +58 -17
- package/lib/vendor/blamejs/lib/auth/oid4vp.js +2 -0
- package/lib/vendor/blamejs/lib/auth/openid-federation.js +2 -0
- package/lib/vendor/blamejs/lib/auth/passkey.js +2 -0
- package/lib/vendor/blamejs/lib/auth/password.js +2 -0
- package/lib/vendor/blamejs/lib/auth/saml.js +68 -7
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc-disclosure.js +2 -0
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc-holder.js +2 -0
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc-issuer.js +2 -0
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +9 -0
- package/lib/vendor/blamejs/lib/auth/status-list.js +7 -0
- package/lib/vendor/blamejs/lib/auth/step-up-policy.js +2 -0
- package/lib/vendor/blamejs/lib/auth/step-up.js +2 -0
- package/lib/vendor/blamejs/lib/auth-bot-challenge.js +5 -8
- package/lib/vendor/blamejs/lib/auth-header.js +2 -0
- package/lib/vendor/blamejs/lib/backup/bundle.js +2 -0
- package/lib/vendor/blamejs/lib/backup/crypto.js +2 -0
- package/lib/vendor/blamejs/lib/backup/index.js +14 -0
- package/lib/vendor/blamejs/lib/backup/manifest.js +2 -0
- package/lib/vendor/blamejs/lib/base32.js +2 -0
- package/lib/vendor/blamejs/lib/boot-gates.js +2 -0
- package/lib/vendor/blamejs/lib/bounded-map.js +3 -1
- package/lib/vendor/blamejs/lib/breach-deadline.js +2 -0
- package/lib/vendor/blamejs/lib/break-glass.js +10 -9
- package/lib/vendor/blamejs/lib/budr.js +2 -0
- package/lib/vendor/blamejs/lib/bundler.js +2 -0
- package/lib/vendor/blamejs/lib/cache-redis.js +2 -0
- package/lib/vendor/blamejs/lib/cache-status.js +2 -0
- package/lib/vendor/blamejs/lib/cache.js +13 -5
- package/lib/vendor/blamejs/lib/calendar.js +2 -0
- package/lib/vendor/blamejs/lib/canonical-json.js +19 -3
- package/lib/vendor/blamejs/lib/cbor.js +2 -0
- package/lib/vendor/blamejs/lib/cdn-cache-control.js +2 -0
- package/lib/vendor/blamejs/lib/cert.js +2 -0
- package/lib/vendor/blamejs/lib/chain-writer.js +2 -0
- package/lib/vendor/blamejs/lib/circuit-breaker.js +2 -0
- package/lib/vendor/blamejs/lib/cli-helpers.js +2 -0
- package/lib/vendor/blamejs/lib/cli.js +57 -20
- package/lib/vendor/blamejs/lib/client-hints.js +2 -0
- package/lib/vendor/blamejs/lib/cloud-events.js +2 -0
- package/lib/vendor/blamejs/lib/cluster-provider-db.js +2 -0
- package/lib/vendor/blamejs/lib/cluster-storage.js +2 -0
- package/lib/vendor/blamejs/lib/cluster.js +2 -0
- package/lib/vendor/blamejs/lib/cms-codec.js +2 -0
- package/lib/vendor/blamejs/lib/codepoint-class.js +2 -0
- package/lib/vendor/blamejs/lib/compliance-ai-act-logging.js +2 -0
- package/lib/vendor/blamejs/lib/compliance-ai-act-prohibited.js +2 -0
- package/lib/vendor/blamejs/lib/compliance-ai-act-risk.js +2 -0
- package/lib/vendor/blamejs/lib/compliance-ai-act-transparency.js +2 -0
- package/lib/vendor/blamejs/lib/compliance-ai-act.js +2 -0
- package/lib/vendor/blamejs/lib/compliance-eaa.js +2 -0
- package/lib/vendor/blamejs/lib/compliance-sanctions-aliases.js +2 -0
- package/lib/vendor/blamejs/lib/compliance-sanctions-fetcher.js +2 -0
- package/lib/vendor/blamejs/lib/compliance-sanctions-fuzzy.js +2 -0
- package/lib/vendor/blamejs/lib/compliance-sanctions.js +2 -0
- package/lib/vendor/blamejs/lib/compliance.js +2 -0
- package/lib/vendor/blamejs/lib/config-drift.js +2 -0
- package/lib/vendor/blamejs/lib/config.js +2 -0
- package/lib/vendor/blamejs/lib/consent.js +2 -0
- package/lib/vendor/blamejs/lib/constants.js +2 -0
- package/lib/vendor/blamejs/lib/content-credentials.js +2 -0
- package/lib/vendor/blamejs/lib/content-digest.js +2 -0
- package/lib/vendor/blamejs/lib/cookies.js +2 -0
- package/lib/vendor/blamejs/lib/cose.js +2 -0
- package/lib/vendor/blamejs/lib/cra-report.js +2 -0
- package/lib/vendor/blamejs/lib/crdt.js +2 -0
- package/lib/vendor/blamejs/lib/credential-hash.js +2 -0
- package/lib/vendor/blamejs/lib/crypto-field.js +2 -0
- package/lib/vendor/blamejs/lib/crypto-hpke-pq.js +2 -0
- package/lib/vendor/blamejs/lib/crypto-hpke.js +2 -0
- package/lib/vendor/blamejs/lib/crypto-oprf.js +2 -0
- package/lib/vendor/blamejs/lib/crypto-xwing.js +2 -0
- package/lib/vendor/blamejs/lib/crypto.js +2 -0
- package/lib/vendor/blamejs/lib/csp.js +2 -0
- package/lib/vendor/blamejs/lib/csv.js +14 -1
- package/lib/vendor/blamejs/lib/cwt.js +2 -0
- package/lib/vendor/blamejs/lib/daemon.js +2 -0
- package/lib/vendor/blamejs/lib/dark-patterns.js +2 -0
- package/lib/vendor/blamejs/lib/data-act.js +2 -0
- package/lib/vendor/blamejs/lib/db-collection.js +2 -0
- package/lib/vendor/blamejs/lib/db-declare-row-policy.js +2 -0
- package/lib/vendor/blamejs/lib/db-declare-view.js +2 -0
- package/lib/vendor/blamejs/lib/db-file-lifecycle.js +2 -0
- package/lib/vendor/blamejs/lib/db-query.js +2 -0
- package/lib/vendor/blamejs/lib/db-role-context.js +2 -0
- package/lib/vendor/blamejs/lib/db-schema.js +2 -0
- package/lib/vendor/blamejs/lib/db.js +2 -0
- package/lib/vendor/blamejs/lib/dbsc.js +2 -0
- package/lib/vendor/blamejs/lib/ddl-change-control.js +2 -0
- package/lib/vendor/blamejs/lib/deprecate.js +2 -0
- package/lib/vendor/blamejs/lib/dev.js +2 -0
- package/lib/vendor/blamejs/lib/did.js +2 -0
- package/lib/vendor/blamejs/lib/dora.js +2 -0
- package/lib/vendor/blamejs/lib/dr-runbook.js +2 -0
- package/lib/vendor/blamejs/lib/dsa.js +2 -0
- package/lib/vendor/blamejs/lib/dsr.js +2 -0
- package/lib/vendor/blamejs/lib/dual-control.js +11 -15
- package/lib/vendor/blamejs/lib/early-hints.js +2 -0
- package/lib/vendor/blamejs/lib/eat.js +4 -1
- package/lib/vendor/blamejs/lib/error-page.js +2 -0
- package/lib/vendor/blamejs/lib/events.js +2 -0
- package/lib/vendor/blamejs/lib/external-db-migrate.js +2 -0
- package/lib/vendor/blamejs/lib/external-db.js +2 -0
- package/lib/vendor/blamejs/lib/fapi2.js +2 -0
- package/lib/vendor/blamejs/lib/fda-21cfr11.js +2 -0
- package/lib/vendor/blamejs/lib/fdx.js +2 -0
- package/lib/vendor/blamejs/lib/fedcm.js +2 -0
- package/lib/vendor/blamejs/lib/file-type.js +2 -0
- package/lib/vendor/blamejs/lib/file-upload.js +139 -21
- package/lib/vendor/blamejs/lib/flag-cache.js +2 -0
- package/lib/vendor/blamejs/lib/flag-evaluation-context.js +2 -0
- package/lib/vendor/blamejs/lib/flag-providers.js +2 -0
- package/lib/vendor/blamejs/lib/flag-targeting.js +4 -7
- package/lib/vendor/blamejs/lib/flag.js +2 -0
- package/lib/vendor/blamejs/lib/forms.js +11 -0
- package/lib/vendor/blamejs/lib/framework-error.js +2 -0
- package/lib/vendor/blamejs/lib/framework-files.js +2 -0
- package/lib/vendor/blamejs/lib/framework-schema.js +2 -0
- package/lib/vendor/blamejs/lib/framework-sha1-hibp.js +2 -0
- package/lib/vendor/blamejs/lib/fsm.js +2 -0
- package/lib/vendor/blamejs/lib/gate-contract.js +2 -0
- package/lib/vendor/blamejs/lib/gdpr-ropa.js +2 -0
- package/lib/vendor/blamejs/lib/graphql-federation.js +2 -0
- package/lib/vendor/blamejs/lib/guard-agent-registry.js +2 -0
- package/lib/vendor/blamejs/lib/guard-all.js +2 -0
- package/lib/vendor/blamejs/lib/guard-archive.js +2 -0
- package/lib/vendor/blamejs/lib/guard-auth.js +2 -0
- package/lib/vendor/blamejs/lib/guard-cidr.js +2 -0
- package/lib/vendor/blamejs/lib/guard-csv.js +2 -0
- package/lib/vendor/blamejs/lib/guard-domain.js +2 -0
- package/lib/vendor/blamejs/lib/guard-dsn.js +2 -0
- package/lib/vendor/blamejs/lib/guard-email.js +2 -0
- package/lib/vendor/blamejs/lib/guard-envelope.js +2 -0
- package/lib/vendor/blamejs/lib/guard-event-bus-payload.js +2 -0
- package/lib/vendor/blamejs/lib/guard-event-bus-topic.js +2 -0
- package/lib/vendor/blamejs/lib/guard-filename.js +2 -0
- package/lib/vendor/blamejs/lib/guard-graphql.js +2 -0
- package/lib/vendor/blamejs/lib/guard-html-wcag-aria.js +2 -0
- package/lib/vendor/blamejs/lib/guard-html-wcag-forms.js +2 -0
- package/lib/vendor/blamejs/lib/guard-html-wcag-tables.js +2 -0
- package/lib/vendor/blamejs/lib/guard-html-wcag-tagwalk.js +2 -0
- package/lib/vendor/blamejs/lib/guard-html-wcag.js +2 -0
- package/lib/vendor/blamejs/lib/guard-html.js +2 -0
- package/lib/vendor/blamejs/lib/guard-idempotency-key.js +2 -0
- package/lib/vendor/blamejs/lib/guard-image.js +2 -0
- package/lib/vendor/blamejs/lib/guard-imap-command.js +2 -0
- package/lib/vendor/blamejs/lib/guard-jmap.js +2 -0
- package/lib/vendor/blamejs/lib/guard-json.js +2 -0
- package/lib/vendor/blamejs/lib/guard-jsonpath.js +2 -0
- package/lib/vendor/blamejs/lib/guard-jwt.js +2 -0
- package/lib/vendor/blamejs/lib/guard-list-id.js +2 -0
- package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +2 -0
- package/lib/vendor/blamejs/lib/guard-mail-compose.js +2 -0
- package/lib/vendor/blamejs/lib/guard-mail-move.js +2 -0
- package/lib/vendor/blamejs/lib/guard-mail-query.js +2 -0
- package/lib/vendor/blamejs/lib/guard-mail-reply.js +2 -0
- package/lib/vendor/blamejs/lib/guard-mail-sieve.js +2 -0
- package/lib/vendor/blamejs/lib/guard-managesieve-command.js +2 -0
- package/lib/vendor/blamejs/lib/guard-markdown.js +2 -0
- package/lib/vendor/blamejs/lib/guard-message-id.js +2 -0
- package/lib/vendor/blamejs/lib/guard-mime.js +2 -0
- package/lib/vendor/blamejs/lib/guard-oauth.js +14 -1
- package/lib/vendor/blamejs/lib/guard-pdf.js +2 -0
- package/lib/vendor/blamejs/lib/guard-pop3-command.js +2 -0
- package/lib/vendor/blamejs/lib/guard-posture-chain.js +2 -0
- package/lib/vendor/blamejs/lib/guard-regex.js +59 -0
- package/lib/vendor/blamejs/lib/guard-saga-config.js +2 -0
- package/lib/vendor/blamejs/lib/guard-shell.js +2 -0
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +2 -0
- package/lib/vendor/blamejs/lib/guard-snapshot-envelope.js +2 -0
- package/lib/vendor/blamejs/lib/guard-sql.js +2 -0
- package/lib/vendor/blamejs/lib/guard-stream-args.js +2 -0
- package/lib/vendor/blamejs/lib/guard-svg.js +2 -0
- package/lib/vendor/blamejs/lib/guard-template.js +2 -0
- package/lib/vendor/blamejs/lib/guard-tenant-id.js +2 -0
- package/lib/vendor/blamejs/lib/guard-text.js +2 -0
- package/lib/vendor/blamejs/lib/guard-time.js +2 -0
- package/lib/vendor/blamejs/lib/guard-trace-context.js +2 -0
- package/lib/vendor/blamejs/lib/guard-uuid.js +2 -0
- package/lib/vendor/blamejs/lib/guard-xml.js +2 -0
- package/lib/vendor/blamejs/lib/guard-yaml.js +2 -0
- package/lib/vendor/blamejs/lib/hal.js +2 -0
- package/lib/vendor/blamejs/lib/handlers.js +2 -0
- package/lib/vendor/blamejs/lib/honeytoken.js +2 -0
- package/lib/vendor/blamejs/lib/html-balance.js +2 -0
- package/lib/vendor/blamejs/lib/http-client-cache.js +2 -0
- package/lib/vendor/blamejs/lib/http-client-cookie-jar.js +2 -0
- package/lib/vendor/blamejs/lib/http-client.js +2 -0
- package/lib/vendor/blamejs/lib/http-message-signature.js +144 -11
- package/lib/vendor/blamejs/lib/http2-teardown.js +2 -0
- package/lib/vendor/blamejs/lib/i18n-messageformat.js +35 -6
- package/lib/vendor/blamejs/lib/i18n.js +2 -0
- package/lib/vendor/blamejs/lib/iab-mspa.js +2 -0
- package/lib/vendor/blamejs/lib/iab-tcf.js +2 -0
- package/lib/vendor/blamejs/lib/importmap-integrity.js +2 -0
- package/lib/vendor/blamejs/lib/inbox.js +2 -0
- package/lib/vendor/blamejs/lib/incident-report.js +2 -0
- package/lib/vendor/blamejs/lib/ip-utils.js +2 -0
- package/lib/vendor/blamejs/lib/jobs.js +2 -0
- package/lib/vendor/blamejs/lib/jose-jwe-experimental.js +2 -0
- package/lib/vendor/blamejs/lib/json-merge-patch.js +2 -0
- package/lib/vendor/blamejs/lib/json-patch.js +2 -0
- package/lib/vendor/blamejs/lib/json-path.js +2 -0
- package/lib/vendor/blamejs/lib/json-pointer.js +2 -0
- package/lib/vendor/blamejs/lib/json-schema.js +13 -1
- package/lib/vendor/blamejs/lib/jsonapi.js +2 -0
- package/lib/vendor/blamejs/lib/jtd.js +2 -0
- package/lib/vendor/blamejs/lib/jwk.js +2 -0
- package/lib/vendor/blamejs/lib/keychain.js +32 -10
- package/lib/vendor/blamejs/lib/lazy-require.js +2 -0
- package/lib/vendor/blamejs/lib/legal-hold.js +2 -0
- package/lib/vendor/blamejs/lib/link-header.js +2 -0
- package/lib/vendor/blamejs/lib/local-db-thin.js +2 -0
- package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +2 -0
- package/lib/vendor/blamejs/lib/log-stream-local.js +2 -0
- package/lib/vendor/blamejs/lib/log-stream-otlp-grpc.js +2 -0
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +2 -0
- package/lib/vendor/blamejs/lib/log-stream-syslog.js +2 -0
- package/lib/vendor/blamejs/lib/log-stream-webhook.js +2 -0
- package/lib/vendor/blamejs/lib/log-stream.js +2 -0
- package/lib/vendor/blamejs/lib/log.js +2 -0
- package/lib/vendor/blamejs/lib/lro.js +2 -0
- package/lib/vendor/blamejs/lib/mail-agent.js +2 -0
- package/lib/vendor/blamejs/lib/mail-arc-reuse-token.js +20 -0
- package/lib/vendor/blamejs/lib/mail-arc-sign.js +32 -14
- package/lib/vendor/blamejs/lib/mail-arf.js +4 -0
- package/lib/vendor/blamejs/lib/mail-auth.js +93 -19
- package/lib/vendor/blamejs/lib/mail-bimi.js +26 -10
- package/lib/vendor/blamejs/lib/mail-bounce.js +23 -6
- package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +2 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +2 -0
- package/lib/vendor/blamejs/lib/mail-crypto.js +2 -0
- package/lib/vendor/blamejs/lib/mail-dav.js +2 -0
- package/lib/vendor/blamejs/lib/mail-deploy.js +2 -0
- package/lib/vendor/blamejs/lib/mail-dkim.js +44 -5
- package/lib/vendor/blamejs/lib/mail-greylist.js +2 -0
- package/lib/vendor/blamejs/lib/mail-helo.js +16 -0
- package/lib/vendor/blamejs/lib/mail-journal.js +2 -0
- package/lib/vendor/blamejs/lib/mail-mdn.js +17 -0
- package/lib/vendor/blamejs/lib/mail-rbl.js +2 -0
- package/lib/vendor/blamejs/lib/mail-require-tls.js +2 -0
- package/lib/vendor/blamejs/lib/mail-scan.js +2 -0
- package/lib/vendor/blamejs/lib/mail-send-deliver.js +32 -7
- package/lib/vendor/blamejs/lib/mail-server-imap.js +2 -0
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +23 -11
- package/lib/vendor/blamejs/lib/mail-server-managesieve.js +2 -0
- package/lib/vendor/blamejs/lib/mail-server-mx.js +2 -0
- package/lib/vendor/blamejs/lib/mail-server-net.js +2 -0
- package/lib/vendor/blamejs/lib/mail-server-pop3.js +2 -0
- package/lib/vendor/blamejs/lib/mail-server-rate-limit.js +2 -0
- package/lib/vendor/blamejs/lib/mail-server-registry.js +2 -0
- package/lib/vendor/blamejs/lib/mail-server-submission.js +2 -0
- package/lib/vendor/blamejs/lib/mail-server-tls.js +2 -0
- package/lib/vendor/blamejs/lib/mail-sieve.js +2 -0
- package/lib/vendor/blamejs/lib/mail-spam-score.js +2 -0
- package/lib/vendor/blamejs/lib/mail-srs.js +8 -0
- package/lib/vendor/blamejs/lib/mail-store-fts.js +2 -0
- package/lib/vendor/blamejs/lib/mail-store.js +2 -0
- package/lib/vendor/blamejs/lib/mail-unsubscribe.js +2 -0
- package/lib/vendor/blamejs/lib/mail.js +11 -0
- package/lib/vendor/blamejs/lib/markup-escape.js +2 -0
- package/lib/vendor/blamejs/lib/markup-tokenizer.js +2 -0
- package/lib/vendor/blamejs/lib/mcp-tool-registry.js +2 -0
- package/lib/vendor/blamejs/lib/mcp.js +4 -2
- package/lib/vendor/blamejs/lib/mdoc.js +2 -0
- package/lib/vendor/blamejs/lib/metrics.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/age-gate.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/ai-act-disclosure.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/assetlinks.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/attach-user.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/bearer-auth.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/bot-disclose.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/bot-guard.js +10 -1
- package/lib/vendor/blamejs/lib/middleware/clear-site-data.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/compose-pipeline.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/compression.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/cookies.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/cors.js +7 -0
- package/lib/vendor/blamejs/lib/middleware/csp-nonce.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/csp-report.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +12 -5
- package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/db-role-for.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/deny-response.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/dpop.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/error-handler.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/flag-context.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/gpc.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/headers.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/health.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/host-allowlist.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +30 -2
- package/lib/vendor/blamejs/lib/middleware/index.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/nel.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/network-allowlist.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/no-cache.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/protected-resource-metadata.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/request-id.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/request-log.js +6 -0
- package/lib/vendor/blamejs/lib/middleware/require-aal.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/require-auth.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/require-content-type.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/require-methods.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/require-mtls.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/require-step-up.js +42 -1
- package/lib/vendor/blamejs/lib/middleware/scim-server.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/security-headers.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/security-txt.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/span-http-server.js +12 -0
- package/lib/vendor/blamejs/lib/middleware/speculation-rules.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/sse.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/trace-log-correlation.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/trace-propagate.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/tus-upload.js +2 -0
- package/lib/vendor/blamejs/lib/middleware/web-app-manifest.js +2 -0
- package/lib/vendor/blamejs/lib/migration-files.js +2 -0
- package/lib/vendor/blamejs/lib/migrations.js +2 -0
- package/lib/vendor/blamejs/lib/mime-parse.js +5 -1
- package/lib/vendor/blamejs/lib/module-loader.js +2 -0
- package/lib/vendor/blamejs/lib/money.js +2 -0
- package/lib/vendor/blamejs/lib/mtls-ca.js +2 -0
- package/lib/vendor/blamejs/lib/mtls-engine-default.js +2 -0
- package/lib/vendor/blamejs/lib/network-byte-quota.js +76 -14
- package/lib/vendor/blamejs/lib/network-dane.js +2 -0
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +55 -18
- package/lib/vendor/blamejs/lib/network-dns.js +2 -0
- package/lib/vendor/blamejs/lib/network-dnssec.js +15 -2
- package/lib/vendor/blamejs/lib/network-heartbeat.js +2 -0
- package/lib/vendor/blamejs/lib/network-nts.js +2 -0
- package/lib/vendor/blamejs/lib/network-proxy.js +2 -0
- package/lib/vendor/blamejs/lib/network-smtp-policy.js +6 -1
- package/lib/vendor/blamejs/lib/network-tls.js +99 -5
- package/lib/vendor/blamejs/lib/network-tsig.js +13 -1
- package/lib/vendor/blamejs/lib/network.js +2 -0
- package/lib/vendor/blamejs/lib/nis2-report.js +2 -0
- package/lib/vendor/blamejs/lib/nist-crosswalk.js +2 -0
- package/lib/vendor/blamejs/lib/nonce-store.js +2 -0
- package/lib/vendor/blamejs/lib/notify.js +2 -0
- package/lib/vendor/blamejs/lib/ntp-check.js +2 -0
- package/lib/vendor/blamejs/lib/numeric-bounds.js +2 -0
- package/lib/vendor/blamejs/lib/numeric-checks.js +2 -0
- package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +2 -0
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +2 -0
- package/lib/vendor/blamejs/lib/object-store/gcs-bucket-ops.js +2 -0
- package/lib/vendor/blamejs/lib/object-store/gcs.js +2 -0
- package/lib/vendor/blamejs/lib/object-store/http-put.js +2 -0
- package/lib/vendor/blamejs/lib/object-store/http-request.js +2 -0
- package/lib/vendor/blamejs/lib/object-store/index.js +2 -0
- package/lib/vendor/blamejs/lib/object-store/local.js +2 -0
- package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +2 -0
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +2 -0
- package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +4 -2
- package/lib/vendor/blamejs/lib/observability-tracer.js +2 -0
- package/lib/vendor/blamejs/lib/observability.js +2 -0
- package/lib/vendor/blamejs/lib/openapi-paths-builder.js +2 -0
- package/lib/vendor/blamejs/lib/openapi-schema-walk.js +2 -0
- package/lib/vendor/blamejs/lib/openapi-security.js +2 -0
- package/lib/vendor/blamejs/lib/openapi-yaml.js +2 -0
- package/lib/vendor/blamejs/lib/openapi.js +2 -0
- package/lib/vendor/blamejs/lib/otel-export.js +2 -0
- package/lib/vendor/blamejs/lib/outbox.js +2 -0
- package/lib/vendor/blamejs/lib/pagination.js +2 -0
- package/lib/vendor/blamejs/lib/parsers/index.js +2 -0
- package/lib/vendor/blamejs/lib/parsers/safe-env.js +2 -0
- package/lib/vendor/blamejs/lib/parsers/safe-ini.js +2 -0
- package/lib/vendor/blamejs/lib/parsers/safe-toml.js +2 -0
- package/lib/vendor/blamejs/lib/parsers/safe-xml.js +2 -0
- package/lib/vendor/blamejs/lib/parsers/safe-yaml.js +2 -0
- package/lib/vendor/blamejs/lib/permissions.js +2 -0
- package/lib/vendor/blamejs/lib/pick.js +2 -0
- package/lib/vendor/blamejs/lib/pipl-cn.js +2 -0
- package/lib/vendor/blamejs/lib/pqc-agent.js +2 -0
- package/lib/vendor/blamejs/lib/pqc-gate.js +2 -0
- package/lib/vendor/blamejs/lib/pqc-software.js +2 -0
- package/lib/vendor/blamejs/lib/privacy-pass.js +2 -0
- package/lib/vendor/blamejs/lib/privacy.js +2 -0
- package/lib/vendor/blamejs/lib/problem-details.js +2 -0
- package/lib/vendor/blamejs/lib/process-spawn.js +2 -0
- package/lib/vendor/blamejs/lib/promise-pool.js +2 -0
- package/lib/vendor/blamejs/lib/protobuf-encoder.js +2 -0
- package/lib/vendor/blamejs/lib/protocol-dispatcher.js +2 -0
- package/lib/vendor/blamejs/lib/public-suffix.js +45 -5
- package/lib/vendor/blamejs/lib/pubsub-cluster.js +2 -0
- package/lib/vendor/blamejs/lib/pubsub-redis.js +2 -0
- package/lib/vendor/blamejs/lib/pubsub.js +2 -0
- package/lib/vendor/blamejs/lib/queue-local.js +28 -11
- package/lib/vendor/blamejs/lib/queue-redis.js +79 -17
- package/lib/vendor/blamejs/lib/queue-sqs.js +2 -0
- package/lib/vendor/blamejs/lib/queue.js +5 -3
- package/lib/vendor/blamejs/lib/redact.js +2 -0
- package/lib/vendor/blamejs/lib/redis-client.js +30 -5
- package/lib/vendor/blamejs/lib/render.js +2 -0
- package/lib/vendor/blamejs/lib/request-helpers.js +11 -0
- package/lib/vendor/blamejs/lib/resource-access-lock.js +2 -0
- package/lib/vendor/blamejs/lib/restore-bundle.js +2 -0
- package/lib/vendor/blamejs/lib/restore-rollback.js +2 -0
- package/lib/vendor/blamejs/lib/restore.js +2 -0
- package/lib/vendor/blamejs/lib/retention.js +2 -0
- package/lib/vendor/blamejs/lib/retry.js +2 -0
- package/lib/vendor/blamejs/lib/rfc3339.js +2 -0
- package/lib/vendor/blamejs/lib/router.js +109 -19
- package/lib/vendor/blamejs/lib/safe-archive.js +2 -0
- package/lib/vendor/blamejs/lib/safe-async.js +44 -0
- package/lib/vendor/blamejs/lib/safe-buffer.js +66 -0
- package/lib/vendor/blamejs/lib/safe-decompress.js +2 -0
- package/lib/vendor/blamejs/lib/safe-dns.js +2 -0
- package/lib/vendor/blamejs/lib/safe-ical.js +2 -0
- package/lib/vendor/blamejs/lib/safe-icap.js +7 -0
- package/lib/vendor/blamejs/lib/safe-json.js +2 -0
- package/lib/vendor/blamejs/lib/safe-jsonpath.js +2 -0
- package/lib/vendor/blamejs/lib/safe-mime.js +2 -0
- package/lib/vendor/blamejs/lib/safe-mount-info.js +2 -0
- package/lib/vendor/blamejs/lib/safe-path.js +2 -0
- package/lib/vendor/blamejs/lib/safe-redirect.js +2 -0
- package/lib/vendor/blamejs/lib/safe-schema.js +2 -0
- package/lib/vendor/blamejs/lib/safe-sieve.js +2 -0
- package/lib/vendor/blamejs/lib/safe-smtp.js +2 -0
- package/lib/vendor/blamejs/lib/safe-sql.js +2 -0
- package/lib/vendor/blamejs/lib/safe-url.js +2 -0
- package/lib/vendor/blamejs/lib/safe-vcard.js +2 -0
- package/lib/vendor/blamejs/lib/sandbox-worker.js +2 -0
- package/lib/vendor/blamejs/lib/sandbox.js +2 -0
- package/lib/vendor/blamejs/lib/scheduler.js +2 -0
- package/lib/vendor/blamejs/lib/scitt.js +2 -0
- package/lib/vendor/blamejs/lib/sd-notify.js +2 -0
- package/lib/vendor/blamejs/lib/sec-cyber.js +2 -0
- package/lib/vendor/blamejs/lib/security-assert.js +2 -0
- package/lib/vendor/blamejs/lib/seeders.js +2 -0
- package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +2 -0
- package/lib/vendor/blamejs/lib/self-update.js +104 -56
- package/lib/vendor/blamejs/lib/server-timing.js +2 -0
- package/lib/vendor/blamejs/lib/session-device-binding.js +2 -0
- package/lib/vendor/blamejs/lib/session-stores.js +2 -0
- package/lib/vendor/blamejs/lib/session.js +71 -32
- package/lib/vendor/blamejs/lib/slug.js +2 -0
- package/lib/vendor/blamejs/lib/sql.js +2 -0
- package/lib/vendor/blamejs/lib/sse.js +2 -0
- package/lib/vendor/blamejs/lib/ssrf-guard.js +9 -1
- package/lib/vendor/blamejs/lib/standard-webhooks.js +2 -0
- package/lib/vendor/blamejs/lib/static.js +54 -20
- package/lib/vendor/blamejs/lib/storage.js +2 -0
- package/lib/vendor/blamejs/lib/stream-throttle.js +2 -0
- package/lib/vendor/blamejs/lib/structured-fields.js +2 -0
- package/lib/vendor/blamejs/lib/subject.js +2 -0
- package/lib/vendor/blamejs/lib/tcpa-10dlc.js +2 -0
- package/lib/vendor/blamejs/lib/template.js +2 -0
- package/lib/vendor/blamejs/lib/tenant-quota.js +2 -0
- package/lib/vendor/blamejs/lib/test-harness.js +2 -0
- package/lib/vendor/blamejs/lib/testing.js +2 -0
- package/lib/vendor/blamejs/lib/time.js +2 -0
- package/lib/vendor/blamejs/lib/tls-exporter.js +2 -0
- package/lib/vendor/blamejs/lib/totp.js +2 -0
- package/lib/vendor/blamejs/lib/tracing.js +2 -0
- package/lib/vendor/blamejs/lib/tsa.js +2 -0
- package/lib/vendor/blamejs/lib/uri-template.js +2 -0
- package/lib/vendor/blamejs/lib/uuid.js +2 -0
- package/lib/vendor/blamejs/lib/validate-opts.js +2 -0
- package/lib/vendor/blamejs/lib/vault/index.js +2 -0
- package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +2 -0
- package/lib/vendor/blamejs/lib/vault/passphrase-source.js +2 -0
- package/lib/vendor/blamejs/lib/vault/rotate.js +2 -0
- package/lib/vendor/blamejs/lib/vault/seal-pem-file.js +2 -0
- package/lib/vendor/blamejs/lib/vault/wrap.js +2 -0
- package/lib/vendor/blamejs/lib/vault-aad.js +2 -0
- package/lib/vendor/blamejs/lib/vc.js +31 -0
- package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +10 -10
- package/lib/vendor/blamejs/lib/vendor/public-suffix-list.dat +2 -4
- package/lib/vendor/blamejs/lib/vendor/public-suffix-list.data.js +745 -745
- package/lib/vendor/blamejs/lib/vendor-data.js +2 -0
- package/lib/vendor/blamejs/lib/vex.js +2 -0
- package/lib/vendor/blamejs/lib/watcher.js +2 -0
- package/lib/vendor/blamejs/lib/web-push-vapid.js +2 -0
- package/lib/vendor/blamejs/lib/webhook-dispatcher.js +33 -5
- package/lib/vendor/blamejs/lib/webhook.js +2 -0
- package/lib/vendor/blamejs/lib/websocket-channels.js +2 -0
- package/lib/vendor/blamejs/lib/websocket.js +2 -0
- package/lib/vendor/blamejs/lib/wiki-concepts.js +2 -0
- package/lib/vendor/blamejs/lib/worker-pool.js +2 -0
- package/lib/vendor/blamejs/lib/worm.js +2 -0
- package/lib/vendor/blamejs/lib/ws-client.js +2 -0
- package/lib/vendor/blamejs/lib/x509-chain.js +2 -0
- package/lib/vendor/blamejs/lib/xml-c14n.js +10 -7
- package/lib/vendor/blamejs/oss-fuzz/projects/blamejs/Dockerfile +7 -6
- package/lib/vendor/blamejs/package-lock.json +533 -0
- package/lib/vendor/blamejs/package.json +3 -3
- package/lib/vendor/blamejs/release-notes/v0.15.x.json +2284 -0
- package/lib/vendor/blamejs/release-notes/v0.16.0.json +44 -0
- package/lib/vendor/blamejs/release-notes/v0.16.1.json +36 -0
- package/lib/vendor/blamejs/release-notes/v0.16.2.json +71 -0
- package/lib/vendor/blamejs/scripts/build-vendored-sbom.js +2 -0
- package/lib/vendor/blamejs/scripts/check-actions-currency.js +113 -1
- package/lib/vendor/blamejs/scripts/check-api-snapshot.js +2 -0
- package/lib/vendor/blamejs/scripts/check-changelog-extract.js +2 -0
- package/lib/vendor/blamejs/scripts/check-esbuild-pin.js +33 -40
- package/lib/vendor/blamejs/scripts/check-pack-against-gitignore.js +2 -0
- package/lib/vendor/blamejs/scripts/check-services.js +2 -0
- package/lib/vendor/blamejs/scripts/check-vendor-currency.js +2 -0
- package/lib/vendor/blamejs/scripts/consolidate-release-notes.js +2 -0
- package/lib/vendor/blamejs/scripts/gen-migrating.js +2 -0
- package/lib/vendor/blamejs/scripts/generate-changelog-entry.js +2 -0
- package/lib/vendor/blamejs/scripts/generate-release-signing-key.js +2 -0
- package/lib/vendor/blamejs/scripts/generate-ssdf-attestation.js +2 -0
- package/lib/vendor/blamejs/scripts/pin-all.js +215 -0
- package/lib/vendor/blamejs/scripts/refresh-api-snapshot.js +2 -0
- package/lib/vendor/blamejs/scripts/refresh-vendor-manifest.js +2 -0
- package/lib/vendor/blamejs/scripts/release.js +5 -1
- package/lib/vendor/blamejs/scripts/sha3-digest.js +2 -0
- package/lib/vendor/blamejs/scripts/sign-release-artifact.js +2 -0
- package/lib/vendor/blamejs/scripts/test-integration.js +2 -0
- package/lib/vendor/blamejs/scripts/test-wiki-integration.js +2 -0
- package/lib/vendor/blamejs/scripts/validate-source-comment-blocks.js +2 -0
- package/lib/vendor/blamejs/scripts/vendor-data-gen.js +2 -0
- package/lib/vendor/blamejs/scripts/vendor-data-keygen.js +2 -0
- package/lib/vendor/blamejs/test/00-primitives.js +35 -1
- package/lib/vendor/blamejs/test/10-state.js +2 -0
- package/lib/vendor/blamejs/test/20-db.js +2 -0
- package/lib/vendor/blamejs/test/30-chain.js +2 -0
- package/lib/vendor/blamejs/test/40-consumers.js +2 -0
- package/lib/vendor/blamejs/test/50-integration.js +2 -0
- package/lib/vendor/blamejs/test/_helpers.js +2 -0
- package/lib/vendor/blamejs/test/_smoke-worker.js +2 -0
- package/lib/vendor/blamejs/test/fixtures/worker-pool/echo.js +2 -0
- package/lib/vendor/blamejs/test/helpers/_codebase-shingle-worker.js +2 -0
- package/lib/vendor/blamejs/test/helpers/_codebase-shingle.js +2 -0
- package/lib/vendor/blamejs/test/helpers/_shape-match.js +2 -0
- package/lib/vendor/blamejs/test/helpers/check.js +2 -0
- package/lib/vendor/blamejs/test/helpers/cluster.js +2 -0
- package/lib/vendor/blamejs/test/helpers/db.js +2 -0
- package/lib/vendor/blamejs/test/helpers/drivers.js +2 -0
- package/lib/vendor/blamejs/test/helpers/fs-watch.js +2 -0
- package/lib/vendor/blamejs/test/helpers/http.js +2 -0
- package/lib/vendor/blamejs/test/helpers/index.js +2 -0
- package/lib/vendor/blamejs/test/helpers/json-round-trip.js +2 -0
- package/lib/vendor/blamejs/test/helpers/mocks.js +2 -0
- package/lib/vendor/blamejs/test/helpers/otel.js +2 -0
- package/lib/vendor/blamejs/test/helpers/services.js +2 -0
- package/lib/vendor/blamejs/test/helpers/wait.js +2 -0
- package/lib/vendor/blamejs/test/integration/audit-actor-binding-pg.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/audit-chain-external-db.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/audit-stack-mysql.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/audit-stack-postgres.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/backup-restore-objectstore.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/cache.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/cluster-provider-mysql.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/data-layer-cluster-mysql.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/data-layer-cluster-pg.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/data-layer-mysql-privacy.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/data-layer-mysql.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/data-layer-pg.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/data-layer-postgres.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/db-layer-mysql.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/db-layer-postgres.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/distributed-scheduler-fencing-pg.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/external-db-postgres.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/federation-auth.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/framework-schema-mysql.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/http-client.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/log-stream-cloudwatch.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/log-stream.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/mail-dkim.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/mail-smtp.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/mtls-ca.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/network-dns.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/network-heartbeat.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/ntp-check.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/object-store-azure.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/object-store-gcs.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/object-store-sigv4.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/pqc-pkcs8-forward-compat.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/pubsub.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/queue-cluster-mysql.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/queue-cluster-pg.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/queue-redis.test.js +115 -0
- package/lib/vendor/blamejs/test/integration/queue-sqs.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/redis-client-tls.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/redis-reconnect-toxiproxy.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/sql-fts5-catalog-sqlite.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/ssrf-guard.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/tls-classical-downgrade-audit.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/webhook-dispatcher-pg.test.js +15 -0
- package/lib/vendor/blamejs/test/integration/websocket-permessage-deflate.test.js +2 -0
- package/lib/vendor/blamejs/test/integration/ws-client-roundtrip.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/a2a-tasks.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/a2a.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/access-lock.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/acme-coverage.test.js +441 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/acme-failclosed.test.js +131 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/acme.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/age-gate.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-event-bus.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-idempotency.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +23 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-posture-chain.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-saga.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-snapshot.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-stream.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-tenant.test.js +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-trace.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-adverse-decision.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-aedt-bias-audit.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-capability.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-content-detect.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-disclosure-apply-all.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-disclosure.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-dp.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-frontier-protocol.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-input.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-model-manifest.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-output.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-pref.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-prompt.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-quota.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt-rejection-envelope.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-gz.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-sniff-envelope.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-tar-hardening.test.js +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-tar.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-wrap-passphrase.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-wrap.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-zip-stream.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive.test.js +4 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/arg-parser.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/asn1-der.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/asyncapi.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-conflict-path.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-exclusive-temp.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-fd-read-errorfor-bypass.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-fd-read.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-open-append-nofollow.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-open-nofollow.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-rename-retry.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-write-excl.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-write-stream.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/attach-user-bearer-scheme.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-chain-corrupted-anchor.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-chain-incremental-verify.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-checkpoint-false-rollback.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-cve-defensive.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-daily-review.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-export-cadf.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-framework-namespaces.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-query-self-log.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-safeemit-redacts-secrets.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-segregation.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-sign-anchor.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-sign-ml-dsa-65.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-signing-key-rotation.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-tools-dual-control.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-tools-return-bytes.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-use-store.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-verifybundle-tamper.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-bot-challenge-verifier.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-bot-challenge.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-jar.test.js +9 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +56 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-lockout.test.js +121 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-oauth-coverage.test.js +482 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-oauth-failclosed.test.js +257 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-password-audit.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-saml-coverage.test.js +671 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-saml-failclosed.test.js +179 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-status-list.test.js +19 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/azure-blob-bucket-ops.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/azure-blob-key-encoding.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-bundle-info.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-clone-bundle.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-find-bundles.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-index-coverage.test.js +690 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-index-failclosed.test.js +103 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-key-rotation.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-manifest-signature.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-object-store-adapter.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-residency-posture.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-rewrap-all.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-rewrap-bundle.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-scheduletest-drill.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-verify-all-bundles.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-verify-bundle.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-worker.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/base32.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/bearer-auth.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-chunked-malformed.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-error-redaction.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-smuggling.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/boot-gates.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/bot-guard.test.js +12 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/bounded-map.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/breach-deadline.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +46 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/budr.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/bundler-engine.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cache-status.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cache.test.js +12 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/canonical-json-jcs.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/canonical-json.test.js +28 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cbor.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cdn-cache-control.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cert.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/chain-writer-multichain.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ciba-authreqid-binding.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/clear-site-data.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-api-key.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-audit-verify-chain.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-backup.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-config-drift.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-coverage.test.js +238 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-erase.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-file-type.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-helpers.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-mtls.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-password.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-restore.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-retention.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-security.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-vault.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/client-hints.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cloud-events.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cluster-storage.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cluster-vault-rotation.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cms-codec.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +791 -13
- package/lib/vendor/blamejs/test/layer-0-primitives/codepoint-class.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-cascade.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-eaa.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-eu-ai-act-posture.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-sanctions.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compression-range.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/config-drift.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/config.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/consent-purposes.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/content-digest.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cors.test.js +12 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cose.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cra-report.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crdt.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/credential-hash.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-base64url.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-envelope.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-aad-downgrade.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-dual-read-migrate.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-per-row-key.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-unseal-rate-cap.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-upgrade-dialect.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-hash-files-parallel.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-hash-stream.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-hpke-pq.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-hpke.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-interop-oracles.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-mlkem768-x25519.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-namespace-hash.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-oprf.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-random-int.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-self-test.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-xwing.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csp-builder.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csp-nonce.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csrf-protect.test.js +40 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csv.test.js +11 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cwt.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/daemon.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/daily-byte-quota.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dane.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dark-patterns.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/data-act.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-collection-extensions.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-collection.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-init-extensions.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-key-aad.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-cross-schema.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-extensions.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-sealed-field-in.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-role-for.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-reconcile-emittable.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-transaction.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-stream-and-payload-shape.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-vacuum.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-worm.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ddl-change-control.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/declare-row-policy.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/declare-view.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/defineguard-default-gate-posture-caps.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/defineguard-resolve-opts.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/deny-response.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/did.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dns-dnssec-algorithm.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dns-null-mx.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dnssec.test.js +37 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dora.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dpop-alg-kty.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dpop-htu-peergating.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-replaystore-required.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dr-runbook.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsa.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsr-state-rules.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dual-control.test.js +28 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/early-hints.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/eat.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/erase-posture-vacuum.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/events.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/exploit-replay.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-migrate.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-non-atomic-backend.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-routing.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fapi2.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fda-21cfr11.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fdx.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fedcm-dbsc.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +124 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3-cert-bad-validity.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/file-type.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/file-upload-content-safety-skip-audit.test.js +66 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/file-upload-ownership.test.js +214 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/flag.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/forensic-snapshot.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fsm.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/gate-contract-content-gate.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/gcs-bucket-ops.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/gdpr-ropa.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-agent-registry.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-all.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-archive.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-csv.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-dsn.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-email.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-envelope.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-event-bus-payload.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-event-bus-topic.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-filename.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-gate-disposition-coverage.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-html-wcag.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-html.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-idempotency-key.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-image.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-imap-command.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-jmap.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-json.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-list-id.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-list-unsubscribe.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-mail-compose.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-mail-move.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-mail-query.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-mail-reply.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-mail-sieve.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-managesieve-command.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-markdown.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-message-id.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-oauth-replay-failopen.test.js +57 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-pdf.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-pop3-command.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-posture-chain.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-saga-config.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-smtp-command.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-snapshot-envelope.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-stream-args.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-svg.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-tenant-id.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-text.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-trace-context.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-xml.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-yaml.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/hal.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/honeytoken.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/html-balance.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-cache.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-throttle-transform.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-message-signature.test.js +315 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/i18n-messageformat.test.js +26 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/i18n.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/iab-mspa.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/iab-tcf.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/idempotency-key.test.js +40 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/importmap-integrity.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/inbox.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/incident-report.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ip-utils.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/jose-jwe-experimental.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/json-api.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/json-merge-patch.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/json-patch.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/json-path.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/json-round-trip-helper.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/json-schema.test.js +26 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/jtd.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/jwk.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/jwt-external.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/keychain-coverage.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/keychain-failclosed.test.js +185 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/keychain.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/legal-hold.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/link-header.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/local-db-thin.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/log-stream-cloudwatch.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/log-stream-otlp-grpc.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/log-stream-otlp.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/lro.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-agent.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-arf.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-coverage.test.js +437 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-dmarc-policy-failclosed.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-failclosed.test.js +144 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +289 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi-cert-validity-unparseable.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-bounce.test.js +61 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-canspam.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-pgp-experimental.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-pgp.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime-bad-validity.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-deploy-tlsrpt.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-deploy.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim-numericdate-failclosed.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim.test.js +130 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-feedback-id.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-greylist.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-header-injection.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-helo.test.js +17 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-journal.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-mdn.test.js +29 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-proxy-framing-bounds.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-rbl.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-require-tls.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-scan.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +113 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-imap.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-jmap.test.js +50 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-managesieve.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-pop3.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-rate-limit.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-registry.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-submission.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-tls.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-sieve.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-spam-score.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-srs.test.js +21 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-store-fts.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-store.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-unsubscribe.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mcp-tool-registry.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mdoc.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/metrics-shadow-registry.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/metrics-snapshot.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/middleware-compose-pipeline.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mime-parse.test.js +11 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/money.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mtls-ca-paths.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mtls-ca-revocation.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/nel.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-allowlist.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-byte-quota.test.js +65 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-lookup-timeout-default.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver-timeout.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver.test.js +28 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-heartbeat-passive.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-nts-handshake-byte-cap.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-smtp-policy-coverage.test.js +565 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-tls-build-options.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-tls-ct-inclusion.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-tls.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-tsig.test.js +26 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/nis2-report.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/nist-crosswalk.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/no-cache.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/nonce-store-release.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/notify.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/numeric-bounds.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/object-store-range-header.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/object-store-versioned-delete.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/observability-tracing.test.js +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/observability.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/openapi.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otel-export.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +8 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/outbox-inflight-reaper.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/output-header-hardening.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pagination.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/parser-verify-hardening.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/parsers-standalone.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/passkey-real-vectors.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/passkey.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/permissions.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pipl-cn.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pqc-agent-curve.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pqc-software.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/privacy-pass.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/privacy-vendor-review.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/problem-details.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/process-spawn.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/promise-pool.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/protected-resource-metadata.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/proto-shadow-allowlist.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/protobuf-encoder.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/protocol-dispatcher.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/public-suffix.test.js +29 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pubsub.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-dlq-extend-lease.test.js +44 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-flow-repeat.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-priority-rate-progress.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-sqs.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-cluster.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-memory-getorinsert.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-registry.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-xff-spoofing.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redact-dlp.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +16 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-helpers.test.js +7 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-id-async-context.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-log.test.js +3 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/require-auth-cache-control.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/require-mtls.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/resource-access-lock.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/restore-blob-remap.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retention-dryrun-no-vacuum.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retention-floor.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retention-sweep-termination.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retry.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router-body-validation.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router-coverage.test.js +592 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router-cross-origin-redirect.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router-failclosed.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router-tls0rtt.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router-use-path-scope.test.js +59 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-archive-auto-unwrap.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-archive-inspect-unwrap.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-async-loops.test.js +41 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-async-parallel.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-buffer-linear-scans.test.js +13 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-decompress.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-dns.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-icap.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-json-stringify-for-script.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-jsonpath.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-mime.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-mount-info.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-path.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-redirect.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-sieve.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-smtp.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-canonicalize.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-idn-homograph.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-vcard.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-xml.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-mdq-wrapping.test.js +237 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-slo.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notbefore.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-exactly-once.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-watchdog-stale-settle.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scitt.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc-ecdsa-p1363.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-notify.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sec-cyber.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/security-assert.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/security-txt.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/seeders.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/self-update-poll-asset-digest.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/self-update-standalone-verifier-ecdsa-encoding.test.js +9 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/self-update-standalone-verifier.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/self-update.test.js +87 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/server-timing.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-binding-unreadable-failclosed.test.js +80 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-destroy-all-store-backed.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding-ipv6-canonical-and-no-store.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-valid-from.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/shape-match.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-multipart-sse.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/slug.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/smtp-policy.test.js +5 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/source-comment-blocks.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/speculation-rules.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sql-coverage.test.js +422 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sse-backpressure.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sse.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ssrf-guard.test.js +23 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/standard-webhooks.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +33 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/step-up.test.js +44 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/storage-chunk-scratch.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/storage-presigned-url.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/stream-throttle.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/structured-fields-codec.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/structured-fields.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tcpa-10dlc.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/template-escape-html.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tenant-quota.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/test-coverage.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/test-harness.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/testing-request.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/testing.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/time.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tls-exporter.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tls-ocsp-ct.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tls-ocsp-freshness.test.js +6 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/tls-ocsp-verify.test.js +127 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/tls-pinset-drift.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tls-preferred-groups.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tracing.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tsa.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/uri-template.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/uuid.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/validate-opts-port.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/validate-opts-shape.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vault-aad.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vault-default-store.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vault-rotate-aad.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vault-seal-pem-file.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vc.test.js +25 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vendor-currency-classify.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vendor-data.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vendor-manifest.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vex.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/watcher.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/web-push-vapid.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook-dispatcher.test.js +77 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook-verify-nonce-atomic.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/websocket-channels.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/websocket-extension-header.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/worker-pool-recycle-race.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/worker-pool.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/worm.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ws-client.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/x509-chain-ca-enforcement.test.js +117 -1
- package/lib/vendor/blamejs/test/layer-1-state/api-key.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-5-integration/bundler-output.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-5-integration/external-db-residency.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-5-integration/guard-host-integration.test.js +2 -0
- package/lib/vendor/blamejs/test/layer-5-integration/security-chaos.test.js +2 -0
- package/lib/vendor/blamejs/test/smoke.js +2 -0
- package/package.json +5 -2
- package/lib/vendor/blamejs/release-notes/v0.15.0.json +0 -77
- package/lib/vendor/blamejs/release-notes/v0.15.1.json +0 -22
- package/lib/vendor/blamejs/release-notes/v0.15.10.json +0 -53
- package/lib/vendor/blamejs/release-notes/v0.15.11.json +0 -52
- package/lib/vendor/blamejs/release-notes/v0.15.12.json +0 -47
- package/lib/vendor/blamejs/release-notes/v0.15.13.json +0 -81
- package/lib/vendor/blamejs/release-notes/v0.15.14.json +0 -122
- package/lib/vendor/blamejs/release-notes/v0.15.15.json +0 -73
- package/lib/vendor/blamejs/release-notes/v0.15.16.json +0 -94
- package/lib/vendor/blamejs/release-notes/v0.15.17.json +0 -52
- package/lib/vendor/blamejs/release-notes/v0.15.18.json +0 -49
- package/lib/vendor/blamejs/release-notes/v0.15.19.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.15.2.json +0 -22
- package/lib/vendor/blamejs/release-notes/v0.15.20.json +0 -27
- package/lib/vendor/blamejs/release-notes/v0.15.21.json +0 -51
- package/lib/vendor/blamejs/release-notes/v0.15.22.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.15.23.json +0 -22
- package/lib/vendor/blamejs/release-notes/v0.15.24.json +0 -22
- package/lib/vendor/blamejs/release-notes/v0.15.25.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.15.26.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.15.27.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.15.28.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.15.29.json +0 -27
- package/lib/vendor/blamejs/release-notes/v0.15.3.json +0 -39
- package/lib/vendor/blamejs/release-notes/v0.15.30.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.15.31.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.15.32.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.15.33.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.15.34.json +0 -27
- package/lib/vendor/blamejs/release-notes/v0.15.35.json +0 -27
- package/lib/vendor/blamejs/release-notes/v0.15.36.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.15.37.json +0 -26
- package/lib/vendor/blamejs/release-notes/v0.15.38.json +0 -26
- package/lib/vendor/blamejs/release-notes/v0.15.4.json +0 -39
- package/lib/vendor/blamejs/release-notes/v0.15.5.json +0 -22
- package/lib/vendor/blamejs/release-notes/v0.15.6.json +0 -59
- package/lib/vendor/blamejs/release-notes/v0.15.7.json +0 -43
- package/lib/vendor/blamejs/release-notes/v0.15.8.json +0 -48
- package/lib/vendor/blamejs/release-notes/v0.15.9.json +0 -58
|
@@ -0,0 +1,2284 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-consolidated-schema.json",
|
|
3
|
+
"minor": "0.15",
|
|
4
|
+
"releases": [
|
|
5
|
+
{
|
|
6
|
+
"version": "0.15.68",
|
|
7
|
+
"date": "2026-07-02",
|
|
8
|
+
"headline": "A security-hardening release: mail delivery-status / return-receipt / ARC builders reject CR, LF, and NUL in header-line fields (header injection); the XML canonicalizer's nesting cap now actually fires (stack-overflow DoS on deeply-nested SAML/WebDAV input); and the Idempotency-Key replay cache is scoped to the authenticated principal (cross-actor response disclosure) — plus archive/SRS byte hardening",
|
|
9
|
+
"summary": "b.mail composes RFC 3464 delivery-status notifications (b.mail.send.deliver(...).buildDsn, b.mailBounce.dsn.build), RFC 8098 message-disposition notifications (b.mailMdn.build), and RFC 8617 ARC signatures (b.mail.arc.sign) by interpolating operator- and peer-supplied values — recipient addresses, the reporting-MTA name, the remote server's 5xx diagnostic reply, and the ARC authserv-id / domain / selector — into `Name: value` header lines. A value containing a CR or LF let a hostile original sender (whose message is being bounced) or a malicious peer MX (whose 5xx reply is echoed into the DSN) inject an extra header or forge an additional multipart/report part. Structured fields (addresses, MTA names, identifiers) are now rejected outright, and the free-text 5xx diagnostic — which is legitimately multi-line — is folded to a single line. The same class is closed in two adjacent places: tar and zip entry names refuse CR/LF (a bare LF in an over-long tar name flowed into the POSIX pax extended header, where a crafted name could forge a `path=` record that overrides the ustar name and escapes the extraction directory), and SRS envelope-from rewriting refuses control characters in the address it re-emits as a MAIL FROM. A new b.safeBuffer.assertHeaderSafe primitive centralizes the reject-CR/LF/NUL check every header builder now composes.",
|
|
10
|
+
"sections": [
|
|
11
|
+
{
|
|
12
|
+
"heading": "Security",
|
|
13
|
+
"items": [
|
|
14
|
+
{
|
|
15
|
+
"title": "Delivery-status notifications refuse header-injecting fields; the 5xx diagnostic is folded",
|
|
16
|
+
"body": "b.mail.send.deliver(...).buildDsn and b.mailBounce.dsn.build build an RFC 3464 multipart/report message from the failing recipient, the original sender, the reporting-MTA name, the enhanced status code, and the remote server's 5xx diagnostic reply. The 5xx reply is attacker-influenceable (it comes from the peer MX) and the addresses can come from a message being bounced. A CR or LF in any of these smuggled a new header or forged a report part into a message the operator then relays. Structured fields now reject CR/LF/NUL (the composer throws rather than emit an ambiguous message), and the free-text diagnostic is folded to one line so its text is preserved without introducing a line break."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"title": "Message-disposition notifications refuse header-injecting fields",
|
|
20
|
+
"body": "b.mailMdn.build places the final recipient, original recipient, original message-id, reporting user-agent, and the From/To/Subject of the return receipt on header lines. Because an MDN is generated in response to an inbound message, these fields can carry attacker-chosen content; a CR or LF injected a header into the receipt. Each structured field now rejects CR/LF/NUL before the message is built."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"title": "ARC signing refuses CR/LF/NUL in the authentication-results identity",
|
|
24
|
+
"body": "b.mail.arc.sign already refused CR/LF in the authentication-results value, but the ARC authserv-id, signing domain, and selector — interpolated verbatim into the ARC-Authentication-Results, ARC-Seal, and ARC-Message-Signature header block — were only checked for emptiness. A CR or LF in one of them injected a header into the signed block. All three now reject CR/LF/NUL."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"title": "Archive entry names refuse CR/LF, closing a pax path-override escape",
|
|
28
|
+
"body": "tar and zip entry names already rejected a NUL byte and `..` segments but not CR/LF. In a tar archive, a bare LF in an over-long name flowed into the POSIX pax extended header (`len key=value\\n`); because a pax `path=` record overrides the ustar header's name, a crafted name could inject an absolute `path=` record and write outside the extraction directory — an escape the `..` check does not catch. Both the tar and zip name normalizers now reject CR/LF."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"title": "SRS envelope rewriting refuses control characters in the address",
|
|
32
|
+
"body": "b.mail.srs rewrite / srs1Rewrite / reverse embed the address they are given into the SRS0/SRS1 envelope address that becomes a subsequent SMTP MAIL FROM. They now reject CR/LF/NUL in the input address so a malformed address cannot be smuggled into an envelope command."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"title": "The XML canonicalizer's nesting-depth cap now actually fires (stack-overflow DoS)",
|
|
36
|
+
"body": "b.xmlC14n.parse — the recursive-descent parser behind SAML and WebDAV canonicalization — carried a documented 200-level nesting cap that was dead: the depth counter was a per-frame-local incremented then decremented around each single recursive call, so it never exceeded 1 at the check. Deeply-nested untrusted XML recursed unbounded and overflowed the stack, crashing the process — a pre-authentication denial of service for any endpoint that canonicalizes attacker-supplied XML. The depth is now threaded through the recursion and enforced at entry, so nesting past the cap is refused before it can exhaust the stack."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"title": "The Idempotency-Key replay cache is scoped to the authenticated principal",
|
|
40
|
+
"body": "b.middleware.idempotencyKey keyed its replay/response cache slot solely on the client-supplied Idempotency-Key header, with no binding to the authenticated principal. Two principals presenting the same (shared or guessed) key collided: one was served the other's cached response (cross-actor disclosure), or an attacker could pre-seed a key to force a 422 on the real client (cross-actor denial). The slot is now scoped to the authenticated principal, resolved via b.requestHelpers.extractActorContext and overridable with the new opts.scopeFn. Mount the middleware after authentication so the request carries the principal; slots for unauthenticated requests share a single anonymous scope."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"title": "Mail and data parsers reject reserved prototype keys in untrusted input",
|
|
44
|
+
"body": "b.csv.parse, b.mime-parse (Content-Type parameters), b.eat (Entity Attestation Token claims), b.mailBimi (tiny-PS attributes), b.mailArf (feedback-report fields), and b.safeIcap (ICAP response headers) built plain objects keyed by names taken directly from untrusted input. A key of __proto__, constructor, or prototype would shadow or re-parent the object, letting an attacker taint a downstream type check. Every one of these sites now refuses the reserved keys — CSV throws csv/forbidden-header (an operator DDL surface); the parsers that must stay lenient drop the poisoned key and keep parsing."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"title": "Verifiable-credential JOSE verification binds the algorithm to the key's curve",
|
|
48
|
+
"body": "b.vc.verify / b.vc.verifyPresentation resolved the public key and then verified with the algorithm named in the attacker-controlled JWS header, with no check that the two agree. A header claiming ES384 (or EdDSA) could be verified against a P-256 key an ECDSA curve/type confusion (CWE-347, RFC 7518 §3.4). The verifier now rejects any alg whose required curve/key-type doesn't match the resolved key, before the signature check."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"title": "Span-event names are redacted on the telemetry wire",
|
|
52
|
+
"body": "The OTLP span exporter ran span names and exception messages through the telemetry redactor but passed span-EVENT names through verbatim, so a value-shape secret (a connection string, a token) placed in an event name egressed unredacted. Event names now route through the same redactor as span names, on both the JSON and protobuf wire paths."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"title": "Session refresh enforces the idle and absolute timeout floors",
|
|
56
|
+
"body": "b.session.touch refreshed a session's activity timestamp without re-checking the idle / absolute timeout floors that b.session.verify enforces, so a floor-expired session could be resurrected by a refresh instead of being killed. Both floors are now evaluated on the refresh path (as on verify), and a session past either floor is deleted rather than extended."
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"heading": "Changed",
|
|
62
|
+
"items": [
|
|
63
|
+
{
|
|
64
|
+
"title": "Refreshed the vendored public-suffix list and the pinned CI action SHAs",
|
|
65
|
+
"body": "The vendored Mozilla Public Suffix List (which backs registrable-domain boundaries in b.guardDomain and cookie-scope checks) is refreshed to its current upstream revision, and the SHA-pinned GitHub Actions in the release and scanning workflows are bumped to their latest upstream releases. No operator action required."
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"title": "Concurrent DNS cache misses are coalesced into a single upstream lookup",
|
|
69
|
+
"body": "b.network.dns.resolver now single-flights in-flight queries: when several callers miss the cache for the same name at once, one upstream lookup is issued and the others await its result, instead of each firing its own request (a cache stampede that amplified load on the upstream resolver). Each caller still applies its own DNSSEC validate gate against the shared answer."
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"heading": "Added",
|
|
75
|
+
"items": [
|
|
76
|
+
{
|
|
77
|
+
"title": "b.safeBuffer.assertHeaderSafe(value, label, ErrorClass, code)",
|
|
78
|
+
"body": "Throws when a string bound for a CRLF-delimited protocol line contains CR, LF, or a NUL byte, in the caller's own error domain. Structured header fields (addresses, domains, identifiers, MTA names) compose it to reject."
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"title": "b.safeBuffer.foldHeaderText(value, replacement?)",
|
|
82
|
+
"body": "Folds free-text bound for a header line onto one line — replaces every CR and LF with the replacement (default a space) and removes every NUL. Used for values that may legitimately wrap, such as a multi-line SMTP 5xx reply echoed into a DSN diagnostic line, where rejecting would be too strict; unlike a plain CR/LF strip it also removes NUL, which is never valid in a header value and is treated specially by downstream mail parsers."
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"version": "0.15.67",
|
|
90
|
+
"date": "2026-06-29",
|
|
91
|
+
"headline": "Path-scoped router middleware can no longer be bypassed by percent-encoding the request path: the router refuses an encoded path separator or null byte and decodes the path once, so a security gate and the resource it guards always agree on the path",
|
|
92
|
+
"summary": "b.router supports path-scoped middleware — router.use('/admin', gate) runs a gate (requireAal, bearerAuth, requireMtls, csrfProtect, …) only for requests under a prefix. The gate matched the request path with its percent-escapes intact, while downstream consumers such as b.staticServe and router.serveStatic percent-decode the path before resolving the file. Because the gate and the consumer disagreed on decoding, an attacker could encode a character in the guarded segment (/%61dmin/secret) or hide a separator (/admin%2fsecret) so the gate's segment match missed while the consumer still reached the protected resource — an authentication, authorization, CSRF, or mTLS bypass for any resource served under a scoped gate. The router now refuses a request whose path contains an encoded path separator (%2f, %5c) or null byte (%00), and decodes the remaining escapes exactly once, so the gate, the route matcher, and every consumer act on a single canonical path. A request that legitimately needs those bytes was always ambiguous and is now rejected with 400 rather than silently routed two different ways. Route parameters captured from the path are now percent-decoded, matching the conventional behavior.",
|
|
93
|
+
"sections": [
|
|
94
|
+
{
|
|
95
|
+
"heading": "Security",
|
|
96
|
+
"items": [
|
|
97
|
+
{
|
|
98
|
+
"title": "Path-scoped middleware is no longer bypassable via percent-encoded paths",
|
|
99
|
+
"body": "A gate mounted with router.use(prefix, mw) matched req.pathname with percent-escapes preserved, but b.staticServe and router.serveStatic decode the path before resolving the resource. An attacker could percent-encode a character in the guarded segment (/%61dmin/secret, where %61 is 'a') so the segment compare saw '%61dmin' and skipped the gate, or hide a separator (/admin%2fsecret) so the gate saw one segment while the file resolver saw two — in both cases the protected resource was served without the gate running. The router now rejects a path containing an encoded separator (%2f/%5c) or null byte (%00) with 400, and percent-decodes the path exactly once before any path-scoped decision, so the gate and the resource it protects always resolve the same path. Operators who mounted authentication, authorization, CSRF, or mTLS gates with the scoped form no longer have a silent bypass beneath them."
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"heading": "Changed",
|
|
105
|
+
"items": [
|
|
106
|
+
{
|
|
107
|
+
"title": "Encoded path separators and null bytes in the request path are refused; route params are decoded",
|
|
108
|
+
"body": "A request whose path contains %2f, %5c, or %00 is answered with 400 Bad Request — these have no unambiguous routing meaning (a consumer would treat them as a separator or terminator), and a request that needs a literal slash in a value should carry it in a query parameter. Path parameters captured by a route pattern (for example :id) are now percent-decoded before they reach the handler in req.params, matching conventional router behavior; handlers that previously received a still-encoded value will now receive the decoded form."
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"version": "0.15.66",
|
|
116
|
+
"date": "2026-06-29",
|
|
117
|
+
"headline": "Recursive serializers and parsers refuse pathologically deep input with a typed error instead of overflowing the stack and crashing the process",
|
|
118
|
+
"summary": "Several of the framework's recursive walkers over attacker-reachable input lacked an effective nesting cap, so a deeply nested value could exhaust the V8 call stack and throw an uncaught RangeError — crashing the process (a denial of service). b.canonicalJson is the most exposed: content-credentials verification canonicalises an untrusted manifest before it checks the signature, so a hostile credential could crash a verifier pre-authentication. b.jsonSchema.validate had a depth guard, but its limit was set so high the stack overflowed before the guard could fire, so validating a request body against a recursive schema crashed rather than returning a typed error. b.i18n.messageFormat parsed and rendered nested plural/select cases with no cap. Each walker now throws a typed framework error well before native overflow, and legitimate (even deeply nested) input is unaffected. The release also corrects DNSSEC signature-window comparison to the RFC 1982 serial-number arithmetic the spec requires, and makes the Redis client treat a malformed reply frame as a connection fault rather than letting it crash the host.",
|
|
119
|
+
"sections": [
|
|
120
|
+
{
|
|
121
|
+
"heading": "Security",
|
|
122
|
+
"items": [
|
|
123
|
+
{
|
|
124
|
+
"title": "Canonical JSON refuses excessively nested input before it can overflow the stack",
|
|
125
|
+
"body": "b.canonicalJson.stringify and stringifyJcs walk the value recursively. They detected circular references but had no nesting cap, so a deeply nested (acyclic) object or array overflowed the V8 stack with an uncaught RangeError. This is reachable before authentication: content-credentials verification canonicalises the untrusted manifest to hash it before verifying the signature, so a hostile credential could crash the verifier. Both serializers now throw a typed nesting-depth error at a depth far beyond any legitimate signed document and well short of native overflow."
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"title": "JSON Schema validation depth guard now fires before the stack overflows",
|
|
129
|
+
"body": "b.jsonSchema.validate caps subschema-validation nesting to defend against a recursive schema (for example items pointing back at the root with $ref) applied to a deeply nested instance — both attacker-controlled when validating a request body. The cap was set so high the V8 stack overflowed first, so the guard never ran and a crafted body crashed the validator with an uncaught RangeError instead of the typed reference-depth error. The limit is now well under native overflow; legitimate documents — deeply nested or wide — continue to validate."
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"title": "ICU MessageFormat refuses pathologically nested templates",
|
|
133
|
+
"body": "b.i18n.messageFormat.parse and format recurse once per nested plural/select case, with no depth cap. A template nested thousands of levels deep overflowed the stack. format() and parse() are public and b.i18n.t can render operator-supplied entries, so a hostile template is a denial-of-service vector; it now fails as a typed bad-template error. Real-world nesting (a handful of levels) is unaffected."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"title": "Redis client treats a malformed reply frame as a connection fault, not a crash",
|
|
137
|
+
"body": "The RESP decoder recurses on nested arrays with no depth cap, and the data handler did not guard the parse, so a malformed or hostilely deep frame from a server threw out of the socket callback and could crash the host. The decoder now caps reply nesting and any parse fault tears the socket down and rejects in-flight commands for a reconnect — the same path as any other lost connection."
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"heading": "Fixed",
|
|
143
|
+
"items": [
|
|
144
|
+
{
|
|
145
|
+
"title": "DNSSEC compares RRSIG validity windows with RFC 1982 serial arithmetic",
|
|
146
|
+
"body": "b.network.dns.dnssec compared an RRSIG's inception and expiration against the current time by magnitude. RFC 4034 §3.1.5 requires the 32-bit timestamps to be interpreted with RFC 1982 serial-number arithmetic, which agrees with a plain comparison only while the values stay below 2^31. A signature whose window straddles the 2^31 (January 2038) or 2^32 (February 2106) boundary was mis-ordered — an in-window signature rejected, or a stale one accepted. The comparison now masks the clock into the same 32-bit serial space and tests the wrapped signed delta."
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"version": "0.15.65",
|
|
154
|
+
"date": "2026-06-29",
|
|
155
|
+
"headline": "The account-takeover kill-switch now actually locks the account: b.auth.lockout gains a lock() method and the kill-switch engages it through the operator's lockout instance",
|
|
156
|
+
"summary": "b.auth.atoKillSwitch.trigger is the incident-response path for a compromised account: it destroys the user's sessions and then locks them out of new logins. The lockout step called b.auth.lockout.lock(), but no such method existed (and the kill-switch invoked it on the lockout module, which has no store), so the call threw and was swallowed — the kill-switch reported success while never locking the account, leaving an attacker who still held the credentials free to log straight back in. b.auth.lockout instances now expose lock(key, { durationMs?, untilMs?, reason? }) to force an account into lockout immediately, atomically, independent of the failure counter, and atoKillSwitch.trigger engages it through the lockout instance the operator passes as opts.lockout. When no lockout instance is supplied the step is skipped and the result's lockoutApplied is false (with an audit row), rather than silently claiming the account was locked. The release also closes a lock-clear race in lockout and refuses a self-disabling window.",
|
|
157
|
+
"sections": [
|
|
158
|
+
{
|
|
159
|
+
"heading": "Security",
|
|
160
|
+
"items": [
|
|
161
|
+
{
|
|
162
|
+
"title": "Account-takeover kill-switch engages the lockout instead of silently doing nothing",
|
|
163
|
+
"body": "b.auth.atoKillSwitch.trigger invoked b.auth.lockout.lock(), which did not exist; the call threw and was caught by a best-effort guard, so the kill-switch destroyed sessions but never locked the account — an attacker still holding valid credentials could immediately re-authenticate. b.auth.lockout instances now provide lock() (an atomic, compare-and-set forced lockout), and the kill-switch calls it on the lockout instance supplied as opts.lockout. Without a lockout instance the lockout step cannot run; the result's lockoutApplied is false and an audit row records that the account was not locked, so an operator is not misled into believing a compromised account was secured. opts.lockout is now the lockout instance (or false to skip), not a boolean toggle."
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"title": "lockout clears the failure counter atomically and refuses a zero window",
|
|
167
|
+
"body": "b.auth.lockout.recordSuccess and unlock cleared state with a read-then-delete, which could erase a lock a concurrent recordFailure had just engaged; they now clear under the same compare-and-set the failure path uses. create() also refuses windowMs: 0, which previously disabled lockout entirely (every failure decayed immediately and the zero-TTL state never persisted)."
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"heading": "Added",
|
|
173
|
+
"items": [
|
|
174
|
+
{
|
|
175
|
+
"title": "b.auth.lockout instances expose lock()",
|
|
176
|
+
"body": "lock(key, { durationMs?, untilMs?, reason? }) forces an account into lockout immediately — the operator action behind an ATO kill-switch or incident response — independent of the failure counter, defaulting to a long admin lock. A forced lock is released only by an explicit unlock(): recordSuccess() does not clear it, so a successful login by someone who still holds the compromised password cannot release a kill-switch lock. It uses the same atomic compare-and-set as the failure counter and throws if the lock cannot be committed (the caller must know it did not lock)."
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"version": "0.15.64",
|
|
184
|
+
"date": "2026-06-29",
|
|
185
|
+
"headline": "Token and HTTP-message-signature verifiers now reject a non-finite clock-skew or tolerance value instead of letting it disable the expiry, not-before, and future-dating checks",
|
|
186
|
+
"summary": "Several verifiers read an operator-supplied clock-skew or tolerance value with a bare type check that accepted Infinity and NaN. Because the resulting comparison (for example exp + skew < now, or age > tolerance) is always false when the skew or tolerance is Infinity, a misconfigured non-finite value silently disabled the time-window check — so an expired token, a not-yet-valid token, a future-dated signature, or a replayed (too-old) signature would verify. The affected paths are the shared external-JWT verifier b.auth.jwt.verifyExternal (which the JAR / signed-request-object path also uses), the SD-JWT-VC credential verifier, the OAuth ID-token and client-attestation verifiers, and the HTTP message signature verifier's freshness and clock-skew windows. Each now requires a present skew or tolerance to be a non-negative finite number: the throw-based JWT, SD-JWT-VC, and OAuth verifiers reject a malformed value, and the verdict-returning HTTP-message-signature verifier falls back to its safe default rather than disabling the window. A build check now fails if any verifier reads one of these options without a finiteness guard.",
|
|
187
|
+
"sections": [
|
|
188
|
+
{
|
|
189
|
+
"heading": "Security",
|
|
190
|
+
"items": [
|
|
191
|
+
{
|
|
192
|
+
"title": "Non-finite clock-skew / tolerance no longer disables a verifier's time-window check",
|
|
193
|
+
"body": "b.auth.jwt.verifyExternal, b.auth.sdJwtVc.verify, b.auth.oauth (ID-token and client-attestation verification), and the HTTP message signature verifier read their clock-skew, max-clock-skew, max-PoP-age, and tolerance options with a bare `typeof === \"number\"` check that let Infinity and NaN through. With such a value the expiry / not-before / future-dating comparison is always false, so the check was silently skipped and an expired or not-yet-valid token — or a future-dated or replayed signature — would verify. The JWT, SD-JWT-VC, and OAuth verifiers now reject a non-finite or negative skew (the option is operator configuration, not attacker-controlled), and the HTTP message signature verifier falls back to its default tolerance and skew. The CWT, DPoP, SAML, and OpenID Federation verifiers already enforced this and are unchanged."
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"heading": "Added",
|
|
199
|
+
"items": [
|
|
200
|
+
{
|
|
201
|
+
"title": "verifyExternal and jar.parse accept an `now` clock override",
|
|
202
|
+
"body": "b.auth.jwt.verifyExternal and b.auth.jar.parse now accept an optional `now` (a finite epoch-milliseconds value) to evaluate a token's exp / nbf / iat as of a specific instant instead of the wall clock — consistent with b.auth.sdJwtVc.verify. A negative clockSkewMs is no longer accepted as a way to shift the evaluation time; use `now`."
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"heading": "Detectors",
|
|
208
|
+
"items": [
|
|
209
|
+
{
|
|
210
|
+
"title": "Verifier clock-skew / tolerance options must be finite-guarded",
|
|
211
|
+
"body": "A build check fails if a verifier reads a clock-skew or tolerance option with a bare `typeof === \"number\"` without a finiteness guard (a non-negative-finite check, an inline isFinite fallback, or a create-time non-negative-finite schema), preventing a future verifier from reintroducing the disable-the-window class."
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"version": "0.15.63",
|
|
219
|
+
"date": "2026-06-29",
|
|
220
|
+
"headline": "OID4VCI now enforces single-use of a pre-authorized code and of a single-use access token under concurrency, so two simultaneous requests can no longer mint two credentials from one",
|
|
221
|
+
"summary": "On the OID4VCI credential issuer, two single-use values were consumed by a delete whose result was ignored, so concurrent requests could each act on the same value. exchangePreAuthorizedCode read the pre-authorized code's entry, validated the transaction code, then deleted the code and minted an access token without checking that its own call had removed the entry — two simultaneous /token requests with the same code each saw the entry, each deleted it, and each minted a distinct access token, issuing two access tokens (and ultimately two credentials) from a code OID4VCI requires to be single-use. issueCredential had the same shape: with single-use access tokens (the default), it minted the credential first and deleted the access token afterward as best-effort cleanup, so two concurrent requests bearing the same token both read it and the same not-yet-rotated c_nonce, both proofs verified, and both minted a credential. Both paths now claim the value with an atomic delete and proceed only when that delete succeeded; the losing request is refused. The transaction-code and proof checks still run first, so a bad transaction code or proof does not consume the value (a wallet can retry).",
|
|
222
|
+
"sections": [
|
|
223
|
+
{
|
|
224
|
+
"heading": "Security",
|
|
225
|
+
"items": [
|
|
226
|
+
{
|
|
227
|
+
"title": "OID4VCI pre-authorized code and access token are single-use under concurrency",
|
|
228
|
+
"body": "exchangePreAuthorizedCode and issueCredential consumed their single-use value (the pre-authorized code, and the single-use access token) with a delete whose return was discarded, and in issueCredential's case after the credential was already minted. Two concurrent requests carrying the same value therefore each succeeded — minting two access tokens from one pre-authorized code, or two credentials from one single-use access token — defeating the single-use guarantee OID4VCI §3.5 requires (an authorization intended for one credential could yield two). Both paths now delete the value atomically and issue only if that delete removed it, refusing the request that lost the race; the transaction-code and proof verifications run before the claim, so an invalid attempt does not burn the value. If the operator's credential issuer throws after the access token has been claimed (a transient signer outage), the token is restored so the wallet can retry rather than being permanently consumed without a credential."
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"version": "0.15.62",
|
|
236
|
+
"date": "2026-06-29",
|
|
237
|
+
"headline": "ARC evaluation now reads each hop's instance with the same strict parser the signature checks use, so a crafted ARC-Authentication-Results header can no longer forge the upstream auth-results surfaced to downstream policy",
|
|
238
|
+
"summary": "b.mail.arc.evaluate returns finalAr — the most recent hop's ARC-Authentication-Results, the receiver's view of the upstream authentication results — which operators may key downstream decisions on. The instance tag (i=) on each ARC header was parsed by three different regexes: the indexing pass that drives the AMS/AS signature checks required i= with no surrounding space and at most three digits, while the finalAr extraction and the AMS header-retention test accepted a looser form (a space around =, unbounded digits). When a sealer signs without covering ARC-Authentication-Results in its AMS h= (permitted by RFC 8617 and supported by the verifier), an attacker holding no key could append a second ARC-Authentication-Results written so the strict pass ignored it while the loose pass consumed it — forging finalAr on a chain that still verified as pass. All ARC instance reads now route through one strict parser, so the evaluation surfaces the same hop the signatures validated. The release also repairs the b.mail.arc.sign excludeAarFromAms option (it was read but rejected by option validation, so the documented opt-out was unreachable) and routes the ARC-Seal signature's b= stripping through the shared tag-aware helper.",
|
|
239
|
+
"sections": [
|
|
240
|
+
{
|
|
241
|
+
"heading": "Fixed",
|
|
242
|
+
"items": [
|
|
243
|
+
{
|
|
244
|
+
"title": "ARC finalAr is read from the strictly-indexed hop, not a looser rescan",
|
|
245
|
+
"body": "b.mail.arc.evaluate extracted finalAr (and validated the per-hop AMS header retention) with a regex that accepted ARC instance tags the signature-indexing pass rejected — a space around i= or more than three digits. A sealer that omits ARC-Authentication-Results from its AMS h= leaves the AAR outside signature coverage; an attacker could then inject a second ARC-Authentication-Results whose instance the strict crypto pass skipped but the finalAr pass accepted, presenting attacker-chosen upstream auth-results on a chain that still reported pass. Every ARC instance read now goes through a single strict parser, so finalAr is always the AAR the chain's signatures actually covered."
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"title": "b.mail.arc.sign accepts excludeAarFromAms again",
|
|
249
|
+
"body": "The excludeAarFromAms option was read when building the AMS h= list but was missing from the function's option allow-list, so passing it raised an unknown-option error — the documented opt-out could not be used. It is now accepted."
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"title": "ARC-Seal b= stripping uses the shared tag-aware helper",
|
|
253
|
+
"body": "The ARC-Seal verification stripped the signature's b= value with a regex that could mis-zero a value containing b= inside another tag; it now uses the same tag-aware stripper as DKIM, so canonicalization matches the signer in every case."
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"heading": "Detectors",
|
|
259
|
+
"items": [
|
|
260
|
+
{
|
|
261
|
+
"title": "ARC instance parsing must use the shared strict reader",
|
|
262
|
+
"body": "A check fails the build if any ARC instance (i=) parsing regex is added outside the single shared reader, preventing a future divergence between the signature-indexing pass and the finalAr / header-retention passes."
|
|
263
|
+
}
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
]
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"version": "0.15.61",
|
|
270
|
+
"date": "2026-06-29",
|
|
271
|
+
"headline": "The local and Redis job queues fence completion, failure, and lease extension on the lease the caller actually holds, so a worker finishing after its lease expired can no longer disturb a job another worker has since taken over",
|
|
272
|
+
"summary": "On the local and Redis queue backends, complete(), fail(), and extendLease() identified a job only by its id. When a worker's lease expired, the sweep returned the job to the ready set and another worker leased and began running it; if the original worker then finished late, its complete() could mark the new worker's in-progress job done (and double-fire a cron repeat or re-release flow children), and its fail() could re-queue or dead-letter a job the new worker was still executing. Each lease now carries the job's attempts value (incremented once per lease), and complete(), fail(), and extendLease() act only when that value still matches — so a call from a worker that no longer holds the lease changes nothing. The generic consumer threads this automatically; the SQS backend already bound these actions to the message's receipt handle and is unchanged.",
|
|
273
|
+
"sections": [
|
|
274
|
+
{
|
|
275
|
+
"heading": "Fixed",
|
|
276
|
+
"items": [
|
|
277
|
+
{
|
|
278
|
+
"title": "Local and Redis queues bind complete/fail/extendLease to the held lease",
|
|
279
|
+
"body": "A long-running handler whose lease expired and was swept could have its job re-leased to a second worker; when the first worker finished, complete() marked the second worker's in-progress job done — double-firing a cron-recurring job's next enqueue and re-releasing its flow dependents — while fail() re-queued or dead-lettered the job the second worker was still running (re-executing or discarding in-flight work). The backends now fence each of these calls on the leased attempts value, which is bumped once per lease; only the worker that holds the current lease can complete, fail, or extend it. A stale call returns without mutating the queue. This brings the local and Redis backends to parity with the SQS backend, which already bound these actions to the message receipt handle."
|
|
280
|
+
}
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"version": "0.15.60",
|
|
287
|
+
"date": "2026-06-29",
|
|
288
|
+
"headline": "`requireStepUp` binds the elevation grant to the authenticated principal, refusing a grant minted for a different user (cross-user step-up replay)",
|
|
289
|
+
"summary": "The b.middleware.requireStepUp gate accepts an operator-issued step-up elevation grant from the X-Step-Up-Grant header and verifies it with b.auth.stepUp.grant.verify. An elevation grant carries the subject it was minted for (payload.sub), but the middleware verified only the grant's signature, expiry, and scope — never that the grant's subject matched the request's authenticated principal. A grant minted for one user (and then leaked through a shared cache, a log line, a referrer, or a shared device) therefore satisfied the step-up requirement for ANY other authenticated user who presented it, elevating their session to the granted assurance level without ever completing a step-up ceremony. requireStepUp now passes the resolved principal as the grant's required subject, so the grant satisfies step-up only for the user it was issued to. The principal is resolved from whichever shape the authenticator populated — a session's req.user.id / req.user.userId, or the JWT subject (req.user.claims.sub / req.user.sub) set by bearerAuth with an external verifier — so a grant legitimately minted for a JWT subject still binds. A request with no resolvable principal cannot bind the grant and falls through to the claims-based challenge.",
|
|
290
|
+
"sections": [
|
|
291
|
+
{
|
|
292
|
+
"heading": "Security",
|
|
293
|
+
"items": [
|
|
294
|
+
{
|
|
295
|
+
"title": "Step-up elevation grants are bound to the authenticated principal",
|
|
296
|
+
"body": "requireStepUp's grant path called b.auth.stepUp.grant.verify with only the grant scope, not the subject, so any holder of a valid, unexpired, scope-matching elevation grant passed the step-up gate regardless of which user the request was authenticated as — a leaked or shared grant elevated a different user's session (cross-user step-up replay). The grant already binds a subject at mint time and the verifier supports a subject check; the middleware now supplies the request's principal as the required subject, refusing a grant whose subject does not match. The principal is read from whichever field the authenticator set — a session's id/userId or the JWT subject (claims.sub / sub) from bearerAuth's external verifier — so a grant minted for any of those binds correctly. A request with no authenticated principal cannot bind a grant and is handled by the normal claims-based step-up challenge. The grant verifier's signature/expiry/scope/jti-revocation checks are unchanged."
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
]
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"version": "0.15.59",
|
|
304
|
+
"date": "2026-06-29",
|
|
305
|
+
"headline": "OCSP response validation binds the response to the certificate's issuer (issuerNameHash + issuerKeyHash), not the serial number alone, refusing a wrong-issuer \"good\"",
|
|
306
|
+
"summary": "An OCSP SingleResponse identifies the certificate it covers by a CertID of (hashAlgorithm, issuerNameHash, issuerKeyHash, serialNumber) — RFC 6960 §4.1.1. b.network.tls.ocsp.evaluate matched a response to the certificate under validation by the serial number alone and never compared the CertID's issuer hashes. Because a serial number is unique only within one issuer, a responder key that serves more than one issuer identity — a delegated OCSP responder, or a CA key spanning issuers — could have a signed \"good\" response for serial-S under issuer-Y accepted as proof for the serial-S certificate under issuer-X. The evaluator now recomputes the expected issuerNameHash and issuerKeyHash from the issuer certificate and refuses a response whose CertID issuer hashes do not match. b.network.tls.ocsp.fetch supplies the issuer certificate automatically (its issuerPem is the leaf's issuer); ocsp.requireGood and direct ocsp.evaluate callers pass the issuer cert explicitly as the new issuerCertDer (requireGood's issuerPem may be a delegated OCSP responder rather than the issuer, so it is not used as the issuer), and a response with no issuer certificate available stays bound on serial plus signature as before.",
|
|
307
|
+
"sections": [
|
|
308
|
+
{
|
|
309
|
+
"heading": "Security",
|
|
310
|
+
"items": [
|
|
311
|
+
{
|
|
312
|
+
"title": "OCSP evaluate binds the response CertID to the issuer, not the serial alone (RFC 6960 §4.1.1)",
|
|
313
|
+
"body": "b.network.tls.ocsp.evaluate selected the matching SingleResponse by normalized serial number only, ignoring the CertID's issuerNameHash and issuerKeyHash. Since serials are unique only per issuer, a \"good\" response signed by a key that also serves a different issuer (a delegated id-kp-OCSPSigning responder, or a shared CA key) could be replayed as proof for a same-serial certificate under another issuer. evaluate now recomputes the expected issuerNameHash = Hash(issuer DN) and issuerKeyHash = Hash(issuer public key) under the CertID's own hash algorithm and refuses the response if either differs (\"wrong-issuer response\"). b.network.tls.ocsp.fetch forwards the issuer certificate automatically (its issuerPem is the leaf's issuer); ocsp.requireGood and a direct ocsp.evaluate caller enable the binding by passing issuerCertDer (the issuer cert DER) — requireGood's issuerPem may be a delegated OCSP responder, so the issuer cert is taken explicitly rather than derived from it — and a call without an issuer certificate retains the prior serial-plus-signature binding."
|
|
314
|
+
}
|
|
315
|
+
]
|
|
316
|
+
}
|
|
317
|
+
]
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"version": "0.15.58",
|
|
321
|
+
"date": "2026-06-29",
|
|
322
|
+
"headline": "File-upload content-safety scanning now also runs the gate for a file's magic-byte-detected type, so a mislabeled file can't dodge the scanner for its real type by choosing the extension",
|
|
323
|
+
"summary": "b.fileUpload selected the per-extension content-safety gate purely from the upload's filename extension, which the uploader controls. A file whose magic bytes identify one type but whose name carries another extension (e.g. a PDF named photo.png) was therefore scanned by the gate for the named extension — or, when no gate was registered for that extension, not scanned at all — so an uploader could dodge the scanner configured for the file's real type by renaming it. When a fileType detector is wired, finalize now also runs the content-safety gate for the type the magic bytes identify, in addition to the filename-extension gate, so the scanner for the real type runs even under a mismatched name. Magic-byte-less text formats (HTML, SVG, CSV) cannot be classified this way; that residual is covered by serving uploads with an explicit Content-Type plus X-Content-Type-Options: nosniff and by registering a content-safety gate for every accepted extension.",
|
|
324
|
+
"sections": [
|
|
325
|
+
{
|
|
326
|
+
"heading": "Security",
|
|
327
|
+
"items": [
|
|
328
|
+
{
|
|
329
|
+
"title": "Content-safety gate selection follows the sniffed type, not just the filename extension",
|
|
330
|
+
"body": "finalize chose the content-safety gate from nodePath.extname(filename), so a file's real type could be hidden behind a chosen extension: a PDF named photo.png ran the .png gate (or, with no .png gate, skipped scanning) and never reached the .pdf scanner. When opts.fileType is wired, finalize now detects the magic-byte type and, if it differs from the filename extension and a gate is registered for it, runs that gate too (alongside the filename-extension gate), refusing or sanitizing per the gate's decision. Existing behavior is unchanged when no fileType detector is wired or the detected type matches the extension. Formats without magic bytes (HTML/SVG/CSV/plain text) remain undetectable by content sniffing — defend those by serving stored files with a fixed Content-Type and X-Content-Type-Options: nosniff, and by registering a content-safety gate for each accepted extension."
|
|
331
|
+
}
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
]
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
"version": "0.15.57",
|
|
338
|
+
"date": "2026-06-29",
|
|
339
|
+
"headline": "The RFC 9421 HTTP-message-signature verifier now enforces a required-coverage floor by default, refusing a signature whose covered set omits `@method` / `@target-uri` (and `content-digest` for bodied requests)",
|
|
340
|
+
"summary": "b.crypto.httpSig.verify built the signature base entirely from the covered-component list carried in the message's own Signature-Input header and never checked that the signature actually covered the security-relevant parts of the request (RFC 9421 §3.2). A signature covering only @authority therefore verified even after the method, target URI, or body were changed — so a captured signature could be replayed across a different method and path, or a request body swapped, under an otherwise-valid signature. verify now refuses a signature whose covered set omits a required component, returning { valid: false, reason: \"missing-required-component\", missing: [...] } before the cryptographic check. By default (security-on, not opt-in) it requires @method and @target-uri on every request, plus content-digest when the request carries a body; an operator can pass requiredComponents to assert an exact set, or requiredComponents: [] to waive the floor (the signature itself is still verified).",
|
|
341
|
+
"sections": [
|
|
342
|
+
{
|
|
343
|
+
"heading": "Security",
|
|
344
|
+
"items": [
|
|
345
|
+
{
|
|
346
|
+
"title": "httpSig.verify enforces a required-coverage floor (RFC 9421 §3.2)",
|
|
347
|
+
"body": "The verifier trusted the covered-component list from the message's Signature-Input header without requiring that any particular component be covered, so an under-covered signature (e.g. covering only @authority) verified while the method, target URI, and body were free to change — a captured signature replayed across method+path, or a swapped body, passed verification. verify now refuses a signature missing a required component with reason \"missing-required-component\" (and a missing[] list) before the crypto check. The default floor is @method + @target-uri on every request plus content-digest for bodied requests; requiredComponents overrides it explicitly, and requiredComponents: [] waives the floor for callers that intentionally sign a narrower set (the signature itself is still verified)."
|
|
348
|
+
}
|
|
349
|
+
]
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"heading": "Migration",
|
|
353
|
+
"items": [
|
|
354
|
+
{
|
|
355
|
+
"title": "Signers must cover @method + @target-uri (and content-digest for bodied requests)",
|
|
356
|
+
"body": "If you verify HTTP message signatures with b.crypto.httpSig.verify, signatures whose covered set omits @method or @target-uri (or content-digest on a request with a body) now fail with reason \"missing-required-component\". Broaden the signer's covered set to include them (recommended), pass requiredComponents to assert the exact components your application requires, or pass requiredComponents: [] to keep the prior behavior of accepting whatever the signer covered. Verifying a fully-covered signature is unchanged."
|
|
357
|
+
}
|
|
358
|
+
]
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
"version": "0.15.56",
|
|
364
|
+
"date": "2026-06-29",
|
|
365
|
+
"headline": "Break-glass and dual-control wildcard scope matching is now segment-aware, and the JMAP cross-tenant gate validates every account-id argument (including `fromAccountId`), closing two authorization gaps",
|
|
366
|
+
"summary": "Two authorization gates under-checked their input. b.breakGlass and b.dualControl matched a wildcard scope/role by stripping a trailing \"*\" and doing a raw string-prefix comparison (requireScope.indexOf(prefix) === 0), which ignores \":\" segment boundaries — so a partial-segment grant like \"phi:a*\" satisfied a required \"phi:admin\", letting a holder break-glass-unseal or approve a flow they were not scoped for. Both now match through the canonical, segment-aware b.permissions.match, so only an exact scope or a whole-segment wildcard (\"phi:*\") satisfies the requirement. Separately, the JMAP server's cross-tenant gate validated only a method call's destination accountId, not the fromAccountId that /copy methods (Email/copy, CalendarEvent/copy — RFC 8620 §5.4) read their SOURCE account from — so an actor could name a foreign fromAccountId and copy another tenant's data. The gate now validates every account-id argument the call names.",
|
|
367
|
+
"sections": [
|
|
368
|
+
{
|
|
369
|
+
"heading": "Security",
|
|
370
|
+
"items": [
|
|
371
|
+
{
|
|
372
|
+
"title": "Break-glass / dual-control wildcard scope matching is segment-aware",
|
|
373
|
+
"body": "b.breakGlass grant (requireScope) and b.dualControl approval (approverRoles) matched a wildcard-shaped actor scope/role by stripping its trailing \"*\" and testing requireScope.indexOf(prefix) === 0 — a raw string prefix that ignores the \":\" segment structure. A partial-segment grant therefore over-matched: \"phi:a*\" satisfied \"phi:admin\" (and \"p*\" satisfied any \"phi:...\"), so an actor with a narrower grant could break-glass-unseal sealed data or approve a dual-control flow they were not authorized for. Both sites now match through b.permissions.match, where \"*\" must occupy a whole \":\"-delimited segment — \"phi:*\" still satisfies \"phi:admin\", but \"phi:a*\" does not. Actor scopes/roles flow from operator-trusted assignment, so this tightens a defense-in-depth gate rather than a request-controlled bypass."
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
"title": "JMAP cross-tenant gate validates every account-id argument, not only the destination",
|
|
377
|
+
"body": "The JMAP server rejects a method call that names an accountId outside the actor's enumerated set (RFC 8620 §3.6.1). It checked only accountId, but /copy methods (Email/copy, CalendarEvent/copy — RFC 8620 §5.4) also carry fromAccountId, the SOURCE account they read from. An actor enumerated for their own destination account could therefore name a foreign fromAccountId and have the operator's copy handler read another tenant's messages. The gate now validates every account-id argument (any *AccountId) against the actor's enumerated accounts before the handler runs, so a foreign source account is refused with accountNotFound."
|
|
378
|
+
}
|
|
379
|
+
]
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
"heading": "Detectors",
|
|
383
|
+
"items": [
|
|
384
|
+
{
|
|
385
|
+
"title": "Scope/role wildcard matching must use b.permissions.match",
|
|
386
|
+
"body": "A codebase-patterns detector flags the hand-rolled wildcard idiom (a trailing-\"*\" check plus a slice(0,-1) strip plus a raw indexOf(prefix)===0 prefix match), so a new authorization gate cannot reintroduce a segment-unaware scope match instead of routing through b.permissions.match."
|
|
387
|
+
}
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
]
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"version": "0.15.55",
|
|
394
|
+
"date": "2026-06-29",
|
|
395
|
+
"headline": "`b.fileUpload` enforces per-upload ownership: acceptChunk / finalize / status / cancelUpload now refuse a caller who is not the upload's owner, closing an IDOR",
|
|
396
|
+
"summary": "A file-upload manager recorded the owner of each upload at init() but never checked it again: acceptChunk, finalize, status, and cancelUpload looked the upload up by the caller-supplied uploadId alone. The only gate was the coarse \"fileUpload.<op>\" permission scope, which says whether an actor may perform the operation at all — not whether they own the specific upload. So any caller holding the (commonly shared) fileUpload.finalize / cancel / status / accept scope could finalize, cancel, read the status of, or push chunks into another actor's in-flight upload by its uploadId (an insecure-direct-object-reference, CWE-639). Each lifecycle method now compares the calling actor against the stored owner (actor.id || actor.userId, captured at init) and refuses a non-owner. Operator admin tooling that must act across actors opts in with create({ allowCrossActor: true }); when a permissions instance is wired, cross-actor access additionally requires the new \"fileUpload.admin\" scope, so the escape hatch is itself gated. Uploads created without an actor are owned by the anonymous identity and unreachable by a named actor (fail-closed).",
|
|
397
|
+
"sections": [
|
|
398
|
+
{
|
|
399
|
+
"heading": "Security",
|
|
400
|
+
"items": [
|
|
401
|
+
{
|
|
402
|
+
"title": "File-upload lifecycle methods enforce per-upload ownership (IDOR fix)",
|
|
403
|
+
"body": "acceptChunk, finalize, status, and cancelUpload previously authorized only against the coarse fileUpload.<op> capability scope, never against the upload's recorded owner, so a caller with that scope could act on any actor's upload by guessing or enumerating its uploadId. Each method now refuses a caller whose identity (actor.id || actor.userId) does not match the owner stored at init() — raising fileUpload OWNERSHIP_VIOLATION — so an upload is reachable only by the actor that created it. Cross-actor admin tooling opts in via create({ allowCrossActor: true }) plus the new fileUpload.admin permission scope (required when permissions are wired); list({ scopeToActor: false }) remains the deliberate cross-actor listing view. Operators whose workflows legitimately hand an upload between actors must set allowCrossActor and grant fileUpload.admin to the acting principal."
|
|
404
|
+
}
|
|
405
|
+
]
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
"heading": "Detectors",
|
|
409
|
+
"items": [
|
|
410
|
+
{
|
|
411
|
+
"title": "fileUpload lifecycle methods must enforce ownership",
|
|
412
|
+
"body": "A codebase-patterns detector flags any fileUpload lifecycle method that loads an upload by a request-supplied uploadId without calling the ownership check before acting, so a newly added method cannot silently reintroduce the IDOR."
|
|
413
|
+
}
|
|
414
|
+
]
|
|
415
|
+
}
|
|
416
|
+
]
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
"version": "0.15.54",
|
|
420
|
+
"date": "2026-06-28",
|
|
421
|
+
"headline": "SAML federation-metadata (MDQ) verification binds the signature to the consumed EntityDescriptor and refuses signature-wrapping, closing an IdP trust-anchor takeover",
|
|
422
|
+
"summary": "b.auth.saml.fetchMdq validates signed federation metadata with the same XML-dsig verifier the assertion path uses, but discarded the verified reference and never bound it to the EntityDescriptor the operator consumes — unlike verifyResponse, which already enforces that the signature covers the element it trusts. Combined with a first-child Signature lookup, an attacker controlling or interposing on the MDQ source could wrap a genuinely federation-signed EntityDescriptor under a forged container (or bury it in an EntityDescriptor whose own ID and signing certificate are attacker-chosen): the signature still validated over the buried element, fetchMdq returned the whole document, and the operator extracted the attacker's certificate as the IdP signing key — a full SAML authentication bypass (the CVE-2024-45409 / ruby-saml signature-wrapping class). fetchMdq now requires the metadata root to be a single EntityDescriptor, binds the verified reference to that root's ID (mirroring verifyResponse), confirms the signed entityID is the one requested, and refuses a duplicate top-level Signature.",
|
|
423
|
+
"sections": [
|
|
424
|
+
{
|
|
425
|
+
"heading": "Security",
|
|
426
|
+
"items": [
|
|
427
|
+
{
|
|
428
|
+
"title": "fetchMdq binds the federation signature to the consumed EntityDescriptor (XML signature-wrapping defense)",
|
|
429
|
+
"body": "On the MDQ trust-bootstrap path, fetchMdq verified the federation signature but discarded the reference it covered, so the signature did not have to cover the EntityDescriptor whose signing certificate the operator subsequently trusts. An attacker with a malicious or interposed MDQ responder could pair a genuine federation signature over a buried entity with a forged outer/sibling EntityDescriptor carrying their own signing certificate; fetchMdq accepted and returned the wrapped document, and the operator installed the attacker certificate as the IdP trust anchor — forging arbitrary assertions thereafter. fetchMdq now refuses a non-EntityDescriptor root (the EntitiesDescriptor wrapper), refuses a duplicate top-level Signature, binds the verified reference to the document-root EntityDescriptor's ID, and confirms the signed entityID matches the requested one — mirroring the wrapping defenses verifyResponse already applies to assertions. New error codes: auth-saml/mdq-not-entity-descriptor, auth-saml/mdq-duplicate-signature, auth-saml/mdq-entity-mismatch (plus the existing auth-saml/signed-different-element for a buried-reference root). Verification of legitimately signed single-entity metadata is unchanged."
|
|
430
|
+
}
|
|
431
|
+
]
|
|
432
|
+
}
|
|
433
|
+
]
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
"version": "0.15.53",
|
|
437
|
+
"date": "2026-06-28",
|
|
438
|
+
"headline": "DKIM (and ARC) verification refuses a body-length-limited (`l=`) signature once content has been appended past the signed octets, closing the append-after-signature attack",
|
|
439
|
+
"summary": "A DKIM signature with an `l=` body-length tag covers only the first `l=` octets of the canonicalized body, so anyone in the delivery path can append arbitrary unsigned content after the signed prefix and the body hash still matches. b.mail.dkim.verify honored such a signature and returned `pass`, with only a non-load-bearing warning that no consumer read — so b.mail.inbound.verify granted an aligned DMARC `pass` to a message whose delivered body diverged from the signed bytes (RFC 6376 §8.2). Verification now refuses an `l=` signature once the delivered body extends beyond the signed octets: the verified prefix no longer authenticates the appended content, so the result is a body-hash `fail` rather than a `pass`. Signatures whose `l=` exactly covers the body (no appended content) are unchanged, and an operator who must accept legacy `l=` senders can opt back in with verify({ acceptBodyLengthLimit: true }). ARC-Message-Signature verification, which reuses the same verifier, inherits the refusal unconditionally.",
|
|
440
|
+
"sections": [
|
|
441
|
+
{
|
|
442
|
+
"heading": "Security",
|
|
443
|
+
"items": [
|
|
444
|
+
{
|
|
445
|
+
"title": "DKIM verify refuses an l= signature with content appended past the signed octets",
|
|
446
|
+
"body": "The DKIM `l=` tag (RFC 6376 §3.5) limits the body hash to the first `l=` canonicalized octets, leaving any trailing body unsigned — the documented append-after-signature exposure (RFC 6376 §8.2). b.mail.dkim.verify hashed the prefix, matched `bh=`, and returned `pass` for the whole message, even though the recipient received appended content the signer never authenticated; the only signal was a warning string that b.mail.inbound.verify / dmarc.evaluate never inspected, so a forged trailer rode an aligned DMARC `pass`. verify now returns a body-hash `fail` when an `l=` signature leaves delivered body content beyond the signed octets (the framework already refuses `l=` at sign time). A signature whose `l=` covers the entire body still verifies; verify({ acceptBodyLengthLimit: true }) restores the prior accept-with-warning behavior for operators with legacy `l=` senders."
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
"title": "ARC-Message-Signature verification inherits the same refusal",
|
|
450
|
+
"body": "ARC chain validation verifies each hop's ARC-Message-Signature through the same DKIM verifier, so an AMS carrying `l=` with appended content is now refused as a body-hash `fail` — failing the chain — rather than validating over a prefix. The ARC path does not expose the acceptBodyLengthLimit opt-out, so it is unconditionally fail-closed for an appended-content `l=`."
|
|
451
|
+
}
|
|
452
|
+
]
|
|
453
|
+
}
|
|
454
|
+
]
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
"version": "0.15.52",
|
|
458
|
+
"date": "2026-06-28",
|
|
459
|
+
"headline": "An email address carrying more than one `@` is now refused instead of having its domain read from the wrong segment, closing a DMARC/SPF alignment bypass and an outbound mis-delivery path",
|
|
460
|
+
"summary": "An RFC 5322 addr-spec has exactly one `@`, but blamejs derived the domain from a multi-@ address inconsistently across sites. For an inbound From like `user@attacker.example@victim.example`, the From-header parser took the RIGHTMOST `@` segment (victim.example) to gate DMARC and set the displayed author, while the DMARC and SPF evaluators re-derived the domain from the LEFTMOST segment (attacker.example) via split(\"@\")[1]. So SPF/DMARC could authenticate a domain the attacker controls while the message displays the victim's domain — and the victim's `_dmarc` policy was never consulted, so a strict `p=reject` author domain could be impersonated. b.mail now refuses any From or MAIL FROM addr-spec with more than one `@` at every domain-derivation site (inbound From extraction, dmarc.evaluate, and spf.verify), and outbound delivery refuses a multi-@ recipient as a permanent bad-address rather than routing to the leftmost segment's MX.",
|
|
461
|
+
"sections": [
|
|
462
|
+
{
|
|
463
|
+
"heading": "Security",
|
|
464
|
+
"items": [
|
|
465
|
+
{
|
|
466
|
+
"title": "A multi-@ From address can no longer split DMARC/SPF alignment from the displayed author",
|
|
467
|
+
"body": "The inbound From-header parser derived the author domain from the rightmost `@` of a bare addr-spec, while dmarc.evaluate and spf.verify re-derived it from the leftmost `@` (split(\"@\")[1]). A crafted From or MAIL FROM such as `user@attacker.example@victim.example` therefore authenticated attacker.example (which the attacker controls, with a permissive SPF/DMARC posture) while the displayed author was victim.example, whose `_dmarc` record was never queried — a DMARC alignment bypass (CWE-290) against any domain that publishes p=reject. An addr-spec has exactly one `@` (RFC 5322 §3.4.1); a From or MAIL FROM with more than one `@` is now treated as malformed at every derivation site (inbound From extraction yields no author domain and fails closed to reject; dmarc.evaluate and spf.verify refuse it), so the authenticated domain and the displayed domain can no longer diverge."
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
"title": "Outbound delivery refuses a multi-@ recipient instead of routing to the wrong host",
|
|
471
|
+
"body": "Outbound SMTP delivery derived the recipient domain with split(\"@\")[1], so a multi-@ recipient like `victim@internal.host@external.com` would have its MX looked up for the leftmost segment (internal.host) and the message routed there — a mis-delivery / exfiltration path when recipients are influenced by untrusted input. A recipient with more than one `@` is now refused as a permanent bad-address before any MX lookup."
|
|
472
|
+
}
|
|
473
|
+
]
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
"heading": "Detectors",
|
|
477
|
+
"items": [
|
|
478
|
+
{
|
|
479
|
+
"title": "Leftmost-@ email-domain derivation must reject a multi-@ address",
|
|
480
|
+
"body": "A codebase-patterns detector flags any `str.split(\"@\")[1]` email-domain derivation that is not preceded, within its function, by a single-@ rejection (`str.indexOf(\"@\") !== str.lastIndexOf(\"@\")`). This keeps the leftmost-vs-rightmost `@` divergence from being reintroduced at a new derivation site; a purely informational, non-routing use is marked inline."
|
|
481
|
+
}
|
|
482
|
+
]
|
|
483
|
+
}
|
|
484
|
+
]
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
"version": "0.15.51",
|
|
488
|
+
"date": "2026-06-29",
|
|
489
|
+
"headline": "`b.guardOauth` and `b.session.verify` now fail closed when a backing store errors, instead of silently accepting a request whose security check could not be completed",
|
|
490
|
+
"summary": "Two verifiers swallowed an error from a backing store and continued as if the check had passed. b.guardOauth's authorization-code replay defense wrapped the operator's seenCodeStore.hasSeen() call in a silent catch, so a store backend outage skipped the replay check entirely and a replayed authorization code was accepted — even though codeReusePolicy is reject at every profile. A store error now adds a high-severity oauth.code-reuse-unverifiable refusal, so the flow is denied (fail-closed) when reuse cannot be ruled out. b.session.verify enforces a device-fingerprint binding stored in the session's sealed data column; when that column could not be decrypted (key-rotation skew, database corruption, or a tamper of the independently-sealed cell) the failure was swallowed and the entire fingerprint gate — including requireFingerprintMatch and maxAnomalyScore — was skipped, so a strict-mode session was accepted from any device. An unreadable binding under a strict policy is now treated as a failure to prove the binding and the session is refused.",
|
|
491
|
+
"sections": [
|
|
492
|
+
{
|
|
493
|
+
"heading": "Security",
|
|
494
|
+
"items": [
|
|
495
|
+
{
|
|
496
|
+
"title": "OAuth authorization-code replay check fails closed on a store error",
|
|
497
|
+
"body": "b.guardOauth's code-reuse defense calls the operator-supplied seenCodeStore.hasSeen(code) to refuse a replayed authorization code (RFC 6749 §10.5). The call was wrapped in a drop-silent catch, so when the store backend errored (e.g. a Redis/DB outage) the exception was swallowed, no replay issue was raised, and the flow validated — accepting a code that could not be proven unused, despite codeReusePolicy being reject at every profile. A store error now raises a high-severity oauth.code-reuse-unverifiable issue, so the gate refuses the flow when reuse cannot be ruled out."
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
"title": "Session verify fails closed when the device-fingerprint binding can't be decrypted",
|
|
501
|
+
"body": "b.session.verify stores the device-fingerprint binding inside the session's AEAD-sealed data column. When that column failed to decrypt or parse (key-rotation skew, database corruption, or a tamper of the separately-sealed cell), the failure was swallowed and the fingerprint gate was skipped entirely — so a session under a strict binding policy (requireFingerprintMatch:true or a maxAnomalyScore threshold) was accepted from any device, silently voiding the advertised drift-kills-the-session guarantee. A present-but-undecryptable binding column under a strict policy is now treated as a failure to prove the binding and the session is refused; sessions without a binding, and the default non-strict mode, are unchanged."
|
|
502
|
+
}
|
|
503
|
+
]
|
|
504
|
+
}
|
|
505
|
+
]
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
"version": "0.15.50",
|
|
509
|
+
"date": "2026-06-28",
|
|
510
|
+
"headline": "`b.mail.bimi` closes a VMC certificate authorization bypass, and the host/origin comparisons in `b.ssrfGuard`, `b.middleware.csrfProtect`, and `b.mail.dmarc` now canonicalize both sides so case, trailing-dot, and IDN differences cannot decide a security check",
|
|
511
|
+
"summary": "Four security and correctness decisions compared a host, origin, or domain where one side was normalized and the other was not, so two values that denote the same host in different encodings reached different verdicts. The most serious was in b.mail.bimi.fetchAndVerifyMark: when a VMC/CMC certificate's URI Subject Alternative Name could not be parsed as a URL (for example, a host carrying userinfo), the matcher fell back to a raw substring search of the whole SAN string — so a CA-chained certificate whose real host was attacker-controlled but whose SAN contained the victim domain anywhere (in the userinfo or path) was accepted to vouch for that victim domain. The fallback is removed (an unparseable URI SAN now fails closed) and both the certificate host and the BIMI domain are canonicalized before comparison. b.ssrfGuard allow/deny lists compared the operator's entries verbatim against the URL parser's already-lowercased host, so a mixed-case or trailing-dot deny entry silently failed to block its host; both sides now canonicalize through canonicalizeHost. b.middleware.csrfProtect canonicalized the candidate Origin via the URL parser but built the same-origin baseline by raw concatenation of the Host header, refusing a legitimate same-origin request whose Host was mixed-case or carried an explicit default port; the baseline and allowedOrigins now go through the same canonicalizer. b.mail.dmarc strict alignment compared the From and SPF/DKIM authentication domains with only case-folding, failing an aligned message whose authentication domain carried a trailing dot or an IDN label; both are now canonicalized the same way the relaxed path already was. A new b.publicSuffix.canonicalDomain primitive provides the shared encoding-stable host form.",
|
|
512
|
+
"sections": [
|
|
513
|
+
{
|
|
514
|
+
"heading": "Security",
|
|
515
|
+
"items": [
|
|
516
|
+
{
|
|
517
|
+
"title": "BIMI VMC certificate SubjectAltName authorization bypass closed",
|
|
518
|
+
"body": "b.mail.bimi.fetchAndVerifyMark binds a verified mark certificate to the BIMI domain via its Subject Alternative Name. When a URI SAN could not be parsed as a URL (e.g. a host with userinfo, or a malformed/homograph URI), the matcher fell back to a raw substring search of the entire SAN string, so a CA-chained certificate whose actual host was attacker-controlled — but whose SAN contained the victim domain as a substring (in the userinfo or path) — was accepted to vouch for the victim domain. The substring fallback is removed: a URI SAN the URL parser refuses now fails closed, and both the certificate host and the BIMI domain are canonicalized (lowercase, trailing-dot strip, IDN A-label) before an exact host comparison."
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
"title": "SSRF allow/deny lists now match the host case-insensitively",
|
|
522
|
+
"body": "b.ssrfGuard.createAllowlist compared each operator allow/deny entry verbatim against the URL parser's host, which is already lowercased. A mixed-case or trailing-dot denylist entry therefore failed to match its own host and did not block it. Both the host and each non-CIDR entry now canonicalize through canonicalizeHost before comparison, so a denylisted host is blocked regardless of the case or trailing-dot form the operator wrote."
|
|
523
|
+
}
|
|
524
|
+
]
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
"heading": "Fixed",
|
|
528
|
+
"items": [
|
|
529
|
+
{
|
|
530
|
+
"title": "CSRF Origin check no longer refuses a legitimate same-origin request",
|
|
531
|
+
"body": "b.middleware.csrfProtect canonicalized the incoming Origin/Referer through the URL parser but built the same-origin baseline by concatenating the raw Host header, and compared allowedOrigins verbatim. A legitimate same-origin POST whose Host header was mixed-case or carried an explicit default port (:80/:443) was refused as cross-origin. The baseline and each allowedOrigins entry now pass through the same origin canonicalizer as the candidate."
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
"title": "DMARC strict alignment canonicalizes the compared domains",
|
|
535
|
+
"body": "b.mail.dmarc strict alignment (aspf=s / adkim=s) compared the From domain against the SPF/DKIM authentication domain with only case-folding, while the relaxed path already normalized via the public-suffix lookup. An aligned message whose authentication domain carried a trailing dot or an IDN label was wrongly failed. Both domains are now canonicalized identically before the strict comparison."
|
|
536
|
+
}
|
|
537
|
+
]
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
"heading": "Added",
|
|
541
|
+
"items": [
|
|
542
|
+
{
|
|
543
|
+
"title": "b.publicSuffix.canonicalDomain — encoding-stable host form",
|
|
544
|
+
"body": "Returns the bare canonical host form of a domain (lowercase, single trailing dot stripped, IDN labels as their A-label/punycode form) for identity comparison, without walking the public-suffix list. Two values that denote the same host in different encodings return the same string; an invalid or hostile host returns the empty string and matches nothing. It is the shared building block for the DMARC-alignment and certificate SAN authorization comparisons above."
|
|
545
|
+
}
|
|
546
|
+
]
|
|
547
|
+
}
|
|
548
|
+
]
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
"version": "0.15.49",
|
|
552
|
+
"date": "2026-06-28",
|
|
553
|
+
"headline": "`b.crypto.httpSig` now canonicalizes `@query-param` names and values per RFC 9421 §2.2.8, so its HTTP Message Signatures interoperate with conformant peers",
|
|
554
|
+
"summary": "b.crypto.httpSig built the signature base for a @query-param component from the raw on-wire query bytes — the name was matched with encodeURIComponent and the value was emitted verbatim, with no decode-then-reencode. RFC 9421 §2.2.8 requires both the name and the value to be canonicalized: parsed as application/x-www-form-urlencoded (so a '+' and a %20 both become a space, and hex case is normalized) and then re-encoded, with a space rendered as %20. Because the framework signed and verified with the same raw bytes, blamejs-to-blamejs round-trips still worked, but a signature covering a query parameter whose name or value required encoding (a space, a '+', mixed or lowercase percent-encoding) did not match the base a conformant peer constructs — and an emitted identifier could even carry a literal space that the verifier then could not parse. Sign now emits the canonical percent-encoded name and signs the canonical value, and both sign and verify resolve the value through the same canonicalizer; the framework's base now matches the RFC's own published §2.2.8 example vectors. The whole-query @query component (§2.2.7), which the RFC defines as the raw encoded query, is unchanged, and signatures over plain-ASCII parameter names and values are byte-identical to before.",
|
|
555
|
+
"sections": [
|
|
556
|
+
{
|
|
557
|
+
"heading": "Fixed",
|
|
558
|
+
"items": [
|
|
559
|
+
{
|
|
560
|
+
"title": "HTTP Message Signatures @query-param canonicalization (RFC 9421 §2.2.8)",
|
|
561
|
+
"body": "b.crypto.httpSig now canonicalizes a @query-param component's name and value per RFC 9421 §2.2.8 — decode as application/x-www-form-urlencoded then re-encode, so a '+'-encoded space becomes %20, a %20 and a '+' resolve identically, and percent-encoding hex case is normalized to uppercase. Previously the name was matched with encodeURIComponent and the value was emitted raw, so a signature covering a query parameter that required encoding did not match the signature base a conformant RFC 9421 peer builds, and an emitted ;name=\"...\" identifier could carry a literal space the verifier could not parse. Sign emits the canonical name and signs the canonical value; verify resolves the value through the same canonicalizer and reproduces the received identifier per §2.5. The framework's signature base now matches the RFC's published §2.2.8 example vectors. The whole-query @query component (§2.2.7) stays the raw encoded query, and signatures over plain-ASCII parameters are byte-identical to before."
|
|
562
|
+
}
|
|
563
|
+
]
|
|
564
|
+
}
|
|
565
|
+
]
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
"version": "0.15.48",
|
|
569
|
+
"date": "2026-06-28",
|
|
570
|
+
"headline": "`b.network.dns.tsig` now accepts a message it signed with a non-default Original ID, fixing a sign/verify digest mismatch that made the originalId option non-functional (RFC 8945)",
|
|
571
|
+
"summary": "b.network.dns.tsig.verify restores the Original ID carried in the TSIG RDATA into the DNS message header before computing the HMAC, so a signed message survives an on-wire ID rewrite by a forwarder. b.network.dns.tsig.sign digested the message's current header ID instead of the Original ID, so any message signed with the originalId option set to a value other than the message's header ID produced a MAC the framework's own verify rejected (BADSIG) — the advertised originalId option was effectively non-functional for any non-default value. sign() now digests the Original-ID form, matching verify(); when originalId equals the message's header ID (the default) the digest is byte-for-byte identical to before, so existing signatures and the reference vectors are unaffected.",
|
|
572
|
+
"sections": [
|
|
573
|
+
{
|
|
574
|
+
"heading": "Fixed",
|
|
575
|
+
"items": [
|
|
576
|
+
{
|
|
577
|
+
"title": "TSIG: a message signed with a non-default Original ID now verifies (RFC 8945)",
|
|
578
|
+
"body": "b.network.dns.tsig.verify restores the Original ID carried in the TSIG RDATA into the DNS message header before computing the HMAC, so a signed message survives an on-wire ID rewrite by a forwarder. b.network.dns.tsig.sign digested the message's current header ID instead of the Original ID, so any message signed with the originalId option set to a value other than the message's header ID produced a MAC the framework's own verify rejected (BADSIG). sign() now digests the Original-ID form, matching verify(); when originalId equals the message's header ID (the default) the digest is byte-for-byte identical to before, so existing signatures and the reference vectors are unaffected."
|
|
579
|
+
}
|
|
580
|
+
]
|
|
581
|
+
}
|
|
582
|
+
]
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
"version": "0.15.47",
|
|
586
|
+
"date": "2026-06-28",
|
|
587
|
+
"headline": "`b.mail.arc.verify` now returns chainStatus=pass for a cryptographically valid ARC chain — three defects in the ARC-Message-Signature verification path that made every real chain fail are fixed",
|
|
588
|
+
"summary": "ARC-Message-Signature (AMS) verification reuses the DKIM verifier against a synthetic message, and three independent defects in that seam caused b.mail.arc.verify to reject every cryptographically valid ARC chain — its own and those from upstream relays. First, the AMS i= tag is an RFC 8617 instance number (1..50), not a DKIM Agent/User Identifier, but it was run through the RFC 6376 §3.5 AUID-must-be-a-subdomain-of-d= check, which permerrored every chain. Second, the synthetic renames the AMS header to DKIM-Signature so the DKIM verifier can find it, but the signature header was then canonicalized under that renamed field name instead of ARC-Message-Signature, so the b= signature never matched what the relay signed. Third, when sealing hop i>=2 the signer canonicalized a prior hop's ARC-Authentication-Results into the AMS instead of the current hop's, so multi-hop chains failed verification past the first hop. All three are fixed: arc.verify now confirms valid single- and multi-hop chains as cv=pass. The RFC 6376 §3.5 AUID/d= binding check remains a non-opt-out default on the public b.mail.dkim.verify path — the ARC reuse signal that skips it is framework-internal and cannot be set through the public options.",
|
|
589
|
+
"sections": [
|
|
590
|
+
{
|
|
591
|
+
"heading": "Fixed",
|
|
592
|
+
"items": [
|
|
593
|
+
{
|
|
594
|
+
"title": "ARC chain verification now succeeds for valid chains (it previously failed every one)",
|
|
595
|
+
"body": "b.mail.arc.verify reused the DKIM verifier to check each ARC-Message-Signature, and three defects in that path made it reject all cryptographically valid ARC chains. (1) The AMS i= instance number (RFC 8617 §4.1.2) was treated as a DKIM AUID and rejected by the RFC 6376 §3.5 AUID/d= binding check (permerror). (2) The synthetic message renames the AMS header to DKIM-Signature so the verifier can locate it, but the signature header was canonicalized under the renamed name rather than ARC-Message-Signature, so the b= value could never match the relay's signature. (3) For hops at instance 2 and beyond, b.mail.arc.sign canonicalized a prior hop's ARC-Authentication-Results into the AMS rather than the current hop's, breaking verification past the first hop. arc.verify now returns chainStatus=pass for valid single- and multi-hop chains; the DKIM AUID check stays enforced on the public DKIM verify path."
|
|
596
|
+
}
|
|
597
|
+
]
|
|
598
|
+
}
|
|
599
|
+
]
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
"version": "0.15.46",
|
|
603
|
+
"date": "2026-06-28",
|
|
604
|
+
"headline": "`b.auth.fidoMds3` now enforces basicConstraints cA:TRUE on every intermediate link of the MDS3 x5c chain, closing a bypass where a non-CA certificate spliced in as an intermediate could sign a forged FIDO metadata BLOB that fido-mds3 accepted",
|
|
605
|
+
"summary": "fido-mds3's x5c chain validation checked each intermediate link with X509Certificate.checkIssued(), which validates the issuer/subject linkage and signature but does NOT enforce the basicConstraints CA bit; only the final tail-to-root anchor used the framework's cA-enforcing helper. An attacker holding a non-CA (cA:FALSE) end-entity certificate that legitimately chains to a trusted MDS3 root (and its private key) could splice it in as an intermediate, sign a forged leaf with it, and have fido-mds3 accept an attacker-authored MDS3 metadata BLOB as authentic — the classic basicConstraints bypass (CVE-2002-0862 class). Every intermediate link now routes through x509Chain.issuerValidlyIssued, which enforces basicConstraints cA:TRUE in addition to the issuance and signature linkage, matching the mdoc / tsa / bimi / content-credentials / S-MIME chain walkers. A codebase guard now fails the build if any chain walker uses a bare issuance checkIssued() instead.",
|
|
606
|
+
"sections": [
|
|
607
|
+
{
|
|
608
|
+
"heading": "Security",
|
|
609
|
+
"items": [
|
|
610
|
+
{
|
|
611
|
+
"title": "fido-mds3 x5c intermediate links enforce the CA bit (basicConstraints bypass closed)",
|
|
612
|
+
"body": "b.auth.fidoMds3 validated each intermediate x5c link with X509Certificate.checkIssued(), which does not enforce basicConstraints cA:TRUE, so a non-CA certificate inserted as an intermediate was accepted as an issuer. An attacker with a cA:FALSE end-entity cert chaining to a trusted MDS3 root could sign a forged leaf and have a forged FIDO metadata BLOB accepted as authentic (CVE-2002-0862 class). Each intermediate link now routes through x509Chain.issuerValidlyIssued (cA:TRUE + issuance + signature), the same hardened test the framework's other certificate-chain walkers already use. The default GlobalSign MDS3 root and operator-supplied caCertificate roots are both covered."
|
|
613
|
+
}
|
|
614
|
+
]
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
"heading": "Detectors",
|
|
618
|
+
"items": [
|
|
619
|
+
{
|
|
620
|
+
"title": "Build guard: a cert-chain issuance link must use issuerValidlyIssued, not bare checkIssued",
|
|
621
|
+
"body": "A codebase guard now fails the build if a certificate-chain walker validates an issuance link with a bare X509Certificate.checkIssued() (which omits the basicConstraints CA check) instead of x509Chain.issuerValidlyIssued, so the basicConstraints bypass cannot reappear at a new walker. A self-signed-root check (cert.checkIssued(cert)) is not an issuance link and is unaffected."
|
|
622
|
+
}
|
|
623
|
+
]
|
|
624
|
+
}
|
|
625
|
+
]
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
"version": "0.15.45",
|
|
629
|
+
"date": "2026-06-28",
|
|
630
|
+
"headline": "`b.selfUpdate.swap` now re-hashes the asset against the hash `verify` returned and refuses to install on a mismatch, closing the window where an asset swapped between verify and swap could be installed after the signature check passed",
|
|
631
|
+
"summary": "selfUpdate.verify (signature-checks the asset and returns its hash) and selfUpdate.swap (renames the new asset into place) were two separate path-keyed operations with nothing binding the bytes swap installs to the bytes verify checked. An attacker able to write the asset path in the window between the two calls — or who pointed verify at a different inode via a symlink — could have the signature-verified bytes replaced before swap renamed the file into place, installing unverified code. swap now requires an expectedHash and re-hashes the from bytes against it immediately before the install, refusing with selfupdate/swap-hash-mismatch on any difference; the re-check runs right before the rename so the residual is a sub-millisecond window rather than the operator-controlled gap between verify and swap. Because swap now requires expectedHash, callers must pass the hash that verify returned (await verify(...) then swap({ ..., expectedHash: result.hash })).",
|
|
632
|
+
"sections": [
|
|
633
|
+
{
|
|
634
|
+
"heading": "Security",
|
|
635
|
+
"items": [
|
|
636
|
+
{
|
|
637
|
+
"title": "self-update install is bound to the signature-verified bytes (verify→swap TOCTOU closed)",
|
|
638
|
+
"body": "selfUpdate.swap renamed the new asset into place with no integrity re-check, so the bytes it installed were not bound to the bytes selfUpdate.verify had signature-checked: an attacker who could write the asset path between verify and swap (or repoint a symlink) could install unverified code. swap now re-hashes the from bytes against a required expectedHash (the hash verify returns) immediately before the install and refuses a mismatch (selfupdate/swap-hash-mismatch), with the check positioned right before the rename to minimize the residual window. A symlinked or differing-inode asset is caught the same way, because the install source is what gets re-hashed."
|
|
639
|
+
}
|
|
640
|
+
]
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
"heading": "Migration",
|
|
644
|
+
"items": [
|
|
645
|
+
{
|
|
646
|
+
"title": "selfUpdate.swap requires expectedHash",
|
|
647
|
+
"body": "swap now requires an expectedHash option (refused at the call with selfupdate/bad-expected-hash if absent). Pass the hash that selfUpdate.verify returns: const v = await selfUpdate.verify({ assetPath, signaturePath, pubkeyPem }); await selfUpdate.swap({ from, to, backupTo, expectedHash: v.hash }). An optional hashAlgo (default sha3-512, matching verify's default) selects the digest when verify used a non-default algorithm."
|
|
648
|
+
}
|
|
649
|
+
]
|
|
650
|
+
}
|
|
651
|
+
]
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
"version": "0.15.44",
|
|
655
|
+
"date": "2026-06-28",
|
|
656
|
+
"headline": "`b.fileUpload` now rejects the bare path tokens \".\" and \"..\" as upload IDs, closing a path traversal where a \"..\" id resolved to the staging directory's parent and a cancel or cleanup could recursively delete it",
|
|
657
|
+
"summary": "b.fileUpload validated the uploadId with a character-class regex that permits the dot character, so the bare path tokens \".\" and \"..\" passed validation even though they are not ordinary identifiers. The uploadId is joined under the configured staging directory to locate an upload's files, so \".\" resolved to the staging directory itself and \"..\" to its parent: an operation keyed on a \"..\" uploadId acted OUTSIDE the staging directory. The most damaging path is cancelUpload (and finalize/expiry cleanup), which removes the upload directory with a recursive rmSync — keyed on \"..\" that would recursively delete the staging directory's parent; chunk writes for a \"..\" id also land outside staging. The validator now rejects \".\" and \"..\" before any filesystem operation. Every public method validates the uploadId there, so init, acceptChunk, finalize, status, and cancelUpload are all covered, and legitimate dotted IDs (for example \"build.v2\") are unaffected.",
|
|
658
|
+
"sections": [
|
|
659
|
+
{
|
|
660
|
+
"heading": "Security",
|
|
661
|
+
"items": [
|
|
662
|
+
{
|
|
663
|
+
"title": "Upload IDs of \".\" and \"..\" are refused (path-traversal / parent-directory deletion)",
|
|
664
|
+
"body": "fileUpload's uploadId regex allows the dot character, so the bare tokens \".\" and \"..\" passed validation and, joined under the staging directory, resolved to the staging directory or its parent. An operation keyed on a \"..\" id acted outside staging — most seriously cancelUpload / cleanup, whose recursive rmSync would have deleted the staging directory's parent, and chunk writes that would land outside staging. The validator now rejects \".\" and \"..\" up front; because every public method validates the id, init / acceptChunk / finalize / status / cancelUpload are all covered. Legitimate dotted IDs are unaffected. Operators who never pass caller-controlled upload IDs were not exposed."
|
|
665
|
+
}
|
|
666
|
+
]
|
|
667
|
+
}
|
|
668
|
+
]
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
"version": "0.15.43",
|
|
672
|
+
"date": "2026-06-28",
|
|
673
|
+
"headline": "`b.auth.lockout`'s failed-attempt counter now accumulates with an atomic compare-and-set, so a brute-force attacker spreading failed logins across multiple nodes can no longer lose increments and stay under the lockout threshold",
|
|
674
|
+
"summary": "b.auth.lockout tracked failed attempts with a cache read-modify-write: read the counter, increment it, write it back. On a multi-node deployment two concurrent failures for the same account on different nodes both read the same value, each added one, and one write clobbered the other — a lost update that let an attacker spreading attempts across nodes exceed maxAttempts without ever triggering the lockout, weakening brute-force protection on a cluster. The counter now runs the whole decision (window decay, increment, lockout-ladder) through the cache's atomic update() (compare-and-set, retried on the cluster backend under contention), so every failure is counted regardless of which node records it. The lockout's documented fail-open posture is preserved — a genuinely unreachable cache still allows the attempt and signals the error — but a cache backend that cannot perform an atomic update now surfaces loud at first use instead of silently disabling the lockout.",
|
|
675
|
+
"sections": [
|
|
676
|
+
{
|
|
677
|
+
"heading": "Security",
|
|
678
|
+
"items": [
|
|
679
|
+
{
|
|
680
|
+
"title": "Lockout failure counter is atomic across nodes (no lost increments)",
|
|
681
|
+
"body": "b.auth.lockout accumulated failed attempts with a non-atomic cache get -> increment -> set, so concurrent failures for one account across a multi-node deployment lost increments and an attacker could exceed maxAttempts without engaging the lockout. The counter now uses the cache's atomic compare-and-set update(), counting every failure across nodes, with the existing exponential lockout ladder and window-decay logic unchanged. The cache backing a lockout must support atomic update() (b.cache does; the create() check now requires it), and a backend that can't commit an atomic update surfaces loud at first use rather than silently disabling brute-force protection. A genuinely unreachable cache still fails open by design."
|
|
682
|
+
}
|
|
683
|
+
]
|
|
684
|
+
}
|
|
685
|
+
]
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
"version": "0.15.42",
|
|
689
|
+
"date": "2026-06-28",
|
|
690
|
+
"headline": "The agent orchestrator and tenant registries serialize registration per name, so two concurrent register() calls for the same name can no longer both create a row (duplicate-create / lost registration), and a new b.safeAsync.keyedSerializer exposes that per-key serialization",
|
|
691
|
+
"summary": "b.agent.orchestrator and b.agent.tenant registered a name with a check-then-create: read the backend row for the name, throw a duplicate error if it exists, otherwise write the new row. Because the read and the write are separated by an await, two concurrent register() calls for the same name both observed absence and both wrote — a duplicate-create where the second silently clobbered the first and both callers saw success, violating the one-row-per-name invariant. register() (and unregister()) now run through a per-name in-process serializer, so concurrent calls for the same name apply one at a time and the second is correctly refused as a duplicate; distinct names still run concurrently. The serializer is exposed as b.safeAsync.keyedSerializer() for any read-modify-write or check-then-create that must not interleave per key. It is in-process only: a registry backend shared across processes still needs its own atomic create or unique constraint to refuse a cross-process duplicate.",
|
|
692
|
+
"sections": [
|
|
693
|
+
{
|
|
694
|
+
"heading": "Fixed",
|
|
695
|
+
"items": [
|
|
696
|
+
{
|
|
697
|
+
"title": "Agent orchestrator + tenant registries serialize registration per name (no duplicate-create race)",
|
|
698
|
+
"body": "b.agent.orchestrator.register and b.agent.tenant.register did a check-then-create (await backend.get -> throw-if-exists -> await backend.set) with an await between the read and the write, so two concurrent registrations of the same name both passed the duplicate check and both wrote — the second silently clobbering the first while both callers saw success. Registration now serializes per name in-process, so concurrent calls for one name apply sequentially and the second is refused with the duplicate error; distinct names are unaffected. A backend shared across processes still needs its own uniqueness constraint to refuse a cross-process duplicate."
|
|
699
|
+
}
|
|
700
|
+
]
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
"heading": "Added",
|
|
704
|
+
"items": [
|
|
705
|
+
{
|
|
706
|
+
"title": "b.safeAsync.keyedSerializer — serialize async work per key",
|
|
707
|
+
"body": "b.safeAsync.keyedSerializer() returns a { run(key, fn) } that queues fn behind any in-flight or queued work for the same key and runs it once they settle, so a read-modify-write or a check-then-create on a shared store cannot interleave with another call for the same key in the same process. Distinct keys run concurrently, and the per-key chain is dropped once it drains. It is the serialization the agent registries now use, and the same primitive backs the lockout and bot-challenge per-key failure counters."
|
|
708
|
+
}
|
|
709
|
+
]
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
"heading": "Detectors",
|
|
713
|
+
"items": [
|
|
714
|
+
{
|
|
715
|
+
"title": "Build guard: a registry check-then-create must serialize per key",
|
|
716
|
+
"body": "A codebase guard now fails the build if a primitive does an async check-then-create on a pluggable backend (await backend.get -> throw a /duplicate error -> await backend.set) without serializing per key, so the duplicate-create race fixed here cannot reappear at a new registry."
|
|
717
|
+
}
|
|
718
|
+
]
|
|
719
|
+
}
|
|
720
|
+
]
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
"version": "0.15.41",
|
|
724
|
+
"date": "2026-06-28",
|
|
725
|
+
"headline": "Counters kept on a shared cache — byte quotas and the static server's bandwidth/concurrency caps — now accumulate with an atomic compare-and-set, so concurrent requests can no longer lose each other's charges and slip under the limit",
|
|
726
|
+
"summary": "Several caps maintained a counter on a cache with a non-atomic read-modify-write: read the current value, add to it in memory, write it back. On a cache shared across nodes, two concurrent requests both read the same value, each added only its own contribution, and one write clobbered the other — a lost update that under-counted the cap and let a peer slip under it. b.network.byteQuota (and the b.middleware.dailyByteQuota that composes it) and the static server's per-actor/global bandwidth and per-actor concurrency caps all did this. They now accumulate through the cache's atomic update() (compare-and-set, with retry on the cluster backend under contention), so every concurrent charge is counted. A cache backing these caps must support atomic update(); b.cache provides it, and both primitives refuse a get/set-only cache at construction. The single-node in-memory paths were already safe (they accumulate synchronously).",
|
|
727
|
+
"sections": [
|
|
728
|
+
{
|
|
729
|
+
"heading": "Fixed",
|
|
730
|
+
"items": [
|
|
731
|
+
{
|
|
732
|
+
"title": "Byte quota on a shared cache counts concurrent requests atomically (no lost updates)",
|
|
733
|
+
"body": "b.network.byteQuota's cache backend accumulated bytes with a get → add → set that is not atomic, so concurrent requests from one peer on a multi-node deployment lost each other's byte charges and the rolling daily byte budget was under-enforced. Accounting now uses the cache's atomic compare-and-set update(), counting every concurrent request, and retries the cluster CAS under a contention burst rather than dropping a charge. A cache wired to a byte quota must support update() — b.cache does; a plain get/set cache is refused at create() (byte-quota/cache-no-atomic-update), and a backend that can't actually commit an atomic update surfaces loud on first use instead of silently disabling the quota. b.middleware.dailyByteQuota, which composes byteQuota, inherits the fix."
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
"title": "Static server bandwidth + concurrency caps count concurrent requests atomically",
|
|
737
|
+
"body": "b.staticServe's per-actor and global bandwidth caps and its per-actor concurrency cap kept their counters on the cache with a non-atomic get → add → set, so concurrent downloads from one actor on a multi-replica deployment lost each other's charges and the caps were under-enforced (a bandwidth/concurrency limit bypass). The counters now accumulate through the cache's atomic update() with the same contention retry, and the quota cache is required to support update() at create()."
|
|
738
|
+
}
|
|
739
|
+
]
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
"heading": "Detectors",
|
|
743
|
+
"items": [
|
|
744
|
+
{
|
|
745
|
+
"title": "Build guard: a cache-backed counter must use atomic update(), not get → set",
|
|
746
|
+
"body": "A codebase guard now fails the build if a primitive accumulates a counter on a cache with a get → mutate → set read-modify-write instead of the atomic update(), so the lost-update class fixed here cannot reappear at a new cap. Caches used for lookups or cache-aside (the value is replaced wholesale, not incremented), or whose writes are serialized in-process, are allowlisted with the reason they cannot lose an increment."
|
|
747
|
+
}
|
|
748
|
+
]
|
|
749
|
+
}
|
|
750
|
+
]
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
"version": "0.15.40",
|
|
754
|
+
"date": "2026-06-27",
|
|
755
|
+
"headline": "The durable webhook dispatcher's retry poller now claims due deliveries with FOR UPDATE SKIP LOCKED on Postgres and MySQL, so two pollers running at once (multiple app nodes) can no longer both grab the same delivery and send the webhook twice in one cycle",
|
|
756
|
+
"summary": "The webhook dispatcher's retry poller claimed due deliveries by flipping them pending->in-flight and then re-selecting the in-flight rows by id. On Postgres or MySQL under the default READ COMMITTED isolation, two pollers running concurrently could both re-select the same row: the loser's UPDATE matched zero rows (the winner had already flipped it), but its reselect-by-id still re-read the row the winner had just claimed, so both pollers attempted the same HTTP delivery in one cycle. The claim now row-locks the due rows with SELECT ... FOR UPDATE SKIP LOCKED on the row-locking backends, so concurrent pollers see disjoint sets and each delivery is claimed by exactly one poller; sqlite (a single writer) keeps the existing mark-then-reselect, which it serializes safely. This matches the claim used by the framework's transactional outbox and cluster queue. Receivers that already dedup on the X-Webhook-Delivery-Id header were protected from a duplicate POST; this closes the at-most-once-per-cycle gap at the dispatcher itself.",
|
|
757
|
+
"sections": [
|
|
758
|
+
{
|
|
759
|
+
"heading": "Fixed",
|
|
760
|
+
"items": [
|
|
761
|
+
{
|
|
762
|
+
"title": "Webhook retry pollers no longer double-deliver under concurrency on Postgres / MySQL",
|
|
763
|
+
"body": "b.webhook.dispatcher's processRetries() claimed due deliveries with a mark-then-reselect that had no row lock, so on Postgres / MySQL at READ COMMITTED two concurrent pollers (for example, the dispatcher running on more than one app node) could both hand back the same delivery and POST it twice in a single retry cycle. The claim now uses SELECT ... FOR UPDATE SKIP LOCKED on those backends so each due row is locked by exactly one poller and concurrent pollers claim disjoint sets; the rows a poller selects are exactly the rows it owns. sqlite keeps the mark-then-reselect path, which its single writer serializes. Operators running the dispatcher on a single node, or whose receivers dedup on X-Webhook-Delivery-Id, were not exposed to a duplicate delivery; no configuration change is required to pick up the fix."
|
|
764
|
+
}
|
|
765
|
+
]
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
"heading": "Detectors",
|
|
769
|
+
"items": [
|
|
770
|
+
{
|
|
771
|
+
"title": "Build guard: a competing-consumer claim must use FOR UPDATE SKIP LOCKED",
|
|
772
|
+
"body": "A codebase guard now fails the build if a poller that claims due rows across workers — SELECT status='pending' inside a transaction, then flip the rows to in-flight — omits FOR UPDATE SKIP LOCKED on the row-locking backends. Without the row lock, two pollers under READ COMMITTED both claim the same row; the guard keeps any future poller from re-introducing the shape, with the transactional outbox and cluster queue as the reference claims."
|
|
773
|
+
}
|
|
774
|
+
]
|
|
775
|
+
}
|
|
776
|
+
]
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
"version": "0.15.39",
|
|
780
|
+
"date": "2026-06-27",
|
|
781
|
+
"headline": "Nine more places that matched an operator-supplied regex against request data — User-Agent, Origin, request path, form fields, SMTP HELO, release-asset names — now screen the pattern for catastrophic backtracking (ReDoS) before use, and a new b.guardRegex.assertSafe helper makes that screening one call",
|
|
782
|
+
"summary": "The previous release screened feature-flag and MCP regex patterns for ReDoS but did not sweep every place the framework matches an operator-supplied regex against attacker-controlled input. Nine more were found and fixed: the bot guard (User-Agent), CORS (Origin), the HTTP span middleware and the shared request skip-matcher used by CSRF / fetch-metadata / rate-limit / access-lock / age-gate and the request logger (request path), static serving (hashed-asset path pattern), form field validation (submitted field value), SMTP HELO generic-rDNS patterns (HELO name), and the self-updater's asset/signature patterns (names from a remote release feed). Each accepted an operator RegExp with only a type check and ran it on every matching request, so an accidentally catastrophic pattern such as (a+)+$ or ((a)+)+$ could pin a CPU on a crafted input — a length cap does not bound backtracking. Every one now screens the pattern through b.guardRegex at configuration time. A new b.guardRegex.assertSafe(input, label?, ErrorClass?, code?) primitive performs that screen in one call (accepting a RegExp or a pattern string), which operators can also use on their own patterns.",
|
|
783
|
+
"sections": [
|
|
784
|
+
{
|
|
785
|
+
"heading": "Security",
|
|
786
|
+
"items": [
|
|
787
|
+
{
|
|
788
|
+
"title": "Operator regex patterns matched against request data are screened for ReDoS framework-wide",
|
|
789
|
+
"body": "An operator-supplied RegExp matched against attacker-controllable input is a denial-of-service surface if it has a catastrophic-backtracking shape: the input triggers exponential work in the regex engine. Nine sites accepted such patterns with only an `instanceof RegExp` type check and executed them per request — bot-guard against the User-Agent, CORS against the Origin header, the HTTP span middleware and the shared skip-path matcher (CSRF / fetch-metadata / rate-limit / access-lock / age-gate / request-log) against the request path, static serving against the request path, form validation against the submitted field value, SMTP HELO checks against the HELO name, and the self-updater against asset names from a remote release feed. Each now routes the pattern through b.guardRegex at configuration time, so a catastrophic shape is refused up front instead of being weaponized by a crafted request. A length bound on the input is not a defense: a nested-quantifier pattern backtracks catastrophically at a few dozen characters."
|
|
790
|
+
}
|
|
791
|
+
]
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
"heading": "Detectors",
|
|
795
|
+
"items": [
|
|
796
|
+
{
|
|
797
|
+
"title": "Build guard: an operator regex matched against request input must be ReDoS-screened",
|
|
798
|
+
"body": "A codebase guard now fails the build if a primitive accepts an operator-supplied RegExp and executes it against request input without screening the pattern through b.guardRegex.assertSafe — so the catastrophic-backtracking class fixed in this release cannot be reintroduced at a new site (the trusted-input cases — local filesystem paths, operator config keys, operator-owned schemas — are explicitly allowlisted)."
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
"title": "Build guard: process.moduleLoadList filters must match the 'NativeModule X' naming",
|
|
802
|
+
"body": "A guard now fails the build if a test filters process.moduleLoadList by the 'node:X' name only. Node 20+ records a loaded builtin as 'NativeModule X', so a 'node:'-only filter in an edge-runtime no-eager-load test would rot green and miss a reintroduced top-level networking require."
|
|
803
|
+
}
|
|
804
|
+
]
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
"heading": "Added",
|
|
808
|
+
"items": [
|
|
809
|
+
{
|
|
810
|
+
"title": "b.guardRegex.assertSafe — screen a RegExp or pattern string for ReDoS in one call",
|
|
811
|
+
"body": "b.guardRegex.assertSafe(input, label?, ErrorClass?, code?, opts?) screens an already-compiled RegExp (its source) or a raw pattern string for the catastrophic-backtracking classes — nested, alternation-with, and lookaround quantifiers — throwing the supplied framework-error class (or the underlying GuardRegexError) on a hostile pattern and returning the input on success. It allows large or open-ended bounded repeats (`{8,}`, `{n,m}`): a single counted repeat matches in linear time and legitimate patterns (including the framework's own defaults) use them. It is the config-time guard used by the request-lifecycle fixes above, and operators can apply it to their own patterns before matching them against untrusted input."
|
|
812
|
+
}
|
|
813
|
+
]
|
|
814
|
+
}
|
|
815
|
+
]
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
"version": "0.15.38",
|
|
819
|
+
"date": "2026-06-27",
|
|
820
|
+
"headline": "Regex patterns supplied in feature-flag targeting rules and MCP tool input schemas are now screened for catastrophic-backtracking (ReDoS) shapes before compilation, so a pattern matched against request data can't pin a CPU",
|
|
821
|
+
"summary": "Two places compiled a caller-supplied regex pattern and `.test()`'d it against request-controlled input with only a length bound as the stated defense: a feature-flag targeting condition (`op: \"regex\"`) matched against runtime attribute values, and an MCP tool's input-schema `pattern` matched against tool-call arguments. A length bound is not a ReDoS defense — a catastrophic-backtracking pattern such as `(a+)+$` is six characters and pins a CPU on a crafted input. Both patterns now pass through `b.guardRegex` (strict profile) before compilation, which refuses nested-quantifier, alternation-with-quantifier, and quantifier-inside-lookaround shapes. A ReDoS-shaped flag pattern is refused when the rules are validated; a ReDoS-shaped MCP schema pattern fails tool-input validation. Patterns built from the framework's own static tables, operator-owned JSON Schema patterns, the Sieve glob translator (which cannot express nested quantifiers), and the I-Regexp translator (linear by dialect) are unchanged.",
|
|
822
|
+
"sections": [
|
|
823
|
+
{
|
|
824
|
+
"heading": "Security",
|
|
825
|
+
"items": [
|
|
826
|
+
{
|
|
827
|
+
"title": "Feature-flag regex targeting conditions are screened for ReDoS before compilation",
|
|
828
|
+
"body": "A flag targeting rule with `op: \"regex\"` compiled the operator-supplied pattern and `.test()`'d it against runtime attribute values, guarded only by a 200-character length cap. Length does not bound catastrophic backtracking, so a pattern like `(a+)+$` combined with an attacker-controlled attribute value could pin a CPU during flag evaluation. The pattern is now screened through b.guardRegex (strict) when the rules are validated, and a catastrophic-backtracking shape is refused with a clear error."
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
"title": "MCP tool input-schema patterns are screened for ReDoS before matching request input",
|
|
832
|
+
"body": "b.mcp.validateToolInput compiled a tool author's input-schema `pattern` and matched it against tool-call argument values; the 4096-character input cap does not bound backtracking (a `(a+)+$` pattern blows up at roughly forty input characters). The schema pattern is now screened through b.guardRegex (strict) before compilation, so a ReDoS-shaped pattern in a registered tool's schema fails input validation instead of letting hostile arguments hang the validator."
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
"title": "b.guardRegex now catches wrapped nested-quantifier patterns",
|
|
836
|
+
"body": "The nested-quantifier detector matched a quantified group whose body contained a quantifier, but its inner match was paren-blind, so wrapping the inner quantifier in an extra group — `((a)+)+`, `(([a-z]+)*)*`, `((a+))+` — slipped past it while remaining catastrophic. A structural scan now tracks group nesting and refuses an unbounded-quantified group whose body itself contains an unbounded quantifier at any depth, so the wrapped forms are rejected alongside the bare `(a+)+`. Bounded repeats (`{n}`, `{n,m}`) are unaffected. This strengthens every b.guardRegex caller, including the flag-targeting and MCP screening above."
|
|
837
|
+
}
|
|
838
|
+
]
|
|
839
|
+
}
|
|
840
|
+
]
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
"version": "0.15.37",
|
|
844
|
+
"date": "2026-06-27",
|
|
845
|
+
"headline": "Several numeric options that silently accepted a non-finite value — disabling a clock-skew / freshness check or a resource cap — now reject it, so an `Infinity` skew or limit can no longer turn off the protection it configures",
|
|
846
|
+
"summary": "A number of configuration options validated a numeric value with a bare `typeof === \"number\" && value >= 0` check, which accepts `Infinity`. Where the value is a clock-skew tolerance or a resource cap, an `Infinity` (or `NaN`) silently disabled the very check it tunes: a CWT / OCSP-staple / ARC clock-skew of `Infinity` made the expiry, freshness, and expiration comparisons unsatisfiable (an expired token / a replayed pre-revocation \"good\" response / an expired ARC seal would be accepted); a WebSocket-client `maxMessageBytes` / `maxFrameBytes` / `handshakeTimeoutMs` of `Infinity` disabled the inbound-OOM and stalled-handshake defenses; and inbox / flag-cache / audit-chain size and count caps of `Infinity` admitted unbounded data. These options now route through the finite-bounds validator: a present non-finite value is refused at the entry point (or falls back to the safe default on the result-returning paths). Options where an unbounded value is a deliberate intent — reconnect \"retry indefinitely\", inbox \"retain indefinitely\" — continue to accept `Infinity`.",
|
|
847
|
+
"sections": [
|
|
848
|
+
{
|
|
849
|
+
"heading": "Security",
|
|
850
|
+
"items": [
|
|
851
|
+
{
|
|
852
|
+
"title": "A non-finite clock-skew no longer disables CWT / OCSP / ARC time checks",
|
|
853
|
+
"body": "b.cwt.verify, the OCSP-staple freshness check in b.network.tls, and b.mail.arc.verify each took an operator clock-skew tolerance validated as `typeof === \"number\" && >= 0`, which accepts `Infinity`. Because each check is of the form `now > deadline + skew`, a skew of `Infinity` made it unsatisfiable and silently turned the check off: an expired CWT verified, a stale (post-nextUpdate) OCSP \"good\" response — the exact reply an attacker replays after the certificate is revoked — was accepted, and an expired ARC seal passed. A present clock-skew that is not a non-negative finite integer is now refused (b.cwt.verify throws cwt/bad-clock-skew; the OCSP and ARC paths fall back to their safe default). Regression tests assert an expired token / stale staple / expired ARC seal is still rejected when the skew is `Infinity`."
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
"title": "WebSocket-client inbound caps can no longer be disabled by an Infinity value",
|
|
857
|
+
"body": "b.wsClient.connect validated maxMessageBytes, maxFrameBytes, and handshakeTimeoutMs (and the ping/pong keepalive intervals) with a bare numeric check that accepted `Infinity`, which disabled the inbound-message and frame size limits — the defenses against a malicious server sending an unbounded message — and the handshake timeout. A present non-finite value for these is now refused at connect time. The reconnect maxAttempts still accepts `Infinity` (a deliberate \"reconnect indefinitely\" intent)."
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
"title": "Inbox, flag-cache, and audit-chain caps reject a non-finite limit",
|
|
861
|
+
"body": "The inbox maxPayloadBytes / messageIdMaxLen / sourceMaxLen caps, the flag-cache ttlMs / maxEntries, and the audit-chain partition fan-out cap each accepted `Infinity`, disabling the cap (unbounded stored payloads, a never-expiring or unbounded cache, unbounded fan-out). These now require a positive finite integer — refused at create time, or clamped to the bounded default on the result-returning verify path. The inbox retentionDays still accepts `Infinity` (retain indefinitely)."
|
|
862
|
+
}
|
|
863
|
+
]
|
|
864
|
+
}
|
|
865
|
+
]
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
"version": "0.15.36",
|
|
869
|
+
"date": "2026-06-27",
|
|
870
|
+
"headline": "Internal test-suite hardening only — the published library's runtime behavior and public API are unchanged (source-comment marker text aside)",
|
|
871
|
+
"summary": "The codebase-patterns guard class that allows a bare comma/semicolon split on token-only RFC header grammars was re-verified from scratch and renamed to a descriptive token, with its old name recorded as retired. Each live marked site (RRULE, RFC 9421 component identifiers, TLS-RPT rua, SCIM attribute paths) was re-read and confirmed to split a grammar with no quoted-string members. Five marker comments that suppressed nothing were removed or turned into plain explanatory comments. No runtime code, public API, or wire format changed.",
|
|
872
|
+
"sections": [
|
|
873
|
+
{
|
|
874
|
+
"heading": "Detectors",
|
|
875
|
+
"items": [
|
|
876
|
+
{
|
|
877
|
+
"title": "Bare token-only header-split suppression class re-verified, renamed, and pruned of inert markers",
|
|
878
|
+
"body": "Each marked bare `.split(\",\")` / `.split(\";\")` on an RFC header value was re-read and confirmed to operate on a token-only grammar (no quoted-string members, so a quote-aware splitter is unnecessary): RFC 5545 RRULE, RFC 9421 signature component identifiers, RFC 8460 TLS-RPT rua, and RFC 7644 SCIM attribute paths. The guard class was renamed to a descriptive token and its old name added to the retired-token set. Five marker comments that the detector never actually evaluated (two sat on a date-normalizing `.replace`, three in header parsers the guard intentionally does not scan) were removed or converted to plain comments. This is test-suite tooling plus source-comment text; no shipped framework behavior changed."
|
|
879
|
+
}
|
|
880
|
+
]
|
|
881
|
+
}
|
|
882
|
+
]
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
"version": "0.15.35",
|
|
886
|
+
"date": "2026-06-26",
|
|
887
|
+
"headline": "`b.metrics` shadow registry no longer lets a comma in a label value forge extra Prometheus label pairs — a label-injection that downstream tenant-scoping or authorization selectors could be tricked by",
|
|
888
|
+
"summary": "The shadow registry serialized a metric's label set into a `name=value,name=value` string key and re-split it on `,` to build the Prometheus exposition. A label VALUE containing a comma therefore split into multiple label pairs, so a single operator-set label could forge additional label pairs in the scrape output (for example turning one `route` label into a `route` plus an attacker-named label), which downstream tenant-scoping filters, authorization selectors, recording rules, and alerting trust as a boundary; two distinct label sets could also collide into one cardinality bucket. The label key now uses canonical-JSON of the string-coerced label set (the same approach the main registry already used) and the render path emits the structured label set kept alongside that key rather than splitting or re-parsing, so a `,` or `=` inside a label value stays inside the value and a label NAMED `constructor`, `prototype`, or `__proto__` (all valid Prometheus label names) is preserved instead of being dropped. Note: the cardinality keys exposed by `shadowRegistry().snapshot()` are now canonical-JSON strings (e.g. `{\"route\":\"/api\"}`) rather than `route=/api`.",
|
|
889
|
+
"sections": [
|
|
890
|
+
{
|
|
891
|
+
"heading": "Security",
|
|
892
|
+
"items": [
|
|
893
|
+
{
|
|
894
|
+
"title": "Label values can no longer forge extra Prometheus label pairs in the shadow registry",
|
|
895
|
+
"body": "b.metrics shadowRegistry built a metric's cardinality key by joining `name=value` pairs with commas, then split that string on commas when rendering the Prometheus exposition. A comma (and `=`) in a label value therefore broke one value into several forged label pairs in the scrape output, and two different label sets could collide into the same cardinality bucket. The key is now canonical-JSON of the string-coerced label set and the render emits the structured label set kept alongside that key rather than splitting or re-parsing, so a label value is emitted as a single quoted value with its commas/equals intact, distinct label sets get distinct keys, and a label NAMED `constructor`, `prototype`, or `__proto__` (each a valid Prometheus label name) is preserved rather than dropped. Deterministic regression tests assert a comma-bearing label value produces exactly one label pair and that the reserved-name labels survive render."
|
|
896
|
+
}
|
|
897
|
+
]
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
"heading": "Changed",
|
|
901
|
+
"items": [
|
|
902
|
+
{
|
|
903
|
+
"title": "shadowRegistry().snapshot() cardinality keys are canonical-JSON",
|
|
904
|
+
"body": "As a consequence of the label-injection fix, the per-series cardinality keys in a shadow-registry snapshot are now canonical-JSON strings (e.g. `{\"tenant\":\"a\"}`) instead of the previous `tenant=a` form. Code that reads those keys directly from snapshot() should parse them as JSON; the Prometheus render output is unchanged for conforming label values."
|
|
905
|
+
}
|
|
906
|
+
]
|
|
907
|
+
}
|
|
908
|
+
]
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
"version": "0.15.34",
|
|
912
|
+
"date": "2026-06-26",
|
|
913
|
+
"headline": "`b.mail.crypto.pgp.verify` now accepts every valid RSA OpenPGP signature — one whose value happened to have a high zero byte (about 1 in 256) was being rejected",
|
|
914
|
+
"summary": "An OpenPGP RSA signature is an integer modulo the key's modulus, and ~1 in 256 signatures have a value that begins with a zero byte. The MPI encoding strips those leading zero bytes (RFC 9580 §3.2), but b.mail.crypto.pgp.verify passed the stripped value straight to the RSA verification, which requires a signature exactly the modulus byte length — so a perfectly valid signature was intermittently reported invalid. The signature is now left-padded back to the modulus width before verification, exactly as the Ed25519 path already pads its components. Verification of every valid RSA signature is now reliable, including signatures produced by other OpenPGP implementations. Also includes internal test-tooling: two more guard-suite suppression classes were re-verified and their tokens retired.",
|
|
915
|
+
"sections": [
|
|
916
|
+
{
|
|
917
|
+
"heading": "Fixed",
|
|
918
|
+
"items": [
|
|
919
|
+
{
|
|
920
|
+
"title": "RSA OpenPGP signatures with a high zero byte now verify reliably",
|
|
921
|
+
"body": "b.mail.crypto.pgp.verify read the RSA signature MPI (whose leading zero bytes the OpenPGP wire format strips) and handed it to the RSA verification without restoring the stripped bytes. Node's RSA verify requires the signature to be exactly the modulus byte length, so a signature whose value had one or more high zero bytes — about 1 in 256 of all signatures, for any key — was rejected as invalid even though it was correct. The signature is now left-padded to the modulus width before verification (the same correction the Ed25519 verification path already applied to its R/S components). This affects signatures the framework produces and signatures from other OpenPGP implementations alike. A deterministic regression test searches for a short-MPI signature and asserts it verifies."
|
|
922
|
+
}
|
|
923
|
+
]
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
"heading": "Detectors",
|
|
927
|
+
"items": [
|
|
928
|
+
{
|
|
929
|
+
"title": "Two more suppression-marker classes re-verified and their tokens retired",
|
|
930
|
+
"body": "The raw-hash-compare and seal-without-aad guard classes were re-read and confirmed (a data-residency region tag compared with === — not a secret; and two intentional non-AEAD-bound seals — a non-regulated plain-mode column whose AAD is enforced by the posture seal-envelope floor where it matters, and a throwaway vault-readiness probe), then renamed to descriptive tokens with their old names added to the retired-token set. Test-suite tooling only."
|
|
931
|
+
}
|
|
932
|
+
]
|
|
933
|
+
}
|
|
934
|
+
]
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
"version": "0.15.33",
|
|
938
|
+
"date": "2026-06-26",
|
|
939
|
+
"headline": "Internal test-suite hardening only — the published library's runtime behavior and public API are unchanged (source-comment marker text aside)",
|
|
940
|
+
"summary": "Three more suppression classes in the codebase-patterns guard suite were re-verified from scratch and renamed to descriptive tokens, with their old names recorded as retired and their in-source marker comments updated to match. No runtime code, public API, or wire format changed.",
|
|
941
|
+
"sections": [
|
|
942
|
+
{
|
|
943
|
+
"heading": "Detectors",
|
|
944
|
+
"items": [
|
|
945
|
+
{
|
|
946
|
+
"title": "Three more suppression-marker classes re-verified and their tokens retired",
|
|
947
|
+
"body": "Continuing the re-verification pass: each marked site for the math-random-noncrypto, raw-new-url, and dynamic-require guard classes was re-read and confirmed (non-security jitter/sampling; URL parsing for shape/origin inspection or behind the safe wrapper; operator-supplied module loads), then the class was renamed to a descriptive token and its old name added to the retired-token set. This is test-suite tooling; no shipped framework behavior changed."
|
|
948
|
+
}
|
|
949
|
+
]
|
|
950
|
+
}
|
|
951
|
+
]
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
"version": "0.15.32",
|
|
955
|
+
"date": "2026-06-26",
|
|
956
|
+
"headline": "Internal test-suite hardening only — the published library's runtime behavior and public API are unchanged (source-comment marker text aside)",
|
|
957
|
+
"summary": "Three more suppression classes in the codebase-patterns guard suite were re-verified from scratch and renamed to descriptive tokens, with their old names recorded as retired so the prior approval cannot be silently resurrected and their in-source marker comments updated to match. No runtime code, public API, or wire format changed.",
|
|
958
|
+
"sections": [
|
|
959
|
+
{
|
|
960
|
+
"heading": "Detectors",
|
|
961
|
+
"items": [
|
|
962
|
+
{
|
|
963
|
+
"title": "Three more suppression-marker classes re-verified and their tokens retired",
|
|
964
|
+
"body": "Continuing the re-verification pass: each marked site for the process-exit, hand-rolled buffer-collect, and raw-outbound-http guard classes was re-read and confirmed (operator-opt-in exits; bounded protocol-framing / TLV assembly; framework-routed outbound calls), then the class was renamed to a descriptive token and its old name added to the retired-token set. This is test-suite tooling; no shipped framework behavior changed."
|
|
965
|
+
}
|
|
966
|
+
]
|
|
967
|
+
}
|
|
968
|
+
]
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
"version": "0.15.31",
|
|
972
|
+
"date": "2026-06-26",
|
|
973
|
+
"headline": "Internal test-suite hardening only — the published library's runtime behavior and public API are unchanged (source-comment marker text aside)",
|
|
974
|
+
"summary": "The codebase-patterns guard suite gains a gate that retires a renamed suppression-marker token: once a suppression class is re-verified and renamed, its old token cannot be silently re-registered, so a later suppression has to be re-examined under the new name instead of inheriting a stale approval. Eight suppression classes were re-verified and renamed to descriptive tokens this release, and their in-source marker comments were updated to match. No runtime code, public API, or wire format changed.",
|
|
975
|
+
"sections": [
|
|
976
|
+
{
|
|
977
|
+
"heading": "Detectors",
|
|
978
|
+
"items": [
|
|
979
|
+
{
|
|
980
|
+
"title": "Re-verification gate retires a renamed suppression-marker token",
|
|
981
|
+
"body": "A new guard refuses to re-register a retired suppression-marker token, and the orphan check points a stale marker at its current name. When a suppression class is re-verified, it is renamed to a descriptive token and the old name is recorded as retired, so the old approval cannot be quietly resurrected — a future suppression must be re-examined and re-stamped under the new name. This is test-suite tooling; no shipped framework behavior changed."
|
|
982
|
+
}
|
|
983
|
+
]
|
|
984
|
+
}
|
|
985
|
+
]
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
"version": "0.15.30",
|
|
989
|
+
"date": "2026-06-26",
|
|
990
|
+
"headline": "Importing the webhook module for inbound signature verification no longer pulls the Node HTTP client (or its `node:http` / `node:https` / `node:http2` chain), so `b.webhook.verify` runs in a Worker / edge runtime when the module is imported directly",
|
|
991
|
+
"summary": "lib/webhook and its delivery store eagerly required the outbound HTTP client at module load — directly, and transitively through the persistence-backed dispatcher — so importing the webhook module threw in a Cloudflare Workers / edge runtime that has no node:http / node:net / node:tls, even though b.webhook.verify needs only HMAC-SHA-256 and a timing-safe compare. The HTTP client and the SQL-backed dispatcher are now resolved lazily, only on the outbound send / dispatch paths; importing the module for inbound verification loads no Node networking module. Verified by asserting no networking builtin appears in process.moduleLoadList on the verify path. The framework's aggregate entry point still initializes the networking stack eagerly, so the edge verify path is the webhook module imported directly rather than the aggregate b namespace.",
|
|
992
|
+
"sections": [
|
|
993
|
+
{
|
|
994
|
+
"heading": "Fixed",
|
|
995
|
+
"items": [
|
|
996
|
+
{
|
|
997
|
+
"title": "Inbound webhook verification loads without the Node networking stack (Worker / edge runtime)",
|
|
998
|
+
"body": "The webhook module and its dispatcher delivery store required the outbound HTTP client (node:http / node:https / node:http2) at module load, so importing the module threw in a runtime without those builtins — even though b.webhook.verify is HMAC-SHA-256 plus a timing-safe compare and the dispatcher (SQL-backed persistence) is only needed for outbound delivery. The HTTP client and the dispatcher are now resolved lazily, on the outbound send / dispatch paths only; the inbound verify path loads no Node networking module. An edge handler that imports the webhook module directly can pre-verify an inbound Stripe webhook signature with the primitive's hardening (Stripe-Signature size cap, v1-hex anti-DoS cap, minimum-tolerance floor, optional atomic replay store) instead of hand-rolling the t=/v1= parse, tolerance window, and timing-safe compare. The framework's aggregate entry still loads the networking stack eagerly; import the webhook module directly for the edge verify path."
|
|
999
|
+
}
|
|
1000
|
+
]
|
|
1001
|
+
}
|
|
1002
|
+
]
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
"version": "0.15.29",
|
|
1006
|
+
"date": "2026-06-25",
|
|
1007
|
+
"headline": "JSON parsing of operator and untrusted input is routed through `b.safeJson` everywhere, so a `\"__proto__\"` member is stripped and the parse is size-bounded — `b.openapi.parse`, `b.asyncapi.parse`, and `b.sandbox.run` were using a raw `JSON.parse`",
|
|
1008
|
+
"summary": "b.openapi.parse and b.asyncapi.parse parsed an operator-supplied document string with a raw JSON.parse, which re-creates a \"__proto__\" member as an own key and imposes no size bound; b.sandbox.run parsed the untrusted sandboxed code's serialized result the same way (proto member kept, depth unbounded). All three now route through b.safeJson, which strips the prototype-pollution keys and applies the depth / key / size caps. A new b.safeJson.parseStringOrObject primitive captures the recurring \"accept a document as a JSON string OR a pre-built object\" shape (a string is parsed safely, an object passes through) that openapi and asyncapi had each hand-rolled, with each consumer's typed error class and a generous document size cap passed as data. Valid input parses identically to before; a malicious or oversized document is now defused rather than accepted.",
|
|
1009
|
+
"sections": [
|
|
1010
|
+
{
|
|
1011
|
+
"heading": "Security",
|
|
1012
|
+
"items": [
|
|
1013
|
+
{
|
|
1014
|
+
"title": "Operator and untrusted JSON parses strip __proto__ and bound size (no raw JSON.parse)",
|
|
1015
|
+
"body": "A raw JSON.parse keeps a \"__proto__\" object member as an own key (a prototype-pollution gadget for any downstream merge) and parses unbounded input. b.openapi.parse and b.asyncapi.parse used it on operator document strings, and b.sandbox.run used it on the untrusted sandboxed code's JSON-serialized result. All now route through b.safeJson — the prototype-pollution keys are stripped and the depth / key / size caps apply (a 16 MiB document cap for openapi/asyncapi, the existing result-bytes cap for the sandbox). Internal JSON parses (the sealed DSR store payloads, the VEX canonical re-format) route through the same primitive too, so the guarantee cannot be bypassed one call site at a time. Valid input is unaffected; a \"__proto__\"-laden or oversized payload is defused."
|
|
1016
|
+
}
|
|
1017
|
+
]
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
"heading": "Added",
|
|
1021
|
+
"items": [
|
|
1022
|
+
{
|
|
1023
|
+
"title": "b.safeJson.parseStringOrObject(input, opts?)",
|
|
1024
|
+
"body": "Accepts either a JSON string — parsed through b.safeJson.parse, so the prototype-pollution-key strip and depth / key / size caps apply — or an already-decoded plain object (returned unchanged). For the recurring \"operator hands me a document as a JSON string or a pre-built object\" surface (b.openapi / b.asyncapi). The consumer's typed error class, error codes, label, and size cap are passed as options, so a raw JSON.parse on operator input cannot be hand-rolled per consumer."
|
|
1025
|
+
}
|
|
1026
|
+
]
|
|
1027
|
+
}
|
|
1028
|
+
]
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
"version": "0.15.28",
|
|
1032
|
+
"date": "2026-06-25",
|
|
1033
|
+
"headline": "`b.network.dns` and `b.network.tls` errors now carry a usable terminal-vs-transient signal on `err.permanent`, so a caller's retry loop re-attempts only the failures a retry can fix",
|
|
1034
|
+
"summary": "DnsError was declared always-transient and NetworkTlsError always-permanent, so a consumer driving its own retry loop got the wrong signal in both directions: it would retry a permanent DNS failure (a bad host, bad options, an unsupported query, an NXDOMAIN-style no-result, or a caller-shape/config error such as an unconfigured transport or an invalid resolver list) forever, and it would refuse to retry a transient TLS-over-network failure (an ECH connection failure, a handshake timeout, DNS momentarily unavailable). err.permanent now reflects each error's actual transience, derived from its code and failing closed (an unknown code is permanent, so a hopeless target is not retried indefinitely): for DnsError only a failed network round-trip (a lookup timeout, a resolve/reverse query that failed on the wire, a failed DoH/DoT exchange, a DDR discovery that found nothing) is transient — config, input, and environment errors raised before any network work are permanent; for NetworkTlsError only the network-layer ECH failures are transient. TlsTrustError remains always-permanent by design — a trust-verification failure (bad CA, fingerprint mismatch, OCSP not-good, CT violation, an unreachable OCSP responder) must never be silently retried past the trust decision.",
|
|
1035
|
+
"sections": [
|
|
1036
|
+
{
|
|
1037
|
+
"heading": "Fixed",
|
|
1038
|
+
"items": [
|
|
1039
|
+
{
|
|
1040
|
+
"title": "DnsError / NetworkTlsError expose a correct terminal/transient signal; TlsTrustError stays terminal",
|
|
1041
|
+
"body": "b.network.dns's DnsError was always-transient and b.network.tls's NetworkTlsError always-permanent, so err.permanent misled a consumer's retry loop in both directions — retrying a permanent DNS error (bad host / options / unsupported type / no-result) indefinitely, and never retrying a transient TLS network error (ECH connect failure, handshake timeout, DNS unavailable). err.permanent now reflects each error's actual transience by code and fails closed (unknown codes are permanent): DnsError is transient only for a failed network round-trip (dns/lookup-timeout, dns/resolve-failed, dns/reverse-failed, dns/doh-failed, dns/dot-failed, dns/dot-handshake-failed, dns/ddr-not-discovered, dns/system-failed), and permanent otherwise — including the caller-shape / environment errors raised before any network work (dns/transport-unavailable for an unconfigured transport, dns/dnr-no-resolvers for an empty/invalid resolver list, dns/setservers-failed for an invalid address, dns/no-system-resolvers when none are configured). NetworkTlsError is transient only for the network-layer ECH failures (tls/ech-connect-failed, tls/ech-timeout, tls/ech-dns-unavailable), permanent otherwise. TlsTrustError remains always-permanent so a trust-verification failure is never silently retried."
|
|
1042
|
+
}
|
|
1043
|
+
]
|
|
1044
|
+
}
|
|
1045
|
+
]
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
"version": "0.15.27",
|
|
1049
|
+
"date": "2026-06-25",
|
|
1050
|
+
"headline": "Internal test-harness reliability only — the published library is byte-for-byte identical to 0.15.26",
|
|
1051
|
+
"summary": "The legacy single-layer smoke files used fixed-duration setTimeout sleeps to wait for asynchronous conditions (a job processed, a lease TTL lapsing, an audit flush completing). On a contended CI runner a fixed sleep is both flake-prone (too short under load) and slow (it always burns the full budget). Those condition-waits are converted to the harness's polling helpers — waitUntil for observable predicates, passiveObserve for deliberate real-time elapses, withTestTimeout for hang guards — which exit early on fast platforms and give contended platforms the full budget. No shipped framework code changed; this release is byte-for-byte identical to 0.15.26 for operators.",
|
|
1052
|
+
"sections": [
|
|
1053
|
+
{
|
|
1054
|
+
"heading": "Fixed",
|
|
1055
|
+
"items": [
|
|
1056
|
+
{
|
|
1057
|
+
"title": "Smoke layer files poll for conditions instead of fixed setTimeout sleeps",
|
|
1058
|
+
"body": "The single-layer smoke files waited on asynchronous conditions with fixed-duration setTimeout sleeps, which flake under SMOKE_PARALLEL load and always burn their full budget. The condition-waits are converted to the polling helpers (waitUntil / passiveObserve / withTestTimeout) so they exit early on fast platforms and stay robust on contended ones; non-wait timers (abort triggers, child/socket watchdogs, simulated-latency mocks) are unchanged. This is test-harness reliability only — no shipped framework behavior changed."
|
|
1059
|
+
}
|
|
1060
|
+
]
|
|
1061
|
+
}
|
|
1062
|
+
]
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
"version": "0.15.26",
|
|
1066
|
+
"date": "2026-06-25",
|
|
1067
|
+
"headline": "Internal test-harness correctness only — the published library is byte-for-byte identical to 0.15.25",
|
|
1068
|
+
"summary": "The smoke runner requires each test module and awaits its exported run(). Several tests were instead written as a top-level (async function () {...})() IIFE that runs detached at require-time, so the runner measured and reported the test's result before the IIFE's post-await assertions executed — those checks silently never ran (one parser test exercised 4 of its 26 assertions, and a failure after the first await would have gone unseen as a false pass). Those tests are converted to the exported-run form so the runner awaits their full assertion set, and a codebase-patterns detector now refuses a top-level async IIFE in a test file so the pattern cannot return. No shipped framework code changed; this release is byte-for-byte identical to 0.15.25 for operators.",
|
|
1069
|
+
"sections": [
|
|
1070
|
+
{
|
|
1071
|
+
"heading": "Fixed",
|
|
1072
|
+
"items": [
|
|
1073
|
+
{
|
|
1074
|
+
"title": "Detached-IIFE tests now run their full assertion set under the smoke runner",
|
|
1075
|
+
"body": "A test written as a top-level (async function () {...})() IIFE runs detached when the runner requires it: the runner only awaits an exported run(), so it reported the file's result before the IIFE's awaited assertions executed, and every check after the first await silently did not count (one parsers test ran 4 of 26). Such tests are rewritten to define async function run(), export it, and invoke it under if (require.main === module), so the runner awaits the complete set; a detector refuses a re-introduced top-level async IIFE in a test file. This is test-harness correctness only — no shipped framework behavior changed."
|
|
1076
|
+
}
|
|
1077
|
+
]
|
|
1078
|
+
}
|
|
1079
|
+
]
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
"version": "0.15.25",
|
|
1083
|
+
"date": "2026-06-25",
|
|
1084
|
+
"headline": "`b.sandbox.run` no longer leaks the worker thread's MessagePort — it now waits for the worker to terminate before resolving",
|
|
1085
|
+
"summary": "b.sandbox.run spawned a worker thread to execute untrusted code and called worker.terminate() on both the result and timeout paths, but it settled the caller's promise BEFORE the asynchronous terminate() completed — leaving the worker's MessagePort alive past the resolve. In a long-lived process that runs sandboxed code repeatedly, each call leaked a MessagePort handle, keeping the event loop populated and slowing graceful shutdown. The call now awaits worker.terminate() before settling, so the handle is released when the promise resolves. Behaviour and return values are unchanged.",
|
|
1086
|
+
"sections": [
|
|
1087
|
+
{
|
|
1088
|
+
"heading": "Fixed",
|
|
1089
|
+
"items": [
|
|
1090
|
+
{
|
|
1091
|
+
"title": "b.sandbox.run releases the worker MessagePort before resolving",
|
|
1092
|
+
"body": "b.sandbox.run resolved (or rejected) the caller's promise as soon as the worker reported a result or the timeout fired, while worker.terminate() — which is asynchronous — was still in flight, so the worker thread's MessagePort outlived the call and lingered as an open handle. Repeated sandbox runs in a long-lived process accumulated leaked MessagePorts that kept the event loop alive and delayed shutdown. The result and timeout paths now defer settling until worker.terminate() resolves; the error and exit paths already imply the worker is gone. The return value, audit events, and timeout semantics are unchanged."
|
|
1093
|
+
}
|
|
1094
|
+
]
|
|
1095
|
+
}
|
|
1096
|
+
]
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
"version": "0.15.24",
|
|
1100
|
+
"date": "2026-06-25",
|
|
1101
|
+
"headline": "Supply-chain hardening for the build: the example wiki container's base images are digest-pinned (and tracked for patches by Dependabot), and the SEA-build esbuild binary pin is centralized and release-enforced so a version bump can't silently lose its reviewed hash",
|
|
1102
|
+
"summary": "A build supply-chain hardening release; no runtime API changes. The example wiki container's Chainguard base images are now pinned by digest (tag@sha256) for reproducible builds and provenance, with Dependabot's docker updater bumping the digests so they keep tracking Chainguard's continuous CVE rebuilds; the published multi-arch image is unaffected because the release workflow still resolves the rolling tag to a fresh digest at build time, so the scanned image equals the published image and the committed digest never reaches the image-scan gate. The esbuild native-binary supply-chain pin — the reviewed per-platform SHA-256 the bundler smoke gate checks the on-disk compiler against — is centralized in one file and enforced both at release time and in the pattern gate by a single shared verifier: the version package.json and the CI/publish workflows install must carry a complete reviewed-hash entry, so bumping esbuild without re-reviewing and re-pinning the binary now fails closed instead of silently degrading the verification to a skip.",
|
|
1103
|
+
"sections": [
|
|
1104
|
+
{
|
|
1105
|
+
"heading": "Security",
|
|
1106
|
+
"items": [
|
|
1107
|
+
{
|
|
1108
|
+
"title": "Wiki container base images are digest-pinned and patch-tracked",
|
|
1109
|
+
"body": "examples/wiki/Dockerfile now pins its Chainguard base images by digest (cgr.dev/chainguard/node:<tag>@sha256:...) for reproducible local builds and supply-chain provenance, and Dependabot's docker updater bumps those digests so they keep tracking Chainguard's continuously-rebuilt CVE fixes instead of freezing. The published multi-arch image is unchanged: the release workflow still resolves the rolling tag to a fresh digest at build time and passes it as a build-arg, so the scanned image equals the published image and always carries current patches — the committed digest never reaches the image-scan gate."
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
"title": "esbuild SEA-build binary pin is centralized and fails closed on an un-reviewed bump",
|
|
1113
|
+
"body": "The reviewed per-platform SHA-256 hashes that the bundler smoke gate verifies the esbuild native compiler against are now a single source of truth (scripts/esbuild-binary-pin.json), checked by one shared verifier that runs both at release time and in the pattern gate. It enforces that the esbuild version package.json declares and the CI/publish workflows install carries a complete reviewed-hash entry for every required platform. Previously a bump to a version with no reviewed hash made the smoke gate note-and-skip the binary verification; that omission now fails the release, so the binary pin can no longer be silently lost. The hashes remain captured by hand on each bump (diff the published tarballs, sha256 the binary) — the verifier checks the pin is present, it never derives it."
|
|
1114
|
+
}
|
|
1115
|
+
]
|
|
1116
|
+
}
|
|
1117
|
+
]
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
"version": "0.15.23",
|
|
1121
|
+
"date": "2026-06-24",
|
|
1122
|
+
"headline": "`b.wsClient` error objects now carry a usable terminal-vs-transient signal (and the client's auto-reconnect, previously dead, works again), and `b.safePath` resolves cross-platform containment with the target platform's path semantics — fixing both a false refusal of in-base paths and a backslash-traversal escape when validating for Windows on a POSIX host",
|
|
1123
|
+
"summary": "Two correctness fixes. b.wsClient marked every WsClientError permanent, so a consumer driving its own reconnect loop could not tell a terminal handshake failure (a bad URL, a 4xx rejection, an accept-mismatch, a protocol-violation frame) from a transient one (a 5xx handshake rejection, a pong/handshake timeout, a dropped socket) and had to re-derive the taxonomy from error codes. err.permanent now reflects the actual transience of each error, the single bad-status code is split by the carried HTTP status (4xx terminal, 5xx transient) with the status exposed as err.statusCode, and — because the client's own auto-reconnect keyed off the same always-true flag — auto-reconnect was silently disabled for every transient failure and now fires correctly. Separately, b.safePath.resolve / resolveOrNull / validate performed its lexical containment with the runtime path module while the per-segment naming walk used opts.platform. The two disagreeing broke cross-platform validation both ways: every legitimate in-base path was refused with safe-path/escapes-base when opts.platform differed from the host, and — more seriously — a POSIX host validating opts.platform: \"windows\" accepted a backslash traversal (ok\\..\\..\\outside), because the runtime resolver treats \\ as an ordinary filename character and never collapsed the .. segments, so the path escaped the base once a Windows consumer read the backslashes. The lexical resolve and containment boundary now use the target platform's path module (node:path.win32 / node:path.posix), matching the segment walk, so in-base paths resolve and cross-platform traversals are refused under any opts.platform override; the realpath check, which touches the live filesystem, keeps its runtime resolve.",
|
|
1124
|
+
"sections": [
|
|
1125
|
+
{
|
|
1126
|
+
"heading": "Fixed",
|
|
1127
|
+
"items": [
|
|
1128
|
+
{
|
|
1129
|
+
"title": "b.wsClient: WsClientError carries a real terminal/transient signal, and auto-reconnect works again",
|
|
1130
|
+
"body": "WsClientError was declared always-permanent, so err.permanent was true for every error — a consumer's reconnect loop could not distinguish a terminal handshake failure (bad URL, bad subprotocol, malformed handshake header, accept-mismatch, bad upgrade, bad status line, a 4xx rejection, an oversized/protocol-violation frame) from a transient one (a 5xx handshake rejection, a pong- or handshake-timeout, a dropped socket) and had to maintain its own error-code list tracking the framework's taxonomy. err.permanent now reflects each error's actual transience, derived from its code (a new/unknown code defaults to terminal, so it fails closed rather than redialing a hopeless target forever); the single ws-client/bad-status code is split by the response status (4xx terminal, 5xx transient) and the status is exposed as err.statusCode (the existing err.status alias is preserved). The same always-permanent flag also drove the client's built-in auto-reconnect, which therefore never retried any framework-surfaced transient failure (a 5xx handshake or a keepalive timeout); reconnect now fires for transient failures and still skips terminal ones."
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
"title": "b.safePath: cross-platform containment resolves with the target platform's path semantics",
|
|
1134
|
+
"body": "b.safePath.resolve / resolveOrNull / validate performed its lexical containment (resolve rel under base, then bound the result with a separator slice) using the runtime path module, while the per-segment naming walk used opts.platform. When the two disagreed, validation broke both ways. Benign direction: a Linux service validating server-origin names against the stricter Windows ruleset (the recommended cross-platform pattern) had every legitimate in-base path refused with safe-path/escapes-base, because the runtime-separated resolved path could never match a Windows-separator boundary. Security direction: a POSIX host validating opts.platform: \"windows\" accepted a backslash traversal such as ok\\..\\..\\outside — the segment walk splits on \\ for Windows, but the runtime resolver on POSIX treats \\ as an ordinary filename character and never collapsed the .. segments, so the path passed containment and resolved to <base>/ok\\..\\..\\outside, which escapes the base once a Windows consumer interprets the backslashes. The lexical resolve and the containment boundary now use the target platform's path module (node:path.win32 when validating for Windows, node:path.posix otherwise), matching the segment walk, so in-base paths resolve correctly and a genuine traversal is refused under any opts.platform override. The realpath check, which resolves symlinks on the live filesystem, keeps a separate runtime resolve because a foreign-platform path cannot be symlink-resolved on the host. opts.platform continues to gate the per-segment naming rules (reserved names, trailing dot/space, NTFS ADS colon)."
|
|
1135
|
+
}
|
|
1136
|
+
]
|
|
1137
|
+
}
|
|
1138
|
+
]
|
|
1139
|
+
},
|
|
1140
|
+
{
|
|
1141
|
+
"version": "0.15.22",
|
|
1142
|
+
"date": "2026-06-24",
|
|
1143
|
+
"headline": "Internal test-harness reliability only — the published library is byte-for-byte identical to 0.15.21",
|
|
1144
|
+
"summary": "This release changes only the repository's own test harness and test files; nothing under the shipped tarball (index.js, bin/, lib/) is touched, so the API, behavior, and published files are identical to 0.15.21 and operators have nothing to do. The smoke worker now attributes a fire-and-forget failure — an unawaited promise or an unreleased handle (a retry timer, a shutdown wait, a watcher/reload restart) that throws AFTER a test's assertions already passed — to the exact test that caused it and retries it once, instead of the prior unattributable, intermittent failure that only surfaced on a resource-starved CI runner. Five test files that invoked their run() at module scope (re-running and, in some cases, exiting the worker before it could report a result) are corrected to run only under require.main === module, and a test-discipline check keeps the pattern from returning. An opt-in handle-leak audit (SMOKE_AUDIT_HANDLES) surfaces tests that hold a timer / socket / server / worker past completion, for a follow-up cleanup pass.",
|
|
1145
|
+
"sections": [
|
|
1146
|
+
{
|
|
1147
|
+
"heading": "Changed",
|
|
1148
|
+
"items": [
|
|
1149
|
+
{
|
|
1150
|
+
"title": "Smoke test harness attributes late-async-error failures and fixes module-level test self-execution",
|
|
1151
|
+
"body": "The forked smoke worker installs unhandledRejection / uncaughtException handlers and a settle tick so a failure that fires after a test's assertions pass — a leaked handle's callback or an unawaited promise from retry / shutdown / reload logic — is reported against the test that caused it and retried once (a transient passes, a persistent one fails again and names the bug), replacing the prior intermittent, unattributable 'fork failed' that only appeared on a starved CI runner. Five test files (two integration, three layer-0, plus two wiki-suite harnesses) that called run() at module scope are corrected to execute only under require.main === module so they no longer double-run or exit the worker prematurely when required, and a codebase-patterns test-discipline detector prevents the unguarded-module-level-run shape from recurring. A SMOKE_AUDIT_HANDLES opt-in reports per-file handle leaks for a tracked cleanup. None of this is in the published package."
|
|
1152
|
+
}
|
|
1153
|
+
]
|
|
1154
|
+
}
|
|
1155
|
+
]
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
"version": "0.15.21",
|
|
1159
|
+
"date": "2026-06-24",
|
|
1160
|
+
"headline": "Adds a portable compare-and-swap UPDATE, an IPv6 /64 rate-limit key, a verify-only legacy-TOTP path, request-id AsyncLocalStorage scoping, schema-agnostic backup signing, side-effect-free FSM transition resolution, and the public codepoint-threat catalog — and fixes an FSM state transition that committed without an audit record when its entry hook threw",
|
|
1161
|
+
"summary": "A batch of additive primitives that close gaps operators hit composing the framework: b.sql.guardedUpdate builds the cross-instance-safe compare-and-swap UPDATE (the conditional-INSERT sibling) with b.sql.casWon reading the won/lost result; b.requestHelpers.ipKey (and a rateLimit ipKeyMode) key an IPv6 client by its routing-significant /64 so one end-site can't rotate the low 64 bits to evade a per-IP limit; b.auth.totp gains an opt-in verify-only SHA-1 path for one-final-login legacy-secret migration while generation stays SHA-512-only; b.middleware.requestId can bind the id into an AsyncLocalStorage scope that survives the awaited route chain; b.backupManifest.signBytes/verifyBytes authenticate a consumer's own canonical bytes with the framework signing key; b.fsm gains a side-effect-free instance.target() and a way to defer its transition audit when composed with an external claim; and the codepoint-threat catalog the guard family composes is exposed as b.codepointClass. A correctness fix: an FSM transition whose onEnter hook threw left the state committed but emitted no audit record — the record now always fires (stamped a failure outcome with the hook error) so a state change is never unaudited.",
|
|
1162
|
+
"sections": [
|
|
1163
|
+
{
|
|
1164
|
+
"heading": "Added",
|
|
1165
|
+
"items": [
|
|
1166
|
+
{
|
|
1167
|
+
"title": "b.sql.guardedUpdate + b.sql.casWon — portable compare-and-swap UPDATE",
|
|
1168
|
+
"body": "b.sql.guardedUpdate(table) builds an UPDATE whose required guardWhere(col, expected) fence makes it land only when the row is still in the expected value — the cross-instance-safe way to advance a status or version on a single-statement-per-request backend (a D1-over-HTTP bridge or any autocommit-only adapter without interactive transactions), and the conditional-UPDATE sibling of b.sql.insertSelectWhere. It refuses to render without a fence (an unfenced guardedUpdate is just a plain update). b.sql.casWon(result) reads the won/lost verdict from the affected-row count, normalizing the rowCount / changes / affectedRows field-name divergence across adapters and throwing on an indeterminate result so a phantom win can't ship. Standard SQL across SQLite / Postgres / MySQL; guardWhereOp(col, op, expected) supports a non-equality fence (an optimistic-version or balance guard), and a null fence renders IS NULL."
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
"title": "b.requestHelpers.ipKey + rateLimit ipKeyMode — IPv6 /64 keying",
|
|
1172
|
+
"body": "b.requestHelpers.ipKey(ip, { ipv6Bits }) derives a stable rate-limit / blocklist key: an IPv4 address verbatim (one IPv4 is one host) but an IPv6 address collapsed to its routing-significant /64 prefix. A single IPv6 end-site is allocated a whole /64 (RFC 6177 / RFC 4291) and freely rotates the low 64 bits, so keying on the full 128-bit address lets one site mint unlimited fresh keys and walk a per-IP throttle or evade an exact-address block; keying on the /64 closes that while still distinguishing real end-sites. b.middleware.rateLimit gains ipKeyMode: \"prefix64\" to apply this to its default key (the audit record still logs the full client IP)."
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
"title": "b.auth.totp verify-only SHA-1 path for legacy-secret migration",
|
|
1176
|
+
"body": "b.auth.totp.verify(secret, code, { algorithm: \"sha1\", verifyOnly: true }) authenticates a single legacy code during a re-enrollment flow, so a consumer migrating pre-existing RFC-6238-default (SHA-1) secrets can reuse the maintained verifier — separator stripping, 64-bit counter, drift / replay semantics — instead of hand-rolling a parallel HOTP. SHA-1 is still refused on every generation path (compute / generate / uri), so new-enrollment posture stays SHA-512-only, and the flag is honored only by verify(); each such verification emits an auth.totp.legacy_sha1_verify audit signal."
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
"title": "b.middleware.requestId AsyncLocalStorage scoping",
|
|
1180
|
+
"body": "b.middleware.requestId({ asyncContext: true }) binds the request id into the framework's AsyncLocalStorage scope so b.log.getRequestId() (and every b.log.create-built logger) returns it inside awaited route-handler code, not just on req.requestId. Because the b.router dispatch model runs the route handler after the middleware returns, the binding uses AsyncLocalStorage.enterWith (which persists forward across the awaited chain) rather than a callback wrap that would close before the handler runs; each request runs in its own async context so the binding stays request-scoped. The underlying b.log.enterRequestId(id) is exposed for callers wiring their own middleware."
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
"title": "b.backupManifest.signBytes / verifyBytes — schema-agnostic signing",
|
|
1184
|
+
"body": "b.backupManifest.signBytes(canonicalBytes) and verifyBytes(canonicalBytes, signatureBlock, { expectedFingerprint }) sign and verify a consumer's own canonical bytes with the same audit-sign keypair and fingerprint pinning as the v1-manifest sign() / verifySignature(), without adopting the framework's manifest schema — so a bespoke backup-header format can authenticate itself against the framework signing key. Fingerprint pinning is bound to the key the signature actually verifies under: the pin is checked against the fingerprint recomputed from the signature block's own public key (the new b.auditSign.fingerprintOf(publicKeyPem) helper), not the block's self-asserted fingerprint field, so a block can't claim a trusted fingerprint while being signed by a different key. The same binding now also applies to the v1-manifest verifySignature(). b.auditSign.verify additionally no longer requires init() when given an explicit public key, so a downstream verifier that holds only a trusted public key can check a detached signature."
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
"title": "b.fsm side-effect-free transition resolution + deferrable audit",
|
|
1188
|
+
"body": "instance.target(event) resolves a transition's destination state side-effect-free — the same edge and guard check as can() but returning the to-state (or null when the edge is illegal or guard-refused) — so a consumer composing an external compare-and-swap (b.sql.guardedUpdate on an autocommit-only substrate) can build the SET status = <to> claim without calling transition(), which would mutate state and emit an audit before the cross-instance claim is known to land. transition(event, { audit: false }) suppresses the built-in emit so that composition can emit its own enriched record once the claim resolves."
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
"title": "b.codepointClass — the codepoint-threat catalog on the public surface",
|
|
1192
|
+
"body": "The Unicode bidi-override / C0-control / zero-width / null-byte / Unicode-Tags tables and the UTS #39 confusable-script detector that the b.guard* family composes internally are now exposed as b.codepointClass, so a consumer can build a custom unconstrained-free-text screen — detectCharThreats / assertNoCharThreats / applyCharStripPolicies / scriptFor / detectMixedScripts plus the compiled regexes — without re-rolling the regexes (where the zero-width class is mistyped and the astral Unicode-Tags block forgotten) or coupling to an internal module path. For a ready-made free-text guard, b.guardText remains the first-class entry point."
|
|
1193
|
+
}
|
|
1194
|
+
]
|
|
1195
|
+
},
|
|
1196
|
+
{
|
|
1197
|
+
"heading": "Fixed",
|
|
1198
|
+
"items": [
|
|
1199
|
+
{
|
|
1200
|
+
"title": "An FSM transition committed its state change without an audit record when the entry hook threw",
|
|
1201
|
+
"body": "b.fsm commits the new state and pushes the history entry before running the destination state's onEnter hook. When that hook threw, control skipped the audit emission entirely — leaving a committed state transition with no audit record, a compliance gap for any regime that requires state changes to be auditable. The transition audit now always fires even when onEnter throws: it records the committed transition stamped with a failure outcome and the hook error, and still re-raises the error to the caller (the documented contract that an onEnter throw surfaces so the operator can roll back is unchanged)."
|
|
1202
|
+
}
|
|
1203
|
+
]
|
|
1204
|
+
}
|
|
1205
|
+
]
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
"version": "0.15.20",
|
|
1209
|
+
"date": "2026-06-24",
|
|
1210
|
+
"headline": "Vendored SBOM version fields are derived from the bundle so they cannot drift, and the Public Suffix List + @simplewebauthn/server bundles are refreshed",
|
|
1211
|
+
"summary": "The vendor manifest recorded each bundled package's version in two scanner-facing places that were hand-maintained and could drift from the code actually shipped — the structured components[].version (the CycloneDX component versions) and the cpe string. Two had drifted: peculiar-pki's @peculiar/x509 component read 1.13.0 while the bundle shipped 2.0.0, and @noble/curves' cpe read 0.0.0 while the bundle shipped 2.2.0. A CVE scanner (Trivy / Grype / a CycloneDX export, or a consumer mirroring the manifest into its own SBOM) keys on those structured fields, so an advisory was matched against the wrong version — a false negative on a real fix or a false positive on a patched one. Both fields are corrected, the vendor-bundle script now derives them from the actually-installed package versions at bundle time so they cannot drift again, and a manifest gate fails the build if they ever disagree. Separately, the vendored Mozilla Public Suffix List is refreshed to the current upstream revision and @simplewebauthn/server is refreshed to 13.3.2.",
|
|
1212
|
+
"sections": [
|
|
1213
|
+
{
|
|
1214
|
+
"heading": "Fixed",
|
|
1215
|
+
"items": [
|
|
1216
|
+
{
|
|
1217
|
+
"title": "Vendored SBOM version metadata is bundle-derived, not hand-maintained",
|
|
1218
|
+
"body": "lib/vendor/MANIFEST.json recorded each package's version in two places a CVE/SBOM scanner reads — the structured components[].version sub-object and the cpe string — both hand-maintained alongside the human version string, so they could (and did) drift from the bundled code. @peculiar/x509's component version read 1.13.0 while the bundle shipped 2.0.0; @noble/curves' cpe read 0.0.0 while the bundle shipped 2.2.0. Either drift makes a scanner match advisories against the wrong version. Both are corrected to the shipped versions, and the durable fix is structural: scripts/vendor-update.sh now writes both the structured component versions and the cpe version from the ACTUALLY-INSTALLED package versions captured at bundle time, so a maintainer can no longer update one field and forget the other. A smoke-time manifest gate additionally fails the build if any component or cpe version disagrees with the package version, catching a manual drift before it ships."
|
|
1219
|
+
}
|
|
1220
|
+
]
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
"heading": "Changed",
|
|
1224
|
+
"items": [
|
|
1225
|
+
{
|
|
1226
|
+
"title": "Vendored Public Suffix List and @simplewebauthn/server refreshed",
|
|
1227
|
+
"body": "The vendored Mozilla Public Suffix List is refreshed to the current upstream revision (used by b.publicSuffix for DMARC / BIMI / cookie-scope / same-site domain classification). @simplewebauthn/server is refreshed from 13.3.1 to 13.3.2, which improves WebAuthn attestation certificate-path validation; the published-tarball diff was reviewed (no install scripts, no network/eval, a self-contained code change) before re-vendoring."
|
|
1228
|
+
}
|
|
1229
|
+
]
|
|
1230
|
+
}
|
|
1231
|
+
]
|
|
1232
|
+
},
|
|
1233
|
+
{
|
|
1234
|
+
"version": "0.15.19",
|
|
1235
|
+
"date": "2026-06-22",
|
|
1236
|
+
"headline": "Restores the wiki container build by keeping its base image on the continuously-patched rolling tag",
|
|
1237
|
+
"summary": "A follow-up to 0.15.18. The framework code is unchanged from 0.15.18 — this patch reverts one part of that release's supply-chain pass: the example wiki container's Chainguard base images had been pinned to specific digests, but Chainguard rebuilds those images continuously to ship CVE fixes, so the frozen digest fell behind an upstream fix within hours and the container's Trivy CRITICAL/HIGH release gate rejected the build (a fixed npm/undici denial-of-service the rolling tag already carried). For a deployed, Trivy-gated image, tracking the rolling, always-patched tag is the correct posture; the wiki Dockerfile is back on it. The ClusterFuzzLite fuzz base (not deployed, not release-gated) stays digest-pinned with Dependabot keeping it current.",
|
|
1238
|
+
"sections": [
|
|
1239
|
+
{
|
|
1240
|
+
"heading": "Fixed",
|
|
1241
|
+
"items": [
|
|
1242
|
+
{
|
|
1243
|
+
"title": "Wiki container builds again on a continuously-patched base",
|
|
1244
|
+
"body": "0.15.18 digest-pinned the example wiki container's Chainguard base images (runtime + builder). Because Chainguard rebuilds those images continuously to ship CVE fixes, the pinned digest went stale within hours and the container's Trivy CRITICAL/HIGH release gate rejected the build over an already-fixed npm/undici DoS (CVE-2026-12151) the rolling tag carried. The wiki Dockerfile tracks the rolling tag again, so the deployed image is always CVE-current and the build passes the release gate. To keep the Trivy-scanned image identical to the multi-arch image that is published (they are built separately), the release workflow resolves the rolling tag to a digest once at build time and feeds it to both builds via build-args — scan-equals-publish without a committed pin that goes stale. (This trades the OSSF Scorecard PinnedDependencies signal for CVE currency on a deployed, gate-scanned image — the right call here; the non-deployed ClusterFuzzLite fuzz base remains digest-pinned with Dependabot bumping it.) The framework package itself is identical to 0.15.18."
|
|
1245
|
+
}
|
|
1246
|
+
]
|
|
1247
|
+
}
|
|
1248
|
+
]
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
"version": "0.15.18",
|
|
1252
|
+
"date": "2026-06-22",
|
|
1253
|
+
"headline": "OCSP response-freshness enforcement is restored, DPoP request-URI reconstruction peer-gates forwarded headers, and container/tool supply-chain pinning is tightened",
|
|
1254
|
+
"summary": "Three hardening fixes. The stapled-OCSP evaluator parsed each response's thisUpdate / nextUpdate with Date.parse, but those fields are already numeric (unix-ms), so Date.parse returned NaN — the freshness guard then rejected every signature-valid response, fresh or stale, with a misleading \"missing thisUpdate\", and the real future-dated / past-nextUpdate window checks (RFC 6960 §4.2.2.1) were unreachable dead code. b.network.tls.ocsp.evaluate / requireGood now read the numeric fields directly: a stale or future-dated response is refused and a fresh \"good\" response is accepted, so OCSP stapling validation works again. b.middleware.dpop reconstructed the absolute request URI — the cryptographically-bound htu — trusting X-Forwarded-Proto / X-Forwarded-Host from any caller whenever trustForwardedHeaders was set, so a direct attacker could forge the scheme or authority and make a proof signed for one origin validate against another; both now resolve through the peer-gated b.requestHelpers.trustedProtocol / trustedHost (honored only from a declared trusted-proxy peer), matching csrf-protect / security-headers / cors, and the bare trustForwardedHeaders boolean is refused. The supply-chain pass pins the wiki container and ClusterFuzzLite fuzz base images to digests (Dependabot keeps them current) and the npm-publish bundle tools to exact versions.",
|
|
1255
|
+
"sections": [
|
|
1256
|
+
{
|
|
1257
|
+
"heading": "Security",
|
|
1258
|
+
"items": [
|
|
1259
|
+
{
|
|
1260
|
+
"title": "OCSP response-freshness enforcement restored (RFC 6960 §4.2.2.1)",
|
|
1261
|
+
"body": "The stapled-OCSP evaluator computed thisUpdate / nextUpdate via Date.parse(), but the parser hands those fields back as unix-ms NUMBERS (Date.UTC(...)). Date.parse() coerces its argument to a string and a bare-integer string is not a recognized date, so the result was always NaN: the !isFinite guard then rejected every signature-valid response — fresh OR stale — with a misleading \"missing thisUpdate\", and the genuine staleness checks (future-dated thisUpdate, past nextUpdate) sat behind it as unreachable dead code, with the stale-rejection branch latently fail-open. b.network.tls.ocsp.evaluate / requireGood now read the numeric fields directly: a stale (past-nextUpdate) or future-dated response is refused, and a fresh \"good\" response is accepted — OCSP stapling validation, and its replay defense, work again."
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
"title": "DPoP htu reconstruction peer-gates X-Forwarded-Proto and X-Forwarded-Host",
|
|
1265
|
+
"body": "b.middleware.dpop rebuilds the absolute request URI (scheme + authority + path) that the proof's cryptographically-bound htu claim (RFC 9449 §4.3) is verified against. When the legacy trustForwardedHeaders: true was set it derived the scheme and host from X-Forwarded-Proto / X-Forwarded-Host trusted from ANY caller, with no immediate-peer check — a direct attacker could forge X-Forwarded-Proto: https or a victim X-Forwarded-Host and make a proof signed for one origin validate against another (htu confusion). Both now resolve through the peer-gated b.requestHelpers.trustedProtocol / trustedHost, which honor the forwarded headers only when the immediate connection is a declared trusted proxy — the same fail-closed model csrf-protect (Secure cookie), security-headers (HSTS), cors (same-origin), and bot-guard (secure context) already use. A codebase-patterns detector now flags any further middleware that reads X-Forwarded-Proto / -Host directly for a scheme/authority decision."
|
|
1266
|
+
}
|
|
1267
|
+
]
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
"heading": "Added",
|
|
1271
|
+
"items": [
|
|
1272
|
+
{
|
|
1273
|
+
"title": "b.requestHelpers.requestHost and b.requestHelpers.trustedHost",
|
|
1274
|
+
"body": "Peer-gated request-authority resolvers — the host companions to requestProtocol / trustedProtocol. trustedHost(opts) returns { resolve(req) => string|null, peerGated }: with trustedProxies (CIDRs) X-Forwarded-Host is honored only from a trusted-proxy peer; with hostResolver(req) the operator owns it; with neither, only the request's own Host header is used and a forged X-Forwarded-Host is ignored. requestHost(req, opts?) is the low-level resolver (default Host-only; a peer predicate gates the forwarded header). For reconstructing an absolute request URL (a DPoP htu, an origin/issuer string, a redirect base) behind a proxy without trusting a forgeable header."
|
|
1275
|
+
}
|
|
1276
|
+
]
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
"heading": "Changed",
|
|
1280
|
+
"items": [
|
|
1281
|
+
{
|
|
1282
|
+
"title": "Container and bundle-tool supply-chain pinning tightened",
|
|
1283
|
+
"body": "The wiki container's base images (cgr.dev/chainguard/node runtime + builder stages) and the ClusterFuzzLite fuzz base (gcr.io/oss-fuzz-base/base-builder-javascript) are pinned to image digests; Dependabot's docker ecosystem keeps both current, so the pin is reproducible without freezing CVE patches. The npm-publish workflow's bundle tools (esbuild, postject) are pinned to exact versions, matching CI. The OSS-Fuzz project-submission Dockerfile is intentionally left tracking the upstream base-builder, per OSS-Fuzz convention."
|
|
1284
|
+
}
|
|
1285
|
+
]
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
"heading": "Migration",
|
|
1289
|
+
"items": [
|
|
1290
|
+
{
|
|
1291
|
+
"title": "DPoP: trustForwardedHeaders is replaced by trustedProxies",
|
|
1292
|
+
"body": "b.middleware.dpop no longer honors the bare trustForwardedHeaders: true boolean — it trusted forgeable X-Forwarded-Proto / X-Forwarded-Host from any caller. Behind a reverse proxy, declare your proxy CIDRs via trustedProxies: [\"10.0.0.0/8\", …] (peer-gates both headers for the htu reconstruction), or own the reconstruction via protocolResolver(req) / hostResolver(req) / getHtu(req). A bare trustForwardedHeaders: true now throws at create() with that guidance. Apps not behind a proxy need no change — the default already derives the scheme from the TLS socket and the host from the request's Host header."
|
|
1293
|
+
}
|
|
1294
|
+
]
|
|
1295
|
+
}
|
|
1296
|
+
]
|
|
1297
|
+
},
|
|
1298
|
+
{
|
|
1299
|
+
"version": "0.15.17",
|
|
1300
|
+
"date": "2026-06-22",
|
|
1301
|
+
"headline": "The job queue runs on a Postgres or MySQL cluster backend, and a set of correctness and confidentiality fixes land for encrypted-API rejections, flow-job ordering, the self-update signature verifier, and bounded file reads",
|
|
1302
|
+
"summary": "The local job queue's lease path is now dialect-aware, so the queue works when its store is a Postgres or MySQL cluster backend rather than only single-node SQLite. The previous lease was a single SQLite-only statement: it double-quoted identifiers (which MySQL reads as string literals), updated a table named in its own subquery (MySQL error 1093), and used RETURNING (which MySQL rejects), so the first enqueue or lease against a cluster backend failed outright. The lease now resolves the active backend dialect and composes per-dialect SQL — a transactional SELECT ... FOR UPDATE SKIP LOCKED claim over the head of the queue followed by a guarded UPDATE against a literal id list, returning the claimed rows via RETURNING on Postgres and a re-select on MySQL — with backtick (MySQL) or double-quote (Postgres) identifier quoting, mirroring the framework's outbox claim. enqueue, lease, complete, fail, sweep, and dead-letter all run against the cluster backend, verified end-to-end against live MySQL and Postgres. Alongside the queue work: a request rejected on an established encrypted-API session is now returned inside the session envelope instead of leaking the rejection reason in cleartext; a flow child that declares dependencies can no longer be leased before those dependencies run during the brief window of the two-pass flow enqueue; the standalone self-update verifier classifies an ECDSA signature's encoding by its ASN.1 structure rather than its byte length and fails closed on an indeterminate shape; the self-update poller surfaces each asset's content digest; and the bounded synchronous file reader routes every failure branch through its typed error mapper.",
|
|
1303
|
+
"sections": [
|
|
1304
|
+
{
|
|
1305
|
+
"heading": "Fixed",
|
|
1306
|
+
"items": [
|
|
1307
|
+
{
|
|
1308
|
+
"title": "The job queue's lease runs dialect-correct SQL on a Postgres or MySQL cluster backend",
|
|
1309
|
+
"body": "When the queue's store is a cluster backend, the lease previously failed at the first enqueue or claim: it was composed as one SQLite-only statement that double-quoted column identifiers (a syntax error on MySQL, which treats double quotes as string literals), updated the jobs table from a subquery that named the same table (MySQL error 1093), and relied on RETURNING (which MySQL does not support). The lease now resolves the active backend dialect and builds per-dialect SQL — a transactional SELECT ... FOR UPDATE SKIP LOCKED over the head of the queue, then a guarded UPDATE ... WHERE status = 'pending' AND id IN (...) with a literal id list (no self-referencing subquery), reading the claimed rows back via RETURNING on Postgres and a re-select by id on MySQL — and quotes identifiers with backticks on MySQL and double quotes on Postgres. enqueue, lease, complete, fail, sweep, and the dead-letter operations now work against a cluster backend, with sealed-at-rest job payloads round-tripping correctly; single-node SQLite continues to use the original single-statement claim. The behavior is verified end-to-end against live MySQL and Postgres."
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
"title": "Encrypted-API rejections that would disclose session state ride the session envelope",
|
|
1313
|
+
"body": "On a per-session encrypted API channel, a rejection that reveals session state — an expired or rotated session, or a request admitted past the replay gate whose ciphertext then failed authentication — returned its reason as cleartext JSON, disclosing which check failed to a network observer of an otherwise-encrypted channel. Those rejections are now encrypted inside the session envelope under the response counter the server persists for them. The generic 'rejected' refusals that carry no session-lifecycle detail (a stale or duplicate request counter) stay plaintext: they reveal nothing a 400 status does not, and encrypting them would consume a response counter the server does not persist on those paths — desyncing the session's monotonic counter and bricking it for the next genuine request. Pre-session handshake failures (a malformed bootstrap, a stale timestamp, an unknown session) also remain plaintext, since no key exists yet to encrypt under."
|
|
1314
|
+
},
|
|
1315
|
+
{
|
|
1316
|
+
"title": "A flow child with dependencies can no longer be leased before its dependencies",
|
|
1317
|
+
"body": "b.queue.enqueueFlow inserts a flow's children in two passes (the second resolves dependency names to the sibling job ids assigned in the first). The first pass previously enqueued every child as immediately leaseable and relied on the second pass to park the dependency-bearing children, leaving a window in which a consumer polling between the two passes could lease a child before the jobs it depends on had run. A dependency-bearing child is now parked at enqueue time on the first pass, so it is never leaseable in that window; the second pass only rewrites its dependency names to the resolved job ids. Children with no dependencies remain immediately leaseable."
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
"title": "The self-update poller carries each asset's content digest",
|
|
1321
|
+
"body": "b.selfUpdate.poll() now includes the content digest for each asset and its detached signature in the result it returns, so a caller can verify a downloaded asset against the digest advertised by the release feed."
|
|
1322
|
+
},
|
|
1323
|
+
{
|
|
1324
|
+
"title": "Bounded file reads always return the typed error shape",
|
|
1325
|
+
"body": "The bounded synchronous file reader could surface an unmapped error on a few failure branches — a refused symlink whose target had since been removed, a short read, an integrity mismatch — rather than the typed error its callers expect. Every failure branch now passes through the reader's error mapper, so a caller always receives the documented error kind."
|
|
1326
|
+
}
|
|
1327
|
+
]
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
"heading": "Changed",
|
|
1331
|
+
"items": [
|
|
1332
|
+
{
|
|
1333
|
+
"title": "Bundled Public Suffix List refreshed to current upstream",
|
|
1334
|
+
"body": "The vendored Mozilla Public Suffix List — which b.publicSuffix uses to derive organizational domains for DMARC (psd= / np=), BIMI, cookie-scope checks, and same-site policy — is refreshed to the latest upstream revision. Domain classification reflects recent additions and removals to the list."
|
|
1335
|
+
}
|
|
1336
|
+
]
|
|
1337
|
+
},
|
|
1338
|
+
{
|
|
1339
|
+
"heading": "Security",
|
|
1340
|
+
"items": [
|
|
1341
|
+
{
|
|
1342
|
+
"title": "The standalone self-update verifier detects ECDSA signature encoding by structure",
|
|
1343
|
+
"body": "The standalone update-signature verifier inferred whether an ECDSA signature was DER- or IEEE-P1363-encoded from its byte length — a heuristic two distinct encodings can collide on, which could lead a valid-but-misclassified signature to be parsed under the wrong scheme. It now parses the signature's ASN.1 structure to classify the encoding and fails closed on an indeterminate shape rather than guessing."
|
|
1344
|
+
}
|
|
1345
|
+
]
|
|
1346
|
+
}
|
|
1347
|
+
]
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
"version": "0.15.16",
|
|
1351
|
+
"date": "2026-06-22",
|
|
1352
|
+
"headline": "A correctness and hardening release: a broad set of credential and certificate verifiers now fail closed on a present-but-unparseable timestamp instead of silently skipping the check, outbound network clients gain an overall wall-clock timeout and a bounded framing buffer, DMARC refuses a record with no usable policy, append-only log sinks refuse a symlinked path, and the interactive-transaction and replay-store contracts refuse a backend that cannot honor them",
|
|
1353
|
+
"summary": "This release closes a family of fail-open and resource-exhaustion gaps without adding opt-ins to the request path. A recurring pattern across the verifiers — a timestamp parsed with a lenient parser, then gated by isFinite() so an unparseable value disables the check — is converted to fail-closed everywhere it appears: DKIM x=/t=/l=, ARC AMS/AS t=/x=, SAML Bearer and holder-of-key NotBefore, and the FIDO MDS3 / BIMI / S/MIME certificate validity windows now refuse a malformed value rather than accepting the credential. DMARC now validates and requires a usable p= policy and never recommends delivery for a failing message with an absent or unrecognized policy. Outbound clients that exposed a single timeout but applied it only as an idle (zero-progress) timer now also enforce it as an overall wall-clock cap, so a peer that trickles bytes within the idle window can no longer hold a request open indefinitely: the encrypted HTTP client, the object-store and SQS request paths, the CloudWatch / OTLP / webhook log sinks, and the password breach check. The DNS resolver and the lower-level DNS client gain a per-query wall-clock deadline that also tears the socket down, the SMTP client and the proxy CONNECT tunnel bound their framing buffers and add an overall deadline, and the NTS key-establishment handshake bounds its accumulator. The append-only local log sink and the daemon log open with O_NOFOLLOW so a symlink planted at the path is refused rather than followed. b.externalDb.transaction and b.outbox now refuse a backend that declares it cannot provide an interactive transaction instead of silently running a non-atomic block, and the GraphQL-federation replay store adopts the framework's atomic check-and-insert contract.",
|
|
1354
|
+
"sections": [
|
|
1355
|
+
{
|
|
1356
|
+
"heading": "Security",
|
|
1357
|
+
"items": [
|
|
1358
|
+
{
|
|
1359
|
+
"title": "Certificate and credential verifiers fail closed on an unparseable timestamp",
|
|
1360
|
+
"body": "A present-but-unparseable date no longer silently disables a validity check. DKIM x= / t= (signature expiry, future-date, ordering) and l= (body-length cap), ARC AMS/AS t= / x=, SAML Bearer and holder-of-key SubjectConfirmationData NotBefore, and the FIDO MDS3, BIMI, and S/MIME certificate validity windows previously parsed the value, then guarded the comparison with isFinite() — so a value that did not parse (NaN) skipped the check and the credential was accepted. Each now refuses a present-but-unparseable value (a permerror / not-valid result) and only an absent optional field is ignored, matching the existing fail-closed handling of the SAML Conditions block."
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
"title": "DMARC refuses a record with no usable policy",
|
|
1364
|
+
"body": "The DMARC policy tag p= (and the subdomain sp=) were taken verbatim with no validation and p= was never required, so the disposition mapped a null or typo'd policy to the catch-all \"deliver\". A failing, unaligned message against a domain whose record omitted p= or carried an unrecognized value was therefore delivered. p= and sp= are now validated against none|quarantine|reject, p= is required for a record to carry a policy at all, an invalid record is a permerror (not a transient temperror), and the disposition is an explicit fail-closed mapping that never delivers a failing message on an absent or unrecognized policy."
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
"title": "Outbound clients enforce an overall wall-clock timeout, not only an idle timeout",
|
|
1368
|
+
"body": "Several clients exposed a single timeout but forwarded it to the underlying request only as idleTimeoutMs (a zero-progress cap that resets on every received byte), leaving no overall bound — a peer that trickles one byte inside each idle window holds the call open indefinitely. b.httpClient.encrypted (which dropped timeoutMs entirely, and now also forwards the caller's AbortSignal), the object-store request path and Azure/GCS bucket operations, the SQS queue client, the CloudWatch / OTLP / webhook log sinks, and the password HIBP breach check now set the overall wall-clock timeout alongside the idle timeout."
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
"title": "DNS lookups gain a default wall-clock deadline and tear the socket down",
|
|
1372
|
+
"body": "The DNS resolver's DoH transport had no time bound of any kind and the lower-level DNS client defaulted its lookup timeout to zero (no deadline). A non-responsive or slow-trickle upstream — reachable while resolving DKIM/SPF/BIMI records for inbound mail — held the lookup pending forever. The resolver now takes a per-query timeout (default 10s) wrapping every query and CNAME hop, the DNS client defaults its lookup timeout to 10s (operators opt out with a zero timeout), and the raw DoH/DoT request paths arm a socket timeout that destroys the connection on a stall rather than leaking the descriptor."
|
|
1373
|
+
},
|
|
1374
|
+
{
|
|
1375
|
+
"title": "SMTP, proxy-CONNECT, and NTS clients bound their read buffers and add deadlines",
|
|
1376
|
+
"body": "The SMTP client accumulated the server's reply without a byte cap and relied on an idle timer alone; a hostile or broken MX that never sent a line terminator could exhaust memory, and a slow trickle held the transaction open. It now caps the accumulated reply (refusing once it exceeds the response ceiling) and enforces an overall transaction deadline. The proxy CONNECT-tunnel client, which accumulated the proxy's reply unbounded with no socket timeout, now caps the header buffer and times out the connect. The NTS key-establishment handshake reader caps its record accumulator so a server streaming non-terminating records cannot exhaust memory before the handshake timer fires."
|
|
1377
|
+
},
|
|
1378
|
+
{
|
|
1379
|
+
"title": "Append-only log sinks refuse a symlinked path",
|
|
1380
|
+
"body": "The local log sink and the daemon log opened the active file in append mode without O_NOFOLLOW, so a symlink planted at the log path (including one re-planted in the race window after a caller's pre-check) was followed, redirecting log writes to an attacker-chosen file. Both now open with O_NOFOLLOW via the new b.atomicFile.openAppendNoFollowSync, refusing a symlinked final path component."
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
"title": "Transaction and replay-store contracts refuse a backend that cannot honor them",
|
|
1384
|
+
"body": "b.externalDb.transaction and b.outbox.create now refuse a backend that declares it cannot provide an interactive transaction (a stateless / autocommit-per-statement adapter) instead of silently running BEGIN, the body, and COMMIT on different sessions — no isolation, no rollback — while reporting success. b.graphqlFederation.guardSdl now consults its replay nonce store through the framework's atomic checkAndInsert(nonce, expireAt) contract (the same one b.webhook and b.nonceStore use) rather than a non-atomic has()-then-remember() check that raced concurrent redeliveries; a store lacking checkAndInsert is refused at construction."
|
|
1385
|
+
}
|
|
1386
|
+
]
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
"heading": "Fixed",
|
|
1390
|
+
"items": [
|
|
1391
|
+
{
|
|
1392
|
+
"title": "b.session.destroyAllForUser works for pluggable-store consumers",
|
|
1393
|
+
"body": "A consumer configuring sessions through b.session.useStore without calling b.db.init() got a db/not-initialized error from destroyAllForUser — every \"log out everywhere\" / suspend / delete path rejected with a 500 after the store rows had already been deleted, because the stateless valid-from boundary write was routed only to the framework database. The boundary now prefers the framework database when one is initialized and otherwise falls back to the configured session store (provisioning its table on demand), so store-backed deployments raise the stateless boundary successfully; it still fails closed when neither a framework database nor a store is available."
|
|
1394
|
+
}
|
|
1395
|
+
]
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
"heading": "Added",
|
|
1399
|
+
"items": [
|
|
1400
|
+
{
|
|
1401
|
+
"title": "b.atomicFile.openAppendNoFollowSync — symlink-refusing append open",
|
|
1402
|
+
"body": "Opens a long-lived append target (an active log file kept open across appends and reopened on rotation) with O_WRONLY | O_APPEND | O_CREAT | O_NOFOLLOW: the file is created or appended normally, but a symlink at the final path component fails the open closed (ELOOP) rather than redirecting writes. The append-sink counterpart to openNoFollowSync (read) and the exclusive-create temp open."
|
|
1403
|
+
},
|
|
1404
|
+
{
|
|
1405
|
+
"title": "b.externalDb.supportsTransactions() and stateless-adapter declaration",
|
|
1406
|
+
"body": "A backend adapter may declare supportsTransactions: false (a stateless / autocommit-per-statement adapter) and/or provide a batch(client, statements) hook for an atomic multi-statement path. b.externalDb.supportsTransactions() reports whether the picked backend can provide an interactive transaction, so a consumer built on the dual-write guarantee can refuse a non-atomic backend up front."
|
|
1407
|
+
},
|
|
1408
|
+
{
|
|
1409
|
+
"title": "Store-free device fingerprint",
|
|
1410
|
+
"body": "b.sessionDeviceBinding.fingerprint(req, opts) is now a static entry point returning the request-shape digest with no store, and b.sessionDeviceBinding.create() with neither a bindingStore nor storeInSession returns an instance whose stateless fingerprint() works while bind()/verify() throw a clear \"no store configured\". Soft device binding for self-validating tokens (a sealed cookie or JWT carrying the fingerprint) no longer needs a fabricated no-op store."
|
|
1411
|
+
}
|
|
1412
|
+
]
|
|
1413
|
+
},
|
|
1414
|
+
{
|
|
1415
|
+
"heading": "Changed",
|
|
1416
|
+
"items": [
|
|
1417
|
+
{
|
|
1418
|
+
"title": "Device fingerprint IP masking is canonical",
|
|
1419
|
+
"body": "b.sessionDeviceBinding masked the client IP into its prefix bucket by textual group slicing with no IPv6 normalization, so equivalent forms of one address (a ::-compressed form vs its fully-expanded form) hashed to different fingerprints and a roaming client could be logged out on a false drift. It now masks through the same canonical prefix helper b.session uses, preserving the configured prefix width (the IPv6 default stays /48, and ipPrefixBits is honored). b.requestHelpers.ipPrefix accepts { v4Bits, v6Bits } to override the /24 + /64 default for a function-form fingerprint or other reuse."
|
|
1420
|
+
}
|
|
1421
|
+
]
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
"heading": "Migration",
|
|
1425
|
+
"items": [
|
|
1426
|
+
{
|
|
1427
|
+
"title": "DNS lookups now time out by default",
|
|
1428
|
+
"body": "The lower-level DNS client's lookup timeout previously defaulted to zero (no deadline); it now defaults to 10 seconds. A deployment that intentionally ran DNS lookups with no wall-clock bound must set the lookup timeout to zero explicitly to restore the old behavior. The b.network.dns.resolver default timeout is also 10 seconds and is configurable via its timeoutMs option."
|
|
1429
|
+
},
|
|
1430
|
+
{
|
|
1431
|
+
"title": "b.graphqlFederation.guardSdl nonceStore contract",
|
|
1432
|
+
"body": "guardSdl's optional replay nonceStore now requires the atomic checkAndInsert(nonce, expireAt) contract (b.nonceStore.create is the reference store) and refuses the previous { has, remember } shape at construction. Operators using the old shape should pass b.nonceStore.create(...) or an adapter exposing checkAndInsert."
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
"title": "Interactive transactions on a stateless backend are refused",
|
|
1436
|
+
"body": "If an externalDb backend adapter declares supportsTransactions: false, b.externalDb.transaction and b.outbox.create now throw rather than running a silently non-atomic block. Existing stateful adapters (Postgres / MySQL / SQLite, and any adapter that supplies beginTx/commit/rollback or omits the flag) are unaffected. A stateless / autocommit-per-statement adapter should supply interactive transaction hooks or a batch adapter."
|
|
1437
|
+
}
|
|
1438
|
+
]
|
|
1439
|
+
}
|
|
1440
|
+
]
|
|
1441
|
+
},
|
|
1442
|
+
{
|
|
1443
|
+
"version": "0.15.15",
|
|
1444
|
+
"date": "2026-06-21",
|
|
1445
|
+
"headline": "A focused correctness and hardening release: more authentication and signature verifiers fail closed (JWT issuer, SAML recipient binding, OAuth nonce, CIBA token binding, FIDO certification level), the WebSocket client bounds a fragmented message before it completes, DMARC resolves the From domain before the policy lookup, and the CSP builder refuses a directive-injecting source; fixes a middleware pipeline promise that never settled on a write-and-halt, and exposes the X.509 CA-bit issuer test, an IP-prefix helper, and a reverse-proxy-aware session fingerprint option",
|
|
1446
|
+
"summary": "This release continues to tighten existing protections without adding opt-ins on the request path. A further set of authentication and signature verifiers now refuse malformed or unbound credentials: JWT verify rejects an array-valued issuer claim (an any-match bypass, CVE-2025-30144 class), SAML requires the mandatory Recipient on a Bearer or holder-of-key confirmation and enforces every AudienceRestriction, OAuth no longer skips the ID-token nonce check on an empty nonce, CIBA binds the returned ID token to its auth_req_id, the FIDO MDS3 certification level reflects the authenticator's current status rather than its historical maximum, and a JOSE header that decodes to a non-object is a typed error rather than an uncaught throw. The WebSocket client now bounds a fragmented incoming message by its running total instead of only at the final frame, DMARC validates the From-header domain before the policy lookup, the data-residency write gate compares the residency column case-insensitively, and the SQL builder refuses an equality against NULL and validates every IN-list element. The CSP builder refuses a source token containing whitespace or a semicolon (directive injection). A middleware pipeline that a request handler halted by writing the response without calling next() left its promise pending forever, retaining the request closure — it now settles. New surface is additive and backward compatible: the basicConstraints-enforcing X.509 issuer test is exposed publicly, the session device fingerprint can resolve the client IP through a trusted-proxy allowlist, and the /24+/64 IP-prefix masking it uses is exposed for reuse.",
|
|
1447
|
+
"sections": [
|
|
1448
|
+
{
|
|
1449
|
+
"heading": "Added",
|
|
1450
|
+
"items": [
|
|
1451
|
+
{
|
|
1452
|
+
"title": "b.x509Chain — the CA-bit-enforcing X.509 issuer test",
|
|
1453
|
+
"body": "isCaCert(cert) and issuerValidlyIssued(issuer, subject) are now public. Node's X509Certificate.checkIssued() does not enforce the basicConstraints cA bit (CVE-2002-0862 class), so a non-CA leaf can be accepted as an issuer; these helpers require cA:TRUE and verify the signature, fail closed on any malformed input, and are the same test the framework's own chain walkers use. A consumer validating a chain outside a TLS handshake (an operator-uploaded CA bundle, a non-handshake PQ-signed certificate) can now use the hardened path instead of raw checkIssued()."
|
|
1454
|
+
},
|
|
1455
|
+
{
|
|
1456
|
+
"title": "Reverse-proxy-aware session device fingerprint",
|
|
1457
|
+
"body": "b.session.create / verify / rotate accept { trustedProxies } (CIDRs) or { clientIpResolver } so the built-in clientIp / clientIpPrefix fingerprint fields resolve the real client IP behind a trusted proxy, consistent with b.requestHelpers.trustedClientIp. Without the option the fingerprint still binds to the bare socket peer (unchanged default), so existing fingerprints keep matching; pass the same option to create, verify, and rotate. The /24 (IPv4) + /64 (IPv6) subnet masking is exposed as b.requestHelpers.ipPrefix for an operator using a function-form fingerprint field."
|
|
1458
|
+
}
|
|
1459
|
+
]
|
|
1460
|
+
},
|
|
1461
|
+
{
|
|
1462
|
+
"heading": "Security",
|
|
1463
|
+
"items": [
|
|
1464
|
+
{
|
|
1465
|
+
"title": "More authentication and signature verifiers fail closed",
|
|
1466
|
+
"body": "JWT verify rejects an array-valued iss claim (it had passed through a generic any-match built for the legitimately-array aud claim, so a multi-issuer array satisfied a single-issuer expectation — CVE-2025-30144 class), matching the external JWT and OIDC verifiers. SAML verifyResponse requires the Recipient attribute on a Bearer or holder-of-key SubjectConfirmation and confirms it equals the ACS URL (an absent Recipient was silently skipped), and enforces every AudienceRestriction (AND-combined per SAML core). OAuth no longer fails open and skips the ID-token nonce replay check when the supplied nonce is empty. CIBA binds the polled / notified ID token to its auth_req_id so a token minted for another request can't be accepted. The FIDO MDS3 certification level reflects the authenticator's current status, not the highest level it ever held, so a later decertification or downgrade is honored by a step-up policy. A JOSE header or payload that decodes to JSON null or a non-object now raises a typed authentication error instead of an uncaught TypeError."
|
|
1467
|
+
},
|
|
1468
|
+
{
|
|
1469
|
+
"title": "WebSocket fragmented-message reassembly is bounded before completion",
|
|
1470
|
+
"body": "The client enforced maxMessageBytes only when a fragmented message's final frame arrived, so a peer could stream unbounded continuation frames and exhaust memory before the limit was ever checked. The running reassembly total is now enforced per frame, and a frame arriving after close is ignored."
|
|
1471
|
+
},
|
|
1472
|
+
{
|
|
1473
|
+
"title": "DMARC resolves the From domain before the policy lookup",
|
|
1474
|
+
"body": "The From-header bare addr-spec domain extraction did not reject RFC 5322 group syntax or a trailing semicolon, so a crafted From header could yield a corrupted domain that defeated the DMARC alignment / policy lookup. The domain is now validated (length-bounded, rejecting the group and punctuation forms) before it is used."
|
|
1475
|
+
},
|
|
1476
|
+
{
|
|
1477
|
+
"title": "Data-residency and SQL builder correctness",
|
|
1478
|
+
"body": "The per-row data-residency write gate compares the residency column name case-insensitively, so a raw UPDATE that spells the column in a different letter case can no longer slip the cross-border transfer check. The SQL builder refuses an equality against NULL (col = NULL is always false; use IS NULL) and validates every element of an IN-list on the Postgres = ANY(?) path, matching the sqlite / MySQL path."
|
|
1479
|
+
},
|
|
1480
|
+
{
|
|
1481
|
+
"title": "CSP builder refuses a directive-injecting source",
|
|
1482
|
+
"body": "A CSP source is a single non-whitespace token. build() and mergeDirectives() now reject a source containing whitespace or a semicolon — because the emitter space-joins sources and semicolon-joins directives, a value like \"https://x; script-src https://evil\" injected a live directive."
|
|
1483
|
+
}
|
|
1484
|
+
]
|
|
1485
|
+
},
|
|
1486
|
+
{
|
|
1487
|
+
"heading": "Changed",
|
|
1488
|
+
"items": [
|
|
1489
|
+
{
|
|
1490
|
+
"title": "DKIM l= is counted over the canonicalized body",
|
|
1491
|
+
"body": "On verify, the l= body-length tag was applied to the raw body before canonicalization, so a legitimate relaxed/relaxed signature whose l= matched the canonicalized length was rejected with a body-hash mismatch whenever relaxed canonicalization changed the byte count within the first l= octets. The body is now canonicalized first and the canonicalized octet stream truncated to l= (RFC 6376 §3.4.5)."
|
|
1492
|
+
},
|
|
1493
|
+
{
|
|
1494
|
+
"title": "PIPL cross-border security-assessment threshold documentation corrected",
|
|
1495
|
+
"body": "The b.pipl.sccFilingAssessment contract stated the CAC security assessment becomes mandatory above 100,000 cumulative PI subjects (the superseded 2022 figure). The builder enforces the current CAC Provisions on Promoting and Regulating Cross-Border Data Flows — a security assessment above 1,000,000 non-sensitive PI subjects or 10,000 sensitive, with the 100,000–1,000,000 band in the standard-contract / certification tier. The documentation now matches the enforced thresholds."
|
|
1496
|
+
}
|
|
1497
|
+
]
|
|
1498
|
+
},
|
|
1499
|
+
{
|
|
1500
|
+
"heading": "Fixed",
|
|
1501
|
+
"items": [
|
|
1502
|
+
{
|
|
1503
|
+
"title": "composePipeline settles its promise when a middleware halts",
|
|
1504
|
+
"body": "A regular middleware that wrote the response and returned without calling next() — the intended way to halt the chain from an auth / rate-limit / bot block — left the promise returned by b.middleware.composePipeline pending forever, retaining its request/response closure (an unbounded leak under sustained blocked traffic). The halt path now settles the promise without advancing to the route handler; the same applies to an error handler that consumes the error without calling next()."
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
"title": "Quality-list header parsing is quote-aware",
|
|
1508
|
+
"body": "parseQualityList mis-read a q= substring that appeared inside a quoted media-range or Accept-* parameter, mis-weighting content negotiation. It now parses the q parameter with quote-aware splitting."
|
|
1509
|
+
}
|
|
1510
|
+
]
|
|
1511
|
+
}
|
|
1512
|
+
]
|
|
1513
|
+
},
|
|
1514
|
+
{
|
|
1515
|
+
"version": "0.15.14",
|
|
1516
|
+
"date": "2026-06-20",
|
|
1517
|
+
"headline": "A broad security-hardening release: every request-IP trust decision is peer-gated against the immediate connection (closing an X-Forwarded-For spoofing class — one behavior change, see Migration), a wide set of authentication and signature verifiers now fail closed, pre-auth parsers are hardened against algorithmic-complexity and frame-desync attacks, and untrusted-key allowlist lookups, at-rest encryption binding, and on-disk writes are tightened framework-wide; adds streaming and exclusive atomic-write primitives, a script-context JSON serializer, and HTTP-client bandwidth throttling",
|
|
1518
|
+
"summary": "This release tightens existing protections across the framework rather than adding opt-ins. The headline behavior change is how the client IP is trusted: the IP gates that make security decisions (network allowlist, rate limiter, break-glass grant pin) no longer trust a bare leftmost X-Forwarded-For value when trustProxy is set without an explicit proxy allowlist — they resolve the client IP by a peer-gated right-to-left walk anchored at the connecting socket, so a direct caller can no longer forge an allowed address. Operators who terminate behind a proxy now declare its address range in trustedProxies (see Migration). Alongside that, a large set of verifiers that could accept a malformed or unsigned credential now fail closed (SAML audience restriction, FDA electronic-signature, SD-JWT key-binding replay, DPoP, CIBA, mdoc trust anchor, COSE algorithm/curve binding, agent-snapshot plaintext under a regulated posture); pre-authentication parsers that an attacker reaches before any credential check are hardened against quadratic-time and frame-desync denial of service (CBOR, JSONPath, tar/PAX, gzip, YAML, ASN.1 DER, the scheduler's timer math); allowlist and posture lookups keyed by untrusted input can no longer reach Object.prototype; the framework's own file writes are now symlink-refusing and atomic; at-rest and egress paths bind ciphertext to its location and redact nested secrets; and several lost-update races are serialized. New surface is additive: streaming and exclusive atomic-write primitives, a serializer safe to embed in an inline <script>, and download/upload bandwidth throttling on the HTTP client.",
|
|
1519
|
+
"sections": [
|
|
1520
|
+
{
|
|
1521
|
+
"heading": "Migration",
|
|
1522
|
+
"items": [
|
|
1523
|
+
{
|
|
1524
|
+
"title": "X-Forwarded-For trust now requires an explicit proxy allowlist",
|
|
1525
|
+
"body": "The IP gates that enforce a security decision — network allowlist, rate limiter, and the break-glass grant IP pin — previously resolved the client IP from the leftmost X-Forwarded-For value whenever trustProxy was truthy. A direct caller could set that header to any address and walk an IP allowlist or evade a rate-limit key (CWE-290 / CWE-348). These gates now fail closed: with trustProxy set but no trustedProxies, they ignore X-Forwarded-For and use the connecting socket's peer address. To keep trusting a forwarded client IP, set trustedProxies to the CIDR(s) of your front proxy/load balancer — the client IP is then resolved by a peer-gated right-to-left walk that stops at the first hop not in the allowlist. For an allowlist / rate-limit / break-glass gate the new default is stricter (it trusts less), so an operator who has not migrated sees tighter enforcement, never looser. Display-only consumers (error pages, request logs) keep the lenient resolution and carry a docstring warning. The wiki example's WIKI_TRUST_PROXY flag is replaced by WIKI_ADMIN_TRUSTED_PROXIES (a CIDR list)."
|
|
1526
|
+
}
|
|
1527
|
+
]
|
|
1528
|
+
},
|
|
1529
|
+
{
|
|
1530
|
+
"heading": "Added",
|
|
1531
|
+
"items": [
|
|
1532
|
+
{
|
|
1533
|
+
"title": "b.atomicFile.writeStream(filepath, source, opts?) and b.atomicFile.writeExclSync(filepath, data, opts?)",
|
|
1534
|
+
"body": "Two atomic-write primitives that complete the read-side fdSafeReadSync family. writeStream writes a stream to a fresh O_EXCL|O_NOFOLLOW temporary file with an output-byte cap, then renames into place; writeExclSync stages a synchronous write through a verify-then-rename (clearing any stale temp under O_EXCL|O_NOFOLLOW first) for the small sealed-state round-trips (vault seal / unseal / rotate). Both refuse to follow a symlink at the destination or temp path, so an attacker who pre-creates a link can't redirect the write. The framework's own predictable-temp and symlink-following write sites now route through them; fdSafeReadSync also gains a withStat option that returns the bound fd's stat alongside the bytes."
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
"title": "b.safeJson.stringifyForScript(value, opts?)",
|
|
1538
|
+
"body": "A JSON serializer safe to embed directly in an inline <script> block: it escapes <, >, and & and the U+2028 / U+2029 line separators so a value containing </script> or a JS line terminator cannot break out of the script context (DOM-XSS). The import-map helper gains scriptTag(importMap, { nonce }) built on it, and the speculation-rules inline emitter uses it."
|
|
1539
|
+
},
|
|
1540
|
+
{
|
|
1541
|
+
"title": "HTTP-client bandwidth throttling",
|
|
1542
|
+
"body": "b.httpClient requests accept a bandwidth limit that interposes a throttling transform on both the download and upload paths, so a large transfer can be rate-shaped without buffering the whole body. Useful for bounding the egress a single outbound call can consume."
|
|
1543
|
+
},
|
|
1544
|
+
{
|
|
1545
|
+
"title": "Incremental audit-chain verification",
|
|
1546
|
+
"body": "The audit verifiers accept a from/to range so a long chain can be verified in increments rather than head-to-tail every time, report an accurate rowsVerified count, and keep the newest review when reconciling. b.vault also exposes getDefaultStore() for callers that need the process's default seal/unseal store handle."
|
|
1547
|
+
}
|
|
1548
|
+
]
|
|
1549
|
+
},
|
|
1550
|
+
{
|
|
1551
|
+
"heading": "Security",
|
|
1552
|
+
"items": [
|
|
1553
|
+
{
|
|
1554
|
+
"title": "Request-IP trust is peer-gated end to end",
|
|
1555
|
+
"body": "The client-IP resolution that every IP gate depends on is extracted into one trust model (a trustedProxies-aware, peer-gated right-to-left walk) so the network allowlist, rate limiter, break-glass pin, and bot-guard audit attribution all decide identity the same way. X-Forwarded-Proto is peer-gated across the security middleware on the same model, and a WebSocket reconnect to a swapped target is re-validated against the SSRF / blocked-host guard before the connection is reused. See Migration for the one behavior change."
|
|
1556
|
+
},
|
|
1557
|
+
{
|
|
1558
|
+
"title": "Authentication and signature verifiers fail closed",
|
|
1559
|
+
"body": "A set of verifiers that could accept a malformed or unsigned credential now refuse it: SAML verifyResponse fails closed on a missing AudienceRestriction or unparseable Conditions; the FDA 21 CFR 11 electronic-signature verify refuses a record with no signature (recordHash alone is self-consistency, not authentication); the SD-JWT key-binding JWT requires an audience and nonce (replay binding) and an expiry when configured; DPoP cross-checks the proof's alg against its embedded JWK key type before importing it; CIBA verifies the ID token on poll and notification; the mdoc trust anchor is enforced (no fail-open); COSE binds the signature algorithm to the key curve; and agent-snapshot refuses allowPlaintext under a regulated compliance posture and gates sealing on vault initialization."
|
|
1560
|
+
},
|
|
1561
|
+
{
|
|
1562
|
+
"title": "Pre-auth parsers hardened against complexity and frame-desync denial of service",
|
|
1563
|
+
"body": "Parsers an attacker reaches before any credential check are bounded. CBOR rejects the quadratic-time duplicate-map-key construction (it ran O(n^2) on every COSE / CWT / EAT / mdoc verify) and rejects non-minimal integer/key encodings in canonical mode; JSONPath caps the node-list cross-product that could exhaust memory and rejects the filter expression that could overflow the parser stack; the tar reader caps a random-access entry read and resynchronizes a PAX header whose size field is non-numeric; the gzip reader caps a random-access output size; the YAML scanner replaces a backtracking pattern with a linear scan; the ASN.1 DER reader caps high-tag-number and OID sub-identifier decoding and rejects non-minimal forms; and the scheduler clamps far-future timers so a 32-bit overflow can't make a timer fire immediately or an interval tight-loop."
|
|
1564
|
+
},
|
|
1565
|
+
{
|
|
1566
|
+
"title": "Allowlist and posture lookups keyed by untrusted input can't reach the prototype",
|
|
1567
|
+
"body": "Lookups of the form map[untrustedKey] across the framework's allowlist and compliance-posture tables are now own-property checks, so a key like __proto__ or constructor resolves as absent instead of returning an inherited value and walking the gate (CWE-1321). The shared posture-accessor that several guards built by hand is centralized, and an inverse detector flags any re-introduced unguarded indexed lookup."
|
|
1568
|
+
},
|
|
1569
|
+
{
|
|
1570
|
+
"title": "Framework file writes are symlink-refusing and atomic",
|
|
1571
|
+
"body": "The predictable-temp and symlink-following write sites (cookie jar, config-drift snapshot, archive extraction, local object store, backup bundle, and the vault passphrase seal/unseal/rotate) now write through the atomic-write primitives above, so a pre-planted symlink at the destination or temp path can no longer redirect a privileged write, and a partial write can't leave a torn file. A re-introduced raw write without the exclusive/no-follow flags is flagged by a detector."
|
|
1572
|
+
},
|
|
1573
|
+
{
|
|
1574
|
+
"title": "X.509 issuer chains enforce the CA basic constraint",
|
|
1575
|
+
"body": "Node's checkIssued() does not verify that the issuer certificate is actually a CA (CVE-2002-0862 class), so a leaf signed by a non-CA certificate could be accepted as an issuer. Certificate-chain walking is centralized in one helper that requires the issuer's basicConstraints cA bit and verifies the signature, and every chain consumer (TSA, BIMI, S/MIME, mdoc, content-credentials, the FIDO MDS3 anchor match) routes through it."
|
|
1576
|
+
},
|
|
1577
|
+
{
|
|
1578
|
+
"title": "Header, value, and markup injection closed",
|
|
1579
|
+
"body": "Mail rejects CRLF in Reply-To and in custom header keys/values (SMTP header injection); the GDPR data-export CSV writer neutralizes formula-injection prefixes; the content guards decode no-semicolon numeric HTML entities when revealing a dangerous URL scheme (so javascript: is caught); the early-hints emitter rejects CRLF in pushed link values; the HTML/SVG/BIMI comment scanners honor the WHATWG abrupt-comment-close forms (--!>, <!-->) so a mutation-XSS comment can't smuggle markup; and the CSP builder no longer emits a host source alongside 'none'."
|
|
1580
|
+
},
|
|
1581
|
+
{
|
|
1582
|
+
"title": "At-rest and egress data is bound to its context",
|
|
1583
|
+
"body": "Backup restore binds each encrypted file blob to its manifest path as additional authenticated data, so a blob remapped to another entry fails the AEAD tag on restore (signed and unsigned bundles alike), and threads the signature-verification options through extraction; the data-subject-request lookup dual-reads the keyed and legacy subject hashes so a subject is still found across the hashing change (GDPR Art. 17); the log redactor collapses a secret nested as an array or object value, not only as a scalar; and the OTLP exporter redacts the span name and status message, not just attributes, before they leave the process."
|
|
1584
|
+
},
|
|
1585
|
+
{
|
|
1586
|
+
"title": "Network-protocol verification hardened",
|
|
1587
|
+
"body": "OCSP responses are bound to the requested certificate serial; the WebSocket reader rejects reserved opcodes and reserved bits; the DNS parser refuses a record that reads past the message buffer; the NTP client rejects a reply whose origin timestamp does not echo the request (and the SNTP cookie path was reworked to bind the exchange); DANE TLSA matching is constant-time; and MTA-STS policy fetch fails closed rather than degrading to no-policy on an ambiguous response."
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
"title": "Byte caps are measured in bytes, and middleware enforcement is per-instance",
|
|
1591
|
+
"body": "Size limits that compared a byte budget against a string's character length (so multibyte input slipped a ~2-4x looser bound) now measure actual bytes — guard-json per-string cap, the per-tenant quota header budget, the JMAP request cap. The CSRF and fetch-metadata middleware track their gate per instance, so mounting a second, stricter instance still enforces (a shared request flag previously let the first, lenient mount disable it); the CSRF cookie is still issued once per response. The WebAssembly global is stripped from the sandbox worker so sandboxed code cannot JIT around the isolation."
|
|
1592
|
+
},
|
|
1593
|
+
{
|
|
1594
|
+
"title": "Lost-update races serialized",
|
|
1595
|
+
"body": "Concurrent failure recording in the login lockout and bot-challenge gates is serialized per key, so parallel failed attempts can no longer each read the same pre-write counter and stay under the lockout / challenge threshold. The inline webhook delivery is claimed before the POST so a concurrent retry pass can't double-send it, and a transient transport failure now backs off and retries instead of being dead-lettered."
|
|
1596
|
+
}
|
|
1597
|
+
]
|
|
1598
|
+
},
|
|
1599
|
+
{
|
|
1600
|
+
"heading": "Changed",
|
|
1601
|
+
"items": [
|
|
1602
|
+
{
|
|
1603
|
+
"title": "DORA major-incident reporting clock is four hours",
|
|
1604
|
+
"body": "The Digital Operational Resilience Act initial-notification deadline is corrected to four hours from classification (it had used the generic 24-hour default), incident classification weighs the affected-client-base percentage, and the deadline anchors to the prior filing where one exists."
|
|
1605
|
+
},
|
|
1606
|
+
{
|
|
1607
|
+
"title": "Range and conditional-request correctness",
|
|
1608
|
+
"body": "Compression skips a 206 Partial Content or any Content-Range response (RFC 7233 §4.1), so it can't drop Content-Length over a now-compressed byte interval; static-file conditional handling gives If-None-Match precedence over If-Modified-Since and If-Match over If-Unmodified-Since (RFC 7232); and the object store honors the [start, end] array range contract that previously fell through to a full read."
|
|
1609
|
+
},
|
|
1610
|
+
{
|
|
1611
|
+
"title": "Declared request bodies are always validated",
|
|
1612
|
+
"body": "A route that declares a body schema now validates it even when no body was parsed, so an omitted required body is rejected at the router instead of reaching the handler — mirroring the always-run params and query checks."
|
|
1613
|
+
}
|
|
1614
|
+
]
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
"heading": "Fixed",
|
|
1618
|
+
"items": [
|
|
1619
|
+
{
|
|
1620
|
+
"title": "Object store getStream works on remote backends",
|
|
1621
|
+
"body": "getStream() returned a stream wrapping an unresolved promise on every remote backend, so it never delivered bytes; it now resolves the object before streaming."
|
|
1622
|
+
},
|
|
1623
|
+
{
|
|
1624
|
+
"title": "Queue and body-parser correctness",
|
|
1625
|
+
"body": "The local queue's fail()/complete() paths guard on the current row status so a stale lease can't resurrect or double-process a job, and the body parser's raw() mode no longer matches every content type by default (it had answered 415 to nothing or everything depending on the wildcard)."
|
|
1626
|
+
},
|
|
1627
|
+
{
|
|
1628
|
+
"title": "Sanctions screening and GraphQL SDL probing no longer under-match",
|
|
1629
|
+
"body": "Sanctions screening no longer skips a candidate whose type doesn't match the query's type filter (a sanctioned entity of an unspecified type was missed) and reports a typeMatch flag; the GraphQL federation guard probes for the _service / _entities introspection fields by word boundary (an aliased field bypassed the prefix check) and over batched-array request bodies. The per-tenant query budget is a true sliding window, so a burst straddling the window boundary can't briefly double the rate."
|
|
1630
|
+
}
|
|
1631
|
+
]
|
|
1632
|
+
}
|
|
1633
|
+
]
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
"version": "0.15.13",
|
|
1637
|
+
"date": "2026-06-19",
|
|
1638
|
+
"headline": "Consolidates a large set of duplicated, security-sensitive code paths — injected-dependency validation, positive-integer option caps, TOCTOU-safe file reads, markup escaping, audit emission, structured-field parsing, and untrusted-JWK import — onto shared hardened primitives, so a fix or guard now applies everywhere at once; adds a TOCTOU-safe file reader; and makes several configuration errors throw typed framework errors instead of a bare Error",
|
|
1639
|
+
"summary": "Across the framework, logic that had been hand-rolled in many places (and had quietly drifted between copies) is now single-sourced through shared primitives. The practical effect for operators is consistency: the strongest available guard is applied everywhere a given operation happens, rather than in whichever copy happened to have it. Injected dependencies (stores, backends, vaults, db handles, query objects) are validated through one contract; operator-tunable positive-integer options (pool sizes, byte caps, timeouts, stream limits) validate through one bounds primitive that forwards the caller's non-retryable / HTTP-status flags; the open-fd → fstat → read-fully pattern that several call sites used to defend file reads against a swap-after-stat race (CWE-367) is now the b.atomicFile.fdSafeReadSync primitive, with symlink refusal, inode-equality, a byte cap, and an integrity hash available as options and each caller's exact typed error preserved; and the XML/HTML markup escapers (including the b.guardHtml / b.guardSvg sanitizer output) route through one b.markupEscape so the base & < > \" chain can't drift between them. A few configuration-validation paths that threw a bare Error (httpClient.configurePool, b.db stream-limit validation) now throw the module's typed framework error. No security defaults change; this release tightens and unifies existing protections rather than adding opt-ins.",
|
|
1640
|
+
"sections": [
|
|
1641
|
+
{
|
|
1642
|
+
"heading": "Added",
|
|
1643
|
+
"items": [
|
|
1644
|
+
{
|
|
1645
|
+
"title": "b.atomicFile.fdSafeReadSync(filepath, opts?)",
|
|
1646
|
+
"body": "A TOCTOU-safe synchronous file read (CWE-367 / js/file-system-race): it opens the path read-only, then binds the size, content, and integrity measurement to the inode the fd holds open, so a swap between stat and read can't change which bytes are returned. Optional guards layer on top — maxBytes (refuse an over-cap file), refuseSymlink + inodeCheck (refuse a symlink source and any inode swap, the strongest posture for operator-writable paths), expectedHash (SHA3-512), encoding (return a string), allowShortRead — and an errorFor(kind, detail) callback lets each caller keep its own typed error. The framework's own file reads (atomic-file, the TLS certificate-path loader, the sealed-PEM source reader, the backup bundle reader) all route through it."
|
|
1647
|
+
},
|
|
1648
|
+
{
|
|
1649
|
+
"title": "Shared substrate primitives for previously-duplicated logic",
|
|
1650
|
+
"body": "A set of shared primitives now back call sites that had each re-implemented the same logic: validateOpts.requireMethods (gains a `permanent` flag) for injected-dependency contract checks; numericBounds positive-finite-int validators (gain an errorOpts { permanent, statusCode }) for tunable integer options; b.markupEscape for the & < > \" markup chain; structured-field parsing helpers (parseTagList, forEachKeyValue, splitUnquoted, stripDoubleQuotes, unfoldHeaderContinuations); crypto.importPublicJwk and crypto.makeBase64UrlDecoder; safeSql.toPositional; safeBuffer.makeByteCoercer; nonceStore.enforceReplay; and the audit/observability namespaced emitters. These are mostly invisible substrate — the operator-visible benefit is that a guard or fix added to one of them now covers every caller."
|
|
1651
|
+
}
|
|
1652
|
+
]
|
|
1653
|
+
},
|
|
1654
|
+
{
|
|
1655
|
+
"heading": "Changed",
|
|
1656
|
+
"items": [
|
|
1657
|
+
{
|
|
1658
|
+
"title": "Configuration errors throw typed framework errors",
|
|
1659
|
+
"body": "Two configuration-validation paths that threw a bare `Error` now throw the module's typed framework error: httpClient.configurePool (bad maxSockets / maxFreeSockets / keepAliveMsecs → HttpClientError, code httpclient/bad-opts) and the b.db stream-limit validation (Query.stream → DbQueryError). The thrown error remains an Error subclass, so existing `instanceof Error` / try-catch handling is unaffected; code that matched the old free-text message should match the typed error's `code` instead. Several routed validators also report a slightly more precise message (\"positive finite integer\" instead of \"positive integer\")."
|
|
1660
|
+
}
|
|
1661
|
+
]
|
|
1662
|
+
},
|
|
1663
|
+
{
|
|
1664
|
+
"heading": "Security",
|
|
1665
|
+
"items": [
|
|
1666
|
+
{
|
|
1667
|
+
"title": "TOCTOU-safe file reads applied uniformly",
|
|
1668
|
+
"body": "The open-fd → fstat → read pattern that anchors a file read to one inode against a swap-after-stat race is now centralized in b.atomicFile.fdSafeReadSync, and every framework file read routes through it. The sealed-PEM source reader keeps the strongest posture (symlink refusal + inode-equality + byte cap), and those guards are now available as options to any caller — a re-introduced hand-rolled read-loop is flagged by a codebase-pattern detector."
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
"title": "Injected-dependency and integer-option validation can't silently drift",
|
|
1672
|
+
"body": "Injected stores / backends / vaults / db handles / query objects are validated through one contract (validateOpts.requireMethods), and operator-tunable positive-integer options through one bounds primitive (numericBounds), both forwarding the caller's non-retryable / HTTP-status error flags so a config error stays permanent / carries its 4xx. Inverse detectors flag any re-introduced inline check, so the validation can no longer be partially copied or weakened in one place."
|
|
1673
|
+
},
|
|
1674
|
+
{
|
|
1675
|
+
"title": "Single markup-escape chain for the HTML/SVG sanitizers",
|
|
1676
|
+
"body": "The b.guardHtml and b.guardSvg sanitizer output escapers, the AI-Act transparency and mail HTML escapers, and the XML report escapers now share one b.markupEscape for the base & < > \" chain (apostrophe form as a parameter; each caller keeps its own input coercion and any extra escapes such as guardHtml.escapeAttr's backtick / = IE-attribute-injection hardening). Byte-for-byte parity with the prior escapers was verified across an XSS-vector corpus, so the consolidation cannot have introduced an escaping gap; a re-inlined & < > \" chain anywhere else is flagged by a detector."
|
|
1677
|
+
},
|
|
1678
|
+
{
|
|
1679
|
+
"title": "One hardening point for untrusted public-JWK import",
|
|
1680
|
+
"body": "The createPublicKey-from-JWK call that imports attacker-controlled key material (DID publicKeyJwk, DNSKEY, COSE_Key, DPoP / OAuth / OIDC / DBSC proof JWKs) is centralized in b.crypto.importPublicJwk, so the untrusted-key import has a single audited home; each caller keeps its own kty/crv pre-validation and typed error."
|
|
1681
|
+
},
|
|
1682
|
+
{
|
|
1683
|
+
"title": "Linear-time Content-Security-Policy header parsing",
|
|
1684
|
+
"body": "The CSP response-header parser split directives on a `/\\s*,\\s*/` regex that ran in O(n^2) on a long comma-less run of whitespace, letting a crafted header stall a worker (js/polynomial-redos). It now splits on the literal comma and trims each item, which is linear and byte-for-byte equivalent."
|
|
1685
|
+
},
|
|
1686
|
+
{
|
|
1687
|
+
"title": "Parsed INI and OpenAPI path maps can't reach Object.prototype",
|
|
1688
|
+
"body": "The INI parser already refuses a `__proto__` / `constructor` / `prototype` section or key (it throws before any write); as defense-in-depth, every parsed node — and the OpenAPI paths map keyed by URL pattern — is now a null-prototype object, so even a future gap could only ever set an own property, never mutate Object.prototype (CWE-1321)."
|
|
1689
|
+
}
|
|
1690
|
+
]
|
|
1691
|
+
},
|
|
1692
|
+
{
|
|
1693
|
+
"heading": "Fixed",
|
|
1694
|
+
"items": [
|
|
1695
|
+
{
|
|
1696
|
+
"title": "Webhook deliveries retry transient transport failures instead of dead-lettering them",
|
|
1697
|
+
"body": "A delivery that failed with a transient transport error (connection reset, timeout, 5xx) was recorded as permanently failed and dead-lettered rather than retried. The destination-safety check (SSRF / blocked-host guard) now runs in its own step and is the only permanent failure; a transport failure backs off and retries up to the configured limit."
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
"title": "Webhook deliveries table creates on MySQL",
|
|
1701
|
+
"body": "The pending-delivery index used a partial-index (WHERE) clause that MySQL does not support, so the deliveries table failed to create on a MySQL backend. The index is now dialect-aware — partial on PostgreSQL / SQLite, plain on MySQL."
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
"title": "Inline webhook delivery can't be double-sent by concurrent retry processing",
|
|
1705
|
+
"body": "An inline delivery is now claimed (status set to in-flight with a claim timestamp) before the POST, so a concurrent retry pass can't pick up and re-send the same delivery; a row left in-flight by a crash is reclaimed after the stale-claim window."
|
|
1706
|
+
},
|
|
1707
|
+
{
|
|
1708
|
+
"title": "Audit checkpoint can never be anchored against a replaced database",
|
|
1709
|
+
"body": "close() fires a best-effort final checkpoint as the database shuts down. In a narrow close-then-reopen window that checkpoint's write could resume after a fresh database had opened and anchor the prior database's chain tip into it. checkpoint() now binds to the database it read the tip from and refuses to write (returns null, fail-closed) if that handle has since been closed or replaced — so a checkpoint is only ever written against the database it measured."
|
|
1710
|
+
}
|
|
1711
|
+
]
|
|
1712
|
+
}
|
|
1713
|
+
]
|
|
1714
|
+
},
|
|
1715
|
+
{
|
|
1716
|
+
"version": "0.15.12",
|
|
1717
|
+
"date": "2026-06-14",
|
|
1718
|
+
"headline": "Hardens a set of defense-in-depth seams: a single-pass structured-field unescape, a constant-time content-digest member match, complete reserved-character stripping, a Trojan-Source escape on the boot logger, generic body-parse error responses, and an audit trail whenever outbound TLS certificate validation is disabled",
|
|
1719
|
+
"summary": "A sweep of low-severity but real hardening items. RFC 8941 structured-field string values (HTTP Message Signatures, Client Hints, Cache-Control) were un-escaped with two chained replaces that mis-decoded an escaped backslash adjacent to another escape; they now use one left-to-right pass that decodes each escape exactly once. The HTTP Message Signature content-digest check dropped a dead identity-replace and now matches the sha3-512 member by an exact, top-level, constant-time comparison instead of an unanchored substring scan. b.guardFilename's reserved-character strip used a non-global regex that left every separator after the first; it now strips all of them. The boot logger's TTY branch wrote raw text, bypassing the Trojan-Source / control-character escape the main logger applies — it now escapes the bidi and C0/newline control classes on every sink. The body parser no longer echoes a caught exception's detail (an fs errno + temp path, or a parse hook's thrown message) to the HTTP client — the client gets a generic status phrase while the full detail stays on the audit chain. And any outbound TLS connection that runs with peer-certificate validation disabled (an explicit operator opt-in, never a default) now emits a tls.insecure_skip_verify audit + observability event so the degraded posture is visible for compliance and incident response.",
|
|
1720
|
+
"sections": [
|
|
1721
|
+
{
|
|
1722
|
+
"heading": "Added",
|
|
1723
|
+
"items": [
|
|
1724
|
+
{
|
|
1725
|
+
"title": "b.structuredFields.unescapeSfStringBody(body)",
|
|
1726
|
+
"body": "A single-pass decode of the RFC 8941 §3.3.3 quoted-string backslash escapes (the bytes between the surrounding quotes). It replaces the chained two-`.replace()` form, which is not equivalent to one decode — whichever pass runs first can rewrite a backslash the other escape sequence owns, so a lone escaped backslash decoded to two. The HTTP Message Signature, Client Hints, and Cache-Control sf-string readers now route through it."
|
|
1727
|
+
},
|
|
1728
|
+
{
|
|
1729
|
+
"title": "tls.insecure_skip_verify audit event",
|
|
1730
|
+
"body": "b.network.tls.auditInsecureTls(meta) emits an audit + observability event at the point an outbound TLS connection honors rejectUnauthorized:false / allowInsecure. The connectWithEch, OTLP-gRPC log stream, syslog-TLS log stream, and SMTP transports all emit it when an operator disables certificate validation — parallel to the existing tls.classical_downgrade audit. No default changes; the framework never disables validation itself."
|
|
1731
|
+
}
|
|
1732
|
+
]
|
|
1733
|
+
},
|
|
1734
|
+
{
|
|
1735
|
+
"heading": "Security",
|
|
1736
|
+
"items": [
|
|
1737
|
+
{
|
|
1738
|
+
"title": "Single-pass structured-field string unescape",
|
|
1739
|
+
"body": "The RFC 8941 sf-string readers in HTTP Message Signatures (Signature-Input covered-component names), Client Hints, and Cache-Control directive values un-escaped with `.replace(/\\\\\\\\/g,\"\\\\\").replace(/\\\\\"/g,'\"')` — two sequential passes that mis-decode adjacent escapes (a lone escaped backslash became two). All four sites now use the single-pass b.structuredFields.unescapeSfStringBody. It is fail-closed (a mis-decoded covered-component name just fails the signature check, never bypasses it); the fix restores RFC-conformant interop with peers that legitimately escape these values."
|
|
1740
|
+
},
|
|
1741
|
+
{
|
|
1742
|
+
"title": "Constant-time, member-anchored content-digest verification",
|
|
1743
|
+
"body": "b.crypto.httpSig.verify's covered content-digest check dropped a dead no-op replace and now parses the Content-Digest header into its top-level members and matches the sha3-512 member EXACTLY, in constant time (b.crypto.timingSafeEqual), rather than scanning for the digest text as a substring anywhere in the header. The Content-Digest header is already bound by the signature, so the substring form was not reachably exploitable; the change removes the latent ambiguity and the timing channel."
|
|
1744
|
+
},
|
|
1745
|
+
{
|
|
1746
|
+
"title": "Reserved-character filename strip removes every occurrence",
|
|
1747
|
+
"body": "b.guardFilename's reservedCharPolicy:\"strip\" (the permissive profile) used a non-global regex, so only the FIRST reserved character — including path separators — was replaced and the rest leaked through. The strip is now global: every reserved character is removed. Not a traversal bypass (the unconditional security floor still throws on `..`, null bytes, NTFS ADS, UNC, overlong UTF-8), but the strip is now complete and consistent."
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
"title": "Boot logger escapes control + bidi characters on every sink",
|
|
1751
|
+
"body": "The boot-time logger's TTY branch wrote the raw message, bypassing the Trojan-Source (bidi) and control-character escapes the main logger applies — a hostile message could forge extra log lines on a terminal (CWE-117) or re-order the visible line (CVE-2021-42574). Both boot branches now escape the bidi and C0/newline control classes, matching the create() path and the logger's advertised guarantee."
|
|
1752
|
+
},
|
|
1753
|
+
{
|
|
1754
|
+
"title": "Body-parser error responses never echo internal detail",
|
|
1755
|
+
"body": "The body-parser's terminal error path surfaced a caught exception's message verbatim to the HTTP client — a multipart filesystem error leaked the errno + temp path, and a parse hook's thrown error (which can carry secrets) was echoed back. The client now gets a curated message only for a framework-classified 4xx error and a generic status phrase otherwise; the parse-hook wrapper carries a fixed message, and full diagnostics stay on the audit chain server-side (CWE-209). The cluster leader-discovery endpoint's error body is generalized the same way."
|
|
1756
|
+
}
|
|
1757
|
+
]
|
|
1758
|
+
}
|
|
1759
|
+
]
|
|
1760
|
+
},
|
|
1761
|
+
{
|
|
1762
|
+
"version": "0.15.11",
|
|
1763
|
+
"date": "2026-06-14",
|
|
1764
|
+
"headline": "Replaces a family of quadratic-time regexes that hostile input could use to stall a worker with linear scans, refuses a relocatable sealed-cell downgrade on the read side, fails closed when enabling row-level security behind a non-native driver, and scans the vendored crypto for known CVEs on every build",
|
|
1765
|
+
"summary": "Several text-handling primitives stripped trailing whitespace or extracted a mail address with a regex whose backtracking is quadratic in the input length on adversarial strings — a request body, a YAML document, a CSV cell, or a From header crafted as a long run of spaces could pin a worker's CPU. Each is now a linear character scan with identical output. The HTML-content check the MCP tool surface applies gained the vbscript: and data:text/html vectors it was missing. On the data-at-rest side, an AAD-bound (or per-row-key) column now refuses a plain, unbound vault cell on read — a relocatable envelope an attacker with write access could copy in from another row defeats the cross-row binding, so the field is nulled rather than surfaced; operators mid-migration opt back in with registerTable({ allowPlainMigration: true }). declareRowPolicy now treats row-level-security as enabled only on a value that unambiguously means true, so a non-native Postgres driver that returns the string \"f\" can no longer be read as \"already on\" and silently skip the ENABLE that protects the table's rows. Finally, because the framework's crypto is vendored rather than installed, npm audit and Dependabot never see it: every build now matches the vendored versions against the OSV vulnerability database, with a complementary Semgrep pass and workflow-file static analysis alongside.",
|
|
1766
|
+
"sections": [
|
|
1767
|
+
{
|
|
1768
|
+
"heading": "Added",
|
|
1769
|
+
"items": [
|
|
1770
|
+
{
|
|
1771
|
+
"title": "b.safeBuffer.indexAfterOpenTag(html, tagName)",
|
|
1772
|
+
"body": "A linear helper that returns the offset just past a `<tag ...>` opening tag (case-insensitive), or -1 when absent or unterminated — the insertion point a response rewriter uses to splice content after <body> or <head> without a regex. It replaces the O(n^2) html.match(/<body[^>]*>/i) shape and is stricter than it: a real tag boundary is required after the name, so <bodyfoo> is not mistaken for <body>."
|
|
1773
|
+
}
|
|
1774
|
+
]
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
"heading": "Security",
|
|
1778
|
+
"items": [
|
|
1779
|
+
{
|
|
1780
|
+
"title": "Linear-time replacements across a family of quadratic regexes (ReDoS class)",
|
|
1781
|
+
"body": "Several primitives located or stripped text with a regex whose backtracking is quadratic in V8 on adversarial input (CWE-1333): b.safeBuffer and the safe-env / safe-yaml / guard-csv parsers stripped trailing horizontal whitespace with /[ \\t]+$/; b.mail extracted the address from a `Name <addr>` header with /<([^>]+)>/; the bot-disclosure and speculation-rules response middleware found the <body> insertion point with /<body[^>]*>/i; and the BIMI certificate-chain splitter walked PEM blocks with a lazy /BEGIN[\\s\\S]*?END/ scan. A crafted field — a long run of spaces, an unterminated bracket, a body carrying many <body starts with no closing >, a chain of BEGIN markers — could drive a worker's CPU to seconds of work. Each is now a linear scan: a shared b.safeBuffer.stripTrailingHspace (backward char walk), b.safeBuffer.indexAfterOpenTag (forward indexOf walk for the tag insertion point), a forward indexOf for address extraction, and an indexOf walk for the PEM split. Output is byte-identical (the tag-find is stricter — it no longer mistakes <bodyfoo> for <body>), and 400K-character adversarial inputs that took 8–85 seconds now complete in under 2 ms."
|
|
1782
|
+
},
|
|
1783
|
+
{
|
|
1784
|
+
"title": "MCP HTML-content check covers vbscript: and data:text/html",
|
|
1785
|
+
"body": "The dangerous-markup check applied to text/html tool content matched <script>/<iframe>/<object>/<embed> and javascript: URLs but not the vbscript: scheme or data:text/html payloads. Both are now refused; data: URLs carrying non-HTML media (data:image/png and similar) are unaffected."
|
|
1786
|
+
},
|
|
1787
|
+
{
|
|
1788
|
+
"title": "AAD-bound columns refuse a plain sealed cell on read",
|
|
1789
|
+
"body": "b.cryptoField.unsealRow now refuses a plain, unbound vault: envelope found on an AAD-bound (or per-row-key) column and nulls the field instead of returning it. A plain envelope carries no per-cell binding, so a writer who could place one — copied from anywhere under the same vault root — would otherwise relocate a value across rows or columns and defeat the copy-protection the AAD binding advertises. Operators migrating pre-AAD rows up to bound ciphertext opt into a bounded acceptance window with registerTable({ allowPlainMigration: true }) and clear it when migration completes."
|
|
1790
|
+
},
|
|
1791
|
+
{
|
|
1792
|
+
"title": "Row-level-security enablement fails closed on non-native drivers",
|
|
1793
|
+
"body": "b.db.declareRowPolicy read pg_class.relrowsecurity to skip a redundant ENABLE ROW LEVEL SECURITY, but tested it with a bare truthiness check. A native pg driver returns a JS boolean; a proxy or ORM may return the string \"f\" for a disabled table — and \"f\" is truthy, so the check read it as already-enabled and silently skipped the ENABLE, leaving every row in the table unprotected while the migration reported success. RLS now counts as enabled only on a value that unambiguously means true (true, 1, or \"t\"/\"true\"/\"1\"/\"on\"/\"yes\"); every other shape re-issues ENABLE, a harmless no-op on an already-enabled table."
|
|
1794
|
+
},
|
|
1795
|
+
{
|
|
1796
|
+
"title": "Vendored-crypto CVE scanning, complementary SAST, and workflow static analysis in CI",
|
|
1797
|
+
"body": "The framework ships zero npm runtime dependencies — its crypto (the noble suite, the WebAuthn server, the PKI layer) is vendored under lib/vendor/, where npm audit, Dependabot, and Socket cannot see it. Every build now generates a CycloneDX SBOM of the vendored tree (each library carrying an npm purl) and runs it through OSV-Scanner, matching the exact pinned version against the OSV vulnerability database; a published CVE or GHSA affecting a vendored version fails the build so the copy is refreshed before merge. A Semgrep pass (registry security-audit + javascript packs at ERROR severity) runs alongside CodeQL as complementary SAST, and actionlint statically checks the workflow files. All three install the OSS tool from its upstream release, matching the existing secret-scan gate's posture."
|
|
1798
|
+
}
|
|
1799
|
+
]
|
|
1800
|
+
},
|
|
1801
|
+
{
|
|
1802
|
+
"heading": "Detectors",
|
|
1803
|
+
"items": [
|
|
1804
|
+
{
|
|
1805
|
+
"title": "Quadratic trailing-whitespace and tag-find regex detectors",
|
|
1806
|
+
"body": "Two codebase-pattern detectors refuse reintroduction of the quadratic shapes: the /[ \\t]+$/ trailing-whitespace strip (as .replace, .test, or via the named TRAILING_HSPACE_RE export) outside the linear helper that owns it, and the str.match(/<tag[^>]*>/) document-tag find that the response middleware must route through b.safeBuffer.indexAfterOpenTag. Each is proven to fire on the removed shape and stay silent on the linear replacement, so the ReDoS class cannot creep back into a new parser, guard, or response rewriter."
|
|
1807
|
+
}
|
|
1808
|
+
]
|
|
1809
|
+
}
|
|
1810
|
+
]
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
"version": "0.15.10",
|
|
1814
|
+
"date": "2026-06-13",
|
|
1815
|
+
"headline": "Makes S3 Object-Lock version erasure reachable through the object store, and pins the build toolchain's native binary to a reviewed hash",
|
|
1816
|
+
"summary": "The object store gains the versioned-delete surface its S3 Object Lock support always needed for real erasure. An unversioned delete on a versioning-enabled (Object-Lock) bucket only writes a delete-marker — the data version survives — so the framework's own delete could report success while a record protected for compliance, or one a data subject asked to erase, stayed on disk. b.objectStore / b.storage now carry a versionId: put and saveRaw return the version they created, deleteFile(key, { versionId, bypassGovernanceRetention }) targets a specific version (refused — not silently delete-markered — when it is under an active retention), and listVersions enumerates versions and delete-markers so an erasure workflow can find them. Backends with no version surface (the filesystem backend, and the current Azure and GCS adapters) refuse a versioned delete loudly rather than silently dropping the current object. Separately, the build toolchain's native bundler binary is now verified against a reviewed SHA-256 pin so a tampered or drifted binary is caught before it bundles the framework.",
|
|
1817
|
+
"sections": [
|
|
1818
|
+
{
|
|
1819
|
+
"heading": "Added",
|
|
1820
|
+
"items": [
|
|
1821
|
+
{
|
|
1822
|
+
"title": "Versioned object delete + listVersions for S3 Object-Lock erasure",
|
|
1823
|
+
"body": "b.storage.deleteFile and the b.objectStore sigv4 backend now accept opts.versionId to erase a specific object version, and opts.bypassGovernanceRetention to lift a GOVERNANCE-mode retention for callers with the permission (COMPLIANCE stays immutable to everyone). b.storage.saveRaw and the backend put now return the versionId they created on a versioning-enabled bucket, and a new b.storage.listVersions(prefix) / backend listVersions enumerates every version and delete-marker (key, versionId, isLatest, deleteMarker, size, lastModified, etag) so a right-to-erasure or crypto-shred workflow can target prior versions. On a backend with no version surface, listVersions throws VERSIONS_UNSUPPORTED and a versioned delete throws VERSIONID_UNSUPPORTED rather than silently acting on the current object."
|
|
1824
|
+
},
|
|
1825
|
+
{
|
|
1826
|
+
"title": "b.localDb.thin reaches SQLite resource-limit parity (limits option)",
|
|
1827
|
+
"body": "b.localDb.thin now opens its node:sqlite handle with the same parse-time statement-size cap as b.db and the CLI — a SQL statement over 1 MiB is rejected at parse time, the SQLITE_LIMIT_LENGTH floor that guards prepare()/exec() of raw SQL against an attacker-influenced megaquery (SQLite's default is 1 GB). The cap is on by default; a new limits option (e.g. { sqlLength: 2 * 1024 * 1024 } or other SQLITE_LIMIT_* keys) lets an operator raise or extend it. Previously the thin opener had no limits plumbing, so a consumer on that path could not reach parity with the rest of the framework's SQLite surface."
|
|
1828
|
+
}
|
|
1829
|
+
]
|
|
1830
|
+
},
|
|
1831
|
+
{
|
|
1832
|
+
"heading": "Fixed",
|
|
1833
|
+
"items": [
|
|
1834
|
+
{
|
|
1835
|
+
"title": "S3 Object-Lock version erasure is reachable through the framework delete path",
|
|
1836
|
+
"body": "On a versioning-enabled (Object-Lock) bucket, an unversioned DELETE only writes a delete-marker — the protected data version survives untouched — yet the framework's delete had no versionId surface, so it issued the unversioned form and reported success while the bytes the lock protects remained. A retention or legal hold could therefore look enforced to the framework caller while the operation WORM actually blocks was unreachable. The delete path now targets the exact version: deleting a version under a COMPLIANCE retention is refused (it throws, even with bypassGovernanceRetention), a no-retention version erases cleanly, and the enforcement is proven end-to-end against MinIO through the framework's own API."
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
"title": "b.configDrift.verifyVendorIntegrity is now working-directory-independent",
|
|
1840
|
+
"body": "The vendored-dependency integrity check resolved each manifest file path against process.cwd(), so it only worked when run from the application root. Run from anywhere else it read-failed every entry (reporting ok:false), and under a crafted working directory that happened to contain a clean vendor tree it could hash a different tree than the one actually loaded. It now resolves each file under the framework's own vendor directory by default — the tree loaded at runtime — and honors an explicit libVendorDir for verifying a deployed tree elsewhere, so the result no longer depends on where the process was started."
|
|
1841
|
+
}
|
|
1842
|
+
]
|
|
1843
|
+
},
|
|
1844
|
+
{
|
|
1845
|
+
"heading": "Security",
|
|
1846
|
+
"items": [
|
|
1847
|
+
{
|
|
1848
|
+
"title": "Build toolchain native binary pinned to a reviewed hash",
|
|
1849
|
+
"body": "The native bundler binary the build toolchain runs (esbuild's per-platform compiler, a development dependency that never ships in the runtime) is now verified against a SHA-256 pin captured by diffing the published package tarballs and hashing the binary. The build gate fails if the on-disk binary does not match the reviewed hash for its (version, platform); for a version that has not been reviewed it notes the gap and skips rather than trusting an unverified binary. A cross-artifact check keeps the version in agreement across package.json, the CI install step, and the hash map, so the gate can never quietly test a version that was never diffed — closing a real drift where CI had been installing an older patch than package.json declared. The reviewed diff is benign: version strings plus an installer size-bound and error-message hardening, no new install hooks, files, or network paths, and no runtime-dependency impact."
|
|
1850
|
+
}
|
|
1851
|
+
]
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
"heading": "Detectors",
|
|
1855
|
+
"items": [
|
|
1856
|
+
{
|
|
1857
|
+
"title": "Object-store erasure guard, esbuild-pin agreement, + structural re-anchoring of the lint detectors",
|
|
1858
|
+
"body": "A new guard locks the object-store delete path to the versioned-erasure contract: b.storage.deleteFile must thread versionId to the backend, so it can never silently revert to the WORM-blind unversioned delete. A second guard enforces that the esbuild build-tool version agrees across package.json, the CI install step, and the binary-hash map, so a future bump can't update one and leave the gate testing an unreviewed version. Separately, the framework's internal codebase-pattern lint detectors were re-anchored from fixed character spans to structural code boundaries, so they keep matching the code they guard as those functions grow rather than aging out of range; reviving them surfaced a few internal validation and transaction sites that now route through shared helpers (a required positive-integer-with-range validator and an async transaction wrapper) instead of hand-rolling the check. No public API change."
|
|
1859
|
+
}
|
|
1860
|
+
]
|
|
1861
|
+
}
|
|
1862
|
+
]
|
|
1863
|
+
},
|
|
1864
|
+
{
|
|
1865
|
+
"version": "0.15.9",
|
|
1866
|
+
"date": "2026-06-13",
|
|
1867
|
+
"headline": "Raises the Node floor to 24.16, adds SQLite parse-time resource caps, retries Windows rename locks on every atomic write and download, and ships a one-call secure logout that wipes client-side state",
|
|
1868
|
+
"summary": "This release moves the engines floor to the current Node 24 LTS patch level and adds three hardening primitives. node:sqlite handles now construct with SQLITE_LIMIT_* caps: a statement over 1 MiB is rejected at parse time (a DoS floor on the raw-SQL surface, complementary to the existing row-count gate) and ATTACH DATABASE is denied. Every final temp-to-destination rename — the file written by an atomic write, a downloaded file, a sealed vault key, a rotated log, an extracted archive entry — now routes through a single retry that rides out a transient Windows lock (antivirus, the search indexer, or a file-sync client briefly holding the destination), instead of surfacing the lock as a hard failure; the retry, previously hand-rolled and unreachable, is now the reusable b.atomicFile.renameWithRetry. And b.session.logout destroys a session and tells the browser to wipe its client-side state in one call: it emits an RFC 9527 Clear-Site-Data header and expires the session cookie before destroying the row, the secure-default logout that previously had to be assembled by hand.",
|
|
1869
|
+
"sections": [
|
|
1870
|
+
{
|
|
1871
|
+
"heading": "Changed",
|
|
1872
|
+
"items": [
|
|
1873
|
+
{
|
|
1874
|
+
"title": "Node engines floor raised to >=24.16.0",
|
|
1875
|
+
"body": "The minimum supported Node is now 24.16.0 (the current Node 24 LTS patch level), up from 24.14.1. This is an LTS-currency bump — there are no Node CVE fixes between 24.14.1 and 24.16.0 (24.14.1 already carried the CVE-2026-21713 HMAC fix); it keeps the framework on the latest patched LTS and makes the node:sqlite resource-cap hardening below available everywhere. Pre-1.0, operators upgrade across the floor; Node 26 continues to satisfy it."
|
|
1876
|
+
}
|
|
1877
|
+
]
|
|
1878
|
+
},
|
|
1879
|
+
{
|
|
1880
|
+
"heading": "Security",
|
|
1881
|
+
"items": [
|
|
1882
|
+
{
|
|
1883
|
+
"title": "SQLite parse-time statement-size cap",
|
|
1884
|
+
"body": "Every node:sqlite database the framework opens — the main db handle and the CLI's handle — now constructs with a SQLITE_LIMIT_LENGTH cap: a SQL statement over 1 MiB is rejected at parse time. Because the query builder parameterizes every value, the size cap guards the raw-SQL surface (`b.db.runSql`) against an attacker-influenced megaquery the parser would otherwise process (SQLite's default is 1 GB); it is a parse-time DoS floor complementary to the existing row-count gate. Legitimate framework and operator statements are far under the cap."
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
"title": "Windows rename-lock retry on every atomic rename and download",
|
|
1888
|
+
"body": "On Windows a freshly-written file's destination is briefly held by antivirus, the search indexer, or a file-sync client (Dropbox, OneDrive), surfacing as a transient EPERM / EACCES / EBUSY on rename even though the temp file is fine. `b.atomicFile.writeSync` already retried this, but `b.httpClient.downloadStream` did not — a download into a cloud-synced or AV-scanned directory could fail hard on the lock. The retry is now the reusable `b.atomicFile.renameWithRetry`, and every final temp-to-destination rename in the framework routes through it: downloads, sealed vault keys, CA key/cert writes, log rotation, archive extraction, config-drift sidecars, the self-update binary swap, and restore/rollback moves. A non-transient error still throws immediately; POSIX renames are unaffected."
|
|
1889
|
+
}
|
|
1890
|
+
]
|
|
1891
|
+
},
|
|
1892
|
+
{
|
|
1893
|
+
"heading": "Added",
|
|
1894
|
+
"items": [
|
|
1895
|
+
{
|
|
1896
|
+
"title": "b.session.logout — one-call secure logout",
|
|
1897
|
+
"body": "`b.session.logout(res, token, opts?)` destroys the server-side session AND tells the browser to wipe its client-side state in one call: it emits an RFC 9527 Clear-Site-Data response header (cookies + storage + cache + execution contexts by default) and expires the session cookie, then destroys the session row. `b.session.destroy` alone is a store operation with no response object, so it could not wipe the browser's cached pages, storage, or a stale tab still holding the now-revoked cookie — that wiring previously had to be mounted by hand. Pass `cookieName` to match a non-default cookie and `types` to choose the Clear-Site-Data directives."
|
|
1898
|
+
}
|
|
1899
|
+
]
|
|
1900
|
+
},
|
|
1901
|
+
{
|
|
1902
|
+
"heading": "Fixed",
|
|
1903
|
+
"items": [
|
|
1904
|
+
{
|
|
1905
|
+
"title": "b.watcher canonicalizes its root on Windows",
|
|
1906
|
+
"body": "`b.watcher.create` now resolves its `root` to the real long path before watching. On Windows a root with an 8.3 short-name component (the system temp directory commonly resolves to one) made the native recursive backend deliver long-name event paths that no longer prefix-matched the watched root, which could abort the process under a strict libuv fs-event assertion. The watcher now canonicalizes the root (expanding short names and resolving symlinks), so events match the watched directory on Windows."
|
|
1907
|
+
}
|
|
1908
|
+
]
|
|
1909
|
+
},
|
|
1910
|
+
{
|
|
1911
|
+
"heading": "Detectors",
|
|
1912
|
+
"items": [
|
|
1913
|
+
{
|
|
1914
|
+
"title": "Rename-retry, SQLite-limits, and Clear-Site-Data guards",
|
|
1915
|
+
"body": "Three recurrence detectors ship with the fixes: a bare `nodeFs.renameSync` final rename that doesn't route through `atomicFile.renameWithRetry`; a main `DatabaseSync` handle constructed without the SQLITE_LIMIT_LENGTH `limits`; and a hand-rolled Clear-Site-Data header value that skips the shared RFC 9527 builder."
|
|
1916
|
+
}
|
|
1917
|
+
]
|
|
1918
|
+
}
|
|
1919
|
+
]
|
|
1920
|
+
},
|
|
1921
|
+
{
|
|
1922
|
+
"version": "0.15.8",
|
|
1923
|
+
"date": "2026-06-13",
|
|
1924
|
+
"headline": "Redacts OTLP log-sink attributes to close a secret/PII egress the span fix missed, adds EU DSA and China PIPL cross-border compliance record-builders, ships an SSDF producer self-attestation with every release, and makes the published tarball reproducible",
|
|
1925
|
+
"summary": "This release closes a telemetry egress hole, adds two compliance record-builder namespaces, and hardens the release supply chain. The OTLP log sinks (HTTP-JSON and gRPC) shipped a log record's meta attributes and the resource attributes to the collector unredacted — a log line carrying a bearer token, password, or API key reached the wire verbatim (CWE-532). The 0.15.4 fix wired the telemetry redactor into the span and metric exporters but the log sinks were missed; both now run record and resource attributes through the same redactor before serialization. New b.dsa builds the EU Digital Services Act (Reg 2022/2065) records an intermediary or platform must keep — Art. 16 notice-and-action, Art. 17 statement of reasons, and the Art. 15 / 24(3) transparency report. New b.pipl builds the China PIPL cross-border transfer records — an Art. 38/40 assessment that determines whether a CAC security assessment is mandatory (CIIO, important data, or the volume / sensitive-PI thresholds), and an Art. 40 security-assessment certificate. On the supply-chain side, every release now ships ssdf-attestation.json, a machine-readable NIST SP 800-218 / OMB M-22-18 producer self-attestation mapping each secure-development practice to its implementing control, and the published tarball is now packed with SOURCE_DATE_EPOCH so an operator can rebuild it byte-for-byte from the release commit.",
|
|
1926
|
+
"sections": [
|
|
1927
|
+
{
|
|
1928
|
+
"heading": "Added",
|
|
1929
|
+
"items": [
|
|
1930
|
+
{
|
|
1931
|
+
"title": "b.dsa — EU Digital Services Act compliance record-builders",
|
|
1932
|
+
"body": "`b.dsa` builds the dated, frozen records the EU Digital Services Act (Regulation (EU) 2022/2065) requires an online intermediary or platform to keep: `b.dsa.noticeAndAction` (Art. 16) records a notice against a piece of content and computes the action-due window; `b.dsa.statementOfReasons` (Art. 17) records a moderation decision with its legal or contractual ground (exactly one is required), the facts, whether it was automated, and the redress routes offered; `b.dsa.transparencyReport` (Art. 15 / 24(3)) aggregates the period counts into a report with the next-due date. The builders perform no network I/O and emit a best-effort audit event; they map to the `dsa` compliance posture."
|
|
1933
|
+
},
|
|
1934
|
+
{
|
|
1935
|
+
"title": "b.pipl — China PIPL cross-border transfer record-builders",
|
|
1936
|
+
"body": "`b.pipl.sccFilingAssessment` builds a PIPL Art. 38/40/55 cross-border transfer assessment and determines the lawful mechanism: it forces a CAC security assessment (over a self-selected standard contract or certification) when the exporter is a critical-information-infrastructure operator, exports important data, handles personal information of more than 1,000,000 individuals, or crosses the cumulative volume / sensitive-PI thresholds. `b.pipl.securityAssessmentCertificate` records an Art. 40 security-assessment self-declaration with a 3-year validity clock. Both return frozen dated records and map to the `pipl-cn` posture."
|
|
1937
|
+
},
|
|
1938
|
+
{
|
|
1939
|
+
"title": "SSDF producer self-attestation shipped with every release",
|
|
1940
|
+
"body": "Every release now attaches `ssdf-attestation.json` — a machine-readable NIST SP 800-218 (SSDF v1.1) / OMB M-22-18 producer self-attestation. It maps each secure-software-development practice to its implementing control in the tree (SLSA L3 provenance, SSH-signed tags, vendored zero-runtime-dep supply chain, OSV-Scanner gating, coordinated disclosure) and is deterministic from the release commit. Its sha256 is a subject of the SLSA L3 provenance, so verifying the provenance verifies the attestation has not been tampered with. Downstream consumers who require SSDF supplier-compliance evidence can download it from the release page."
|
|
1941
|
+
}
|
|
1942
|
+
]
|
|
1943
|
+
},
|
|
1944
|
+
{
|
|
1945
|
+
"heading": "Security",
|
|
1946
|
+
"items": [
|
|
1947
|
+
{
|
|
1948
|
+
"title": "OTLP log sinks redact record and resource attributes before export",
|
|
1949
|
+
"body": "`b.logStream`'s OTLP log sinks (HTTP-JSON and gRPC) shipped a log record's `meta` attributes and the sink's resource attributes to the collector UNREDACTED, so a log line whose meta carried a bearer token, password, or API key — or a credential placed in a resource attribute — reached the OTLP wire verbatim (CWE-532). The 0.15.4 change baked the telemetry redactor into the span and metric exporters but its detector was anchored on the span/metric encoder function names, leaving the log sinks uncovered. Both log sinks now run record and resource attributes through `b.observability.redactAttrs` before serialization, the same egress contract the span and metric exporters already hold."
|
|
1950
|
+
},
|
|
1951
|
+
{
|
|
1952
|
+
"title": "Reproducible published tarball (SOURCE_DATE_EPOCH)",
|
|
1953
|
+
"body": "The release workflow now exports `SOURCE_DATE_EPOCH` (derived from the tagged commit's author date) before `npm pack`, so the mtime stamped into every tar header is deterministic. An operator can re-pack the package from the same commit and match the published tarball's sha256 byte-for-byte, strengthening the source-to-artifact verification path alongside the existing SLSA L3 provenance and PQC signatures."
|
|
1954
|
+
}
|
|
1955
|
+
]
|
|
1956
|
+
},
|
|
1957
|
+
{
|
|
1958
|
+
"heading": "Detectors",
|
|
1959
|
+
"items": [
|
|
1960
|
+
{
|
|
1961
|
+
"title": "otlp-log-sink-encodes-attrs-without-redactor",
|
|
1962
|
+
"body": "Fires when an OTLP log-sink encoder hands a raw `record.meta` or resource-attribute map to serialization without routing it through `observability.redactAttrs` — the class the span/metric detector could not see because the log sinks carry the OTLP-logs schema function names."
|
|
1963
|
+
}
|
|
1964
|
+
]
|
|
1965
|
+
}
|
|
1966
|
+
]
|
|
1967
|
+
},
|
|
1968
|
+
{
|
|
1969
|
+
"version": "0.15.7",
|
|
1970
|
+
"date": "2026-06-13",
|
|
1971
|
+
"headline": "Hardens the new URL canonicalizer against an IPv4-mapped allowlist bypass, enforces the OIDC azp authorized-party check, and closes a set of audited correctness gaps in retention, credential rehashing, the scheduler, and SD-JWT key binding",
|
|
1972
|
+
"summary": "This release deepens the URL/host canonicalizer shipped in 0.15.6 and clears a batch of audited correctness gaps. The canonicalizer now folds an IPv4-mapped IPv6 address (::ffff:1.2.3.4) to its embedded IPv4 and strips every trailing dot from a host, so a dual-stack peer can no longer slip past an operator's dotted-IPv4 allowlist and host./host.. no longer evade a host comparison. (NAT64 and 6to4 hosts are deliberately kept as IPv6 so canonicalizing then classifying agrees with the SSRF classifier, which treats them as reserved.) On the OIDC side, verifyIdToken now enforces OIDC Core 3.1.3.7: a multi-audience ID token must carry an azp (authorized party) and a present azp must equal the client_id, closing a confused-deputy hole where a token minted for a different client but listing this RP in its audience array verified clean. The rest are audited fixes: retention.complianceFloor now honors the active posture set via applyPosture (the documented inheritance was unimplemented); credentialHash.needsRehash now drives the advertised SHAKE256 length-rotation (raising the output length now triggers a rehash, upgrade-only); the task scheduler no longer lets a run abandoned by its watchdog clobber the next run's state or emit a stale success when its slow promise settles late; and the SD-JWT key-binding JWT compares its audience and nonce in constant time.",
|
|
1973
|
+
"sections": [
|
|
1974
|
+
{
|
|
1975
|
+
"heading": "Security",
|
|
1976
|
+
"items": [
|
|
1977
|
+
{
|
|
1978
|
+
"title": "The URL canonicalizer folds IPv4-mapped IPv6 addresses to IPv4",
|
|
1979
|
+
"body": "`b.safeUrl.canonicalize` / `b.ssrfGuard.canonicalizeHost` now fold an IPv4-mapped IPv6 address (`::ffff:1.2.3.4`, the `::ffff:0:0/96` block) to its embedded IPv4 dotted form. Previously it canonicalized to an IPv6 string, so a dual-stack peer never unified with an operator's `1.2.3.4` allow/deny entry — an allowlist bypass. Only the IPv4-mapped block folds, because the SSRF classifier maps it to the embedded v4 verdict directly; NAT64 (`64:ff9b::/96`) and 6to4 (`2002::/16`) are deliberately kept as IPv6, since the classifier treats a NAT64 literal as reserved and folding it would turn a blocked verdict into an allowed public IPv4. The host canonicalizer also now strips every trailing dot, so `host`, `host.`, and `host..` collapse to one form."
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
"title": "verifyIdToken enforces the OIDC azp (authorized party) check",
|
|
1983
|
+
"body": "`b.auth.oauth`'s `verifyIdToken` validated only that its `client_id` was present in the token's `aud`, ignoring `azp`. Per OIDC Core 3.1.3.7, a multi-audience ID token must carry an `azp` and a present `azp` must equal the RP's `client_id`. Without it, a token whose authorized party is a different client — but whose `aud` array also lists this RP — verified clean (a confused-deputy / token-substitution hole). The verifier now rejects a multi-audience token with no `azp` (`auth-oauth/azp-required`) and any token whose `azp` is not the `client_id` (`auth-oauth/azp-mismatch`). A single-audience token with no `azp` (the common case) is unaffected."
|
|
1984
|
+
},
|
|
1985
|
+
{
|
|
1986
|
+
"title": "SD-JWT key-binding audience and nonce compare in constant time",
|
|
1987
|
+
"body": "`b.auth.sdJwtVc.verify` compared the key-binding JWT's `aud` and `nonce` with a short-circuiting `!==`, while the adjacent `sd_hash` check already used a constant-time compare. The `nonce` is a verifier-issued replay-defense value, so a non-constant-time compare leaks a matching-prefix timing oracle; both checks now use the constant-time helper."
|
|
1988
|
+
}
|
|
1989
|
+
]
|
|
1990
|
+
},
|
|
1991
|
+
{
|
|
1992
|
+
"heading": "Fixed",
|
|
1993
|
+
"items": [
|
|
1994
|
+
{
|
|
1995
|
+
"title": "retention.complianceFloor honors the active compliance posture",
|
|
1996
|
+
"body": "`b.retention.complianceFloor` required an explicit posture and never read the active posture set by `applyPosture` (the `b.compliance.set` cascade), so the documented inheritance was unimplemented dead state. It now inherits the active posture when none is passed — `complianceFloor(candidateTtlMs)` uses the active posture, an explicit posture still overrides it — and `applyPosture(null)` now clears the active posture (it was a silent no-op, so `b.compliance.clear` could not reset it)."
|
|
1997
|
+
},
|
|
1998
|
+
{
|
|
1999
|
+
"title": "credentialHash.needsRehash drives the SHAKE256 length-rotation",
|
|
2000
|
+
"body": "`b.credentialHash.needsRehash` never compared the stored SHAKE256 digest length against the configured length, so raising the output length never triggered a rehash and the advertised length-rotation was a silent no-op. It now flags a digest shorter than the configured/default length for rehash (upgrade-only — a longer-than-target digest is never shortened, matching the Argon2 convention)."
|
|
2001
|
+
},
|
|
2002
|
+
{
|
|
2003
|
+
"title": "The scheduler no longer lets a watchdog-abandoned run corrupt the next run",
|
|
2004
|
+
"body": "`b.scheduler`'s watchdog force-clears a task's running flag after `maxJobMs` so a hung handler can't lock out future fires, and the next tick re-fires. The original slow promise then settled late and unconditionally overwrote the task's running / lastFinish / lastError state and emitted a `system.scheduler.task.success`|`failure` for a run the watchdog had already given up on — clobbering the new run and double-counting. Each run is now tagged with a generation that the watchdog and every fire bump; a settle whose tag is stale is ignored."
|
|
2005
|
+
}
|
|
2006
|
+
]
|
|
2007
|
+
}
|
|
2008
|
+
]
|
|
2009
|
+
},
|
|
2010
|
+
{
|
|
2011
|
+
"version": "0.15.6",
|
|
2012
|
+
"date": "2026-06-12",
|
|
2013
|
+
"headline": "Closes SAML and OIDC assertion-replay windows, bounds SSE memory under a slow client, restores at-least-once delivery for a crashed outbox publisher, makes sealed-column membership queries work, ships JOSE-conformant SD-JWT signatures, and adds a URL canonicalizer for SSRF-safe comparison",
|
|
2014
|
+
"summary": "A security and correctness release. On the identity surface: a SAML Response whose Bearer (or Holder-of-Key) SubjectConfirmation omits NotOnOrAfter is now rejected instead of accepted as fresh-forever, and the OIDC ID-token verifier no longer lets a caller disable expiry validation on a normal token - the exp bypass is restricted to back-channel-logout tokens and bounded by an issued-at freshness floor. SD-JWT-VC ES256/ES384 signatures are now emitted as JOSE raw r||s, so credentials this issuer signs verify in conformant wallets and verifiers. A new b.safeUrl.canonicalize (and b.ssrfGuard.canonicalizeHost) collapses obfuscated host and IP forms to one canonical string so allowlist and SSRF comparisons can't be bypassed by encoding tricks. On the reliability side: server-sent-event channels now cap their per-connection outbound buffer and evict a stalled client instead of growing the heap without bound; the outbox reclaims a job left in-flight by a publisher that crashed mid-delivery; the background worker pool no longer drops a task queued behind one that timed out; a retention preview no longer rewrites the whole database file; an equality / membership query on an encrypted column now hashes each candidate (membership queries previously failed outright); and the on-read re-hash of a legacy lookup digest now runs on Postgres and MySQL handles, not only SQLite.",
|
|
2015
|
+
"sections": [
|
|
2016
|
+
{
|
|
2017
|
+
"heading": "Security",
|
|
2018
|
+
"items": [
|
|
2019
|
+
{
|
|
2020
|
+
"title": "SAML SubjectConfirmation without NotOnOrAfter is rejected",
|
|
2021
|
+
"body": "`b.auth.saml` SP response verification treated the `NotOnOrAfter` attribute on a Bearer SubjectConfirmationData as optional: a confirmation that omitted it was accepted with no upper bound on the assertion's freshness - a captured assertion replayable indefinitely. SAML 2.0 Web Browser SSO Profile §4.1.4.2 requires Bearer confirmations to carry NotOnOrAfter. `verifyResponse` now rejects a confirmation that is missing NotOnOrAfter, has an unparseable value, or is expired; the Holder-of-Key path (Profile §3.1) is hardened the same way, including the previously-accepted unparseable-value case."
|
|
2022
|
+
},
|
|
2023
|
+
{
|
|
2024
|
+
"title": "ID-token expiry can no longer be disabled on a normal token",
|
|
2025
|
+
"body": "`b.auth.oauth`'s `verifyIdToken` honored a `skipExpCheck` option with no constraint, so any caller could verify an expired - or replayed - ID token. That option exists only for OIDC Back-Channel Logout tokens, which carry no `exp`. It is now self-guarding: `verifyIdToken` rejects `skipExpCheck` (`auth-oauth/skip-exp-check-not-allowed`) on any token that lacks the back-channel-logout event claim, and even for a logout token it enforces an `iat` freshness floor (`auth-oauth/logout-token-stale`). The internal logout path is unaffected."
|
|
2026
|
+
},
|
|
2027
|
+
{
|
|
2028
|
+
"title": "Server-sent-event channels bound their outbound buffer",
|
|
2029
|
+
"body": "An SSE channel wrote to the response with no regard for backpressure and no cap on buffered bytes, so a single stalled client could make the server buffer events until the heap was exhausted (a memory-exhaustion denial of service). Each channel now tracks its unflushed-byte count and, past a per-channel cap (`maxBufferedBytes`, default 1 MiB), closes the connection and throws `sse/backpressure` - evicting the slow consumer instead of buffering without limit. A client that keeps up is never affected."
|
|
2030
|
+
},
|
|
2031
|
+
{
|
|
2032
|
+
"title": "URL and host canonicalizer for SSRF-safe comparison",
|
|
2033
|
+
"body": "New `b.safeUrl.canonicalize(url, opts?)` and `b.ssrfGuard.canonicalizeHost(host)` return the canonical, comparable form of a URL or host: scheme and host lowercased, IDN hosts emitted as their punycode A-label (a confusable / mixed-script host is rejected), every base of an IP literal (decimal, octal, hex, dotted, IPv4-mapped and zero-compressed IPv6) collapsed to one canonical address, default ports stripped, trailing-dot hosts normalized, and path percent-encoding normalized per RFC 3986. Use it to build host allowlists, deduplicate URLs, or compare a fetch target so an allowlist check can't be bypassed by encoding the same address a different way."
|
|
2034
|
+
}
|
|
2035
|
+
]
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
"heading": "Fixed",
|
|
2039
|
+
"items": [
|
|
2040
|
+
{
|
|
2041
|
+
"title": "SD-JWT-VC ES256 / ES384 signatures are JOSE-conformant",
|
|
2042
|
+
"body": "`b.auth.sdJwtVc` signed and verified ES256 / ES384 credentials with node:crypto's default DER ECDSA encoding instead of the raw r||s (`ieee-p1363`) form JOSE and EUDI wallets require, so a credential this issuer signed was rejected by conformant verifiers and the library rejected conformant holders' key-binding JWTs. The issuer JWT and the holder KB-JWT now both sign and verify with `ieee-p1363`, matching the rest of the framework's JOSE signers."
|
|
2043
|
+
},
|
|
2044
|
+
{
|
|
2045
|
+
"title": "Membership queries on an encrypted column now work",
|
|
2046
|
+
"body": "Querying an encrypted (sealed) column with `IN` / `$in` - `b.db.from(table).whereIn(\"email\", [...])` or `b.db.collection(table).find({ email: { $in: [...] } })` - threw, because the sealed-field-to-derived-hash rewrite passed the whole candidate array to the hash lookup as a single value. Each candidate is now hashed individually and matched against both its active keyed digest and its legacy digest, so membership queries on an encrypted column return the right rows, including rows written before the lookup-hash default changed."
|
|
2047
|
+
},
|
|
2048
|
+
{
|
|
2049
|
+
"title": "A timing-out background task no longer drops the task queued behind it",
|
|
2050
|
+
"body": "When a `b.workerPool` task timed out or its worker errored, the slot was returned to the idle pool and drained one moment before it was marked for recycling, so a task queued behind it could be dispatched to the worker about to be terminated - and came back as `workerpool/worker-exit` (or hung) instead of running. The slot is now marked recycling before the queue is drained, so the queued task waits for the replacement worker."
|
|
2051
|
+
},
|
|
2052
|
+
{
|
|
2053
|
+
"title": "The outbox recovers a job stranded by a crashed publisher",
|
|
2054
|
+
"body": "`b.outbox` claims a row by flipping it to in-flight, but the claim scan only selected pending rows, so a publisher that crashed between claiming a row and recording delivery left that row in-flight forever - the event was silently dropped, breaking at-least-once delivery. The outbox now stamps each claim with a timestamp and, at the start of every poll, returns any in-flight row older than the claim lease (`claimReclaimMs`, default 5 minutes) to the pending pool so it is delivered. An existing outbox table gains the new column automatically."
|
|
2055
|
+
},
|
|
2056
|
+
{
|
|
2057
|
+
"title": "A retention preview no longer rewrites the whole database",
|
|
2058
|
+
"body": "Previewing a retention rule with `b.retention` (`run(name, { dryRun: true })` or the `retention preview` command) ran a full database VACUUM for every candidate row under a regulated posture (gdpr / hipaa / and similar), because the per-row erase - which schedules the vacuum - ran before the dry-run check. A preview now computes what it would erase without touching the database; the vacuum runs only on a real erase."
|
|
2059
|
+
},
|
|
2060
|
+
{
|
|
2061
|
+
"title": "On-read lookup-digest upgrade runs on Postgres and MySQL",
|
|
2062
|
+
"body": "When `b.cryptoField.unsealRow` re-hashed a legacy lookup digest to the current keyed form and persisted it, the UPDATE was always built for SQLite, so on a Postgres or MySQL handle the durable rewrite quoted identifiers for the wrong dialect and silently no-opped - the legacy digest stayed on disk and the migration never completed off SQLite. The rewrite now uses the handle's own dialect."
|
|
2063
|
+
}
|
|
2064
|
+
]
|
|
2065
|
+
}
|
|
2066
|
+
]
|
|
2067
|
+
},
|
|
2068
|
+
{
|
|
2069
|
+
"version": "0.15.5",
|
|
2070
|
+
"date": "2026-06-12",
|
|
2071
|
+
"headline": "Legal-hold and subject-restriction PII is sealed at rest, and a guard's compliance-posture forensic and runtime caps are applied on its default gate",
|
|
2072
|
+
"summary": "This release closes two data-protection gaps. The legal-hold registry and the subject-restriction flag stored their free-text fields - the legal basis, custodian, ticket citation, and restriction reason that link a data subject to a legal matter - in clear, because those local tables were written through a raw SQL path that bypassed the structured builder's at-rest sealing. Those columns are now sealed on write and unsealed on read, the same way the DSR ticket store already seals subject identifiers. Separately, a content guard built on the standard gate contract and gated with a compliance posture (for example b.guardCidr.gate({ compliancePosture: \"hipaa\" })) silently dropped that posture's forensic-snapshot cap and the profile's runtime cap, because the default gate passed the caller's raw options straight to the gate builder instead of resolving the profile and posture first - so a regulated-posture refusal carried no forensic evidence. The default gate now resolves the profile and posture before building the gate, matching the hand-written guard gates.",
|
|
2073
|
+
"sections": [
|
|
2074
|
+
{
|
|
2075
|
+
"heading": "Security",
|
|
2076
|
+
"items": [
|
|
2077
|
+
{
|
|
2078
|
+
"title": "Legal-hold and subject-restriction PII is sealed at rest",
|
|
2079
|
+
"body": "`b.legalHold`'s `_blamejs_legal_hold` registry stored the hold reason, custodian, placed-by, and citation in clear, and `b.subject.restrict`'s `_blamejs_subject_restrictions` stored the restriction reason in clear - free text that ties a data subject to a litigation hold or an Art. 18 processing restriction. Those rows were written through a raw `sql.insert` + `prepare().run()` path that bypassed the structured builder's automatic field sealing (the subject-restrictions table even declared the field as sealed, but the raw write never applied it). Both now seal those columns on write and unseal on read through `cryptoField`, so the legal-basis and custodian text is encrypted at rest under the deployment's vault key. Pre-existing plaintext rows continue to read correctly (the unseal path passes through an already-plaintext value)."
|
|
2080
|
+
},
|
|
2081
|
+
{
|
|
2082
|
+
"title": "A guard's default gate applies its compliance-posture forensic and runtime caps",
|
|
2083
|
+
"body": "A guard built on `b.gateContract.defineGuard` with the standard gate (no bespoke gate) and gated with a compliance posture dropped that posture's `forensicSnippetBytes` cap and the profile's `maxRuntimeMs` cap: the default gate passed the caller's raw options straight to the gate builder, which reads those caps directly, but the values live on the resolved profile and posture, not the raw options. The effect was that a regulated-posture refusal captured no forensic snapshot (the cap defaulted to 0, i.e. disabled) and the check ran without the profile's runtime bound. The default gate now resolves the profile and posture before building the gate - matching the hand-written guard gates - so `gate({ compliancePosture: \"hipaa\" })` applies the posture's forensic cap and the profile's runtime cap as documented."
|
|
2084
|
+
}
|
|
2085
|
+
]
|
|
2086
|
+
}
|
|
2087
|
+
]
|
|
2088
|
+
},
|
|
2089
|
+
{
|
|
2090
|
+
"version": "0.15.4",
|
|
2091
|
+
"date": "2026-06-12",
|
|
2092
|
+
"headline": "Telemetry attribute values are redacted before they leave the process, per-row data residency is enforced on every write and export path, DDL routes through the single-statement gate, the DPoP middleware requires its replay store, and session rotation re-keys the device binding",
|
|
2093
|
+
"summary": "This release closes a set of egress, data-residency, and session-binding gaps. OTLP span, span-event, and resource attributes are now scrubbed through the telemetry redactor before serialization, on both the JSON and protobuf paths, matching the metric exporter - an attribute value holding a bearer token, password, or API key no longer reaches the collector verbatim. Per-row data residency, previously enforced only at the structured query builder, is now enforced on the three paths that bypassed it: raw SQL writes, read-replica fan-out, and backup export. Every CREATE TABLE / ALTER TABLE the schema reconciler and the DSR store emit now passes through the same single-statement gate the query builder uses. The DPoP middleware now requires its replay store at mount time instead of silently mounting a proof-of-possession gate that performed no replay check. And session rotation re-keys the device fingerprint to the new session id, so a rotated session stays bound to its device instead of falsely reporting drift on the next request.",
|
|
2094
|
+
"sections": [
|
|
2095
|
+
{
|
|
2096
|
+
"heading": "Security",
|
|
2097
|
+
"items": [
|
|
2098
|
+
{
|
|
2099
|
+
"title": "OTLP exporter redacts span, event, and resource attribute values before egress",
|
|
2100
|
+
"body": "Span, span-event, and resource attributes were serialized to the OTLP collector verbatim on both the JSON and protobuf encodings - the metric exporter scrubbed its attributes through the telemetry redactor, but the span exporter did not. An attribute value carrying a secret or PII (a bearer token in `authorization`, a `password`, an `api_key`) was therefore shipped in clear to whatever collector the deployment points at (CWE-532). Every attribute-map encoder now runs each value through `b.observability.redactAttrs` (default composes `b.redact.redact`, dropping any attribute whose redactor throws) before the wire payload, so telemetry is redacted like the log and audit egress paths. The new `b.observability.redactAttrs(attrs)` is available for operators building custom exporters."
|
|
2101
|
+
},
|
|
2102
|
+
{
|
|
2103
|
+
"title": "Per-row data residency is enforced on raw writes, read replicas, and backups",
|
|
2104
|
+
"body": "Per-row residency was enforced only at the structured query builder. Three paths reached storage or left the deployment without it: raw SQL writes (`b.db.runSql` / `b.db.prepare().run()`, INSERT and UPDATE forms) bypassed the local residency check entirely, so a cross-border row could be written under a regulated posture with no refusal; read-replica fan-out dropped the row-residency tag, routing a regulated read with no row region identified to a residency-tagged, non-cross-border replica with no check; and `b.backup.create`'s residency check compared only the single deployment region to the destination, blind to a per-row-residency table that admits rows from several regions. Raw writes now parse the target table and residency value and apply the same gate the builder does; the replica read now fails closed when the row region is unidentified; and backup now emits a per-row cross-border advisory for any declared residency table whose admitted regions differ from the backup destination."
|
|
2105
|
+
},
|
|
2106
|
+
{
|
|
2107
|
+
"title": "Schema and DSR DDL routes through the single-statement gate",
|
|
2108
|
+
"body": "The CREATE TABLE / ALTER TABLE statements emitted by the schema reconciler and the DSR ticket store were assembled and run without the single-statement / NUL / unterminated-quote / unbalanced-paren gate that every query the builder emits already passes. That gate is now a shared check both the builder's catalog emitter and the schema/DSR DDL path call, so a terminator, comment marker, or unbalanced quote that reached a DDL fragment is refused at emit time on every backend."
|
|
2109
|
+
},
|
|
2110
|
+
{
|
|
2111
|
+
"title": "DPoP middleware requires its replay store at mount time",
|
|
2112
|
+
"body": "`b.middleware.dpop` documented `replayStore` as required, but the factory read it optionally and gated the jti-replay check behind its presence - omitting it mounted a proof-of-possession gate that performed no replay check, so a captured DPoP proof could be replayed indefinitely (RFC 9449 §11.1). The middleware now requires the store at config time: a missing store, or a store lacking `checkAndInsert`, throws when the middleware is created instead of failing open at request time. The low-level `b.auth.dpop.verify` primitive keeps `replayStore` optional for advanced callers that track jti themselves."
|
|
2113
|
+
}
|
|
2114
|
+
]
|
|
2115
|
+
},
|
|
2116
|
+
{
|
|
2117
|
+
"heading": "Fixed",
|
|
2118
|
+
"items": [
|
|
2119
|
+
{
|
|
2120
|
+
"title": "Session rotation re-keys the device fingerprint to the new session id",
|
|
2121
|
+
"body": "A session's optional device fingerprint is keyed to its session id, so that a stolen database cannot replay the binding. `b.session.rotate` moved the session id but left the stored fingerprint keyed to the old id, so the next `verify` recomputed the fingerprint against the new id and mismatched - reporting a false `fingerprintDrift` (which destroys the session under strict operators, logging the user out on every rotation) or silently breaking the binding. Rotation now re-keys the fingerprint to the new session id from the live request: pass the same `{ req, fingerprintFields }` to `rotate`. A fingerprint-bound session rotated without `req` now throws, because the binding cannot otherwise follow the new id; unbound sessions are unaffected."
|
|
2122
|
+
}
|
|
2123
|
+
]
|
|
2124
|
+
}
|
|
2125
|
+
]
|
|
2126
|
+
},
|
|
2127
|
+
{
|
|
2128
|
+
"version": "0.15.3",
|
|
2129
|
+
"date": "2026-06-12",
|
|
2130
|
+
"headline": "DDL hardening in b.sql, schema-confined column introspection on Postgres and MySQL, and a classical-downgrade audit on proxy-tunneled TLS",
|
|
2131
|
+
"summary": "This release hardens the data layer and closes a transport audit gap. The b.sql builder refuses an unrecognised column type that carries a statement terminator, quote, or comment marker - the one position in an otherwise quote-by-construction DDL builder where a verbatim string reached the emitted statement - and routes the finished CREATE TABLE through the same single-statement gate every other verb uses. The schema reconciler's column introspection is now confined to the schema or database the bare-named CREATE TABLE actually writes into (current_schema() on Postgres, DATABASE() on MySQL), so a same-named table in another schema no longer pollutes the column set, silently skipping an ADD COLUMN or fabricating false schema drift that refuses a regulated-posture boot. Two further builder gaps are fixed: a column-level primary key combined with a composite primaryKey now fails at build time with a clear error instead of producing invalid DDL, and a MySQL upsert read-back keyed by a cast or a server-evaluated function now renders the cast (or refuses the function) instead of binding an internal wrapper. Finally, an HTTPS request sent through a configured proxy now emits the tls.classical_downgrade audit when the handshake falls back to a classical group, the same as a direct connection.",
|
|
2132
|
+
"sections": [
|
|
2133
|
+
{
|
|
2134
|
+
"heading": "Security",
|
|
2135
|
+
"items": [
|
|
2136
|
+
{
|
|
2137
|
+
"title": "b.sql refuses an injection-bearing verbatim column type and gates every CREATE TABLE",
|
|
2138
|
+
"body": "An unrecognised column type passed to b.sql.createTable / alterTable was emitted into the DDL verbatim - the single raw-emission position in a builder that otherwise quotes every identifier and guards every constraint fragment. A type such as \"text); DROP TABLE secrets; --\" could therefore smuggle a stacked statement. The builder now refuses, at build time, a verbatim type carrying a statement terminator or comment marker, and routes the finished CREATE TABLE / ALTER TABLE statement through the same single-statement / NUL / unterminated-quote / unbalanced-paren gate every SELECT / INSERT / UPDATE / DELETE / UPSERT already used - so an unbalanced quote is caught there. Legitimate types are unaffected: VARCHAR(255), NUMERIC(10,2), DOUBLE PRECISION, TIMESTAMP WITH TIME ZONE, and MySQL ENUM('a','b') / SET(...) (which need balanced quotes) all still pass."
|
|
2139
|
+
}
|
|
2140
|
+
]
|
|
2141
|
+
},
|
|
2142
|
+
{
|
|
2143
|
+
"heading": "Fixed",
|
|
2144
|
+
"items": [
|
|
2145
|
+
{
|
|
2146
|
+
"title": "Schema reconciliation reads columns from the right schema on Postgres and MySQL",
|
|
2147
|
+
"body": "The reconciler's column introspection queried information_schema with no schema filter, so on a Postgres instance or MySQL server hosting more than one schema/database with a same-named table, the live column set was the union across schemas. That could silently skip an ADD COLUMN the table needed, or report false drift that refuses a boot under a pinned regulated posture. Introspection is now confined to current_schema() (Postgres) / DATABASE() (MySQL) - the schema the bare-named CREATE TABLE lands in. SQLite (PRAGMA, per-file) is unchanged."
|
|
2148
|
+
},
|
|
2149
|
+
{
|
|
2150
|
+
"title": "createTable rejects a contradictory primary-key declaration at build time",
|
|
2151
|
+
"body": "Declaring both a column-level primary key (primaryKey / autoIncrement / serial) and a composite opts.primaryKey emitted two PRIMARY KEY clauses, which every dialect rejects at the driver mid-migration. The builder now refuses the contradiction at build time with a clear error; a single column PK, or a composite primaryKey with no column-level PK, is unaffected."
|
|
2152
|
+
},
|
|
2153
|
+
{
|
|
2154
|
+
"title": "MySQL upsert read-back resolves a cast or function conflict key instead of binding a wrapper",
|
|
2155
|
+
"body": "On MySQL, an upsert whose conflict key was a b.sql.cast(...) or b.sql.fn(...) built a read-back SELECT that bound the wrapper object, so the read-back matched no rows. A cast conflict key now renders as CAST(? AS type) binding the inner value; a server-evaluated function conflict key (which has no stable read-back identity) is refused with a clear error. Plain scalar conflict keys are unchanged."
|
|
2156
|
+
},
|
|
2157
|
+
{
|
|
2158
|
+
"title": "Proxy-tunneled TLS emits the classical-downgrade audit",
|
|
2159
|
+
"body": "An HTTPS upstream reached through a configured proxy performed its TLS handshake without emitting the tls.classical_downgrade audit on a classical-group fallback, leaving the post-quantum-readiness inventory incomplete for proxied requests. Both the upstream handshake and the proxy-leg handshake now emit the audit on a classical fallback, matching the direct connection path. The handshake itself is unchanged (still hybrid-preferred TLSv1.3)."
|
|
2160
|
+
}
|
|
2161
|
+
]
|
|
2162
|
+
}
|
|
2163
|
+
]
|
|
2164
|
+
},
|
|
2165
|
+
{
|
|
2166
|
+
"version": "0.15.2",
|
|
2167
|
+
"date": "2026-06-12",
|
|
2168
|
+
"headline": "Object keys with a space, +, &, or other reserved characters now sign correctly against S3-compatible stores and Google Cloud Storage",
|
|
2169
|
+
"summary": "The SigV4 request signer (and the Google Cloud Storage V4 signer that shares it) URI-encoded the object-key path a second time when building the canonical request it signs, while the request on the wire carried the path encoded once. Amazon S3 — and every S3-compatible store such as MinIO, Cloudflare R2, Wasabi, and Backblaze B2, plus GCS — signs the canonical path encoded exactly once, so any object key containing a space, a +, an &, parentheses, or a non-ASCII character was signed over a path the server never received and the request was rejected with HTTP 403 SignatureDoesNotMatch. Keys built only from unreserved characters were unaffected, which is why the regression went unnoticed. This release makes the canonical-path encoding match the service: S3 and GCS encode the path once, while the AWS services that genuinely require a second encoding (SQS, CloudWatch Logs, SNS) keep it. Object reads, writes, deletes, listings, presigned URLs, and backup or restore through the object-store adapter now succeed for keys with reserved characters. Separately, the bucket-operations key encoder now uses the AWS reserved-character set, so a key containing !, *, ', or ( is escaped to match the bytes the store signs over.",
|
|
2170
|
+
"sections": [
|
|
2171
|
+
{
|
|
2172
|
+
"heading": "Fixed",
|
|
2173
|
+
"items": [
|
|
2174
|
+
{
|
|
2175
|
+
"title": "SigV4 and GCS V4 sign object-key paths with the single encoding S3 and GCS expect",
|
|
2176
|
+
"body": "A request to read, write, delete, list, or presign an object whose key contained a space, +, &, (, ), or a non-ASCII character failed with 403 SignatureDoesNotMatch against S3, every S3-compatible store, and Google Cloud Storage, because the canonical request double-encoded the path the wire carried once. The signer is now service-aware: S3 and GCS sign the path encoded once (matching the wire and the store's own canonicalization), while SQS, CloudWatch Logs, and SNS keep the second encoding the AWS spec requires for those services. No configuration change or migration is needed — object operations and presigned URLs for keys with reserved characters simply start working. Object keys made only of unreserved characters are byte-for-byte unchanged."
|
|
2177
|
+
},
|
|
2178
|
+
{
|
|
2179
|
+
"title": "Bucket-operations key encoder escapes the AWS reserved set",
|
|
2180
|
+
"body": "The bucket-level object operations encoded key path segments with a generic URI encoder that left !, *, ', and ( unescaped. Those now route through the same AWS encoder the read and write paths use, so a key containing one of those characters is escaped consistently and signs correctly."
|
|
2181
|
+
}
|
|
2182
|
+
]
|
|
2183
|
+
}
|
|
2184
|
+
]
|
|
2185
|
+
},
|
|
2186
|
+
{
|
|
2187
|
+
"version": "0.15.1",
|
|
2188
|
+
"date": "2026-06-11",
|
|
2189
|
+
"headline": "Sealed-column lookups find rows written before the v0.15.0 hash change, and API-key secrets re-hash to the active algorithm on verify",
|
|
2190
|
+
"summary": "v0.15.0 changed the default derived hash — the blind index a sealed column is looked up by — from an unkeyed salted hash to a keyed MAC, and promised a transparent migration via a dual read. But no lookup path actually performed the dual read, so on a deployment that already held data, a lookup by a sealed column (a session's user id, an API key's owner, an audit actor or resource, a consent or data-subject id, a mail thread) computed only the new keyed digest and missed every row written before the upgrade. This release wires the dual read into every lookup: a sealed-column equality now matches both the active keyed digest and the legacy salted digest, so pre-upgrade rows are found again. That restores two correctness guarantees the gap had quietly broken — revoking all of a user's sessions no longer skips sessions created before the upgrade, and a subject erasure no longer leaves pre-upgrade rows behind. Separately, the framework's API-key store now re-hashes a stored secret to the active hash algorithm on the next successful verify, the transparent rotation the credential-hash primitive documented but the store had never performed.",
|
|
2191
|
+
"sections": [
|
|
2192
|
+
{
|
|
2193
|
+
"heading": "Fixed",
|
|
2194
|
+
"items": [
|
|
2195
|
+
{
|
|
2196
|
+
"title": "Sealed-column lookups match rows written before the v0.15.0 keyed-MAC change",
|
|
2197
|
+
"body": "After v0.15.0 flipped the default derived-hash mode to a keyed MAC, a lookup by a sealed column computed only the new keyed digest, so rows written under the previous salted-hash default were no longer found — a silent index miss on every existing deployment. Every framework lookup path now dual-reads: `b.db.from(...).where(sealedField, value)` and the framework's own api-key, session, audit, consent, data-subject, and mail-thread lookups match the column against both the active keyed digest and the legacy salted digest (`b.db.hashCandidatesFor` exposes the candidate list for operator code). No migration or operator action is required; rows re-hash to the keyed form on read over time and the candidate set collapses back to a single value. Two correctness consequences are restored: revoking all of a user's sessions now also revokes sessions created before the upgrade, and a data-subject erasure now also deletes (and crypto-shreds) the subject's pre-upgrade rows."
|
|
2198
|
+
},
|
|
2199
|
+
{
|
|
2200
|
+
"title": "API-key secret hashes upgrade to the active algorithm on verify",
|
|
2201
|
+
"body": "The framework's API-key store now re-hashes a stored secret to the configured hash algorithm on the next successful verify (leader-gated, best-effort, primary-match only), emitting an `apikey.secret_rehash` audit and observability event. This is the transparent rotation `b.credentialHash` documents — a key stored under an older algorithm or parameter set silently moves to the current one as it is used, with no change to the verify result or the returned record."
|
|
2202
|
+
}
|
|
2203
|
+
]
|
|
2204
|
+
}
|
|
2205
|
+
]
|
|
2206
|
+
},
|
|
2207
|
+
{
|
|
2208
|
+
"version": "0.15.0",
|
|
2209
|
+
"date": "2026-06-08",
|
|
2210
|
+
"headline": "A chainable SQL builder, MySQL as a first-class data backend, and a keyed lookup-hash default — the data layer goes tri-dialect",
|
|
2211
|
+
"summary": "This release makes the framework's data layer dialect-portable. `b.sql` is a new chainable query builder that quotes every identifier by construction, binds every value as a placeholder, and emits dialect-correct SQL for SQLite, Postgres, and MySQL; `b.guardSql` validates result rows against NUL bytes, quote-jump sequences, and per-column / total size boundaries. The entire framework data layer — the signed audit chain, cluster leadership and fencing, sessions, break-glass, the local queue, cache, scheduler, migrations, consent, and mail storage — is rebuilt on `b.sql`, which makes MySQL a supported external-database backend alongside Postgres and SQLite. Running the data layer on a real Postgres server surfaced two latent correctness bugs that only a non-SQLite backend exposes: identifiers were emitted unquoted at DDL time but read back camelCase (Postgres folds unquoted names to lowercase, silently breaking the audit chain, consent chain, cluster fencing, and vault-key consistency), and sealed columns coerced Buffer / object payloads through `String()` before encryption (corrupting non-string ciphertext); both are fixed by quoting every identifier and coercing per the column's declared type. Derived-hash columns — the blind-index lookup columns registered through `registerTable` — now default to a keyed MAC (SHAKE256 under the vault's per-deployment MAC key) instead of an unkeyed salted hash, so an exfiltrated database can no longer be brute-forced or rainbow-tabled to recover the indexed values; existing salted-hash indexes are read through a dual-read path and migrated forward, so the change is non-breaking. Audit-signing key rotation now preserves every historical checkpoint (rotation previously stranded checkpoints signed under the prior key). New cross-border data-residency postures (appi-jp, pdpa-sg, uk-gdpr) enforce a mandatory storage vacuum after erasure. Outbound TLS appends classical X25519 to its key-exchange preference so it can complete a handshake with a peer that advertises no post-quantum hybrid — previously the hybrids-only preference failed those handshakes outright — and emits a `tls.classical_downgrade` audit event whenever a connection lands on a classical group. Outbound HTTP/2 negotiation falls back to HTTP/1.1 against servers that only speak h1, the network heartbeat honors a target's permitted protocols instead of pinning cleartext targets down, a WebSocket connection closes cleanly on a peer's TCP half-close instead of wedging open, and the Azure blob backend encodes object-key path segments correctly.",
|
|
2212
|
+
"sections": [
|
|
2213
|
+
{
|
|
2214
|
+
"heading": "Added",
|
|
2215
|
+
"items": [
|
|
2216
|
+
{
|
|
2217
|
+
"title": "b.sql — a dialect-aware chainable SQL builder",
|
|
2218
|
+
"body": "`b.sql` composes SELECT / INSERT / UPDATE / UPSERT / DELETE / DDL from a fluent chain. Every identifier is validated and quoted by construction (`\"name\"` on SQLite/Postgres, `` `name` `` on MySQL), every value binds as a placeholder rather than interpolating, and the emitted statement is validated as a single balanced, single-statement query before it leaves the builder. Pass `{ dialect: \"postgres\" | \"mysql\" | \"sqlite\" }` (default SQLite) to target a backend; `upsert` emits the dialect-final conflict syntax. It composes `b.safeSql` for the identifier and placeholder primitives, so a SQL string can no longer be assembled by hand inside the framework."
|
|
2219
|
+
},
|
|
2220
|
+
{
|
|
2221
|
+
"title": "b.guardSql — result-row output validation",
|
|
2222
|
+
"body": "`b.guardSql` gates query result rows against embedded NUL bytes, quote-jump sequences, and configurable per-column and total-size boundaries, with the standard guard profiles (strict / balanced / permissive) and compliance postures (hipaa / pci-dss / gdpr / soc2). It is the output-side complement to the input-side `b.safeSql`."
|
|
2223
|
+
},
|
|
2224
|
+
{
|
|
2225
|
+
"title": "MySQL is a first-class data backend",
|
|
2226
|
+
"body": "MySQL joins Postgres and SQLite as a supported external-database backend. The framework's schema reconciler emits MySQL DDL, and the full data layer — signed audit chain (append + verify), cluster leadership and lease fencing, sessions, break-glass, the local queue, cache, scheduler, migrations, and consent — runs against a real MySQL server. Select the backend at `b.cluster.init` / `b.externalDb` configuration via the `dialect` option; `b.clusterStorage.dialect()` exposes the configured backend dialect to composing code."
|
|
2227
|
+
},
|
|
2228
|
+
{
|
|
2229
|
+
"title": "Cross-border erasure postures: appi-jp, pdpa-sg, uk-gdpr",
|
|
2230
|
+
"body": "Three data-residency compliance postures are added (Japan APPI, Singapore PDPA, UK GDPR). Each requires a mandatory storage vacuum after erasure (so deleted rows are reclaimed from the page store, not just tombstoned), a signed audit chain, encrypted backups, and a minimum TLS version. Pin one with `b.compliance.set`."
|
|
2231
|
+
}
|
|
2232
|
+
]
|
|
2233
|
+
},
|
|
2234
|
+
{
|
|
2235
|
+
"heading": "Security",
|
|
2236
|
+
"items": [
|
|
2237
|
+
{
|
|
2238
|
+
"title": "Lookup-hash columns default to a keyed MAC",
|
|
2239
|
+
"body": "Derived-hash (blind-index) columns registered through `registerTable` now default to `derivedHashMode: \"hmac-shake256\"` — SHAKE256 keyed with the vault's per-deployment MAC key — instead of the previous unkeyed `salted-sha3`. The index value is no longer recomputable from the indexed plaintext alone, so an attacker who exfiltrates the database cannot brute-force or rainbow-table a lookup column (for example a subject-email index) without also holding the vault MAC key (CWE-916 / CWE-759). Existing `salted-sha3` indexes are read through a dual-read path and re-derived on write, so deployments upgrade without re-indexing up front."
|
|
2240
|
+
},
|
|
2241
|
+
{
|
|
2242
|
+
"title": "Postgres identifier casing no longer breaks the audit and cluster chains",
|
|
2243
|
+
"body": "Identifiers were written unquoted in DDL but read back in camelCase. On Postgres (which folds an unquoted identifier to lowercase) this silently desynchronized the signed audit chain, the consent chain, cluster leadership and fencing, and vault-key consistency — each reads a column the server had stored under a lowercased name. Every framework identifier is now quoted at both DDL and query time so the stored and read names match on every dialect (CWE-670). SQLite deployments were unaffected and remain byte-compatible."
|
|
2244
|
+
},
|
|
2245
|
+
{
|
|
2246
|
+
"title": "Sealed columns preserve non-string payloads",
|
|
2247
|
+
"body": "A sealed column coerced its value through `String()` before encryption, corrupting Buffer and object payloads (a Buffer became `\"[object Object]\"`-class garbage, an object its `toString`). Sealed values are now encoded per the column's declared type before the seal, so binary and structured payloads round-trip intact (CWE-704)."
|
|
2248
|
+
},
|
|
2249
|
+
{
|
|
2250
|
+
"title": "Audit-signing key rotation preserves historical checkpoints",
|
|
2251
|
+
"body": "Rotating the audit-signing key stranded every checkpoint signed under the prior key — `verifyCheckpoints` ignored the per-fingerprint history file the rotation writes, so post-rotation verification failed on otherwise-valid historical checkpoints. Verification now resolves each checkpoint's signing key by fingerprint (`getPublicKeyByFingerprint`) across the rotation history, so the full chain verifies after a key rotation."
|
|
2252
|
+
},
|
|
2253
|
+
{
|
|
2254
|
+
"title": "Outbound TLS reaches classical-only peers and audits the downgrade",
|
|
2255
|
+
"body": "The framework's outbound TLS offered only ML-KEM hybrid key-exchange groups, so a handshake with a peer that does not advertise a post-quantum hybrid — most of today's internet — failed outright (`handshake_failure`), leaving outbound connections to webhooks, OAuth providers, ACME directories, object stores, and DoT/DoH/SMTP/Redis-over-TLS unable to complete. Classical X25519 is now appended to the group preference so the hybrid is still negotiated whenever the peer supports it, and the connection completes over classical X25519 when it does not. Every connection that lands on a classical group rather than the post-quantum hybrid emits a `tls.classical_downgrade` audit event (carrying the negotiated group) so operators can observe and alert on which peers are not yet post-quantum-capable."
|
|
2256
|
+
},
|
|
2257
|
+
{
|
|
2258
|
+
"title": "Transport reachability and correctness",
|
|
2259
|
+
"body": "Outbound HTTP/2 negotiation now falls back to HTTP/1.1 when a TLS server offers only h1 (an ALPN protocol_version alert no longer fails the request). The network heartbeat honors a target's permitted protocols instead of dropping non-default ones, so a cleartext `http://` health target is no longer reported permanently down. A WebSocket connection closes cleanly when a peer half-closes its TCP socket (a bare FIN) instead of wedging the connection open. The Azure blob backend percent-encodes each object-key path segment, so a key containing reserved characters can no longer corrupt the request URL."
|
|
2260
|
+
}
|
|
2261
|
+
]
|
|
2262
|
+
},
|
|
2263
|
+
{
|
|
2264
|
+
"heading": "Fixed",
|
|
2265
|
+
"items": [
|
|
2266
|
+
{
|
|
2267
|
+
"title": "Cross-border erasure performs the mandatory vacuum",
|
|
2268
|
+
"body": "Erasure under the uk-gdpr, appi-jp, and pdpa-sg residency postures now runs the storage vacuum the posture mandates, reclaiming erased rows from the page store rather than leaving them recoverable as free-list tombstones."
|
|
2269
|
+
}
|
|
2270
|
+
]
|
|
2271
|
+
},
|
|
2272
|
+
{
|
|
2273
|
+
"heading": "Migration",
|
|
2274
|
+
"items": [
|
|
2275
|
+
{
|
|
2276
|
+
"title": "Derived-hash default change is non-breaking; MySQL is opt-in",
|
|
2277
|
+
"body": "Lookup-hash columns default to the keyed MAC on new writes and migrate existing rows on access through the dual-read path — no upfront re-indexing, no operator action required. To pin the previous unkeyed index (for example to keep a column byte-compatible with an external system), pass `derivedHashMode: \"salted-sha3\"` to `registerTable`. MySQL as a data backend is opt-in: existing SQLite and Postgres deployments are unchanged unless you set `dialect: \"mysql\"`. The Postgres identifier-casing and sealed-column-coercion fixes change the emitted DDL and the at-rest encoding of non-string sealed values; a Postgres deployment created before this release reconciles its schema to the quoted identifiers on the next schema-ensure pass."
|
|
2278
|
+
}
|
|
2279
|
+
]
|
|
2280
|
+
}
|
|
2281
|
+
]
|
|
2282
|
+
}
|
|
2283
|
+
]
|
|
2284
|
+
}
|