@blamejs/blamejs-shop 0.4.56 → 0.4.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +426 -366
- package/lib/vendor/blamejs/.github/codeql/codeql-config.yml +25 -0
- package/lib/vendor/blamejs/.github/workflows/actions-lint.yml +3 -3
- package/lib/vendor/blamejs/.github/workflows/cflite_batch.yml +1 -1
- package/lib/vendor/blamejs/.github/workflows/cflite_pr.yml +1 -1
- package/lib/vendor/blamejs/.github/workflows/ci.yml +13 -13
- package/lib/vendor/blamejs/.github/workflows/codeql.yml +8 -7
- package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +2 -2
- package/lib/vendor/blamejs/.github/workflows/release-container.yml +4 -4
- package/lib/vendor/blamejs/.github/workflows/scorecard.yml +1 -1
- package/lib/vendor/blamejs/.github/workflows/sha-to-tag-verify.yml +1 -1
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +1381 -4
- package/lib/vendor/blamejs/eslint.config.mjs +1 -0
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +4 -1
- package/lib/vendor/blamejs/examples/wiki/lib/symbol-index.js +5 -2
- package/lib/vendor/blamejs/examples/wiki/routes/pages.js +4 -1
- package/lib/vendor/blamejs/fuzz/guard-text.fuzz.js +20 -0
- package/lib/vendor/blamejs/index.js +2 -0
- package/lib/vendor/blamejs/lib/a2a-tasks.js +7 -23
- package/lib/vendor/blamejs/lib/acme.js +6 -5
- package/lib/vendor/blamejs/lib/agent-event-bus.js +5 -4
- package/lib/vendor/blamejs/lib/agent-idempotency.js +2 -5
- package/lib/vendor/blamejs/lib/agent-orchestrator.js +2 -5
- package/lib/vendor/blamejs/lib/agent-saga.js +3 -5
- package/lib/vendor/blamejs/lib/agent-tenant.js +2 -5
- package/lib/vendor/blamejs/lib/ai-adverse-decision.js +2 -15
- package/lib/vendor/blamejs/lib/ai-capability.js +1 -6
- package/lib/vendor/blamejs/lib/ai-dp.js +1 -5
- package/lib/vendor/blamejs/lib/ai-input.js +2 -2
- package/lib/vendor/blamejs/lib/ai-pref.js +3 -8
- package/lib/vendor/blamejs/lib/ai-quota.js +3 -14
- package/lib/vendor/blamejs/lib/api-key.js +37 -28
- package/lib/vendor/blamejs/lib/archive-adapters.js +2 -4
- package/lib/vendor/blamejs/lib/archive-entry-policy.js +32 -0
- package/lib/vendor/blamejs/lib/archive-read.js +5 -17
- package/lib/vendor/blamejs/lib/archive-tar-read.js +5 -16
- package/lib/vendor/blamejs/lib/archive.js +2 -10
- package/lib/vendor/blamejs/lib/arg-parser.js +7 -6
- package/lib/vendor/blamejs/lib/asyncapi-traits.js +2 -6
- package/lib/vendor/blamejs/lib/atomic-file.js +108 -31
- package/lib/vendor/blamejs/lib/audit-chain.js +133 -53
- package/lib/vendor/blamejs/lib/audit-daily-review.js +24 -14
- package/lib/vendor/blamejs/lib/audit-emit.js +82 -0
- package/lib/vendor/blamejs/lib/audit-sign.js +257 -0
- package/lib/vendor/blamejs/lib/audit.js +84 -0
- package/lib/vendor/blamejs/lib/auth/access-lock.js +5 -27
- package/lib/vendor/blamejs/lib/auth/dpop.js +23 -35
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +9 -15
- package/lib/vendor/blamejs/lib/auth/jwt-external.js +26 -8
- package/lib/vendor/blamejs/lib/auth/jwt.js +13 -15
- package/lib/vendor/blamejs/lib/auth/lockout.js +6 -25
- package/lib/vendor/blamejs/lib/auth/oauth.js +67 -45
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +55 -32
- package/lib/vendor/blamejs/lib/auth/oid4vp.js +3 -2
- package/lib/vendor/blamejs/lib/auth/openid-federation.js +6 -6
- package/lib/vendor/blamejs/lib/auth/passkey.js +3 -3
- package/lib/vendor/blamejs/lib/auth/password.js +7 -1
- package/lib/vendor/blamejs/lib/auth/saml.js +37 -27
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc-holder.js +2 -14
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc-issuer.js +2 -14
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +1 -1
- package/lib/vendor/blamejs/lib/auth/status-list.js +7 -7
- package/lib/vendor/blamejs/lib/auth/step-up.js +6 -12
- package/lib/vendor/blamejs/lib/auth-bot-challenge.js +6 -37
- package/lib/vendor/blamejs/lib/backup/bundle.js +11 -18
- package/lib/vendor/blamejs/lib/backup/index.js +14 -47
- package/lib/vendor/blamejs/lib/bounded-map.js +112 -1
- package/lib/vendor/blamejs/lib/breach-deadline.js +1 -11
- package/lib/vendor/blamejs/lib/cache.js +7 -18
- package/lib/vendor/blamejs/lib/cbor.js +2 -1
- package/lib/vendor/blamejs/lib/cdn-cache-control.js +8 -9
- package/lib/vendor/blamejs/lib/cert.js +3 -10
- package/lib/vendor/blamejs/lib/chain-writer.js +162 -47
- package/lib/vendor/blamejs/lib/cli.js +5 -1
- package/lib/vendor/blamejs/lib/client-hints.js +7 -9
- package/lib/vendor/blamejs/lib/cloud-events.js +40 -31
- package/lib/vendor/blamejs/lib/cluster-storage.js +2 -38
- package/lib/vendor/blamejs/lib/cms-codec.js +2 -1
- package/lib/vendor/blamejs/lib/codepoint-class.js +67 -1
- package/lib/vendor/blamejs/lib/compliance-ai-act-logging.js +1 -6
- package/lib/vendor/blamejs/lib/compliance-ai-act-transparency.js +2 -4
- package/lib/vendor/blamejs/lib/compliance-eaa.js +1 -11
- package/lib/vendor/blamejs/lib/compliance-sanctions-fetcher.js +21 -28
- package/lib/vendor/blamejs/lib/compliance-sanctions.js +2 -14
- package/lib/vendor/blamejs/lib/compliance.js +2 -9
- package/lib/vendor/blamejs/lib/config-drift.js +2 -12
- package/lib/vendor/blamejs/lib/content-digest.js +10 -8
- package/lib/vendor/blamejs/lib/cookies.js +5 -11
- package/lib/vendor/blamejs/lib/cose.js +19 -11
- package/lib/vendor/blamejs/lib/cra-report.js +1 -11
- package/lib/vendor/blamejs/lib/crypto-field.js +5 -10
- package/lib/vendor/blamejs/lib/crypto.js +120 -3
- package/lib/vendor/blamejs/lib/csp.js +235 -3
- package/lib/vendor/blamejs/lib/daemon.js +42 -41
- package/lib/vendor/blamejs/lib/data-act.js +19 -9
- package/lib/vendor/blamejs/lib/db-query.js +6 -40
- package/lib/vendor/blamejs/lib/db.js +34 -12
- package/lib/vendor/blamejs/lib/dbsc.js +5 -6
- package/lib/vendor/blamejs/lib/ddl-change-control.js +18 -14
- package/lib/vendor/blamejs/lib/deprecate.js +4 -5
- package/lib/vendor/blamejs/lib/did.js +6 -2
- package/lib/vendor/blamejs/lib/dsr.js +8 -12
- package/lib/vendor/blamejs/lib/external-db-migrate.js +21 -22
- package/lib/vendor/blamejs/lib/external-db.js +14 -26
- package/lib/vendor/blamejs/lib/fda-21cfr11.js +12 -8
- package/lib/vendor/blamejs/lib/fdx.js +22 -18
- package/lib/vendor/blamejs/lib/file-upload.js +80 -66
- package/lib/vendor/blamejs/lib/flag-evaluation-context.js +5 -8
- package/lib/vendor/blamejs/lib/framework-error.js +12 -0
- package/lib/vendor/blamejs/lib/framework-schema.js +19 -0
- package/lib/vendor/blamejs/lib/fsm.js +7 -12
- package/lib/vendor/blamejs/lib/gate-contract.js +869 -38
- package/lib/vendor/blamejs/lib/gdpr-ropa.js +4 -13
- package/lib/vendor/blamejs/lib/graphql-federation.js +1 -1
- package/lib/vendor/blamejs/lib/guard-agent-registry.js +2 -1
- package/lib/vendor/blamejs/lib/guard-all.js +1 -0
- package/lib/vendor/blamejs/lib/guard-archive.js +9 -30
- package/lib/vendor/blamejs/lib/guard-auth.js +23 -80
- package/lib/vendor/blamejs/lib/guard-cidr.js +20 -96
- package/lib/vendor/blamejs/lib/guard-csv.js +135 -196
- package/lib/vendor/blamejs/lib/guard-domain.js +23 -106
- package/lib/vendor/blamejs/lib/guard-dsn.js +16 -13
- package/lib/vendor/blamejs/lib/guard-email.js +46 -53
- package/lib/vendor/blamejs/lib/guard-envelope.js +1 -1
- package/lib/vendor/blamejs/lib/guard-event-bus-topic.js +2 -1
- package/lib/vendor/blamejs/lib/guard-filename.js +12 -60
- package/lib/vendor/blamejs/lib/guard-graphql.js +28 -75
- package/lib/vendor/blamejs/lib/guard-html-wcag.js +15 -2
- package/lib/vendor/blamejs/lib/guard-html.js +65 -117
- package/lib/vendor/blamejs/lib/guard-idempotency-key.js +2 -1
- package/lib/vendor/blamejs/lib/guard-image.js +280 -77
- package/lib/vendor/blamejs/lib/guard-imap-command.js +8 -9
- package/lib/vendor/blamejs/lib/guard-json.js +87 -103
- package/lib/vendor/blamejs/lib/guard-jsonpath.js +20 -88
- package/lib/vendor/blamejs/lib/guard-jwt.js +32 -114
- package/lib/vendor/blamejs/lib/guard-list-id.js +2 -7
- package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +2 -7
- package/lib/vendor/blamejs/lib/guard-mail-compose.js +5 -6
- package/lib/vendor/blamejs/lib/guard-mail-move.js +2 -1
- package/lib/vendor/blamejs/lib/guard-mail-query.js +5 -3
- package/lib/vendor/blamejs/lib/guard-mail-sieve.js +6 -7
- package/lib/vendor/blamejs/lib/guard-managesieve-command.js +5 -4
- package/lib/vendor/blamejs/lib/guard-markdown.js +76 -110
- package/lib/vendor/blamejs/lib/guard-message-id.js +5 -6
- package/lib/vendor/blamejs/lib/guard-mime.js +20 -99
- package/lib/vendor/blamejs/lib/guard-oauth.js +19 -73
- package/lib/vendor/blamejs/lib/guard-pdf.js +65 -72
- package/lib/vendor/blamejs/lib/guard-pop3-command.js +12 -13
- package/lib/vendor/blamejs/lib/guard-posture-chain.js +2 -1
- package/lib/vendor/blamejs/lib/guard-regex.js +24 -99
- package/lib/vendor/blamejs/lib/guard-saga-config.js +2 -1
- package/lib/vendor/blamejs/lib/guard-shell.js +22 -87
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +8 -11
- package/lib/vendor/blamejs/lib/guard-sql.js +15 -13
- package/lib/vendor/blamejs/lib/guard-stream-args.js +2 -1
- package/lib/vendor/blamejs/lib/guard-svg.js +95 -140
- package/lib/vendor/blamejs/lib/guard-template.js +23 -80
- package/lib/vendor/blamejs/lib/guard-tenant-id.js +2 -1
- package/lib/vendor/blamejs/lib/guard-text.js +592 -0
- package/lib/vendor/blamejs/lib/guard-time.js +27 -95
- package/lib/vendor/blamejs/lib/guard-uuid.js +21 -93
- package/lib/vendor/blamejs/lib/guard-xml.js +76 -106
- package/lib/vendor/blamejs/lib/guard-yaml.js +24 -60
- package/lib/vendor/blamejs/lib/http-client-cache.js +5 -12
- package/lib/vendor/blamejs/lib/http-client-cookie-jar.js +2 -4
- package/lib/vendor/blamejs/lib/http-client.js +8 -21
- package/lib/vendor/blamejs/lib/http-message-signature.js +3 -2
- package/lib/vendor/blamejs/lib/i18n-messageformat.js +5 -7
- package/lib/vendor/blamejs/lib/i18n.js +83 -26
- package/lib/vendor/blamejs/lib/inbox.js +8 -8
- package/lib/vendor/blamejs/lib/incident-report.js +3 -21
- package/lib/vendor/blamejs/lib/ip-utils.js +49 -6
- package/lib/vendor/blamejs/lib/jobs.js +3 -2
- package/lib/vendor/blamejs/lib/keychain.js +6 -18
- package/lib/vendor/blamejs/lib/legal-hold.js +6 -15
- package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +44 -112
- package/lib/vendor/blamejs/lib/log-stream-otlp-grpc.js +17 -14
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +16 -80
- package/lib/vendor/blamejs/lib/log-stream-webhook.js +20 -92
- package/lib/vendor/blamejs/lib/mail-arc-sign.js +8 -3
- package/lib/vendor/blamejs/lib/mail-arf.js +3 -2
- package/lib/vendor/blamejs/lib/mail-auth.js +40 -66
- package/lib/vendor/blamejs/lib/mail-bimi.js +19 -39
- package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +3 -2
- package/lib/vendor/blamejs/lib/mail-dav.js +8 -35
- package/lib/vendor/blamejs/lib/mail-deploy.js +6 -9
- package/lib/vendor/blamejs/lib/mail-dkim.js +19 -38
- package/lib/vendor/blamejs/lib/mail-greylist.js +15 -26
- package/lib/vendor/blamejs/lib/mail-helo.js +2 -3
- package/lib/vendor/blamejs/lib/mail-journal.js +2 -1
- package/lib/vendor/blamejs/lib/mail-mdn.js +4 -3
- package/lib/vendor/blamejs/lib/mail-rbl.js +5 -8
- package/lib/vendor/blamejs/lib/mail-scan.js +2 -2
- package/lib/vendor/blamejs/lib/mail-send-deliver.js +33 -20
- package/lib/vendor/blamejs/lib/mail-server-imap.js +32 -87
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +35 -51
- package/lib/vendor/blamejs/lib/mail-server-managesieve.js +29 -83
- package/lib/vendor/blamejs/lib/mail-server-mx.js +33 -74
- package/lib/vendor/blamejs/lib/mail-server-net.js +177 -0
- package/lib/vendor/blamejs/lib/mail-server-pop3.js +30 -83
- package/lib/vendor/blamejs/lib/mail-server-rate-limit.js +30 -6
- package/lib/vendor/blamejs/lib/mail-server-submission.js +34 -73
- package/lib/vendor/blamejs/lib/mail-server-tls.js +89 -0
- package/lib/vendor/blamejs/lib/mail-spam-score.js +7 -8
- package/lib/vendor/blamejs/lib/mail-store.js +3 -2
- package/lib/vendor/blamejs/lib/mail.js +6 -11
- package/lib/vendor/blamejs/lib/markup-escape.js +31 -0
- package/lib/vendor/blamejs/lib/markup-tokenizer.js +54 -0
- package/lib/vendor/blamejs/lib/mcp-tool-registry.js +26 -23
- package/lib/vendor/blamejs/lib/mcp.js +1 -1
- package/lib/vendor/blamejs/lib/mdoc.js +11 -12
- package/lib/vendor/blamejs/lib/metrics.js +32 -30
- package/lib/vendor/blamejs/lib/middleware/age-gate.js +3 -23
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +11 -22
- package/lib/vendor/blamejs/lib/middleware/assetlinks.js +2 -7
- package/lib/vendor/blamejs/lib/middleware/bearer-auth.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +31 -48
- package/lib/vendor/blamejs/lib/middleware/bot-disclose.js +7 -10
- package/lib/vendor/blamejs/lib/middleware/bot-guard.js +2 -10
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +13 -2
- package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +6 -26
- package/lib/vendor/blamejs/lib/middleware/deny-response.js +19 -1
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +7 -0
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +4 -20
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +20 -28
- package/lib/vendor/blamejs/lib/middleware/scim-server.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/security-headers.js +9 -1
- package/lib/vendor/blamejs/lib/middleware/security-txt.js +2 -6
- package/lib/vendor/blamejs/lib/middleware/tus-upload.js +12 -27
- package/lib/vendor/blamejs/lib/middleware/web-app-manifest.js +2 -7
- package/lib/vendor/blamejs/lib/migrations.js +4 -13
- package/lib/vendor/blamejs/lib/mime-parse.js +34 -17
- package/lib/vendor/blamejs/lib/module-loader.js +44 -0
- package/lib/vendor/blamejs/lib/money.js +105 -0
- package/lib/vendor/blamejs/lib/mtls-ca.js +116 -36
- package/lib/vendor/blamejs/lib/network-byte-quota.js +4 -18
- package/lib/vendor/blamejs/lib/network-dane.js +8 -6
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +97 -6
- package/lib/vendor/blamejs/lib/network-dnssec.js +16 -16
- package/lib/vendor/blamejs/lib/network-heartbeat.js +3 -2
- package/lib/vendor/blamejs/lib/network-smtp-policy.js +29 -41
- package/lib/vendor/blamejs/lib/network-tls.js +40 -42
- package/lib/vendor/blamejs/lib/nis2-report.js +1 -11
- package/lib/vendor/blamejs/lib/nonce-store.js +81 -3
- package/lib/vendor/blamejs/lib/numeric-bounds.js +22 -8
- package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +2 -6
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +5 -45
- package/lib/vendor/blamejs/lib/object-store/gcs.js +8 -71
- package/lib/vendor/blamejs/lib/object-store/http-put.js +1 -5
- package/lib/vendor/blamejs/lib/object-store/http-request.js +128 -0
- package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +2 -1
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +9 -77
- package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +9 -25
- package/lib/vendor/blamejs/lib/observability.js +95 -0
- package/lib/vendor/blamejs/lib/openapi-paths-builder.js +7 -3
- package/lib/vendor/blamejs/lib/otel-export.js +7 -10
- package/lib/vendor/blamejs/lib/outbox.js +43 -37
- package/lib/vendor/blamejs/lib/pagination.js +11 -15
- package/lib/vendor/blamejs/lib/parsers/safe-env.js +5 -4
- package/lib/vendor/blamejs/lib/parsers/safe-ini.js +10 -4
- package/lib/vendor/blamejs/lib/parsers/safe-toml.js +11 -9
- package/lib/vendor/blamejs/lib/parsers/safe-xml.js +2 -2
- package/lib/vendor/blamejs/lib/parsers/safe-yaml.js +7 -6
- package/lib/vendor/blamejs/lib/pick.js +63 -5
- package/lib/vendor/blamejs/lib/pipl-cn.js +69 -59
- package/lib/vendor/blamejs/lib/privacy-pass.js +8 -6
- package/lib/vendor/blamejs/lib/problem-details.js +2 -5
- package/lib/vendor/blamejs/lib/pubsub.js +4 -8
- package/lib/vendor/blamejs/lib/redact.js +2 -1
- package/lib/vendor/blamejs/lib/render.js +4 -2
- package/lib/vendor/blamejs/lib/request-helpers.js +133 -6
- package/lib/vendor/blamejs/lib/restore.js +5 -22
- package/lib/vendor/blamejs/lib/retention.js +3 -5
- package/lib/vendor/blamejs/lib/safe-async.js +457 -0
- package/lib/vendor/blamejs/lib/safe-buffer.js +137 -6
- package/lib/vendor/blamejs/lib/safe-decompress.js +2 -1
- package/lib/vendor/blamejs/lib/safe-dns.js +2 -1
- package/lib/vendor/blamejs/lib/safe-ical.js +12 -26
- package/lib/vendor/blamejs/lib/safe-json.js +7 -8
- package/lib/vendor/blamejs/lib/safe-jsonpath.js +6 -5
- package/lib/vendor/blamejs/lib/safe-mime.js +25 -29
- package/lib/vendor/blamejs/lib/safe-redirect.js +2 -5
- package/lib/vendor/blamejs/lib/safe-schema.js +7 -6
- package/lib/vendor/blamejs/lib/safe-sql.js +126 -0
- package/lib/vendor/blamejs/lib/safe-vcard.js +12 -26
- package/lib/vendor/blamejs/lib/sandbox-worker.js +9 -2
- package/lib/vendor/blamejs/lib/sandbox.js +3 -2
- package/lib/vendor/blamejs/lib/scheduler.js +5 -12
- package/lib/vendor/blamejs/lib/sec-cyber.js +82 -37
- package/lib/vendor/blamejs/lib/seeders.js +4 -11
- package/lib/vendor/blamejs/lib/self-update.js +92 -69
- package/lib/vendor/blamejs/lib/session-device-binding.js +112 -66
- package/lib/vendor/blamejs/lib/session.js +194 -6
- package/lib/vendor/blamejs/lib/sql.js +225 -78
- package/lib/vendor/blamejs/lib/sse.js +6 -10
- package/lib/vendor/blamejs/lib/static.js +136 -98
- package/lib/vendor/blamejs/lib/storage.js +4 -2
- package/lib/vendor/blamejs/lib/structured-fields.js +275 -16
- package/lib/vendor/blamejs/lib/tenant-quota.js +2 -20
- package/lib/vendor/blamejs/lib/tsa.js +11 -15
- package/lib/vendor/blamejs/lib/validate-opts.js +154 -8
- package/lib/vendor/blamejs/lib/vault/seal-pem-file.js +30 -48
- package/lib/vendor/blamejs/lib/vault-aad.js +3 -2
- package/lib/vendor/blamejs/lib/vc.js +2 -7
- package/lib/vendor/blamejs/lib/vex.js +35 -13
- package/lib/vendor/blamejs/lib/web-push-vapid.js +23 -20
- package/lib/vendor/blamejs/lib/webhook-dispatcher.js +706 -0
- package/lib/vendor/blamejs/lib/webhook.js +43 -18
- package/lib/vendor/blamejs/lib/websocket-channels.js +9 -7
- package/lib/vendor/blamejs/lib/websocket.js +7 -3
- package/lib/vendor/blamejs/lib/worm.js +2 -3
- package/lib/vendor/blamejs/lib/ws-client.js +8 -10
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.13.json +81 -0
- package/lib/vendor/blamejs/scripts/check-changelog-extract.js +1 -1
- package/lib/vendor/blamejs/test/00-primitives.js +136 -7
- package/lib/vendor/blamejs/test/10-state.js +9 -3
- package/lib/vendor/blamejs/test/30-chain.js +40 -0
- package/lib/vendor/blamejs/test/helpers/check.js +18 -0
- package/lib/vendor/blamejs/test/helpers/db.js +4 -0
- package/lib/vendor/blamejs/test/helpers/index.js +1 -0
- package/lib/vendor/blamejs/test/integration/audit-chain-external-db.test.js +2 -1
- package/lib/vendor/blamejs/test/integration/audit-stack-postgres.test.js +2 -1
- package/lib/vendor/blamejs/test/integration/data-layer-mysql.test.js +8 -1
- package/lib/vendor/blamejs/test/integration/data-layer-pg.test.js +1 -1
- package/lib/vendor/blamejs/test/integration/framework-schema-mysql.test.js +2 -1
- package/lib/vendor/blamejs/test/integration/webhook-dispatcher-pg.test.js +269 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-fd-read.test.js +149 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-framework-namespaces.test.js +51 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-sign-anchor.test.js +88 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-use-store.test.js +55 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-object-store-adapter.test.js +2 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/bot-guard.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/bounded-map.test.js +129 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/chain-writer-multichain.test.js +107 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-api-key.test.js +12 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +2788 -3214
- package/lib/vendor/blamejs/test/layer-0-primitives/codepoint-class.test.js +78 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-base64url.test.js +63 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-hash-stream.test.js +2 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/csp-builder.test.js +50 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csp-nonce.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/daemon.test.js +2 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/defineguard-resolve-opts.test.js +76 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/flag.test.js +9 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/gate-contract-content-gate.test.js +290 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-csv.test.js +84 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-email.test.js +78 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-filename.test.js +23 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-gate-disposition-coverage.test.js +93 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-html.test.js +60 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-image.test.js +184 -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-json.test.js +54 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-managesieve-command.test.js +10 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-markdown.test.js +45 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-pdf.test.js +101 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-pop3-command.test.js +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-svg.test.js +76 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-text.test.js +365 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-xml.test.js +35 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-yaml.test.js +36 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/i18n.test.js +20 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/inbox.test.js +15 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ip-utils.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-arf.test.js +10 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +39 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim.test.js +36 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-greylist.test.js +17 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-tls.test.js +39 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-store.test.js +17 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mime-parse.test.js +106 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/money.test.js +17 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mtls-ca-revocation.test.js +98 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/nonce-store-release.test.js +77 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/observability.test.js +48 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-memory-getorinsert.test.js +100 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-helpers.test.js +88 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-async-loops.test.js +278 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-buffer-linear-scans.test.js +37 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-jsonpath.test.js +14 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +19 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/self-update.test.js +2 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/session-valid-from.test.js +73 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +100 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/structured-fields.test.js +80 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/validate-opts-shape.test.js +235 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook-dispatcher.test.js +374 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook.test.js +75 -8
- package/lib/vendor/blamejs/test/layer-5-integration/guard-host-integration.test.js +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Durable signed-webhook delivery store — the middle the framework was
|
|
4
|
+
* missing between b.webhook.signer (one inline POST, lost on exhaustion) and
|
|
5
|
+
* b.outbox (durable at-least-once, but terminates at an in-process publisher
|
|
6
|
+
* and never speaks HTTP). b.webhook.dispatcher composes the pieces already in
|
|
7
|
+
* the framework: it registers endpoints, fans one business event out to every
|
|
8
|
+
* subscribed endpoint as its own durable delivery row, signs each via
|
|
9
|
+
* b.webhook.signer, attempts HTTP delivery, backs off across attempts on the
|
|
10
|
+
* b.outbox retry curve, dead-letters after N attempts, and exposes a
|
|
11
|
+
* list/retry/replay surface an operator console can drive.
|
|
12
|
+
*
|
|
13
|
+
* Storage is the operator's b.externalDb (the same way b.outbox takes it).
|
|
14
|
+
* Per-endpoint secrets are sealed at rest with b.vault.seal — never stored
|
|
15
|
+
* plaintext. Destination URLs are validated through b.safeUrl at registration
|
|
16
|
+
* AND re-validated at every delivery attempt (DNS-rebinding / SSRF defense).
|
|
17
|
+
*
|
|
18
|
+
* Exposed as b.webhook.dispatcher; lives in its own module because a durable
|
|
19
|
+
* delivery store is a distinct domain from the stateless sign/verify surface
|
|
20
|
+
* in lib/webhook.js.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
var C = require("./constants");
|
|
24
|
+
var sql = require("./sql");
|
|
25
|
+
var safeSql = require("./safe-sql");
|
|
26
|
+
var safeUrl = require("./safe-url");
|
|
27
|
+
var safeJson = require("./safe-json");
|
|
28
|
+
var bCrypto = require("./crypto");
|
|
29
|
+
var httpClient = require("./http-client");
|
|
30
|
+
var frameworkSchema = require("./framework-schema");
|
|
31
|
+
var validateOpts = require("./validate-opts");
|
|
32
|
+
var lazyRequire = require("./lazy-require");
|
|
33
|
+
var { WebhookDispatcherError } = require("./framework-error");
|
|
34
|
+
|
|
35
|
+
// lib/webhook re-exports this module as b.webhook.dispatcher, so requiring it
|
|
36
|
+
// eagerly here is a cycle — and webhook.js reassigns module.exports at the
|
|
37
|
+
// bottom, which would hand us a stale empty object. Defer to call time.
|
|
38
|
+
var webhookSign = lazyRequire(function () { return require("./webhook"); });
|
|
39
|
+
var vault = lazyRequire(function () { return require("./vault"); });
|
|
40
|
+
// ssrfGuard.checkUrl resolves the host and refuses private / loopback /
|
|
41
|
+
// link-local / metadata destinations — safeUrl.parse only refuses by protocol
|
|
42
|
+
// + userinfo, so it alone does NOT stop SSRF to an internal IP. Composed at
|
|
43
|
+
// registration AND every delivery attempt (DNS-rebinding defense).
|
|
44
|
+
var ssrfGuard = lazyRequire(function () { return require("./ssrf-guard"); });
|
|
45
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
46
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
47
|
+
|
|
48
|
+
var _err = WebhookDispatcherError.factory;
|
|
49
|
+
|
|
50
|
+
// ---- defaults ----
|
|
51
|
+
var DEFAULT_MAX_ATTEMPTS = 8;
|
|
52
|
+
var DEFAULT_BACKOFF_INITIAL = C.TIME.seconds(5);
|
|
53
|
+
var DEFAULT_BACKOFF_MAX = C.TIME.minutes(60);
|
|
54
|
+
var DEFAULT_BACKOFF_FACTOR = 2;
|
|
55
|
+
var DEFAULT_CLAIM_RECLAIM_MS = C.TIME.minutes(5);
|
|
56
|
+
var DEFAULT_BATCH_SIZE = 100;
|
|
57
|
+
var SIGNER_ALGO = "hmac-sha3-512"; // framework symmetric webhook MAC
|
|
58
|
+
var DELIVERY_ID_BYTES = C.BYTES.bytes(16);
|
|
59
|
+
var WILDCARD_EVENT = "*";
|
|
60
|
+
|
|
61
|
+
// HTTP success band — a 2xx is delivered; everything else (incl. 3xx/4xx/5xx)
|
|
62
|
+
// is a delivery failure that backs off and eventually dead-letters.
|
|
63
|
+
var HTTP_OK_MIN = 200;
|
|
64
|
+
var HTTP_OK_MAX = 300;
|
|
65
|
+
|
|
66
|
+
function _validateTableName(name, label) {
|
|
67
|
+
validateOpts.requireNonEmptyString(name, label, WebhookDispatcherError, "webhook-dispatcher/bad-opts");
|
|
68
|
+
// safeSql.quoteIdentifier refuses an injection-bearing name at construction.
|
|
69
|
+
safeSql.quoteIdentifier(name);
|
|
70
|
+
return name;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _sqlDialect(externalDb) {
|
|
74
|
+
var d = externalDb && externalDb.dialect;
|
|
75
|
+
if (d === "postgres" || d === "postgresql") return "postgres";
|
|
76
|
+
if (d === "mysql") return "mysql";
|
|
77
|
+
return "sqlite";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Coerce an integer column read back from the backend to a JS number. Drivers
|
|
81
|
+
// differ: node:sqlite returns INTEGER as a number, but a text protocol (and a
|
|
82
|
+
// node-postgres BIGINT/int8) returns it as a STRING — `"1" + 1` would
|
|
83
|
+
// string-concat to "11", corrupting the attempt counter. Normalize on read so
|
|
84
|
+
// the dispatcher's arithmetic is driver-agnostic.
|
|
85
|
+
function _intOf(v) {
|
|
86
|
+
if (typeof v === "number") return v;
|
|
87
|
+
if (v === null || v === undefined || v === "") return 0;
|
|
88
|
+
var n = Number(v);
|
|
89
|
+
return isFinite(n) ? n : 0;
|
|
90
|
+
}
|
|
91
|
+
function _intOrNull(v) {
|
|
92
|
+
if (v === null || v === undefined || v === "") return null;
|
|
93
|
+
var n = Number(v);
|
|
94
|
+
return isFinite(n) ? n : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function _coercePayloadString(payload) {
|
|
98
|
+
if (typeof payload === "string") return payload;
|
|
99
|
+
if (Buffer.isBuffer(payload)) return payload.toString("utf8");
|
|
100
|
+
// Objects serialize via safeJson (proto-safe, stable) so the signed body is
|
|
101
|
+
// deterministic and the receiver verifies the same bytes we signed.
|
|
102
|
+
return safeJson.stringify(payload);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @primitive b.webhook.dispatcher
|
|
107
|
+
* @signature b.webhook.dispatcher(opts)
|
|
108
|
+
* @since 0.15.13
|
|
109
|
+
* @status stable
|
|
110
|
+
* @related b.webhook.signer, b.webhook.verify, b.outbox.create, b.nonceStore.create
|
|
111
|
+
*
|
|
112
|
+
* Build a durable signed-webhook delivery store backed by the operator's
|
|
113
|
+
* `b.externalDb`. The returned object exposes:
|
|
114
|
+
*
|
|
115
|
+
* - `declareSchema(xdb?)` — idempotent `CREATE TABLE` for the endpoints +
|
|
116
|
+
* deliveries tables (run once at boot, like `b.outbox.declareSchema`).
|
|
117
|
+
* - `registerEndpoint({ endpointId, url, eventTypes, secret })` — persist a
|
|
118
|
+
* subscriber. The URL is validated through `b.safeUrl` (SSRF destinations
|
|
119
|
+
* refused); the secret is sealed at rest with `b.vault.seal`. `eventTypes`
|
|
120
|
+
* is an array of event names, or `["*"]` to receive every event.
|
|
121
|
+
* - `removeEndpoint(endpointId)` / `listEndpoints()`.
|
|
122
|
+
* - `dispatch(eventType, payload)` — fan the event out to every subscribed
|
|
123
|
+
* endpoint as its own durable delivery row, sign each via
|
|
124
|
+
* `b.webhook.signer`, and attempt delivery once inline. Returns
|
|
125
|
+
* `{ delivered, failed, deliveries: [...] }`.
|
|
126
|
+
* - `processRetries()` — poll/alarm entry point: claim every delivery whose
|
|
127
|
+
* `next_attempt_at` is due, re-attempt, back off on the `b.outbox` curve,
|
|
128
|
+
* and dead-letter after `maxAttempts`. Reaps deliveries stranded
|
|
129
|
+
* in-flight by a crashed worker. Returns `{ attempted, delivered, dead }`.
|
|
130
|
+
* - `deliveries.list({ endpointId?, status?, limit? })` / `deliveries.get(id)`
|
|
131
|
+
* / `deliveries.retry(id)` — operator-console surface.
|
|
132
|
+
* - `dlq.list({ limit? })` / `dlq.replay(id)` — dead-letter inspect + replay.
|
|
133
|
+
*
|
|
134
|
+
* Each delivery carries a stable `X-Webhook-Delivery-Id` (so a re-delivery is
|
|
135
|
+
* deduped by the receiver, not rejected as a replay) plus the signer's fresh
|
|
136
|
+
* per-attempt nonce in the signature (replay defense at the signature layer).
|
|
137
|
+
*
|
|
138
|
+
* @opts
|
|
139
|
+
* externalDb: b.externalDb, // required — storage backend
|
|
140
|
+
* endpointsTable: string, // default frameworkSchema.tableName("webhook_endpoints")
|
|
141
|
+
* deliveriesTable: string, // default frameworkSchema.tableName("webhook_deliveries")
|
|
142
|
+
* maxAttempts: number, // default 8 → then dead-letter
|
|
143
|
+
* retryBackoff: { initialMs, maxMs, factor }, // default 5s / 60min / 2x
|
|
144
|
+
* claimReclaimMs: number, // default 5 min stale-in-flight lease
|
|
145
|
+
* batchSize: number, // default 100 deliveries per processRetries
|
|
146
|
+
* signatureHeader: string, // forwarded to b.webhook.signer
|
|
147
|
+
* allowedProtocols: object, // b.safeUrl protocol set (default ALLOW_HTTP_TLS)
|
|
148
|
+
* allowInternalDestinations: boolean, // default false — refuse SSRF (private/loopback/metadata)
|
|
149
|
+
* httpRequest: function, // (url, body, headers) → { status } — inject for tests
|
|
150
|
+
* now: function, // clock injection → ms epoch
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* var wd = b.webhook.dispatcher({ externalDb: b.externalDb });
|
|
154
|
+
* await wd.declareSchema();
|
|
155
|
+
* await wd.registerEndpoint({
|
|
156
|
+
* endpointId: "acct_42",
|
|
157
|
+
* url: "https://partner.example/hooks",
|
|
158
|
+
* eventTypes: ["invoice.paid", "invoice.refunded"],
|
|
159
|
+
* secret: "whsec_partner_secret",
|
|
160
|
+
* });
|
|
161
|
+
* await wd.dispatch("invoice.paid", { id: "inv_1", amount: 4200 });
|
|
162
|
+
* // later, from a cron / alarm:
|
|
163
|
+
* await wd.processRetries();
|
|
164
|
+
*/
|
|
165
|
+
function dispatcher(opts) {
|
|
166
|
+
validateOpts.shape(opts, {
|
|
167
|
+
externalDb: { methods: ["query", "transaction"] },
|
|
168
|
+
endpointsTable: "optional-string",
|
|
169
|
+
deliveriesTable: "optional-string",
|
|
170
|
+
maxAttempts: "optional-positive-finite",
|
|
171
|
+
batchSize: "optional-positive-finite",
|
|
172
|
+
claimReclaimMs: "optional-positive-finite",
|
|
173
|
+
retryBackoff: { optional: true, shape: {
|
|
174
|
+
initialMs: "optional-positive-finite",
|
|
175
|
+
maxMs: "optional-positive-finite",
|
|
176
|
+
factor: "optional-positive-finite",
|
|
177
|
+
} },
|
|
178
|
+
signatureHeader: "optional-string",
|
|
179
|
+
allowedProtocols: "optional-plain-object",
|
|
180
|
+
allowInternalDestinations: "optional-boolean",
|
|
181
|
+
httpRequest: "optional-function",
|
|
182
|
+
now: "optional-function",
|
|
183
|
+
}, "webhook.dispatcher", WebhookDispatcherError, "webhook-dispatcher/bad-opts");
|
|
184
|
+
var externalDb = opts.externalDb;
|
|
185
|
+
var endpointsTable = _validateTableName(
|
|
186
|
+
opts.endpointsTable || frameworkSchema.tableName("webhook_endpoints"),
|
|
187
|
+
"dispatcher: endpointsTable");
|
|
188
|
+
var deliveriesTable = _validateTableName(
|
|
189
|
+
opts.deliveriesTable || frameworkSchema.tableName("webhook_deliveries"),
|
|
190
|
+
"dispatcher: deliveriesTable");
|
|
191
|
+
|
|
192
|
+
var maxAttempts = opts.maxAttempts || DEFAULT_MAX_ATTEMPTS;
|
|
193
|
+
var batchSize = opts.batchSize || DEFAULT_BATCH_SIZE;
|
|
194
|
+
var claimReclaimMs = opts.claimReclaimMs || DEFAULT_CLAIM_RECLAIM_MS;
|
|
195
|
+
|
|
196
|
+
var backoff = opts.retryBackoff || {};
|
|
197
|
+
var backoffInitial = backoff.initialMs || DEFAULT_BACKOFF_INITIAL;
|
|
198
|
+
var backoffMax = backoff.maxMs || DEFAULT_BACKOFF_MAX;
|
|
199
|
+
var backoffFactor = backoff.factor || DEFAULT_BACKOFF_FACTOR;
|
|
200
|
+
|
|
201
|
+
var allowedProtocols = opts.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
|
|
202
|
+
var signatureHeader = opts.signatureHeader || null;
|
|
203
|
+
// Default-secure: refuse delivery to internal IP ranges. An operator whose
|
|
204
|
+
// subscribers genuinely live on a private network opts in explicitly.
|
|
205
|
+
var allowInternal = opts.allowInternalDestinations === true;
|
|
206
|
+
var clock = typeof opts.now === "function" ? opts.now : function () { return Date.now(); };
|
|
207
|
+
|
|
208
|
+
// Refuse an SSRF / malformed destination: protocol + userinfo via safeUrl,
|
|
209
|
+
// IP classification (private / loopback / metadata) via ssrfGuard. Async
|
|
210
|
+
// because the IP check resolves the host. Throws a dispatcher-coded error.
|
|
211
|
+
async function _assertSafeDestination(url, where) {
|
|
212
|
+
safeUrl.parse(url, { allowedProtocols: allowedProtocols, errorClass: WebhookDispatcherError });
|
|
213
|
+
try {
|
|
214
|
+
await ssrfGuard().checkUrl(url, { allowInternal: allowInternal });
|
|
215
|
+
} catch (e) {
|
|
216
|
+
if (e && e.isSsrfError) {
|
|
217
|
+
throw _err("webhook-dispatcher/ssrf-refused", where + ": " + e.message);
|
|
218
|
+
}
|
|
219
|
+
throw e;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Default transport: a real signed POST through the framework http client.
|
|
224
|
+
// Injectable so a test (or a non-HTTP transport) substitutes its own.
|
|
225
|
+
var httpRequest = opts.httpRequest || function (url, body, headers) {
|
|
226
|
+
return httpClient.request({
|
|
227
|
+
method: "POST",
|
|
228
|
+
url: url,
|
|
229
|
+
headers: headers,
|
|
230
|
+
body: body,
|
|
231
|
+
allowedProtocols: allowedProtocols,
|
|
232
|
+
errorClass: WebhookDispatcherError,
|
|
233
|
+
}).then(function (res) {
|
|
234
|
+
return { status: (res && (res.statusCode || res.status)) || 0 };
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Per-secret signer cache — one b.webhook.signer bound to each endpoint
|
|
239
|
+
// secret, keyed by the (unsealed) secret so re-deliveries reuse it.
|
|
240
|
+
var _signerCache = Object.create(null);
|
|
241
|
+
function _signerFor(secret) {
|
|
242
|
+
if (_signerCache[secret]) return _signerCache[secret];
|
|
243
|
+
var s = webhookSign().signer({
|
|
244
|
+
algo: SIGNER_ALGO,
|
|
245
|
+
keys: { v1: secret },
|
|
246
|
+
signatureHeader: signatureHeader || undefined,
|
|
247
|
+
});
|
|
248
|
+
_signerCache[secret] = s;
|
|
249
|
+
return s;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function _backoffMs(attempts) {
|
|
253
|
+
var ms = backoffInitial * Math.pow(backoffFactor, Math.max(0, attempts - 1));
|
|
254
|
+
if (ms > backoffMax) ms = backoffMax;
|
|
255
|
+
return ms;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function _nowDate() { return new Date(clock()); }
|
|
259
|
+
|
|
260
|
+
// ---- schema ----
|
|
261
|
+
async function declareSchema(xdb) {
|
|
262
|
+
var target = xdb || externalDb;
|
|
263
|
+
var dialect = _sqlDialect(target);
|
|
264
|
+
var tsType = dialect === "postgres" ? "TIMESTAMPTZ" : "TIMESTAMP";
|
|
265
|
+
var endpointsDdl = sql.toExternalSql(sql.createTable(endpointsTable, [
|
|
266
|
+
{ name: "id", serial: true },
|
|
267
|
+
{ name: "endpoint_id", type: "VARCHAR(255)", notNull: true },
|
|
268
|
+
{ name: "url", type: "TEXT", notNull: true },
|
|
269
|
+
{ name: "event_types", type: "TEXT", notNull: true },
|
|
270
|
+
{ name: "secret_sealed", type: "TEXT", notNull: true },
|
|
271
|
+
{ name: "disabled", type: "INTEGER", notNull: true, default: 0 },
|
|
272
|
+
{ name: "created_at", type: tsType, notNull: true },
|
|
273
|
+
], { dialect: dialect }), dialect);
|
|
274
|
+
var endpointsIdx = sql.toExternalSql(sql.createIndex(endpointsTable + "_eid_idx",
|
|
275
|
+
endpointsTable, ["endpoint_id"], { dialect: dialect }), dialect);
|
|
276
|
+
|
|
277
|
+
var deliveriesDdl = sql.toExternalSql(sql.createTable(deliveriesTable, [
|
|
278
|
+
{ name: "id", serial: true },
|
|
279
|
+
{ name: "delivery_id", type: "VARCHAR(64)", notNull: true },
|
|
280
|
+
{ name: "endpoint_id", type: "VARCHAR(255)", notNull: true },
|
|
281
|
+
{ name: "url", type: "TEXT", notNull: true },
|
|
282
|
+
{ name: "event_type", type: "VARCHAR(255)", notNull: true },
|
|
283
|
+
{ name: "payload", type: "TEXT", notNull: true },
|
|
284
|
+
{ name: "idempotency_id", type: "VARCHAR(64)", notNull: true },
|
|
285
|
+
{ name: "status", type: "VARCHAR(16)", notNull: true, default: "pending" },
|
|
286
|
+
{ name: "attempts", type: "INTEGER", notNull: true, default: 0 },
|
|
287
|
+
{ name: "next_attempt_at", type: tsType, notNull: true },
|
|
288
|
+
{ name: "claimed_at", type: tsType },
|
|
289
|
+
{ name: "delivered_at", type: tsType },
|
|
290
|
+
{ name: "response_status", type: "INTEGER" },
|
|
291
|
+
{ name: "last_error", type: "TEXT" },
|
|
292
|
+
{ name: "created_at", type: tsType, notNull: true },
|
|
293
|
+
], { dialect: dialect }), dialect);
|
|
294
|
+
// Index on the due-pending pool the retry poller scans. Postgres/SQLite
|
|
295
|
+
// get a PARTIAL index (status = 'pending'); MySQL has no partial indexes
|
|
296
|
+
// (sql.createIndex refuses `where` for the mysql dialect), so it gets a
|
|
297
|
+
// plain index on next_attempt_at — the processRetries query still filters
|
|
298
|
+
// status = 'pending', so correctness is unchanged, only the index is a
|
|
299
|
+
// touch less selective.
|
|
300
|
+
var deliveriesIdxOpts = { dialect: dialect };
|
|
301
|
+
if (dialect !== "mysql") deliveriesIdxOpts.where = "status = 'pending'";
|
|
302
|
+
var deliveriesIdx = sql.toExternalSql(sql.createIndex(deliveriesTable + "_pending_idx",
|
|
303
|
+
deliveriesTable, ["next_attempt_at"], deliveriesIdxOpts), dialect);
|
|
304
|
+
|
|
305
|
+
await target.query(endpointsDdl.sql, endpointsDdl.params);
|
|
306
|
+
await target.query(endpointsIdx.sql, endpointsIdx.params);
|
|
307
|
+
await target.query(deliveriesDdl.sql, deliveriesDdl.params);
|
|
308
|
+
await target.query(deliveriesIdx.sql, deliveriesIdx.params);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ---- endpoint registration ----
|
|
312
|
+
async function registerEndpoint(ep) {
|
|
313
|
+
validateOpts.shape(ep, {
|
|
314
|
+
endpointId: "required-string",
|
|
315
|
+
url: "required-string",
|
|
316
|
+
secret: "required-string",
|
|
317
|
+
eventTypes: "optional-string-array", // element validation; required-non-empty checked below
|
|
318
|
+
}, "dispatcher.registerEndpoint", WebhookDispatcherError, "webhook-dispatcher/bad-opts");
|
|
319
|
+
if (!Array.isArray(ep.eventTypes) || ep.eventTypes.length === 0) {
|
|
320
|
+
throw _err("webhook-dispatcher/bad-opts",
|
|
321
|
+
"registerEndpoint: eventTypes must be a non-empty array of event names (or [\"*\"])");
|
|
322
|
+
}
|
|
323
|
+
// Refuse an SSRF / malformed destination at registration time — fail fast,
|
|
324
|
+
// before a single delivery row is written.
|
|
325
|
+
await _assertSafeDestination(ep.url, "registerEndpoint");
|
|
326
|
+
|
|
327
|
+
var dialect = _sqlDialect(externalDb);
|
|
328
|
+
var sealedSecret = vault().seal(ep.secret);
|
|
329
|
+
var stmt = sql.insert(endpointsTable, { dialect: dialect })
|
|
330
|
+
.values({
|
|
331
|
+
endpoint_id: ep.endpointId,
|
|
332
|
+
url: ep.url,
|
|
333
|
+
event_types: safeJson.stringify(ep.eventTypes),
|
|
334
|
+
secret_sealed: sealedSecret,
|
|
335
|
+
disabled: 0,
|
|
336
|
+
created_at: _nowDate(),
|
|
337
|
+
})
|
|
338
|
+
.toExternalSql(dialect);
|
|
339
|
+
await externalDb.query(stmt.sql, stmt.params);
|
|
340
|
+
_emitAudit("webhook.dispatcher.endpoint.register", "success",
|
|
341
|
+
{ endpointId: ep.endpointId, eventTypes: ep.eventTypes });
|
|
342
|
+
return { endpointId: ep.endpointId };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function removeEndpoint(endpointId) {
|
|
346
|
+
validateOpts.requireNonEmptyString(endpointId, "removeEndpoint: endpointId",
|
|
347
|
+
WebhookDispatcherError, "webhook-dispatcher/bad-opts");
|
|
348
|
+
var dialect = _sqlDialect(externalDb);
|
|
349
|
+
var stmt = sql.delete(endpointsTable, { dialect: dialect })
|
|
350
|
+
.where("endpoint_id", endpointId)
|
|
351
|
+
.toExternalSql(dialect);
|
|
352
|
+
await externalDb.query(stmt.sql, stmt.params);
|
|
353
|
+
return { endpointId: endpointId, removed: true };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function listEndpoints() {
|
|
357
|
+
var dialect = _sqlDialect(externalDb);
|
|
358
|
+
var stmt = sql.select(endpointsTable, { dialect: dialect })
|
|
359
|
+
.columns(["endpoint_id", "url", "event_types", "disabled", "created_at"])
|
|
360
|
+
.toExternalSql(dialect);
|
|
361
|
+
var res = await externalDb.query(stmt.sql, stmt.params);
|
|
362
|
+
return ((res && res.rows) || []).map(function (r) {
|
|
363
|
+
return {
|
|
364
|
+
endpointId: r.endpoint_id,
|
|
365
|
+
url: r.url,
|
|
366
|
+
eventTypes: safeJson.parse(r.event_types),
|
|
367
|
+
disabled: _isTruthy(r.disabled),
|
|
368
|
+
createdAt: r.created_at,
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Load every enabled endpoint subscribed to eventType. event_types is a JSON
|
|
374
|
+
// array stored as TEXT (portable across the three dialects); membership is
|
|
375
|
+
// resolved in JS because a portable JSON-contains predicate doesn't exist
|
|
376
|
+
// across sqlite / postgres / mysql. The endpoint count for a single tenant's
|
|
377
|
+
// webhook fan-out is modest, so the full scan is acceptable.
|
|
378
|
+
async function _subscribedEndpoints(eventType) {
|
|
379
|
+
var all = await listEndpoints();
|
|
380
|
+
return all.filter(function (e) {
|
|
381
|
+
if (e.disabled) return false;
|
|
382
|
+
var types = e.eventTypes || [];
|
|
383
|
+
return types.indexOf(eventType) !== -1 || types.indexOf(WILDCARD_EVENT) !== -1;
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function _loadEndpointRow(endpointId) {
|
|
388
|
+
var dialect = _sqlDialect(externalDb);
|
|
389
|
+
var stmt = sql.select(endpointsTable, { dialect: dialect })
|
|
390
|
+
.columns(["endpoint_id", "url", "secret_sealed", "disabled"])
|
|
391
|
+
.where("endpoint_id", endpointId)
|
|
392
|
+
.limit(1)
|
|
393
|
+
.toExternalSql(dialect);
|
|
394
|
+
var res = await externalDb.query(stmt.sql, stmt.params);
|
|
395
|
+
return (res && res.rows && res.rows[0]) || null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ---- dispatch / delivery ----
|
|
399
|
+
async function dispatch(eventType, payload) {
|
|
400
|
+
validateOpts.requireNonEmptyString(eventType, "dispatch: eventType",
|
|
401
|
+
WebhookDispatcherError, "webhook-dispatcher/bad-opts");
|
|
402
|
+
if (payload === undefined || payload === null) {
|
|
403
|
+
throw _err("webhook-dispatcher/bad-opts", "dispatch: payload required");
|
|
404
|
+
}
|
|
405
|
+
var bodyStr = _coercePayloadString(payload);
|
|
406
|
+
var endpoints = await _subscribedEndpoints(eventType);
|
|
407
|
+
var dialect = _sqlDialect(externalDb);
|
|
408
|
+
var results = [];
|
|
409
|
+
for (var i = 0; i < endpoints.length; i += 1) {
|
|
410
|
+
var ep = endpoints[i];
|
|
411
|
+
var deliveryId = bCrypto.generateToken(DELIVERY_ID_BYTES);
|
|
412
|
+
var idempotencyId = bCrypto.generateToken(DELIVERY_ID_BYTES);
|
|
413
|
+
// Insert already CLAIMED for the inline attempt (status 'in-flight' +
|
|
414
|
+
// claimed_at = now). processRetries() only claims status='pending' rows,
|
|
415
|
+
// so a poller running during the slow inline POST cannot grab this row
|
|
416
|
+
// and double-deliver it. The inline _attemptDelivery transitions it to
|
|
417
|
+
// delivered / pending(+backoff) / dead; if this process dies mid-POST,
|
|
418
|
+
// _reapStaleInflight reclaims it after claimReclaimMs.
|
|
419
|
+
var insertStmt = sql.insert(deliveriesTable, { dialect: dialect })
|
|
420
|
+
.values({
|
|
421
|
+
delivery_id: deliveryId,
|
|
422
|
+
endpoint_id: ep.endpointId,
|
|
423
|
+
url: ep.url,
|
|
424
|
+
event_type: eventType,
|
|
425
|
+
payload: bodyStr,
|
|
426
|
+
idempotency_id: idempotencyId,
|
|
427
|
+
status: "in-flight",
|
|
428
|
+
attempts: 0,
|
|
429
|
+
next_attempt_at: _nowDate(),
|
|
430
|
+
claimed_at: _nowDate(),
|
|
431
|
+
created_at: _nowDate(),
|
|
432
|
+
})
|
|
433
|
+
.toExternalSql(dialect);
|
|
434
|
+
await externalDb.query(insertStmt.sql, insertStmt.params);
|
|
435
|
+
// Best-effort first attempt inline; on failure it is scheduled for retry.
|
|
436
|
+
var r = await _attemptDelivery(deliveryId);
|
|
437
|
+
results.push(r);
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
delivered: results.filter(function (r) { return r.ok; }).length,
|
|
441
|
+
failed: results.filter(function (r) { return !r.ok; }).length,
|
|
442
|
+
deliveries: results,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function _loadDelivery(deliveryId) {
|
|
447
|
+
var dialect = _sqlDialect(externalDb);
|
|
448
|
+
var stmt = sql.select(deliveriesTable, { dialect: dialect })
|
|
449
|
+
.columns(["delivery_id", "endpoint_id", "url", "event_type", "payload",
|
|
450
|
+
"idempotency_id", "status", "attempts"])
|
|
451
|
+
.where("delivery_id", deliveryId)
|
|
452
|
+
.limit(1)
|
|
453
|
+
.toExternalSql(dialect);
|
|
454
|
+
var res = await externalDb.query(stmt.sql, stmt.params);
|
|
455
|
+
return (res && res.rows && res.rows[0]) || null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Attempt one delivery by id: sign with the endpoint's sealed secret, POST,
|
|
459
|
+
// and transition the row on the outcome (delivered / retry / dead).
|
|
460
|
+
async function _attemptDelivery(deliveryId) {
|
|
461
|
+
var row = await _loadDelivery(deliveryId);
|
|
462
|
+
if (!row) return { deliveryId: deliveryId, ok: false, status: 0, error: "delivery row not found" };
|
|
463
|
+
var epRow = await _loadEndpointRow(row.endpoint_id);
|
|
464
|
+
if (!epRow) {
|
|
465
|
+
await _markDead(deliveryId, _intOf(row.attempts) + 1, "endpoint no longer registered");
|
|
466
|
+
return { deliveryId: deliveryId, ok: false, status: 0, dead: true, error: "endpoint missing" };
|
|
467
|
+
}
|
|
468
|
+
var attemptNo = _intOf(row.attempts) + 1;
|
|
469
|
+
// Re-validate the destination at delivery time — a host that resolved
|
|
470
|
+
// public at registration but rebound to an internal IP is refused here.
|
|
471
|
+
// An SSRF refusal / malformed URL is PERMANENT (won't fix on retry) →
|
|
472
|
+
// dead-letter immediately. Permanence is decided HERE by the failure
|
|
473
|
+
// CLASS, not by err.permanent: the transport (httpClient) errors below
|
|
474
|
+
// are thrown with WebhookDispatcherError, which is alwaysPermanent, so
|
|
475
|
+
// reading err.permanent would mis-mark a timeout / network error / 5xx
|
|
476
|
+
// as permanent and dead-letter it on the first attempt.
|
|
477
|
+
try {
|
|
478
|
+
await _assertSafeDestination(row.url, "deliver");
|
|
479
|
+
} catch (err) {
|
|
480
|
+
return await _onFailure(deliveryId, attemptNo,
|
|
481
|
+
(err && err.message) || String(err), true);
|
|
482
|
+
}
|
|
483
|
+
// Sign + POST. Transport errors (network, TLS, timeout, DNS) and any
|
|
484
|
+
// non-2xx HTTP status are TRANSIENT — back off and retry (capped at
|
|
485
|
+
// maxAttempts, after which _onFailure dead-letters).
|
|
486
|
+
try {
|
|
487
|
+
var secret = vault().unseal(epRow.secret_sealed);
|
|
488
|
+
var signer = _signerFor(secret);
|
|
489
|
+
var signed = signer.sign(row.payload);
|
|
490
|
+
var headers = Object.assign({}, signed.headers, {
|
|
491
|
+
"Content-Type": "application/json",
|
|
492
|
+
"X-Webhook-Delivery-Id": row.delivery_id,
|
|
493
|
+
"X-Webhook-Event-Type": row.event_type,
|
|
494
|
+
"X-Webhook-Idempotency-Id": row.idempotency_id,
|
|
495
|
+
});
|
|
496
|
+
var result = await httpRequest(row.url, row.payload, headers);
|
|
497
|
+
var status = (result && (result.status || result.statusCode)) || 0;
|
|
498
|
+
if (status >= HTTP_OK_MIN && status < HTTP_OK_MAX) {
|
|
499
|
+
await _markDelivered(deliveryId, attemptNo, status);
|
|
500
|
+
return { deliveryId: deliveryId, ok: true, status: status };
|
|
501
|
+
}
|
|
502
|
+
return await _onFailure(deliveryId, attemptNo, "delivery returned HTTP " + status, false);
|
|
503
|
+
} catch (err) {
|
|
504
|
+
return await _onFailure(deliveryId, attemptNo,
|
|
505
|
+
(err && err.message) || String(err), false);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function _markDelivered(deliveryId, attemptNo, status) {
|
|
510
|
+
var dialect = _sqlDialect(externalDb);
|
|
511
|
+
var stmt = sql.update(deliveriesTable, { dialect: dialect })
|
|
512
|
+
.set({
|
|
513
|
+
status: "delivered",
|
|
514
|
+
attempts: attemptNo,
|
|
515
|
+
delivered_at: _nowDate(),
|
|
516
|
+
response_status: status,
|
|
517
|
+
claimed_at: null,
|
|
518
|
+
})
|
|
519
|
+
.where("delivery_id", deliveryId)
|
|
520
|
+
.toExternalSql(dialect);
|
|
521
|
+
await externalDb.query(stmt.sql, stmt.params);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// A failed attempt: dead-letter once attempts reach maxAttempts OR the error
|
|
525
|
+
// is permanent (won't fix on retry), else reschedule on the backoff curve.
|
|
526
|
+
async function _onFailure(deliveryId, attemptNo, errMsg, permanent) {
|
|
527
|
+
if (permanent || attemptNo >= maxAttempts) {
|
|
528
|
+
await _markDead(deliveryId, attemptNo, errMsg);
|
|
529
|
+
return { deliveryId: deliveryId, ok: false, status: 0, dead: true, error: errMsg };
|
|
530
|
+
}
|
|
531
|
+
var dialect = _sqlDialect(externalDb);
|
|
532
|
+
var nextAt = new Date(clock() + _backoffMs(attemptNo));
|
|
533
|
+
var stmt = sql.update(deliveriesTable, { dialect: dialect })
|
|
534
|
+
.set({
|
|
535
|
+
status: "pending",
|
|
536
|
+
attempts: attemptNo,
|
|
537
|
+
next_attempt_at: nextAt,
|
|
538
|
+
last_error: errMsg,
|
|
539
|
+
claimed_at: null,
|
|
540
|
+
})
|
|
541
|
+
.where("delivery_id", deliveryId)
|
|
542
|
+
.toExternalSql(dialect);
|
|
543
|
+
await externalDb.query(stmt.sql, stmt.params);
|
|
544
|
+
return { deliveryId: deliveryId, ok: false, status: 0, error: errMsg, nextAttemptAt: nextAt };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function _markDead(deliveryId, attemptNo, errMsg) {
|
|
548
|
+
var dialect = _sqlDialect(externalDb);
|
|
549
|
+
var stmt = sql.update(deliveriesTable, { dialect: dialect })
|
|
550
|
+
.set({ status: "dead", attempts: attemptNo, last_error: errMsg, claimed_at: null })
|
|
551
|
+
.where("delivery_id", deliveryId)
|
|
552
|
+
.toExternalSql(dialect);
|
|
553
|
+
await externalDb.query(stmt.sql, stmt.params);
|
|
554
|
+
_emitAudit("webhook.dispatcher.dead-letter", "failure",
|
|
555
|
+
{ deliveryId: deliveryId, attempts: attemptNo, error: errMsg });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ---- retry poller ----
|
|
559
|
+
// Reset deliveries stranded 'in-flight' by a crashed worker (claimed but
|
|
560
|
+
// never resolved) back to 'pending' once the lease expires — the
|
|
561
|
+
// at-least-once guarantee b.outbox's reaper proved is the framework's job.
|
|
562
|
+
async function _reapStaleInflight() {
|
|
563
|
+
var dialect = _sqlDialect(externalDb);
|
|
564
|
+
var cutoff = new Date(clock() - claimReclaimMs);
|
|
565
|
+
var stmt = sql.update(deliveriesTable, { dialect: dialect })
|
|
566
|
+
.set({ status: "pending", claimed_at: null })
|
|
567
|
+
.whereRaw("status = 'in-flight'", [], { allowLiterals: true })
|
|
568
|
+
.whereRaw("(claimed_at IS NULL OR claimed_at <= ?)", [cutoff])
|
|
569
|
+
.toExternalSql(dialect);
|
|
570
|
+
await externalDb.query(stmt.sql, stmt.params);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Claim due-pending deliveries by flipping them to 'in-flight' inside a
|
|
574
|
+
// transaction (mark-then-reselect; gated on status='pending' so two pollers
|
|
575
|
+
// can't both claim the same row), then attempt each claimed delivery.
|
|
576
|
+
async function processRetries() {
|
|
577
|
+
await _reapStaleInflight();
|
|
578
|
+
var dialect = _sqlDialect(externalDb);
|
|
579
|
+
var claimed = await externalDb.transaction(async function (xdb) {
|
|
580
|
+
var nowDate = _nowDate();
|
|
581
|
+
var sel = sql.select(deliveriesTable, { dialect: dialect })
|
|
582
|
+
.columns(["delivery_id"])
|
|
583
|
+
.whereRaw("status = 'pending'", [], { allowLiterals: true })
|
|
584
|
+
.whereRaw("next_attempt_at <= ?", [nowDate])
|
|
585
|
+
.orderBy("next_attempt_at")
|
|
586
|
+
.limit(batchSize)
|
|
587
|
+
.toExternalSql(dialect);
|
|
588
|
+
var rows = await xdb.query(sel.sql, sel.params);
|
|
589
|
+
var ids = ((rows && rows.rows) || []).map(function (r) { return r.delivery_id; });
|
|
590
|
+
if (ids.length === 0) return [];
|
|
591
|
+
var mark = sql.update(deliveriesTable, { dialect: dialect })
|
|
592
|
+
.set({ status: "in-flight", claimed_at: _nowDate() })
|
|
593
|
+
.whereRaw("status = 'pending'", [], { allowLiterals: true })
|
|
594
|
+
.whereInArray("delivery_id", ids)
|
|
595
|
+
.toExternalSql(dialect);
|
|
596
|
+
await xdb.query(mark.sql, mark.params);
|
|
597
|
+
var after = sql.select(deliveriesTable, { dialect: dialect })
|
|
598
|
+
.columns(["delivery_id"])
|
|
599
|
+
.whereRaw("status = 'in-flight'", [], { allowLiterals: true })
|
|
600
|
+
.whereInArray("delivery_id", ids)
|
|
601
|
+
.toExternalSql(dialect);
|
|
602
|
+
var afterRows = await xdb.query(after.sql, after.params);
|
|
603
|
+
return ((afterRows && afterRows.rows) || []).map(function (r) { return r.delivery_id; });
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
var attempted = 0, delivered = 0, dead = 0;
|
|
607
|
+
for (var i = 0; i < claimed.length; i += 1) {
|
|
608
|
+
var r = await _attemptDelivery(claimed[i]);
|
|
609
|
+
attempted += 1;
|
|
610
|
+
if (r.ok) delivered += 1;
|
|
611
|
+
if (r.dead) dead += 1;
|
|
612
|
+
}
|
|
613
|
+
return { attempted: attempted, delivered: delivered, dead: dead };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ---- operator-console surface ----
|
|
617
|
+
function _mapDelivery(r) {
|
|
618
|
+
return {
|
|
619
|
+
deliveryId: r.delivery_id,
|
|
620
|
+
endpointId: r.endpoint_id,
|
|
621
|
+
eventType: r.event_type,
|
|
622
|
+
status: r.status,
|
|
623
|
+
attempts: _intOf(r.attempts),
|
|
624
|
+
nextAttemptAt: r.next_attempt_at,
|
|
625
|
+
deliveredAt: r.delivered_at,
|
|
626
|
+
responseStatus: _intOrNull(r.response_status),
|
|
627
|
+
lastError: r.last_error,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
var DELIVERY_VIEW_COLS = ["delivery_id", "endpoint_id", "event_type", "status",
|
|
632
|
+
"attempts", "next_attempt_at", "delivered_at", "response_status", "last_error"];
|
|
633
|
+
|
|
634
|
+
async function _listDeliveries(filter) {
|
|
635
|
+
filter = filter || {};
|
|
636
|
+
var dialect = _sqlDialect(externalDb);
|
|
637
|
+
var builder = sql.select(deliveriesTable, { dialect: dialect }).columns(DELIVERY_VIEW_COLS);
|
|
638
|
+
if (filter.endpointId) builder.where("endpoint_id", filter.endpointId);
|
|
639
|
+
if (filter.status) builder.where("status", filter.status);
|
|
640
|
+
builder.orderBy("id").limit(filter.limit || DEFAULT_BATCH_SIZE);
|
|
641
|
+
var stmt = builder.toExternalSql(dialect);
|
|
642
|
+
var res = await externalDb.query(stmt.sql, stmt.params);
|
|
643
|
+
return ((res && res.rows) || []).map(_mapDelivery);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function _getDelivery(deliveryId) {
|
|
647
|
+
var dialect = _sqlDialect(externalDb);
|
|
648
|
+
var stmt = sql.select(deliveriesTable, { dialect: dialect })
|
|
649
|
+
.columns(DELIVERY_VIEW_COLS)
|
|
650
|
+
.where("delivery_id", deliveryId)
|
|
651
|
+
.limit(1)
|
|
652
|
+
.toExternalSql(dialect);
|
|
653
|
+
var res = await externalDb.query(stmt.sql, stmt.params);
|
|
654
|
+
var row = res && res.rows && res.rows[0];
|
|
655
|
+
return row ? _mapDelivery(row) : null;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Reset a delivery to the pending pool for an immediate re-attempt. Used by
|
|
659
|
+
// both deliveries.retry (any status) and dlq.replay (dead → pending).
|
|
660
|
+
async function _requeue(deliveryId) {
|
|
661
|
+
validateOpts.requireNonEmptyString(deliveryId, "retry: deliveryId",
|
|
662
|
+
WebhookDispatcherError, "webhook-dispatcher/bad-opts");
|
|
663
|
+
var dialect = _sqlDialect(externalDb);
|
|
664
|
+
var stmt = sql.update(deliveriesTable, { dialect: dialect })
|
|
665
|
+
.set({ status: "pending", attempts: 0, next_attempt_at: _nowDate(), claimed_at: null, last_error: null })
|
|
666
|
+
.where("delivery_id", deliveryId)
|
|
667
|
+
.toExternalSql(dialect);
|
|
668
|
+
await externalDb.query(stmt.sql, stmt.params);
|
|
669
|
+
return await _attemptDelivery(deliveryId);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function _emitAudit(action, outcome, metadata) {
|
|
673
|
+
try {
|
|
674
|
+
audit().safeEmit({ action: action, outcome: outcome, metadata: metadata || {} });
|
|
675
|
+
} catch (_e) { /* audit is a drop-silent hot-path sink — never crash the delivery */ }
|
|
676
|
+
try {
|
|
677
|
+
observability().safeEvent(action, 1, metadata || {});
|
|
678
|
+
} catch (_e) { /* drop-silent */ }
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return {
|
|
682
|
+
declareSchema: declareSchema,
|
|
683
|
+
registerEndpoint: registerEndpoint,
|
|
684
|
+
removeEndpoint: removeEndpoint,
|
|
685
|
+
listEndpoints: listEndpoints,
|
|
686
|
+
dispatch: dispatch,
|
|
687
|
+
processRetries: processRetries,
|
|
688
|
+
deliveries: {
|
|
689
|
+
list: _listDeliveries,
|
|
690
|
+
get: _getDelivery,
|
|
691
|
+
retry: _requeue,
|
|
692
|
+
},
|
|
693
|
+
dlq: {
|
|
694
|
+
list: function (filter) {
|
|
695
|
+
filter = filter || {};
|
|
696
|
+
return _listDeliveries({ status: "dead", limit: filter.limit, endpointId: filter.endpointId });
|
|
697
|
+
},
|
|
698
|
+
replay: _requeue,
|
|
699
|
+
},
|
|
700
|
+
close: function () { _signerCache = Object.create(null); },
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function _isTruthy(v) { return v === true || v === 1 || v === "1" || v === "true"; }
|
|
705
|
+
|
|
706
|
+
module.exports = { dispatcher: dispatcher };
|