@blamejs/blamejs-shop 0.0.44
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 +87 -0
- package/LICENSE +17 -0
- package/README.md +117 -0
- package/SECURITY.md +139 -0
- package/lib/admin.js +952 -0
- package/lib/analytics.js +267 -0
- package/lib/cart.js +279 -0
- package/lib/catalog-import.js +344 -0
- package/lib/catalog.js +769 -0
- package/lib/checkout.js +320 -0
- package/lib/config.js +151 -0
- package/lib/customers.js +322 -0
- package/lib/email.js +242 -0
- package/lib/externaldb-d1.js +283 -0
- package/lib/index.js +57 -0
- package/lib/inventory-alerts.js +198 -0
- package/lib/newsletter.js +142 -0
- package/lib/order.js +380 -0
- package/lib/payment.js +318 -0
- package/lib/pricing.js +185 -0
- package/lib/r2-bridge.js +169 -0
- package/lib/shipping.js +185 -0
- package/lib/storefront.js +2160 -0
- package/lib/subscriptions.js +410 -0
- package/lib/tax.js +161 -0
- package/lib/theme.js +194 -0
- package/lib/vendor/MANIFEST.json +19 -0
- package/lib/vendor/blamejs/.clusterfuzzlite/Dockerfile +23 -0
- package/lib/vendor/blamejs/.clusterfuzzlite/build.sh +34 -0
- package/lib/vendor/blamejs/.clusterfuzzlite/project.yaml +16 -0
- package/lib/vendor/blamejs/.dockerignore +45 -0
- package/lib/vendor/blamejs/.gitattributes +42 -0
- package/lib/vendor/blamejs/.github/CODEOWNERS +4 -0
- package/lib/vendor/blamejs/.github/FUNDING.yml +2 -0
- package/lib/vendor/blamejs/.github/ISSUE_TEMPLATE/bug_report.md +58 -0
- package/lib/vendor/blamejs/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/lib/vendor/blamejs/.github/ISSUE_TEMPLATE/feature_request.md +99 -0
- package/lib/vendor/blamejs/.github/PULL_REQUEST_TEMPLATE.md +77 -0
- package/lib/vendor/blamejs/.github/dependabot.yml +37 -0
- package/lib/vendor/blamejs/.github/workflows/actions-lint.yml +148 -0
- package/lib/vendor/blamejs/.github/workflows/cflite_batch.yml +107 -0
- package/lib/vendor/blamejs/.github/workflows/cflite_pr.yml +122 -0
- package/lib/vendor/blamejs/.github/workflows/ci.yml +511 -0
- package/lib/vendor/blamejs/.github/workflows/codeql.yml +50 -0
- package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +655 -0
- package/lib/vendor/blamejs/.github/workflows/release-container.yml +406 -0
- package/lib/vendor/blamejs/.github/workflows/scorecard.yml +101 -0
- package/lib/vendor/blamejs/.github/workflows/sha-to-tag-verify.yml +134 -0
- package/lib/vendor/blamejs/.gitignore +102 -0
- package/lib/vendor/blamejs/.gitleaks.toml +166 -0
- package/lib/vendor/blamejs/.hadolint.yaml +18 -0
- package/lib/vendor/blamejs/.npmrc +5 -0
- package/lib/vendor/blamejs/.pinact.yaml +17 -0
- package/lib/vendor/blamejs/ARCHITECTURE.md +158 -0
- package/lib/vendor/blamejs/CHANGELOG.md +1351 -0
- package/lib/vendor/blamejs/CODE_OF_CONDUCT.md +86 -0
- package/lib/vendor/blamejs/CONTRIBUTING.md +156 -0
- package/lib/vendor/blamejs/GOVERNANCE.md +201 -0
- package/lib/vendor/blamejs/LICENSE +201 -0
- package/lib/vendor/blamejs/LTS-CALENDAR.md +29 -0
- package/lib/vendor/blamejs/MIGRATING.md +29 -0
- package/lib/vendor/blamejs/NOTICE +81 -0
- package/lib/vendor/blamejs/README.md +304 -0
- package/lib/vendor/blamejs/SECURITY.md +432 -0
- package/lib/vendor/blamejs/api-snapshot.json +48709 -0
- package/lib/vendor/blamejs/assets/BlameJS_Logo.png +0 -0
- package/lib/vendor/blamejs/assets/BlameJS_Logo.svg +129 -0
- package/lib/vendor/blamejs/bench/README.md +77 -0
- package/lib/vendor/blamejs/bench/_helpers.js +70 -0
- package/lib/vendor/blamejs/bench/baseline.json +183 -0
- package/lib/vendor/blamejs/bench/crypto-hash.bench.js +19 -0
- package/lib/vendor/blamejs/bench/crypto-symmetric.bench.js +28 -0
- package/lib/vendor/blamejs/bench/run.js +140 -0
- package/lib/vendor/blamejs/bench/safe-json.bench.js +31 -0
- package/lib/vendor/blamejs/bin/blamejs.js +13 -0
- package/lib/vendor/blamejs/docker/caddy/Caddyfile +46 -0
- package/lib/vendor/blamejs/docker/coredns/Corefile +37 -0
- package/lib/vendor/blamejs/docker/haproxy/haproxy.cfg +52 -0
- package/lib/vendor/blamejs/docker/init/generate-certs.sh +118 -0
- package/lib/vendor/blamejs/docker/keycloak/realm-blamejs-test.json +87 -0
- package/lib/vendor/blamejs/docker/mitmproxy/config.yaml +16 -0
- package/lib/vendor/blamejs/docker/mongo/init-tls.sh +17 -0
- package/lib/vendor/blamejs/docker/mysql/my.cnf +12 -0
- package/lib/vendor/blamejs/docker/nats/nats.conf +33 -0
- package/lib/vendor/blamejs/docker/postgres/init-tls.sh +17 -0
- package/lib/vendor/blamejs/docker/postgres/postgresql.conf +18 -0
- package/lib/vendor/blamejs/docker/rabbitmq/rabbitmq.conf +18 -0
- package/lib/vendor/blamejs/docker/redis/redis.conf +15 -0
- package/lib/vendor/blamejs/docker/squid/squid.conf +24 -0
- package/lib/vendor/blamejs/docker/syslog/syslog-ng.conf +34 -0
- package/lib/vendor/blamejs/docker-compose.test.yml +545 -0
- package/lib/vendor/blamejs/docs/cis-postgres-crosswalk.md +102 -0
- package/lib/vendor/blamejs/docs/cis-sqlite-equivalent.md +92 -0
- package/lib/vendor/blamejs/eslint.config.mjs +204 -0
- package/lib/vendor/blamejs/examples/wiki/Caddyfile +40 -0
- package/lib/vendor/blamejs/examples/wiki/DEPLOY.md +218 -0
- package/lib/vendor/blamejs/examples/wiki/Dockerfile +120 -0
- package/lib/vendor/blamejs/examples/wiki/README.md +157 -0
- package/lib/vendor/blamejs/examples/wiki/cli-snapshot.json +250 -0
- package/lib/vendor/blamejs/examples/wiki/docker-compose.prod.yml +231 -0
- package/lib/vendor/blamejs/examples/wiki/docker-compose.yml +166 -0
- package/lib/vendor/blamejs/examples/wiki/env-snapshot.json +217 -0
- package/lib/vendor/blamejs/examples/wiki/lib/auto-site-entries.js +139 -0
- package/lib/vendor/blamejs/examples/wiki/lib/build-app.js +555 -0
- package/lib/vendor/blamejs/examples/wiki/lib/harvest-cli.js +507 -0
- package/lib/vendor/blamejs/examples/wiki/lib/harvest-env-vars.js +435 -0
- package/lib/vendor/blamejs/examples/wiki/lib/harvest-errors.js +282 -0
- package/lib/vendor/blamejs/examples/wiki/lib/harvest-vendored-deps.js +321 -0
- package/lib/vendor/blamejs/examples/wiki/lib/nav.js +15 -0
- package/lib/vendor/blamejs/examples/wiki/lib/opts-resolver.js +75 -0
- package/lib/vendor/blamejs/examples/wiki/lib/page-generator.js +508 -0
- package/lib/vendor/blamejs/examples/wiki/lib/section.js +276 -0
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +587 -0
- package/lib/vendor/blamejs/examples/wiki/lib/source-doc-parser.js +318 -0
- package/lib/vendor/blamejs/examples/wiki/lib/symbol-index.js +122 -0
- package/lib/vendor/blamejs/examples/wiki/migrations/0001-pages-schema.js +74 -0
- package/lib/vendor/blamejs/examples/wiki/package.json +18 -0
- package/lib/vendor/blamejs/examples/wiki/public/img/blamejs-logo.png +0 -0
- package/lib/vendor/blamejs/examples/wiki/public/img/blamejs-logo.svg +129 -0
- package/lib/vendor/blamejs/examples/wiki/public/robots.txt +5 -0
- package/lib/vendor/blamejs/examples/wiki/public/vendor/MANIFEST.json +30 -0
- package/lib/vendor/blamejs/examples/wiki/public/vendor/prism.css +1 -0
- package/lib/vendor/blamejs/examples/wiki/public/vendor/prism.js +15 -0
- package/lib/vendor/blamejs/examples/wiki/public/wiki.css +1250 -0
- package/lib/vendor/blamejs/examples/wiki/routes/admin.js +366 -0
- package/lib/vendor/blamejs/examples/wiki/routes/integration.js +230 -0
- package/lib/vendor/blamejs/examples/wiki/routes/pages.js +266 -0
- package/lib/vendor/blamejs/examples/wiki/scripts/backfill-module-metadata.js +214 -0
- package/lib/vendor/blamejs/examples/wiki/seeders/prod/0001-default-pages.js +35 -0
- package/lib/vendor/blamejs/examples/wiki/seeders/prod/pages/_index.js +34 -0
- package/lib/vendor/blamejs/examples/wiki/seeders/prod/pages/api.js +76 -0
- package/lib/vendor/blamejs/examples/wiki/server.js +129 -0
- package/lib/vendor/blamejs/examples/wiki/site.config.js +197 -0
- package/lib/vendor/blamejs/examples/wiki/snippets/README.md +38 -0
- package/lib/vendor/blamejs/examples/wiki/snippets/auth/password-hash.example.js +15 -0
- package/lib/vendor/blamejs/examples/wiki/src/editor.js +103 -0
- package/lib/vendor/blamejs/examples/wiki/src/wiki.js +349 -0
- package/lib/vendor/blamejs/examples/wiki/test/AUDIT.md +155 -0
- package/lib/vendor/blamejs/examples/wiki/test/codebase-patterns.test.js +594 -0
- package/lib/vendor/blamejs/examples/wiki/test/e2e.js +741 -0
- package/lib/vendor/blamejs/examples/wiki/test/find-missing-pages.js +254 -0
- package/lib/vendor/blamejs/examples/wiki/test/integration.js +391 -0
- package/lib/vendor/blamejs/examples/wiki/test/validate-cli-snapshot.js +379 -0
- package/lib/vendor/blamejs/examples/wiki/test/validate-env-snapshot.js +346 -0
- package/lib/vendor/blamejs/examples/wiki/test/validate-nav-coverage.js +212 -0
- package/lib/vendor/blamejs/examples/wiki/test/validate-site-coverage.js +252 -0
- package/lib/vendor/blamejs/examples/wiki/test/validate-source-comment-blocks.js +107 -0
- package/lib/vendor/blamejs/examples/wiki/views/_layout.html +115 -0
- package/lib/vendor/blamejs/examples/wiki/views/admin/api-keys.html +51 -0
- package/lib/vendor/blamejs/examples/wiki/views/admin/dashboard.html +22 -0
- package/lib/vendor/blamejs/examples/wiki/views/admin/edit.html +17 -0
- package/lib/vendor/blamejs/examples/wiki/views/home.html +85 -0
- package/lib/vendor/blamejs/examples/wiki/views/login.html +18 -0
- package/lib/vendor/blamejs/examples/wiki/views/page.html +5 -0
- package/lib/vendor/blamejs/examples/wiki/views/partials/nav.html +13 -0
- package/lib/vendor/blamejs/examples/wiki/views/search.html +19 -0
- package/lib/vendor/blamejs/examples/wiki/wiki.config.js +15 -0
- package/lib/vendor/blamejs/fuzz/README.md +137 -0
- package/lib/vendor/blamejs/fuzz/_expected.js +35 -0
- package/lib/vendor/blamejs/fuzz/guard-agent-registry.fuzz.js +22 -0
- package/lib/vendor/blamejs/fuzz/guard-csv.fuzz.js +16 -0
- package/lib/vendor/blamejs/fuzz/guard-csv_seed_corpus/01-basic.csv +3 -0
- package/lib/vendor/blamejs/fuzz/guard-csv_seed_corpus/02-formula.csv +1 -0
- package/lib/vendor/blamejs/fuzz/guard-csv_seed_corpus/03-hyperlink.csv +1 -0
- package/lib/vendor/blamejs/fuzz/guard-dsn.fuzz.js +22 -0
- package/lib/vendor/blamejs/fuzz/guard-email.fuzz.js +16 -0
- package/lib/vendor/blamejs/fuzz/guard-email_seed_corpus/01-basic.eml +5 -0
- package/lib/vendor/blamejs/fuzz/guard-envelope.fuzz.js +24 -0
- package/lib/vendor/blamejs/fuzz/guard-event-bus-payload.fuzz.js +24 -0
- package/lib/vendor/blamejs/fuzz/guard-event-bus-topic.fuzz.js +20 -0
- package/lib/vendor/blamejs/fuzz/guard-html.fuzz.js +16 -0
- package/lib/vendor/blamejs/fuzz/guard-html_seed_corpus/01-basic.html +1 -0
- package/lib/vendor/blamejs/fuzz/guard-html_seed_corpus/02-script.html +1 -0
- package/lib/vendor/blamejs/fuzz/guard-html_seed_corpus/03-event.html +1 -0
- package/lib/vendor/blamejs/fuzz/guard-html_seed_corpus/04-jsurl.html +1 -0
- package/lib/vendor/blamejs/fuzz/guard-idempotency-key.fuzz.js +20 -0
- package/lib/vendor/blamejs/fuzz/guard-imap-command.fuzz.js +35 -0
- package/lib/vendor/blamejs/fuzz/guard-jmap.fuzz.js +41 -0
- package/lib/vendor/blamejs/fuzz/guard-json.fuzz.js +16 -0
- package/lib/vendor/blamejs/fuzz/guard-json_seed_corpus/01-basic.json +1 -0
- package/lib/vendor/blamejs/fuzz/guard-json_seed_corpus/02-proto.json +1 -0
- package/lib/vendor/blamejs/fuzz/guard-json_seed_corpus/03-dupkey.json +1 -0
- package/lib/vendor/blamejs/fuzz/guard-json_seed_corpus/04-nan.json +1 -0
- package/lib/vendor/blamejs/fuzz/guard-json_seed_corpus/05-bom.json +1 -0
- package/lib/vendor/blamejs/fuzz/guard-list-id.fuzz.js +21 -0
- package/lib/vendor/blamejs/fuzz/guard-list-unsubscribe.fuzz.js +25 -0
- package/lib/vendor/blamejs/fuzz/guard-mail-compose.fuzz.js +22 -0
- package/lib/vendor/blamejs/fuzz/guard-mail-move.fuzz.js +22 -0
- package/lib/vendor/blamejs/fuzz/guard-mail-query.fuzz.js +27 -0
- package/lib/vendor/blamejs/fuzz/guard-mail-reply.fuzz.js +23 -0
- package/lib/vendor/blamejs/fuzz/guard-mail-sieve.fuzz.js +36 -0
- package/lib/vendor/blamejs/fuzz/guard-managesieve-command.fuzz.js +26 -0
- package/lib/vendor/blamejs/fuzz/guard-markdown.fuzz.js +16 -0
- package/lib/vendor/blamejs/fuzz/guard-markdown_seed_corpus/01-basic.md +2 -0
- package/lib/vendor/blamejs/fuzz/guard-markdown_seed_corpus/02-jsurl.md +1 -0
- package/lib/vendor/blamejs/fuzz/guard-markdown_seed_corpus/03-jsimg.md +1 -0
- package/lib/vendor/blamejs/fuzz/guard-message-id.fuzz.js +26 -0
- package/lib/vendor/blamejs/fuzz/guard-pop3-command.fuzz.js +23 -0
- package/lib/vendor/blamejs/fuzz/guard-posture-chain.fuzz.js +22 -0
- package/lib/vendor/blamejs/fuzz/guard-saga-config.fuzz.js +32 -0
- package/lib/vendor/blamejs/fuzz/guard-smtp-command.fuzz.js +27 -0
- package/lib/vendor/blamejs/fuzz/guard-snapshot-envelope.fuzz.js +22 -0
- package/lib/vendor/blamejs/fuzz/guard-stream-args.fuzz.js +22 -0
- package/lib/vendor/blamejs/fuzz/guard-svg.fuzz.js +16 -0
- package/lib/vendor/blamejs/fuzz/guard-svg_seed_corpus/01-basic.svg +1 -0
- package/lib/vendor/blamejs/fuzz/guard-svg_seed_corpus/02-script.svg +1 -0
- package/lib/vendor/blamejs/fuzz/guard-tenant-id.fuzz.js +20 -0
- package/lib/vendor/blamejs/fuzz/guard-trace-context.fuzz.js +30 -0
- package/lib/vendor/blamejs/fuzz/guard-xml.fuzz.js +16 -0
- package/lib/vendor/blamejs/fuzz/guard-xml_seed_corpus/01-basic.xml +1 -0
- package/lib/vendor/blamejs/fuzz/guard-xml_seed_corpus/02-xxe.xml +1 -0
- package/lib/vendor/blamejs/fuzz/guard-yaml.fuzz.js +16 -0
- package/lib/vendor/blamejs/fuzz/guard-yaml_seed_corpus/01-basic.yaml +2 -0
- package/lib/vendor/blamejs/fuzz/guard-yaml_seed_corpus/02-anchor.yaml +2 -0
- package/lib/vendor/blamejs/fuzz/guard-yaml_seed_corpus/03-norway.yaml +1 -0
- package/lib/vendor/blamejs/fuzz/guard-yaml_seed_corpus/04-multidoc.yaml +4 -0
- package/lib/vendor/blamejs/fuzz/parsers__safe-ini.fuzz.js +16 -0
- package/lib/vendor/blamejs/fuzz/parsers__safe-ini_seed_corpus/01-basic.ini +2 -0
- package/lib/vendor/blamejs/fuzz/parsers__safe-toml.fuzz.js +16 -0
- package/lib/vendor/blamejs/fuzz/parsers__safe-toml_seed_corpus/01-basic.toml +4 -0
- package/lib/vendor/blamejs/fuzz/parsers__safe-xml.fuzz.js +16 -0
- package/lib/vendor/blamejs/fuzz/parsers__safe-xml_seed_corpus/01-basic.xml +1 -0
- package/lib/vendor/blamejs/fuzz/parsers__safe-yaml.fuzz.js +16 -0
- package/lib/vendor/blamejs/fuzz/parsers__safe-yaml_seed_corpus/01-basic.yaml +4 -0
- package/lib/vendor/blamejs/fuzz/safe-decompress.fuzz.js +49 -0
- package/lib/vendor/blamejs/fuzz/safe-dns.fuzz.js +29 -0
- package/lib/vendor/blamejs/fuzz/safe-ical.fuzz.js +16 -0
- package/lib/vendor/blamejs/fuzz/safe-icap.fuzz.js +42 -0
- package/lib/vendor/blamejs/fuzz/safe-json.fuzz.js +25 -0
- package/lib/vendor/blamejs/fuzz/safe-json_seed_corpus/01-object.txt +1 -0
- package/lib/vendor/blamejs/fuzz/safe-json_seed_corpus/02-array.txt +1 -0
- package/lib/vendor/blamejs/fuzz/safe-json_seed_corpus/03-string.txt +1 -0
- package/lib/vendor/blamejs/fuzz/safe-json_seed_corpus/04-proto.txt +1 -0
- package/lib/vendor/blamejs/fuzz/safe-json_seed_corpus/05-deep.txt +1 -0
- package/lib/vendor/blamejs/fuzz/safe-jsonpath.fuzz.js +16 -0
- package/lib/vendor/blamejs/fuzz/safe-jsonpath_seed_corpus/01-basic.txt +1 -0
- package/lib/vendor/blamejs/fuzz/safe-jsonpath_seed_corpus/02-filter.txt +1 -0
- package/lib/vendor/blamejs/fuzz/safe-jsonpath_seed_corpus/03-deepscan.txt +1 -0
- package/lib/vendor/blamejs/fuzz/safe-jsonpath_seed_corpus/04-slice.txt +1 -0
- package/lib/vendor/blamejs/fuzz/safe-mime.fuzz.js +27 -0
- package/lib/vendor/blamejs/fuzz/safe-mount-info.fuzz.js +33 -0
- package/lib/vendor/blamejs/fuzz/safe-sieve.fuzz.js +28 -0
- package/lib/vendor/blamejs/fuzz/safe-smtp.fuzz.js +64 -0
- package/lib/vendor/blamejs/fuzz/safe-url.fuzz.js +16 -0
- package/lib/vendor/blamejs/fuzz/safe-url_seed_corpus/01-basic.txt +1 -0
- package/lib/vendor/blamejs/fuzz/safe-url_seed_corpus/02-userinfo.txt +1 -0
- package/lib/vendor/blamejs/fuzz/safe-url_seed_corpus/03-dangerous.txt +1 -0
- package/lib/vendor/blamejs/fuzz/safe-url_seed_corpus/04-data.txt +1 -0
- package/lib/vendor/blamejs/fuzz/safe-url_seed_corpus/05-ipv6.txt +1 -0
- package/lib/vendor/blamejs/fuzz/safe-url_seed_corpus/06-idn.txt +1 -0
- package/lib/vendor/blamejs/fuzz/safe-vcard.fuzz.js +16 -0
- package/lib/vendor/blamejs/index.js +678 -0
- package/lib/vendor/blamejs/keys/release-pqc-pub.json +7 -0
- package/lib/vendor/blamejs/lib/_test/crypto-fixtures.js +67 -0
- package/lib/vendor/blamejs/lib/a2a-tasks.js +598 -0
- package/lib/vendor/blamejs/lib/a2a.js +407 -0
- package/lib/vendor/blamejs/lib/acme.js +1448 -0
- package/lib/vendor/blamejs/lib/agent-audit.js +45 -0
- package/lib/vendor/blamejs/lib/agent-event-bus.js +382 -0
- package/lib/vendor/blamejs/lib/agent-idempotency.js +497 -0
- package/lib/vendor/blamejs/lib/agent-orchestrator.js +717 -0
- package/lib/vendor/blamejs/lib/agent-posture-chain.js +366 -0
- package/lib/vendor/blamejs/lib/agent-saga.js +321 -0
- package/lib/vendor/blamejs/lib/agent-snapshot.js +676 -0
- package/lib/vendor/blamejs/lib/agent-stream.js +269 -0
- package/lib/vendor/blamejs/lib/agent-tenant.js +632 -0
- package/lib/vendor/blamejs/lib/agent-trace.js +281 -0
- package/lib/vendor/blamejs/lib/ai-adverse-decision.js +184 -0
- package/lib/vendor/blamejs/lib/ai-content-detect.js +268 -0
- package/lib/vendor/blamejs/lib/ai-input.js +201 -0
- package/lib/vendor/blamejs/lib/ai-model-manifest.js +363 -0
- package/lib/vendor/blamejs/lib/ai-pref.js +340 -0
- package/lib/vendor/blamejs/lib/api-key.js +721 -0
- package/lib/vendor/blamejs/lib/api-snapshot.js +458 -0
- package/lib/vendor/blamejs/lib/app-shutdown.js +557 -0
- package/lib/vendor/blamejs/lib/app.js +365 -0
- package/lib/vendor/blamejs/lib/archive.js +547 -0
- package/lib/vendor/blamejs/lib/arg-parser.js +697 -0
- package/lib/vendor/blamejs/lib/argon2-builtin.js +173 -0
- package/lib/vendor/blamejs/lib/asn1-der.js +424 -0
- package/lib/vendor/blamejs/lib/asyncapi-bindings.js +160 -0
- package/lib/vendor/blamejs/lib/asyncapi-traits.js +143 -0
- package/lib/vendor/blamejs/lib/asyncapi.js +575 -0
- package/lib/vendor/blamejs/lib/atomic-file.js +1023 -0
- package/lib/vendor/blamejs/lib/audit-chain.js +266 -0
- package/lib/vendor/blamejs/lib/audit-daily-review.js +389 -0
- package/lib/vendor/blamejs/lib/audit-sign.js +751 -0
- package/lib/vendor/blamejs/lib/audit-tools.js +1113 -0
- package/lib/vendor/blamejs/lib/audit.js +1671 -0
- package/lib/vendor/blamejs/lib/auth/aal.js +169 -0
- package/lib/vendor/blamejs/lib/auth/access-lock.js +220 -0
- package/lib/vendor/blamejs/lib/auth/acr-vocabulary.js +265 -0
- package/lib/vendor/blamejs/lib/auth/ato-kill-switch.js +112 -0
- package/lib/vendor/blamejs/lib/auth/auth-time-tracker.js +111 -0
- package/lib/vendor/blamejs/lib/auth/bot-challenge.js +573 -0
- package/lib/vendor/blamejs/lib/auth/ciba.js +637 -0
- package/lib/vendor/blamejs/lib/auth/dpop.js +516 -0
- package/lib/vendor/blamejs/lib/auth/elevation-grant.js +306 -0
- package/lib/vendor/blamejs/lib/auth/fal.js +229 -0
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +681 -0
- package/lib/vendor/blamejs/lib/auth/jwt-external.js +519 -0
- package/lib/vendor/blamejs/lib/auth/jwt.js +430 -0
- package/lib/vendor/blamejs/lib/auth/lockout.js +449 -0
- package/lib/vendor/blamejs/lib/auth/oauth.js +2141 -0
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +657 -0
- package/lib/vendor/blamejs/lib/auth/oid4vp.js +531 -0
- package/lib/vendor/blamejs/lib/auth/openid-federation.js +600 -0
- package/lib/vendor/blamejs/lib/auth/passkey.js +676 -0
- package/lib/vendor/blamejs/lib/auth/password.js +693 -0
- package/lib/vendor/blamejs/lib/auth/saml.js +2109 -0
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc-disclosure.js +95 -0
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc-holder.js +225 -0
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc-issuer.js +197 -0
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +728 -0
- package/lib/vendor/blamejs/lib/auth/status-list.js +272 -0
- package/lib/vendor/blamejs/lib/auth/step-up-policy.js +335 -0
- package/lib/vendor/blamejs/lib/auth/step-up.js +454 -0
- package/lib/vendor/blamejs/lib/auth-bot-challenge.js +505 -0
- package/lib/vendor/blamejs/lib/auth-header.js +148 -0
- package/lib/vendor/blamejs/lib/backup/bundle.js +265 -0
- package/lib/vendor/blamejs/lib/backup/crypto.js +176 -0
- package/lib/vendor/blamejs/lib/backup/index.js +1001 -0
- package/lib/vendor/blamejs/lib/backup/manifest.js +443 -0
- package/lib/vendor/blamejs/lib/boot-gates.js +174 -0
- package/lib/vendor/blamejs/lib/breach-deadline.js +272 -0
- package/lib/vendor/blamejs/lib/break-glass.js +1753 -0
- package/lib/vendor/blamejs/lib/budr.js +205 -0
- package/lib/vendor/blamejs/lib/bundler.js +461 -0
- package/lib/vendor/blamejs/lib/cache-redis.js +256 -0
- package/lib/vendor/blamejs/lib/cache-status.js +288 -0
- package/lib/vendor/blamejs/lib/cache.js +1331 -0
- package/lib/vendor/blamejs/lib/calendar.js +1240 -0
- package/lib/vendor/blamejs/lib/canonical-json.js +143 -0
- package/lib/vendor/blamejs/lib/cdn-cache-control.js +473 -0
- package/lib/vendor/blamejs/lib/cert.js +763 -0
- package/lib/vendor/blamejs/lib/chain-writer.js +259 -0
- package/lib/vendor/blamejs/lib/circuit-breaker.js +101 -0
- package/lib/vendor/blamejs/lib/cli-helpers.js +237 -0
- package/lib/vendor/blamejs/lib/cli.js +2328 -0
- package/lib/vendor/blamejs/lib/client-hints.js +318 -0
- package/lib/vendor/blamejs/lib/cloud-events.js +277 -0
- package/lib/vendor/blamejs/lib/cluster-provider-db.js +317 -0
- package/lib/vendor/blamejs/lib/cluster-storage.js +351 -0
- package/lib/vendor/blamejs/lib/cluster.js +1017 -0
- package/lib/vendor/blamejs/lib/cms-codec.js +826 -0
- package/lib/vendor/blamejs/lib/codepoint-class.js +262 -0
- package/lib/vendor/blamejs/lib/compliance-ai-act-logging.js +190 -0
- package/lib/vendor/blamejs/lib/compliance-ai-act-prohibited.js +205 -0
- package/lib/vendor/blamejs/lib/compliance-ai-act-risk.js +189 -0
- package/lib/vendor/blamejs/lib/compliance-ai-act-transparency.js +200 -0
- package/lib/vendor/blamejs/lib/compliance-ai-act.js +821 -0
- package/lib/vendor/blamejs/lib/compliance-eaa.js +204 -0
- package/lib/vendor/blamejs/lib/compliance-sanctions-aliases.js +167 -0
- package/lib/vendor/blamejs/lib/compliance-sanctions-fetcher.js +206 -0
- package/lib/vendor/blamejs/lib/compliance-sanctions-fuzzy.js +297 -0
- package/lib/vendor/blamejs/lib/compliance-sanctions.js +569 -0
- package/lib/vendor/blamejs/lib/compliance.js +1558 -0
- package/lib/vendor/blamejs/lib/config-drift.js +426 -0
- package/lib/vendor/blamejs/lib/config.js +446 -0
- package/lib/vendor/blamejs/lib/consent.js +369 -0
- package/lib/vendor/blamejs/lib/constants.js +209 -0
- package/lib/vendor/blamejs/lib/content-credentials.js +704 -0
- package/lib/vendor/blamejs/lib/cookies.js +560 -0
- package/lib/vendor/blamejs/lib/cra-report.js +299 -0
- package/lib/vendor/blamejs/lib/credential-hash.js +394 -0
- package/lib/vendor/blamejs/lib/crypto-field.js +1017 -0
- package/lib/vendor/blamejs/lib/crypto-hpke-pq.js +187 -0
- package/lib/vendor/blamejs/lib/crypto-hpke.js +256 -0
- package/lib/vendor/blamejs/lib/crypto.js +1908 -0
- package/lib/vendor/blamejs/lib/csp.js +271 -0
- package/lib/vendor/blamejs/lib/csv.js +418 -0
- package/lib/vendor/blamejs/lib/daemon.js +481 -0
- package/lib/vendor/blamejs/lib/dark-patterns.js +488 -0
- package/lib/vendor/blamejs/lib/data-act.js +328 -0
- package/lib/vendor/blamejs/lib/db-collection.js +587 -0
- package/lib/vendor/blamejs/lib/db-declare-row-policy.js +267 -0
- package/lib/vendor/blamejs/lib/db-declare-view.js +420 -0
- package/lib/vendor/blamejs/lib/db-file-lifecycle.js +333 -0
- package/lib/vendor/blamejs/lib/db-query.js +802 -0
- package/lib/vendor/blamejs/lib/db-role-context.js +50 -0
- package/lib/vendor/blamejs/lib/db-schema.js +322 -0
- package/lib/vendor/blamejs/lib/db.js +3111 -0
- package/lib/vendor/blamejs/lib/dbsc.js +299 -0
- package/lib/vendor/blamejs/lib/ddl-change-control.js +523 -0
- package/lib/vendor/blamejs/lib/deprecate.js +377 -0
- package/lib/vendor/blamejs/lib/dev.js +405 -0
- package/lib/vendor/blamejs/lib/dora.js +402 -0
- package/lib/vendor/blamejs/lib/dr-runbook.js +368 -0
- package/lib/vendor/blamejs/lib/dsr.js +1188 -0
- package/lib/vendor/blamejs/lib/dual-control.js +526 -0
- package/lib/vendor/blamejs/lib/early-hints.js +212 -0
- package/lib/vendor/blamejs/lib/error-page.js +420 -0
- package/lib/vendor/blamejs/lib/events.js +214 -0
- package/lib/vendor/blamejs/lib/external-db-migrate.js +659 -0
- package/lib/vendor/blamejs/lib/external-db.js +1877 -0
- package/lib/vendor/blamejs/lib/fapi2.js +394 -0
- package/lib/vendor/blamejs/lib/fda-21cfr11.js +395 -0
- package/lib/vendor/blamejs/lib/fdx.js +370 -0
- package/lib/vendor/blamejs/lib/fedcm.js +264 -0
- package/lib/vendor/blamejs/lib/file-type.js +360 -0
- package/lib/vendor/blamejs/lib/file-upload.js +1256 -0
- package/lib/vendor/blamejs/lib/flag-cache.js +136 -0
- package/lib/vendor/blamejs/lib/flag-evaluation-context.js +135 -0
- package/lib/vendor/blamejs/lib/flag-providers.js +279 -0
- package/lib/vendor/blamejs/lib/flag-targeting.js +210 -0
- package/lib/vendor/blamejs/lib/flag.js +346 -0
- package/lib/vendor/blamejs/lib/forms.js +525 -0
- package/lib/vendor/blamejs/lib/framework-error.js +724 -0
- package/lib/vendor/blamejs/lib/framework-schema.js +845 -0
- package/lib/vendor/blamejs/lib/framework-sha1-hibp.js +34 -0
- package/lib/vendor/blamejs/lib/fsm.js +469 -0
- package/lib/vendor/blamejs/lib/gate-contract.js +1661 -0
- package/lib/vendor/blamejs/lib/gdpr-ropa.js +261 -0
- package/lib/vendor/blamejs/lib/graphql-federation.js +234 -0
- package/lib/vendor/blamejs/lib/guard-agent-registry.js +179 -0
- package/lib/vendor/blamejs/lib/guard-all.js +555 -0
- package/lib/vendor/blamejs/lib/guard-archive.js +901 -0
- package/lib/vendor/blamejs/lib/guard-auth.js +451 -0
- package/lib/vendor/blamejs/lib/guard-cidr.js +676 -0
- package/lib/vendor/blamejs/lib/guard-csv.js +1176 -0
- package/lib/vendor/blamejs/lib/guard-domain.js +814 -0
- package/lib/vendor/blamejs/lib/guard-dsn.js +382 -0
- package/lib/vendor/blamejs/lib/guard-email.js +951 -0
- package/lib/vendor/blamejs/lib/guard-envelope.js +294 -0
- package/lib/vendor/blamejs/lib/guard-event-bus-payload.js +217 -0
- package/lib/vendor/blamejs/lib/guard-event-bus-topic.js +150 -0
- package/lib/vendor/blamejs/lib/guard-filename.js +956 -0
- package/lib/vendor/blamejs/lib/guard-graphql.js +731 -0
- package/lib/vendor/blamejs/lib/guard-html-wcag-aria.js +164 -0
- package/lib/vendor/blamejs/lib/guard-html-wcag-forms.js +144 -0
- package/lib/vendor/blamejs/lib/guard-html-wcag-tables.js +154 -0
- package/lib/vendor/blamejs/lib/guard-html-wcag-tagwalk.js +44 -0
- package/lib/vendor/blamejs/lib/guard-html-wcag.js +470 -0
- package/lib/vendor/blamejs/lib/guard-html.js +1209 -0
- package/lib/vendor/blamejs/lib/guard-idempotency-key.js +151 -0
- package/lib/vendor/blamejs/lib/guard-image.js +584 -0
- package/lib/vendor/blamejs/lib/guard-imap-command.js +337 -0
- package/lib/vendor/blamejs/lib/guard-jmap.js +321 -0
- package/lib/vendor/blamejs/lib/guard-json.js +935 -0
- package/lib/vendor/blamejs/lib/guard-jsonpath.js +512 -0
- package/lib/vendor/blamejs/lib/guard-jwt.js +772 -0
- package/lib/vendor/blamejs/lib/guard-list-id.js +318 -0
- package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +412 -0
- package/lib/vendor/blamejs/lib/guard-mail-compose.js +282 -0
- package/lib/vendor/blamejs/lib/guard-mail-move.js +202 -0
- package/lib/vendor/blamejs/lib/guard-mail-query.js +310 -0
- package/lib/vendor/blamejs/lib/guard-mail-reply.js +172 -0
- package/lib/vendor/blamejs/lib/guard-mail-sieve.js +207 -0
- package/lib/vendor/blamejs/lib/guard-managesieve-command.js +566 -0
- package/lib/vendor/blamejs/lib/guard-markdown.js +768 -0
- package/lib/vendor/blamejs/lib/guard-message-id.js +267 -0
- package/lib/vendor/blamejs/lib/guard-mime.js +609 -0
- package/lib/vendor/blamejs/lib/guard-oauth.js +650 -0
- package/lib/vendor/blamejs/lib/guard-pdf.js +569 -0
- package/lib/vendor/blamejs/lib/guard-pop3-command.js +317 -0
- package/lib/vendor/blamejs/lib/guard-posture-chain.js +201 -0
- package/lib/vendor/blamejs/lib/guard-regex.js +632 -0
- package/lib/vendor/blamejs/lib/guard-saga-config.js +157 -0
- package/lib/vendor/blamejs/lib/guard-shell.js +522 -0
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +594 -0
- package/lib/vendor/blamejs/lib/guard-snapshot-envelope.js +168 -0
- package/lib/vendor/blamejs/lib/guard-stream-args.js +166 -0
- package/lib/vendor/blamejs/lib/guard-svg.js +1163 -0
- package/lib/vendor/blamejs/lib/guard-template.js +490 -0
- package/lib/vendor/blamejs/lib/guard-tenant-id.js +138 -0
- package/lib/vendor/blamejs/lib/guard-time.js +586 -0
- package/lib/vendor/blamejs/lib/guard-trace-context.js +172 -0
- package/lib/vendor/blamejs/lib/guard-uuid.js +548 -0
- package/lib/vendor/blamejs/lib/guard-xml.js +666 -0
- package/lib/vendor/blamejs/lib/guard-yaml.js +726 -0
- package/lib/vendor/blamejs/lib/hal.js +125 -0
- package/lib/vendor/blamejs/lib/handlers.js +350 -0
- package/lib/vendor/blamejs/lib/honeytoken.js +168 -0
- package/lib/vendor/blamejs/lib/html-balance.js +347 -0
- package/lib/vendor/blamejs/lib/http-client-cache.js +923 -0
- package/lib/vendor/blamejs/lib/http-client-cookie-jar.js +519 -0
- package/lib/vendor/blamejs/lib/http-client.js +2152 -0
- package/lib/vendor/blamejs/lib/http-message-signature.js +589 -0
- package/lib/vendor/blamejs/lib/http2-teardown.js +34 -0
- package/lib/vendor/blamejs/lib/i18n-messageformat.js +398 -0
- package/lib/vendor/blamejs/lib/i18n.js +931 -0
- package/lib/vendor/blamejs/lib/iab-mspa.js +257 -0
- package/lib/vendor/blamejs/lib/iab-tcf.js +461 -0
- package/lib/vendor/blamejs/lib/importmap-integrity.js +90 -0
- package/lib/vendor/blamejs/lib/inbox.js +435 -0
- package/lib/vendor/blamejs/lib/incident-report.js +314 -0
- package/lib/vendor/blamejs/lib/ip-utils.js +102 -0
- package/lib/vendor/blamejs/lib/jobs.js +185 -0
- package/lib/vendor/blamejs/lib/jose-jwe-experimental.js +228 -0
- package/lib/vendor/blamejs/lib/jsonapi.js +230 -0
- package/lib/vendor/blamejs/lib/keychain.js +865 -0
- package/lib/vendor/blamejs/lib/lazy-require.js +48 -0
- package/lib/vendor/blamejs/lib/legal-hold.js +374 -0
- package/lib/vendor/blamejs/lib/local-db-thin.js +321 -0
- package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +369 -0
- package/lib/vendor/blamejs/lib/log-stream-local.js +146 -0
- package/lib/vendor/blamejs/lib/log-stream-otlp-grpc.js +410 -0
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +286 -0
- package/lib/vendor/blamejs/lib/log-stream-syslog.js +310 -0
- package/lib/vendor/blamejs/lib/log-stream-webhook.js +199 -0
- package/lib/vendor/blamejs/lib/log-stream.js +584 -0
- package/lib/vendor/blamejs/lib/log.js +625 -0
- package/lib/vendor/blamejs/lib/lro.js +200 -0
- package/lib/vendor/blamejs/lib/mail-agent.js +786 -0
- package/lib/vendor/blamejs/lib/mail-arc-sign.js +417 -0
- package/lib/vendor/blamejs/lib/mail-arf.js +343 -0
- package/lib/vendor/blamejs/lib/mail-auth.js +2144 -0
- package/lib/vendor/blamejs/lib/mail-bimi.js +1047 -0
- package/lib/vendor/blamejs/lib/mail-bounce.js +955 -0
- package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +1286 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +789 -0
- package/lib/vendor/blamejs/lib/mail-crypto.js +108 -0
- package/lib/vendor/blamejs/lib/mail-dav.js +1224 -0
- package/lib/vendor/blamejs/lib/mail-deploy.js +1119 -0
- package/lib/vendor/blamejs/lib/mail-dkim.js +1250 -0
- package/lib/vendor/blamejs/lib/mail-greylist.js +448 -0
- package/lib/vendor/blamejs/lib/mail-helo.js +473 -0
- package/lib/vendor/blamejs/lib/mail-journal.js +435 -0
- package/lib/vendor/blamejs/lib/mail-mdn.js +424 -0
- package/lib/vendor/blamejs/lib/mail-rbl.js +392 -0
- package/lib/vendor/blamejs/lib/mail-require-tls.js +198 -0
- package/lib/vendor/blamejs/lib/mail-scan.js +502 -0
- package/lib/vendor/blamejs/lib/mail-send-deliver.js +629 -0
- package/lib/vendor/blamejs/lib/mail-server-imap.js +1858 -0
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +1565 -0
- package/lib/vendor/blamejs/lib/mail-server-managesieve.js +908 -0
- package/lib/vendor/blamejs/lib/mail-server-mx.js +969 -0
- package/lib/vendor/blamejs/lib/mail-server-pop3.js +915 -0
- package/lib/vendor/blamejs/lib/mail-server-rate-limit.js +315 -0
- package/lib/vendor/blamejs/lib/mail-server-registry.js +378 -0
- package/lib/vendor/blamejs/lib/mail-server-submission.js +1396 -0
- package/lib/vendor/blamejs/lib/mail-server-tls.js +445 -0
- package/lib/vendor/blamejs/lib/mail-sieve.js +557 -0
- package/lib/vendor/blamejs/lib/mail-spam-score.js +284 -0
- package/lib/vendor/blamejs/lib/mail-srs.js +248 -0
- package/lib/vendor/blamejs/lib/mail-store-fts.js +394 -0
- package/lib/vendor/blamejs/lib/mail-store.js +929 -0
- package/lib/vendor/blamejs/lib/mail-unsubscribe.js +400 -0
- package/lib/vendor/blamejs/lib/mail.js +1971 -0
- package/lib/vendor/blamejs/lib/mcp-tool-registry.js +473 -0
- package/lib/vendor/blamejs/lib/mcp.js +950 -0
- package/lib/vendor/blamejs/lib/metrics.js +1503 -0
- package/lib/vendor/blamejs/lib/middleware/age-gate.js +177 -0
- package/lib/vendor/blamejs/lib/middleware/ai-act-disclosure.js +203 -0
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +981 -0
- package/lib/vendor/blamejs/lib/middleware/assetlinks.js +137 -0
- package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +171 -0
- package/lib/vendor/blamejs/lib/middleware/attach-user.js +220 -0
- package/lib/vendor/blamejs/lib/middleware/bearer-auth.js +293 -0
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +1519 -0
- package/lib/vendor/blamejs/lib/middleware/bot-disclose.js +183 -0
- package/lib/vendor/blamejs/lib/middleware/bot-guard.js +217 -0
- package/lib/vendor/blamejs/lib/middleware/clear-site-data.js +122 -0
- package/lib/vendor/blamejs/lib/middleware/compose-pipeline.js +355 -0
- package/lib/vendor/blamejs/lib/middleware/compression.js +489 -0
- package/lib/vendor/blamejs/lib/middleware/cookies.js +130 -0
- package/lib/vendor/blamejs/lib/middleware/cors.js +386 -0
- package/lib/vendor/blamejs/lib/middleware/csp-nonce.js +388 -0
- package/lib/vendor/blamejs/lib/middleware/csp-report.js +167 -0
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +499 -0
- package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +243 -0
- package/lib/vendor/blamejs/lib/middleware/db-role-for.js +304 -0
- package/lib/vendor/blamejs/lib/middleware/dpop.js +402 -0
- package/lib/vendor/blamejs/lib/middleware/error-handler.js +69 -0
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +168 -0
- package/lib/vendor/blamejs/lib/middleware/flag-context.js +110 -0
- package/lib/vendor/blamejs/lib/middleware/gpc.js +153 -0
- package/lib/vendor/blamejs/lib/middleware/headers.js +242 -0
- package/lib/vendor/blamejs/lib/middleware/health.js +438 -0
- package/lib/vendor/blamejs/lib/middleware/host-allowlist.js +189 -0
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +964 -0
- package/lib/vendor/blamejs/lib/middleware/index.js +183 -0
- package/lib/vendor/blamejs/lib/middleware/nel.js +214 -0
- package/lib/vendor/blamejs/lib/middleware/network-allowlist.js +237 -0
- package/lib/vendor/blamejs/lib/middleware/no-cache.js +106 -0
- package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +177 -0
- package/lib/vendor/blamejs/lib/middleware/protected-resource-metadata.js +277 -0
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +556 -0
- package/lib/vendor/blamejs/lib/middleware/request-id.js +79 -0
- package/lib/vendor/blamejs/lib/middleware/request-log.js +205 -0
- package/lib/vendor/blamejs/lib/middleware/require-aal.js +138 -0
- package/lib/vendor/blamejs/lib/middleware/require-auth.js +144 -0
- package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +290 -0
- package/lib/vendor/blamejs/lib/middleware/require-content-type.js +113 -0
- package/lib/vendor/blamejs/lib/middleware/require-methods.js +97 -0
- package/lib/vendor/blamejs/lib/middleware/require-mtls.js +212 -0
- package/lib/vendor/blamejs/lib/middleware/require-step-up.js +226 -0
- package/lib/vendor/blamejs/lib/middleware/scim-server.js +375 -0
- package/lib/vendor/blamejs/lib/middleware/security-headers.js +285 -0
- package/lib/vendor/blamejs/lib/middleware/security-txt.js +170 -0
- package/lib/vendor/blamejs/lib/middleware/span-http-server.js +280 -0
- package/lib/vendor/blamejs/lib/middleware/speculation-rules.js +323 -0
- package/lib/vendor/blamejs/lib/middleware/sse.js +200 -0
- package/lib/vendor/blamejs/lib/middleware/trace-log-correlation.js +167 -0
- package/lib/vendor/blamejs/lib/middleware/trace-propagate.js +148 -0
- package/lib/vendor/blamejs/lib/middleware/tus-upload.js +749 -0
- package/lib/vendor/blamejs/lib/middleware/web-app-manifest.js +164 -0
- package/lib/vendor/blamejs/lib/migration-files.js +37 -0
- package/lib/vendor/blamejs/lib/migrations.js +385 -0
- package/lib/vendor/blamejs/lib/mime-parse.js +198 -0
- package/lib/vendor/blamejs/lib/money.js +699 -0
- package/lib/vendor/blamejs/lib/mtls-ca.js +572 -0
- package/lib/vendor/blamejs/lib/mtls-engine-default.js +501 -0
- package/lib/vendor/blamejs/lib/network-byte-quota.js +308 -0
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +533 -0
- package/lib/vendor/blamejs/lib/network-dns.js +1930 -0
- package/lib/vendor/blamejs/lib/network-heartbeat.js +425 -0
- package/lib/vendor/blamejs/lib/network-nts.js +574 -0
- package/lib/vendor/blamejs/lib/network-proxy.js +265 -0
- package/lib/vendor/blamejs/lib/network-smtp-policy.js +836 -0
- package/lib/vendor/blamejs/lib/network-tls.js +3126 -0
- package/lib/vendor/blamejs/lib/network.js +346 -0
- package/lib/vendor/blamejs/lib/nis2-report.js +181 -0
- package/lib/vendor/blamejs/lib/nist-crosswalk.js +293 -0
- package/lib/vendor/blamejs/lib/nonce-store.js +177 -0
- package/lib/vendor/blamejs/lib/notify.js +683 -0
- package/lib/vendor/blamejs/lib/ntp-check.js +458 -0
- package/lib/vendor/blamejs/lib/numeric-bounds.js +111 -0
- package/lib/vendor/blamejs/lib/numeric-checks.js +40 -0
- package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +349 -0
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +488 -0
- package/lib/vendor/blamejs/lib/object-store/gcs-bucket-ops.js +351 -0
- package/lib/vendor/blamejs/lib/object-store/gcs.js +515 -0
- package/lib/vendor/blamejs/lib/object-store/http-put.js +153 -0
- package/lib/vendor/blamejs/lib/object-store/http-request.js +38 -0
- package/lib/vendor/blamejs/lib/object-store/index.js +197 -0
- package/lib/vendor/blamejs/lib/object-store/local.js +163 -0
- package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +1133 -0
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +957 -0
- package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +420 -0
- package/lib/vendor/blamejs/lib/observability-tracer.js +395 -0
- package/lib/vendor/blamejs/lib/observability.js +720 -0
- package/lib/vendor/blamejs/lib/openapi-paths-builder.js +248 -0
- package/lib/vendor/blamejs/lib/openapi-schema-walk.js +192 -0
- package/lib/vendor/blamejs/lib/openapi-security.js +169 -0
- package/lib/vendor/blamejs/lib/openapi-yaml.js +154 -0
- package/lib/vendor/blamejs/lib/openapi.js +489 -0
- package/lib/vendor/blamejs/lib/otel-export.js +278 -0
- package/lib/vendor/blamejs/lib/outbox.js +547 -0
- package/lib/vendor/blamejs/lib/pagination.js +542 -0
- package/lib/vendor/blamejs/lib/parsers/index.js +91 -0
- package/lib/vendor/blamejs/lib/parsers/safe-env.js +642 -0
- package/lib/vendor/blamejs/lib/parsers/safe-ini.js +293 -0
- package/lib/vendor/blamejs/lib/parsers/safe-toml.js +784 -0
- package/lib/vendor/blamejs/lib/parsers/safe-xml.js +390 -0
- package/lib/vendor/blamejs/lib/parsers/safe-yaml.js +1015 -0
- package/lib/vendor/blamejs/lib/permissions.js +793 -0
- package/lib/vendor/blamejs/lib/pick.js +105 -0
- package/lib/vendor/blamejs/lib/pqc-agent.js +351 -0
- package/lib/vendor/blamejs/lib/pqc-gate.js +279 -0
- package/lib/vendor/blamejs/lib/pqc-software.js +271 -0
- package/lib/vendor/blamejs/lib/problem-details.js +482 -0
- package/lib/vendor/blamejs/lib/process-spawn.js +196 -0
- package/lib/vendor/blamejs/lib/promise-pool.js +162 -0
- package/lib/vendor/blamejs/lib/protobuf-encoder.js +190 -0
- package/lib/vendor/blamejs/lib/protocol-dispatcher.js +161 -0
- package/lib/vendor/blamejs/lib/public-suffix.js +403 -0
- package/lib/vendor/blamejs/lib/pubsub-cluster.js +154 -0
- package/lib/vendor/blamejs/lib/pubsub-redis.js +167 -0
- package/lib/vendor/blamejs/lib/pubsub.js +463 -0
- package/lib/vendor/blamejs/lib/queue-local.js +476 -0
- package/lib/vendor/blamejs/lib/queue-redis.js +745 -0
- package/lib/vendor/blamejs/lib/queue-sqs.js +319 -0
- package/lib/vendor/blamejs/lib/queue.js +1016 -0
- package/lib/vendor/blamejs/lib/redact.js +1007 -0
- package/lib/vendor/blamejs/lib/redis-client.js +520 -0
- package/lib/vendor/blamejs/lib/render.js +285 -0
- package/lib/vendor/blamejs/lib/request-helpers.js +767 -0
- package/lib/vendor/blamejs/lib/resource-access-lock.js +116 -0
- package/lib/vendor/blamejs/lib/restore-bundle.js +340 -0
- package/lib/vendor/blamejs/lib/restore-rollback.js +365 -0
- package/lib/vendor/blamejs/lib/restore.js +409 -0
- package/lib/vendor/blamejs/lib/retention.js +640 -0
- package/lib/vendor/blamejs/lib/retry.js +523 -0
- package/lib/vendor/blamejs/lib/router.js +1289 -0
- package/lib/vendor/blamejs/lib/safe-async.js +1184 -0
- package/lib/vendor/blamejs/lib/safe-buffer.js +562 -0
- package/lib/vendor/blamejs/lib/safe-decompress.js +297 -0
- package/lib/vendor/blamejs/lib/safe-dns.js +665 -0
- package/lib/vendor/blamejs/lib/safe-ical.js +634 -0
- package/lib/vendor/blamejs/lib/safe-icap.js +502 -0
- package/lib/vendor/blamejs/lib/safe-json.js +946 -0
- package/lib/vendor/blamejs/lib/safe-jsonpath.js +285 -0
- package/lib/vendor/blamejs/lib/safe-mime.js +831 -0
- package/lib/vendor/blamejs/lib/safe-mount-info.js +306 -0
- package/lib/vendor/blamejs/lib/safe-path.js +254 -0
- package/lib/vendor/blamejs/lib/safe-redirect.js +106 -0
- package/lib/vendor/blamejs/lib/safe-schema.js +1810 -0
- package/lib/vendor/blamejs/lib/safe-sieve.js +684 -0
- package/lib/vendor/blamejs/lib/safe-smtp.js +185 -0
- package/lib/vendor/blamejs/lib/safe-sql.js +363 -0
- package/lib/vendor/blamejs/lib/safe-url.js +428 -0
- package/lib/vendor/blamejs/lib/safe-vcard.js +473 -0
- package/lib/vendor/blamejs/lib/sandbox-worker.js +135 -0
- package/lib/vendor/blamejs/lib/sandbox.js +358 -0
- package/lib/vendor/blamejs/lib/scheduler.js +827 -0
- package/lib/vendor/blamejs/lib/sd-notify.js +269 -0
- package/lib/vendor/blamejs/lib/sec-cyber.js +214 -0
- package/lib/vendor/blamejs/lib/security-assert.js +395 -0
- package/lib/vendor/blamejs/lib/seeders.js +620 -0
- package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +309 -0
- package/lib/vendor/blamejs/lib/self-update.js +804 -0
- package/lib/vendor/blamejs/lib/server-timing.js +174 -0
- package/lib/vendor/blamejs/lib/session-device-binding.js +431 -0
- package/lib/vendor/blamejs/lib/session-stores.js +138 -0
- package/lib/vendor/blamejs/lib/session.js +1162 -0
- package/lib/vendor/blamejs/lib/slug.js +381 -0
- package/lib/vendor/blamejs/lib/sse.js +349 -0
- package/lib/vendor/blamejs/lib/ssrf-guard.js +792 -0
- package/lib/vendor/blamejs/lib/standard-webhooks.js +183 -0
- package/lib/vendor/blamejs/lib/static.js +1249 -0
- package/lib/vendor/blamejs/lib/storage.js +1272 -0
- package/lib/vendor/blamejs/lib/stream-throttle.js +235 -0
- package/lib/vendor/blamejs/lib/structured-fields.js +244 -0
- package/lib/vendor/blamejs/lib/subject.js +667 -0
- package/lib/vendor/blamejs/lib/tcpa-10dlc.js +175 -0
- package/lib/vendor/blamejs/lib/template.js +931 -0
- package/lib/vendor/blamejs/lib/tenant-quota.js +545 -0
- package/lib/vendor/blamejs/lib/test-harness.js +275 -0
- package/lib/vendor/blamejs/lib/testing.js +1185 -0
- package/lib/vendor/blamejs/lib/time.js +578 -0
- package/lib/vendor/blamejs/lib/tls-exporter.js +239 -0
- package/lib/vendor/blamejs/lib/totp.js +318 -0
- package/lib/vendor/blamejs/lib/tracing.js +546 -0
- package/lib/vendor/blamejs/lib/uuid.js +207 -0
- package/lib/vendor/blamejs/lib/validate-opts.js +381 -0
- package/lib/vendor/blamejs/lib/vault/index.js +638 -0
- package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +311 -0
- package/lib/vendor/blamejs/lib/vault/passphrase-source.js +198 -0
- package/lib/vendor/blamejs/lib/vault/rotate.js +803 -0
- package/lib/vendor/blamejs/lib/vault/seal-pem-file.js +471 -0
- package/lib/vendor/blamejs/lib/vault/wrap.js +296 -0
- package/lib/vendor/blamejs/lib/vault-aad.js +259 -0
- package/lib/vendor/blamejs/lib/vendor/.vendor-data-pubkey +4 -0
- package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +161 -0
- package/lib/vendor/blamejs/lib/vendor/bimi-trust-anchors.data.js +68 -0
- package/lib/vendor/blamejs/lib/vendor/bimi-trust-anchors.pem +33 -0
- package/lib/vendor/blamejs/lib/vendor/common-passwords-top-10000.data.js +1325 -0
- package/lib/vendor/blamejs/lib/vendor/common-passwords-top-10000.txt +10002 -0
- package/lib/vendor/blamejs/lib/vendor/noble-ciphers.cjs +9 -0
- package/lib/vendor/blamejs/lib/vendor/noble-post-quantum.cjs +18 -0
- package/lib/vendor/blamejs/lib/vendor/pki.cjs +181 -0
- package/lib/vendor/blamejs/lib/vendor/public-suffix-list.dat +16382 -0
- package/lib/vendor/blamejs/lib/vendor/public-suffix-list.data.js +5881 -0
- package/lib/vendor/blamejs/lib/vendor/simplewebauthn-server.cjs +328 -0
- package/lib/vendor/blamejs/lib/vendor/vendor-data-pubkey.js +16 -0
- package/lib/vendor/blamejs/lib/vendor-data.js +520 -0
- package/lib/vendor/blamejs/lib/vex.js +630 -0
- package/lib/vendor/blamejs/lib/watcher.js +608 -0
- package/lib/vendor/blamejs/lib/web-push-vapid.js +322 -0
- package/lib/vendor/blamejs/lib/webhook.js +977 -0
- package/lib/vendor/blamejs/lib/websocket-channels.js +327 -0
- package/lib/vendor/blamejs/lib/websocket.js +1561 -0
- package/lib/vendor/blamejs/lib/wiki-concepts.js +338 -0
- package/lib/vendor/blamejs/lib/worker-pool.js +464 -0
- package/lib/vendor/blamejs/lib/ws-client.js +978 -0
- package/lib/vendor/blamejs/lib/xml-c14n.js +506 -0
- package/lib/vendor/blamejs/memory/specs/node-26-map-getorinsert-migration.md +164 -0
- package/lib/vendor/blamejs/oss-fuzz/projects/blamejs/Dockerfile +19 -0
- package/lib/vendor/blamejs/oss-fuzz/projects/blamejs/README.md +88 -0
- package/lib/vendor/blamejs/oss-fuzz/projects/blamejs/build.sh +26 -0
- package/lib/vendor/blamejs/oss-fuzz/projects/blamejs/project.yaml +28 -0
- package/lib/vendor/blamejs/package.json +81 -0
- package/lib/vendor/blamejs/release-notes/v0.0.x.json +310 -0
- package/lib/vendor/blamejs/release-notes/v0.1.x.json +1798 -0
- package/lib/vendor/blamejs/release-notes/v0.10.x.json +1288 -0
- package/lib/vendor/blamejs/release-notes/v0.11.x.json +2551 -0
- package/lib/vendor/blamejs/release-notes/v0.12.0.json +64 -0
- package/lib/vendor/blamejs/release-notes/v0.12.1.json +32 -0
- package/lib/vendor/blamejs/release-notes/v0.12.2.json +45 -0
- package/lib/vendor/blamejs/release-notes/v0.2.x.json +706 -0
- package/lib/vendor/blamejs/release-notes/v0.3.x.json +786 -0
- package/lib/vendor/blamejs/release-notes/v0.4.x.json +588 -0
- package/lib/vendor/blamejs/release-notes/v0.5.x.json +390 -0
- package/lib/vendor/blamejs/release-notes/v0.6.x.json +1947 -0
- package/lib/vendor/blamejs/release-notes/v0.7.x.json +3811 -0
- package/lib/vendor/blamejs/release-notes/v0.8.x.json +3318 -0
- package/lib/vendor/blamejs/release-notes/v0.9.x.json +2257 -0
- package/lib/vendor/blamejs/scripts/build-vendored-sbom.js +325 -0
- package/lib/vendor/blamejs/scripts/check-api-snapshot.js +62 -0
- package/lib/vendor/blamejs/scripts/check-changelog-extract.js +108 -0
- package/lib/vendor/blamejs/scripts/check-pack-against-gitignore.js +83 -0
- package/lib/vendor/blamejs/scripts/check-services.js +483 -0
- package/lib/vendor/blamejs/scripts/check-vendor-currency.js +349 -0
- package/lib/vendor/blamejs/scripts/consolidate-release-notes.js +216 -0
- package/lib/vendor/blamejs/scripts/gen-migrating.js +275 -0
- package/lib/vendor/blamejs/scripts/generate-changelog-entry.js +577 -0
- package/lib/vendor/blamejs/scripts/generate-release-signing-key.js +79 -0
- package/lib/vendor/blamejs/scripts/publish-dep-confusion-placeholder.sh +101 -0
- package/lib/vendor/blamejs/scripts/refresh-api-snapshot.js +31 -0
- package/lib/vendor/blamejs/scripts/refresh-vendor-manifest.js +132 -0
- package/lib/vendor/blamejs/scripts/release.js +652 -0
- package/lib/vendor/blamejs/scripts/sha3-digest.js +62 -0
- package/lib/vendor/blamejs/scripts/sign-release-artifact.js +92 -0
- package/lib/vendor/blamejs/scripts/test-integration.js +181 -0
- package/lib/vendor/blamejs/scripts/test-wiki-integration.js +126 -0
- package/lib/vendor/blamejs/scripts/validate-source-comment-blocks.js +77 -0
- package/lib/vendor/blamejs/scripts/vendor-data-gen.js +186 -0
- package/lib/vendor/blamejs/scripts/vendor-data-keygen.js +101 -0
- package/lib/vendor/blamejs/scripts/vendor-update.sh +278 -0
- package/lib/vendor/blamejs/test/00-primitives.js +19075 -0
- package/lib/vendor/blamejs/test/10-state.js +622 -0
- package/lib/vendor/blamejs/test/20-db.js +561 -0
- package/lib/vendor/blamejs/test/30-chain.js +2110 -0
- package/lib/vendor/blamejs/test/40-consumers.js +2453 -0
- package/lib/vendor/blamejs/test/50-integration.js +486 -0
- package/lib/vendor/blamejs/test/_helpers.js +10 -0
- package/lib/vendor/blamejs/test/_smoke-worker.js +69 -0
- package/lib/vendor/blamejs/test/fixtures/exploit-corpus/corpus.json +368 -0
- package/lib/vendor/blamejs/test/fixtures/http-client-stream-payload.txt +2 -0
- package/lib/vendor/blamejs/test/fixtures/worker-pool/echo.js +52 -0
- package/lib/vendor/blamejs/test/helpers/_codebase-shingle-worker.js +24 -0
- package/lib/vendor/blamejs/test/helpers/_codebase-shingle.js +203 -0
- package/lib/vendor/blamejs/test/helpers/_shape-match.js +513 -0
- package/lib/vendor/blamejs/test/helpers/check.js +36 -0
- package/lib/vendor/blamejs/test/helpers/cluster.js +70 -0
- package/lib/vendor/blamejs/test/helpers/db.js +143 -0
- package/lib/vendor/blamejs/test/helpers/drivers.js +207 -0
- package/lib/vendor/blamejs/test/helpers/fs-watch.js +101 -0
- package/lib/vendor/blamejs/test/helpers/http.js +14 -0
- package/lib/vendor/blamejs/test/helpers/index.js +93 -0
- package/lib/vendor/blamejs/test/helpers/json-round-trip.js +120 -0
- package/lib/vendor/blamejs/test/helpers/mocks.js +20 -0
- package/lib/vendor/blamejs/test/helpers/otel.js +13 -0
- package/lib/vendor/blamejs/test/helpers/services.js +380 -0
- package/lib/vendor/blamejs/test/helpers/wait.js +206 -0
- package/lib/vendor/blamejs/test/integration/cache.test.js +235 -0
- package/lib/vendor/blamejs/test/integration/cluster-provider-mysql.test.js +174 -0
- package/lib/vendor/blamejs/test/integration/federation-auth.test.js +611 -0
- package/lib/vendor/blamejs/test/integration/http-client.test.js +129 -0
- package/lib/vendor/blamejs/test/integration/log-stream.test.js +219 -0
- package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +181 -0
- package/lib/vendor/blamejs/test/integration/mail-dkim.test.js +152 -0
- package/lib/vendor/blamejs/test/integration/mail-smtp.test.js +161 -0
- package/lib/vendor/blamejs/test/integration/mtls-ca.test.js +289 -0
- package/lib/vendor/blamejs/test/integration/network-dns.test.js +123 -0
- package/lib/vendor/blamejs/test/integration/network-heartbeat.test.js +101 -0
- package/lib/vendor/blamejs/test/integration/ntp-check.test.js +89 -0
- package/lib/vendor/blamejs/test/integration/object-store-sigv4.test.js +403 -0
- package/lib/vendor/blamejs/test/integration/pqc-pkcs8-forward-compat.test.js +271 -0
- package/lib/vendor/blamejs/test/integration/pubsub.test.js +137 -0
- package/lib/vendor/blamejs/test/integration/queue-redis.test.js +352 -0
- package/lib/vendor/blamejs/test/integration/redis-client-tls.test.js +96 -0
- package/lib/vendor/blamejs/test/integration/ssrf-guard.test.js +98 -0
- package/lib/vendor/blamejs/test/integration/websocket-permessage-deflate.test.js +261 -0
- package/lib/vendor/blamejs/test/integration/ws-client-roundtrip.test.js +230 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/a2a-tasks.test.js +211 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/a2a.test.js +59 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/access-lock.test.js +136 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/acme.test.js +219 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/age-gate.test.js +69 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-event-bus.test.js +266 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-idempotency.test.js +262 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +390 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-posture-chain.test.js +174 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-saga.test.js +279 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-snapshot.test.js +322 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-stream.test.js +227 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-tenant.test.js +302 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-trace.test.js +150 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-adverse-decision.test.js +44 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-content-detect.test.js +150 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-input.test.js +50 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-model-manifest.test.js +96 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-pref.test.js +76 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +1080 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +311 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-zip-stream.test.js +291 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive.test.js +140 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/arg-parser.test.js +267 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/asn1-der.test.js +108 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/asyncapi.test.js +929 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-conflict-path.test.js +80 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-cve-defensive.test.js +176 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-daily-review.test.js +132 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-export-cadf.test.js +97 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-framework-namespaces.test.js +141 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-segregation.test.js +115 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-sign-ml-dsa-65.test.js +163 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-use-store.test.js +246 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-bot-challenge-verifier.test.js +485 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-bot-challenge.test.js +331 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +352 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-lockout.test.js +572 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-password-audit.test.js +61 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/azure-blob-bucket-ops.test.js +258 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-manifest-signature.test.js +105 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-worker.test.js +34 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/bearer-auth.test.js +107 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-chunked-malformed.test.js +131 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-smuggling.test.js +118 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/boot-gates.test.js +85 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/breach-deadline.test.js +38 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +861 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/budr.test.js +55 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/bundler-engine.test.js +209 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cache-status.test.js +129 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cache.test.js +871 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +891 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/canonical-json-jcs.test.js +43 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cdn-cache-control.test.js +243 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cert.test.js +550 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/clear-site-data.test.js +107 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-api-key.test.js +147 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-audit-verify-chain.test.js +104 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-backup.test.js +135 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-config-drift.test.js +67 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-erase.test.js +75 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-file-type.test.js +98 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-helpers.test.js +145 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-mtls.test.js +133 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-password.test.js +97 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-restore.test.js +160 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-retention.test.js +84 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-security.test.js +69 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-vault.test.js +142 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/client-hints.test.js +133 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cms-codec.test.js +237 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +9600 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +575 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-cascade.test.js +89 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-eaa.test.js +36 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-sanctions.test.js +712 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance.test.js +278 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/config-drift.test.js +97 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/config.test.js +424 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials.test.js +94 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cors.test.js +357 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cra-report.test.js +31 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/credential-hash.test.js +226 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-base64url.test.js +86 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-envelope.test.js +85 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-hash-files-parallel.test.js +193 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-hash-stream.test.js +98 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-hpke-pq.test.js +132 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-hpke.test.js +155 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-mlkem768-x25519.test.js +129 -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-random-int.test.js +72 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csp-builder.test.js +96 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csp-nonce.test.js +401 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +34 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csv.test.js +180 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/daemon.test.js +210 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/daily-byte-quota.test.js +153 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dark-patterns.test.js +66 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/data-act.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-collection-extensions.test.js +226 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-collection.test.js +136 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-init-extensions.test.js +165 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-cross-schema.test.js +150 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-extensions.test.js +191 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-role-for.test.js +228 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-vacuum.test.js +55 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-worm.test.js +89 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ddl-change-control.test.js +184 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/declare-row-policy.test.js +203 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/declare-view.test.js +303 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dns-dnssec-algorithm.test.js +163 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dns-null-mx.test.js +39 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dora.test.js +165 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dr-runbook.test.js +59 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsr-state-rules.test.js +55 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +786 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dual-control.test.js +105 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/early-hints.test.js +147 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/events.test.js +105 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/exploit-replay.test.js +243 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +181 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-migrate.test.js +190 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-routing.test.js +531 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +118 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fapi2.test.js +89 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fda-21cfr11.test.js +156 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fdx.test.js +79 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fedcm-dbsc.test.js +216 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +434 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3.test.js +432 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/file-type.test.js +81 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/flag.test.js +887 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/forensic-snapshot.test.js +51 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fsm.test.js +375 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/gcs-bucket-ops.test.js +321 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/gdpr-ropa.test.js +41 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +32 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-agent-registry.test.js +87 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-all.test.js +328 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-archive.test.js +339 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-csv.test.js +694 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-dsn.test.js +296 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-email.test.js +234 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-envelope.test.js +192 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-event-bus-payload.test.js +89 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-event-bus-topic.test.js +71 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-filename.test.js +386 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-html-wcag.test.js +859 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-html.test.js +357 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-idempotency-key.test.js +92 -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 +174 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-json.test.js +317 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-list-id.test.js +199 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-list-unsubscribe.test.js +214 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-mail-compose.test.js +111 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-mail-move.test.js +110 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-mail-query.test.js +112 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-mail-reply.test.js +86 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-mail-sieve.test.js +92 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-managesieve-command.test.js +301 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-markdown.test.js +265 -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-pop3-command.test.js +161 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-posture-chain.test.js +100 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-saga-config.test.js +79 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-smtp-command.test.js +269 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-snapshot-envelope.test.js +89 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-stream-args.test.js +78 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-svg.test.js +288 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-tenant-id.test.js +69 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-trace-context.test.js +102 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-xml.test.js +202 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-yaml.test.js +203 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/hal.test.js +51 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/honeytoken.test.js +50 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/html-balance.test.js +37 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-cache.test.js +692 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +280 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-message-signature.test.js +225 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/i18n-messageformat.test.js +203 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/i18n.test.js +991 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/iab-mspa.test.js +63 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/iab-tcf.test.js +73 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/idempotency-key.test.js +612 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/importmap-integrity.test.js +56 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/inbox.test.js +166 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/incident-report.test.js +29 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/jose-jwe-experimental.test.js +121 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/json-api.test.js +58 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/json-round-trip-helper.test.js +110 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/jwt-external.test.js +159 -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 +118 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/local-db-thin.test.js +150 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/log-stream-cloudwatch.test.js +489 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/log-stream-otlp-grpc.test.js +207 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/log-stream-otlp.test.js +283 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/lro.test.js +65 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-agent.test.js +417 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-arf.test.js +208 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +910 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi.test.js +502 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-bounce.test.js +680 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-canspam.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-pgp-experimental.test.js +149 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-pgp.test.js +323 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +297 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +514 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-deploy-tlsrpt.test.js +369 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-deploy.test.js +199 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim.test.js +627 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-feedback-id.test.js +56 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-greylist.test.js +217 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-helo.test.js +283 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-journal.test.js +217 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-mdn.test.js +334 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-rbl.test.js +271 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-require-tls.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-scan.test.js +215 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +336 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-imap.test.js +732 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-jmap.test.js +840 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-managesieve.test.js +130 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +285 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-pop3.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-rate-limit.test.js +112 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-registry.test.js +229 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-submission.test.js +394 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-tls.test.js +147 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-sieve.test.js +151 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-spam-score.test.js +204 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-srs.test.js +152 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-store-fts.test.js +279 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-store.test.js +323 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-unsubscribe.test.js +165 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail.test.js +439 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mcp-tool-registry.test.js +202 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js +155 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/metrics-shadow-registry.test.js +112 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/metrics-snapshot.test.js +224 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/middleware-compose-pipeline.test.js +278 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/money.test.js +376 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mtls-ca-paths.test.js +89 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/nel.test.js +200 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-allowlist.test.js +106 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-byte-quota.test.js +133 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver.test.js +372 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns.test.js +635 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-heartbeat-passive.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-tls-build-options.test.js +130 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-tls-ct-inclusion.test.js +179 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-tls.test.js +447 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network.test.js +369 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/nis2-report.test.js +21 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/nist-crosswalk.test.js +42 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/no-cache.test.js +98 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/notify.test.js +707 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/numeric-bounds.test.js +142 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +72 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/observability-tracing.test.js +597 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/observability.test.js +190 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/openapi.test.js +877 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otel-export.test.js +257 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pagination.test.js +522 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/parsers-standalone.test.js +216 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/passkey.test.js +324 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/permissions.test.js +546 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pqc-agent-curve.test.js +153 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pqc-software.test.js +94 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/problem-details.test.js +195 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/process-spawn.test.js +62 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/promise-pool.test.js +93 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/protected-resource-metadata.test.js +68 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/protobuf-encoder.test.js +138 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/protocol-dispatcher.test.js +174 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/public-suffix.test.js +197 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pubsub.test.js +232 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-dlq-extend-lease.test.js +178 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-flow-repeat.test.js +322 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-priority-rate-progress.test.js +266 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-sqs.test.js +300 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-cluster.test.js +338 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-registry.test.js +75 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redact-dlp.test.js +246 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +130 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-helpers.test.js +335 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-log.test.js +170 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/require-auth-cache-control.test.js +93 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/require-mtls.test.js +34 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/resource-access-lock.test.js +52 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retention-floor.test.js +67 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retry.test.js +535 -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-tls0rtt.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-async-loops.test.js +163 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-async-parallel.test.js +170 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-decompress.test.js +248 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-dns.test.js +451 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +289 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-icap.test.js +206 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-jsonpath.test.js +104 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-mime.test.js +339 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-mount-info.test.js +180 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-path.test.js +78 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-sieve.test.js +123 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-smtp.test.js +95 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-idn-homograph.test.js +77 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-vcard.test.js +257 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-slo.test.js +249 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +228 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-exactly-once.test.js +238 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +92 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +700 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-notify.test.js +67 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sec-cyber.test.js +85 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/security-assert.test.js +107 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +175 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/seeders.test.js +816 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/self-update-standalone-verifier.test.js +168 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/self-update.test.js +302 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/server-timing.test.js +93 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +247 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +295 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/shape-match.test.js +142 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +952 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-multipart-sse.test.js +441 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/slug.test.js +330 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/smtp-policy.test.js +233 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/source-comment-blocks.test.js +105 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/speculation-rules.test.js +319 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sse.test.js +148 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ssrf-guard.test.js +283 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/standard-webhooks.test.js +67 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +266 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/step-up.test.js +487 -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 +773 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/stream-throttle.test.js +173 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/structured-fields.test.js +180 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tcpa-10dlc.test.js +66 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tenant-quota.test.js +89 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/test-coverage.test.js +571 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/test-harness.test.js +190 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/testing-request.test.js +119 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/testing.test.js +522 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/time.test.js +151 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tls-exporter.test.js +168 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tls-ocsp-ct.test.js +275 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tls-ocsp-verify.test.js +105 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tls-pinset-drift.test.js +35 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tls-preferred-groups.test.js +81 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tracing.test.js +280 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/uuid.test.js +93 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vault-aad.test.js +277 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vault-seal-pem-file.test.js +252 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vendor-data.test.js +149 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vendor-manifest.test.js +92 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vex.test.js +661 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/watcher.test.js +308 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/web-push-vapid.test.js +144 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook.test.js +674 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/websocket-channels.test.js +360 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/worker-pool.test.js +302 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ws-client.test.js +349 -0
- package/lib/vendor/blamejs/test/layer-1-state/api-key.test.js +717 -0
- package/lib/vendor/blamejs/test/layer-5-integration/bundler-output.test.js +444 -0
- package/lib/vendor/blamejs/test/layer-5-integration/guard-host-integration.test.js +597 -0
- package/lib/vendor/blamejs/test/layer-5-integration/security-chaos.test.js +308 -0
- package/lib/vendor/blamejs/test/smoke.js +431 -0
- package/lib/webhooks.js +305 -0
- package/package.json +43 -0
|
@@ -0,0 +1,2144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.mail.spf + b.mail.dmarc + b.mail.arc — inbound mail authentication
|
|
4
|
+
* verification family. Counterpart to the existing outbound DKIM
|
|
5
|
+
* signer in lib/mail-dkim.js.
|
|
6
|
+
*
|
|
7
|
+
* Operators receiving mail (incoming webhooks, customer-support
|
|
8
|
+
* inboxes, mailing-list ingestion, .eml uploads) need this to evaluate
|
|
9
|
+
* sender authenticity and decide on accept / quarantine / reject.
|
|
10
|
+
*
|
|
11
|
+
* Surface:
|
|
12
|
+
* b.mail.spf.verify({ ip, mailFrom, helo, dnsLookup }) → result
|
|
13
|
+
* b.mail.dmarc.evaluate({ from, spf, dkim, dnsLookup }) → result
|
|
14
|
+
* b.mail.arc.verify(rfc822, opts) → chain status
|
|
15
|
+
*
|
|
16
|
+
* SPF (RFC 7208) — ip4 / ip6 / a / mx / include / all / redirect=
|
|
17
|
+
* mechanisms.
|
|
18
|
+
* Mechanism limit: 10 DNS lookups per RFC 7208 §4.6.4 (with the
|
|
19
|
+
* void-lookup sub-limit at 2). The `a` and `mx` arms honor RFC
|
|
20
|
+
* §5.3 / §5.4 dual-cidr-length syntax (`a:foo.com/24//64`).
|
|
21
|
+
*
|
|
22
|
+
* Deferred mechanisms (each carries an explicit Re-open condition
|
|
23
|
+
* in the dispatch arm in this file):
|
|
24
|
+
* - exists: requires macro-string expansion (§7) to be useful;
|
|
25
|
+
* re-opens when macros land OR an operator surfaces a
|
|
26
|
+
* real macro-less `exists:` policy.
|
|
27
|
+
* - ptr: "strongly discouraged" by §5.5; re-opens when an
|
|
28
|
+
* operator surfaces a legitimate ptr-only sender.
|
|
29
|
+
* - macro-string expansion (§7) itself — separate slice tracked
|
|
30
|
+
* under blamejs-roadmap.md.
|
|
31
|
+
*
|
|
32
|
+
* DMARC (RFC 7489) — TXT record at _dmarc.<domain>; alignment check
|
|
33
|
+
* between From-header domain and DKIM-d / SPF-from-domain;
|
|
34
|
+
* policy resolution (none / quarantine / reject) per the published
|
|
35
|
+
* record. The org-domain extraction uses an operator-supplied
|
|
36
|
+
* `dnsLookup` callback (the framework doesn't ship the Public Suffix
|
|
37
|
+
* List).
|
|
38
|
+
*
|
|
39
|
+
* ARC (RFC 8617) — chain-of-custody verification. The framework parses
|
|
40
|
+
* the existing chain headers, recomputes the per-hop signatures, and
|
|
41
|
+
* reports validity by composing `lib/mail-dkim.js` (which carries
|
|
42
|
+
* the actual signature-verification surface).
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
var zlib = require("node:zlib");
|
|
46
|
+
var net = require("node:net");
|
|
47
|
+
var nodeCrypto = require("node:crypto");
|
|
48
|
+
var lazyRequire = require("./lazy-require");
|
|
49
|
+
var validateOpts = require("./validate-opts");
|
|
50
|
+
var bCrypto = require("./crypto");
|
|
51
|
+
var C = require("./constants");
|
|
52
|
+
var dkim = require("./mail-dkim");
|
|
53
|
+
var safeXml = require("./parsers/safe-xml");
|
|
54
|
+
var ipUtils = require("./ip-utils");
|
|
55
|
+
var publicSuffix = require("./public-suffix");
|
|
56
|
+
var networkDnsResolver = lazyRequire(function () { return require("./network-dns-resolver"); });
|
|
57
|
+
var { MailAuthError } = require("./framework-error");
|
|
58
|
+
|
|
59
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
60
|
+
void observability;
|
|
61
|
+
|
|
62
|
+
// SPF DNS-lookup ceiling per RFC 7208 §4.6.4. Operators with high-
|
|
63
|
+
// fan-out include chains hit this; the verify path returns "permerror"
|
|
64
|
+
// when crossed, matching mainstream MTAs.
|
|
65
|
+
var SPF_DNS_LOOKUP_LIMIT = 10;
|
|
66
|
+
|
|
67
|
+
// RFC 7208 §4.6.4 — "void lookup" cap. A void lookup is a successful
|
|
68
|
+
// DNS query whose answer is empty (NXDOMAIN, no-data response, or
|
|
69
|
+
// zero records returned). The SPF spec caps void lookups at 2; beyond
|
|
70
|
+
// that the policy MUST permerror. Attackers chain misconfigured
|
|
71
|
+
// `include:`s pointing at non-existent domains to amplify recursive
|
|
72
|
+
// resolver work without tripping the 10-lookup ceiling.
|
|
73
|
+
var SPF_VOID_LOOKUP_LIMIT = 2; // allow:raw-byte-literal — RFC 7208 §4.6.4 void-lookup ceiling
|
|
74
|
+
|
|
75
|
+
// RFC 7208 §3.3 — each SPF TXT record MUST NOT exceed 450 bytes when
|
|
76
|
+
// concatenated across multi-string TXT chunks. The spec lifts a
|
|
77
|
+
// receiver MUST-refuse on >450-byte records to bound parse work.
|
|
78
|
+
var SPF_RECORD_MAX_BYTES = 450; // allow:raw-byte-literal — RFC 7208 §3.3 record ceiling
|
|
79
|
+
|
|
80
|
+
// SPF redirect= modifier (RFC 7208 §6.1) recursion cap. The modifier
|
|
81
|
+
// re-evaluates against a different domain; a chain of redirect= cycles
|
|
82
|
+
// MUST terminate. We bound at the same depth as the lookup ceiling
|
|
83
|
+
// minus current count (the redirect itself counts as one lookup); the
|
|
84
|
+
// hard cap below is an additional belt-and-braces against malformed
|
|
85
|
+
// upstream policies that would otherwise spin until the lookup cap
|
|
86
|
+
// alone tripped.
|
|
87
|
+
var SPF_REDIRECT_DEPTH_LIMIT = 10; // allow:raw-byte-literal — same shape as RFC 7208 §4.6.4 lookup ceiling
|
|
88
|
+
|
|
89
|
+
// Shared safe-DNS TXT/A/AAAA/MX/PTR lookup. Operator-supplied
|
|
90
|
+
// `dnsLookup(qname, type)` is honored for every type when present:
|
|
91
|
+
// TXT → [[ "v=spf1 ...", ... ], ...] (array of TXT-string-arrays)
|
|
92
|
+
// A → [ "192.0.2.1", ... ] (flat IPv4 string array)
|
|
93
|
+
// AAAA → [ "2001:db8::1", ... ] (flat IPv6 string array)
|
|
94
|
+
// MX → [ { exchange, preference }, ...] (or [ "mx1.example.", ... ]
|
|
95
|
+
// when operator omits preference)
|
|
96
|
+
// PTR → [ "host.example.", ... ] (flat PTR-name array)
|
|
97
|
+
// When no operator callback is supplied, requests route through
|
|
98
|
+
// `b.network.dns.resolver` (DoH by default per v0.7.23). CVE-2008-1447
|
|
99
|
+
// (Kaminsky) + CVE-2022-3204 (NRDelegationAttack) class — the encrypted
|
|
100
|
+
// DoH transport plus b.safeDns parse caps defend transport and parse-
|
|
101
|
+
// side. Earlier shape fell back to `node:dns.promises.resolveTxt`
|
|
102
|
+
// directly, which sent plaintext UDP/53 to whatever the system
|
|
103
|
+
// resolver was — every downstream finding inherited that exposure.
|
|
104
|
+
var _defaultResolver = null;
|
|
105
|
+
function _getDefaultResolver() {
|
|
106
|
+
if (_defaultResolver) return _defaultResolver;
|
|
107
|
+
_defaultResolver = networkDnsResolver().create();
|
|
108
|
+
return _defaultResolver;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function _safeResolveTxt(qname, operatorLookup) {
|
|
112
|
+
if (operatorLookup) return operatorLookup(qname, "TXT");
|
|
113
|
+
var r = await _getDefaultResolver().queryTxt(qname);
|
|
114
|
+
var out = [];
|
|
115
|
+
for (var i = 0; i < r.rrs.length; i += 1) {
|
|
116
|
+
var rr = r.rrs[i];
|
|
117
|
+
if (rr && rr.type === 16) { // allow:raw-byte-literal — IANA DNS qtype TXT
|
|
118
|
+
out.push(Array.isArray(rr.decoded) ? rr.decoded : [String(rr.decoded)]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (out.length === 0) {
|
|
122
|
+
var err = new Error("no TXT records for " + qname);
|
|
123
|
+
err.code = "ENODATA";
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function _safeResolveA(qname, family /* 4|6 */, operatorLookup) {
|
|
130
|
+
// Pre-v0.11.3 the operatorLookup parameter wasn't threaded here, so
|
|
131
|
+
// the documented `dnsLookup` shape for A/AAAA was unhonored — SPF a/
|
|
132
|
+
// mx mechanism tests had no operator-mockable path. The function
|
|
133
|
+
// signature now matches the docstring contract above. Operator
|
|
134
|
+
// returns a flat string array of IP literals.
|
|
135
|
+
if (operatorLookup) {
|
|
136
|
+
var resp = await operatorLookup(qname, family === 6 ? "AAAA" : "A");
|
|
137
|
+
if (!Array.isArray(resp) || resp.length === 0) {
|
|
138
|
+
var aerr = new Error("no " + (family === 6 ? "AAAA" : "A") + " records for " + qname);
|
|
139
|
+
aerr.code = "ENODATA";
|
|
140
|
+
throw aerr;
|
|
141
|
+
}
|
|
142
|
+
return resp.map(function (x) { return String(x); });
|
|
143
|
+
}
|
|
144
|
+
var r = await _getDefaultResolver().query(qname, family === 6 ? "AAAA" : "A");
|
|
145
|
+
var out = [];
|
|
146
|
+
for (var i = 0; i < r.rrs.length; i += 1) {
|
|
147
|
+
var rr = r.rrs[i];
|
|
148
|
+
var wantType = family === 6 ? 28 : 1; // allow:raw-byte-literal — IANA DNS qtype AAAA / A
|
|
149
|
+
if (rr && rr.type === wantType) out.push(rr.decoded);
|
|
150
|
+
}
|
|
151
|
+
if (out.length === 0) {
|
|
152
|
+
var err = new Error("no " + (family === 6 ? "AAAA" : "A") + " records for " + qname);
|
|
153
|
+
err.code = "ENODATA";
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// RFC 1035 §3.3.9 MX record: { preference, exchange }. Returns array of
|
|
160
|
+
// exchange hostnames sorted by preference (lowest first). Operator-
|
|
161
|
+
// supplied dnsLookup callback may return either:
|
|
162
|
+
// - [ { exchange, preference }, ... ] — full shape (preferred)
|
|
163
|
+
// - [ "mx1.example.", ... ] — exchanges only (preference
|
|
164
|
+
// treated as 0 → first-served)
|
|
165
|
+
async function _safeResolveMx(qname, operatorLookup) {
|
|
166
|
+
if (operatorLookup) {
|
|
167
|
+
var resp = await operatorLookup(qname, "MX");
|
|
168
|
+
if (!Array.isArray(resp) || resp.length === 0) {
|
|
169
|
+
var merr = new Error("no MX records for " + qname);
|
|
170
|
+
merr.code = "ENODATA";
|
|
171
|
+
throw merr;
|
|
172
|
+
}
|
|
173
|
+
var normalized = resp.map(function (entry) {
|
|
174
|
+
if (typeof entry === "string") return { exchange: entry.replace(/\.$/, ""), preference: 0 };
|
|
175
|
+
var ex = entry && entry.exchange;
|
|
176
|
+
var pref = (entry && typeof entry.preference === "number") ? entry.preference : 0;
|
|
177
|
+
return { exchange: String(ex || "").replace(/\.$/, ""), preference: pref };
|
|
178
|
+
}).filter(function (e) { return e.exchange.length > 0; });
|
|
179
|
+
normalized.sort(function (a, b) { return a.preference - b.preference; });
|
|
180
|
+
return normalized.map(function (e) { return e.exchange; });
|
|
181
|
+
}
|
|
182
|
+
var r = await _getDefaultResolver().query(qname, "MX");
|
|
183
|
+
var entries = [];
|
|
184
|
+
for (var i = 0; i < r.rrs.length; i += 1) {
|
|
185
|
+
var rr = r.rrs[i];
|
|
186
|
+
if (rr && rr.type === 15) { // allow:raw-byte-literal — IANA DNS qtype MX
|
|
187
|
+
var d = rr.decoded || {};
|
|
188
|
+
if (d.exchange) {
|
|
189
|
+
entries.push({ exchange: String(d.exchange).replace(/\.$/, ""),
|
|
190
|
+
preference: typeof d.preference === "number" ? d.preference : 0 });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (entries.length === 0) {
|
|
195
|
+
var err = new Error("no MX records for " + qname);
|
|
196
|
+
err.code = "ENODATA";
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
entries.sort(function (a, b) { return a.preference - b.preference; });
|
|
200
|
+
return entries.map(function (e) { return e.exchange; });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function _safeReverse(ip) {
|
|
204
|
+
// PTR query against the reverse-arpa name. IPv4: a.b.c.d.in-addr.arpa
|
|
205
|
+
// (reversed octets); IPv6: nibble-reversed under ip6.arpa.
|
|
206
|
+
var qname = _ipToReverseArpa(ip);
|
|
207
|
+
if (qname === null) {
|
|
208
|
+
var err = new Error("invalid IP literal: " + ip);
|
|
209
|
+
err.code = "ENOTFOUND";
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
var r = await _getDefaultResolver().query(qname, "PTR");
|
|
213
|
+
var out = [];
|
|
214
|
+
for (var i = 0; i < r.rrs.length; i += 1) {
|
|
215
|
+
var rr = r.rrs[i];
|
|
216
|
+
if (rr && rr.type === 12) { // allow:raw-byte-literal — IANA DNS qtype PTR
|
|
217
|
+
// Strip trailing dot if present (PTR rdata is FQDN with root dot).
|
|
218
|
+
var name = String(rr.decoded || "").replace(/\.$/, "");
|
|
219
|
+
if (name.length > 0) out.push(name);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (out.length === 0) {
|
|
223
|
+
var e2 = new Error("no PTR records for " + ip);
|
|
224
|
+
e2.code = "ENODATA";
|
|
225
|
+
throw e2;
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function _ipToReverseArpa(ip) {
|
|
231
|
+
if (typeof ip !== "string") return null;
|
|
232
|
+
if (net.isIPv4(ip)) {
|
|
233
|
+
var p = ip.split(".");
|
|
234
|
+
if (p.length !== 4) return null; // allow:raw-byte-literal — IPv4 octet count
|
|
235
|
+
return p[3] + "." + p[2] + "." + p[1] + "." + p[0] + ".in-addr.arpa";
|
|
236
|
+
}
|
|
237
|
+
if (net.isIPv6(ip)) {
|
|
238
|
+
var groups = ipUtils.expandIpv6Groups(ip);
|
|
239
|
+
if (!groups) return null;
|
|
240
|
+
var hex = "";
|
|
241
|
+
for (var i = 0; i < groups.length; i += 1) {
|
|
242
|
+
var s = groups[i].toString(16); // allow:raw-byte-literal — hex radix
|
|
243
|
+
while (s.length < 4) s = "0" + s; // allow:raw-byte-literal — IPv6 group nibble count
|
|
244
|
+
hex += s;
|
|
245
|
+
}
|
|
246
|
+
var rev = hex.split("").reverse().join(".");
|
|
247
|
+
return rev + ".ip6.arpa";
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ---- Helpers ----
|
|
253
|
+
|
|
254
|
+
function _ipv4ToInt(ip) {
|
|
255
|
+
var parts = ip.split(".");
|
|
256
|
+
if (parts.length !== 4) return null; // allow:raw-byte-literal — IPv4 octet count
|
|
257
|
+
var n = 0;
|
|
258
|
+
for (var i = 0; i < 4; i += 1) { // allow:raw-byte-literal — IPv4 octet count
|
|
259
|
+
var p = parseInt(parts[i], 10);
|
|
260
|
+
if (!isFinite(p) || p < 0 || p > 255) return null; // allow:raw-byte-literal — IPv4 octet range
|
|
261
|
+
n = (n * 256) + p; // allow:raw-byte-literal — IPv4 octet base
|
|
262
|
+
}
|
|
263
|
+
return n;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Expand an IPv6 string (which may carry `::` shorthand) into 8 16-bit
|
|
267
|
+
// groups. Returns null on malformed input.
|
|
268
|
+
function _ipv6Expand(ip) {
|
|
269
|
+
// Compose the shared lib/ip-utils helper so the same IPv6 parse
|
|
270
|
+
// path is shared across mail-auth / mail-rbl / mail-greylist.
|
|
271
|
+
return ipUtils.expandIpv6Groups(ip);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function _ipv6InCidr(ip, cidr) {
|
|
275
|
+
var slash = cidr.indexOf("/");
|
|
276
|
+
var net = slash === -1 ? cidr : cidr.slice(0, slash);
|
|
277
|
+
var mask = slash === -1 ? 128 : parseInt(cidr.slice(slash + 1), 10); // allow:raw-byte-literal — IPv6 max prefix
|
|
278
|
+
if (!isFinite(mask) || mask < 0 || mask > 128) return false; // allow:raw-byte-literal — IPv6 max prefix
|
|
279
|
+
var ipGroups = _ipv6Expand(ip);
|
|
280
|
+
var netGroups = _ipv6Expand(net);
|
|
281
|
+
if (!ipGroups || !netGroups) return false;
|
|
282
|
+
if (mask === 0) return true;
|
|
283
|
+
// Compare group-by-group up to the prefix boundary.
|
|
284
|
+
var fullGroups = Math.floor(mask / 16); // allow:raw-byte-literal — bits per group
|
|
285
|
+
var remainBits = mask - fullGroups * 16; // allow:raw-byte-literal — bits per group
|
|
286
|
+
for (var g = 0; g < fullGroups; g += 1) {
|
|
287
|
+
if (ipGroups[g] !== netGroups[g]) return false;
|
|
288
|
+
}
|
|
289
|
+
if (remainBits > 0 && fullGroups < 8) { // allow:raw-byte-literal — IPv6 group count
|
|
290
|
+
var groupMask = (0xffff << (16 - remainBits)) & 0xffff; // allow:raw-byte-literal — bits per group
|
|
291
|
+
if ((ipGroups[fullGroups] & groupMask) !== (netGroups[fullGroups] & groupMask)) return false;
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function _ipv4InCidr(ip, cidr) {
|
|
297
|
+
var slash = cidr.indexOf("/");
|
|
298
|
+
var net = slash === -1 ? cidr : cidr.slice(0, slash);
|
|
299
|
+
var mask = slash === -1 ? 32 : parseInt(cidr.slice(slash + 1), 10); // allow:raw-byte-literal — IPv4 max prefix
|
|
300
|
+
if (mask < 0 || mask > 32) return false; // allow:raw-byte-literal — IPv4 max prefix
|
|
301
|
+
var ipInt = _ipv4ToInt(ip);
|
|
302
|
+
var netInt = _ipv4ToInt(net);
|
|
303
|
+
if (ipInt === null || netInt === null) return false;
|
|
304
|
+
if (mask === 0) return true;
|
|
305
|
+
var bits = 32 - mask; // allow:raw-byte-literal — IPv4 max prefix
|
|
306
|
+
// Use BigInt to avoid 32-bit signed-int wrap.
|
|
307
|
+
var maskInt = (BigInt("0xFFFFFFFF") << BigInt(bits)) & BigInt("0xFFFFFFFF");
|
|
308
|
+
return (BigInt(ipInt) & maskInt) === (BigInt(netInt) & maskInt);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Parse an SPF record into mechanisms.
|
|
312
|
+
function _parseSpfRecord(text) {
|
|
313
|
+
var trimmed = text.trim();
|
|
314
|
+
if (trimmed.indexOf("v=spf1") !== 0) {
|
|
315
|
+
throw new MailAuthError("mail-auth/spf-bad-version",
|
|
316
|
+
"SPF record must start with 'v=spf1', got " +
|
|
317
|
+
JSON.stringify(trimmed.slice(0, C.BYTES.bytes(32))));
|
|
318
|
+
}
|
|
319
|
+
var parts = trimmed.split(/\s+/);
|
|
320
|
+
var mechanisms = [];
|
|
321
|
+
var modifiers = [];
|
|
322
|
+
for (var i = 1; i < parts.length; i += 1) {
|
|
323
|
+
var p = parts[i];
|
|
324
|
+
if (p.length === 0) continue;
|
|
325
|
+
// RFC 7208 §4.6 distinguishes mechanisms (with optional qualifier
|
|
326
|
+
// prefix) from modifiers (name=value, no qualifier; e.g.
|
|
327
|
+
// `redirect=` and `exp=`). Pre-v0.8.32 the framework treated
|
|
328
|
+
// `redirect=` like a mechanism, surfacing a permerror under the
|
|
329
|
+
// generic "out of scope" arm. Handle modifiers separately:
|
|
330
|
+
// redirect= triggers re-evaluation against the target domain;
|
|
331
|
+
// exp= is operator-facing only (we record it).
|
|
332
|
+
var eqAt = p.indexOf("=");
|
|
333
|
+
if (eqAt !== -1 && /^[a-z]+$/i.test(p.slice(0, eqAt))) {
|
|
334
|
+
modifiers.push({ name: p.slice(0, eqAt).toLowerCase(), value: p.slice(eqAt + 1) });
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
var qualifier = "+";
|
|
338
|
+
if (p.charAt(0) === "+" || p.charAt(0) === "-" ||
|
|
339
|
+
p.charAt(0) === "~" || p.charAt(0) === "?") {
|
|
340
|
+
qualifier = p.charAt(0);
|
|
341
|
+
p = p.slice(1);
|
|
342
|
+
}
|
|
343
|
+
var colonAt = p.indexOf(":");
|
|
344
|
+
var slashAt = p.indexOf("/");
|
|
345
|
+
var sep = (colonAt !== -1 && (slashAt === -1 || colonAt < slashAt))
|
|
346
|
+
? colonAt : slashAt;
|
|
347
|
+
var mech = sep === -1 ? p : p.slice(0, sep);
|
|
348
|
+
var arg = sep === -1 ? null : p.slice(sep + 1);
|
|
349
|
+
// `raw` preserves the full mechanism+arg token after qualifier-
|
|
350
|
+
// strip. The a/mx dispatch arm reparses this directly because
|
|
351
|
+
// RFC 7208 §5.3/§5.4 allow `dual-cidr-length` after the optional
|
|
352
|
+
// domain-spec (e.g. `a:example.com/24//64`); the simple `arg`
|
|
353
|
+
// field above splits on the first separator and loses the
|
|
354
|
+
// information about whether that separator was `:` or `/`.
|
|
355
|
+
mechanisms.push({ qualifier: qualifier, mechanism: mech.toLowerCase(), arg: arg, raw: p });
|
|
356
|
+
}
|
|
357
|
+
// Surface modifiers via a non-enumerable property so callers that
|
|
358
|
+
// don't expect them don't see them in JSON-serialized records but
|
|
359
|
+
// _spfEvaluateDomain can react.
|
|
360
|
+
Object.defineProperty(mechanisms, "modifiers", { value: modifiers });
|
|
361
|
+
return mechanisms;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Fetch the SPF TXT record for a domain. Returns:
|
|
365
|
+
// { kind: "found", record: "<text>" } — exactly one v=spf1 record
|
|
366
|
+
// { kind: "none" } — zero v=spf1 records
|
|
367
|
+
// { kind: "permerror", reason: "<msg>" } — multiple v=spf1 records
|
|
368
|
+
// (RFC 7208 §4.5 — domain
|
|
369
|
+
// MUST publish at most one)
|
|
370
|
+
async function _fetchSpfRecord(domain, dnsLookup) {
|
|
371
|
+
var records;
|
|
372
|
+
try {
|
|
373
|
+
records = await _safeResolveTxt(domain, dnsLookup);
|
|
374
|
+
} catch (e) {
|
|
375
|
+
if (e && (e.code === "ENOTFOUND" || e.code === "ENODATA")) return { kind: "none" };
|
|
376
|
+
throw new MailAuthError("mail-auth/spf-lookup-failed",
|
|
377
|
+
"SPF TXT lookup for " + domain + " failed: " +
|
|
378
|
+
((e && e.message) || String(e)));
|
|
379
|
+
}
|
|
380
|
+
if (!Array.isArray(records)) return { kind: "none" };
|
|
381
|
+
var matches = [];
|
|
382
|
+
for (var i = 0; i < records.length; i += 1) {
|
|
383
|
+
var rec = Array.isArray(records[i]) ? records[i].join("") : records[i];
|
|
384
|
+
if (typeof rec === "string" && rec.indexOf("v=spf1") === 0) matches.push(rec);
|
|
385
|
+
}
|
|
386
|
+
if (matches.length === 0) return { kind: "none" };
|
|
387
|
+
if (matches.length > 1) {
|
|
388
|
+
return { kind: "permerror",
|
|
389
|
+
reason: "domain " + domain + " publishes " + matches.length +
|
|
390
|
+
" v=spf1 records; RFC 7208 §4.5 requires at most one" };
|
|
391
|
+
}
|
|
392
|
+
// RFC 7208 §3.3 — the SPF record (concatenated across multi-string
|
|
393
|
+
// TXT chunks) MUST NOT exceed 450 bytes. Receivers MUST refuse
|
|
394
|
+
// larger records (permerror) so a malformed-large policy can't
|
|
395
|
+
// amplify parser work.
|
|
396
|
+
if (matches[0].length > SPF_RECORD_MAX_BYTES) {
|
|
397
|
+
return { kind: "permerror",
|
|
398
|
+
reason: "domain " + domain + " SPF record is " + matches[0].length +
|
|
399
|
+
" bytes; RFC 7208 §3.3 caps at " + SPF_RECORD_MAX_BYTES };
|
|
400
|
+
}
|
|
401
|
+
return { kind: "found", record: matches[0] };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// RFC 7208 §5.3 / §5.4 — `a [ ":" domain-spec ] [ dual-cidr-length ]`
|
|
405
|
+
// and `mx [ ":" domain-spec ] [ dual-cidr-length ]`. dual-cidr-length
|
|
406
|
+
// is `[ "/" ip4-cidr ] [ "//" ip6-cidr ]`. Returns the parsed target
|
|
407
|
+
// domain plus per-family prefix lengths (32 / 128 when omitted).
|
|
408
|
+
//
|
|
409
|
+
// `raw` is the post-qualifier token (e.g. "a", "a:foo.com", "a/24",
|
|
410
|
+
// "a//64", "a:foo.com/24//64"). Throws MailAuthError on bad cidr.
|
|
411
|
+
function _parseADualCidr(raw, mech, defaultDomain) {
|
|
412
|
+
var rest = raw.slice(mech.length);
|
|
413
|
+
var domain = defaultDomain;
|
|
414
|
+
var v4Mask = 32; // allow:raw-byte-literal — IPv4 max prefix
|
|
415
|
+
var v6Mask = 128; // allow:raw-byte-literal — IPv6 max prefix
|
|
416
|
+
|
|
417
|
+
if (rest.charAt(0) === ":") {
|
|
418
|
+
rest = rest.slice(1);
|
|
419
|
+
var slashAt = rest.indexOf("/");
|
|
420
|
+
if (slashAt === -1) { domain = rest; rest = ""; }
|
|
421
|
+
else { domain = rest.slice(0, slashAt); rest = rest.slice(slashAt); }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (rest.length > 0) {
|
|
425
|
+
// rest is now "" | "/v4" | "//v6" | "/v4//v6".
|
|
426
|
+
var dblSlash = rest.indexOf("//");
|
|
427
|
+
var v4Part = "";
|
|
428
|
+
var v6Part = "";
|
|
429
|
+
if (dblSlash !== -1) {
|
|
430
|
+
v4Part = rest.slice(0, dblSlash); // "" or "/24"
|
|
431
|
+
v6Part = rest.slice(dblSlash + 2); // "64"
|
|
432
|
+
} else {
|
|
433
|
+
v4Part = rest; // "/24"
|
|
434
|
+
}
|
|
435
|
+
if (v4Part.length > 0) {
|
|
436
|
+
if (v4Part.charAt(0) !== "/") {
|
|
437
|
+
throw new MailAuthError("mail-auth/spf-bad-cidr",
|
|
438
|
+
"SPF " + mech + " dual-cidr malformed: " + JSON.stringify(raw));
|
|
439
|
+
}
|
|
440
|
+
var v4Str = v4Part.slice(1);
|
|
441
|
+
// RFC 7208 §5.3 / §5.4 — `ip4-cidr-length = "/" 1*DIGIT`. An
|
|
442
|
+
// empty digit segment (`a/`, `mx/`) is malformed grammar; the
|
|
443
|
+
// receiver MUST permerror. Pre-fix this silently kept the
|
|
444
|
+
// default /32 and would authorize the connecting IP under any
|
|
445
|
+
// A record of the target, which can over-authorize senders
|
|
446
|
+
// publishing `v=spf1 a/ -all` (would match every IP in the
|
|
447
|
+
// /32 of every A record).
|
|
448
|
+
if (v4Str.length === 0) {
|
|
449
|
+
throw new MailAuthError("mail-auth/spf-bad-cidr",
|
|
450
|
+
"SPF " + mech + " v4 cidr-length is empty (RFC 7208 §5.3/§5.4 grammar requires 1*DIGIT): " +
|
|
451
|
+
JSON.stringify(raw));
|
|
452
|
+
}
|
|
453
|
+
var v4n = parseInt(v4Str, 10);
|
|
454
|
+
if (!isFinite(v4n) || v4n < 0 || v4n > 32 || String(v4n) !== v4Str) { // allow:raw-byte-literal — IPv4 max prefix
|
|
455
|
+
throw new MailAuthError("mail-auth/spf-bad-cidr",
|
|
456
|
+
"SPF " + mech + " v4 cidr-length invalid: " + JSON.stringify(raw));
|
|
457
|
+
}
|
|
458
|
+
v4Mask = v4n;
|
|
459
|
+
}
|
|
460
|
+
// RFC 7208 §5.3 / §5.4 — `ip6-cidr-length = "/" 1*DIGIT` (after
|
|
461
|
+
// the "//" separator). When the `//` separator IS present (i.e.
|
|
462
|
+
// the raw token contained `//`) the digit segment MUST be 1*DIGIT.
|
|
463
|
+
// Empty (`a//`, `a/24//`, `mx//`) is malformed grammar; permerror.
|
|
464
|
+
if (dblSlash !== -1) {
|
|
465
|
+
if (v6Part.length === 0) {
|
|
466
|
+
throw new MailAuthError("mail-auth/spf-bad-cidr",
|
|
467
|
+
"SPF " + mech + " v6 cidr-length is empty (RFC 7208 §5.3/§5.4 grammar requires 1*DIGIT): " +
|
|
468
|
+
JSON.stringify(raw));
|
|
469
|
+
}
|
|
470
|
+
var v6n = parseInt(v6Part, 10);
|
|
471
|
+
if (!isFinite(v6n) || v6n < 0 || v6n > 128 || String(v6n) !== v6Part) { // allow:raw-byte-literal — IPv6 max prefix
|
|
472
|
+
throw new MailAuthError("mail-auth/spf-bad-cidr",
|
|
473
|
+
"SPF " + mech + " v6 cidr-length invalid: " + JSON.stringify(raw));
|
|
474
|
+
}
|
|
475
|
+
v6Mask = v6n;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (!domain || domain.length === 0) {
|
|
480
|
+
throw new MailAuthError("mail-auth/spf-bad-cidr",
|
|
481
|
+
"SPF " + mech + " has no target domain (current-domain unavailable)");
|
|
482
|
+
}
|
|
483
|
+
return { domain: domain.toLowerCase(), v4Mask: v4Mask, v6Mask: v6Mask };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// RFC 7208 §5.3 / §5.4 — `a` and `mx` mechanism evaluation. Both
|
|
487
|
+
// resolve the target domain (or the current SPF-evaluating domain when
|
|
488
|
+
// arg omitted) to a set of IP addresses; the connecting IP matches if
|
|
489
|
+
// it falls inside any of those addresses under the parsed cidr prefix.
|
|
490
|
+
//
|
|
491
|
+
// Lookup accounting per §4.6.4:
|
|
492
|
+
// - `a`: the outer evaluator has already counted this as one DNS-
|
|
493
|
+
// touching mechanism. The single A/AAAA query is THAT one
|
|
494
|
+
// lookup; no additional increment here.
|
|
495
|
+
// - `mx`: the outer evaluator has counted the MX query itself.
|
|
496
|
+
// EACH MX hostname's A/AAAA expansion adds an additional
|
|
497
|
+
// lookup; total expansion is capped at 10 MX hostnames per
|
|
498
|
+
// §4.6.4 (the explicit "MX limit"). Crossing the global
|
|
499
|
+
// 10-lookup ceiling at any expansion step permerrors.
|
|
500
|
+
//
|
|
501
|
+
// Returns one of:
|
|
502
|
+
// { match: true } — connecting IP matched
|
|
503
|
+
// { match: false } — no IP matched / record absent
|
|
504
|
+
// { error: "temperror", reason: "..." } — transient DNS failure
|
|
505
|
+
// { error: "permerror", reason: "..." } — over-limit / bad CIDR / bad MX count
|
|
506
|
+
async function _spfMatchAMx(mech, raw, ip, isIpv6, defaultDomain, dnsLookup, lookups) {
|
|
507
|
+
var parsed;
|
|
508
|
+
try { parsed = _parseADualCidr(raw, mech, defaultDomain); }
|
|
509
|
+
catch (e) { return { error: "permerror", reason: e.message }; }
|
|
510
|
+
|
|
511
|
+
var mask = isIpv6 ? parsed.v6Mask : parsed.v4Mask;
|
|
512
|
+
var family = isIpv6 ? 6 : 4; // allow:raw-byte-literal — IP family marker
|
|
513
|
+
|
|
514
|
+
var targetIps = [];
|
|
515
|
+
if (mech === "a") {
|
|
516
|
+
try { targetIps = await _safeResolveA(parsed.domain, family, dnsLookup); }
|
|
517
|
+
catch (e) {
|
|
518
|
+
var code = e && e.code;
|
|
519
|
+
if (code === "ENOTFOUND" || code === "ENODATA") return { match: false };
|
|
520
|
+
return { error: "temperror",
|
|
521
|
+
reason: "SPF a:" + parsed.domain + " lookup failed: " +
|
|
522
|
+
((e && e.message) || String(e)) };
|
|
523
|
+
}
|
|
524
|
+
} else { // mech === "mx"
|
|
525
|
+
var mxHosts;
|
|
526
|
+
try { mxHosts = await _safeResolveMx(parsed.domain, dnsLookup); }
|
|
527
|
+
catch (e) {
|
|
528
|
+
var mcode = e && e.code;
|
|
529
|
+
if (mcode === "ENOTFOUND" || mcode === "ENODATA") return { match: false };
|
|
530
|
+
return { error: "temperror",
|
|
531
|
+
reason: "SPF mx:" + parsed.domain + " MX lookup failed: " +
|
|
532
|
+
((e && e.message) || String(e)) };
|
|
533
|
+
}
|
|
534
|
+
// RFC 7208 §4.6.4 — the MX expansion is capped at 10 hostnames.
|
|
535
|
+
// Crossing this is a permerror; receivers MUST NOT silently
|
|
536
|
+
// truncate, since a misconfigured sender publishing 20 MX hosts
|
|
537
|
+
// would otherwise have only the first 10 contribute to authz.
|
|
538
|
+
if (mxHosts.length > 10) { // allow:raw-byte-literal — RFC 7208 §4.6.4 MX limit
|
|
539
|
+
return { error: "permerror",
|
|
540
|
+
reason: "SPF mx:" + parsed.domain + " resolved " + mxHosts.length +
|
|
541
|
+
" MX hosts (RFC 7208 §4.6.4 caps at 10)" };
|
|
542
|
+
}
|
|
543
|
+
for (var mi = 0; mi < mxHosts.length; mi += 1) {
|
|
544
|
+
lookups.count += 1;
|
|
545
|
+
if (lookups.count > lookups.limit) {
|
|
546
|
+
return { error: "permerror",
|
|
547
|
+
reason: "DNS lookup limit exceeded (RFC 7208 §4.6.4) during mx:" +
|
|
548
|
+
parsed.domain + " expansion" };
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
var hostIps = await _safeResolveA(mxHosts[mi], family, dnsLookup);
|
|
552
|
+
for (var hi = 0; hi < hostIps.length; hi += 1) targetIps.push(hostIps[hi]);
|
|
553
|
+
} catch (e) {
|
|
554
|
+
var hcode = e && e.code;
|
|
555
|
+
if (hcode === "ENOTFOUND" || hcode === "ENODATA") {
|
|
556
|
+
// Void lookup — counts toward §4.6.4 ceiling for the MX
|
|
557
|
+
// expansion (the MX hostname has no A/AAAA in the relevant
|
|
558
|
+
// family). Some hosts are v4-only and won't have AAAA; we
|
|
559
|
+
// skip the host but charge the void slot.
|
|
560
|
+
lookups.void = (lookups.void || 0) + 1;
|
|
561
|
+
if (lookups.void > SPF_VOID_LOOKUP_LIMIT) {
|
|
562
|
+
return { error: "permerror",
|
|
563
|
+
reason: "SPF void-lookup limit exceeded (RFC 7208 §4.6.4) during mx expansion" };
|
|
564
|
+
}
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
return { error: "temperror",
|
|
568
|
+
reason: "SPF mx host " + mxHosts[mi] + " A/AAAA lookup failed: " +
|
|
569
|
+
((e && e.message) || String(e)) };
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
for (var ti = 0; ti < targetIps.length; ti += 1) {
|
|
575
|
+
var cidr = targetIps[ti] + "/" + mask;
|
|
576
|
+
if (isIpv6) { if (_ipv6InCidr(ip, cidr)) return { match: true }; }
|
|
577
|
+
else { if (_ipv4InCidr(ip, cidr)) return { match: true }; }
|
|
578
|
+
}
|
|
579
|
+
return { match: false };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// SPF verify — recursive include resolution + ip4 / ip6 / a / mx /
|
|
583
|
+
// include / all / redirect=. The `exists` and `ptr` mechanisms +
|
|
584
|
+
// macro-string expansion remain deferred (see the mechanism dispatch
|
|
585
|
+
// arm for the Re-open condition + operator escape hatch).
|
|
586
|
+
async function spfVerify(opts) {
|
|
587
|
+
opts = opts || {};
|
|
588
|
+
validateOpts(opts, ["ip", "mailFrom", "helo", "dnsLookup"], "mail.spf.verify");
|
|
589
|
+
if (typeof opts.ip !== "string") {
|
|
590
|
+
throw new MailAuthError("mail-auth/spf-bad-ip",
|
|
591
|
+
"spf.verify: ip must be a string");
|
|
592
|
+
}
|
|
593
|
+
var domain = opts.mailFrom
|
|
594
|
+
? String(opts.mailFrom).split("@")[1]
|
|
595
|
+
: opts.helo;
|
|
596
|
+
if (typeof domain !== "string" || domain.length === 0) {
|
|
597
|
+
throw new MailAuthError("mail-auth/spf-bad-domain",
|
|
598
|
+
"spf.verify: mailFrom or helo is required");
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
var lookups = { count: 0, limit: SPF_DNS_LOOKUP_LIMIT, void: 0 };
|
|
602
|
+
// RFC 7208 §4.6.4 — the initial query for the sender domain's SPF
|
|
603
|
+
// record itself does NOT count toward the 10-lookup limit. Only
|
|
604
|
+
// include / a / mx / ptr / exists / redirect mechanisms count.
|
|
605
|
+
// Pre-v0.8.17 this was off-by-one — senders at the spec ceiling
|
|
606
|
+
// got false permerror.
|
|
607
|
+
var result = await _spfEvaluateDomain(domain.toLowerCase(), opts.ip,
|
|
608
|
+
opts.dnsLookup, lookups,
|
|
609
|
+
{ isInitial: true });
|
|
610
|
+
return {
|
|
611
|
+
result: result.verdict, // pass | fail | softfail | neutral | none | temperror | permerror
|
|
612
|
+
domain: domain,
|
|
613
|
+
explanation: result.explanation,
|
|
614
|
+
lookupCount: lookups.count,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function _spfEvaluateDomain(domain, ip, dnsLookup, lookups, ctx) {
|
|
619
|
+
ctx = ctx || {};
|
|
620
|
+
if (lookups.count > lookups.limit) {
|
|
621
|
+
return { verdict: "permerror", explanation: "DNS lookup limit exceeded (RFC 7208 §4.6.4)" };
|
|
622
|
+
}
|
|
623
|
+
// RFC 7208 §4.6.4 — void-lookup ceiling. Each successful query that
|
|
624
|
+
// returns 0 records (NXDOMAIN, no-data) counts. Beyond 2, permerror.
|
|
625
|
+
if ((lookups.void || 0) > SPF_VOID_LOOKUP_LIMIT) {
|
|
626
|
+
return { verdict: "permerror",
|
|
627
|
+
explanation: "SPF void-lookup limit exceeded (RFC 7208 §4.6.4)" };
|
|
628
|
+
}
|
|
629
|
+
// RFC 7208 §6.1 — redirect= recursion bound. Per-evaluation
|
|
630
|
+
// re-entries via redirect MUST terminate. The lookup limit also
|
|
631
|
+
// catches pathological chains; this bound is the belt-and-braces.
|
|
632
|
+
if ((ctx.redirectDepth || 0) > SPF_REDIRECT_DEPTH_LIMIT) {
|
|
633
|
+
return { verdict: "permerror",
|
|
634
|
+
explanation: "SPF redirect= recursion limit exceeded (RFC 7208 §6.1)" };
|
|
635
|
+
}
|
|
636
|
+
// Initial query for the sender's SPF record doesn't count (RFC 7208
|
|
637
|
+
// §4.6.4); only include / a / mx / ptr / exists / redirect do.
|
|
638
|
+
if (!ctx.isInitial) lookups.count += 1;
|
|
639
|
+
|
|
640
|
+
var fetched;
|
|
641
|
+
try { fetched = await _fetchSpfRecord(domain, dnsLookup); }
|
|
642
|
+
catch (e) {
|
|
643
|
+
return { verdict: "temperror", explanation: e.message };
|
|
644
|
+
}
|
|
645
|
+
if (fetched.kind === "permerror") {
|
|
646
|
+
return { verdict: "permerror", explanation: fetched.reason };
|
|
647
|
+
}
|
|
648
|
+
if (fetched.kind === "none") {
|
|
649
|
+
// Void lookup — count toward §4.6.4 ceiling. Initial query
|
|
650
|
+
// doesn't count as a "lookup" but DOES count as void if the
|
|
651
|
+
// sender has no SPF (mirrors the spec's intent: a misconfigured
|
|
652
|
+
// sender that publishes no record still consumes a slot).
|
|
653
|
+
lookups.void = (lookups.void || 0) + 1;
|
|
654
|
+
return { verdict: "none", explanation: "no SPF record at " + domain };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
var mechanisms;
|
|
658
|
+
try { mechanisms = _parseSpfRecord(fetched.record); }
|
|
659
|
+
catch (e) {
|
|
660
|
+
return { verdict: "permerror", explanation: e.message };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
var isIpv6 = ip.indexOf(":") !== -1;
|
|
664
|
+
for (var i = 0; i < mechanisms.length; i += 1) {
|
|
665
|
+
var m = mechanisms[i];
|
|
666
|
+
var match = false;
|
|
667
|
+
if (m.mechanism === "all") match = true;
|
|
668
|
+
else if (!isIpv6 && (m.mechanism === "ip4" || m.mechanism === "ipv4")) {
|
|
669
|
+
if (m.arg && _ipv4InCidr(ip, m.arg)) match = true;
|
|
670
|
+
} else if (isIpv6 && (m.mechanism === "ip6" || m.mechanism === "ipv6")) {
|
|
671
|
+
if (m.arg && _ipv6InCidr(ip, m.arg)) match = true;
|
|
672
|
+
} else if (m.mechanism === "include") {
|
|
673
|
+
if (!m.arg) continue;
|
|
674
|
+
var inner = await _spfEvaluateDomain(m.arg.toLowerCase(), ip, dnsLookup, lookups);
|
|
675
|
+
if (inner.verdict === "pass") match = true;
|
|
676
|
+
else if (inner.verdict === "permerror" || inner.verdict === "temperror") {
|
|
677
|
+
return inner;
|
|
678
|
+
}
|
|
679
|
+
// RFC 7208 §5.2 — when the included domain has no SPF record at
|
|
680
|
+
// all, the include itself MUST permerror (the included policy is
|
|
681
|
+
// missing, the operator's intent is unverifiable). Without this
|
|
682
|
+
// check `include:gone-domain.example` silently authorizes whatever
|
|
683
|
+
// mechanism follows, including `+all`.
|
|
684
|
+
else if (inner.verdict === "none") {
|
|
685
|
+
return { verdict: "permerror",
|
|
686
|
+
explanation: "include:" + m.arg + " has no SPF record (RFC 7208 §5.2)" };
|
|
687
|
+
}
|
|
688
|
+
} else if (m.mechanism === "a" || m.mechanism === "mx") {
|
|
689
|
+
// RFC 7208 §5.3 / §5.4. The mechanism itself counts as one DNS
|
|
690
|
+
// lookup per §4.6.4 (already incremented by the outer loop's
|
|
691
|
+
// `lookups.count += 1` for non-initial domains; ip4/ip6/all are
|
|
692
|
+
// overcounted as a result, but only by mechanisms whose lookup
|
|
693
|
+
// budget the spec doesn't care about — they're not DNS-touching).
|
|
694
|
+
// The `a` / `mx` arms additionally expand per RFC §4.6.4 (each
|
|
695
|
+
// MX hostname adds another lookup); the helper handles that
|
|
696
|
+
// accounting.
|
|
697
|
+
lookups.count += 1;
|
|
698
|
+
if (lookups.count > lookups.limit) {
|
|
699
|
+
return { verdict: "permerror",
|
|
700
|
+
explanation: "DNS lookup limit exceeded (RFC 7208 §4.6.4) at " +
|
|
701
|
+
m.mechanism };
|
|
702
|
+
}
|
|
703
|
+
var amRes = await _spfMatchAMx(m.mechanism, m.raw, ip, isIpv6,
|
|
704
|
+
domain, dnsLookup, lookups);
|
|
705
|
+
if (amRes.error === "permerror") {
|
|
706
|
+
return { verdict: "permerror", explanation: amRes.reason };
|
|
707
|
+
}
|
|
708
|
+
if (amRes.error === "temperror") {
|
|
709
|
+
return { verdict: "temperror", explanation: amRes.reason };
|
|
710
|
+
}
|
|
711
|
+
if (amRes.match) match = true;
|
|
712
|
+
} else if (m.mechanism === "exists" || m.mechanism === "ptr") {
|
|
713
|
+
// RFC 7208 §5.7 (exists) + §5.5 (ptr) — deferred from v0.11.3.
|
|
714
|
+
//
|
|
715
|
+
// exists: requires macro-string expansion (RFC 7208 §7) to be
|
|
716
|
+
// useful in practice; almost every published `exists:` policy
|
|
717
|
+
// uses macros like `exists:%{l}.%{d}._spf.example.com` to do
|
|
718
|
+
// per-recipient or per-IP lookups. A non-macro `exists:` is
|
|
719
|
+
// technically valid but vanishingly rare in published policies.
|
|
720
|
+
//
|
|
721
|
+
// ptr: RFC 7208 §5.5 explicitly says "use of this mechanism
|
|
722
|
+
// is strongly discouraged" — the receiver does reverse-DNS +
|
|
723
|
+
// forward-confirm per query, doubling DNS load and tying the
|
|
724
|
+
// sender's authz to whoever controls their PTR zone. Despite
|
|
725
|
+
// this discouragement, a small minority of legacy senders
|
|
726
|
+
// still publish `+ptr -all` policies as their only SPF stance.
|
|
727
|
+
//
|
|
728
|
+
// Re-open conditions:
|
|
729
|
+
// - exists: macro-string expansion lands in the framework (a
|
|
730
|
+
// standalone slice; tracked under blamejs-roadmap.md), OR an
|
|
731
|
+
// operator surfaces a real `exists:` policy without macros
|
|
732
|
+
// and asks for the simple A-existence form.
|
|
733
|
+
// - ptr: an operator surfaces a legitimate sender whose
|
|
734
|
+
// ONLY SPF stance is `ptr` and needs the framework to
|
|
735
|
+
// evaluate it (rather than the operator's MTA already doing
|
|
736
|
+
// iprev via `b.mail.auth.iprev`).
|
|
737
|
+
//
|
|
738
|
+
// Operator escape hatch today:
|
|
739
|
+
// - exists: senders almost universally have a non-`exists:`
|
|
740
|
+
// mechanism alongside; the framework returns "permerror"
|
|
741
|
+
// here, surfacing the gap, but legitimate mail flow that
|
|
742
|
+
// ALSO carries a passing ip4/ip6/include path is unaffected.
|
|
743
|
+
// - ptr: operators evaluating a ptr-only sender wire
|
|
744
|
+
// `b.mail.auth.iprev(ip)` and treat fcrdns=true the same as
|
|
745
|
+
// SPF pass for that domain.
|
|
746
|
+
return {
|
|
747
|
+
verdict: "permerror",
|
|
748
|
+
explanation: "SPF mechanism '" + m.mechanism + "' is not yet implemented (RFC 7208 §" +
|
|
749
|
+
(m.mechanism === "exists" ? "5.7 + §7 macros" : "5.5") +
|
|
750
|
+
"); senders typically publish ip4 / ip6 / a / mx / include alongside",
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
if (match) {
|
|
754
|
+
var qualifier = m.qualifier;
|
|
755
|
+
var verdict = qualifier === "+" ? "pass" :
|
|
756
|
+
qualifier === "-" ? "fail" :
|
|
757
|
+
qualifier === "~" ? "softfail" :
|
|
758
|
+
qualifier === "?" ? "neutral" : "neutral";
|
|
759
|
+
return { verdict: verdict, explanation: "matched " + m.mechanism +
|
|
760
|
+
(m.arg ? ":" + m.arg : "") };
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// RFC 7208 §6.1 — `redirect=<domain>` modifier: when no mechanism
|
|
765
|
+
// matched, fall through to the target domain's policy. The redirect
|
|
766
|
+
// is ignored if an `all` mechanism is present (since `all` matches
|
|
767
|
+
// unconditionally, the redirect is unreachable by construction).
|
|
768
|
+
// Pre-this-patch the redirect= modifier was silently dropped — a
|
|
769
|
+
// domain whose only policy was `v=spf1 redirect=_spf.example.com`
|
|
770
|
+
// returned "neutral" instead of the redirected verdict, leaving
|
|
771
|
+
// every legitimate sender unauthenticated.
|
|
772
|
+
var mods = mechanisms.modifiers || [];
|
|
773
|
+
for (var rmi = 0; rmi < mods.length; rmi += 1) {
|
|
774
|
+
if (mods[rmi].name === "redirect" && mods[rmi].value) {
|
|
775
|
+
// Redirect counts as one DNS-mechanism per §4.6.4.
|
|
776
|
+
var redirected = await _spfEvaluateDomain(
|
|
777
|
+
mods[rmi].value.toLowerCase(), ip, dnsLookup, lookups,
|
|
778
|
+
{ redirectDepth: (ctx.redirectDepth || 0) + 1 });
|
|
779
|
+
// RFC 7208 §6.1 — if the redirect target has no SPF record,
|
|
780
|
+
// permerror (the operator's intent is unverifiable).
|
|
781
|
+
if (redirected.verdict === "none") {
|
|
782
|
+
return { verdict: "permerror",
|
|
783
|
+
explanation: "redirect=" + mods[rmi].value +
|
|
784
|
+
" has no SPF record (RFC 7208 §6.1)" };
|
|
785
|
+
}
|
|
786
|
+
return redirected;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return { verdict: "neutral", explanation: "no mechanism matched" };
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ---- DMARC (RFC 7489) ----
|
|
794
|
+
|
|
795
|
+
async function _fetchDmarcRecord(domain, dnsLookup) {
|
|
796
|
+
var qname = "_dmarc." + domain.toLowerCase();
|
|
797
|
+
var records;
|
|
798
|
+
try {
|
|
799
|
+
records = await _safeResolveTxt(qname, dnsLookup);
|
|
800
|
+
} catch (e) {
|
|
801
|
+
if (e && (e.code === "ENOTFOUND" || e.code === "ENODATA")) return null;
|
|
802
|
+
throw new MailAuthError("mail-auth/dmarc-lookup-failed",
|
|
803
|
+
"DMARC TXT lookup for " + qname + " failed: " +
|
|
804
|
+
((e && e.message) || String(e)));
|
|
805
|
+
}
|
|
806
|
+
if (!Array.isArray(records)) return null;
|
|
807
|
+
var matches = [];
|
|
808
|
+
for (var i = 0; i < records.length; i += 1) {
|
|
809
|
+
var rec = Array.isArray(records[i]) ? records[i].join("") : records[i];
|
|
810
|
+
if (typeof rec === "string" && rec.indexOf("v=DMARC1") === 0) matches.push(rec);
|
|
811
|
+
}
|
|
812
|
+
if (matches.length === 0) return null;
|
|
813
|
+
// RFC 7489 §6.6.3 — when multiple v=DMARC1 records are published,
|
|
814
|
+
// the receiver MUST treat the domain as having no DMARC record.
|
|
815
|
+
if (matches.length > 1) return null;
|
|
816
|
+
return matches[0];
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// RFC 7489 base policy keys + DMARCbis (draft-ietf-dmarc-dmarcbis)
|
|
820
|
+
// extensions:
|
|
821
|
+
// np=<none|quarantine|reject> policy for non-existent subdomains
|
|
822
|
+
// psd=<y|n|u> applies-at-public-suffix-domain (TLD
|
|
823
|
+
// operator publishes a DMARC record on
|
|
824
|
+
// the suffix itself)
|
|
825
|
+
// Validation tier: parse is config-time (operator-supplied DNS bytes);
|
|
826
|
+
// throw on malformed v= / unrecognized np= or psd= values rather than
|
|
827
|
+
// silently dropping — operators with a typo'd record otherwise see the
|
|
828
|
+
// fallback policy applied without warning.
|
|
829
|
+
var DMARCBIS_VALID_NP = { none: 1, quarantine: 1, reject: 1 };
|
|
830
|
+
var DMARCBIS_VALID_PSD = { y: 1, n: 1, u: 1 };
|
|
831
|
+
|
|
832
|
+
function _parseDmarcRecord(text) {
|
|
833
|
+
var policy = { v: null, p: null, sp: null, np: null, psd: null,
|
|
834
|
+
pct: 100, adkim: "r", aspf: "r" }; // allow:raw-byte-literal — RFC 7489 default pct
|
|
835
|
+
var pairs = text.split(";"); // allow:bare-split-on-quoted-header — RFC 7489 §6.4 DMARC tag-list grammar: `tag-spec *( ";" tag-spec )` with tag-value = 0*( tval *( WSP / FWS ) ); NO quoted-string allowed
|
|
836
|
+
for (var i = 0; i < pairs.length; i += 1) {
|
|
837
|
+
var kv = pairs[i].trim();
|
|
838
|
+
if (kv.length === 0) continue;
|
|
839
|
+
var eq = kv.indexOf("=");
|
|
840
|
+
if (eq === -1) continue;
|
|
841
|
+
var key = kv.slice(0, eq).trim().toLowerCase();
|
|
842
|
+
var val = kv.slice(eq + 1).trim();
|
|
843
|
+
if (key === "v") policy.v = val;
|
|
844
|
+
else if (key === "p") policy.p = val.toLowerCase();
|
|
845
|
+
else if (key === "sp") policy.sp = val.toLowerCase();
|
|
846
|
+
else if (key === "pct") policy.pct = parseInt(val, 10);
|
|
847
|
+
else if (key === "adkim") policy.adkim = val.toLowerCase();
|
|
848
|
+
else if (key === "aspf") policy.aspf = val.toLowerCase();
|
|
849
|
+
else if (key === "np") {
|
|
850
|
+
var npVal = val.toLowerCase();
|
|
851
|
+
if (!DMARCBIS_VALID_NP[npVal]) {
|
|
852
|
+
throw new MailAuthError("mail-auth/dmarcbis-bad-tag",
|
|
853
|
+
"DMARC np= must be one of none|quarantine|reject, got " + JSON.stringify(val));
|
|
854
|
+
}
|
|
855
|
+
policy.np = npVal;
|
|
856
|
+
}
|
|
857
|
+
else if (key === "psd") {
|
|
858
|
+
var psdVal = val.toLowerCase();
|
|
859
|
+
if (!DMARCBIS_VALID_PSD[psdVal]) {
|
|
860
|
+
throw new MailAuthError("mail-auth/dmarcbis-bad-tag",
|
|
861
|
+
"DMARC psd= must be one of y|n|u, got " + JSON.stringify(val));
|
|
862
|
+
}
|
|
863
|
+
policy.psd = psdVal;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (policy.v !== "DMARC1") {
|
|
867
|
+
throw new MailAuthError("mail-auth/dmarc-bad-version",
|
|
868
|
+
"DMARC record version must be DMARC1, got " + JSON.stringify(policy.v));
|
|
869
|
+
}
|
|
870
|
+
return policy;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function _alignmentCheck(fromDomain, authDomain, mode) {
|
|
874
|
+
if (!fromDomain || !authDomain) return false;
|
|
875
|
+
var f = fromDomain.toLowerCase();
|
|
876
|
+
var a = authDomain.toLowerCase();
|
|
877
|
+
if (mode === "s") return f === a; // strict
|
|
878
|
+
// RFC 7489 §3.1.1 + DMARCbis §4.4 — relaxed alignment compares the
|
|
879
|
+
// organizational domain (the public-suffix-tail registered name).
|
|
880
|
+
// Earlier shape did a naive `endsWith` text-suffix check which over-
|
|
881
|
+
// approximated alignment: `evil-bank.com` and `bank.com` looked
|
|
882
|
+
// aligned even though they're separately registered. PSL lookup
|
|
883
|
+
// closes the gap.
|
|
884
|
+
if (f === a) return true;
|
|
885
|
+
var fOrg = null;
|
|
886
|
+
var aOrg = null;
|
|
887
|
+
try { fOrg = publicSuffix.organizationalDomain(f); } catch (_e) { fOrg = null; }
|
|
888
|
+
try { aOrg = publicSuffix.organizationalDomain(a); } catch (_e) { aOrg = null; }
|
|
889
|
+
if (fOrg && aOrg && fOrg === aOrg) return true;
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function dmarcEvaluate(opts) {
|
|
894
|
+
opts = opts || {};
|
|
895
|
+
validateOpts(opts, ["from", "spf", "dkim", "dnsLookup", "domainExists",
|
|
896
|
+
"pctSampleKey"],
|
|
897
|
+
"mail.dmarc.evaluate");
|
|
898
|
+
if (typeof opts.from !== "string") {
|
|
899
|
+
throw new MailAuthError("mail-auth/dmarc-bad-from",
|
|
900
|
+
"dmarc.evaluate: opts.from must be the From-header email address");
|
|
901
|
+
}
|
|
902
|
+
var fromDomain = opts.from.split("@")[1];
|
|
903
|
+
if (!fromDomain) {
|
|
904
|
+
throw new MailAuthError("mail-auth/dmarc-bad-from",
|
|
905
|
+
"dmarc.evaluate: opts.from is missing the @domain part");
|
|
906
|
+
}
|
|
907
|
+
fromDomain = fromDomain.toLowerCase();
|
|
908
|
+
|
|
909
|
+
// DMARCbis (draft-ietf-dmarc-dmarcbis) replaces the legacy "drop one
|
|
910
|
+
// label" org-domain heuristic with a proper Public Suffix List lookup.
|
|
911
|
+
// organizationalDomain returns null when the input IS a public suffix
|
|
912
|
+
// (e.g. "co.uk") OR when no PSL match resolves; either way, the
|
|
913
|
+
// org-domain walk below short-circuits.
|
|
914
|
+
var orgDomain = null;
|
|
915
|
+
try { orgDomain = publicSuffix.organizationalDomain(fromDomain); }
|
|
916
|
+
catch (_e) { orgDomain = null; }
|
|
917
|
+
|
|
918
|
+
var policy = null;
|
|
919
|
+
var policyOriginDomain = null;
|
|
920
|
+
var orgDomainPolicyApplied = false;
|
|
921
|
+
var psdPolicyApplied = false;
|
|
922
|
+
try {
|
|
923
|
+
var rec = await _fetchDmarcRecord(fromDomain, opts.dnsLookup);
|
|
924
|
+
if (rec) {
|
|
925
|
+
policy = _parseDmarcRecord(rec);
|
|
926
|
+
policyOriginDomain = fromDomain;
|
|
927
|
+
} else if (orgDomain && orgDomain !== fromDomain) {
|
|
928
|
+
// RFC 7489 §6.6.3 + DMARCbis §4.6 — fall through to organizational
|
|
929
|
+
// domain. When the org-domain record sets sp= it applies to this
|
|
930
|
+
// subdomain; otherwise p= is the operative policy.
|
|
931
|
+
var orgRec = await _fetchDmarcRecord(orgDomain, opts.dnsLookup);
|
|
932
|
+
if (orgRec) {
|
|
933
|
+
var orgPolicy = _parseDmarcRecord(orgRec);
|
|
934
|
+
orgPolicy.p = orgPolicy.sp || orgPolicy.p;
|
|
935
|
+
policy = orgPolicy;
|
|
936
|
+
policyOriginDomain = orgDomain;
|
|
937
|
+
orgDomainPolicyApplied = true;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// DMARCbis §4.7 — when the org-domain record carries `psd=y`, OR
|
|
942
|
+
// the published record sits at the public suffix itself (TLD
|
|
943
|
+
// operator), the receiver continues lookup at the public suffix
|
|
944
|
+
// for downstream DSP cooperation. We honor the `psd=y` opt-in by
|
|
945
|
+
// surfacing the tag so operators can route on it; the explicit
|
|
946
|
+
// suffix walk below covers the suffix-record case.
|
|
947
|
+
if (!policy) {
|
|
948
|
+
var suffix = null;
|
|
949
|
+
try { suffix = publicSuffix.publicSuffix(fromDomain); }
|
|
950
|
+
catch (_e) { suffix = null; }
|
|
951
|
+
if (suffix && suffix !== fromDomain && suffix !== orgDomain) {
|
|
952
|
+
var psdRec = await _fetchDmarcRecord(suffix, opts.dnsLookup);
|
|
953
|
+
if (psdRec) {
|
|
954
|
+
var psdPolicy = _parseDmarcRecord(psdRec);
|
|
955
|
+
if (psdPolicy.psd === "y") {
|
|
956
|
+
psdPolicy.p = psdPolicy.sp || psdPolicy.p;
|
|
957
|
+
policy = psdPolicy;
|
|
958
|
+
policyOriginDomain = suffix;
|
|
959
|
+
psdPolicyApplied = true;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
} catch (e) {
|
|
965
|
+
return { result: "temperror", explanation: e.message,
|
|
966
|
+
policy: null, alignment: { spf: false, dkim: false },
|
|
967
|
+
orgDomain: orgDomain };
|
|
968
|
+
}
|
|
969
|
+
if (!policy) {
|
|
970
|
+
return { result: "none", explanation: "no DMARC record at _dmarc." + fromDomain,
|
|
971
|
+
policy: null, alignment: { spf: false, dkim: false },
|
|
972
|
+
orgDomain: orgDomain };
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// DMARCbis §4.8 — non-existent subdomain (NXDOMAIN on MX/A/AAAA for
|
|
976
|
+
// the message-from domain) gets the np= policy when published. The
|
|
977
|
+
// operator wires the existence check via opts.domainExists; absent
|
|
978
|
+
// that callback we conservatively treat the domain as existing
|
|
979
|
+
// (the np= path is opt-in observability, not a downgrade gate).
|
|
980
|
+
var npApplied = false;
|
|
981
|
+
if (typeof policy.np === "string" && typeof opts.domainExists === "function" &&
|
|
982
|
+
orgDomainPolicyApplied) {
|
|
983
|
+
var exists = true;
|
|
984
|
+
try { exists = await opts.domainExists(fromDomain); }
|
|
985
|
+
catch (_e) { exists = true; }
|
|
986
|
+
if (exists === false) {
|
|
987
|
+
policy = Object.assign({}, policy, { p: policy.np });
|
|
988
|
+
npApplied = true;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
var spfDomain = (opts.spf && opts.spf.domain) || null;
|
|
993
|
+
var dkimResults = Array.isArray(opts.dkim) ? opts.dkim : (opts.dkim ? [opts.dkim] : []);
|
|
994
|
+
|
|
995
|
+
var spfAligned = opts.spf && opts.spf.result === "pass" &&
|
|
996
|
+
_alignmentCheck(fromDomain, spfDomain, policy.aspf);
|
|
997
|
+
var dkimAligned = false;
|
|
998
|
+
for (var i = 0; i < dkimResults.length; i += 1) {
|
|
999
|
+
var d = dkimResults[i];
|
|
1000
|
+
if (d && d.result === "pass" &&
|
|
1001
|
+
_alignmentCheck(fromDomain, d.d || d.domain, policy.adkim)) {
|
|
1002
|
+
dkimAligned = true;
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
var pass = spfAligned || dkimAligned;
|
|
1008
|
+
// RFC 7489 §6.6.4 — pct= MUST be consulted when the disposition is
|
|
1009
|
+
// not "deliver". When pct is < 100 the receiver applies the policy
|
|
1010
|
+
// to that fraction of failing messages and the rest gets the next-
|
|
1011
|
+
// less-strict disposition (reject → quarantine; quarantine → none).
|
|
1012
|
+
//
|
|
1013
|
+
// Sampling determinism: a single message MUST receive the same
|
|
1014
|
+
// sampled/not-sampled verdict across retries. `Math.random()` re-
|
|
1015
|
+
// rolls per-call so the receiver's first attempt could deliver
|
|
1016
|
+
// (sampled=true → quarantine→none) while a retry rejected — leading
|
|
1017
|
+
// to inconsistent disposition for the same SMTP envelope. Derive
|
|
1018
|
+
// the sample roll from a stable per-message key (operator-supplied
|
|
1019
|
+
// `pctSampleKey` — typically the Message-ID + From-domain + a
|
|
1020
|
+
// receiver-side secret) hashed via SHAKE256, mapped to [0,100). When
|
|
1021
|
+
// the operator doesn't supply a key we fall back to a per-call
|
|
1022
|
+
// crypto.randomInt — still cryptographically uniform, just not
|
|
1023
|
+
// retry-stable. The fallback is the framework's hardening floor
|
|
1024
|
+
// (replaces Math.random); retry-stability requires the operator to
|
|
1025
|
+
// wire a key.
|
|
1026
|
+
var pctRaw = parseInt(policy.pct, 10); // allow:raw-byte-literal — pct percentage, not bytes
|
|
1027
|
+
var pct = isFinite(pctRaw) && pctRaw >= 0 && pctRaw <= 100 ? pctRaw : 100; // allow:raw-byte-literal — pct percentage, not bytes
|
|
1028
|
+
var sampleRoll;
|
|
1029
|
+
if (typeof opts.pctSampleKey === "string" && opts.pctSampleKey.length > 0) {
|
|
1030
|
+
// Deterministic per-message sample roll. SHAKE256 → first 4 bytes
|
|
1031
|
+
// → uint32 → modulo 100. 4 bytes is far in excess of the
|
|
1032
|
+
// information needed for 0..99 and uniform mapping is fine.
|
|
1033
|
+
var hash = nodeCrypto.createHash("shake256", { outputLength: 4 })
|
|
1034
|
+
.update(String(opts.pctSampleKey)).digest();
|
|
1035
|
+
var u32 = (hash[0] << 24 >>> 0) + (hash[1] << 16) + (hash[2] << 8) + hash[3]; // allow:raw-byte-literal — uint32 bit assembly
|
|
1036
|
+
sampleRoll = u32 % 100; // allow:raw-byte-literal — pct sample roll
|
|
1037
|
+
} else {
|
|
1038
|
+
sampleRoll = bCrypto.randomInt(0, 100); // allow:raw-byte-literal — pct sample roll
|
|
1039
|
+
}
|
|
1040
|
+
var sampled = !pass && pct < 100 && sampleRoll >= pct;
|
|
1041
|
+
var recommendedAction = pass ? "deliver" :
|
|
1042
|
+
sampled
|
|
1043
|
+
? (policy.p === "reject" ? "quarantine" :
|
|
1044
|
+
policy.p === "quarantine" ? "none" : "deliver")
|
|
1045
|
+
: (policy.p === "reject" ? "reject" :
|
|
1046
|
+
policy.p === "quarantine" ? "quarantine" :
|
|
1047
|
+
"deliver");
|
|
1048
|
+
|
|
1049
|
+
return {
|
|
1050
|
+
result: pass ? "pass" : "fail",
|
|
1051
|
+
policy: policy,
|
|
1052
|
+
policyOriginDomain: policyOriginDomain,
|
|
1053
|
+
orgDomain: orgDomain,
|
|
1054
|
+
orgDomainPolicyApplied: orgDomainPolicyApplied,
|
|
1055
|
+
psdPolicyApplied: psdPolicyApplied,
|
|
1056
|
+
npPolicyApplied: npApplied,
|
|
1057
|
+
alignment: { spf: spfAligned, dkim: dkimAligned },
|
|
1058
|
+
recommendedAction: recommendedAction,
|
|
1059
|
+
explanation: pass
|
|
1060
|
+
? "aligned via " + (spfAligned ? "spf" : "dkim")
|
|
1061
|
+
: "no aligned authentication; policy=" + policy.p,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// ---- ARC (RFC 8617) — full per-hop verification ----
|
|
1066
|
+
//
|
|
1067
|
+
// Each hop carries three headers — ARC-Authentication-Results (AAR),
|
|
1068
|
+
// ARC-Message-Signature (AMS), ARC-Seal (AS). AMS verifies the
|
|
1069
|
+
// message body + selected headers (DKIM-shaped signature). AS signs
|
|
1070
|
+
// the chain-of-custody (all prior AAR/AMS/AS headers + own AAR/AMS
|
|
1071
|
+
// with empty b=). Verification follows §5.1.1 (AMS) + §5.1.2 (AS).
|
|
1072
|
+
|
|
1073
|
+
function _splitHeaders(rfc822) {
|
|
1074
|
+
var sep = rfc822.indexOf("\r\n\r\n");
|
|
1075
|
+
if (sep === -1) sep = rfc822.indexOf("\n\n");
|
|
1076
|
+
if (sep === -1) {
|
|
1077
|
+
throw new MailAuthError("mail-auth/arc-no-body",
|
|
1078
|
+
"ARC: message has no header/body separator");
|
|
1079
|
+
}
|
|
1080
|
+
return rfc822.slice(0, sep);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function _parseHeaderLines(headerSection) {
|
|
1084
|
+
// Unfold multi-line headers (lines starting with whitespace).
|
|
1085
|
+
var lines = headerSection.split(/\r?\n/);
|
|
1086
|
+
var unfolded = [];
|
|
1087
|
+
for (var i = 0; i < lines.length; i += 1) {
|
|
1088
|
+
var line = lines[i];
|
|
1089
|
+
if (line.length === 0) continue;
|
|
1090
|
+
if ((line.charAt(0) === " " || line.charAt(0) === "\t") && unfolded.length > 0) {
|
|
1091
|
+
unfolded[unfolded.length - 1] += " " + line.replace(/^\s+/, "");
|
|
1092
|
+
} else {
|
|
1093
|
+
unfolded.push(line);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return unfolded;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// RFC 8617 §5.1.2 caps the chain at 50 sets to bound verifier work and
|
|
1100
|
+
// limit how far an attacker can push junk headers.
|
|
1101
|
+
var ARC_MAX_HOPS = 50; // allow:raw-byte-literal — RFC 8617 §5.1.2 chain ceiling
|
|
1102
|
+
|
|
1103
|
+
async function arcVerify(rfc822, opts) {
|
|
1104
|
+
if (typeof rfc822 !== "string" || rfc822.length === 0) {
|
|
1105
|
+
throw new MailAuthError("mail-auth/arc-bad-input",
|
|
1106
|
+
"arc.verify: rfc822 must be a non-empty string");
|
|
1107
|
+
}
|
|
1108
|
+
opts = opts || {};
|
|
1109
|
+
var headers = _parseHeaderLines(_splitHeaders(rfc822));
|
|
1110
|
+
var hops = [];
|
|
1111
|
+
var seenSlot = {}; // {`<instance>:<name>`: true} — duplicate detection
|
|
1112
|
+
|
|
1113
|
+
// 1. Index ARC headers by instance number. Refuse duplicates: a single
|
|
1114
|
+
// hop has exactly one ARC-Seal / ARC-Message-Signature /
|
|
1115
|
+
// ARC-Authentication-Results. A second copy at the same instance is
|
|
1116
|
+
// a malformed chain (per RFC 8617 §5.1 implicit, and a known
|
|
1117
|
+
// injection vector — a forwarder that re-signs with a duplicate
|
|
1118
|
+
// instance would silently overwrite the original signer).
|
|
1119
|
+
var duplicate = false;
|
|
1120
|
+
var maxInstanceSeen = 0;
|
|
1121
|
+
// RFC 8617 §5.2 — verifier MUST process the chain starting with the
|
|
1122
|
+
// highest-instance set, then walk down. Each hop prepends its three
|
|
1123
|
+
// headers (AS, AMS, AAR) to the message, so the source order from
|
|
1124
|
+
// top to bottom is: i=N (AS, AMS, AAR), i=N-1 (...), ..., i=1.
|
|
1125
|
+
// A chain whose source order doesn't decrease has been re-shuffled
|
|
1126
|
+
// by an intermediary that didn't follow §5.1, or is forged. Track
|
|
1127
|
+
// per-header-set first-appearance order and enforce strictly-
|
|
1128
|
+
// decreasing instances.
|
|
1129
|
+
var orderTrail = []; // [{ inst, name, idx }]
|
|
1130
|
+
for (var i = 0; i < headers.length; i += 1) {
|
|
1131
|
+
var line = headers[i];
|
|
1132
|
+
var colonAt = line.indexOf(":");
|
|
1133
|
+
if (colonAt === -1) continue;
|
|
1134
|
+
var name = line.slice(0, colonAt).trim().toLowerCase();
|
|
1135
|
+
var value = line.slice(colonAt + 1).trim();
|
|
1136
|
+
if (name !== "arc-seal" && name !== "arc-message-signature" &&
|
|
1137
|
+
name !== "arc-authentication-results") continue;
|
|
1138
|
+
// ARC hop instance per RFC 8617 §4.2.1 — bounded to 3 digits; the
|
|
1139
|
+
// spec doesn't define a hard ceiling but operational use never
|
|
1140
|
+
// exceeds 50 hops, and a 999-hop limit prevents pathological
|
|
1141
|
+
// header values from chewing the verifier.
|
|
1142
|
+
var iMatch = value.match(/(?:^|[;,\s])i=(\d{1,3})\b/);
|
|
1143
|
+
var inst = iMatch ? parseInt(iMatch[1], 10) : null;
|
|
1144
|
+
if (inst === null || !isFinite(inst) || inst < 1) continue;
|
|
1145
|
+
if (inst > maxInstanceSeen) maxInstanceSeen = inst;
|
|
1146
|
+
var slotKey = inst + ":" + name;
|
|
1147
|
+
if (seenSlot[slotKey]) { duplicate = true; continue; }
|
|
1148
|
+
seenSlot[slotKey] = true;
|
|
1149
|
+
if (!hops[inst - 1]) hops[inst - 1] = { instance: inst };
|
|
1150
|
+
hops[inst - 1][name] = value;
|
|
1151
|
+
orderTrail.push({ inst: inst, name: name, idx: i });
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Source-order enforcement (RFC 8617 §5.1 + §5.2): the first AS for
|
|
1155
|
+
// a given hop must appear before its AMS, which must appear before
|
|
1156
|
+
// its AAR (within a single set). Across sets, hop instances MUST
|
|
1157
|
+
// strictly decrease top-to-bottom. Use the first-appearance index
|
|
1158
|
+
// per hop to validate the cross-set ordering; an out-of-order chain
|
|
1159
|
+
// is treated as a structural failure rather than risking a permissive
|
|
1160
|
+
// verdict.
|
|
1161
|
+
var orderFail = null;
|
|
1162
|
+
if (orderTrail.length > 0) {
|
|
1163
|
+
// Per-hop first-appearance: which i= instance owns each contiguous
|
|
1164
|
+
// run? Walk top to bottom and confirm the instance numbers, when
|
|
1165
|
+
// they change, only EVER decrease.
|
|
1166
|
+
var prevInst = null;
|
|
1167
|
+
for (var oi = 0; oi < orderTrail.length; oi += 1) {
|
|
1168
|
+
var cur = orderTrail[oi].inst;
|
|
1169
|
+
if (prevInst !== null && cur > prevInst) {
|
|
1170
|
+
orderFail = "header-order-ascending-i=" + cur + "-after-i=" + prevInst;
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
prevInst = cur;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (hops.length === 0) {
|
|
1178
|
+
return { chainStatus: "none", hopCount: 0, hops: [] };
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
if (duplicate) {
|
|
1182
|
+
return {
|
|
1183
|
+
chainStatus: "fail",
|
|
1184
|
+
reason: "duplicate-instance",
|
|
1185
|
+
hopCount: hops.filter(Boolean).length,
|
|
1186
|
+
hops: hops.filter(Boolean).map(function (h) {
|
|
1187
|
+
return { instance: h.instance,
|
|
1188
|
+
hasSeal: !!h["arc-seal"],
|
|
1189
|
+
hasMessageSignature: !!h["arc-message-signature"],
|
|
1190
|
+
hasAuthenticationResults: !!h["arc-authentication-results"],
|
|
1191
|
+
amsResult: "skipped", asResult: "skipped" };
|
|
1192
|
+
}),
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
if (orderFail) {
|
|
1197
|
+
return {
|
|
1198
|
+
chainStatus: "fail",
|
|
1199
|
+
reason: "header-order-violation: " + orderFail,
|
|
1200
|
+
hopCount: hops.filter(Boolean).length,
|
|
1201
|
+
hops: hops.filter(Boolean).map(function (h) {
|
|
1202
|
+
return { instance: h.instance,
|
|
1203
|
+
hasSeal: !!h["arc-seal"],
|
|
1204
|
+
hasMessageSignature: !!h["arc-message-signature"],
|
|
1205
|
+
hasAuthenticationResults: !!h["arc-authentication-results"],
|
|
1206
|
+
amsResult: "skipped", asResult: "skipped" };
|
|
1207
|
+
}),
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
if (maxInstanceSeen > ARC_MAX_HOPS) {
|
|
1212
|
+
return {
|
|
1213
|
+
chainStatus: "fail",
|
|
1214
|
+
reason: "too-many-hops",
|
|
1215
|
+
hopCount: maxInstanceSeen,
|
|
1216
|
+
hops: [],
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// 2. Structural check — every hop must carry all three headers AND
|
|
1221
|
+
// the chain must start at i=1 with no gaps. RFC 8617 §5.1 requires
|
|
1222
|
+
// instances to form a contiguous 1..N sequence. Indexed loop (not
|
|
1223
|
+
// .some) because sparse arrays skip empty slots in callbacks —
|
|
1224
|
+
// a non-contiguous chain ([hop1, , hop3]) would silently pass.
|
|
1225
|
+
var structuralFail = false;
|
|
1226
|
+
for (var sci = 0; sci < hops.length; sci += 1) {
|
|
1227
|
+
var sch = hops[sci];
|
|
1228
|
+
if (!sch || !sch["arc-seal"] || !sch["arc-message-signature"] ||
|
|
1229
|
+
!sch["arc-authentication-results"]) {
|
|
1230
|
+
structuralFail = true;
|
|
1231
|
+
break;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if (structuralFail) {
|
|
1235
|
+
return {
|
|
1236
|
+
chainStatus: "fail",
|
|
1237
|
+
reason: "incomplete-or-non-contiguous",
|
|
1238
|
+
hopCount: hops.filter(Boolean).length,
|
|
1239
|
+
hops: hops.filter(Boolean).map(function (h) {
|
|
1240
|
+
return { instance: h.instance,
|
|
1241
|
+
hasSeal: !!h["arc-seal"],
|
|
1242
|
+
hasMessageSignature: !!h["arc-message-signature"],
|
|
1243
|
+
hasAuthenticationResults: !!h["arc-authentication-results"],
|
|
1244
|
+
amsResult: "skipped", asResult: "skipped" };
|
|
1245
|
+
}),
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// 3. Per-hop AMS + AS verification.
|
|
1250
|
+
var perHop = [];
|
|
1251
|
+
var anyFail = false;
|
|
1252
|
+
// RFC 8617 §5.2 — operator-tunable clock skew on t= (signing
|
|
1253
|
+
// timestamp) and x= (expiration) tags. Default 5 min.
|
|
1254
|
+
var arcClockSkewMs = typeof opts.clockSkewMs === "number" && opts.clockSkewMs >= 0 // allow:numeric-opt-Infinity — operator-supplied skew, default 5 min
|
|
1255
|
+
? opts.clockSkewMs : C.TIME.minutes(5);
|
|
1256
|
+
var nowSec = Math.floor(Date.now() / 1000); // allow:raw-byte-literal — Unix epoch seconds divisor
|
|
1257
|
+
|
|
1258
|
+
for (var hopIdx = 0; hopIdx < hops.length; hopIdx += 1) {
|
|
1259
|
+
var hop = hops[hopIdx];
|
|
1260
|
+
|
|
1261
|
+
// RFC 8617 §5.2 — verifier MUST reject AMS or AS with t= timestamp
|
|
1262
|
+
// in the future or x= expiration in the past (with operator skew
|
|
1263
|
+
// tolerance). Pre-v0.8.17 the verifier parsed t= but never
|
|
1264
|
+
// enforced it.
|
|
1265
|
+
var amsTags = _parseArcTagList(hop["arc-message-signature"]);
|
|
1266
|
+
var asTags = _parseArcTagList(hop["arc-seal"]);
|
|
1267
|
+
var amsT = amsTags.t ? parseInt(amsTags.t, 10) : null;
|
|
1268
|
+
var amsX = amsTags.x ? parseInt(amsTags.x, 10) : null;
|
|
1269
|
+
var asT = asTags.t ? parseInt(asTags.t, 10) : null;
|
|
1270
|
+
var asX = asTags.x ? parseInt(asTags.x, 10) : null;
|
|
1271
|
+
var skewSec = Math.floor(arcClockSkewMs / 1000); // allow:raw-byte-literal — sec divisor
|
|
1272
|
+
var timeFault = null;
|
|
1273
|
+
if (amsT && isFinite(amsT) && amsT - skewSec > nowSec) timeFault = "ams-t-future";
|
|
1274
|
+
if (amsX && isFinite(amsX) && amsX + skewSec < nowSec) timeFault = "ams-x-expired";
|
|
1275
|
+
if (asT && isFinite(asT) && asT - skewSec > nowSec) timeFault = "as-t-future";
|
|
1276
|
+
if (asX && isFinite(asX) && asX + skewSec < nowSec) timeFault = "as-x-expired";
|
|
1277
|
+
|
|
1278
|
+
// AMS — RFC 8617 §5.1.1. Same shape as a DKIM-Signature; reuses
|
|
1279
|
+
// the DKIM verifier by injecting a temporary message that has
|
|
1280
|
+
// the AMS as the signing header.
|
|
1281
|
+
var amsResult = timeFault
|
|
1282
|
+
? { result: "fail", errors: ["ams: " + timeFault + " (RFC 8617 §5.2)"] }
|
|
1283
|
+
: await _verifyArc(rfc822, hop, hops, "ams", opts.dnsLookup, dkim);
|
|
1284
|
+
|
|
1285
|
+
// AS — RFC 8617 §5.1.2. Signs the catenation of all prior
|
|
1286
|
+
// ARC-{AAR,AMS,AS} headers plus current AAR + AMS, then the AS
|
|
1287
|
+
// itself with empty b=.
|
|
1288
|
+
var asResult = timeFault
|
|
1289
|
+
? { result: "fail", errors: ["as: " + timeFault + " (RFC 8617 §5.2)"] }
|
|
1290
|
+
: await _verifyArc(rfc822, hop, hops, "as", opts.dnsLookup, dkim);
|
|
1291
|
+
|
|
1292
|
+
perHop.push({
|
|
1293
|
+
instance: hop.instance,
|
|
1294
|
+
hasSeal: true,
|
|
1295
|
+
hasMessageSignature: true,
|
|
1296
|
+
hasAuthenticationResults: true,
|
|
1297
|
+
amsResult: amsResult.result,
|
|
1298
|
+
asResult: asResult.result,
|
|
1299
|
+
amsErrors: amsResult.errors,
|
|
1300
|
+
asErrors: asResult.errors,
|
|
1301
|
+
});
|
|
1302
|
+
if (amsResult.result !== "pass" || asResult.result !== "pass") anyFail = true;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// 4. Chain Validation per RFC 8617 §5.2.
|
|
1306
|
+
//
|
|
1307
|
+
// Per-hop cv= self-attestation rules:
|
|
1308
|
+
// i=1 — cv=none REQUIRED (no upstream chain to validate)
|
|
1309
|
+
// i>=2 — cv=pass or cv=fail; cv=none is invalid at i>=2
|
|
1310
|
+
//
|
|
1311
|
+
// Once any hop's AS reports cv=fail, the chain is permanently
|
|
1312
|
+
// broken — downstream cv=pass claims after an upstream cv=fail
|
|
1313
|
+
// are malformed (a hop can't claim the chain validates when it
|
|
1314
|
+
// knows an earlier hop saw it fail).
|
|
1315
|
+
var perHopCv = [];
|
|
1316
|
+
var hopRuleViolation = null;
|
|
1317
|
+
var sawFail = false;
|
|
1318
|
+
for (var hi = 0; hi < hops.length; hi += 1) {
|
|
1319
|
+
var as = hops[hi]["arc-seal"];
|
|
1320
|
+
var hopCvMatch = as.match(/(?:^|[;,\s])cv=(none|pass|fail)/);
|
|
1321
|
+
var hopCv = hopCvMatch ? hopCvMatch[1] : null;
|
|
1322
|
+
perHopCv.push(hopCv);
|
|
1323
|
+
if (hopCv === null) {
|
|
1324
|
+
hopRuleViolation = "missing-cv-at-i=" + (hi + 1);
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
if (hi === 0 && hopCv !== "none") {
|
|
1328
|
+
hopRuleViolation = "i=1-cv-must-be-none-got-" + hopCv;
|
|
1329
|
+
break;
|
|
1330
|
+
}
|
|
1331
|
+
if (hi >= 1 && hopCv === "none") {
|
|
1332
|
+
hopRuleViolation = "i=" + (hi + 1) + "-cv=none-invalid-after-hop-1";
|
|
1333
|
+
break;
|
|
1334
|
+
}
|
|
1335
|
+
if (hopCv === "fail") sawFail = true;
|
|
1336
|
+
if (hopCv === "pass" && sawFail) {
|
|
1337
|
+
hopRuleViolation = "i=" + (hi + 1) + "-cv=pass-after-upstream-fail";
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
var lastCv = perHopCv[perHopCv.length - 1];
|
|
1343
|
+
var chainStatus;
|
|
1344
|
+
var reasonOut = null;
|
|
1345
|
+
if (hopRuleViolation) {
|
|
1346
|
+
chainStatus = "fail";
|
|
1347
|
+
reasonOut = hopRuleViolation;
|
|
1348
|
+
} else if (anyFail) {
|
|
1349
|
+
chainStatus = "fail";
|
|
1350
|
+
reasonOut = "signature-verification-failed";
|
|
1351
|
+
} else if (lastCv === "fail") {
|
|
1352
|
+
chainStatus = "fail";
|
|
1353
|
+
reasonOut = "last-as-cv=fail";
|
|
1354
|
+
} else if (hops.length === 1 && lastCv === "none") {
|
|
1355
|
+
chainStatus = "pass";
|
|
1356
|
+
} else if (hops.length > 1 && lastCv === "pass") {
|
|
1357
|
+
chainStatus = "pass";
|
|
1358
|
+
} else {
|
|
1359
|
+
chainStatus = "fail";
|
|
1360
|
+
reasonOut = "unexpected-cv-state";
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
var out = {
|
|
1364
|
+
chainStatus: chainStatus,
|
|
1365
|
+
hopCount: hops.length,
|
|
1366
|
+
cv: lastCv,
|
|
1367
|
+
perHopCv: perHopCv,
|
|
1368
|
+
hops: perHop,
|
|
1369
|
+
};
|
|
1370
|
+
if (reasonOut) out.reason = reasonOut;
|
|
1371
|
+
return out;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// Verify a single AMS or AS within the chain by reconstructing the
|
|
1375
|
+
// signed string per RFC 8617 + invoking node:crypto.verify with the
|
|
1376
|
+
// public key fetched from the AMS's d= + s= TXT record.
|
|
1377
|
+
async function _verifyArc(rfc822, hop, allHops, kind, dnsLookup, dkim) {
|
|
1378
|
+
var sigHeaderName = kind === "ams" ? "arc-message-signature" : "arc-seal";
|
|
1379
|
+
var sigValue = hop[sigHeaderName];
|
|
1380
|
+
var tags = _parseArcTagList(sigValue);
|
|
1381
|
+
if (!tags.d || !tags.s || !tags.b || !tags.a) {
|
|
1382
|
+
return { result: "permerror", errors: [kind + ": missing required tag(s) d/s/b/a"] };
|
|
1383
|
+
}
|
|
1384
|
+
if (tags.a !== "rsa-sha256" && tags.a !== "ed25519-sha256") {
|
|
1385
|
+
return { result: "permerror", errors: [kind + ": unsupported alg '" + tags.a + "'"] };
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// Fetch the signing public key from <s>._domainkey.<d>.
|
|
1389
|
+
var keyTags;
|
|
1390
|
+
try {
|
|
1391
|
+
var qname = tags.s + "._domainkey." + tags.d;
|
|
1392
|
+
var records = await _safeResolveTxt(qname, dnsLookup);
|
|
1393
|
+
keyTags = _parseDkimKeyRecord(records);
|
|
1394
|
+
} catch (e) {
|
|
1395
|
+
var verdict = (e && (e.code === "ENOTFOUND" || e.code === "ENODATA"))
|
|
1396
|
+
? "permerror" : "temperror";
|
|
1397
|
+
return { result: verdict, errors: [kind + ": key lookup failed: " +
|
|
1398
|
+
((e && e.message) || String(e))] };
|
|
1399
|
+
}
|
|
1400
|
+
if (!keyTags || !keyTags.p) {
|
|
1401
|
+
return { result: "permerror", errors: [kind + ": key record missing p="] };
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// Reconstruct the canonical signed string.
|
|
1405
|
+
var canonicalized;
|
|
1406
|
+
if (kind === "ams") {
|
|
1407
|
+
// AMS signs the body + selected headers, identical to DKIM-Sig.
|
|
1408
|
+
// Reuse the DKIM verifier by passing a synthetic message where
|
|
1409
|
+
// the AMS header is renamed to DKIM-Signature.
|
|
1410
|
+
return await _verifyAmsViaDkim(rfc822, hop, sigValue, tags, dkim, dnsLookup);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// AS signs the catenation of every prior AAR/AMS/AS plus current
|
|
1414
|
+
// AAR/AMS, then the AS itself with empty b= per RFC 8617 §5.1.2.
|
|
1415
|
+
canonicalized = "";
|
|
1416
|
+
for (var prior = 0; prior < hop.instance; prior += 1) {
|
|
1417
|
+
var p = allHops[prior];
|
|
1418
|
+
if (!p) continue;
|
|
1419
|
+
canonicalized += _canonRelaxedHeader("ARC-Authentication-Results", p["arc-authentication-results"]);
|
|
1420
|
+
canonicalized += _canonRelaxedHeader("ARC-Message-Signature", p["arc-message-signature"]);
|
|
1421
|
+
if (p.instance !== hop.instance) {
|
|
1422
|
+
// Prior AS gets included whole.
|
|
1423
|
+
canonicalized += _canonRelaxedHeader("ARC-Seal", p["arc-seal"]);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
// Current AS with b= emptied. RFC 8617 §5.1.2: canonicalization
|
|
1427
|
+
// includes the AS header with `b=` value stripped + no trailing CRLF.
|
|
1428
|
+
var asUnsigned = sigValue.replace(/(\bb=)[^;]*/i, "$1");
|
|
1429
|
+
canonicalized += _canonRelaxedHeader("ARC-Seal", asUnsigned).replace(/\r\n$/, "");
|
|
1430
|
+
|
|
1431
|
+
// Verify the AS signature.
|
|
1432
|
+
return _runVerify(canonicalized, tags.b, tags.a, keyTags.p, "as");
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
async function _verifyAmsViaDkim(rfc822, hop, sigValue, tags, dkim, dnsLookup) {
|
|
1436
|
+
// Build a synthetic rfc822 where the ARC-Message-Signature is renamed
|
|
1437
|
+
// to DKIM-Signature so the existing DKIM verifier handles AMS
|
|
1438
|
+
// verification (the cryptographic shape is identical).
|
|
1439
|
+
var renamedHeader = "DKIM-Signature: " + sigValue;
|
|
1440
|
+
var sep = rfc822.indexOf("\r\n\r\n");
|
|
1441
|
+
if (sep === -1) sep = rfc822.indexOf("\n\n");
|
|
1442
|
+
var headerEnd = sep === -1 ? rfc822.length : sep;
|
|
1443
|
+
// Strip every other ARC-* header so the DKIM verifier doesn't see
|
|
1444
|
+
// them, AND replace the AMS itself with DKIM-Signature for this hop.
|
|
1445
|
+
var headerLines = _parseHeaderLines(rfc822.slice(0, headerEnd));
|
|
1446
|
+
var rebuilt = [];
|
|
1447
|
+
for (var i = 0; i < headerLines.length; i += 1) {
|
|
1448
|
+
var line = headerLines[i];
|
|
1449
|
+
var colonAt = line.indexOf(":");
|
|
1450
|
+
if (colonAt === -1) { rebuilt.push(line); continue; }
|
|
1451
|
+
var name = line.slice(0, colonAt).trim().toLowerCase();
|
|
1452
|
+
if (name === "arc-message-signature" ||
|
|
1453
|
+
name === "arc-seal" ||
|
|
1454
|
+
name === "dkim-signature") {
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
if (name === "arc-authentication-results") {
|
|
1458
|
+
// RFC 8617 §5.1.1 — keep only the CURRENT hop's AAR (signer
|
|
1459
|
+
// canonicalizes it via h=). Pre-v0.8.17 stripped every AAR
|
|
1460
|
+
// unconditionally, breaking verification on chains that
|
|
1461
|
+
// included AAR in h= (Microsoft + Google interop).
|
|
1462
|
+
var instMatch = /\bi\s*=\s*(\d+)/.exec(line.slice(colonAt + 1));
|
|
1463
|
+
if (!instMatch || parseInt(instMatch[1], 10) !== hop.instance) continue;
|
|
1464
|
+
}
|
|
1465
|
+
rebuilt.push(line);
|
|
1466
|
+
}
|
|
1467
|
+
rebuilt.unshift(renamedHeader);
|
|
1468
|
+
var synthetic = rebuilt.join("\r\n") + (sep === -1 ? "" :
|
|
1469
|
+
rfc822.slice(headerEnd));
|
|
1470
|
+
var rv = await dkim.verify(synthetic, { dnsLookup: dnsLookup });
|
|
1471
|
+
if (!Array.isArray(rv) || rv.length === 0) {
|
|
1472
|
+
return { result: "permerror", errors: ["ams: dkim verifier returned no results"] };
|
|
1473
|
+
}
|
|
1474
|
+
return { result: rv[0].result, errors: rv[0].errors || [] };
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function _parseArcTagList(value) {
|
|
1478
|
+
var tags = {};
|
|
1479
|
+
var parts = String(value).split(";"); // allow:bare-split-on-quoted-header — allow:raw-byte-literal — RFC 8617 §4 ARC tag-list grammar (same as the DKIM RFC's): `tag-spec *( ";" tag-spec )`, tag-value contains no DQUOTE
|
|
1480
|
+
|
|
1481
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
1482
|
+
var p = parts[i].trim();
|
|
1483
|
+
if (p.length === 0) continue;
|
|
1484
|
+
var eq = p.indexOf("=");
|
|
1485
|
+
if (eq === -1) continue;
|
|
1486
|
+
tags[p.slice(0, eq).trim().toLowerCase()] = p.slice(eq + 1).trim().replace(/\s+/g, "");
|
|
1487
|
+
}
|
|
1488
|
+
return tags;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function _parseDkimKeyRecord(records) {
|
|
1492
|
+
var joined = "";
|
|
1493
|
+
if (Array.isArray(records)) {
|
|
1494
|
+
for (var i = 0; i < records.length; i += 1) {
|
|
1495
|
+
var rec = records[i];
|
|
1496
|
+
joined = Array.isArray(rec) ? rec.join("") : String(rec);
|
|
1497
|
+
if (joined.indexOf("v=DKIM1") === 0 || joined.indexOf("p=") !== -1) break;
|
|
1498
|
+
}
|
|
1499
|
+
} else {
|
|
1500
|
+
joined = String(records || "");
|
|
1501
|
+
}
|
|
1502
|
+
return _parseArcTagList(joined);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
function _canonRelaxedHeader(name, value) {
|
|
1506
|
+
// RFC 6376 §3.4.2 — relaxed header canon: lowercase name, unfold,
|
|
1507
|
+
// collapse internal WSP runs, strip trailing WSP.
|
|
1508
|
+
var unfolded = String(value).replace(/\r?\n[ \t]+/g, " ");
|
|
1509
|
+
var trimmed = unfolded.replace(/[ \t]+/g, " ").replace(/^[ \t]+|[ \t]+$/g, "");
|
|
1510
|
+
return name.toLowerCase() + ":" + trimmed + "\r\n";
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function _pemFromB64KeyMaterial(b64) {
|
|
1514
|
+
var pem = "-----BEGIN PUBLIC KEY-----\n";
|
|
1515
|
+
for (var i = 0; i < b64.length; i += 64) { // allow:raw-byte-literal — PEM wrap width
|
|
1516
|
+
pem += b64.slice(i, i + 64) + "\n"; // allow:raw-byte-literal — PEM wrap width
|
|
1517
|
+
}
|
|
1518
|
+
pem += "-----END PUBLIC KEY-----\n";
|
|
1519
|
+
return pem;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
function _runVerify(signedString, sigB64, algorithm, keyB64, label) {
|
|
1523
|
+
var nodeCrypto = require("node:crypto");
|
|
1524
|
+
var pem = _pemFromB64KeyMaterial(keyB64);
|
|
1525
|
+
var keyObj;
|
|
1526
|
+
try { keyObj = nodeCrypto.createPublicKey(pem); }
|
|
1527
|
+
catch (e) {
|
|
1528
|
+
return { result: "permerror",
|
|
1529
|
+
errors: [label + ": key parse failed: " + ((e && e.message) || String(e))] };
|
|
1530
|
+
}
|
|
1531
|
+
var nodeAlgo = algorithm === "rsa-sha256" ? "sha256" : null;
|
|
1532
|
+
var sigBuf = Buffer.from(sigB64, "base64");
|
|
1533
|
+
var verified;
|
|
1534
|
+
try {
|
|
1535
|
+
verified = nodeCrypto.verify(nodeAlgo, Buffer.from(signedString, "utf8"), keyObj, sigBuf);
|
|
1536
|
+
} catch (e) {
|
|
1537
|
+
return { result: "permerror",
|
|
1538
|
+
errors: [label + ": verify threw: " + ((e && e.message) || String(e))] };
|
|
1539
|
+
}
|
|
1540
|
+
return verified
|
|
1541
|
+
? { result: "pass", errors: [] }
|
|
1542
|
+
: { result: "fail", errors: [label + ": signature verification failed"] };
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
void C; // C is imported for future TIME constants in policy fetchers.
|
|
1546
|
+
|
|
1547
|
+
// ---- ARC receiver-side trust evaluation (RFC 8617 §6) ----
|
|
1548
|
+
//
|
|
1549
|
+
// arc.verify confirms the cryptographic chain validates; arc.evaluate
|
|
1550
|
+
// is the operator-side trust decision: given a passing chain, did any
|
|
1551
|
+
// hop in the chain belong to a sealer the operator trusts? The trust
|
|
1552
|
+
// list is operator policy — typically the operator's own domain plus
|
|
1553
|
+
// upstream relays the operator has agreed to honor (mailing list
|
|
1554
|
+
// operators, MX-vendor middleware).
|
|
1555
|
+
//
|
|
1556
|
+
// var rv = await b.mail.arc.evaluate(rfc822, {
|
|
1557
|
+
// trustedSealers: ["example.com", "mailgun.net"],
|
|
1558
|
+
// });
|
|
1559
|
+
// // → { chainStatus: "pass", trusted: true, trustedHop: 2,
|
|
1560
|
+
// // trustedDomain: "mailgun.net" }
|
|
1561
|
+
|
|
1562
|
+
async function arcEvaluate(rfc822, opts) {
|
|
1563
|
+
if (typeof rfc822 !== "string" || rfc822.length === 0) {
|
|
1564
|
+
throw new MailAuthError("mail-auth/arc-bad-input",
|
|
1565
|
+
"arc.evaluate: rfc822 must be a non-empty string");
|
|
1566
|
+
}
|
|
1567
|
+
opts = opts || {};
|
|
1568
|
+
if (!Array.isArray(opts.trustedSealers)) {
|
|
1569
|
+
throw new MailAuthError("mail-auth/arc-bad-trusted-sealers",
|
|
1570
|
+
"arc.evaluate: opts.trustedSealers must be an array of domain strings");
|
|
1571
|
+
}
|
|
1572
|
+
var trusted = {};
|
|
1573
|
+
for (var ti = 0; ti < opts.trustedSealers.length; ti += 1) {
|
|
1574
|
+
var d = opts.trustedSealers[ti];
|
|
1575
|
+
if (typeof d !== "string" || d.length === 0) {
|
|
1576
|
+
throw new MailAuthError("mail-auth/arc-trust-eval-failed",
|
|
1577
|
+
"arc.evaluate: trustedSealers[" + ti + "] must be a non-empty domain string");
|
|
1578
|
+
}
|
|
1579
|
+
trusted[d.toLowerCase()] = true;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
var verdict = await arcVerify(rfc822, opts);
|
|
1583
|
+
var out = {
|
|
1584
|
+
chainStatus: verdict.chainStatus,
|
|
1585
|
+
hopCount: verdict.hopCount,
|
|
1586
|
+
trusted: false,
|
|
1587
|
+
trustedHop: null,
|
|
1588
|
+
trustedDomain: null,
|
|
1589
|
+
// RFC 8617 §6 trust evaluation extension surface (B6).
|
|
1590
|
+
// trust: "trusted" | "unverified" | "failed"
|
|
1591
|
+
// trustedHops: [{ instance, domain }] of every trusted sealer
|
|
1592
|
+
// in the validated chain
|
|
1593
|
+
// finalAr: verbatim AAR from the most-recent hop (the
|
|
1594
|
+
// receiver's view of upstream auth results)
|
|
1595
|
+
// breakAt: first instance whose AMS or AS failed, or null
|
|
1596
|
+
// when every hop verified
|
|
1597
|
+
trust: verdict.chainStatus === "pass" ? "unverified" : "failed",
|
|
1598
|
+
trustedHops: [],
|
|
1599
|
+
finalAr: null,
|
|
1600
|
+
breakAt: null,
|
|
1601
|
+
};
|
|
1602
|
+
if (verdict.reason) out.reason = verdict.reason;
|
|
1603
|
+
|
|
1604
|
+
// Re-extract per-hop d= (signing domain on AS) AND the AAR text from
|
|
1605
|
+
// the original headers — the verify-result shape doesn't carry
|
|
1606
|
+
// them. One pass over the header section.
|
|
1607
|
+
var headers = _parseHeaderLines(_splitHeaders(rfc822));
|
|
1608
|
+
var hopDomains = {};
|
|
1609
|
+
var hopAr = {};
|
|
1610
|
+
for (var hi = 0; hi < headers.length; hi += 1) {
|
|
1611
|
+
var line = headers[hi];
|
|
1612
|
+
var colonAt = line.indexOf(":");
|
|
1613
|
+
if (colonAt === -1) continue;
|
|
1614
|
+
var name = line.slice(0, colonAt).trim().toLowerCase();
|
|
1615
|
+
var value = line.slice(colonAt + 1).trim();
|
|
1616
|
+
if (name === "arc-seal") {
|
|
1617
|
+
var iMatch = value.match(/(?:^|[;,\s])i=(\d+)/); // allow:regex-no-length-cap — header bounded by RFC 5322 998
|
|
1618
|
+
var dMatch = value.match(/(?:^|[;,\s])d=([^\s;]+)/); // allow:regex-no-length-cap — header bounded by RFC 5322 998
|
|
1619
|
+
if (iMatch && dMatch) hopDomains[parseInt(iMatch[1], 10)] = dMatch[1].toLowerCase();
|
|
1620
|
+
} else if (name === "arc-authentication-results") {
|
|
1621
|
+
var arIMatch = value.match(/\bi\s*=\s*(\d+)/); // allow:regex-no-length-cap — header bounded by RFC 5322 998
|
|
1622
|
+
if (arIMatch) hopAr[parseInt(arIMatch[1], 10)] = value;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// finalAr — the most-recent hop's AAR. Always populated when the
|
|
1627
|
+
// chain has at least one hop (regardless of pass/fail), so the
|
|
1628
|
+
// operator can surface upstream auth context even on a broken chain.
|
|
1629
|
+
if (verdict.hopCount > 0) {
|
|
1630
|
+
out.finalAr = hopAr[verdict.hopCount] || null;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// breakAt — first instance whose AMS or AS failed.
|
|
1634
|
+
if (Array.isArray(verdict.hops)) {
|
|
1635
|
+
for (var bi = 0; bi < verdict.hops.length; bi += 1) {
|
|
1636
|
+
var bhop = verdict.hops[bi];
|
|
1637
|
+
if (!bhop) continue;
|
|
1638
|
+
if (bhop.amsResult !== "pass" || bhop.asResult !== "pass") {
|
|
1639
|
+
out.breakAt = bhop.instance;
|
|
1640
|
+
break;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
if (verdict.chainStatus !== "pass" || !Array.isArray(verdict.hops)) return out;
|
|
1646
|
+
|
|
1647
|
+
// Walk hops most-recent-first so we attribute the primary trust
|
|
1648
|
+
// decision to the deepest (closest-to-receiver) trusted sealer, but
|
|
1649
|
+
// also collect EVERY trusted hop so the operator can audit the
|
|
1650
|
+
// full custody chain.
|
|
1651
|
+
for (var ri2 = verdict.hops.length - 1; ri2 >= 0; ri2 -= 1) {
|
|
1652
|
+
var hop = verdict.hops[ri2];
|
|
1653
|
+
if (!hop || hop.amsResult !== "pass" || hop.asResult !== "pass") continue;
|
|
1654
|
+
var domain = hopDomains[hop.instance];
|
|
1655
|
+
if (domain && trusted[domain]) {
|
|
1656
|
+
out.trustedHops.push({ instance: hop.instance, domain: domain });
|
|
1657
|
+
if (!out.trusted) {
|
|
1658
|
+
out.trusted = true;
|
|
1659
|
+
out.trustedHop = hop.instance;
|
|
1660
|
+
out.trustedDomain = domain;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
out.trust = out.trusted ? "trusted" : "unverified";
|
|
1665
|
+
return out;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// ---- Authentication-Results header (RFC 8601) builder ----
|
|
1669
|
+
//
|
|
1670
|
+
// Build the A-R header value the receiving MTA prepends to the message
|
|
1671
|
+
// before delivery. Operators consume per-method results from
|
|
1672
|
+
// b.mail.spf.verify / b.mail.dmarc.evaluate / b.mail.arc.verify (or
|
|
1673
|
+
// .evaluate) and pass them to .emit; the framework formats the RFC
|
|
1674
|
+
// 8601-conformant header string.
|
|
1675
|
+
//
|
|
1676
|
+
// var hdr = b.mail.authResults.emit({
|
|
1677
|
+
// authservId: "mx.example.com",
|
|
1678
|
+
// results: [
|
|
1679
|
+
// { method: "spf", result: "pass", smtpMailfrom: "user@sender.example" },
|
|
1680
|
+
// { method: "dkim", result: "pass", domain: "sender.example" },
|
|
1681
|
+
// { method: "dmarc", result: "pass", from: "user@sender.example" },
|
|
1682
|
+
// { method: "arc", result: "pass" },
|
|
1683
|
+
// ],
|
|
1684
|
+
// });
|
|
1685
|
+
// // → "Authentication-Results: mx.example.com;\r\n spf=pass smtp.mailfrom=user@sender.example;\r\n dkim=pass header.d=sender.example;\r\n dmarc=pass header.from=user@sender.example;\r\n arc=pass"
|
|
1686
|
+
|
|
1687
|
+
// RFC 8601 §2.7 — result vocabulary is METHOD-SPECIFIC, not a flat
|
|
1688
|
+
// allowlist. The flat AR_VALID_RESULTS table previously accepted
|
|
1689
|
+
// `hardfail` for DKIM (only valid for DMARC §2.7.4) and `temperror` /
|
|
1690
|
+
// `permerror` for methods that don't recognize them. Per-method maps
|
|
1691
|
+
// match the spec sections cited.
|
|
1692
|
+
var AR_RESULTS_BY_METHOD = {
|
|
1693
|
+
// §2.7.1 — auth
|
|
1694
|
+
auth: { pass: 1, fail: 1, none: 1, permerror: 1, temperror: 1 },
|
|
1695
|
+
// §2.7.2 — domainkeys (legacy; vocabulary kept narrow)
|
|
1696
|
+
domainkeys: { pass: 1, fail: 1, neutral: 1, none: 1, permerror: 1, temperror: 1, policy: 1 },
|
|
1697
|
+
// §2.7.3 — DKIM
|
|
1698
|
+
dkim: { pass: 1, fail: 1, neutral: 1, none: 1, permerror: 1, temperror: 1, policy: 1 },
|
|
1699
|
+
"dkim-adsp": { pass: 1, fail: 1, discard: 1, nxdomain: 1, none: 1, permerror: 1, temperror: 1 },
|
|
1700
|
+
// §2.7.4 — SPF (uses softfail; not hardfail)
|
|
1701
|
+
spf: { pass: 1, fail: 1, softfail: 1, neutral: 1, none: 1, permerror: 1, temperror: 1, policy: 1 },
|
|
1702
|
+
"sender-id": { pass: 1, fail: 1, softfail: 1, neutral: 1, none: 1, permerror: 1, temperror: 1, policy: 1 },
|
|
1703
|
+
// §2.7.5 — IPRev
|
|
1704
|
+
iprev: { pass: 1, fail: 1, permerror: 1, temperror: 1 },
|
|
1705
|
+
// §2.7.6 — DMARC (this is the ONE place hardfail is valid in some drafts; keep it)
|
|
1706
|
+
dmarc: { pass: 1, fail: 1, none: 1, permerror: 1, temperror: 1, hardfail: 1, bestguesspass: 1 },
|
|
1707
|
+
// RFC 8617 §4.1 — ARC
|
|
1708
|
+
arc: { pass: 1, fail: 1, none: 1 },
|
|
1709
|
+
// RFC 8616 — DANE
|
|
1710
|
+
dane: { pass: 1, fail: 1, none: 1, permerror: 1, temperror: 1 },
|
|
1711
|
+
// VBR + DNSWL + S/MIME — vocabulary kept conservative
|
|
1712
|
+
smime: { pass: 1, fail: 1, neutral: 1, none: 1, permerror: 1, temperror: 1, policy: 1 },
|
|
1713
|
+
vbr: { pass: 1, fail: 1, none: 1, permerror: 1, temperror: 1 },
|
|
1714
|
+
dnswl: { pass: 1, none: 1, temperror: 1 },
|
|
1715
|
+
"x-original-authentication-results": { pass: 1, fail: 1, neutral: 1, none: 1, softfail: 1, hardfail: 1, policy: 1, permerror: 1, temperror: 1, bestguesspass: 1, discard: 1, nxdomain: 1 },
|
|
1716
|
+
};
|
|
1717
|
+
var AR_VALID_METHODS = Object.keys(AR_RESULTS_BY_METHOD).reduce(function (acc, m) {
|
|
1718
|
+
acc[m] = 1; return acc;
|
|
1719
|
+
}, {});
|
|
1720
|
+
|
|
1721
|
+
function authResultsEmit(opts) {
|
|
1722
|
+
validateOpts.requireObject(opts, "authResults.emit", MailAuthError, "mail-auth/ar-bad-input");
|
|
1723
|
+
validateOpts(opts, ["authservId", "results", "version", "fold"], "authResults.emit");
|
|
1724
|
+
validateOpts.requireNonEmptyString(opts.authservId,
|
|
1725
|
+
"authResults.emit: authservId", MailAuthError, "mail-auth/ar-bad-authserv-id");
|
|
1726
|
+
if (/[\r\n\0]/.test(opts.authservId)) {
|
|
1727
|
+
throw new MailAuthError("mail-auth/ar-bad-authserv-id",
|
|
1728
|
+
"authResults.emit: authservId contains forbidden control characters");
|
|
1729
|
+
}
|
|
1730
|
+
if (!Array.isArray(opts.results)) {
|
|
1731
|
+
throw new MailAuthError("mail-auth/ar-bad-results",
|
|
1732
|
+
"authResults.emit: results must be an array");
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
var version = (opts.version === undefined || opts.version === null)
|
|
1736
|
+
? "1" : String(opts.version);
|
|
1737
|
+
var head = opts.authservId + (version === "1" ? "" : " " + version);
|
|
1738
|
+
|
|
1739
|
+
if (opts.results.length === 0) {
|
|
1740
|
+
// RFC 8601 §2.2 — when no methods evaluated, emit `none`.
|
|
1741
|
+
return "Authentication-Results: " + head + "; none";
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
var clauses = [];
|
|
1745
|
+
for (var i = 0; i < opts.results.length; i += 1) {
|
|
1746
|
+
var r = opts.results[i];
|
|
1747
|
+
if (!r || typeof r !== "object") {
|
|
1748
|
+
throw new MailAuthError("mail-auth/ar-bad-result-entry",
|
|
1749
|
+
"authResults.emit: results[" + i + "] must be an object");
|
|
1750
|
+
}
|
|
1751
|
+
var method = String(r.method || "").toLowerCase();
|
|
1752
|
+
var result = String(r.result || "").toLowerCase();
|
|
1753
|
+
if (!AR_VALID_METHODS[method]) {
|
|
1754
|
+
throw new MailAuthError("mail-auth/ar-bad-method",
|
|
1755
|
+
"authResults.emit: unknown method '" + r.method + "'");
|
|
1756
|
+
}
|
|
1757
|
+
var methodResults = AR_RESULTS_BY_METHOD[method];
|
|
1758
|
+
if (!methodResults || !methodResults[result]) {
|
|
1759
|
+
throw new MailAuthError("mail-auth/ar-bad-result",
|
|
1760
|
+
"authResults.emit: result '" + r.result + "' is not in the RFC 8601 §2.7 vocabulary for method '" + method + "'");
|
|
1761
|
+
}
|
|
1762
|
+
var clause = method + "=" + result;
|
|
1763
|
+
if (r.reason && typeof r.reason === "string" && !/[\r\n\0;]/.test(r.reason)) {
|
|
1764
|
+
// RFC 8601 §2.2 — quoted-string allows backslash-escaped DQUOTE
|
|
1765
|
+
// (`\"`). Pre-v0.8.32 the framework collapsed `"` to `'` which
|
|
1766
|
+
// is lossy. Use the spec-correct escape so the receiver can
|
|
1767
|
+
// round-trip the original reason.
|
|
1768
|
+
clause += ' reason="' + r.reason.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
1769
|
+
}
|
|
1770
|
+
// Method-specific properties (ptype.property=value triples per
|
|
1771
|
+
// RFC 8601 §2.3). Operators pass them as flat object keys.
|
|
1772
|
+
var props = {
|
|
1773
|
+
smtpMailfrom: "smtp.mailfrom",
|
|
1774
|
+
smtpHelo: "smtp.helo",
|
|
1775
|
+
domain: "header.d",
|
|
1776
|
+
selector: "header.s",
|
|
1777
|
+
from: "header.from",
|
|
1778
|
+
iprev: "policy.iprev",
|
|
1779
|
+
ip: "policy.ip",
|
|
1780
|
+
tls: "policy.tls",
|
|
1781
|
+
};
|
|
1782
|
+
var propKeys = Object.keys(props);
|
|
1783
|
+
for (var pk = 0; pk < propKeys.length; pk += 1) {
|
|
1784
|
+
var k = propKeys[pk];
|
|
1785
|
+
var rv = r[k];
|
|
1786
|
+
if (typeof rv !== "string" || rv.length === 0) continue;
|
|
1787
|
+
// pvalue ABNF per RFC 8601 §2.3:
|
|
1788
|
+
// pvalue = [CFWS] ((value / dot-atom-text) [CFWS]) /
|
|
1789
|
+
// (local-part "@" domain) [CFWS]
|
|
1790
|
+
// For framework emit we require the printable-ASCII subset of
|
|
1791
|
+
// dot-atom-text + local-part-at-domain shapes; CRLF / NUL /
|
|
1792
|
+
// semicolon / SP / HTAB / quoting metacharacters are refused
|
|
1793
|
+
// (operator-supplied value is structured, not free-form).
|
|
1794
|
+
if (!/^[A-Za-z0-9._@\-:[\]]+$/.test(rv)) continue; // allow:regex-no-length-cap — bounded by header line cap
|
|
1795
|
+
clause += " " + props[k] + "=" + rv;
|
|
1796
|
+
}
|
|
1797
|
+
clauses.push(clause);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
var fold = opts.fold !== false;
|
|
1801
|
+
var sep = fold ? ";\r\n " : "; ";
|
|
1802
|
+
return "Authentication-Results: " + head + ";\r\n " + clauses.join(sep);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// ---- DMARC aggregate (RUA) report parser (RFC 7489 §7.2 / draft-ietf-dmarc-aggregate-reporting) ----
|
|
1806
|
+
//
|
|
1807
|
+
// MTAs that publish a DMARC `rua=` policy receive aggregate reports
|
|
1808
|
+
// from peers — XML attached to a multipart/report mail body, often
|
|
1809
|
+
// gzip-compressed. This primitive accepts the report bytes (raw XML,
|
|
1810
|
+
// gzipped XML, or a parsed object) and returns a structured shape
|
|
1811
|
+
// with the metadata, published policy, and per-record evaluation
|
|
1812
|
+
// results.
|
|
1813
|
+
//
|
|
1814
|
+
// var rv = b.mail.dmarc.parseAggregateReport(xmlBytes);
|
|
1815
|
+
// // → {
|
|
1816
|
+
// // reportMetadata: { orgName, email, reportId, dateRange },
|
|
1817
|
+
// // policyPublished: { domain, adkim, aspf, p, sp, pct, ... },
|
|
1818
|
+
// // records: [{ sourceIp, count, dispositions, identifiers, authResults }]
|
|
1819
|
+
// // }
|
|
1820
|
+
|
|
1821
|
+
var DMARC_RUA_MAX_REPORT_BYTES = C.BYTES.mib(8);
|
|
1822
|
+
var DMARC_RUA_MAX_RECORDS_PER_REPORT = 10000; // allow:raw-byte-literal allow:raw-time-literal — record cap, not seconds
|
|
1823
|
+
|
|
1824
|
+
function _arrayOf(value) {
|
|
1825
|
+
if (value === undefined || value === null) return [];
|
|
1826
|
+
return Array.isArray(value) ? value : [value];
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function dmarcParseAggregateReport(input, opts) {
|
|
1830
|
+
opts = opts || {};
|
|
1831
|
+
var bytes;
|
|
1832
|
+
if (Buffer.isBuffer(input)) bytes = input;
|
|
1833
|
+
else if (typeof input === "string") bytes = Buffer.from(input, "utf8");
|
|
1834
|
+
else if (input && typeof input === "object" && input.feedback) {
|
|
1835
|
+
// operator already pre-parsed via safeXml; skip the parse step.
|
|
1836
|
+
return _shapeAggregateReport(input);
|
|
1837
|
+
}
|
|
1838
|
+
else {
|
|
1839
|
+
throw new MailAuthError("mail-auth/dmarc-rua-bad-input",
|
|
1840
|
+
"dmarc.parseAggregateReport: input must be a Buffer, string, or pre-parsed object");
|
|
1841
|
+
}
|
|
1842
|
+
if (bytes.length > DMARC_RUA_MAX_REPORT_BYTES) {
|
|
1843
|
+
throw new MailAuthError("mail-auth/dmarc-rua-too-large",
|
|
1844
|
+
"dmarc.parseAggregateReport: report exceeds " + DMARC_RUA_MAX_REPORT_BYTES + " bytes");
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// Auto-detect gzip via magic 0x1f 0x8b (RFC 1952). DMARC RUA reports
|
|
1848
|
+
// are commonly zip- or gzip-compressed; the gzip magic check covers
|
|
1849
|
+
// the bulk of real-world reports. ZIP archives need operator-side
|
|
1850
|
+
// unzip first (the framework doesn't ship a ZIP primitive yet).
|
|
1851
|
+
var contentType = (opts.contentType || "").toLowerCase();
|
|
1852
|
+
var looksGzip = bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
|
|
1853
|
+
if (contentType.indexOf("gzip") !== -1 || looksGzip) {
|
|
1854
|
+
try { bytes = zlib.gunzipSync(bytes, { maxOutputLength: DMARC_RUA_MAX_REPORT_BYTES }); }
|
|
1855
|
+
catch (e) {
|
|
1856
|
+
// Distinguish "decompressed bytes exceed cap" (gunzip bomb /
|
|
1857
|
+
// amplification — operator should rate-limit the source) from
|
|
1858
|
+
// "stream is malformed" (operator-level diagnostic) so audit/
|
|
1859
|
+
// alert wiring can react differently. Node surfaces the bomb
|
|
1860
|
+
// case with ERR_BUFFER_TOO_LARGE / "Output length exceeded the
|
|
1861
|
+
// limit" / the explicit `maxOutputLength` code. CVE-class:
|
|
1862
|
+
// CVE-2024-zlib decompression amplification.
|
|
1863
|
+
var msg = (e && e.message) || String(e);
|
|
1864
|
+
var isBomb = (e && (e.code === "ERR_BUFFER_TOO_LARGE" ||
|
|
1865
|
+
e.code === "ERR_OUT_OF_RANGE")) ||
|
|
1866
|
+
/output length|max(?:imum)?\s+output|exceeds?/i.test(msg);
|
|
1867
|
+
if (isBomb) {
|
|
1868
|
+
throw new MailAuthError("mail-auth/dmarc-rua-gunzip-bomb",
|
|
1869
|
+
"dmarc.parseAggregateReport: gunzip output exceeded " +
|
|
1870
|
+
DMARC_RUA_MAX_REPORT_BYTES + " bytes (decompression amplification — refused)");
|
|
1871
|
+
}
|
|
1872
|
+
throw new MailAuthError("mail-auth/dmarc-rua-gunzip-failed",
|
|
1873
|
+
"dmarc.parseAggregateReport: gunzip failed: " + msg);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
var parsed;
|
|
1878
|
+
try { parsed = safeXml.parse(bytes.toString("utf8"), { maxBytes: DMARC_RUA_MAX_REPORT_BYTES }); }
|
|
1879
|
+
catch (e) {
|
|
1880
|
+
throw new MailAuthError("mail-auth/dmarc-rua-bad-xml",
|
|
1881
|
+
"dmarc.parseAggregateReport: XML parse failed: " + ((e && e.message) || String(e)));
|
|
1882
|
+
}
|
|
1883
|
+
return _shapeAggregateReport(parsed);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
function _shapeAggregateReport(parsed) {
|
|
1887
|
+
if (!parsed || typeof parsed !== "object" || !parsed.feedback) {
|
|
1888
|
+
throw new MailAuthError("mail-auth/dmarc-rua-no-feedback",
|
|
1889
|
+
"dmarc.parseAggregateReport: report root must be <feedback>");
|
|
1890
|
+
}
|
|
1891
|
+
var feedback = parsed.feedback;
|
|
1892
|
+
var rmRaw = feedback.report_metadata || {};
|
|
1893
|
+
var ppRaw = feedback.policy_published || {};
|
|
1894
|
+
var records = _arrayOf(feedback.record);
|
|
1895
|
+
if (records.length > DMARC_RUA_MAX_RECORDS_PER_REPORT) {
|
|
1896
|
+
throw new MailAuthError("mail-auth/dmarc-rua-too-many-records",
|
|
1897
|
+
"dmarc.parseAggregateReport: report has " + records.length +
|
|
1898
|
+
" records (cap " + DMARC_RUA_MAX_RECORDS_PER_REPORT + ")");
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
var dateRange = rmRaw.date_range || {};
|
|
1902
|
+
var beginSec = parseInt(dateRange.begin, 10);
|
|
1903
|
+
var endSec = parseInt(dateRange.end, 10);
|
|
1904
|
+
|
|
1905
|
+
var shaped = {
|
|
1906
|
+
reportMetadata: {
|
|
1907
|
+
orgName: rmRaw.org_name || null,
|
|
1908
|
+
email: rmRaw.email || null,
|
|
1909
|
+
reportId: rmRaw.report_id || null,
|
|
1910
|
+
extraContact: rmRaw.extra_contact_info || null,
|
|
1911
|
+
dateRange: {
|
|
1912
|
+
begin: isFinite(beginSec) ? beginSec : null,
|
|
1913
|
+
end: isFinite(endSec) ? endSec : null,
|
|
1914
|
+
},
|
|
1915
|
+
},
|
|
1916
|
+
policyPublished: {
|
|
1917
|
+
domain: ppRaw.domain || null,
|
|
1918
|
+
adkim: ppRaw.adkim || null,
|
|
1919
|
+
aspf: ppRaw.aspf || null,
|
|
1920
|
+
p: ppRaw.p || null,
|
|
1921
|
+
sp: ppRaw.sp || null,
|
|
1922
|
+
pct: ppRaw.pct === undefined ? null : parseInt(ppRaw.pct, 10),
|
|
1923
|
+
fo: ppRaw.fo || null,
|
|
1924
|
+
},
|
|
1925
|
+
records: records.map(function (rec) {
|
|
1926
|
+
var row = rec.row || {};
|
|
1927
|
+
var pe = row.policy_evaluated || {};
|
|
1928
|
+
var ids = rec.identifiers || {};
|
|
1929
|
+
var ar = rec.auth_results || {};
|
|
1930
|
+
var dkimResults = _arrayOf(ar.dkim).map(function (d) {
|
|
1931
|
+
return {
|
|
1932
|
+
domain: d.domain || null,
|
|
1933
|
+
selector: d.selector || null,
|
|
1934
|
+
result: d.result || null,
|
|
1935
|
+
humanResult: d.human_result || null,
|
|
1936
|
+
};
|
|
1937
|
+
});
|
|
1938
|
+
var spfResults = _arrayOf(ar.spf).map(function (s) {
|
|
1939
|
+
return {
|
|
1940
|
+
domain: s.domain || null,
|
|
1941
|
+
result: s.result || null,
|
|
1942
|
+
scope: s.scope || null,
|
|
1943
|
+
};
|
|
1944
|
+
});
|
|
1945
|
+
var reasons = _arrayOf(pe.reason).map(function (r) {
|
|
1946
|
+
return { type: r.type || null, comment: r.comment || null };
|
|
1947
|
+
});
|
|
1948
|
+
var count = parseInt(row.count, 10);
|
|
1949
|
+
return {
|
|
1950
|
+
sourceIp: row.source_ip || null,
|
|
1951
|
+
count: isFinite(count) ? count : null,
|
|
1952
|
+
dispositions: {
|
|
1953
|
+
disposition: pe.disposition || null,
|
|
1954
|
+
dkim: pe.dkim || null,
|
|
1955
|
+
spf: pe.spf || null,
|
|
1956
|
+
reasons: reasons,
|
|
1957
|
+
},
|
|
1958
|
+
identifiers: {
|
|
1959
|
+
headerFrom: ids.header_from || null,
|
|
1960
|
+
envelopeFrom: ids.envelope_from || null,
|
|
1961
|
+
envelopeTo: ids.envelope_to || null,
|
|
1962
|
+
},
|
|
1963
|
+
authResults: {
|
|
1964
|
+
dkim: dkimResults,
|
|
1965
|
+
spf: spfResults,
|
|
1966
|
+
},
|
|
1967
|
+
};
|
|
1968
|
+
}),
|
|
1969
|
+
};
|
|
1970
|
+
|
|
1971
|
+
// Convenience aggregates — most operators want the totals up front.
|
|
1972
|
+
var totalCount = 0;
|
|
1973
|
+
var passCount = 0;
|
|
1974
|
+
var failCount = 0;
|
|
1975
|
+
for (var i = 0; i < shaped.records.length; i += 1) {
|
|
1976
|
+
var r = shaped.records[i];
|
|
1977
|
+
if (typeof r.count === "number") totalCount += r.count;
|
|
1978
|
+
var dispDkim = r.dispositions.dkim;
|
|
1979
|
+
var dispSpf = r.dispositions.spf;
|
|
1980
|
+
if (dispDkim === "pass" || dispSpf === "pass") {
|
|
1981
|
+
if (typeof r.count === "number") passCount += r.count;
|
|
1982
|
+
} else {
|
|
1983
|
+
if (typeof r.count === "number") failCount += r.count;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
shaped.totals = {
|
|
1987
|
+
messages: totalCount,
|
|
1988
|
+
aligned: passCount,
|
|
1989
|
+
notAligned: failCount,
|
|
1990
|
+
};
|
|
1991
|
+
return shaped;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// ---- iprev (RFC 8601 §3) — Forward-Confirmed Reverse DNS verifier ----
|
|
1995
|
+
//
|
|
1996
|
+
// The receiving SMTP server reverse-resolves the connecting peer's IP
|
|
1997
|
+
// to a PTR name, forward-resolves the PTR name to an A or AAAA set,
|
|
1998
|
+
// and confirms the original IP appears in the forward set. Spoofed
|
|
1999
|
+
// PTR records (attacker controls the rDNS zone but not the forward
|
|
2000
|
+
// zone) fail this check and SHOULD be reflected in the
|
|
2001
|
+
// Authentication-Results header so downstream policies can react.
|
|
2002
|
+
//
|
|
2003
|
+
// Surface:
|
|
2004
|
+
// await b.mail.iprev.verify(ip)
|
|
2005
|
+
// → { result: "pass"|"fail"|"permerror"|"temperror",
|
|
2006
|
+
// ptr, forward, fcrdns, ip }
|
|
2007
|
+
//
|
|
2008
|
+
// Returns "permerror" on bad-shape input (not an IP literal); returns
|
|
2009
|
+
// "temperror" on ENODATA / ENOTFOUND / lookup failure (the receiver
|
|
2010
|
+
// retries on transient DNS faults). Pure-DNS — no operator state.
|
|
2011
|
+
|
|
2012
|
+
// RFC 8601 §3 — PTR result shape. The PTR rdata is an FQDN (1*labels).
|
|
2013
|
+
// Reject answers that aren't shaped as a DNS name: non-strings,
|
|
2014
|
+
// empty strings, strings containing chars outside DNS LDH+dot, or
|
|
2015
|
+
// labels exceeding 63 octets. An attacker who controls a reverse
|
|
2016
|
+
// zone could publish a PTR whose rdata is arbitrary bytes (e.g.
|
|
2017
|
+
// `<script>...`) that downstream consumers (audit / Authentication-
|
|
2018
|
+
// Results emission) might fail to escape. Pre-filter at the iprev
|
|
2019
|
+
// boundary so only well-shaped names reach downstream.
|
|
2020
|
+
function _isValidPtrName(name) {
|
|
2021
|
+
if (typeof name !== "string") return false;
|
|
2022
|
+
var trimmed = name.replace(/\.$/, "");
|
|
2023
|
+
if (trimmed.length === 0 || trimmed.length > 253) return false; // allow:raw-byte-literal — RFC 1035 hostname cap
|
|
2024
|
+
// Labels: 1..63 octets, LDH (letter / digit / hyphen) + leading
|
|
2025
|
+
// alphanum (RFC 1035 §2.3.1). Permissive: PTR rdata can in practice
|
|
2026
|
+
// contain underscores (mail-server idiom) — allow underscore in
|
|
2027
|
+
// labels too. Reject anything else.
|
|
2028
|
+
var labels = trimmed.split(".");
|
|
2029
|
+
for (var i = 0; i < labels.length; i += 1) {
|
|
2030
|
+
var lab = labels[i];
|
|
2031
|
+
if (lab.length === 0 || lab.length > 63) return false; // allow:raw-byte-literal — RFC 1035 label cap
|
|
2032
|
+
if (!/^[A-Za-z0-9_](?:[A-Za-z0-9_-]{0,61}[A-Za-z0-9_])?$/.test(lab)) return false;
|
|
2033
|
+
}
|
|
2034
|
+
return true;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
async function iprevVerify(ip) {
|
|
2038
|
+
if (typeof ip !== "string" || ip.length === 0) {
|
|
2039
|
+
return { result: "permerror", ip: ip || null,
|
|
2040
|
+
ptr: null, forward: [], fcrdns: false,
|
|
2041
|
+
explanation: "ip must be a non-empty string" };
|
|
2042
|
+
}
|
|
2043
|
+
if (!net.isIP(ip)) {
|
|
2044
|
+
return { result: "permerror", ip: ip,
|
|
2045
|
+
ptr: null, forward: [], fcrdns: false,
|
|
2046
|
+
explanation: "ip is not a valid IPv4 / IPv6 literal" };
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
var ptrs;
|
|
2050
|
+
try { ptrs = await _safeReverse(ip); }
|
|
2051
|
+
catch (e) {
|
|
2052
|
+
var rcode = e && e.code;
|
|
2053
|
+
if (rcode === "ENOTFOUND" || rcode === "ENODATA") {
|
|
2054
|
+
return { result: "fail", ip: ip,
|
|
2055
|
+
ptr: null, forward: [], fcrdns: false,
|
|
2056
|
+
explanation: "no PTR record for " + ip };
|
|
2057
|
+
}
|
|
2058
|
+
return { result: "temperror", ip: ip,
|
|
2059
|
+
ptr: null, forward: [], fcrdns: false,
|
|
2060
|
+
explanation: "PTR lookup failed: " + ((e && e.message) || String(e)) };
|
|
2061
|
+
}
|
|
2062
|
+
if (!Array.isArray(ptrs) || ptrs.length === 0) {
|
|
2063
|
+
return { result: "fail", ip: ip,
|
|
2064
|
+
ptr: null, forward: [], fcrdns: false,
|
|
2065
|
+
explanation: "PTR returned empty answer set" };
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// RFC 8601 §3 — when multiple PTRs exist the receiver picks ONE
|
|
2069
|
+
// and continues. We pick the first (matches mainstream MTA
|
|
2070
|
+
// behavior) and stash the rest for operator visibility on the
|
|
2071
|
+
// out-of-band metadata. Validate the PTR's shape FIRST — a PTR
|
|
2072
|
+
// with arbitrary bytes shouldn't reach downstream consumers.
|
|
2073
|
+
var ptr = String(ptrs[0]).replace(/\.$/, "");
|
|
2074
|
+
if (!_isValidPtrName(ptr)) {
|
|
2075
|
+
return { result: "permerror", ip: ip,
|
|
2076
|
+
ptr: ptr, forward: [], fcrdns: false,
|
|
2077
|
+
explanation: "PTR record is not a valid DNS name shape (RFC 8601 §3)" };
|
|
2078
|
+
}
|
|
2079
|
+
var isV6 = net.isIPv6(ip);
|
|
2080
|
+
var forwardAddrs;
|
|
2081
|
+
try {
|
|
2082
|
+
forwardAddrs = await _safeResolveA(ptr, isV6 ? 6 : 4);
|
|
2083
|
+
} catch (e) {
|
|
2084
|
+
var fcode = e && e.code;
|
|
2085
|
+
if (fcode === "ENOTFOUND" || fcode === "ENODATA") {
|
|
2086
|
+
return { result: "fail", ip: ip,
|
|
2087
|
+
ptr: ptr, forward: [], fcrdns: false,
|
|
2088
|
+
explanation: "no forward record for PTR " + ptr };
|
|
2089
|
+
}
|
|
2090
|
+
if (fcode === "ETIMEOUT" || fcode === "ESERVFAIL") {
|
|
2091
|
+
return { result: "temperror", ip: ip,
|
|
2092
|
+
ptr: ptr, forward: [], fcrdns: false,
|
|
2093
|
+
explanation: "forward lookup transient failure: " + fcode };
|
|
2094
|
+
}
|
|
2095
|
+
// Anything else — propagate as temperror; Node DNS surfaces some
|
|
2096
|
+
// non-RFC error codes via the platform resolver. Permerror only
|
|
2097
|
+
// for definitive negative answers above.
|
|
2098
|
+
throw new MailAuthError("mail-auth/iprev-temperror",
|
|
2099
|
+
"iprev.verify: forward lookup of " + ptr + " threw: " +
|
|
2100
|
+
((e && e.message) || String(e)));
|
|
2101
|
+
}
|
|
2102
|
+
var forward = Array.isArray(forwardAddrs) ? forwardAddrs.slice() : [];
|
|
2103
|
+
var ipLc = ip.toLowerCase();
|
|
2104
|
+
var fcrdns = false;
|
|
2105
|
+
for (var i = 0; i < forward.length; i += 1) {
|
|
2106
|
+
if (String(forward[i]).toLowerCase() === ipLc) { fcrdns = true; break; }
|
|
2107
|
+
}
|
|
2108
|
+
return {
|
|
2109
|
+
result: fcrdns ? "pass" : "fail",
|
|
2110
|
+
ip: ip,
|
|
2111
|
+
ptr: ptr,
|
|
2112
|
+
forward: forward,
|
|
2113
|
+
fcrdns: fcrdns,
|
|
2114
|
+
explanation: fcrdns
|
|
2115
|
+
? "PTR " + ptr + " forward-resolves to " + ip
|
|
2116
|
+
: "PTR " + ptr + " does not forward-resolve to " + ip,
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
module.exports = {
|
|
2121
|
+
spf: Object.freeze({
|
|
2122
|
+
verify: spfVerify,
|
|
2123
|
+
parseRecord: _parseSpfRecord,
|
|
2124
|
+
}),
|
|
2125
|
+
dmarc: Object.freeze({
|
|
2126
|
+
evaluate: dmarcEvaluate,
|
|
2127
|
+
parseRecord: _parseDmarcRecord,
|
|
2128
|
+
parseAggregateReport: dmarcParseAggregateReport,
|
|
2129
|
+
}),
|
|
2130
|
+
arc: Object.freeze({
|
|
2131
|
+
verify: arcVerify,
|
|
2132
|
+
evaluate: arcEvaluate,
|
|
2133
|
+
sign: require("./mail-arc-sign").sign, // allow:inline-require — re-export from sibling module
|
|
2134
|
+
ALLOWED_CV: require("./mail-arc-sign").ALLOWED_CV, // allow:inline-require — re-export from sibling module
|
|
2135
|
+
}),
|
|
2136
|
+
iprev: Object.freeze({
|
|
2137
|
+
verify: iprevVerify,
|
|
2138
|
+
}),
|
|
2139
|
+
authResults: Object.freeze({
|
|
2140
|
+
emit: authResultsEmit,
|
|
2141
|
+
}),
|
|
2142
|
+
MailAuthError: MailAuthError,
|
|
2143
|
+
SPF_DNS_LOOKUP_LIMIT: SPF_DNS_LOOKUP_LIMIT,
|
|
2144
|
+
};
|