@adcp/sdk 14.0.0-rc.38 → 14.0.0-rc.39
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/bin/adcp-storyboard-summary.js +286 -0
- package/bin/adcp.js +221 -10
- package/dist/lib/conformance/schemaLoader.d.mts +1 -1
- package/dist/lib/conformance/schemaLoader.d.ts +1 -1
- package/dist/lib/core/AgentClient.d.mts +6 -1
- package/dist/lib/core/AgentClient.d.ts +6 -1
- package/dist/lib/core/AgentClient.js +9 -0
- package/dist/lib/core/AgentClient.mjs +9 -0
- package/dist/lib/core/ConversationTypes.d.mts +29 -0
- package/dist/lib/core/ConversationTypes.d.ts +29 -0
- package/dist/lib/core/SingleAgentClient.d.mts +3 -1
- package/dist/lib/core/SingleAgentClient.d.ts +3 -1
- package/dist/lib/core/SingleAgentClient.js +9 -1
- package/dist/lib/core/SingleAgentClient.mjs +9 -1
- package/dist/lib/core/TaskExecutor.d.mts +13 -1
- package/dist/lib/core/TaskExecutor.d.ts +13 -1
- package/dist/lib/core/TaskExecutor.js +216 -18
- package/dist/lib/core/TaskExecutor.mjs +216 -19
- package/dist/lib/index.d.mts +5 -2
- package/dist/lib/index.d.ts +5 -2
- package/dist/lib/index.js +10 -1
- package/dist/lib/index.mjs +10 -1
- package/dist/lib/media-buy/action-assessment.d.mts +77 -0
- package/dist/lib/media-buy/action-assessment.d.ts +77 -0
- package/dist/lib/media-buy/action-assessment.js +300 -0
- package/dist/lib/media-buy/action-assessment.mjs +284 -0
- package/dist/lib/media-buy/action-constraints.d.mts +9 -0
- package/dist/lib/media-buy/action-constraints.d.ts +9 -0
- package/dist/lib/media-buy/action-constraints.js +170 -0
- package/dist/lib/media-buy/action-constraints.mjs +146 -0
- package/dist/lib/media-buy/action-contracts.d.mts +40 -0
- package/dist/lib/media-buy/action-contracts.d.ts +40 -0
- package/dist/lib/media-buy/action-contracts.js +304 -0
- package/dist/lib/media-buy/action-contracts.mjs +267 -0
- package/dist/lib/media-buy/action-metadata.generated.d.mts +37 -0
- package/dist/lib/media-buy/action-metadata.generated.d.ts +37 -0
- package/dist/lib/media-buy/action-metadata.generated.js +128 -0
- package/dist/lib/media-buy/action-metadata.generated.mjs +101 -0
- package/dist/lib/media-buy/action-types.d.mts +106 -0
- package/dist/lib/media-buy/action-types.d.ts +106 -0
- package/dist/lib/{types/v3-1-beta/tools.generated.js → media-buy/action-types.js} +2 -2
- package/dist/lib/media-buy/actions.d.mts +6 -0
- package/dist/lib/media-buy/actions.d.ts +6 -0
- package/dist/lib/media-buy/actions.js +38 -0
- package/dist/lib/media-buy/actions.mjs +9 -0
- package/dist/lib/media-buy/available-actions.d.mts +2 -1
- package/dist/lib/media-buy/available-actions.d.ts +2 -1
- package/dist/lib/media-buy/available-actions.js +4 -3
- package/dist/lib/media-buy/available-actions.mjs +4 -3
- package/dist/lib/media-buy/compatibility.js +3 -0
- package/dist/lib/media-buy/compatibility.mjs +3 -0
- package/dist/lib/media-buy/index.d.mts +4 -0
- package/dist/lib/media-buy/index.d.ts +4 -0
- package/dist/lib/media-buy/index.js +3 -1
- package/dist/lib/media-buy/index.mjs +1 -0
- package/dist/lib/media-buy/legacy-action-ids.d.mts +1 -0
- package/dist/lib/media-buy/legacy-action-ids.d.ts +1 -0
- package/dist/lib/media-buy/legacy-action-ids.js +50 -0
- package/dist/lib/media-buy/legacy-action-ids.mjs +26 -0
- package/dist/lib/media-buy/mutations.d.mts +70 -0
- package/dist/lib/media-buy/mutations.d.ts +70 -0
- package/dist/lib/media-buy/mutations.js +525 -0
- package/dist/lib/media-buy/mutations.mjs +498 -0
- package/dist/lib/media-buy/preflight.d.mts +20 -69
- package/dist/lib/media-buy/preflight.d.ts +20 -69
- package/dist/lib/media-buy/preflight.js +100 -336
- package/dist/lib/media-buy/preflight.mjs +111 -333
- package/dist/lib/media-buy/targeting-input.d.mts +7 -4
- package/dist/lib/media-buy/targeting-input.d.ts +7 -4
- package/dist/lib/media-buy/types.d.mts +55 -7
- package/dist/lib/media-buy/types.d.ts +55 -7
- package/dist/lib/protocols/rawResponseCapture.d.mts +12 -0
- package/dist/lib/protocols/rawResponseCapture.d.ts +12 -0
- package/dist/lib/protocols/rawResponseCapture.js +33 -3
- package/dist/lib/protocols/rawResponseCapture.mjs +32 -3
- package/dist/lib/registry/types.generated.d.mts +377 -9
- package/dist/lib/registry/types.generated.d.ts +377 -9
- package/dist/lib/reporting/evidence.d.mts +1 -0
- package/dist/lib/reporting/evidence.d.ts +1 -0
- package/dist/lib/reporting/evidence.js +32 -3
- package/dist/lib/reporting/evidence.mjs +31 -3
- package/dist/lib/reporting/ledger/handler.d.mts +51 -1
- package/dist/lib/reporting/ledger/handler.d.ts +51 -1
- package/dist/lib/reporting/ledger/handler.js +306 -38
- package/dist/lib/reporting/ledger/handler.mjs +304 -37
- package/dist/lib/reporting/ledger/health.d.mts +12 -0
- package/dist/lib/reporting/ledger/health.d.ts +12 -0
- package/dist/lib/reporting/ledger/health.js +15 -2
- package/dist/lib/reporting/ledger/health.mjs +13 -1
- package/dist/lib/reporting/ledger/index.d.mts +3 -0
- package/dist/lib/reporting/ledger/index.d.ts +3 -0
- package/dist/lib/reporting/ledger/index.js +7 -1
- package/dist/lib/reporting/ledger/index.mjs +3 -0
- package/dist/lib/reporting/ledger/lifecycle.d.mts +31 -4
- package/dist/lib/reporting/ledger/lifecycle.d.ts +31 -4
- package/dist/lib/reporting/ledger/lifecycle.js +244 -37
- package/dist/lib/reporting/ledger/lifecycle.mjs +244 -37
- package/dist/lib/reporting/ledger/managed-postgres.d.mts +231 -0
- package/dist/lib/reporting/ledger/managed-postgres.d.ts +231 -0
- package/dist/lib/reporting/ledger/managed-postgres.js +1797 -0
- package/dist/lib/reporting/ledger/managed-postgres.mjs +1776 -0
- package/dist/lib/reporting/ledger/managed.d.mts +355 -0
- package/dist/lib/reporting/ledger/managed.d.ts +355 -0
- package/dist/lib/reporting/ledger/managed.js +661 -0
- package/dist/lib/reporting/ledger/managed.mjs +633 -0
- package/dist/lib/reporting/ledger/notification-activity.d.mts +222 -0
- package/dist/lib/reporting/ledger/notification-activity.d.ts +222 -0
- package/dist/lib/reporting/ledger/notification-activity.js +1139 -0
- package/dist/lib/reporting/ledger/notification-activity.mjs +1110 -0
- package/dist/lib/reporting/ledger/postgres.d.mts +161 -1
- package/dist/lib/reporting/ledger/postgres.d.ts +161 -1
- package/dist/lib/reporting/ledger/postgres.js +1153 -21
- package/dist/lib/reporting/ledger/postgres.mjs +1152 -21
- package/dist/lib/reporting/ledger/producer.d.mts +8 -0
- package/dist/lib/reporting/ledger/producer.d.ts +8 -0
- package/dist/lib/reporting/ledger/producer.js +146 -52
- package/dist/lib/reporting/ledger/producer.mjs +146 -51
- package/dist/lib/reporting/ledger/types.d.mts +358 -1
- package/dist/lib/reporting/ledger/types.d.ts +358 -1
- package/dist/lib/reporting/ledger/types.js +3 -0
- package/dist/lib/reporting/ledger/types.mjs +2 -0
- package/dist/lib/reporting/service/conformance.d.mts +35 -0
- package/dist/lib/reporting/service/conformance.d.ts +35 -0
- package/dist/lib/reporting/service/conformance.js +89 -0
- package/dist/lib/reporting/service/conformance.mjs +68 -0
- package/dist/lib/reporting/service/index.d.mts +182 -0
- package/dist/lib/reporting/service/index.d.ts +182 -0
- package/dist/lib/reporting/service/index.js +793 -0
- package/dist/lib/reporting/service/index.mjs +787 -0
- package/dist/lib/reporting/source/index.d.mts +2 -1
- package/dist/lib/reporting/source/index.d.ts +2 -1
- package/dist/lib/reporting/source/index.js +16 -3
- package/dist/lib/reporting/source/index.mjs +12 -1
- package/dist/lib/reporting/source/inline.d.mts +88 -1
- package/dist/lib/reporting/source/inline.d.ts +88 -1
- package/dist/lib/reporting/source/inline.js +1161 -163
- package/dist/lib/reporting/source/inline.mjs +1166 -163
- package/dist/lib/reporting/source/manifest.js +12 -7
- package/dist/lib/reporting/source/manifest.mjs +12 -7
- package/dist/lib/reporting/source/source.d.mts +26 -0
- package/dist/lib/reporting/source/source.d.ts +26 -0
- package/dist/lib/reporting/source/source.js +71 -2
- package/dist/lib/reporting/source/source.mjs +65 -1
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/create-adcp-server.d.mts +18 -0
- package/dist/lib/server/create-adcp-server.d.ts +18 -0
- package/dist/lib/server/create-adcp-server.js +52 -2
- package/dist/lib/server/create-adcp-server.mjs +52 -2
- package/dist/lib/server/decisioning/account.d.mts +1 -1
- package/dist/lib/server/decisioning/account.d.ts +1 -1
- package/dist/lib/server/decisioning/index.d.mts +1 -0
- package/dist/lib/server/decisioning/index.d.ts +1 -0
- package/dist/lib/server/decisioning/platform.d.mts +3 -0
- package/dist/lib/server/decisioning/platform.d.ts +3 -0
- package/dist/lib/server/decisioning/runtime/from-platform.js +88 -10
- package/dist/lib/server/decisioning/runtime/from-platform.mjs +88 -10
- package/dist/lib/server/decisioning/specialisms/reporting.d.mts +38 -0
- package/dist/lib/server/decisioning/specialisms/reporting.d.ts +38 -0
- package/dist/lib/{types/v3-1-beta/index.js → server/decisioning/specialisms/reporting.js} +2 -8
- package/dist/lib/server/decisioning/specialisms/reporting.mjs +0 -0
- package/dist/lib/server/index.d.mts +4 -2
- package/dist/lib/server/index.d.ts +4 -2
- package/dist/lib/server/index.js +5 -0
- package/dist/lib/server/index.mjs +4 -0
- package/dist/lib/server/media-buy-action-resolver.d.mts +63 -0
- package/dist/lib/server/media-buy-action-resolver.d.ts +63 -0
- package/dist/lib/server/media-buy-action-resolver.js +297 -0
- package/dist/lib/server/media-buy-action-resolver.mjs +285 -0
- package/dist/lib/server/media-buy-actions.d.mts +7 -8
- package/dist/lib/server/media-buy-actions.d.ts +7 -8
- package/dist/lib/server/media-buy-actions.js +57 -14
- package/dist/lib/server/media-buy-actions.mjs +63 -14
- package/dist/lib/server/notification-subscriptions/index.d.mts +2 -2
- package/dist/lib/server/notification-subscriptions/index.d.ts +2 -2
- package/dist/lib/server/notification-subscriptions/index.js +2 -0
- package/dist/lib/server/notification-subscriptions/index.mjs +2 -0
- package/dist/lib/server/notification-subscriptions/postgres-runtime.d.mts +3 -1
- package/dist/lib/server/notification-subscriptions/postgres-runtime.d.ts +3 -1
- package/dist/lib/server/notification-subscriptions/postgres-runtime.js +1 -0
- package/dist/lib/server/notification-subscriptions/postgres-runtime.mjs +1 -0
- package/dist/lib/server/notification-subscriptions/runtime.d.mts +12 -1
- package/dist/lib/server/notification-subscriptions/runtime.d.ts +12 -1
- package/dist/lib/server/notification-subscriptions/runtime.js +102 -16
- package/dist/lib/server/notification-subscriptions/runtime.mjs +101 -16
- package/dist/lib/server/notification-subscriptions/types.d.mts +104 -1
- package/dist/lib/server/notification-subscriptions/types.d.ts +104 -1
- package/dist/lib/server/webhook-emitter.d.mts +40 -5
- package/dist/lib/server/webhook-emitter.d.ts +40 -5
- package/dist/lib/server/webhook-emitter.js +23 -12
- package/dist/lib/server/webhook-emitter.mjs +23 -12
- package/dist/lib/storage/interfaces.d.mts +14 -2
- package/dist/lib/storage/interfaces.d.ts +14 -2
- package/dist/lib/testing/compliance/comply.d.mts +9 -3
- package/dist/lib/testing/compliance/comply.d.ts +9 -3
- package/dist/lib/testing/compliance/comply.js +158 -3
- package/dist/lib/testing/compliance/comply.mjs +159 -4
- package/dist/lib/testing/compliance/storyboard-tracks.js +7 -5
- package/dist/lib/testing/compliance/storyboard-tracks.mjs +7 -5
- package/dist/lib/testing/compliance/summary.js +10 -1
- package/dist/lib/testing/compliance/summary.mjs +10 -1
- package/dist/lib/testing/index.d.mts +1 -1
- package/dist/lib/testing/index.d.ts +1 -1
- package/dist/lib/testing/index.js +2 -0
- package/dist/lib/testing/index.mjs +3 -1
- package/dist/lib/testing/storyboard/agent-routing.d.mts +12 -43
- package/dist/lib/testing/storyboard/agent-routing.d.ts +12 -43
- package/dist/lib/testing/storyboard/agent-routing.js +75 -13
- package/dist/lib/testing/storyboard/agent-routing.mjs +70 -12
- package/dist/lib/testing/storyboard/index.d.mts +1 -0
- package/dist/lib/testing/storyboard/index.d.ts +1 -0
- package/dist/lib/testing/storyboard/index.js +3 -0
- package/dist/lib/testing/storyboard/index.mjs +2 -0
- package/dist/lib/testing/storyboard/junit.d.mts +1 -1
- package/dist/lib/testing/storyboard/junit.d.ts +1 -1
- package/dist/lib/testing/storyboard/junit.js +21 -2
- package/dist/lib/testing/storyboard/junit.mjs +21 -2
- package/dist/lib/testing/storyboard/probes.d.mts +130 -0
- package/dist/lib/testing/storyboard/probes.d.ts +130 -0
- package/dist/lib/testing/storyboard/probes.js +682 -10
- package/dist/lib/testing/storyboard/probes.mjs +683 -3
- package/dist/lib/testing/storyboard/request-signing/builder.d.mts +16 -5
- package/dist/lib/testing/storyboard/request-signing/builder.d.ts +16 -5
- package/dist/lib/testing/storyboard/request-signing/grader.d.mts +44 -1
- package/dist/lib/testing/storyboard/request-signing/grader.d.ts +44 -1
- package/dist/lib/testing/storyboard/request-signing/grader.js +46 -22
- package/dist/lib/testing/storyboard/request-signing/grader.mjs +43 -21
- package/dist/lib/testing/storyboard/request-signing/probe-dispatch.d.mts +86 -11
- package/dist/lib/testing/storyboard/request-signing/probe-dispatch.d.ts +86 -11
- package/dist/lib/testing/storyboard/request-signing/probe-dispatch.js +119 -25
- package/dist/lib/testing/storyboard/request-signing/probe-dispatch.mjs +117 -26
- package/dist/lib/testing/storyboard/request-signing/synthesize.js +22 -2
- package/dist/lib/testing/storyboard/request-signing/synthesize.mjs +22 -2
- package/dist/lib/testing/storyboard/runner.d.mts +103 -0
- package/dist/lib/testing/storyboard/runner.d.ts +103 -0
- package/dist/lib/testing/storyboard/runner.js +1090 -268
- package/dist/lib/testing/storyboard/runner.mjs +1096 -261
- package/dist/lib/testing/storyboard/seeding.d.mts +4 -1
- package/dist/lib/testing/storyboard/seeding.d.ts +4 -1
- package/dist/lib/testing/storyboard/seeding.js +55 -24
- package/dist/lib/testing/storyboard/seeding.mjs +55 -24
- package/dist/lib/testing/storyboard/test-kit.d.mts +37 -3
- package/dist/lib/testing/storyboard/test-kit.d.ts +37 -3
- package/dist/lib/testing/storyboard/test-kit.js +8 -2
- package/dist/lib/testing/storyboard/test-kit.mjs +7 -2
- package/dist/lib/testing/storyboard/types.d.mts +132 -10
- package/dist/lib/testing/storyboard/types.d.ts +132 -10
- package/dist/lib/testing/storyboard/types.js +17 -0
- package/dist/lib/testing/storyboard/types.mjs +16 -0
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/testing/storyboard/validations.js +16 -0
- package/dist/lib/testing/storyboard/validations.mjs +16 -0
- package/dist/lib/testing/test-controller.d.mts +1 -1
- package/dist/lib/testing/test-controller.d.ts +1 -1
- package/dist/lib/types/buy-products.d.ts +71 -6
- package/dist/lib/types/control-media-buy.d.ts +25 -11
- package/dist/lib/types/core.generated.d.mts +69 -10
- package/dist/lib/types/core.generated.d.ts +69 -10
- package/dist/lib/types/create-media-buy.d.ts +71 -6
- package/dist/lib/types/forward-compat-error-codes.d.mts +7 -6
- package/dist/lib/types/forward-compat-error-codes.d.ts +7 -6
- package/dist/lib/types/index.d.mts +2 -2
- package/dist/lib/types/index.d.ts +2 -2
- package/dist/lib/types/schemas.generated.d.ts +8040 -8116
- package/dist/lib/types/schemas.generated.js +1074 -309
- package/dist/lib/types/schemas.generated.mjs +1068 -309
- package/dist/lib/types/tools.generated.d.mts +65 -6
- package/dist/lib/types/tools.generated.d.ts +65 -6
- package/dist/lib/types/update-media-buy.d.ts +71 -6
- package/dist/lib/utils/redact-secrets.d.mts +1 -1
- package/dist/lib/utils/redact-secrets.d.ts +1 -1
- package/dist/lib/utils/redact-secrets.js +1 -1
- package/dist/lib/utils/redact-secrets.mjs +1 -1
- package/dist/lib/v2/projection/cache-versions.d.mts +0 -1
- package/dist/lib/v2/projection/cache-versions.d.ts +0 -1
- package/dist/lib/v2/projection/cache-versions.js +1 -15
- package/dist/lib/v2/projection/cache-versions.mjs +1 -14
- package/dist/lib/v2/projection/canonical-properties.js +2 -2
- package/dist/lib/v2/projection/canonical-properties.mjs +3 -3
- package/dist/lib/v2/projection/registry.d.mts +1 -1
- package/dist/lib/v2/projection/registry.d.ts +1 -1
- package/dist/lib/v2/projection/registry.js +2 -2
- package/dist/lib/v2/projection/registry.mjs +3 -3
- package/dist/lib/version.d.mts +5 -5
- package/dist/lib/version.d.ts +5 -5
- package/dist/lib/version.js +3 -8
- package/dist/lib/version.mjs +3 -8
- package/dist/lib/wholesale-feed-sync/index.d.mts +1 -0
- package/dist/lib/wholesale-feed-sync/index.d.ts +1 -0
- package/dist/lib/wholesale-feed-sync/protocol-types.d.mts +42 -0
- package/dist/lib/wholesale-feed-sync/protocol-types.d.ts +42 -0
- package/dist/lib/wholesale-feed-sync/protocol-types.js +16 -0
- package/dist/lib/wholesale-feed-sync/protocol-types.mjs +0 -0
- package/dist/lib/wholesale-feed-sync/sync.d.mts +6 -6
- package/dist/lib/wholesale-feed-sync/sync.d.ts +6 -6
- package/dist/lib/wholesale-feed-sync/sync.js +3 -1
- package/dist/lib/wholesale-feed-sync/sync.mjs +3 -1
- package/dist/lib/wholesale-feed-sync/types.d.mts +15 -14
- package/dist/lib/wholesale-feed-sync/types.d.ts +15 -14
- package/dist/lib/wholesale-feed-sync/webhook-notification.d.mts +7 -5
- package/dist/lib/wholesale-feed-sync/webhook-notification.d.ts +7 -5
- package/dist/lib/wholesale-feed-sync/webhook-notification.js +45 -0
- package/dist/lib/wholesale-feed-sync/webhook-notification.mjs +44 -0
- package/docs/CLI.md +1 -1
- package/docs/TYPE-SUMMARY.md +118 -4
- package/docs/guides/ASYNC-API-REFERENCE.md +62 -1
- package/docs/guides/ASYNC-DEVELOPER-GUIDE.md +25 -0
- package/docs/guides/ASYNC-DOCUMENTATION-INDEX.md +3 -0
- package/docs/guides/BUYER-QUICKSTART-3.2.md +5 -0
- package/docs/guides/DURABLE-BUYER-WRITES.md +337 -0
- package/docs/guides/EXISTING-PLATFORM.md +35 -0
- package/docs/guides/MEDIA-BUY-3.2-COMPATIBILITY.md +7 -3
- package/docs/guides/MEDIA-BUY-ACTION-ASSESSMENT.md +300 -0
- package/docs/guides/REPORTING-LEDGER.md +654 -3
- package/docs/guides/REPORTING-SOURCE-EXECUTOR.md +63 -1
- package/docs/guides/SELLER-QUICKSTART-3.2.md +2 -0
- package/docs/guides/VALIDATE-YOUR-AGENT.md +35 -0
- package/docs/guides/idempotency-crash-recovery.md +4 -0
- package/docs/llms.txt +4 -2
- package/docs/migration-14.x-rc-worksheet.md +5 -5
- package/docs/migration-7.9-to-7.10.md +7 -5
- package/examples/README.md +17 -0
- package/examples/durable-buyer-writes/caller.ts +745 -0
- package/examples/durable-buyer-writes/worker.ts +240 -0
- package/examples/reliable-reporting-service/README.md +100 -0
- package/examples/seller-3.2-starter.ts +7 -8
- package/examples/targeting-input-existing-platform.ts +153 -0
- package/package.json +41 -29
- package/skills/build-seller-agent/specialisms/signed-requests.md +11 -1
- package/skills/cross-cutting.md +28 -11
- package/compliance/cache/3.1.0-beta.7/domains/brand/index.yaml +0 -160
- package/compliance/cache/3.1.0-beta.7/domains/brand/scenarios/distributed_brand_resolution.yaml +0 -415
- package/compliance/cache/3.1.0-beta.7/domains/brand/scenarios/single_side_trust_extension.yaml +0 -454
- package/compliance/cache/3.1.0-beta.7/domains/creative/index.yaml +0 -339
- package/compliance/cache/3.1.0-beta.7/domains/creative/scenarios/billing_out_of_band.yaml +0 -153
- package/compliance/cache/3.1.0-beta.7/domains/creative/scenarios/creative_lifecycle_webhooks.yaml +0 -389
- package/compliance/cache/3.1.0-beta.7/domains/creative/scenarios/native_in_feed.yaml +0 -543
- package/compliance/cache/3.1.0-beta.7/domains/governance/index.yaml +0 -682
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/index.yaml +0 -781
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/audience_buy_flow.yaml +0 -380
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/available_actions.yaml +0 -565
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/billing_finality_delivery.yaml +0 -354
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/canonical_formats.yaml +0 -711
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/clicks_buy_flow.yaml +0 -264
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/completed_views_buy_flow.yaml +0 -344
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/create_media_buy_async.yaml +0 -234
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/creative_fate_after_cancellation.yaml +0 -419
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/creative_reception.yaml +0 -247
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/delivery_reporting.yaml +0 -357
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/dependency_impairment.yaml +0 -633
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/dependency_impairment_cardinality.yaml +0 -800
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/event_dedup_flow.yaml +0 -399
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/frequency_cap_enforcement.yaml +0 -309
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/governance_approved.yaml +0 -214
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/governance_conditions.yaml +0 -199
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/governance_denied.yaml +0 -204
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/governance_denied_recovery.yaml +0 -252
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/invalid_transitions.yaml +0 -289
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/inventory_list_no_match.yaml +0 -148
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/inventory_list_targeting.yaml +0 -276
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/measurement_accountability.yaml +0 -244
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/measurement_terms_rejected.yaml +0 -203
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/pending_creatives_to_start.yaml +0 -274
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/per_creative_conversion_attribution.yaml +0 -500
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/performance_buy_flow.yaml +0 -428
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/performance_buy_flow_roas.yaml +0 -470
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/product_signal_targeting.yaml +0 -373
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/proposal_finalize.yaml +0 -399
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/proposal_finalize_asap_timing.yaml +0 -264
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/proposal_not_found_errors.yaml +0 -257
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/provenance_audit_observation.yaml +0 -333
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/provenance_enforcement.yaml +0 -517
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/provenance_truth_of_claim.yaml +0 -294
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/reach_buy_flow.yaml +0 -823
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/refine_finalize_exclusivity.yaml +0 -360
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/refine_products.yaml +0 -148
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/vendor_metric_accountability.yaml +0 -293
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/vendor_metric_catalog_precondition.yaml +0 -307
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/scenarios/vendor_metric_optimization_flow.yaml +0 -576
- package/compliance/cache/3.1.0-beta.7/domains/media-buy/state-machine.yaml +0 -442
- package/compliance/cache/3.1.0-beta.7/domains/signals/index.yaml +0 -266
- package/compliance/cache/3.1.0-beta.7/domains/sponsored-intelligence/index.yaml +0 -256
- package/compliance/cache/3.1.0-beta.7/index.json +0 -356
- package/compliance/cache/3.1.0-beta.7/protocols/brand/index.yaml +0 -160
- package/compliance/cache/3.1.0-beta.7/protocols/brand/scenarios/distributed_brand_resolution.yaml +0 -415
- package/compliance/cache/3.1.0-beta.7/protocols/brand/scenarios/single_side_trust_extension.yaml +0 -454
- package/compliance/cache/3.1.0-beta.7/protocols/creative/index.yaml +0 -339
- package/compliance/cache/3.1.0-beta.7/protocols/creative/scenarios/billing_out_of_band.yaml +0 -153
- package/compliance/cache/3.1.0-beta.7/protocols/creative/scenarios/creative_lifecycle_webhooks.yaml +0 -389
- package/compliance/cache/3.1.0-beta.7/protocols/creative/scenarios/native_in_feed.yaml +0 -543
- package/compliance/cache/3.1.0-beta.7/protocols/governance/index.yaml +0 -682
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/index.yaml +0 -781
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/audience_buy_flow.yaml +0 -380
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/available_actions.yaml +0 -565
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/billing_finality_delivery.yaml +0 -354
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/canonical_formats.yaml +0 -711
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/clicks_buy_flow.yaml +0 -264
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/completed_views_buy_flow.yaml +0 -344
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/create_media_buy_async.yaml +0 -234
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/creative_fate_after_cancellation.yaml +0 -419
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/creative_reception.yaml +0 -247
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/delivery_reporting.yaml +0 -357
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/dependency_impairment.yaml +0 -633
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/dependency_impairment_cardinality.yaml +0 -800
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/event_dedup_flow.yaml +0 -399
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/frequency_cap_enforcement.yaml +0 -309
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/governance_approved.yaml +0 -214
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/governance_conditions.yaml +0 -199
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/governance_denied.yaml +0 -204
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/governance_denied_recovery.yaml +0 -252
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/invalid_transitions.yaml +0 -289
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/inventory_list_no_match.yaml +0 -148
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/inventory_list_targeting.yaml +0 -276
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/measurement_accountability.yaml +0 -244
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/measurement_terms_rejected.yaml +0 -203
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/pending_creatives_to_start.yaml +0 -274
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/per_creative_conversion_attribution.yaml +0 -500
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/performance_buy_flow.yaml +0 -428
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/performance_buy_flow_roas.yaml +0 -470
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/product_signal_targeting.yaml +0 -373
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/proposal_finalize.yaml +0 -399
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/proposal_finalize_asap_timing.yaml +0 -264
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/proposal_not_found_errors.yaml +0 -257
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/provenance_audit_observation.yaml +0 -333
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/provenance_enforcement.yaml +0 -517
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/provenance_truth_of_claim.yaml +0 -294
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/reach_buy_flow.yaml +0 -823
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/refine_finalize_exclusivity.yaml +0 -360
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/refine_products.yaml +0 -148
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/vendor_metric_accountability.yaml +0 -293
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/vendor_metric_catalog_precondition.yaml +0 -307
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/scenarios/vendor_metric_optimization_flow.yaml +0 -576
- package/compliance/cache/3.1.0-beta.7/protocols/media-buy/state-machine.yaml +0 -442
- package/compliance/cache/3.1.0-beta.7/protocols/signals/index.yaml +0 -266
- package/compliance/cache/3.1.0-beta.7/protocols/sponsored-intelligence/index.yaml +0 -256
- package/compliance/cache/3.1.0-beta.7/specialisms/audience-sync/index.yaml +0 -313
- package/compliance/cache/3.1.0-beta.7/specialisms/brand-rights/index.yaml +0 -350
- package/compliance/cache/3.1.0-beta.7/specialisms/brand-rights/scenarios/governance_denied.yaml +0 -226
- package/compliance/cache/3.1.0-beta.7/specialisms/collection-lists/index.yaml +0 -359
- package/compliance/cache/3.1.0-beta.7/specialisms/content-standards/index.yaml +0 -572
- package/compliance/cache/3.1.0-beta.7/specialisms/creative-ad-server/index.yaml +0 -409
- package/compliance/cache/3.1.0-beta.7/specialisms/creative-generative/generative-seller.yaml +0 -807
- package/compliance/cache/3.1.0-beta.7/specialisms/creative-generative/index.yaml +0 -758
- package/compliance/cache/3.1.0-beta.7/specialisms/creative-template/index.yaml +0 -510
- package/compliance/cache/3.1.0-beta.7/specialisms/governance-aware-seller/index.yaml +0 -143
- package/compliance/cache/3.1.0-beta.7/specialisms/governance-aware-seller/scenarios/governance_multi_agent_rejected.yaml +0 -117
- package/compliance/cache/3.1.0-beta.7/specialisms/governance-delivery-monitor/index.yaml +0 -441
- package/compliance/cache/3.1.0-beta.7/specialisms/governance-spend-authority/denied.yaml +0 -221
- package/compliance/cache/3.1.0-beta.7/specialisms/governance-spend-authority/index.yaml +0 -330
- package/compliance/cache/3.1.0-beta.7/specialisms/property-lists/index.yaml +0 -482
- package/compliance/cache/3.1.0-beta.7/specialisms/sales-broadcast-tv/index.yaml +0 -738
- package/compliance/cache/3.1.0-beta.7/specialisms/sales-catalog-driven/index.yaml +0 -840
- package/compliance/cache/3.1.0-beta.7/specialisms/sales-guaranteed/index.yaml +0 -601
- package/compliance/cache/3.1.0-beta.7/specialisms/sales-non-guaranteed/index.yaml +0 -546
- package/compliance/cache/3.1.0-beta.7/specialisms/sales-proposal-mode/index.yaml +0 -586
- package/compliance/cache/3.1.0-beta.7/specialisms/sales-social/index.yaml +0 -919
- package/compliance/cache/3.1.0-beta.7/specialisms/signal-marketplace/index.yaml +0 -424
- package/compliance/cache/3.1.0-beta.7/specialisms/signal-marketplace/scenarios/governance_denied.yaml +0 -211
- package/compliance/cache/3.1.0-beta.7/specialisms/signal-owned/index.yaml +0 -317
- package/compliance/cache/3.1.0-beta.7/specialisms/sponsored-intelligence/index.yaml +0 -59
- package/compliance/cache/3.1.0-beta.7/test-kits/acme-outdoor-live.yaml +0 -78
- package/compliance/cache/3.1.0-beta.7/test-kits/acme-outdoor.yaml +0 -223
- package/compliance/cache/3.1.0-beta.7/test-kits/billing-gate-runner.yaml +0 -115
- package/compliance/cache/3.1.0-beta.7/test-kits/bistro-oranje.yaml +0 -126
- package/compliance/cache/3.1.0-beta.7/test-kits/distributed-brand-runner.yaml +0 -281
- package/compliance/cache/3.1.0-beta.7/test-kits/nova-motors.yaml +0 -262
- package/compliance/cache/3.1.0-beta.7/test-kits/osei-natural.yaml +0 -126
- package/compliance/cache/3.1.0-beta.7/test-kits/parallel-dispatch-runner.yaml +0 -196
- package/compliance/cache/3.1.0-beta.7/test-kits/rate-limit-trip-runner.yaml +0 -172
- package/compliance/cache/3.1.0-beta.7/test-kits/signed-requests-runner.yaml +0 -155
- package/compliance/cache/3.1.0-beta.7/test-kits/single-side-trust-runner.yaml +0 -294
- package/compliance/cache/3.1.0-beta.7/test-kits/substitution-observer-runner.yaml +0 -688
- package/compliance/cache/3.1.0-beta.7/test-kits/summit-foods.yaml +0 -125
- package/compliance/cache/3.1.0-beta.7/test-kits/webhook-receiver-runner.yaml +0 -265
- package/compliance/cache/3.1.0-beta.7/test-vectors/plan-hash/001-minimal-plan.json +0 -43
- package/compliance/cache/3.1.0-beta.7/test-vectors/plan-hash/002-full-plan.json +0 -217
- package/compliance/cache/3.1.0-beta.7/test-vectors/plan-hash/003-bookkeeping-stripped.json +0 -60
- package/compliance/cache/3.1.0-beta.7/test-vectors/plan-hash/004a-human-review-omitted.json +0 -43
- package/compliance/cache/3.1.0-beta.7/test-vectors/plan-hash/004b-human-review-explicit-null.json +0 -49
- package/compliance/cache/3.1.0-beta.7/test-vectors/plan-hash/005a-policy-categories-order-1.json +0 -53
- package/compliance/cache/3.1.0-beta.7/test-vectors/plan-hash/005b-policy-categories-order-2.json +0 -57
- package/compliance/cache/3.1.0-beta.7/test-vectors/plan-hash/006a-ext-trace-v1.json +0 -49
- package/compliance/cache/3.1.0-beta.7/test-vectors/plan-hash/006b-ext-trace-v2.json +0 -53
- package/compliance/cache/3.1.0-beta.7/test-vectors/plan-hash/007-unicode-objectives.json +0 -43
- package/compliance/cache/3.1.0-beta.7/test-vectors/plan-hash/008-numeric-canonicalization.json +0 -65
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/README.md +0 -220
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/canonicalization.json +0 -241
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/keys.json +0 -60
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/001-no-signature-header.json +0 -24
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/002-wrong-tag.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/003-expired-signature.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/004-window-too-long.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/005-alg-not-allowed.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/006-missing-covered-component.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/007-missing-content-digest.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/008-unknown-keyid.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/009-key-ops-missing-verify.json +0 -27
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/010-content-digest-mismatch.json +0 -33
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/011-malformed-header.json +0 -27
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/012-missing-expires-param.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/013-expires-le-created.json +0 -27
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/014-missing-nonce-param.json +0 -27
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/015-signature-invalid.json +0 -28
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/016-replayed-nonce.json +0 -35
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/017-key-revoked.json +0 -38
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/018-digest-covered-when-forbidden.json +0 -28
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/019-signature-without-signature-input.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/020-rate-abuse.json +0 -34
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/021-duplicate-signature-input-label.json +0 -31
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/022-multi-valued-content-type.json +0 -31
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/023-multi-valued-content-digest.json +0 -32
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/024-unquoted-string-param.json +0 -31
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/025-jwk-alg-crv-mismatch.json +0 -43
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/026-non-ascii-host.json +0 -31
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/027-webhook-registration-authentication-unsigned.json +0 -25
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/negative/028-unsigned-protocol-method-required.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/positive/001-basic-post.json +0 -30
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/positive/002-post-with-content-digest.json +0 -31
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/positive/003-es256-post.json +0 -30
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/positive/004-multiple-signature-labels.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/positive/005-default-port-stripped.json +0 -30
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/positive/006-dot-segment-path.json +0 -30
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/positive/007-query-byte-preserved.json +0 -30
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/positive/008-percent-encoded-path.json +0 -30
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/positive/009-percent-encoded-unreserved-decoded.json +0 -30
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/positive/010-percent-encoded-slash-preserved.json +0 -30
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/positive/011-ipv6-authority.json +0 -30
- package/compliance/cache/3.1.0-beta.7/test-vectors/request-signing/positive/012-ipv6-authority-default-port-stripped.json +0 -30
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/README.md +0 -211
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/keys.json +0 -61
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/001-wrong-tag.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/002-expired-signature.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/003-window-too-long.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/004-alg-not-allowed.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/005-missing-authority-component.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/006-missing-content-digest.json +0 -25
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/007-unknown-keyid.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/008-wrong-adcp-use.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/009-content-digest-mismatch.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/010-malformed-signature-input.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/011-signature-without-input.json +0 -25
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/012-missing-expires-param.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/013-expires-le-created.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/014-missing-nonce-param.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/015-signature-invalid.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/016-replayed-nonce.json +0 -37
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/017-key-revoked.json +0 -32
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/018-rate-abuse.json +0 -33
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/019-revocation-stale.json +0 -32
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/020-key-ops-missing-verify.json +0 -41
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/negative/021-base64-alphabet-mixing.json +0 -26
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/positive/001-basic-post.json +0 -24
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/positive/002-es256-post.json +0 -24
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/positive/003-multiple-signature-labels.json +0 -24
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/positive/004-default-port-stripped.json +0 -24
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/positive/005-percent-encoded-path.json +0 -24
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/positive/006-query-byte-preserved.json +0 -24
- package/compliance/cache/3.1.0-beta.7/test-vectors/webhook-signing/positive/007-body-without-idempotency-key.json +0 -25
- package/compliance/cache/3.1.0-beta.7/universal/billing-gate-dispatch.yaml +0 -450
- package/compliance/cache/3.1.0-beta.7/universal/canonical-format-validate-input.yaml +0 -640
- package/compliance/cache/3.1.0-beta.7/universal/capability-discovery.yaml +0 -125
- package/compliance/cache/3.1.0-beta.7/universal/collection-lists-pagination-integrity.yaml +0 -306
- package/compliance/cache/3.1.0-beta.7/universal/comply-controller-mode-gate.yaml +0 -141
- package/compliance/cache/3.1.0-beta.7/universal/content-standards-pagination-integrity.yaml +0 -326
- package/compliance/cache/3.1.0-beta.7/universal/deterministic-testing.yaml +0 -1430
- package/compliance/cache/3.1.0-beta.7/universal/error-compliance-signals.yaml +0 -377
- package/compliance/cache/3.1.0-beta.7/universal/error-compliance.yaml +0 -528
- package/compliance/cache/3.1.0-beta.7/universal/fictional-entities.yaml +0 -307
- package/compliance/cache/3.1.0-beta.7/universal/get-media-buys-pagination-integrity.yaml +0 -160
- package/compliance/cache/3.1.0-beta.7/universal/get-signals-pagination-integrity.yaml +0 -211
- package/compliance/cache/3.1.0-beta.7/universal/idempotency.yaml +0 -861
- package/compliance/cache/3.1.0-beta.7/universal/notification-config-event-scope.yaml +0 -119
- package/compliance/cache/3.1.0-beta.7/universal/notification-config-lifecycle.yaml +0 -337
- package/compliance/cache/3.1.0-beta.7/universal/notification-config-rejections.yaml +0 -107
- package/compliance/cache/3.1.0-beta.7/universal/pagination-integrity-creative-formats.yaml +0 -265
- package/compliance/cache/3.1.0-beta.7/universal/pagination-integrity-list-accounts.yaml +0 -245
- package/compliance/cache/3.1.0-beta.7/universal/pagination-integrity.yaml +0 -263
- package/compliance/cache/3.1.0-beta.7/universal/property-lists-pagination-integrity.yaml +0 -307
- package/compliance/cache/3.1.0-beta.7/universal/read-tool-idempotency.yaml +0 -405
- package/compliance/cache/3.1.0-beta.7/universal/runner-output-contract.yaml +0 -1266
- package/compliance/cache/3.1.0-beta.7/universal/schema-validation-signals.yaml +0 -181
- package/compliance/cache/3.1.0-beta.7/universal/schema-validation.yaml +0 -548
- package/compliance/cache/3.1.0-beta.7/universal/security.yaml +0 -539
- package/compliance/cache/3.1.0-beta.7/universal/signed-requests.yaml +0 -217
- package/compliance/cache/3.1.0-beta.7/universal/stale-response-advisory.yaml +0 -295
- package/compliance/cache/3.1.0-beta.7/universal/storyboard-schema.yaml +0 -2136
- package/compliance/cache/3.1.0-beta.7/universal/v3-envelope-integrity.yaml +0 -117
- package/compliance/cache/3.1.0-beta.7/universal/version-negotiation.yaml +0 -130
- package/compliance/cache/3.1.0-beta.7/universal/webhook-emission.yaml +0 -411
- package/compliance/cache/3.1.0-beta.7/universal/wholesale-feed-bulk-webhooks.yaml +0 -82
- package/compliance/cache/3.1.0-beta.7/universal/wholesale-feed-product-webhooks.yaml +0 -83
- package/compliance/cache/3.1.0-beta.7/universal/wholesale-feed-products.yaml +0 -151
- package/compliance/cache/3.1.0-beta.7/universal/wholesale-feed-signal-webhooks.yaml +0 -83
- package/compliance/cache/3.1.0-beta.7/universal/wholesale-feed-signals.yaml +0 -149
- package/dist/lib/types/v3-1-beta/index.d.mts +0 -1
- package/dist/lib/types/v3-1-beta/index.d.ts +0 -1
- package/dist/lib/types/v3-1-beta/index.mjs +0 -1
- package/dist/lib/types/v3-1-beta/tools.generated.d.mts +0 -26788
- package/dist/lib/types/v3-1-beta/tools.generated.d.ts +0 -26788
- /package/dist/lib/{types/v3-1-beta/tools.generated.mjs → media-buy/action-types.mjs} +0 -0
|
@@ -0,0 +1,1797 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var managed_postgres_exports = {};
|
|
20
|
+
__export(managed_postgres_exports, {
|
|
21
|
+
PostgresReportingManagedDeliveryStore: () => PostgresReportingManagedDeliveryStore,
|
|
22
|
+
REPORTING_MANAGED_DELIVERY_MIGRATION: () => REPORTING_MANAGED_DELIVERY_MIGRATION
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(managed_postgres_exports);
|
|
25
|
+
var import_node_crypto = require("node:crypto");
|
|
26
|
+
var import_jcs = require('../../utils/jcs.js');
|
|
27
|
+
var import_evidence = require('../evidence.js');
|
|
28
|
+
var import_types = require('./types.js');
|
|
29
|
+
var import_managed = require('./managed.js');
|
|
30
|
+
const REPORTING_MANAGED_DELIVERY_MIGRATION = `
|
|
31
|
+
CREATE TABLE IF NOT EXISTS adcp_reporting_destination_authorizations (
|
|
32
|
+
account_id TEXT NOT NULL,
|
|
33
|
+
destination_ref TEXT NOT NULL,
|
|
34
|
+
generation BIGINT NOT NULL,
|
|
35
|
+
authorized_at TIMESTAMPTZ NOT NULL,
|
|
36
|
+
revoked_at TIMESTAMPTZ,
|
|
37
|
+
cleanup_completed_at TIMESTAMPTZ,
|
|
38
|
+
cleanup_lease_owner TEXT,
|
|
39
|
+
cleanup_lease_generation BIGINT NOT NULL DEFAULT 0,
|
|
40
|
+
cleanup_lease_expires_at TIMESTAMPTZ,
|
|
41
|
+
changed_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
42
|
+
data JSONB NOT NULL,
|
|
43
|
+
PRIMARY KEY (account_id, destination_ref, generation),
|
|
44
|
+
CHECK (generation > 0),
|
|
45
|
+
CHECK (revoked_at IS NULL OR revoked_at >= authorized_at)
|
|
46
|
+
);
|
|
47
|
+
ALTER TABLE adcp_reporting_destination_authorizations
|
|
48
|
+
ADD COLUMN IF NOT EXISTS changed_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp();
|
|
49
|
+
CREATE UNIQUE INDEX IF NOT EXISTS adcp_reporting_destination_authorizations_current
|
|
50
|
+
ON adcp_reporting_destination_authorizations (account_id, destination_ref)
|
|
51
|
+
WHERE revoked_at IS NULL;
|
|
52
|
+
CREATE INDEX IF NOT EXISTS adcp_reporting_destination_authorizations_cleanup
|
|
53
|
+
ON adcp_reporting_destination_authorizations (revoked_at, account_id, destination_ref)
|
|
54
|
+
WHERE revoked_at IS NOT NULL AND cleanup_completed_at IS NULL;
|
|
55
|
+
|
|
56
|
+
CREATE TABLE IF NOT EXISTS adcp_reporting_managed_bindings (
|
|
57
|
+
configuration_id TEXT PRIMARY KEY REFERENCES adcp_reporting_configurations(configuration_id),
|
|
58
|
+
account_id TEXT NOT NULL,
|
|
59
|
+
delivery_config_id TEXT NOT NULL,
|
|
60
|
+
delivery_config_version INTEGER NOT NULL,
|
|
61
|
+
destination_ref TEXT NOT NULL,
|
|
62
|
+
authorization_generation BIGINT NOT NULL,
|
|
63
|
+
semantic_fingerprint TEXT NOT NULL,
|
|
64
|
+
data JSONB NOT NULL,
|
|
65
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
66
|
+
FOREIGN KEY (account_id, destination_ref, authorization_generation)
|
|
67
|
+
REFERENCES adcp_reporting_destination_authorizations(account_id, destination_ref, generation),
|
|
68
|
+
UNIQUE (account_id, delivery_config_id, delivery_config_version)
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
CREATE TABLE IF NOT EXISTS adcp_reporting_materializations (
|
|
72
|
+
materialization_id TEXT PRIMARY KEY,
|
|
73
|
+
account_id TEXT NOT NULL,
|
|
74
|
+
configuration_id TEXT NOT NULL REFERENCES adcp_reporting_managed_bindings(configuration_id),
|
|
75
|
+
obligation_id TEXT NOT NULL REFERENCES adcp_reporting_obligations(obligation_id),
|
|
76
|
+
revision_id TEXT NOT NULL REFERENCES adcp_reporting_revisions(revision_id),
|
|
77
|
+
destination_ref TEXT NOT NULL,
|
|
78
|
+
authorization_generation BIGINT NOT NULL,
|
|
79
|
+
attempt INTEGER NOT NULL,
|
|
80
|
+
status TEXT NOT NULL,
|
|
81
|
+
data JSONB NOT NULL,
|
|
82
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
83
|
+
recorded_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
84
|
+
changed_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
85
|
+
lease_owner TEXT,
|
|
86
|
+
lease_generation BIGINT NOT NULL DEFAULT 0,
|
|
87
|
+
lease_expires_at TIMESTAMPTZ,
|
|
88
|
+
UNIQUE (configuration_id, revision_id, attempt),
|
|
89
|
+
CHECK (attempt > 0),
|
|
90
|
+
CHECK (status IN ('pending', 'available', 'delivered', 'failed'))
|
|
91
|
+
);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS adcp_reporting_materializations_claim
|
|
93
|
+
ON adcp_reporting_materializations (created_at, materialization_id) WHERE status = 'pending';
|
|
94
|
+
CREATE INDEX IF NOT EXISTS adcp_reporting_materializations_obligation
|
|
95
|
+
ON adcp_reporting_materializations (obligation_id, recorded_at, materialization_id);
|
|
96
|
+
CREATE UNIQUE INDEX IF NOT EXISTS adcp_reporting_materializations_success
|
|
97
|
+
ON adcp_reporting_materializations (configuration_id, revision_id)
|
|
98
|
+
WHERE status IN ('available', 'delivered');
|
|
99
|
+
|
|
100
|
+
CREATE TABLE IF NOT EXISTS adcp_reporting_receipts (
|
|
101
|
+
account_id TEXT NOT NULL,
|
|
102
|
+
consumer_id TEXT NOT NULL,
|
|
103
|
+
reporting_receipt_id TEXT NOT NULL,
|
|
104
|
+
receipt_kind TEXT NOT NULL,
|
|
105
|
+
subject_id TEXT NOT NULL,
|
|
106
|
+
supersedes_receipt_id TEXT,
|
|
107
|
+
is_current BOOLEAN NOT NULL,
|
|
108
|
+
semantic_fingerprint TEXT NOT NULL,
|
|
109
|
+
data JSONB NOT NULL,
|
|
110
|
+
received_at TIMESTAMPTZ NOT NULL,
|
|
111
|
+
recorded_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
112
|
+
PRIMARY KEY (account_id, consumer_id, reporting_receipt_id),
|
|
113
|
+
CHECK (receipt_kind IN ('revision', 'adjustment'))
|
|
114
|
+
);
|
|
115
|
+
CREATE UNIQUE INDEX IF NOT EXISTS adcp_reporting_receipts_current
|
|
116
|
+
ON adcp_reporting_receipts (account_id, consumer_id, receipt_kind, subject_id) WHERE is_current;
|
|
117
|
+
CREATE INDEX IF NOT EXISTS adcp_reporting_receipts_readback
|
|
118
|
+
ON adcp_reporting_receipts (account_id, consumer_id, recorded_at, reporting_receipt_id);
|
|
119
|
+
-- The as-of-cutoff leaf asks, per candidate, whether anything recorded by the
|
|
120
|
+
-- cutoff supersedes it. Without this that question is a scan of the caller's
|
|
121
|
+
-- whole receipt history.
|
|
122
|
+
CREATE INDEX IF NOT EXISTS adcp_reporting_receipts_supersedes
|
|
123
|
+
ON adcp_reporting_receipts (account_id, consumer_id, supersedes_receipt_id)
|
|
124
|
+
WHERE supersedes_receipt_id IS NOT NULL;
|
|
125
|
+
-- Every lifecycle read is "the receipts for this obligation's subjects". The
|
|
126
|
+
-- receipt table is global and its other indexes lead with account and
|
|
127
|
+
-- consumer, neither of which that question supplies, so each due obligation
|
|
128
|
+
-- re-scanned the whole receipt history.
|
|
129
|
+
CREATE INDEX IF NOT EXISTS adcp_reporting_receipts_subject
|
|
130
|
+
ON adcp_reporting_receipts (subject_id, receipt_kind, recorded_at);
|
|
131
|
+
|
|
132
|
+
CREATE TABLE IF NOT EXISTS adcp_reporting_receipt_batches (
|
|
133
|
+
account_id TEXT NOT NULL,
|
|
134
|
+
consumer_id TEXT NOT NULL,
|
|
135
|
+
idempotency_key TEXT NOT NULL,
|
|
136
|
+
request_fingerprint TEXT NOT NULL,
|
|
137
|
+
results JSONB NOT NULL,
|
|
138
|
+
recorded_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
139
|
+
PRIMARY KEY (account_id, consumer_id, idempotency_key)
|
|
140
|
+
);
|
|
141
|
+
-- Pruning used to ask "does any live replay row contain this receipt id" per
|
|
142
|
+
-- candidate, which no index could serve for a correlated operand. It now
|
|
143
|
+
-- expands the account's replay rows once instead, so no GIN index is needed
|
|
144
|
+
-- and the batches table is not burdened with maintaining one.
|
|
145
|
+
|
|
146
|
+
-- Agent-wide promises, durable and shared by every store instance and process
|
|
147
|
+
-- that talks to this database. An in-memory bound only constrains the process
|
|
148
|
+
-- that set it, so two replicas could each believe they were authoritative and
|
|
149
|
+
-- publish different windows over the same bindings.
|
|
150
|
+
CREATE TABLE IF NOT EXISTS adcp_reporting_managed_policy (
|
|
151
|
+
policy_key TEXT PRIMARY KEY,
|
|
152
|
+
advertised_recovery_window_seconds BIGINT,
|
|
153
|
+
advertised_status_retention_days BIGINT,
|
|
154
|
+
advertised_resource_retention_days BIGINT,
|
|
155
|
+
advertised_authorization_revocation_seconds BIGINT,
|
|
156
|
+
recorded_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
157
|
+
changed_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
|
|
158
|
+
);
|
|
159
|
+
-- Upgrade registries installed by earlier Managed Delivery release candidates.
|
|
160
|
+
ALTER TABLE adcp_reporting_managed_policy
|
|
161
|
+
ADD COLUMN IF NOT EXISTS advertised_resource_retention_days BIGINT;
|
|
162
|
+
ALTER TABLE adcp_reporting_managed_policy
|
|
163
|
+
ADD COLUMN IF NOT EXISTS advertised_authorization_revocation_seconds BIGINT;
|
|
164
|
+
-- Every policy read or adoption locks this durable row. Creating it in the
|
|
165
|
+
-- migration avoids a missing-row predicate gap on a fresh registry.
|
|
166
|
+
INSERT INTO adcp_reporting_managed_policy (policy_key)
|
|
167
|
+
VALUES ('agent')
|
|
168
|
+
ON CONFLICT (policy_key) DO NOTHING;
|
|
169
|
+
|
|
170
|
+
-- Permanent, compact identity for a receipt whose body has aged out.
|
|
171
|
+
-- Bodies are retained only through the advertised horizon, but identity is
|
|
172
|
+
-- forever: a pruned receipt_id must never bind different content later, and a
|
|
173
|
+
-- subject that reached a terminal accepted leaf must never reopen because the
|
|
174
|
+
-- row proving it was terminal has expired.
|
|
175
|
+
CREATE TABLE IF NOT EXISTS adcp_reporting_receipt_tombstones (
|
|
176
|
+
account_id TEXT NOT NULL,
|
|
177
|
+
consumer_id TEXT NOT NULL,
|
|
178
|
+
reporting_receipt_id TEXT NOT NULL,
|
|
179
|
+
receipt_kind TEXT NOT NULL,
|
|
180
|
+
subject_id TEXT NOT NULL,
|
|
181
|
+
status TEXT NOT NULL,
|
|
182
|
+
was_current BOOLEAN NOT NULL,
|
|
183
|
+
semantic_fingerprint TEXT NOT NULL,
|
|
184
|
+
pruned_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
185
|
+
PRIMARY KEY (account_id, consumer_id, reporting_receipt_id)
|
|
186
|
+
);
|
|
187
|
+
-- When the pruned receipt itself was recorded, and what it superseded. A
|
|
188
|
+
-- tombstone without them is a conclusion with no place in time: a historical
|
|
189
|
+
-- projection applied an acceptance that had not happened yet at its cutoff,
|
|
190
|
+
-- and a chain whose successor was pruned let the predecessor it superseded
|
|
191
|
+
-- come back as the live leaf. Nullable for rows written before this column
|
|
192
|
+
-- existed; readers fall back to pruned_at, which is never earlier.
|
|
193
|
+
ALTER TABLE adcp_reporting_receipt_tombstones
|
|
194
|
+
ADD COLUMN IF NOT EXISTS subject_recorded_at TIMESTAMPTZ;
|
|
195
|
+
ALTER TABLE adcp_reporting_receipt_tombstones
|
|
196
|
+
ADD COLUMN IF NOT EXISTS supersedes_receipt_id TEXT;
|
|
197
|
+
CREATE INDEX IF NOT EXISTS adcp_reporting_receipt_tombstones_supersedes
|
|
198
|
+
ON adcp_reporting_receipt_tombstones (account_id, consumer_id, supersedes_receipt_id)
|
|
199
|
+
WHERE supersedes_receipt_id IS NOT NULL;
|
|
200
|
+
CREATE INDEX IF NOT EXISTS adcp_reporting_receipt_tombstones_subject
|
|
201
|
+
ON adcp_reporting_receipt_tombstones (account_id, consumer_id, receipt_kind, subject_id);
|
|
202
|
+
|
|
203
|
+
-- Compact terminal state for a materialization whose row has been pruned.
|
|
204
|
+
-- Attempt history is control state, not evidence: without it a revision whose
|
|
205
|
+
-- attempts were exhausted, or which already succeeded, restarts at attempt 1
|
|
206
|
+
-- the moment its rows age out.
|
|
207
|
+
CREATE TABLE IF NOT EXISTS adcp_reporting_materialization_tombstones (
|
|
208
|
+
configuration_id TEXT NOT NULL,
|
|
209
|
+
revision_id TEXT NOT NULL,
|
|
210
|
+
account_id TEXT NOT NULL,
|
|
211
|
+
obligation_id TEXT NOT NULL,
|
|
212
|
+
highest_attempt INTEGER NOT NULL,
|
|
213
|
+
reached_success BOOLEAN NOT NULL,
|
|
214
|
+
pruned_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
215
|
+
-- When the delivery this records actually succeeded, so a projection at an
|
|
216
|
+
-- earlier cutoff does not read it as already delivered.
|
|
217
|
+
reached_success_at TIMESTAMPTZ,
|
|
218
|
+
PRIMARY KEY (configuration_id, revision_id)
|
|
219
|
+
);
|
|
220
|
+
-- These rows are permanent and every lifecycle projection reads them by
|
|
221
|
+
-- obligation and success. The primary key leads with configuration, which
|
|
222
|
+
-- that question does not supply, so the scan grew without bound for the life
|
|
223
|
+
-- of the deployment.
|
|
224
|
+
CREATE INDEX IF NOT EXISTS adcp_reporting_materialization_tombstones_obligation
|
|
225
|
+
ON adcp_reporting_materialization_tombstones (obligation_id, reached_success, reached_success_at);
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
ALTER TABLE adcp_reporting_destination_authorizations
|
|
229
|
+
ADD COLUMN IF NOT EXISTS cleanup_lease_issued_at TIMESTAMPTZ;
|
|
230
|
+
`.trim();
|
|
231
|
+
const GENERIC_RECEIPT_MESSAGE = "Receipt does not match authorized current reporting evidence";
|
|
232
|
+
const MAX_PLAN = 1e3;
|
|
233
|
+
const MAX_MATERIALIZATIONS_PER_ACCOUNT = 1e5;
|
|
234
|
+
const MAX_RECEIPT_BATCHES_PER_CONSUMER = 1e4;
|
|
235
|
+
const MAX_RECEIPTS_PER_CONSUMER = 1e5;
|
|
236
|
+
const MAX_RECEIPTS_PER_ARRAY = 100;
|
|
237
|
+
const MAX_RECEIPT_RESULTS = 100;
|
|
238
|
+
const MAX_MATERIALIZATION_ATTEMPTS = 5;
|
|
239
|
+
const REVOCATION_RETRY_BACKOFF_MS = 5e3;
|
|
240
|
+
const RECEIPT_BATCH_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
241
|
+
const MAX_RECEIPT_BATCH_RESULT_BYTES = 64 * 1024;
|
|
242
|
+
class PostgresReportingManagedDeliveryStore {
|
|
243
|
+
constructor(pool, options = {}) {
|
|
244
|
+
this.pool = pool;
|
|
245
|
+
if (options.statusRetentionDays !== void 0) positiveInteger(options.statusRetentionDays, "statusRetentionDays");
|
|
246
|
+
if (options.evidenceRetentionDays !== void 0) {
|
|
247
|
+
positiveInteger(options.evidenceRetentionDays, "evidenceRetentionDays");
|
|
248
|
+
const replayFloorDays = Math.ceil(RECEIPT_BATCH_RETENTION_MS / 864e5);
|
|
249
|
+
if (options.evidenceRetentionDays < replayFloorDays) {
|
|
250
|
+
throw new RangeError(
|
|
251
|
+
`evidenceRetentionDays must be at least the ${replayFloorDays}-day receipt replay retention`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (options.statusRetentionDays !== void 0 && options.evidenceRetentionDays < options.statusRetentionDays) {
|
|
255
|
+
throw new RangeError(
|
|
256
|
+
`evidenceRetentionDays must be at least the advertised statusRetentionDays (${options.statusRetentionDays}); pruning inside an advertised horizon breaks it`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (options.advertisedRecoveryWindowSeconds !== void 0) {
|
|
261
|
+
nonnegativeSafeInteger(options.advertisedRecoveryWindowSeconds, "advertisedRecoveryWindowSeconds");
|
|
262
|
+
}
|
|
263
|
+
this.pendingRecoveryWindowSeconds = options.advertisedRecoveryWindowSeconds;
|
|
264
|
+
this.statusRetentionDays = options.statusRetentionDays;
|
|
265
|
+
this.evidenceRetentionDays = options.evidenceRetentionDays;
|
|
266
|
+
}
|
|
267
|
+
pool;
|
|
268
|
+
evidenceRetentionDays;
|
|
269
|
+
statusRetentionDays;
|
|
270
|
+
pendingRecoveryWindowSeconds;
|
|
271
|
+
policyRegistered = false;
|
|
272
|
+
/**
|
|
273
|
+
* Durably records the agent-wide advertised recovery window.
|
|
274
|
+
*
|
|
275
|
+
* Written to the database, not held in memory: the promise is agent-wide,
|
|
276
|
+
* so a second store instance — in this process or another replica — must be
|
|
277
|
+
* held to the same value, and `installBinding` must read it inside its own
|
|
278
|
+
* transaction rather than trusting whatever the local object happens to
|
|
279
|
+
* know. Multiple replicas monotonically retain the smallest registered
|
|
280
|
+
* maximum: a shorter recovery promise is stronger and can never be widened
|
|
281
|
+
* by a later runtime.
|
|
282
|
+
*/
|
|
283
|
+
async adoptAdvertisedRecoveryWindowSeconds(seconds) {
|
|
284
|
+
nonnegativeSafeInteger(seconds, "automatedRecoveryWindowSeconds");
|
|
285
|
+
await this.upsertPolicy("advertised_recovery_window_seconds", seconds, {
|
|
286
|
+
strongest: Math.min,
|
|
287
|
+
// Checked inside the same transaction that would persist it. Writing
|
|
288
|
+
// first and validating afterwards poisoned the registry: a rejected
|
|
289
|
+
// 60s startup against an installed 900s binding still left a durable
|
|
290
|
+
// 60, and the correct 900s restart was then refused as a conflict.
|
|
291
|
+
validate: (client, effective) => this.validateRecoveryWindow(client, effective.advertised_recovery_window_seconds)
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Durably records the advertised `status_retention_days`.
|
|
296
|
+
*
|
|
297
|
+
* Retention is enforced against the widest promise any runtime registered
|
|
298
|
+
* over this database, so a store configured with a short evidence retention
|
|
299
|
+
* cannot prune inside a horizon a differently configured runtime published.
|
|
300
|
+
*/
|
|
301
|
+
async adoptAdvertisedStatusRetentionDays(days) {
|
|
302
|
+
positiveInteger(days, "statusRetentionDays");
|
|
303
|
+
await this.upsertPolicy("advertised_status_retention_days", days, {
|
|
304
|
+
strongest: Math.max
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/** Registers the complete capability policy under one lock and transaction. */
|
|
308
|
+
async adoptAdvertisedPolicies(policy) {
|
|
309
|
+
nonnegativeSafeInteger(policy.automatedRecoveryWindowSeconds, "automatedRecoveryWindowSeconds");
|
|
310
|
+
positiveInteger(policy.statusRetentionDays, "statusRetentionDays");
|
|
311
|
+
positiveInteger(policy.resourceRetentionDays, "resourceRetentionDays");
|
|
312
|
+
nonnegativeSafeInteger(policy.authorizationRevocationSeconds, "authorizationRevocationSeconds");
|
|
313
|
+
const adopted = await this.upsertPolicies(
|
|
314
|
+
[
|
|
315
|
+
{
|
|
316
|
+
column: "advertised_recovery_window_seconds",
|
|
317
|
+
value: policy.automatedRecoveryWindowSeconds,
|
|
318
|
+
strongest: Math.min
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
column: "advertised_status_retention_days",
|
|
322
|
+
value: policy.statusRetentionDays,
|
|
323
|
+
strongest: Math.max
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
column: "advertised_resource_retention_days",
|
|
327
|
+
value: policy.resourceRetentionDays,
|
|
328
|
+
strongest: Math.max
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
column: "advertised_authorization_revocation_seconds",
|
|
332
|
+
value: policy.authorizationRevocationSeconds,
|
|
333
|
+
strongest: Math.min
|
|
334
|
+
}
|
|
335
|
+
],
|
|
336
|
+
(client, effective) => this.validateRecoveryWindow(client, effective.advertised_recovery_window_seconds)
|
|
337
|
+
);
|
|
338
|
+
return {
|
|
339
|
+
automatedRecoveryWindowSeconds: adopted.advertised_recovery_window_seconds,
|
|
340
|
+
statusRetentionDays: adopted.advertised_status_retention_days,
|
|
341
|
+
resourceRetentionDays: adopted.advertised_resource_retention_days,
|
|
342
|
+
authorizationRevocationSeconds: adopted.advertised_authorization_revocation_seconds
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Flushes constructor-supplied promises into the durable registry.
|
|
347
|
+
*
|
|
348
|
+
* Called from every entry point that depends on them, so a store built with
|
|
349
|
+
* options behaves identically to one a runtime adopted into.
|
|
350
|
+
*/
|
|
351
|
+
async ensurePolicyRegistered() {
|
|
352
|
+
if (this.policyRegistered) return;
|
|
353
|
+
if (this.pendingRecoveryWindowSeconds !== void 0 && this.statusRetentionDays !== void 0) {
|
|
354
|
+
await this.upsertPolicies(
|
|
355
|
+
[
|
|
356
|
+
{
|
|
357
|
+
column: "advertised_recovery_window_seconds",
|
|
358
|
+
value: this.pendingRecoveryWindowSeconds,
|
|
359
|
+
strongest: Math.min
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
column: "advertised_status_retention_days",
|
|
363
|
+
value: this.statusRetentionDays,
|
|
364
|
+
strongest: Math.max
|
|
365
|
+
}
|
|
366
|
+
],
|
|
367
|
+
(client, effective) => this.validateRecoveryWindow(client, effective.advertised_recovery_window_seconds)
|
|
368
|
+
);
|
|
369
|
+
} else if (this.pendingRecoveryWindowSeconds !== void 0) {
|
|
370
|
+
await this.adoptAdvertisedRecoveryWindowSeconds(this.pendingRecoveryWindowSeconds);
|
|
371
|
+
} else if (this.statusRetentionDays !== void 0) {
|
|
372
|
+
await this.adoptAdvertisedStatusRetentionDays(this.statusRetentionDays);
|
|
373
|
+
}
|
|
374
|
+
this.policyRegistered = true;
|
|
375
|
+
}
|
|
376
|
+
async upsertPolicy(column, value, options) {
|
|
377
|
+
await this.upsertPolicies([{ column, value, strongest: options.strongest }], options.validate);
|
|
378
|
+
}
|
|
379
|
+
async upsertPolicies(policies, validate) {
|
|
380
|
+
return this.transaction(async (client) => {
|
|
381
|
+
const current = await this.readPolicyRow(client, "update");
|
|
382
|
+
const effective = { ...current };
|
|
383
|
+
for (const policy of policies) {
|
|
384
|
+
const existing = current[policy.column];
|
|
385
|
+
effective[policy.column] = existing === null ? policy.value : policy.strongest(existing, policy.value);
|
|
386
|
+
}
|
|
387
|
+
await validate?.(client, effective);
|
|
388
|
+
for (const policy of policies) {
|
|
389
|
+
await client.query(
|
|
390
|
+
`UPDATE adcp_reporting_managed_policy SET ${policy.column} = $1,
|
|
391
|
+
changed_at = clock_timestamp() WHERE policy_key = 'agent'`,
|
|
392
|
+
[effective[policy.column]]
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
return effective;
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
async validateRecoveryWindow(client, seconds) {
|
|
399
|
+
const widest = await client.query(
|
|
400
|
+
`WITH installed AS (
|
|
401
|
+
SELECT configuration.data->'schedule'->>'recoveryWindowMilliseconds' AS milliseconds
|
|
402
|
+
FROM adcp_reporting_managed_bindings binding
|
|
403
|
+
JOIN adcp_reporting_configurations configuration
|
|
404
|
+
ON configuration.configuration_id = binding.configuration_id
|
|
405
|
+
)
|
|
406
|
+
SELECT COUNT(*)::text AS installed,
|
|
407
|
+
COUNT(*) FILTER (WHERE milliseconds ~ '^[0-9]+$')::text AS valid,
|
|
408
|
+
MAX(CASE WHEN milliseconds ~ '^[0-9]+$'
|
|
409
|
+
THEN CEIL(milliseconds::numeric / 1000) END)::text AS widest
|
|
410
|
+
FROM installed`
|
|
411
|
+
);
|
|
412
|
+
const row = widest.rows[0];
|
|
413
|
+
if (row?.installed !== row?.valid) {
|
|
414
|
+
throw new Error("Every installed managed Core configuration must have a usable non-negative recovery window");
|
|
415
|
+
}
|
|
416
|
+
const widestInstalled = Number(row?.widest ?? 0);
|
|
417
|
+
if (!Number.isSafeInteger(widestInstalled)) {
|
|
418
|
+
throw new Error("Installed managed Core recovery windows exceed the supported safe-integer range");
|
|
419
|
+
}
|
|
420
|
+
if (seconds < widestInstalled) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
`automatedRecoveryWindowSeconds must be at least the widest installed managed Core recovery window (${widestInstalled}s); advertising ${seconds}s would promise a recovery bound this deployment does not keep for every tenant`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async readPolicyRow(client, lock) {
|
|
427
|
+
const lockClause = lock === "share" ? " FOR SHARE" : lock === "update" ? " FOR UPDATE" : "";
|
|
428
|
+
const result = await client.query(
|
|
429
|
+
`SELECT advertised_recovery_window_seconds::text AS recovery,
|
|
430
|
+
advertised_status_retention_days::text AS status,
|
|
431
|
+
advertised_resource_retention_days::text AS resource,
|
|
432
|
+
advertised_authorization_revocation_seconds::text AS revocation
|
|
433
|
+
FROM adcp_reporting_managed_policy WHERE policy_key = 'agent'${lockClause}`
|
|
434
|
+
);
|
|
435
|
+
const row = result.rows[0];
|
|
436
|
+
if (!row) {
|
|
437
|
+
throw new Error(
|
|
438
|
+
"Managed reporting policy sentinel is missing; apply REPORTING_MANAGED_DELIVERY_MIGRATION before use"
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
advertised_recovery_window_seconds: policyNumber(row?.recovery),
|
|
443
|
+
advertised_status_retention_days: policyNumber(row?.status),
|
|
444
|
+
advertised_resource_retention_days: policyNumber(row?.resource),
|
|
445
|
+
advertised_authorization_revocation_seconds: policyNumber(row?.revocation)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
async readPolicy(client, lock) {
|
|
449
|
+
const policy = await this.readPolicyRow(client, lock);
|
|
450
|
+
return {
|
|
451
|
+
recoveryWindowSeconds: policy.advertised_recovery_window_seconds,
|
|
452
|
+
statusRetentionDays: policy.advertised_status_retention_days,
|
|
453
|
+
resourceRetentionDays: policy.advertised_resource_retention_days,
|
|
454
|
+
authorizationRevocationSeconds: policy.advertised_authorization_revocation_seconds
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
/** SQL fragment scoping a count to the active retention window, if one is set. */
|
|
458
|
+
activeScope(column) {
|
|
459
|
+
return this.evidenceRetentionDays === void 0 ? "" : ` AND ${column} >= clock_timestamp() - (${this.evidenceRetentionDays}::bigint * INTERVAL '1 day')`;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Deletes managed evidence that has aged out of `evidenceRetentionDays`.
|
|
463
|
+
*
|
|
464
|
+
* Only ever removes evidence the deployment no longer promises: nothing
|
|
465
|
+
* inside the retention window, nothing still `pending` or leased, and no
|
|
466
|
+
* receipt batch inside its own replay retention. Immutable evidence stays
|
|
467
|
+
* immutable while it is retained — this frees capacity at the far end of the
|
|
468
|
+
* window rather than rewriting anything. Returns what it removed so a
|
|
469
|
+
* scheduler can page through with `limit`.
|
|
470
|
+
*/
|
|
471
|
+
async pruneExpiredEvidence(input) {
|
|
472
|
+
if (this.evidenceRetentionDays === void 0) {
|
|
473
|
+
throw new Error("pruneExpiredEvidence requires PostgresReportingManagedDeliveryStore({ evidenceRetentionDays })");
|
|
474
|
+
}
|
|
475
|
+
await this.ensurePolicyRegistered();
|
|
476
|
+
const limit = input.limit ?? 1e3;
|
|
477
|
+
positiveInteger(limit, "limit");
|
|
478
|
+
const days = this.evidenceRetentionDays;
|
|
479
|
+
return this.transaction(async (client) => {
|
|
480
|
+
const registered = await this.readPolicy(client, "share");
|
|
481
|
+
const retentionFloor = Math.max(registered.statusRetentionDays ?? 0, registered.resourceRetentionDays ?? 0);
|
|
482
|
+
if (days < retentionFloor) {
|
|
483
|
+
throw new Error(
|
|
484
|
+
`evidenceRetentionDays ${days} is shorter than the strongest advertised status/resource retention of ${retentionFloor} days registered for this database; pruning would cut inside an advertised horizon`
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
const cutoff = (await client.query(
|
|
488
|
+
`SELECT (clock_timestamp() - ($1::bigint * INTERVAL '1 day'))::text AS cutoff`,
|
|
489
|
+
[days]
|
|
490
|
+
)).rows[0].cutoff;
|
|
491
|
+
const doomedReceipts = await client.query(
|
|
492
|
+
`WITH live_referenced AS (
|
|
493
|
+
SELECT DISTINCT batch.consumer_id,
|
|
494
|
+
COALESCE(
|
|
495
|
+
elem ->> 'id',
|
|
496
|
+
elem ->> 'reporting_receipt_id',
|
|
497
|
+
elem -> 'receipt' ->> 'reporting_receipt_id',
|
|
498
|
+
elem -> 'adjustment_receipt' ->> 'reporting_receipt_id'
|
|
499
|
+
) AS reporting_receipt_id
|
|
500
|
+
FROM adcp_reporting_receipt_batches batch,
|
|
501
|
+
LATERAL jsonb_array_elements(batch.results) elem
|
|
502
|
+
-- Only a replay row still inside its own retention pins anything.
|
|
503
|
+
-- Selection now runs before the batch delete, so without this an
|
|
504
|
+
-- already-expired row kept holding its receipts alive forever.
|
|
505
|
+
WHERE batch.account_id = $1
|
|
506
|
+
AND batch.recorded_at >= clock_timestamp() - ($4::bigint * INTERVAL '1 millisecond')
|
|
507
|
+
)
|
|
508
|
+
SELECT receipt.consumer_id, receipt.reporting_receipt_id
|
|
509
|
+
FROM adcp_reporting_receipts receipt
|
|
510
|
+
WHERE receipt.account_id = $1
|
|
511
|
+
AND receipt.recorded_at < $3::timestamptz
|
|
512
|
+
AND NOT EXISTS (
|
|
513
|
+
SELECT 1 FROM live_referenced
|
|
514
|
+
WHERE live_referenced.consumer_id = receipt.consumer_id
|
|
515
|
+
AND live_referenced.reporting_receipt_id = receipt.reporting_receipt_id
|
|
516
|
+
)
|
|
517
|
+
-- An acceptance outlives its own age while the resource it
|
|
518
|
+
-- accepts is still readable. Pruning it sooner produced a period
|
|
519
|
+
-- that reads complete with no receipt to show for it.
|
|
520
|
+
--
|
|
521
|
+
-- An adjustment receipt names no materialization at all: it
|
|
522
|
+
-- names the revision it adjusts. Matching only on
|
|
523
|
+
-- reporting_materialization_id therefore held every revision
|
|
524
|
+
-- acceptance and no adjustment acceptance, so a period whose
|
|
525
|
+
-- revision resource was still readable lost the adjustment
|
|
526
|
+
-- evidence behind its own complete.
|
|
527
|
+
AND NOT EXISTS (
|
|
528
|
+
SELECT 1 FROM adcp_reporting_materializations live
|
|
529
|
+
WHERE live.account_id = receipt.account_id
|
|
530
|
+
AND (
|
|
531
|
+
live.materialization_id = receipt.data ->> 'reporting_materialization_id'
|
|
532
|
+
OR live.revision_id = receipt.data ->> 'adjusts_reporting_revision_id'
|
|
533
|
+
)
|
|
534
|
+
AND (live.data -> 'resource' ->> 'expires_at')::timestamptz > clock_timestamp()
|
|
535
|
+
)
|
|
536
|
+
ORDER BY receipt.recorded_at
|
|
537
|
+
LIMIT $2`,
|
|
538
|
+
[input.account_id, limit, cutoff, RECEIPT_BATCH_RETENTION_MS]
|
|
539
|
+
);
|
|
540
|
+
const doomedMaterializations = await client.query(
|
|
541
|
+
`SELECT materialization.materialization_id
|
|
542
|
+
FROM adcp_reporting_materializations materialization
|
|
543
|
+
WHERE materialization.account_id = $1
|
|
544
|
+
AND materialization.recorded_at < $3::timestamptz
|
|
545
|
+
AND materialization.status <> 'pending'
|
|
546
|
+
AND materialization.lease_owner IS NULL
|
|
547
|
+
AND COALESCE(
|
|
548
|
+
(materialization.data -> 'resource' ->> 'expires_at')::timestamptz <= clock_timestamp(),
|
|
549
|
+
true)
|
|
550
|
+
-- No SURVIVING receipt may still name it. Selection runs before
|
|
551
|
+
-- the receipt delete now, so a receipt already doomed in this
|
|
552
|
+
-- same pass must not keep its materialization alive forever.
|
|
553
|
+
AND NOT EXISTS (
|
|
554
|
+
SELECT 1 FROM adcp_reporting_receipts receipt
|
|
555
|
+
WHERE receipt.account_id = materialization.account_id
|
|
556
|
+
AND receipt.data ->> 'reporting_materialization_id' = materialization.materialization_id
|
|
557
|
+
AND NOT EXISTS (
|
|
558
|
+
SELECT 1 FROM unnest($4::text[], $5::text[]) AS doomed(consumer_id, reporting_receipt_id)
|
|
559
|
+
WHERE doomed.consumer_id = receipt.consumer_id
|
|
560
|
+
AND doomed.reporting_receipt_id = receipt.reporting_receipt_id
|
|
561
|
+
)
|
|
562
|
+
)
|
|
563
|
+
ORDER BY materialization.recorded_at
|
|
564
|
+
LIMIT $2`,
|
|
565
|
+
[
|
|
566
|
+
input.account_id,
|
|
567
|
+
limit,
|
|
568
|
+
cutoff,
|
|
569
|
+
doomedReceipts.rows.map((row) => row.consumer_id),
|
|
570
|
+
doomedReceipts.rows.map((row) => row.reporting_receipt_id)
|
|
571
|
+
]
|
|
572
|
+
);
|
|
573
|
+
await advisoryLock(client, accountLock(input.account_id));
|
|
574
|
+
const batches = await client.query(
|
|
575
|
+
`DELETE FROM adcp_reporting_receipt_batches
|
|
576
|
+
WHERE account_id = $1
|
|
577
|
+
AND recorded_at < clock_timestamp() - ($2::bigint * INTERVAL '1 millisecond')`,
|
|
578
|
+
[input.account_id, RECEIPT_BATCH_RETENTION_MS]
|
|
579
|
+
);
|
|
580
|
+
const doomedConsumerIds = doomedReceipts.rows.map((row) => row.consumer_id);
|
|
581
|
+
const doomedReceiptIds = doomedReceipts.rows.map((row) => row.reporting_receipt_id);
|
|
582
|
+
const receipts = await client.query(
|
|
583
|
+
`WITH doomed_keys AS (
|
|
584
|
+
SELECT * FROM unnest($2::text[], $3::text[]) AS pair(consumer_id, reporting_receipt_id)
|
|
585
|
+
), live_referenced AS (
|
|
586
|
+
-- Selection ran unlocked, so its answer is a proposal, not a
|
|
587
|
+
-- verdict: a replay could commit against a candidate between the
|
|
588
|
+
-- select and this delete, and the delete then removed the receipt
|
|
589
|
+
-- that replay had just promised to reproduce. Re-expanding the
|
|
590
|
+
-- live replay rows under the lock is one pass, and it is narrowed
|
|
591
|
+
-- to the candidates consumers rather than the whole account, so
|
|
592
|
+
-- the lock still holds no per-candidate join filter.
|
|
593
|
+
SELECT DISTINCT batch.consumer_id,
|
|
594
|
+
COALESCE(
|
|
595
|
+
elem ->> 'id',
|
|
596
|
+
elem ->> 'reporting_receipt_id',
|
|
597
|
+
elem -> 'receipt' ->> 'reporting_receipt_id',
|
|
598
|
+
elem -> 'adjustment_receipt' ->> 'reporting_receipt_id'
|
|
599
|
+
) AS reporting_receipt_id
|
|
600
|
+
FROM adcp_reporting_receipt_batches batch,
|
|
601
|
+
LATERAL jsonb_array_elements(batch.results) elem
|
|
602
|
+
WHERE batch.account_id = $1
|
|
603
|
+
AND batch.consumer_id = ANY($2::text[])
|
|
604
|
+
AND batch.recorded_at >= clock_timestamp() - ($4::bigint * INTERVAL '1 millisecond')
|
|
605
|
+
), doomed AS (
|
|
606
|
+
SELECT receipt.* FROM adcp_reporting_receipts receipt
|
|
607
|
+
JOIN doomed_keys ON doomed_keys.consumer_id = receipt.consumer_id
|
|
608
|
+
AND doomed_keys.reporting_receipt_id = receipt.reporting_receipt_id
|
|
609
|
+
WHERE receipt.account_id = $1
|
|
610
|
+
AND receipt.recorded_at < $5::timestamptz
|
|
611
|
+
AND NOT EXISTS (
|
|
612
|
+
SELECT 1 FROM live_referenced
|
|
613
|
+
WHERE live_referenced.consumer_id = receipt.consumer_id
|
|
614
|
+
AND live_referenced.reporting_receipt_id = receipt.reporting_receipt_id
|
|
615
|
+
)
|
|
616
|
+
AND NOT EXISTS (
|
|
617
|
+
SELECT 1 FROM adcp_reporting_materializations live
|
|
618
|
+
WHERE live.account_id = receipt.account_id
|
|
619
|
+
AND (
|
|
620
|
+
live.materialization_id = receipt.data ->> 'reporting_materialization_id'
|
|
621
|
+
OR live.revision_id = receipt.data ->> 'adjusts_reporting_revision_id'
|
|
622
|
+
)
|
|
623
|
+
AND (live.data -> 'resource' ->> 'expires_at')::timestamptz > clock_timestamp()
|
|
624
|
+
)
|
|
625
|
+
), superseded AS (
|
|
626
|
+
-- Successive prunes each tombstoned whatever was current at the
|
|
627
|
+
-- time, so a subject accumulated several current-leaf tombstones:
|
|
628
|
+
-- prune R1 while current, record R2 over it, prune R2. Reading
|
|
629
|
+
-- back, whichever the planner returned first answered for the
|
|
630
|
+
-- chain, and picking R1 rejected R3's legitimate succession to
|
|
631
|
+
-- R2. A subject has exactly one leaf, so demote the older
|
|
632
|
+
-- tombstones the moment a newer leaf is recorded.
|
|
633
|
+
UPDATE adcp_reporting_receipt_tombstones prior
|
|
634
|
+
SET was_current = false
|
|
635
|
+
FROM doomed
|
|
636
|
+
WHERE doomed.is_current
|
|
637
|
+
AND prior.account_id = doomed.account_id
|
|
638
|
+
AND prior.consumer_id = doomed.consumer_id
|
|
639
|
+
AND prior.receipt_kind = doomed.receipt_kind
|
|
640
|
+
AND prior.subject_id = doomed.subject_id
|
|
641
|
+
AND prior.reporting_receipt_id <> doomed.reporting_receipt_id
|
|
642
|
+
AND prior.was_current
|
|
643
|
+
), tombstoned AS (
|
|
644
|
+
INSERT INTO adcp_reporting_receipt_tombstones
|
|
645
|
+
(account_id, consumer_id, reporting_receipt_id, receipt_kind, subject_id,
|
|
646
|
+
status, was_current, semantic_fingerprint, subject_recorded_at, supersedes_receipt_id)
|
|
647
|
+
SELECT account_id, consumer_id, reporting_receipt_id, receipt_kind, subject_id,
|
|
648
|
+
COALESCE(data ->> 'status', 'unknown'), is_current, semantic_fingerprint,
|
|
649
|
+
recorded_at, supersedes_receipt_id
|
|
650
|
+
FROM doomed
|
|
651
|
+
ON CONFLICT (account_id, consumer_id, reporting_receipt_id) DO NOTHING
|
|
652
|
+
)
|
|
653
|
+
DELETE FROM adcp_reporting_receipts target USING doomed
|
|
654
|
+
WHERE target.account_id = doomed.account_id
|
|
655
|
+
AND target.consumer_id = doomed.consumer_id
|
|
656
|
+
AND target.reporting_receipt_id = doomed.reporting_receipt_id`,
|
|
657
|
+
[input.account_id, doomedConsumerIds, doomedReceiptIds, RECEIPT_BATCH_RETENTION_MS, cutoff]
|
|
658
|
+
);
|
|
659
|
+
const materializations = await client.query(
|
|
660
|
+
`WITH referenced AS (
|
|
661
|
+
-- Doomed receipts are already gone above, so a hit here is a
|
|
662
|
+
-- survivor: either one this pass never selected, or one a
|
|
663
|
+
-- concurrent sync recorded after selection read the table.
|
|
664
|
+
SELECT DISTINCT receipt.data ->> 'reporting_materialization_id' AS materialization_id
|
|
665
|
+
FROM adcp_reporting_receipts receipt
|
|
666
|
+
WHERE receipt.account_id = $2
|
|
667
|
+
AND receipt.data ->> 'reporting_materialization_id' = ANY($1::text[])
|
|
668
|
+
), doomed AS (
|
|
669
|
+
SELECT materialization.* FROM adcp_reporting_materializations materialization
|
|
670
|
+
WHERE materialization.materialization_id = ANY($1::text[])
|
|
671
|
+
AND materialization.account_id = $2
|
|
672
|
+
-- Every selection predicate re-asserted under the lock. A
|
|
673
|
+
-- claim, a settle or a re-record between the select and here
|
|
674
|
+
-- makes the row live again.
|
|
675
|
+
AND materialization.recorded_at < $3::timestamptz
|
|
676
|
+
AND materialization.status <> 'pending'
|
|
677
|
+
AND materialization.lease_owner IS NULL
|
|
678
|
+
AND COALESCE(
|
|
679
|
+
(materialization.data -> 'resource' ->> 'expires_at')::timestamptz <= clock_timestamp(),
|
|
680
|
+
true)
|
|
681
|
+
AND NOT EXISTS (
|
|
682
|
+
SELECT 1 FROM referenced
|
|
683
|
+
WHERE referenced.materialization_id = materialization.materialization_id
|
|
684
|
+
)
|
|
685
|
+
), tombstoned AS (
|
|
686
|
+
INSERT INTO adcp_reporting_materialization_tombstones
|
|
687
|
+
(configuration_id, revision_id, account_id, obligation_id, highest_attempt, reached_success,
|
|
688
|
+
reached_success_at)
|
|
689
|
+
SELECT configuration_id, revision_id, account_id, obligation_id,
|
|
690
|
+
MAX(attempt), BOOL_OR(status IN ('available', 'delivered')),
|
|
691
|
+
MIN(changed_at) FILTER (WHERE status IN ('available', 'delivered'))
|
|
692
|
+
FROM doomed GROUP BY configuration_id, revision_id, account_id, obligation_id
|
|
693
|
+
ON CONFLICT (configuration_id, revision_id) DO UPDATE SET
|
|
694
|
+
highest_attempt = GREATEST(
|
|
695
|
+
adcp_reporting_materialization_tombstones.highest_attempt, EXCLUDED.highest_attempt),
|
|
696
|
+
reached_success =
|
|
697
|
+
adcp_reporting_materialization_tombstones.reached_success OR EXCLUDED.reached_success,
|
|
698
|
+
-- The earliest success is what a historical cutoff must compare
|
|
699
|
+
-- against: a later one does not make the delivery newer.
|
|
700
|
+
reached_success_at = LEAST(
|
|
701
|
+
adcp_reporting_materialization_tombstones.reached_success_at, EXCLUDED.reached_success_at)
|
|
702
|
+
)
|
|
703
|
+
DELETE FROM adcp_reporting_materializations target USING doomed
|
|
704
|
+
WHERE target.materialization_id = doomed.materialization_id`,
|
|
705
|
+
[doomedMaterializations.rows.map((row) => row.materialization_id), input.account_id, cutoff]
|
|
706
|
+
);
|
|
707
|
+
return {
|
|
708
|
+
materializations: materializations.rowCount ?? 0,
|
|
709
|
+
receipts: receipts.rowCount ?? 0,
|
|
710
|
+
batches: batches.rowCount ?? 0
|
|
711
|
+
};
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
async probe(coreStore) {
|
|
715
|
+
const authority = coreStore[import_types.REPORTING_LEDGER_AUTHORITY];
|
|
716
|
+
if (!authority) {
|
|
717
|
+
throw new Error("Managed reporting requires a Core store that exposes REPORTING_LEDGER_AUTHORITY");
|
|
718
|
+
}
|
|
719
|
+
if (authority.substrate !== this.pool) {
|
|
720
|
+
throw new Error("Managed reporting Core and add-on stores must share the same PostgreSQL pool");
|
|
721
|
+
}
|
|
722
|
+
if (authority.managedDelivery !== true) {
|
|
723
|
+
throw new Error("Managed reporting requires PostgresReportingLedgerStore({ managedDelivery: true })");
|
|
724
|
+
}
|
|
725
|
+
await this.ensurePolicyRegistered();
|
|
726
|
+
const result = await this.query(
|
|
727
|
+
`SELECT to_regclass('adcp_reporting_managed_bindings') IS NOT NULL
|
|
728
|
+
AND to_regclass('adcp_reporting_materializations') IS NOT NULL
|
|
729
|
+
AND to_regclass('adcp_reporting_receipts') IS NOT NULL AS ready`
|
|
730
|
+
);
|
|
731
|
+
if (result.rows[0]?.ready !== true) {
|
|
732
|
+
throw new Error(
|
|
733
|
+
"Managed reporting schema is unavailable; apply REPORTING_MANAGED_DELIVERY_MIGRATION after REPORTING_LEDGER_MIGRATION"
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
async listInstalledRecoveryWindowSeconds() {
|
|
739
|
+
const result = await this.query(
|
|
740
|
+
`SELECT DISTINCT configuration.data->'schedule'->>'recoveryWindowMilliseconds' AS milliseconds
|
|
741
|
+
FROM adcp_reporting_managed_bindings binding
|
|
742
|
+
JOIN adcp_reporting_configurations configuration
|
|
743
|
+
ON configuration.configuration_id = binding.configuration_id
|
|
744
|
+
ORDER BY milliseconds`
|
|
745
|
+
);
|
|
746
|
+
return result.rows.map((row) => {
|
|
747
|
+
const milliseconds = Number(row.milliseconds);
|
|
748
|
+
if (!Number.isSafeInteger(milliseconds) || milliseconds < 0) {
|
|
749
|
+
throw new Error("Installed managed Core recovery windows must be non-negative");
|
|
750
|
+
}
|
|
751
|
+
return Math.ceil(milliseconds / 1e3);
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
async authorizeDestination(input) {
|
|
755
|
+
positiveInteger(input.generation, "generation");
|
|
756
|
+
await this.transaction(async (client) => {
|
|
757
|
+
await advisoryLock(client, accountLock(input.account_id));
|
|
758
|
+
await advisoryLock(client, authLock(input.account_id, input.destination_ref));
|
|
759
|
+
const current = await client.query(
|
|
760
|
+
`SELECT generation::text FROM adcp_reporting_destination_authorizations
|
|
761
|
+
WHERE account_id = $1 AND destination_ref = $2 AND revoked_at IS NULL`,
|
|
762
|
+
[input.account_id, input.destination_ref]
|
|
763
|
+
);
|
|
764
|
+
if (current.rowCount) {
|
|
765
|
+
if (Number(current.rows[0].generation) === input.generation) return;
|
|
766
|
+
throw new Error("A destination authorization generation is already current");
|
|
767
|
+
}
|
|
768
|
+
const latest = await client.query(
|
|
769
|
+
`SELECT MAX(generation)::text AS generation FROM adcp_reporting_destination_authorizations
|
|
770
|
+
WHERE account_id = $1 AND destination_ref = $2`,
|
|
771
|
+
[input.account_id, input.destination_ref]
|
|
772
|
+
);
|
|
773
|
+
if (input.generation <= Number(latest.rows[0]?.generation ?? 0)) {
|
|
774
|
+
throw new Error("Destination authorization generation must increase after revocation");
|
|
775
|
+
}
|
|
776
|
+
await client.query(
|
|
777
|
+
// Both ends of the authorization window come from the committing
|
|
778
|
+
// database. Mixing a caller's `authorized_at` with a DB-clock
|
|
779
|
+
// `revoked_at` also violates the table's own ordering check whenever
|
|
780
|
+
// the caller's clock runs ahead.
|
|
781
|
+
`INSERT INTO adcp_reporting_destination_authorizations
|
|
782
|
+
(account_id, destination_ref, generation, authorized_at, data)
|
|
783
|
+
VALUES ($1, $2, $3, clock_timestamp(),
|
|
784
|
+
$4::jsonb || jsonb_build_object(
|
|
785
|
+
'authorized_at', ${rfc3339Micro("clock_timestamp()")},
|
|
786
|
+
'requested_authorized_at', $5::text))`,
|
|
787
|
+
[input.account_id, input.destination_ref, input.generation, JSON.stringify(input), input.authorized_at]
|
|
788
|
+
);
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
async revokeDestination(input) {
|
|
792
|
+
return this.transaction(async (client) => {
|
|
793
|
+
await advisoryLock(client, accountLock(input.account_id));
|
|
794
|
+
await advisoryLock(client, authLock(input.account_id, input.destination_ref));
|
|
795
|
+
const updated = await client.query(
|
|
796
|
+
// The SLA is measured from this instant and evaluated against
|
|
797
|
+
// clock_timestamp() everywhere else, so recording a caller's host
|
|
798
|
+
// clock let a fast host shorten the promised window and a slow one
|
|
799
|
+
// extend it. The caller's value is kept only as stated intent.
|
|
800
|
+
`UPDATE adcp_reporting_destination_authorizations
|
|
801
|
+
SET revoked_at = clock_timestamp(),
|
|
802
|
+
changed_at = clock_timestamp(),
|
|
803
|
+
data = data || jsonb_build_object(
|
|
804
|
+
'revoked_at', ${rfc3339Micro("clock_timestamp()")},
|
|
805
|
+
'requested_revoked_at', $4::text
|
|
806
|
+
)
|
|
807
|
+
WHERE account_id = $1 AND destination_ref = $2 AND generation = $3
|
|
808
|
+
AND revoked_at IS NULL`,
|
|
809
|
+
[input.account_id, input.destination_ref, input.generation, input.revoked_at]
|
|
810
|
+
);
|
|
811
|
+
await client.query(
|
|
812
|
+
`UPDATE adcp_reporting_materializations SET
|
|
813
|
+
status = 'failed',
|
|
814
|
+
data = data || jsonb_build_object(
|
|
815
|
+
'status', 'failed',
|
|
816
|
+
'failed_at', ${rfc3339Micro("clock_timestamp()")},
|
|
817
|
+
'failure_code', 'AUTHORIZATION_REVOKED'
|
|
818
|
+
),
|
|
819
|
+
changed_at = clock_timestamp(), lease_owner = NULL, lease_expires_at = NULL
|
|
820
|
+
WHERE account_id = $1 AND destination_ref = $2 AND authorization_generation = $3
|
|
821
|
+
AND status = 'pending'`,
|
|
822
|
+
[input.account_id, input.destination_ref, input.generation]
|
|
823
|
+
);
|
|
824
|
+
return updated.rowCount === 1;
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
async installBinding(binding) {
|
|
828
|
+
positiveInteger(binding.resource_retention_days, "resource_retention_days");
|
|
829
|
+
if (binding.reconciliation_mode === "consumer_receipt" && binding.verification_profile !== "canonical_digest") {
|
|
830
|
+
throw new Error("Reconciled Billing bindings require canonical-digest verification");
|
|
831
|
+
}
|
|
832
|
+
const expectedFingerprint = managedBindingFingerprint(binding);
|
|
833
|
+
if (binding.semantic_fingerprint !== expectedFingerprint) {
|
|
834
|
+
throw new Error("Managed binding semantic fingerprint does not match its immutable content");
|
|
835
|
+
}
|
|
836
|
+
await this.ensurePolicyRegistered();
|
|
837
|
+
return this.transaction(async (client) => {
|
|
838
|
+
const registeredPolicy = await this.readPolicy(client, "update");
|
|
839
|
+
await advisoryLock(client, accountLock(binding.account_id));
|
|
840
|
+
await advisoryLock(client, `adcp-reporting-binding:${binding.account_id}:${binding.delivery_config_id}`);
|
|
841
|
+
const existing = await client.query(
|
|
842
|
+
"SELECT semantic_fingerprint FROM adcp_reporting_managed_bindings WHERE configuration_id = $1",
|
|
843
|
+
[binding.configurationId]
|
|
844
|
+
);
|
|
845
|
+
if (existing.rowCount) {
|
|
846
|
+
if (existing.rows[0]?.semantic_fingerprint !== expectedFingerprint) {
|
|
847
|
+
throw new Error("Immutable managed binding identity names different content");
|
|
848
|
+
}
|
|
849
|
+
return { inserted: false };
|
|
850
|
+
}
|
|
851
|
+
const eligible = await client.query(
|
|
852
|
+
`SELECT configuration.data AS configuration FROM adcp_reporting_configurations configuration
|
|
853
|
+
JOIN adcp_reporting_destination_authorizations authz
|
|
854
|
+
ON authz.account_id = $2 AND authz.destination_ref = $5
|
|
855
|
+
AND authz.generation = $6 AND authz.revoked_at IS NULL
|
|
856
|
+
WHERE configuration.configuration_id = $1 AND configuration.account_id = $2
|
|
857
|
+
AND configuration.delivery_config_id = $3 AND configuration.delivery_config_version = $4
|
|
858
|
+
AND NOT EXISTS (
|
|
859
|
+
SELECT 1 FROM adcp_reporting_obligations obligation
|
|
860
|
+
WHERE obligation.configuration_id = configuration.configuration_id
|
|
861
|
+
)`,
|
|
862
|
+
[
|
|
863
|
+
binding.configurationId,
|
|
864
|
+
binding.account_id,
|
|
865
|
+
binding.delivery_config_id,
|
|
866
|
+
binding.delivery_config_version,
|
|
867
|
+
binding.destination_ref,
|
|
868
|
+
binding.authorization_generation
|
|
869
|
+
]
|
|
870
|
+
);
|
|
871
|
+
if (!eligible.rowCount) throw new Error("Managed binding is not authorized for the exact Core configuration");
|
|
872
|
+
const configuration = eligible.rows[0].configuration;
|
|
873
|
+
const advertisedRecoveryWindowSeconds = registeredPolicy.recoveryWindowSeconds;
|
|
874
|
+
if (advertisedRecoveryWindowSeconds !== null) {
|
|
875
|
+
const windowMilliseconds = Number(configuration.schedule?.recoveryWindowMilliseconds);
|
|
876
|
+
if (!Number.isFinite(windowMilliseconds) || windowMilliseconds < 0) {
|
|
877
|
+
throw new Error("Managed binding Core configuration has no usable recovery window");
|
|
878
|
+
}
|
|
879
|
+
const windowSeconds = Math.ceil(windowMilliseconds / 1e3);
|
|
880
|
+
if (windowSeconds > advertisedRecoveryWindowSeconds) {
|
|
881
|
+
throw new Error(
|
|
882
|
+
`Managed binding Core recovery window is ${windowSeconds}s but this agent advertises automated_recovery_window_seconds ${advertisedRecoveryWindowSeconds}s; installing it would publish a recovery bound the deployment does not keep`
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (binding.feed_purpose === "billing" && binding.reconciliation_mode !== "consumer_receipt") {
|
|
887
|
+
throw new Error("Billing managed bindings require consumer-receipt reconciliation");
|
|
888
|
+
}
|
|
889
|
+
if (binding.feed_purpose === "billing" && configuration.requiredFinality !== "official") {
|
|
890
|
+
throw new Error("Billing managed bindings require a Core configuration with official finality");
|
|
891
|
+
}
|
|
892
|
+
if (configuration.feedPurpose !== binding.feed_purpose) {
|
|
893
|
+
throw new Error("Managed binding feed purpose differs from the exact Core configuration");
|
|
894
|
+
}
|
|
895
|
+
if (binding.reconciliation_mode === "consumer_receipt" && !configuration.canonicalization) {
|
|
896
|
+
throw new Error("Reconciled Billing requires a Core configuration with pinned canonicalization");
|
|
897
|
+
}
|
|
898
|
+
const inserted = await client.query(
|
|
899
|
+
`INSERT INTO adcp_reporting_managed_bindings
|
|
900
|
+
(configuration_id, account_id, delivery_config_id, delivery_config_version,
|
|
901
|
+
destination_ref, authorization_generation, semantic_fingerprint, data, created_at)
|
|
902
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8::jsonb,$9)
|
|
903
|
+
ON CONFLICT DO NOTHING RETURNING semantic_fingerprint`,
|
|
904
|
+
[
|
|
905
|
+
binding.configurationId,
|
|
906
|
+
binding.account_id,
|
|
907
|
+
binding.delivery_config_id,
|
|
908
|
+
binding.delivery_config_version,
|
|
909
|
+
binding.destination_ref,
|
|
910
|
+
binding.authorization_generation,
|
|
911
|
+
binding.semantic_fingerprint,
|
|
912
|
+
JSON.stringify(binding),
|
|
913
|
+
binding.created_at
|
|
914
|
+
]
|
|
915
|
+
);
|
|
916
|
+
if (!inserted.rowCount) {
|
|
917
|
+
const raced = await client.query(
|
|
918
|
+
"SELECT semantic_fingerprint FROM adcp_reporting_managed_bindings WHERE configuration_id = $1",
|
|
919
|
+
[binding.configurationId]
|
|
920
|
+
);
|
|
921
|
+
if (raced.rows[0]?.semantic_fingerprint !== binding.semantic_fingerprint) {
|
|
922
|
+
throw new Error("Immutable managed binding identity names different content");
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return { inserted: inserted.rowCount === 1 };
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
async planMaterializations(input = {}) {
|
|
929
|
+
const limit = input.limit ?? MAX_PLAN;
|
|
930
|
+
positiveInteger(limit, "limit");
|
|
931
|
+
if (limit > MAX_PLAN) throw new RangeError(`limit must not exceed ${MAX_PLAN}`);
|
|
932
|
+
if (!input.account_id) {
|
|
933
|
+
const accounts = await this.query(
|
|
934
|
+
"SELECT DISTINCT account_id FROM adcp_reporting_managed_bindings ORDER BY account_id"
|
|
935
|
+
);
|
|
936
|
+
let planned = 0;
|
|
937
|
+
for (const { account_id } of accounts.rows) {
|
|
938
|
+
if (planned >= limit) break;
|
|
939
|
+
planned += await this.planMaterializations({ account_id, limit: limit - planned });
|
|
940
|
+
}
|
|
941
|
+
return planned;
|
|
942
|
+
}
|
|
943
|
+
return this.transaction(async (client) => {
|
|
944
|
+
const accounts = await client.query(
|
|
945
|
+
`SELECT DISTINCT account_id FROM adcp_reporting_managed_bindings
|
|
946
|
+
WHERE ($1::text IS NULL OR account_id = $1) ORDER BY account_id`,
|
|
947
|
+
[input.account_id ?? null]
|
|
948
|
+
);
|
|
949
|
+
for (const { account_id } of accounts.rows) await advisoryLock(client, accountLock(account_id));
|
|
950
|
+
const capacity = await client.query(
|
|
951
|
+
`SELECT account_id, COUNT(*)::text AS count FROM adcp_reporting_materializations
|
|
952
|
+
WHERE ($1::text IS NULL OR account_id = $1)${this.activeScope("recorded_at")} GROUP BY account_id`,
|
|
953
|
+
[input.account_id ?? null]
|
|
954
|
+
);
|
|
955
|
+
const remainingByAccount = new Map(
|
|
956
|
+
accounts.rows.map(({ account_id }) => [
|
|
957
|
+
account_id,
|
|
958
|
+
MAX_MATERIALIZATIONS_PER_ACCOUNT - Number(capacity.rows.find((value) => value.account_id === account_id)?.count ?? 0)
|
|
959
|
+
])
|
|
960
|
+
);
|
|
961
|
+
const candidates = await client.query(
|
|
962
|
+
`SELECT binding.data AS binding, obligation.data AS obligation, revision.data AS revision,
|
|
963
|
+
GREATEST(COALESCE(MAX(existing.attempt), 0), COALESCE(MAX(tomb.highest_attempt), 0))::integer + 1
|
|
964
|
+
AS attempt
|
|
965
|
+
FROM adcp_reporting_managed_bindings binding
|
|
966
|
+
JOIN adcp_reporting_destination_authorizations authz
|
|
967
|
+
ON authz.account_id = binding.account_id
|
|
968
|
+
AND authz.destination_ref = binding.destination_ref
|
|
969
|
+
AND authz.generation = binding.authorization_generation
|
|
970
|
+
AND authz.revoked_at IS NULL
|
|
971
|
+
JOIN adcp_reporting_obligations obligation ON obligation.configuration_id = binding.configuration_id
|
|
972
|
+
JOIN adcp_reporting_revisions revision ON revision.obligation_id = obligation.obligation_id
|
|
973
|
+
LEFT JOIN adcp_reporting_materializations existing
|
|
974
|
+
ON existing.configuration_id = binding.configuration_id AND existing.revision_id = revision.revision_id
|
|
975
|
+
-- Pruned attempts still count. Without this a revision that exhausted
|
|
976
|
+
-- its attempts, or already succeeded, restarts at attempt 1 as soon as
|
|
977
|
+
-- its rows age out of retention.
|
|
978
|
+
LEFT JOIN adcp_reporting_materialization_tombstones tomb
|
|
979
|
+
ON tomb.configuration_id = binding.configuration_id AND tomb.revision_id = revision.revision_id
|
|
980
|
+
WHERE ($1::text IS NULL OR binding.account_id = $1)
|
|
981
|
+
GROUP BY binding.configuration_id, binding.data, obligation.obligation_id, obligation.data,
|
|
982
|
+
revision.revision_id, revision.data
|
|
983
|
+
HAVING NOT COALESCE(BOOL_OR(existing.status IN ('pending','available','delivered')), false)
|
|
984
|
+
AND NOT COALESCE(BOOL_OR(tomb.reached_success), false)
|
|
985
|
+
AND GREATEST(COALESCE(MAX(existing.attempt), 0), COALESCE(MAX(tomb.highest_attempt), 0))
|
|
986
|
+
< ${MAX_MATERIALIZATION_ATTEMPTS}
|
|
987
|
+
ORDER BY MIN(revision.recorded_at), revision.revision_id
|
|
988
|
+
LIMIT $2`,
|
|
989
|
+
[input.account_id ?? null, limit]
|
|
990
|
+
);
|
|
991
|
+
let planned = 0;
|
|
992
|
+
for (const candidate of candidates.rows) {
|
|
993
|
+
const binding = candidate.binding;
|
|
994
|
+
const remaining = remainingByAccount.get(binding.account_id) ?? 0;
|
|
995
|
+
if (remaining <= 0) continue;
|
|
996
|
+
const obligation = candidate.obligation;
|
|
997
|
+
const revision = candidate.revision;
|
|
998
|
+
const created_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
999
|
+
const materialization = {
|
|
1000
|
+
reporting_materialization_id: `rmat_${(0, import_node_crypto.randomUUID)()}`,
|
|
1001
|
+
reporting_revision_id: revision.reporting_revision_id,
|
|
1002
|
+
reporting_obligation_id: obligation.reporting_obligation_id,
|
|
1003
|
+
delivery_config_id: binding.delivery_config_id,
|
|
1004
|
+
delivery_config_version: binding.delivery_config_version,
|
|
1005
|
+
destination_ref: binding.destination_ref,
|
|
1006
|
+
feed_purpose: binding.feed_purpose,
|
|
1007
|
+
method: binding.method,
|
|
1008
|
+
...binding.transport ? { transport: binding.transport } : {},
|
|
1009
|
+
attempt: candidate.attempt,
|
|
1010
|
+
status: "pending",
|
|
1011
|
+
created_at
|
|
1012
|
+
};
|
|
1013
|
+
const inserted = await client.query(
|
|
1014
|
+
`INSERT INTO adcp_reporting_materializations
|
|
1015
|
+
(materialization_id, account_id, configuration_id, obligation_id, revision_id,
|
|
1016
|
+
destination_ref, authorization_generation, attempt, status, data, created_at)
|
|
1017
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'pending',$9::jsonb,$10)
|
|
1018
|
+
ON CONFLICT DO NOTHING`,
|
|
1019
|
+
[
|
|
1020
|
+
materialization.reporting_materialization_id,
|
|
1021
|
+
binding.account_id,
|
|
1022
|
+
binding.configurationId,
|
|
1023
|
+
obligation.reporting_obligation_id,
|
|
1024
|
+
revision.reporting_revision_id,
|
|
1025
|
+
binding.destination_ref,
|
|
1026
|
+
binding.authorization_generation,
|
|
1027
|
+
candidate.attempt,
|
|
1028
|
+
JSON.stringify(materialization),
|
|
1029
|
+
created_at
|
|
1030
|
+
]
|
|
1031
|
+
);
|
|
1032
|
+
planned += inserted.rowCount ?? 0;
|
|
1033
|
+
if (inserted.rowCount) remainingByAccount.set(binding.account_id, remaining - 1);
|
|
1034
|
+
}
|
|
1035
|
+
return planned;
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
async claimMaterialization(input) {
|
|
1039
|
+
positiveInteger(input.lease_milliseconds, "lease_milliseconds");
|
|
1040
|
+
return this.transaction(async (client) => {
|
|
1041
|
+
const selected = await client.query(
|
|
1042
|
+
`WITH candidate AS (
|
|
1043
|
+
SELECT materialization.materialization_id
|
|
1044
|
+
FROM adcp_reporting_materializations materialization
|
|
1045
|
+
JOIN adcp_reporting_destination_authorizations authz
|
|
1046
|
+
ON authz.account_id = materialization.account_id
|
|
1047
|
+
AND authz.destination_ref = materialization.destination_ref
|
|
1048
|
+
AND authz.generation = materialization.authorization_generation
|
|
1049
|
+
AND authz.revoked_at IS NULL
|
|
1050
|
+
WHERE materialization.status = 'pending'
|
|
1051
|
+
AND ($1::text IS NULL OR materialization.account_id = $1)
|
|
1052
|
+
-- Every claim of a pending row is a delivery attempt, settled
|
|
1053
|
+
-- or not. The attempt column only advances in the planner, so a
|
|
1054
|
+
-- row whose settle kept losing its lease was reclaimed without
|
|
1055
|
+
-- bound and re-delivered each time. Lease generation is the
|
|
1056
|
+
-- durable count of those attempts.
|
|
1057
|
+
AND materialization.lease_generation < ${MAX_MATERIALIZATION_ATTEMPTS}
|
|
1058
|
+
-- One authoritative clock. settleMaterialization fences on
|
|
1059
|
+
-- clock_timestamp(), so issuing the lease from the caller's clock
|
|
1060
|
+
-- made ordinary NTP drift fatal: the claim succeeded, the settle
|
|
1061
|
+
-- matched zero rows, and the row stayed 'pending' at attempt 1
|
|
1062
|
+
-- where the planner's HAVING cannot see it \u2014 so the attempt cap
|
|
1063
|
+
-- never engaged and the adapter was asked to deliver again, and
|
|
1064
|
+
-- again, with no bound.
|
|
1065
|
+
AND (materialization.lease_expires_at IS NULL OR materialization.lease_expires_at <= clock_timestamp())
|
|
1066
|
+
ORDER BY materialization.created_at, materialization.materialization_id
|
|
1067
|
+
FOR UPDATE OF materialization SKIP LOCKED LIMIT 1
|
|
1068
|
+
), claimed AS (
|
|
1069
|
+
UPDATE adcp_reporting_materializations materialization
|
|
1070
|
+
SET lease_owner = $2, lease_generation = lease_generation + 1,
|
|
1071
|
+
lease_expires_at = clock_timestamp() + ($3::bigint * INTERVAL '1 millisecond')
|
|
1072
|
+
FROM candidate WHERE materialization.materialization_id = candidate.materialization_id
|
|
1073
|
+
RETURNING materialization.*
|
|
1074
|
+
)
|
|
1075
|
+
SELECT claimed.materialization_id, claimed.lease_generation::text AS generation,
|
|
1076
|
+
claimed.lease_expires_at AS expires_at, claimed.data AS materialization,
|
|
1077
|
+
binding.data AS binding, obligation.data AS obligation, revision.data AS revision
|
|
1078
|
+
FROM claimed
|
|
1079
|
+
JOIN adcp_reporting_managed_bindings binding ON binding.configuration_id = claimed.configuration_id
|
|
1080
|
+
JOIN adcp_reporting_obligations obligation ON obligation.obligation_id = claimed.obligation_id
|
|
1081
|
+
JOIN adcp_reporting_revisions revision ON revision.revision_id = claimed.revision_id`,
|
|
1082
|
+
[input.account_id ?? null, input.owner, input.lease_milliseconds]
|
|
1083
|
+
);
|
|
1084
|
+
const row = selected.rows[0];
|
|
1085
|
+
if (!row) return null;
|
|
1086
|
+
return {
|
|
1087
|
+
materialization: structuredClone(row.materialization),
|
|
1088
|
+
binding: structuredClone(row.binding),
|
|
1089
|
+
obligation: structuredClone(row.obligation),
|
|
1090
|
+
revision: structuredClone(row.revision),
|
|
1091
|
+
owner: input.owner,
|
|
1092
|
+
generation: Number(row.generation),
|
|
1093
|
+
expires_at: row.expires_at.toISOString()
|
|
1094
|
+
};
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Fails materializations that have used every delivery attempt.
|
|
1099
|
+
*
|
|
1100
|
+
* The claim predicate stops handing them out, so without this they would
|
|
1101
|
+
* stay `pending` — invisible to the planner, which skips an obligation with
|
|
1102
|
+
* any pending row, and so never retried or replanned.
|
|
1103
|
+
*/
|
|
1104
|
+
async failExhaustedMaterializations(input) {
|
|
1105
|
+
const limit = input?.limit ?? 1e3;
|
|
1106
|
+
positiveInteger(limit, "limit");
|
|
1107
|
+
return this.transaction(async (client) => {
|
|
1108
|
+
const accounts = await client.query(
|
|
1109
|
+
`SELECT DISTINCT account_id FROM adcp_reporting_materializations
|
|
1110
|
+
WHERE status = 'pending'
|
|
1111
|
+
AND lease_generation >= ${MAX_MATERIALIZATION_ATTEMPTS}
|
|
1112
|
+
AND (lease_expires_at IS NULL OR lease_expires_at <= clock_timestamp())
|
|
1113
|
+
AND ($1::text IS NULL OR account_id = $1)
|
|
1114
|
+
ORDER BY account_id`,
|
|
1115
|
+
[input?.account_id ?? null]
|
|
1116
|
+
);
|
|
1117
|
+
for (const { account_id } of accounts.rows) await advisoryLock(client, accountLock(account_id));
|
|
1118
|
+
const lockedAccounts = accounts.rows.map((row) => row.account_id);
|
|
1119
|
+
const result = await client.query(
|
|
1120
|
+
`UPDATE adcp_reporting_materializations SET status = 'failed',
|
|
1121
|
+
data = data || jsonb_build_object(
|
|
1122
|
+
'status', 'failed',
|
|
1123
|
+
'failed_at', ${rfc3339Micro("clock_timestamp()")},
|
|
1124
|
+
'failure_code', 'DELIVERY_ATTEMPTS_EXHAUSTED'
|
|
1125
|
+
),
|
|
1126
|
+
changed_at = clock_timestamp(), lease_owner = NULL, lease_expires_at = NULL
|
|
1127
|
+
WHERE
|
|
1128
|
+
-- Re-evaluated after the row lock, not only as a subquery. The
|
|
1129
|
+
-- subquery runs against the statement's snapshot, so a settle that
|
|
1130
|
+
-- committed while this UPDATE waited on the row was overwritten:
|
|
1131
|
+
-- a delivered materialization became failed /
|
|
1132
|
+
-- DELIVERY_ATTEMPTS_EXHAUSTED. Repeating the predicates here makes
|
|
1133
|
+
-- PostgreSQL recheck them against the committed row.
|
|
1134
|
+
status = 'pending'
|
|
1135
|
+
AND lease_generation >= ${MAX_MATERIALIZATION_ATTEMPTS}
|
|
1136
|
+
AND (lease_expires_at IS NULL OR lease_expires_at <= clock_timestamp())
|
|
1137
|
+
AND account_id = ANY($2::text[])
|
|
1138
|
+
AND materialization_id IN (
|
|
1139
|
+
SELECT materialization_id FROM adcp_reporting_materializations
|
|
1140
|
+
WHERE status = 'pending'
|
|
1141
|
+
AND lease_generation >= ${MAX_MATERIALIZATION_ATTEMPTS}
|
|
1142
|
+
AND (lease_expires_at IS NULL OR lease_expires_at <= clock_timestamp())
|
|
1143
|
+
AND account_id = ANY($2::text[])
|
|
1144
|
+
ORDER BY created_at LIMIT $1)`,
|
|
1145
|
+
[limit, lockedAccounts]
|
|
1146
|
+
);
|
|
1147
|
+
return result.rowCount ?? 0;
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
async settleMaterialization(input) {
|
|
1151
|
+
const requested = input.minimum_resource_retention_days;
|
|
1152
|
+
if (requested !== void 0 && (!Number.isSafeInteger(requested) || requested < 0)) {
|
|
1153
|
+
throw new RangeError("minimum_resource_retention_days must be a non-negative safe integer");
|
|
1154
|
+
}
|
|
1155
|
+
if (input.outcome.status !== "failed" && input.outcome.resource.expires_at !== void 0) {
|
|
1156
|
+
(0, import_managed.assertMaterializationOutcome)(
|
|
1157
|
+
input.lease,
|
|
1158
|
+
input.outcome,
|
|
1159
|
+
input.now,
|
|
1160
|
+
Math.max(requested ?? 0, input.lease.binding.resource_retention_days)
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
return this.transaction(async (client) => {
|
|
1164
|
+
const { lease } = input;
|
|
1165
|
+
const registeredPolicy = await this.readPolicy(client, "share");
|
|
1166
|
+
await advisoryLock(client, accountLock(lease.binding.account_id));
|
|
1167
|
+
const minimumRetentionDays = Math.max(
|
|
1168
|
+
requested ?? 0,
|
|
1169
|
+
lease.binding.resource_retention_days,
|
|
1170
|
+
registeredPolicy.resourceRetentionDays ?? 0
|
|
1171
|
+
);
|
|
1172
|
+
const authorized = await client.query(
|
|
1173
|
+
`SELECT 1 FROM adcp_reporting_destination_authorizations
|
|
1174
|
+
WHERE account_id = $1 AND destination_ref = $2 AND generation = $3 AND revoked_at IS NULL`,
|
|
1175
|
+
[lease.binding.account_id, lease.binding.destination_ref, lease.binding.authorization_generation]
|
|
1176
|
+
);
|
|
1177
|
+
const outcome = authorized.rowCount ? input.outcome : { status: "failed", failure_code: "AUTHORIZATION_REVOKED" };
|
|
1178
|
+
const materialization = {
|
|
1179
|
+
...lease.materialization,
|
|
1180
|
+
status: outcome.status,
|
|
1181
|
+
...outcome.status === "failed" ? { failed_at: input.now, failure_code: outcome.failure_code } : { ready_at: input.now, resource: outcome.resource, verification: outcome.verification }
|
|
1182
|
+
};
|
|
1183
|
+
const updated = await client.query(
|
|
1184
|
+
`UPDATE adcp_reporting_materializations SET status = $5, data = $6::jsonb,
|
|
1185
|
+
changed_at = clock_timestamp(), lease_owner = NULL, lease_expires_at = NULL
|
|
1186
|
+
WHERE materialization_id = $1 AND lease_owner = $2 AND lease_generation = $3
|
|
1187
|
+
AND lease_expires_at > clock_timestamp() AND status = 'pending' AND authorization_generation = $4
|
|
1188
|
+
-- Retention is judged entirely by the clock that stores it. A
|
|
1189
|
+
-- worker running behind could otherwise satisfy the window on its
|
|
1190
|
+
-- own clock and publish a resource the database already considers
|
|
1191
|
+
-- expired, or under-retain one \u2014 leaving a delivered
|
|
1192
|
+
-- materialization whose bytes no reader can fetch.
|
|
1193
|
+
-- Exempt only a failed outcome, which retains nothing. Treating
|
|
1194
|
+
-- a NULL expiry as exempt let a direct caller settle a
|
|
1195
|
+
-- successful materialization with no expiry at all: permanently
|
|
1196
|
+
-- readable to the projection, permanently unreadable in fact,
|
|
1197
|
+
-- and never revisited because it is not pending.
|
|
1198
|
+
AND ($5 = 'failed'
|
|
1199
|
+
OR ($7::timestamptz IS NOT NULL
|
|
1200
|
+
AND $7::timestamptz >= clock_timestamp() + ($8::bigint * INTERVAL '1 day')))`,
|
|
1201
|
+
[
|
|
1202
|
+
lease.materialization.reporting_materialization_id,
|
|
1203
|
+
lease.owner,
|
|
1204
|
+
lease.generation,
|
|
1205
|
+
lease.binding.authorization_generation,
|
|
1206
|
+
outcome.status,
|
|
1207
|
+
JSON.stringify(materialization),
|
|
1208
|
+
outcome.status === "failed" ? null : outcome.resource.expires_at,
|
|
1209
|
+
minimumRetentionDays
|
|
1210
|
+
]
|
|
1211
|
+
);
|
|
1212
|
+
if (updated.rowCount === 0 && outcome.status !== "failed") {
|
|
1213
|
+
await client.query(
|
|
1214
|
+
`UPDATE adcp_reporting_materializations SET status = 'failed',
|
|
1215
|
+
data = data || jsonb_build_object(
|
|
1216
|
+
'status', 'failed',
|
|
1217
|
+
'failed_at', ${rfc3339Micro("clock_timestamp()")},
|
|
1218
|
+
'failure_code', 'RESOURCE_RETENTION_INSUFFICIENT'
|
|
1219
|
+
),
|
|
1220
|
+
changed_at = clock_timestamp(), lease_owner = NULL, lease_expires_at = NULL
|
|
1221
|
+
-- Deliberately not fenced on an unexpired lease. If the settle
|
|
1222
|
+
-- waited on the account lock until the lease expired, both it and
|
|
1223
|
+
-- this terminalizer rejected, leaving the row pending \u2014
|
|
1224
|
+
-- reclaimable forever, and re-delivered each time. Owner and
|
|
1225
|
+
-- generation still fence it against a newer holder.
|
|
1226
|
+
WHERE materialization_id = $1 AND lease_owner = $2 AND lease_generation = $3
|
|
1227
|
+
AND status = 'pending'`,
|
|
1228
|
+
[lease.materialization.reporting_materialization_id, lease.owner, lease.generation]
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
return updated.rowCount === 1 && authorized.rowCount === 1 && outcome.status !== "failed";
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
async claimRevocation(input) {
|
|
1235
|
+
positiveInteger(input.lease_milliseconds, "lease_milliseconds");
|
|
1236
|
+
if (input.authorization_revocation_seconds !== void 0) {
|
|
1237
|
+
nonnegativeSafeInteger(input.authorization_revocation_seconds, "authorization_revocation_seconds");
|
|
1238
|
+
}
|
|
1239
|
+
return this.transaction(async (client) => {
|
|
1240
|
+
const registeredPolicy = await this.readPolicy(client, "share");
|
|
1241
|
+
const revocationSeconds = registeredPolicy.authorizationRevocationSeconds === null ? input.authorization_revocation_seconds : Math.min(
|
|
1242
|
+
registeredPolicy.authorizationRevocationSeconds,
|
|
1243
|
+
input.authorization_revocation_seconds ?? registeredPolicy.authorizationRevocationSeconds
|
|
1244
|
+
);
|
|
1245
|
+
const result = await client.query(
|
|
1246
|
+
`WITH candidate AS (
|
|
1247
|
+
SELECT account_id, destination_ref, generation
|
|
1248
|
+
FROM adcp_reporting_destination_authorizations
|
|
1249
|
+
WHERE revoked_at IS NOT NULL AND cleanup_completed_at IS NULL
|
|
1250
|
+
-- Same single-clock rule as claimMaterialization: completeRevocation
|
|
1251
|
+
-- fences on clock_timestamp(), so the lease must be issued from it
|
|
1252
|
+
-- too or cleanup can never commit and the grant is never torn down.
|
|
1253
|
+
--
|
|
1254
|
+
-- The last arm reclaims at the SLA boundary. A worker that
|
|
1255
|
+
-- crashed holding a long lease would otherwise make the grant
|
|
1256
|
+
-- unreclaimable for the rest of that lease while the advertised
|
|
1257
|
+
-- window elapsed. Lease generation fences the old holder, so its
|
|
1258
|
+
-- late completion cannot commit over the new one.
|
|
1259
|
+
-- One stable policy. Reclaiming purely because the SLA had
|
|
1260
|
+
-- elapsed let every worker steal from every other on an already
|
|
1261
|
+
-- late grant: generations bumped each pass and each holder's
|
|
1262
|
+
-- completion was invalidated by the next, so cleanup never
|
|
1263
|
+
-- committed at all. A holder is displaced only once it has had a
|
|
1264
|
+
-- full attempt's worth of time and still not finished, which
|
|
1265
|
+
-- recovers a crashed worker without disturbing a live one.
|
|
1266
|
+
AND (cleanup_lease_expires_at IS NULL
|
|
1267
|
+
OR cleanup_lease_expires_at <= clock_timestamp()
|
|
1268
|
+
OR ($4::bigint IS NOT NULL
|
|
1269
|
+
AND revoked_at + ($4::bigint * INTERVAL '1 second') <= clock_timestamp()
|
|
1270
|
+
AND cleanup_lease_issued_at IS NOT NULL
|
|
1271
|
+
AND cleanup_lease_issued_at + ($5::bigint * INTERVAL '1 millisecond')
|
|
1272
|
+
<= clock_timestamp()))
|
|
1273
|
+
AND ($3::text IS NULL OR account_id = $3)
|
|
1274
|
+
ORDER BY cleanup_lease_generation, revoked_at, account_id, destination_ref
|
|
1275
|
+
FOR UPDATE SKIP LOCKED LIMIT 1
|
|
1276
|
+
)
|
|
1277
|
+
UPDATE adcp_reporting_destination_authorizations target SET
|
|
1278
|
+
cleanup_lease_owner = $1, cleanup_lease_generation = target.cleanup_lease_generation + 1,
|
|
1279
|
+
cleanup_lease_issued_at = clock_timestamp(),
|
|
1280
|
+
cleanup_lease_expires_at = clock_timestamp() + ($2::bigint * INTERVAL '1 millisecond')
|
|
1281
|
+
FROM candidate
|
|
1282
|
+
WHERE target.account_id = candidate.account_id AND target.destination_ref = candidate.destination_ref
|
|
1283
|
+
AND target.generation = candidate.generation
|
|
1284
|
+
RETURNING target.data, target.cleanup_lease_generation::text AS generation,
|
|
1285
|
+
target.cleanup_lease_expires_at AS expires_at,
|
|
1286
|
+
target.revoked_at AS revoked_at,
|
|
1287
|
+
CASE WHEN $4::bigint IS NULL THEN NULL
|
|
1288
|
+
ELSE target.revoked_at + ($4::bigint * INTERVAL '1 second') <= clock_timestamp()
|
|
1289
|
+
END AS overdue,
|
|
1290
|
+
CASE WHEN $4::bigint IS NULL THEN NULL
|
|
1291
|
+
ELSE EXTRACT(EPOCH FROM (target.revoked_at
|
|
1292
|
+
+ ($4::bigint * INTERVAL '1 second') - clock_timestamp())) * 1000
|
|
1293
|
+
END AS remaining_milliseconds`,
|
|
1294
|
+
[
|
|
1295
|
+
input.owner,
|
|
1296
|
+
input.lease_milliseconds,
|
|
1297
|
+
input.account_id ?? null,
|
|
1298
|
+
revocationSeconds ?? null,
|
|
1299
|
+
input.steal_after_milliseconds ?? input.lease_milliseconds
|
|
1300
|
+
]
|
|
1301
|
+
);
|
|
1302
|
+
const row = result.rows[0];
|
|
1303
|
+
if (!row) return null;
|
|
1304
|
+
return {
|
|
1305
|
+
authorization: structuredClone(row.data),
|
|
1306
|
+
owner: input.owner,
|
|
1307
|
+
generation: Number(row.generation),
|
|
1308
|
+
expires_at: row.expires_at.toISOString(),
|
|
1309
|
+
// Every SLA fact comes from the database that committed the
|
|
1310
|
+
// revocation, never from the worker host: a skewed worker must not be
|
|
1311
|
+
// able to decide a grant is still inside its promised window.
|
|
1312
|
+
revoked_at: row.revoked_at.toISOString(),
|
|
1313
|
+
...row.overdue === null ? {} : { overdue: row.overdue },
|
|
1314
|
+
...row.remaining_milliseconds === null ? {} : { remaining_milliseconds: Number(row.remaining_milliseconds) }
|
|
1315
|
+
};
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
async completeRevocation(input) {
|
|
1319
|
+
const result = await this.query(
|
|
1320
|
+
`UPDATE adcp_reporting_destination_authorizations SET cleanup_completed_at = $6::timestamptz,
|
|
1321
|
+
cleanup_lease_owner = NULL, cleanup_lease_expires_at = NULL,
|
|
1322
|
+
data = data || jsonb_build_object('cleanup_completed_at', $6::text)
|
|
1323
|
+
WHERE account_id = $1 AND destination_ref = $2 AND generation = $3
|
|
1324
|
+
AND cleanup_lease_owner = $4 AND cleanup_lease_generation = $5
|
|
1325
|
+
AND cleanup_lease_expires_at > clock_timestamp()
|
|
1326
|
+
AND revoked_at IS NOT NULL AND cleanup_completed_at IS NULL`,
|
|
1327
|
+
[
|
|
1328
|
+
input.lease.authorization.account_id,
|
|
1329
|
+
input.lease.authorization.destination_ref,
|
|
1330
|
+
input.lease.authorization.generation,
|
|
1331
|
+
input.lease.owner,
|
|
1332
|
+
input.lease.generation,
|
|
1333
|
+
input.completed_at
|
|
1334
|
+
]
|
|
1335
|
+
);
|
|
1336
|
+
return result.rowCount === 1;
|
|
1337
|
+
}
|
|
1338
|
+
async releaseRevocation(input) {
|
|
1339
|
+
const result = await this.query(
|
|
1340
|
+
`UPDATE adcp_reporting_destination_authorizations
|
|
1341
|
+
-- Release enables a later recovery, not an immediate one. Clearing
|
|
1342
|
+
-- the lease outright let the very next iteration of the same worker
|
|
1343
|
+
-- tick reclaim the same grant, so one broken provider consumed
|
|
1344
|
+
-- every iteration and nothing else was ever cleaned up. Holding a
|
|
1345
|
+
-- short backoff keeps the grant reclaimable soon without that
|
|
1346
|
+
-- tight loop; the generation bump still fences the failed holder.
|
|
1347
|
+
SET cleanup_lease_owner = NULL, cleanup_lease_issued_at = NULL,
|
|
1348
|
+
cleanup_lease_expires_at = clock_timestamp() + ($6::bigint * INTERVAL '1 millisecond')
|
|
1349
|
+
WHERE account_id = $1 AND destination_ref = $2 AND generation = $3
|
|
1350
|
+
AND cleanup_lease_owner = $4 AND cleanup_lease_generation = $5
|
|
1351
|
+
AND cleanup_completed_at IS NULL`,
|
|
1352
|
+
[
|
|
1353
|
+
input.lease.authorization.account_id,
|
|
1354
|
+
input.lease.authorization.destination_ref,
|
|
1355
|
+
input.lease.authorization.generation,
|
|
1356
|
+
input.lease.owner,
|
|
1357
|
+
input.lease.generation,
|
|
1358
|
+
input.backoff_milliseconds ?? REVOCATION_RETRY_BACKOFF_MS
|
|
1359
|
+
]
|
|
1360
|
+
);
|
|
1361
|
+
return result.rowCount === 1;
|
|
1362
|
+
}
|
|
1363
|
+
async getReadableResource(input) {
|
|
1364
|
+
const result = await this.query(
|
|
1365
|
+
`SELECT materialization.data AS materialization, binding.data AS binding
|
|
1366
|
+
FROM adcp_reporting_materializations materialization
|
|
1367
|
+
JOIN adcp_reporting_managed_bindings binding ON binding.configuration_id = materialization.configuration_id
|
|
1368
|
+
JOIN adcp_reporting_destination_authorizations authz
|
|
1369
|
+
ON authz.account_id = materialization.account_id
|
|
1370
|
+
AND authz.destination_ref = materialization.destination_ref
|
|
1371
|
+
AND authz.generation = materialization.authorization_generation
|
|
1372
|
+
AND authz.revoked_at IS NULL
|
|
1373
|
+
WHERE materialization.account_id = $1
|
|
1374
|
+
AND materialization.status IN ('available','delivered')
|
|
1375
|
+
AND materialization.data->'resource'->>'resource_ref' = $2
|
|
1376
|
+
AND (materialization.data->'resource'->>'expires_at')::timestamptz > clock_timestamp()`,
|
|
1377
|
+
[input.account_id, input.resource_ref]
|
|
1378
|
+
);
|
|
1379
|
+
const row = result.rows[0];
|
|
1380
|
+
return row ? { materialization: structuredClone(row.materialization), binding: structuredClone(row.binding) } : null;
|
|
1381
|
+
}
|
|
1382
|
+
async isAuthorizationCurrent(input) {
|
|
1383
|
+
const result = await this.query(
|
|
1384
|
+
`SELECT 1 FROM adcp_reporting_destination_authorizations
|
|
1385
|
+
WHERE account_id = $1 AND destination_ref = $2 AND generation = $3 AND revoked_at IS NULL`,
|
|
1386
|
+
[input.account_id, input.destination_ref, input.generation]
|
|
1387
|
+
);
|
|
1388
|
+
return result.rowCount === 1;
|
|
1389
|
+
}
|
|
1390
|
+
async syncReceiptBatch(input) {
|
|
1391
|
+
const revisionCount = input.entries.filter((entry) => entry.kind === "revision").length;
|
|
1392
|
+
if (!/^[A-Za-z0-9_.:-]{16,255}$/.test(input.idempotency_key) || input.entries.length < 1 || input.entries.length > MAX_RECEIPT_RESULTS || revisionCount > MAX_RECEIPTS_PER_ARRAY || input.entries.length - revisionCount > MAX_RECEIPTS_PER_ARRAY || input.entries.some(
|
|
1393
|
+
(entry) => (entry.kind === "revision" ? !(0, import_evidence.isReportingReceiptEvidence)(entry.receipt) : !(0, import_evidence.isReportingAdjustmentReceiptEvidence)(entry.receipt)) || Buffer.byteLength(JSON.stringify(entry.receipt), "utf8") > 64 * 1024
|
|
1394
|
+
)) {
|
|
1395
|
+
throw new Error("Reporting receipt transaction failed");
|
|
1396
|
+
}
|
|
1397
|
+
return this.transaction(async (client) => {
|
|
1398
|
+
await advisoryLock(client, accountLock(input.account_id));
|
|
1399
|
+
await advisoryLock(client, `adcp-reporting-receipts:${input.account_id}:${input.consumer_id}`);
|
|
1400
|
+
const receivedAt = (await client.query(`SELECT ${rfc3339Micro("clock_timestamp()")} AS now`)).rows[0].now;
|
|
1401
|
+
const prior = await client.query(
|
|
1402
|
+
`SELECT request_fingerprint, results FROM adcp_reporting_receipt_batches
|
|
1403
|
+
WHERE account_id = $1 AND consumer_id = $2 AND idempotency_key = $3`,
|
|
1404
|
+
[input.account_id, input.consumer_id, input.idempotency_key]
|
|
1405
|
+
);
|
|
1406
|
+
if (prior.rows[0]) {
|
|
1407
|
+
if (prior.rows[0].request_fingerprint !== input.request_fingerprint) {
|
|
1408
|
+
return input.entries.map((entry) => idempotencyConflict(entry.receipt.reporting_receipt_id));
|
|
1409
|
+
}
|
|
1410
|
+
return this.replayReceiptBatch(client, input, prior.rows[0].results);
|
|
1411
|
+
}
|
|
1412
|
+
await client.query(
|
|
1413
|
+
`DELETE FROM adcp_reporting_receipt_batches
|
|
1414
|
+
WHERE account_id = $1 AND consumer_id = $2
|
|
1415
|
+
AND recorded_at < clock_timestamp() - ($3::bigint * INTERVAL '1 millisecond')`,
|
|
1416
|
+
[input.account_id, input.consumer_id, RECEIPT_BATCH_RETENTION_MS]
|
|
1417
|
+
);
|
|
1418
|
+
const capacity = await client.query(
|
|
1419
|
+
`SELECT
|
|
1420
|
+
(SELECT COUNT(*) FROM adcp_reporting_receipt_batches
|
|
1421
|
+
WHERE account_id = $1 AND consumer_id = $2)::text AS batches,
|
|
1422
|
+
(SELECT COUNT(*) FROM adcp_reporting_receipts
|
|
1423
|
+
WHERE account_id = $1 AND consumer_id = $2${this.activeScope("recorded_at")})::text AS receipts`,
|
|
1424
|
+
[input.account_id, input.consumer_id]
|
|
1425
|
+
);
|
|
1426
|
+
if (Number(capacity.rows[0]?.batches ?? 0) >= MAX_RECEIPT_BATCHES_PER_CONSUMER) {
|
|
1427
|
+
return input.entries.map((entry) => failed(entry.receipt.reporting_receipt_id));
|
|
1428
|
+
}
|
|
1429
|
+
let remainingReceipts = MAX_RECEIPTS_PER_CONSUMER - Number(capacity.rows[0]?.receipts ?? 0);
|
|
1430
|
+
const duplicateIds = duplicates(input.entries.map((entry) => entry.receipt.reporting_receipt_id));
|
|
1431
|
+
const alreadyStored = await client.query(
|
|
1432
|
+
`SELECT reporting_receipt_id, semantic_fingerprint FROM adcp_reporting_receipts
|
|
1433
|
+
WHERE account_id = $1 AND consumer_id = $2 AND reporting_receipt_id = ANY($3::text[])`,
|
|
1434
|
+
[input.account_id, input.consumer_id, input.entries.map((entry) => entry.receipt.reporting_receipt_id)]
|
|
1435
|
+
);
|
|
1436
|
+
const storedFingerprints = new Map(
|
|
1437
|
+
alreadyStored.rows.map((row) => [row.reporting_receipt_id, row.semantic_fingerprint])
|
|
1438
|
+
);
|
|
1439
|
+
const buried = await client.query(
|
|
1440
|
+
`SELECT reporting_receipt_id, semantic_fingerprint FROM adcp_reporting_receipt_tombstones
|
|
1441
|
+
WHERE account_id = $1 AND consumer_id = $2 AND reporting_receipt_id = ANY($3::text[])`,
|
|
1442
|
+
[input.account_id, input.consumer_id, input.entries.map((entry) => entry.receipt.reporting_receipt_id)]
|
|
1443
|
+
);
|
|
1444
|
+
for (const row of buried.rows) storedFingerprints.set(row.reporting_receipt_id, row.semantic_fingerprint);
|
|
1445
|
+
const resolvesToStored = input.entries.map(
|
|
1446
|
+
(entry) => storedFingerprints.get(entry.receipt.reporting_receipt_id) === digest(entry.receipt)
|
|
1447
|
+
);
|
|
1448
|
+
const duplicateSubjects = duplicates(
|
|
1449
|
+
input.entries.filter((_entry, index) => !resolvesToStored[index]).map((entry) => subjectKey(entry))
|
|
1450
|
+
);
|
|
1451
|
+
const results = [];
|
|
1452
|
+
for (const [index, entry] of input.entries.entries()) {
|
|
1453
|
+
if (duplicateIds.has(entry.receipt.reporting_receipt_id) || !resolvesToStored[index] && duplicateSubjects.has(subjectKey(entry))) {
|
|
1454
|
+
results.push(failed(entry.receipt.reporting_receipt_id));
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
const result = await this.recordReceipt(client, input, entry, receivedAt, remainingReceipts);
|
|
1458
|
+
results.push(result);
|
|
1459
|
+
if (result.result === "recorded") remainingReceipts -= 1;
|
|
1460
|
+
}
|
|
1461
|
+
const stored = results.map((result, index) => ({
|
|
1462
|
+
kind: result.result,
|
|
1463
|
+
id: receiptResultId(result, input.entries[index]),
|
|
1464
|
+
entry: input.entries[index].kind,
|
|
1465
|
+
...result.result === "failed" ? { errorCode: "INVALID_REQUEST" } : {}
|
|
1466
|
+
}));
|
|
1467
|
+
const storedJson = JSON.stringify(stored);
|
|
1468
|
+
if (Buffer.byteLength(storedJson, "utf8") > MAX_RECEIPT_BATCH_RESULT_BYTES) {
|
|
1469
|
+
throw new Error("Reporting receipt batch replay record exceeds its byte budget");
|
|
1470
|
+
}
|
|
1471
|
+
await client.query(
|
|
1472
|
+
`INSERT INTO adcp_reporting_receipt_batches
|
|
1473
|
+
(account_id, consumer_id, idempotency_key, request_fingerprint, results)
|
|
1474
|
+
VALUES ($1,$2,$3,$4,$5::jsonb)`,
|
|
1475
|
+
[input.account_id, input.consumer_id, input.idempotency_key, input.request_fingerprint, storedJson]
|
|
1476
|
+
);
|
|
1477
|
+
return results;
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Rebuilds a prior batch response from the compact replay record plus the
|
|
1482
|
+
* append-only receipt rows, re-checking authorization exactly as the first
|
|
1483
|
+
* call did so a revoked destination cannot be replayed back into disclosure.
|
|
1484
|
+
*/
|
|
1485
|
+
/**
|
|
1486
|
+
* Replays a prior batch response exactly.
|
|
1487
|
+
*
|
|
1488
|
+
* An exact same-key replay is side-effect free and returns the caller its own
|
|
1489
|
+
* previously recorded verdict, so later revocation of the destination does
|
|
1490
|
+
* not change it. Downgrading a replay to `failed` once authorization ended
|
|
1491
|
+
* broke the protocol-wide rule that a replayed idempotency key returns the
|
|
1492
|
+
* same response, and bought nothing: the body is the caller's own receipt
|
|
1493
|
+
* echoed back with the verdict it already received, so a revoked caller
|
|
1494
|
+
* learns nothing it did not already hold. Fail-closed still governs every
|
|
1495
|
+
* path that *accepts* new evidence — `loadReceiptEvidence` joins
|
|
1496
|
+
* `revoked_at IS NULL` — and nothing here reads another consumer's rows.
|
|
1497
|
+
*/
|
|
1498
|
+
async replayReceiptBatch(client, input, stored) {
|
|
1499
|
+
const replay = [];
|
|
1500
|
+
for (const [index, entry] of input.entries.entries()) {
|
|
1501
|
+
const result = stored[index];
|
|
1502
|
+
const legacy = legacyReceiptBatchResult(result);
|
|
1503
|
+
if (legacy) {
|
|
1504
|
+
replay.push(structuredClone(legacy));
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
if (!result || result.kind === "failed") {
|
|
1508
|
+
replay.push(failed(entry.receipt.reporting_receipt_id));
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
const row = await client.query(
|
|
1512
|
+
`SELECT data FROM adcp_reporting_receipts
|
|
1513
|
+
WHERE account_id = $1 AND consumer_id = $2 AND reporting_receipt_id = $3`,
|
|
1514
|
+
[input.account_id, input.consumer_id, result.id]
|
|
1515
|
+
);
|
|
1516
|
+
let data = row.rows[0]?.data;
|
|
1517
|
+
if (!data) {
|
|
1518
|
+
const tombstone = await client.query(
|
|
1519
|
+
`SELECT ${rfc3339Micro("COALESCE(subject_recorded_at, pruned_at)")} AS recorded_at, semantic_fingerprint
|
|
1520
|
+
FROM adcp_reporting_receipt_tombstones
|
|
1521
|
+
WHERE account_id = $1 AND consumer_id = $2 AND reporting_receipt_id = $3`,
|
|
1522
|
+
[input.account_id, input.consumer_id, result.id]
|
|
1523
|
+
);
|
|
1524
|
+
const buried = tombstone.rows[0];
|
|
1525
|
+
if (buried && result.id === entry.receipt.reporting_receipt_id && buried.semantic_fingerprint === digest(entry.receipt)) {
|
|
1526
|
+
data = { ...entry.receipt, received_at: buried.recorded_at };
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
if (!data) {
|
|
1530
|
+
replay.push(failed(entry.receipt.reporting_receipt_id));
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
replay.push(
|
|
1534
|
+
result.kind === "recorded" ? recorded(result.entry, structuredClone(data)) : unchanged(result.entry, structuredClone(data))
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
return replay;
|
|
1538
|
+
}
|
|
1539
|
+
async recordReceipt(client, batch, entry, receivedAt, remainingReceipts) {
|
|
1540
|
+
const fingerprint = digest(entry.receipt);
|
|
1541
|
+
const existing = await client.query(
|
|
1542
|
+
`SELECT semantic_fingerprint, data FROM adcp_reporting_receipts
|
|
1543
|
+
WHERE account_id = $1 AND consumer_id = $2 AND reporting_receipt_id = $3`,
|
|
1544
|
+
[batch.account_id, batch.consumer_id, entry.receipt.reporting_receipt_id]
|
|
1545
|
+
);
|
|
1546
|
+
if (existing.rows[0]) {
|
|
1547
|
+
if (existing.rows[0].semantic_fingerprint !== fingerprint) return failed(entry.receipt.reporting_receipt_id);
|
|
1548
|
+
return unchanged(entry.kind, existing.rows[0].data);
|
|
1549
|
+
}
|
|
1550
|
+
const tombstones = await client.query(
|
|
1551
|
+
`SELECT reporting_receipt_id, subject_id, status, was_current, semantic_fingerprint,
|
|
1552
|
+
${rfc3339Micro("COALESCE(subject_recorded_at, pruned_at)")} AS recorded_at
|
|
1553
|
+
FROM adcp_reporting_receipt_tombstones
|
|
1554
|
+
WHERE account_id = $1 AND consumer_id = $2
|
|
1555
|
+
AND (reporting_receipt_id = $3 OR (receipt_kind = $4 AND subject_id = $5))
|
|
1556
|
+
-- Newest leaf first. The demotion above keeps at most one current
|
|
1557
|
+
-- tombstone per subject, but rows written before it existed, and any
|
|
1558
|
+
-- future shape that tombstones two leaves at once, must still resolve
|
|
1559
|
+
-- to one deterministic answer rather than to whatever the planner
|
|
1560
|
+
-- emitted first.
|
|
1561
|
+
ORDER BY was_current DESC, pruned_at DESC, reporting_receipt_id DESC`,
|
|
1562
|
+
[batch.account_id, batch.consumer_id, entry.receipt.reporting_receipt_id, entry.kind, subjectIdFor(entry)]
|
|
1563
|
+
);
|
|
1564
|
+
const ownTombstone = tombstones.rows.find(
|
|
1565
|
+
(tombstone) => tombstone.reporting_receipt_id === entry.receipt.reporting_receipt_id
|
|
1566
|
+
);
|
|
1567
|
+
if (ownTombstone) {
|
|
1568
|
+
if (ownTombstone.semantic_fingerprint !== fingerprint) return failed(entry.receipt.reporting_receipt_id);
|
|
1569
|
+
return unchanged(entry.kind, { ...entry.receipt, received_at: ownTombstone.recorded_at });
|
|
1570
|
+
}
|
|
1571
|
+
for (const tombstone of tombstones.rows) {
|
|
1572
|
+
if (tombstone.status === "accepted" && tombstone.was_current) {
|
|
1573
|
+
return failed(entry.receipt.reporting_receipt_id);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
const tombstonedLeafId = tombstones.rows.find(
|
|
1577
|
+
(tombstone) => tombstone.was_current && tombstone.subject_id === subjectIdFor(entry) && tombstone.reporting_receipt_id !== entry.receipt.reporting_receipt_id
|
|
1578
|
+
)?.reporting_receipt_id;
|
|
1579
|
+
const evidence = await this.loadReceiptEvidence(client, batch.account_id, entry);
|
|
1580
|
+
if (!evidence) return failed(entry.receipt.reporting_receipt_id);
|
|
1581
|
+
const matches = entry.kind === "revision" ? (0, import_managed.receiptEvidenceMatches)(entry.receipt, evidence.materialization) : (0, import_managed.adjustmentReceiptEvidenceMatches)(entry.receipt, evidence.adjustment);
|
|
1582
|
+
const wellFormed = entry.receipt.status === "accepted" ? matches : Boolean(entry.receipt.rejection_codes?.length);
|
|
1583
|
+
if (!wellFormed) {
|
|
1584
|
+
return failed(entry.receipt.reporting_receipt_id);
|
|
1585
|
+
}
|
|
1586
|
+
const current = await client.query(
|
|
1587
|
+
`SELECT reporting_receipt_id, data FROM adcp_reporting_receipts
|
|
1588
|
+
WHERE account_id = $1 AND consumer_id = $2 AND receipt_kind = $3 AND subject_id = $4 AND is_current`,
|
|
1589
|
+
[batch.account_id, batch.consumer_id, entry.kind, evidence.subjectId]
|
|
1590
|
+
);
|
|
1591
|
+
const leaf = current.rows[0];
|
|
1592
|
+
const supersedes = entry.receipt.supersedes_reporting_receipt_id;
|
|
1593
|
+
if (leaf && (leaf.data.status === "accepted" || supersedes !== leaf.reporting_receipt_id) || // The pruned leaf answers for the chain exactly as a live one would:
|
|
1594
|
+
// only its own successor may extend the subject. Re-presenting the
|
|
1595
|
+
// tombstoned leaf itself is not a succession and is handled above, so
|
|
1596
|
+
// it never reaches here.
|
|
1597
|
+
!leaf && tombstonedLeafId !== void 0 && supersedes !== tombstonedLeafId || !leaf && tombstonedLeafId === void 0 && supersedes !== void 0) {
|
|
1598
|
+
return failed(entry.receipt.reporting_receipt_id);
|
|
1599
|
+
}
|
|
1600
|
+
if (remainingReceipts <= 0) return failed(entry.receipt.reporting_receipt_id);
|
|
1601
|
+
if (leaf) {
|
|
1602
|
+
await client.query(
|
|
1603
|
+
`UPDATE adcp_reporting_receipts SET is_current = false
|
|
1604
|
+
WHERE account_id = $1 AND consumer_id = $2 AND reporting_receipt_id = $3 AND is_current`,
|
|
1605
|
+
[batch.account_id, batch.consumer_id, leaf.reporting_receipt_id]
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
const stored = { ...entry.receipt, received_at: receivedAt };
|
|
1609
|
+
await client.query(
|
|
1610
|
+
`INSERT INTO adcp_reporting_receipts
|
|
1611
|
+
(account_id, consumer_id, reporting_receipt_id, receipt_kind, subject_id,
|
|
1612
|
+
supersedes_receipt_id, is_current, semantic_fingerprint, data, received_at, recorded_at)
|
|
1613
|
+
VALUES ($1,$2,$3,$4,$5,$6,true,$7,$8::jsonb,$9::timestamptz,$9::timestamptz)`,
|
|
1614
|
+
[
|
|
1615
|
+
batch.account_id,
|
|
1616
|
+
batch.consumer_id,
|
|
1617
|
+
entry.receipt.reporting_receipt_id,
|
|
1618
|
+
entry.kind,
|
|
1619
|
+
evidence.subjectId,
|
|
1620
|
+
supersedes ?? null,
|
|
1621
|
+
fingerprint,
|
|
1622
|
+
JSON.stringify(stored),
|
|
1623
|
+
receivedAt
|
|
1624
|
+
]
|
|
1625
|
+
);
|
|
1626
|
+
return recorded(entry.kind, stored);
|
|
1627
|
+
}
|
|
1628
|
+
async loadReceiptEvidence(client, accountId, entry) {
|
|
1629
|
+
if (entry.kind === "revision") {
|
|
1630
|
+
const receipt2 = entry.receipt;
|
|
1631
|
+
const result2 = await client.query(
|
|
1632
|
+
`SELECT materialization.data AS materialization
|
|
1633
|
+
FROM adcp_reporting_materializations materialization
|
|
1634
|
+
JOIN adcp_reporting_managed_bindings binding ON binding.configuration_id = materialization.configuration_id
|
|
1635
|
+
JOIN adcp_reporting_destination_authorizations authz
|
|
1636
|
+
ON authz.account_id = materialization.account_id
|
|
1637
|
+
AND authz.destination_ref = materialization.destination_ref
|
|
1638
|
+
AND authz.generation = materialization.authorization_generation
|
|
1639
|
+
AND authz.revoked_at IS NULL
|
|
1640
|
+
JOIN adcp_reporting_revisions revision ON revision.revision_id = materialization.revision_id
|
|
1641
|
+
JOIN adcp_reporting_obligations obligation ON obligation.obligation_id = materialization.obligation_id
|
|
1642
|
+
WHERE materialization.account_id = $1 AND materialization.materialization_id = $2
|
|
1643
|
+
AND materialization.revision_id = $3 AND materialization.obligation_id = $4
|
|
1644
|
+
AND materialization.status IN ('available','delivered')
|
|
1645
|
+
-- RC3 ties the receiptable revision to the obligation's own
|
|
1646
|
+
-- required_finality, exactly as the read projection does when it
|
|
1647
|
+
-- picks the required revision. Hard-coding 'official' made every
|
|
1648
|
+
-- snapshot-finality consumer_receipt configuration unreconcilable:
|
|
1649
|
+
-- the projection asked for a receipt the store always refused.
|
|
1650
|
+
AND (obligation.data->>'requiredFinality' <> 'official' OR revision.finality = 'official')
|
|
1651
|
+
AND binding.data->>'reconciliation_mode' = 'consumer_receipt'`,
|
|
1652
|
+
[
|
|
1653
|
+
accountId,
|
|
1654
|
+
receipt2.reporting_materialization_id,
|
|
1655
|
+
receipt2.reporting_revision_id,
|
|
1656
|
+
receipt2.reporting_obligation_id
|
|
1657
|
+
]
|
|
1658
|
+
);
|
|
1659
|
+
return result2.rows[0] ? { subjectId: receipt2.reporting_revision_id, materialization: result2.rows[0].materialization } : null;
|
|
1660
|
+
}
|
|
1661
|
+
const receipt = entry.receipt;
|
|
1662
|
+
const result = await client.query(
|
|
1663
|
+
`SELECT adjustment.data AS adjustment
|
|
1664
|
+
FROM adcp_reporting_adjustments adjustment
|
|
1665
|
+
JOIN adcp_reporting_revisions revision ON revision.revision_id = adjustment.adjusts_revision_id
|
|
1666
|
+
JOIN adcp_reporting_obligations obligation ON obligation.obligation_id = adjustment.obligation_id
|
|
1667
|
+
JOIN adcp_reporting_managed_bindings binding ON binding.configuration_id = obligation.configuration_id
|
|
1668
|
+
JOIN adcp_reporting_destination_authorizations authz
|
|
1669
|
+
ON authz.account_id = binding.account_id AND authz.destination_ref = binding.destination_ref
|
|
1670
|
+
AND authz.generation = binding.authorization_generation AND authz.revoked_at IS NULL
|
|
1671
|
+
WHERE binding.account_id = $1 AND adjustment.adjustment_id = $2
|
|
1672
|
+
AND adjustment.adjusts_revision_id = $3
|
|
1673
|
+
-- Unlike a revision receipt this arm stays pinned to 'official': an
|
|
1674
|
+
-- adjustment only ever corrects an already-official revision, which
|
|
1675
|
+
-- commitAdjustment enforces at write time.
|
|
1676
|
+
AND revision.finality = 'official'
|
|
1677
|
+
AND binding.data->>'reconciliation_mode' = 'consumer_receipt'`,
|
|
1678
|
+
[accountId, receipt.reporting_adjustment_id, receipt.adjusts_reporting_revision_id]
|
|
1679
|
+
);
|
|
1680
|
+
return result.rows[0] ? { subjectId: receipt.reporting_adjustment_id, adjustment: result.rows[0].adjustment } : null;
|
|
1681
|
+
}
|
|
1682
|
+
async transaction(body) {
|
|
1683
|
+
let client;
|
|
1684
|
+
try {
|
|
1685
|
+
client = await this.pool.connect();
|
|
1686
|
+
} catch (cause) {
|
|
1687
|
+
throw new Error("PostgresReportingManagedDeliveryStore database connection failed", { cause });
|
|
1688
|
+
}
|
|
1689
|
+
let releaseError;
|
|
1690
|
+
let transactionStarted = false;
|
|
1691
|
+
try {
|
|
1692
|
+
await client.query("BEGIN");
|
|
1693
|
+
transactionStarted = true;
|
|
1694
|
+
const value = await body(client);
|
|
1695
|
+
await client.query("COMMIT");
|
|
1696
|
+
return value;
|
|
1697
|
+
} catch (cause) {
|
|
1698
|
+
if (transactionStarted) {
|
|
1699
|
+
try {
|
|
1700
|
+
await client.query("ROLLBACK");
|
|
1701
|
+
} catch (rollbackCause) {
|
|
1702
|
+
releaseError = rollbackCause instanceof Error ? rollbackCause : new Error("Managed reporting rollback failed");
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
throw new Error("PostgresReportingManagedDeliveryStore transaction failed", { cause });
|
|
1706
|
+
} finally {
|
|
1707
|
+
client.release(releaseError);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
async query(sql, values) {
|
|
1711
|
+
try {
|
|
1712
|
+
return await this.pool.query(sql, values);
|
|
1713
|
+
} catch (cause) {
|
|
1714
|
+
throw new Error("PostgresReportingManagedDeliveryStore database operation failed", { cause });
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
function recorded(kind, data) {
|
|
1719
|
+
return kind === "revision" ? { result: "recorded", receipt: data } : { result: "recorded", adjustment_receipt: data };
|
|
1720
|
+
}
|
|
1721
|
+
function unchanged(kind, data) {
|
|
1722
|
+
return kind === "revision" ? { result: "unchanged", receipt: data } : { result: "unchanged", adjustment_receipt: data };
|
|
1723
|
+
}
|
|
1724
|
+
function failed(reporting_receipt_id) {
|
|
1725
|
+
return {
|
|
1726
|
+
result: "failed",
|
|
1727
|
+
reporting_receipt_id,
|
|
1728
|
+
errors: [{ code: "INVALID_REQUEST", message: GENERIC_RECEIPT_MESSAGE, recovery: "correctable" }]
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
function idempotencyConflict(reporting_receipt_id) {
|
|
1732
|
+
return {
|
|
1733
|
+
result: "failed",
|
|
1734
|
+
reporting_receipt_id,
|
|
1735
|
+
errors: [
|
|
1736
|
+
{
|
|
1737
|
+
code: "IDEMPOTENCY_CONFLICT",
|
|
1738
|
+
message: "Idempotency key was reused with different receipt content",
|
|
1739
|
+
recovery: "correctable"
|
|
1740
|
+
}
|
|
1741
|
+
]
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
function receiptResultId(result, entry) {
|
|
1745
|
+
if (result.result === "failed") return result.reporting_receipt_id;
|
|
1746
|
+
const body = "receipt" in result ? result.receipt : result.adjustment_receipt;
|
|
1747
|
+
return body?.reporting_receipt_id ?? entry.receipt.reporting_receipt_id;
|
|
1748
|
+
}
|
|
1749
|
+
function legacyReceiptBatchResult(value) {
|
|
1750
|
+
if (!value || typeof value !== "object" || !("result" in value)) return void 0;
|
|
1751
|
+
return value;
|
|
1752
|
+
}
|
|
1753
|
+
function subjectIdFor(entry) {
|
|
1754
|
+
return entry.kind === "revision" ? entry.receipt.reporting_revision_id : entry.receipt.reporting_adjustment_id;
|
|
1755
|
+
}
|
|
1756
|
+
function subjectKey(entry) {
|
|
1757
|
+
return entry.kind === "revision" ? `revision:${entry.receipt.reporting_revision_id}` : `adjustment:${entry.receipt.reporting_adjustment_id}`;
|
|
1758
|
+
}
|
|
1759
|
+
function duplicates(values) {
|
|
1760
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1761
|
+
const duplicate = /* @__PURE__ */ new Set();
|
|
1762
|
+
for (const value of values) (seen.has(value) ? duplicate : seen).add(value);
|
|
1763
|
+
return duplicate;
|
|
1764
|
+
}
|
|
1765
|
+
function digest(value) {
|
|
1766
|
+
return (0, import_node_crypto.createHash)("sha256").update((0, import_jcs.canonicalize)(value)).digest("hex");
|
|
1767
|
+
}
|
|
1768
|
+
function managedBindingFingerprint(binding) {
|
|
1769
|
+
const { created_at: _createdAt, semantic_fingerprint: _fingerprint, ...semantic } = binding;
|
|
1770
|
+
return digest(semantic);
|
|
1771
|
+
}
|
|
1772
|
+
function nonnegativeSafeInteger(value, name) {
|
|
1773
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
1774
|
+
}
|
|
1775
|
+
function positiveInteger(value, name) {
|
|
1776
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError(`${name} must be a positive safe integer`);
|
|
1777
|
+
}
|
|
1778
|
+
function policyNumber(value) {
|
|
1779
|
+
return value === null || value === void 0 ? null : Number(value);
|
|
1780
|
+
}
|
|
1781
|
+
async function advisoryLock(client, key) {
|
|
1782
|
+
await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [key]);
|
|
1783
|
+
}
|
|
1784
|
+
function authLock(accountId, destinationRef) {
|
|
1785
|
+
return `adcp-reporting-auth:${accountId}:${destinationRef}`;
|
|
1786
|
+
}
|
|
1787
|
+
function rfc3339Micro(expression) {
|
|
1788
|
+
return `to_char(${expression} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`;
|
|
1789
|
+
}
|
|
1790
|
+
function accountLock(accountId) {
|
|
1791
|
+
return `adcp-reporting-account:${accountId}`;
|
|
1792
|
+
}
|
|
1793
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1794
|
+
0 && (module.exports = {
|
|
1795
|
+
PostgresReportingManagedDeliveryStore,
|
|
1796
|
+
REPORTING_MANAGED_DELIVERY_MIGRATION
|
|
1797
|
+
});
|