@adcp/sdk 14.0.0-rc.37 → 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/content-mismatch.d.mts +126 -0
- package/dist/lib/reporting/content-mismatch.d.ts +126 -0
- package/dist/lib/reporting/content-mismatch.js +90 -0
- package/dist/lib/reporting/content-mismatch.mjs +66 -0
- 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/index.d.mts +3 -1
- package/dist/lib/reporting/index.d.ts +3 -1
- package/dist/lib/reporting/index.js +3 -0
- package/dist/lib/reporting/index.mjs +2 -0
- 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/reconciliation.d.mts +362 -2
- package/dist/lib/reporting/reconciliation.d.ts +362 -2
- package/dist/lib/reporting/reconciliation.js +1051 -7
- package/dist/lib/reporting/reconciliation.mjs +1053 -7
- 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/index.d.mts +22 -1
- package/dist/lib/schemas/index.d.ts +22 -1
- package/dist/lib/schemas/index.js +7 -0
- package/dist/lib/schemas/index.mjs +7 -0
- 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/validation/schema-loader.d.mts +9 -1
- package/dist/lib/validation/schema-loader.d.ts +9 -1
- package/dist/lib/validation/schema-loader.js +65 -8
- package/dist/lib/validation/schema-loader.mjs +64 -8
- 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 +80 -1
- package/docs/guides/MEDIA-BUY-3.2-COMPATIBILITY.md +57 -0
- package/docs/guides/MEDIA-BUY-ACTION-ASSESSMENT.md +300 -0
- package/docs/guides/REPORTING-LEDGER.md +654 -3
- package/docs/guides/REPORTING-RECONCILIATION.md +50 -0
- 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 +60 -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/SKILL.md +2 -0
- 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
|
@@ -2,6 +2,110 @@
|
|
|
2
2
|
|
|
3
3
|
`@adcp/sdk/reporting/ledger` turns a conforming reporting source into durable seller-side Reliable Reporting Core. It is separate from the buyer-side `reconcileReporting` API.
|
|
4
4
|
|
|
5
|
+
## Recommended: install the lifecycle service
|
|
6
|
+
|
|
7
|
+
`createReliableReportingService` is the adapter-first production path. A
|
|
8
|
+
provider adapter supplies one bounded slice fetch and two immutable offering
|
|
9
|
+
descriptions; the service reuses the PostgreSQL ledger, source executor,
|
|
10
|
+
producer, handlers, and decisioning-platform account resolver.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { Pool } from 'pg';
|
|
14
|
+
import { PostgresReportingLedgerStore } from '@adcp/sdk/reporting/ledger';
|
|
15
|
+
import { createReliableReportingService } from '@adcp/sdk/reporting/service';
|
|
16
|
+
import { createAdcpServerFromPlatform } from '@adcp/sdk/server';
|
|
17
|
+
|
|
18
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
19
|
+
const store = new PostgresReportingLedgerStore(pool, {
|
|
20
|
+
acknowledgeIsolatedDatabase: true,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const reporting = createReliableReportingService({
|
|
24
|
+
store,
|
|
25
|
+
adapters: {
|
|
26
|
+
// These descriptions are immutable declarations owned by your adapter.
|
|
27
|
+
gam: { sourceOffering: gamSourceOffering, deliveryOffering: gamDeliveryOffering,
|
|
28
|
+
fetchSlice: (slice, ctx) => gam.fetchDeliverySlice(slice, ctx) },
|
|
29
|
+
},
|
|
30
|
+
contact: { name: 'Reporting operations', email: 'reporting@example.com' },
|
|
31
|
+
automatedRecoveryWindowSeconds: 86_400,
|
|
32
|
+
statusRetentionDays: 90,
|
|
33
|
+
|
|
34
|
+
// `account` is the framework-resolved account, not request.account.
|
|
35
|
+
resolveSource: account => ({
|
|
36
|
+
adapterId: 'gam',
|
|
37
|
+
sourceScope: { network_id: account.ctx_metadata.gam.networkId },
|
|
38
|
+
sourceTimezone: account.ctx_metadata.gam.reportingTimezone,
|
|
39
|
+
}),
|
|
40
|
+
// Resolve from trusted commercial/account state. The result is frozen into
|
|
41
|
+
// the generation and every obligation; no request-body fallback exists.
|
|
42
|
+
resolveCurrency: account => account.ctx_metadata.gam.currency,
|
|
43
|
+
// The authorization boundary for shared upstream networks: return only the
|
|
44
|
+
// constituents this account may report on. Never echo the declaration.
|
|
45
|
+
resolveCoverage: async account => ({
|
|
46
|
+
constituents: await bookings.authorizedReportingConstituents(account.id),
|
|
47
|
+
}),
|
|
48
|
+
resolveConsumerId: ctx => {
|
|
49
|
+
if (!ctx.agent) throw new Error('Authenticated buyer-agent registry required');
|
|
50
|
+
return ctx.agent.agent_url;
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await pool.query(reporting.setup.migrations[0]);
|
|
55
|
+
const installedPlatform = reporting.install(platform);
|
|
56
|
+
const server = createAdcpServerFromPlatform(installedPlatform, serverOptions);
|
|
57
|
+
|
|
58
|
+
reporting.start({ intervalMilliseconds: 60_000, deploymentWide: true });
|
|
59
|
+
process.once('SIGTERM', () => void reporting.stop());
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The service advertises Reliable Reporting Core only. An inline adapter cannot
|
|
63
|
+
turn on Managed Delivery, Reconciled Billing, receipts, webhook activity, or
|
|
64
|
+
reporting notifications. `sync_reporting_status` is advertised only when
|
|
65
|
+
`resolveConsumerId` is installed and the supplied ledger implements its
|
|
66
|
+
atomic consumer-status methods. Follow-up work adds those higher tiers; do not
|
|
67
|
+
place them in a manual capability override.
|
|
68
|
+
|
|
69
|
+
Install a buyer declaration after the account and its media-buy scope have
|
|
70
|
+
been authorized and resolved. `installConfiguration` intentionally accepts no
|
|
71
|
+
account, `sourceScope`, contract, timezone, currency, `constituents`, or
|
|
72
|
+
`mediaBuyIds` fields from the declaration. Pass the framework-resolved
|
|
73
|
+
`ctx.account`; trusted callbacks derive the remaining lineage. `resolveCoverage`
|
|
74
|
+
is the media-buy/package authorization boundary and must derive the denominator
|
|
75
|
+
from the resolved account: `sourceScope` may legitimately name a shared upstream
|
|
76
|
+
network, in which case the constituent list is the only thing keeping one
|
|
77
|
+
buyer's orders out of another buyer's report. `mediaBuyIds` is always derived
|
|
78
|
+
from the returned constituents, so a buyer-named order ID can never reach
|
|
79
|
+
`fetchSlice`. `expectedCurrency`, `expectedSourceTimezone`, and
|
|
80
|
+
`expectedMediaBuyIds` are optional assertions and fail closed on conflict.
|
|
81
|
+
The service rejects credential-shaped keys and `ctx_metadata` anywhere in the
|
|
82
|
+
returned `sourceScope`, then applies the source contract and existing ledger
|
|
83
|
+
immutability checks. Return the resulting secret-free configuration state from
|
|
84
|
+
your `sync_accounts` implementation.
|
|
85
|
+
|
|
86
|
+
For tenant-partitioned jobs, call `runCycle({ accountId })`, or configure
|
|
87
|
+
`start({ accountIds: [...] })`. Every planner and worker call receives that
|
|
88
|
+
same account boundary. A deployment-owned worker must explicitly pass
|
|
89
|
+
`deploymentWide: true`; use that form only when one trusted service instance is
|
|
90
|
+
authorized for every account in the store. Widening is reachable only through
|
|
91
|
+
that opt-in: a cycle with a missing, empty, or overlong `accountId` is refused
|
|
92
|
+
rather than silently promoted to a deployment-wide scan. Under `accountIds`,
|
|
93
|
+
one account's failed cycle is reported to `onError` and the remaining accounts
|
|
94
|
+
still run, so a persistently failing tenant cannot starve the tenants behind
|
|
95
|
+
it. Planning is resumable and bounded;
|
|
96
|
+
set `maxObligationsPerAccount` and `maxWorkerIterationsPerAccount` for tighter
|
|
97
|
+
operational limits. `stop()` aborts current source work, waits for settlement,
|
|
98
|
+
and wakes a sleeping scheduler immediately. Planning is a bounded ledger
|
|
99
|
+
operation rather than abortable source I/O, so shutdown waits for an in-flight
|
|
100
|
+
planning pass to settle and does not begin its worker afterward.
|
|
101
|
+
|
|
102
|
+
Run `runReliableReportingServiceConformanceV1` against an isolated test ledger
|
|
103
|
+
before deployment. It covers source replay, two-account isolation,
|
|
104
|
+
configuration replay/frozen currency, lifecycle start/stop, and Core
|
|
105
|
+
capability truthfulness.
|
|
106
|
+
|
|
107
|
+
## Advanced: assemble the primitives directly
|
|
108
|
+
|
|
5
109
|
```ts
|
|
6
110
|
import { Pool } from 'pg';
|
|
7
111
|
import {
|
|
@@ -39,15 +143,562 @@ Pass `getReportingStatus` and `getMediaBuyDelivery` directly to the matching `cr
|
|
|
39
143
|
|
|
40
144
|
The planner uses fixed millisecond periods and an explicitly frozen IANA source timezone. Calendar or billing-cycle schedules should be expanded by the seller into immutable period boundaries before installation; the SDK intentionally has no Temporal dependency. At period end, the obligation freezes the constituent denominator and coverage. A zero-row source object commits like any other revision. Absence remains an empty revision association. A deployment with per-tenant workers should pass the resolved `account_id` to both `planObligations()` and `runWorker()`; omitting it intentionally runs a deployment-wide worker.
|
|
41
145
|
|
|
42
|
-
|
|
146
|
+
### Migrating an existing manual lifecycle
|
|
147
|
+
|
|
148
|
+
Keep the same `PostgresReportingLedgerStore` and run the same
|
|
149
|
+
`REPORTING_LEDGER_MIGRATION`; there is no second store and no data migration.
|
|
150
|
+
Move each inline fetch plus its source/delivery offering into an entry in
|
|
151
|
+
`adapters`, move account routing and currency lookup into the two trusted
|
|
152
|
+
resolvers, and replace manual producer/handler/capability assembly with
|
|
153
|
+
`reporting.install(platform)`. Replace cron calls to `planObligations` and
|
|
154
|
+
`runWorker` with `runCycle` or `start`. Remove manual reporting capability
|
|
155
|
+
overrides so discovery has one owner. Existing configuration IDs and semantic
|
|
156
|
+
fingerprints remain compatible because the service delegates installation to
|
|
157
|
+
the existing producer: replaying a generation that predates the reserved
|
|
158
|
+
adapter route key reuses its stored `sourceScope` verbatim, so the fingerprint
|
|
159
|
+
still matches and the replay does not trip generation immutability. Only new
|
|
160
|
+
generations carry the reserved key. With one installed adapter, pre-service
|
|
161
|
+
obligations without the reserved adapter route continue through that sole
|
|
162
|
+
adapter. A
|
|
163
|
+
multi-adapter migration must create a new immutable configuration generation
|
|
164
|
+
with an explicit route. An adapter supplies exactly one of `fetchSlice` or
|
|
165
|
+
`executor`: `fetchSlice` is the inline boundary, and `executor` accepts any
|
|
166
|
+
`ReportingSourceWithReaderV1` — including a paginated, asynchronous, or
|
|
167
|
+
externally staged one — so a custom executor that needs those capabilities
|
|
168
|
+
runs under the service today rather than waiting for a future extension. The
|
|
169
|
+
inline-only knob `inlineReplayRetention` applies to `fetchSlice` adapters and
|
|
170
|
+
is ignored by an adapter that brings its own executor.
|
|
171
|
+
|
|
172
|
+
`reporting.install(platform)` mutates that platform object in place and
|
|
173
|
+
requires it to be extensible; this preserves class instances and private-field
|
|
174
|
+
methods that a shallow wrapper would break. It also requires the platform's
|
|
175
|
+
native `accounts.upsert` seam. The service cannot truthfully advertise `configuration_task:
|
|
176
|
+
sync_accounts` without it. That account method remains responsible for mapping
|
|
177
|
+
an authorized wire reporting configuration to the service's resolved input and
|
|
178
|
+
calling `installConfiguration`; the service does not claim a generic mapping
|
|
179
|
+
that the current protocol does not define.
|
|
180
|
+
|
|
181
|
+
Official configurations also pin a `finalityPolicy` (`policyId` plus `source_final` or `contractual_cutoff`). For `source_final`, set `sourceSignal` to the exact opaque signal identifier the adapter places in the manifest finality evidence's `evidenceRef`; the worker requires an exact match before irreversible official publication. `expected_at` and the wire delivery SLA use the same official deadline: the service refuses a configuration whose `officialAfterMilliseconds` disagrees with the `schedule.delivery_sla` its offering advertises, and omitting the field derives that same advertised value.
|
|
182
|
+
|
|
183
|
+
Every revision stores its rows together with an RFC 8785 JCS SHA-256 binding and exact decimal control totals for requested numeric metrics. A revision number and obligation are immutable. Official revisions are terminal; later source corrections are immutable adjustments bound to the official revision, never superseding revisions. Status snapshots omit row payloads, are capped at 8 MiB, expire after 15 minutes, and keep cursor pages stable over the flat obligation/revision/adjustment union. A periods response returns an opaque `changes_checkpoint`; echo that value verbatim as `changes_after` rather than supplying a timestamp. Account-scoped write/snapshot locks make those checkpoints gap-free for SDK store writes. The default table set is deployment-wide; use a dedicated database/schema and acknowledge that boundary explicitly. `sourceScope` must contain opaque routing identities only—never credentials or bearer tokens—because it is retained with the obligation. Retained resource locations are held to the same rule, and at the seam that persists them rather than only in the worker's pre-flight, because a caller driving `settleMaterialization` directly would otherwise store a presigned location that `get_reporting_status` then publishes. Query and fragment are refused on the raw string rather than on a successful parse, because a relative path carrying a presigning query never parsed as a URL at all; userinfo is refused as a colon-separated pair before the `@`, which is the credential shape, plus any `http(s)` userinfo at all. A blanket `@` rule would refuse `abfss://container@account.dfs.core.windows.net/...` and a Snowflake stage reference, neither of which carries a secret.
|
|
184
|
+
|
|
185
|
+
## Managed Delivery and Reconciled Billing
|
|
186
|
+
|
|
187
|
+
Core remains the default and has no destination, external-resource, or receipt dependency. To opt into the higher tiers, apply `REPORTING_MANAGED_DELIVERY_MIGRATION` **after** `REPORTING_LEDGER_MIGRATION`, explicitly construct the Core store with `managedDelivery: true`, create a `PostgresReportingManagedDeliveryStore`, and pass both stores with a destination adapter to `createReportingManagedDeliveryRuntime`. The async factory proves the stores share one authority and validates the RC3 tier wiring before returning it. It advertises `managed_delivery` only when an immutable binding, delivery, bounded resource reading, generation-fenced revocation, and at least one verification profile are installed. The advertised automated recovery window must be at least the widest installed managed Core configuration recovery window. It advertises `reconciled_billing` and `receipt_task` only when an authenticated consumer resolver and canonical-digest verification are also installed.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import {
|
|
191
|
+
PostgresReportingLedgerStore,
|
|
192
|
+
PostgresReportingManagedDeliveryStore,
|
|
193
|
+
REPORTING_MANAGED_DELIVERY_MIGRATION,
|
|
194
|
+
createReportingManagedDeliveryRuntime,
|
|
195
|
+
reportingManagedDeliveryBindingV1,
|
|
196
|
+
} from '@adcp/sdk/reporting/ledger';
|
|
197
|
+
|
|
198
|
+
await pool.query(REPORTING_MANAGED_DELIVERY_MIGRATION);
|
|
199
|
+
const managedStore = new PostgresReportingManagedDeliveryStore(pool);
|
|
200
|
+
const managedCoreStore = new PostgresReportingLedgerStore(pool, {
|
|
201
|
+
acknowledgeIsolatedDatabase: true,
|
|
202
|
+
managedDelivery: true,
|
|
203
|
+
});
|
|
204
|
+
await managedStore.authorizeDestination({
|
|
205
|
+
account_id: internalAccountId,
|
|
206
|
+
destination_ref: destinationRef,
|
|
207
|
+
generation: 1,
|
|
208
|
+
authorized_at: new Date().toISOString(),
|
|
209
|
+
});
|
|
210
|
+
await managedStore.installBinding(
|
|
211
|
+
reportingManagedDeliveryBindingV1({
|
|
212
|
+
// Binds the Core configuration generation installed above. That
|
|
213
|
+
// configuration's `schedule.recoveryWindowMilliseconds` is 3_600_000, which
|
|
214
|
+
// is what `automatedRecoveryWindowSeconds: 3600` below must meet or exceed.
|
|
215
|
+
...bindingForInstalledCoreConfiguration,
|
|
216
|
+
account_id: internalAccountId,
|
|
217
|
+
destination_ref: destinationRef,
|
|
218
|
+
authorization_generation: 1,
|
|
219
|
+
})
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
type SellerContext = { account?: unknown; agent: { agent_url: string } };
|
|
223
|
+
const managed = await createReportingManagedDeliveryRuntime<SellerContext>({
|
|
224
|
+
coreStore: managedCoreStore,
|
|
225
|
+
store: managedStore,
|
|
226
|
+
adapter: destinationAdapter,
|
|
227
|
+
offerings: reportingDeliveryOfferings,
|
|
228
|
+
resolveConsumerId: context => context.agent.agent_url,
|
|
229
|
+
// MUST be at least the widest installed managed binding's Core
|
|
230
|
+
// `schedule.recoveryWindowMilliseconds` / 1000 — see the precondition note below.
|
|
231
|
+
automatedRecoveryWindowSeconds: 3600,
|
|
232
|
+
statusRetentionDays: 90,
|
|
233
|
+
resourceRetentionDays: 30,
|
|
234
|
+
authorizationRevocationSeconds: 60,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Supply these to the matching server slots/capability document.
|
|
238
|
+
const { getReportingStatus, getMediaBuyDelivery, syncReportingReceipts } = managed;
|
|
239
|
+
const reportingDelivery = managed.reportingDeliveryCapabilities;
|
|
240
|
+
|
|
241
|
+
// Run from a durable scheduler; replicas safely share SKIP LOCKED leases.
|
|
242
|
+
await managed.runWorker({ maxIterations: 100 });
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`automated_recovery_window_seconds` is published once per agent, in one capability document, while Core `schedule.recoveryWindowMilliseconds` is per configuration. The advertised value is a **maximum** — the longest a due obligation may stay `delayed` while automated recovery continues before it becomes `action_required` — so one agent-wide number is truthful exactly when it is at least every installed window. `createReportingManagedDeliveryRuntime` enforces that bound and nothing more: advertising less than the widest installed window is refused with the offending value named, advertising more is conservative and allowed, sub-second Core windows are rounded up to the whole second the capability is expressed in, and a deployment with no managed binding — a fresh install, or one that has just offboarded its last managed tenant — starts normally. Heterogeneous tenants behind one agent therefore need no separate endpoint per cohort: advertise the widest window they run. The bound is enforced on the write path as well as at startup — `adoptAdvertisedPolicies` validates existing bindings while exclusively locking the same durable policy sentinel as `installBinding`, and `installBinding` refuses a later Core configuration whose recovery window exceeds the durable bound. `listInstalledRecoveryWindowSeconds` is optional direct-store introspection, not an authoritative publication check: an install can otherwise land between a list and a later adoption.
|
|
246
|
+
|
|
247
|
+
All four capability promises are durable and database-wide. The atomic hook adopts recovery and authorization-revocation maximums in the stronger, decreasing direction, and status and resource-retention minimums in the stronger, increasing direction. It returns those effective values so a weaker replica still runs its worker to the strongest policy already registered; PostgreSQL also enforces resource retention at settlement and authorization revocation at claim time for direct callers. A rejected binding check or policy write leaves all four columns unchanged, and a later replica can never weaken an adopted promise. Custom stores used by `createReportingManagedDeliveryRuntime` must provide the same atomic, binding-fenced contract; the optional separate recovery/status hooks remain only for compatible direct-store use and are not sufficient for capability publication. Apply `REPORTING_MANAGED_DELIVERY_MIGRATION` on upgrade as well as first install: it adds the resource-retention and revocation columns to an existing two-column registry without replacing prior promises.
|
|
248
|
+
|
|
249
|
+
Authorize a destination generation and install its immutable binding before creating any obligation for that Core configuration. Once an obligation is observable, only an exact idempotent replay of the existing binding is accepted, so the managed tier cannot appear outside the Core changes checkpoint. Build the binding with `reportingManagedDeliveryBindingV1`, which binds the exact internal account, Core configuration generation, destination authorization generation, feed purpose, method, verification profile, reconciliation mode, and resource-retention promise. That promise is a floor rather than a default at settlement: an explicit `minimum_resource_retention_days` may tighten it, never waive it, and a negative value is refused. Only a failed outcome is exempt from it — a successful one must name an expiry the database can check, or a direct store caller could persist a permanently successful materialization whose bytes no reader can fetch and which nothing revisits. It is judged only by the clock that stores the row — `assertMaterializationOutcome` deliberately does not re-check the horizon against the worker's clock, because a worker running ahead of the database then refused a resource the database considers well inside the window and reported it as `DELIVERY_FAILED`. Every claim of a pending materialization counts toward the delivery-attempt cap, and the worker calls `failExhaustedMaterializations` before planning so a row that used them all is failed rather than left pending — that sweep takes the same account lock the lifecycle apply holds, in the canonical order, because it mutates exactly the state the lifecycle compare-and-set fences and could otherwise commit `pending` -> `failed` between a matching state-version check and that apply's commit; it mutates only the accounts it locked, so a lease that expires while it waits on another account's lock is left for the next sweep rather than failed unfenced — which the planner treats as work in flight, making the revision neither claimable nor replannable. That sweep re-checks eligibility against the row it finally locked rather than only the set it selected, so a settlement that commits while the sweep waits on the row is not overwritten with `DELIVERY_ATTEMPTS_EXHAUSTED`. Revocation commits the durable deny first, fails queued work, and makes resource reads and new receipt evidence fail closed; provider-side grant cleanup is a separately leased worker action. Reauthorization uses a strictly greater generation and a new Core configuration generation. It never re-enables an old binding.
|
|
250
|
+
|
|
251
|
+
The worker claims and commits through short PostgreSQL transactions but performs destination I/O outside them. Pass `authorizationRevocationSeconds` to `runWorker()` when using a direct store; `createReportingManagedDeliveryRuntime` uses the strongest value returned by atomic adoption. PostgreSQL's claim boundary independently applies the durable minimum, so a different replica cannot widen or omit the promise: a cleanup attempt is clipped so it cannot run past `revoked_at` plus the window, a failed attempt's retry lease never outlasts the window, and a grant that has already outlived it is returned as `revocationsOverdue` for alarming. Every SLA fact — `revoked_at`, the remaining window, and whether a grant is overdue — comes from the database that committed the revocation, never the worker host, and the exact boundary counts as overdue. A crashed worker's lease is reclaimable at that boundary even before it expires, with lease generation fencing the old holder — but only once the holder has had a full attempt's worth of time, so live workers cannot steal from each other on an already-late grant and leave cleanup permanently uncommitted. `revoked_at` and `authorized_at` are both written by the committing database, never by the caller, and the strongest of the binding, caller and durable resource-retention floors is judged entirely in SQL against that clock; a settle the database refuses as under-retained is terminalized rather than left pending forever. A failed attempt gives up its lease under a short retry backoff rather than holding it for a full lease or clearing it outright — clearing it let the next iteration of the same worker tick reclaim the same grant, so one broken provider consumed every iteration — so the grant stays reclaimable and its elapsed SLA is visible rather than masked by a lease that has not expired; `claimRevocation` orders by cleanup lease generation, which the failed attempt already incremented, so releasing cannot let one broken grant starve the queue. Delivery, revocation, and resource reads have hard SDK deadlines even when an adapter ignores cancellation; resource descriptors are limited to 1 MiB and resource bodies to 64 MiB by default. Adapters must advertise `revocationFencesDeliveryGenerations: true`, make the logical `(configuration generation, revision, destination generation)` write idempotent, and install a provider-side generation tombstone before `revoke()` returns. Every delivery must be keyed by that generation and refuse a tombstoned generation, including a late provider write that completes after the SDK timed out. A successful commit requires the lease generation and unexpired lease, current authorization, exact Core row count and control totals, all evidence required by the selected verification profile, the official canonical digest when required, an immutable native version reference when applicable, and an `expires_at` satisfying the configured retention window.
|
|
252
|
+
|
|
253
|
+
`sync_reporting_receipts` derives both account and consumer from authenticated context, and `received_at` is the server's to assign: a request carrying it is refused with a typed `VALIDATION_ERROR` rather than quietly stripped, so the handler and the request schema agree on what a valid request is. RC3 caps `receipts` and `adjustment_receipts` at 100 each in JSON Schema, and bounds the batch as a whole in the request's `x-adcp-validation.batch_identity`: receipt IDs must be unique across both arrays, "whose combined length MUST NOT exceed 100". That annotation is normative prose rather than machine-checked on this path — `x-adcp-validation` is registered as an AJV keyword for commercial terms only — so the handler enforces the combined bound itself. A request that satisfies both per-array caps but exceeds 100 combined is therefore spec-invalid — and unanswerable regardless, since `results` is capped at 100 while one result per submitted receipt is required — so it is refused with a `VALIDATION_ERROR` envelope rather than an over-long `results` array. A finality filter hides managed evidence with the revision it names: returning a materialization or receipt for a revision `finality: official` omitted left the response carrying public references to a revision it does not contain. An adjustment receipt and the adjustment it names are separate pagination items, so a small `max_results` puts them on different pages; the named adjustment therefore travels with the receipt as context on whichever page carries it, and the RC3 rule that a receipt may not appear without the correction it names holds per page. Only items are counted by the cursor, so paging itself is unchanged. Its PostgreSQL implementation serializes each caller namespace, records a compact per-entry verdict for idempotency replay — receipt bodies are rehydrated from the append-only receipt table rather than duplicated — and retains those replay rows for 30 days so a consumer at the per-consumer batch cap is throttled instead of permanently locked out. An exact same-key replay is side-effect free and returns the caller its own recorded verdict even after the destination authorization is revoked. A byte-identical re-presentation of an already-recorded receipt under a fresh idempotency key likewise returns `unchanged`: both paths resolve immutable, caller-owned state and write no new evidence. Revocation governs what may be newly *accepted*, not what the caller's own stored identity answers, so no other consumer's state is read; genuinely new evidence while revoked is still refused. It exposes append-only histories only to that consumer. Revision receipts must name the exact obligation, successful materialization, current authorization, and verification evidence, and a revision whose finality satisfies the obligation's own `required_finality`, so a snapshot-finality contract reconciles on the same terms an official one does. `billing` feeds are not such a contract: RC3 requires `required_finality: official` for them unconditionally, and both `installConfiguration` and `installBinding` refuse the combination for a new generation — an immutable generation that predates the rule reinstalls unchanged, because the replay is resolved before either the offering or the validation — the binding revalidates the referenced Core configuration atomically, because a generation created before that rule existed still sits in the database — so a terminal accepted billing receipt can never land against a provisional revision. Accepted leaves are terminal; a rejected leaf may be repaired only by an exact `supersedes_reporting_receipt_id` — and a leaf whose body has been pruned still owns its subject, so the successor named by its tombstone is admitted while a fresh root carrying the same content is not. Entries that resolve to an already-stored receipt are resolved before the batch's duplicate-subject rule is applied, because such an entry writes nothing and is not competing for the subject: a batch carrying an existing rejected receipt together with its own correction records the correction rather than failing both. Duplicate receipt IDs remain a property of the whole submitted batch. A request with no receipts at all is refused with a typed `VALIDATION_ERROR` before any per-entry refusal, including a wrong-account one, because `results` has `minItems: 1` and a per-entry answer to an empty batch is an empty — schema-invalid — body. Later source corrections remain Core adjustments and receive separate append-only adjustment receipts—neither official revisions nor earlier receipts are rewritten. The per-consumer receipt cap is admission control on new rows, applied at the point one would be inserted: an exact resubmission of a receipt that already exists stores nothing, so it replays as `unchanged` at the cap rather than flipping to `failed` on the row count alone, and a refusal for capacity leaves the current leaf undemoted. Lookup failures intentionally share one generic error so the task cannot probe another account's retained objects.
|
|
254
|
+
|
|
255
|
+
The lifecycle compare-and-set covers managed state as well as Core evidence. The projection reads managed rows in their own transaction, so `getManagedLifecycleProjection` returns a `managedStateVersion` token over the obligation's materializations, receipts (including current-leaf flips), adjustments and destination-authorization state; `applyLifecycleProjection` re-reads it inside the apply transaction and refuses a stale apply. Without it a revocation, receipt or settled materialization arriving between projection and apply would be overwritten by a health computed before it existed — most visibly as a `complete` persisted and webhooked over a receipt that had just arrived. Omit `ledgerAsOf` outside a deadline sweep and the store resolves its own cutoff: a host `toISOString()` is millisecond-truncated while these columns are microsecond, so a caller-taken "now" sorts before a row written in the same millisecond and silently drops it. A pinned cutoff is also clamped to the ledger's own clock — only where there is one to clamp against; a store with no `readLedgerInstant` takes a first-attempt pin exactly as given, because `now` is precisely what pinning overrides — at the reconciler and again when the watermark is written, and a store with its own clock uses nothing but that clock on a compare-and-set retry. When a store resolves a different instant than the one it was asked for, that resolved instant is what the transition and the watermark use — the projection was computed at it, so stamping the caller's value instead watermarked a moment later than the read and buried everything in between — taking the later of the two put the caller's future pin back the moment the first attempt lost its race. A host running fast pinned an instant the database had not reached, the watermark took that instant, and every database-timestamped change inside the skew was then permanently behind it — excluded from the projection that wrote it and never due again, which left a `complete` transition and its webhook standing over a revocation that had already contradicted it. The producer's own reconciles pass their host instant as a fallback clock rather than as a pin for the same reason. Every comparison against the cutoff runs in SQL for the same reason. The projection reads and the token are taken in one `REPEATABLE READ` snapshot, so a settle committing between them cannot pair a pre-settle health with a post-settle token — the one combination the CAS would otherwise accept. A refused apply is recomputed against a **fresh** authoritative cutoff and retried immediately, bounded; the cutoff never moves backwards. The token covers materializations, receipts including current-leaf flips, adjustments, consumer statuses and destination-authorization state. The externally supplied obligated-consumer roster cannot be re-read inside the apply transaction, so it is versioned separately and re-checked immediately before the apply — every sweep refreshes a bounded slice of its managed obligations' roster versions before selecting, paging on a cursor the refresh itself advances — on failure as well as success, so a tenant whose authorization service is down yields its slot instead of occupying it every sweep, and a difference from the version last reconciled is itself a due condition. The apply fences on that observation too: it locks the obligation's lifecycle row, refuses when the published version has moved since the re-check, and never writes its own version over a newer one — otherwise a refresh landing in that window was overwritten by the version the projection had used, and since due-ness is exactly "observed differs from processed", the roster change had nothing left to re-arm from. Publishing only inside a reconcile was circular — the reconcile needs the obligation to already be due, which is what the roster change was meant to cause. Supply `version` from `obligatedConsumers`, or its resolved content is hashed for you — the projection and the re-check hash the same set, so an unversioned roster converges instead of burning its retry budget. A roster returned with `complete: true` is authoritative and excludes principals it does not list — in the lifecycle fold as well as the live projection and the digest — which is what stops a same-account principal inserting itself into the obligated set by posting a receipt. An incomplete roster is only a hint, so observed principals are still unioned in. Mutable managed state is placed at that cutoff rather than at now: `changed_at` after the cutoff means the materialization was still `pending` then, and a revocation counts only once `revoked_at` is at or before it, so a later settlement is never backdated into an earlier transition.
|
|
256
|
+
|
|
257
|
+
Managed-only changes are lifecycle candidates in their own right. A settlement, revocation, receipt, adjustment, consumer status, external-roster change or resource expiry after the last reconcile makes the obligation due, so a persisted `complete` cannot outlive a live status that has since degraded — Core deadlines alone would never reschedule it. Candidacy is measured against a per-obligation watermark in `adcp_reporting_lifecycle_state`, written on every reconcile including one that changes no health, and stamped with the cutoff the projection read at rather than the commit instant. Sweeps take that cutoff from the ledger's own clock rather than the worker host, so a fast host cannot stamp a future watermark and bury database-timestamped work committed inside the skew — anything recorded after that cutoff stays due instead of being buried by the write that follows it; without it a change with no health effect keeps the obligation due forever and, in a fair-ordered page, starves everything behind it. Sweeps reconcile each obligation in isolation, so one tenant's failing roster callback cannot abort the obligations queued after it, and a failure records an exponential backoff cursor so a page of failing tenants yields its slots instead of monopolising every sweep — including a reconcile that exhausts its compare-and-set retry budget, which is a failure rather than a completion. The backoff never advances the watermark, so the work stays visible as unresolved.
|
|
258
|
+
|
|
259
|
+
`MAX_MATERIALIZATIONS_PER_ACCOUNT` and `MAX_RECEIPTS_PER_CONSUMER` are lifetime counts by default. Because managed evidence is immutable, a long-lived account eventually reaches them and then stops planning materializations and refuses every receipt with no way back. Construct the store with `new PostgresReportingManagedDeliveryStore(pool, { evidenceRetentionDays })` to make the caps active-scope: only evidence recorded inside that window counts against them, and `pruneExpiredEvidence({ account_id, limit })` deletes what has fallen outside it — never anything still inside the window, never a `pending` or leased materialization, and never a receipt batch inside its own replay retention. Retention has a floor: at least the 30-day receipt replay retention, and at least the strongest durable status or resource-retention horizon registered for the database. Adoption and binding installation exclusively lock the migration-created policy sentinel before account/binding locks; settlement, revocation claim, and pruning hold shared locks on it through their writes. A stronger promise therefore cannot land between a weaker check and its write, while unrelated hot-path readers remain concurrent. Pruning genuinely does its selection before taking the account lock, and asks which receipts a live replay row still names by expanding those rows once rather than posing a containment question per candidate — a shape no index could serve, which held the lock long enough to fail concurrent Core writes with 55P03. Because that selection is unlocked it is a proposal rather than a verdict: every victim is revalidated under the account lock against current live replay rows, surviving receipts and unexpired resources before it is deleted, so a replay that commits in the gap keeps the receipt it promised to reproduce. The revalidation is narrowed to the candidates' own consumers and keys, so the lock still holds no per-candidate scan. The registry lock is held to commit inside the transaction that acts on it, and a precondition such as the installed-window check runs inside that same transaction so a refusal rolls the write back rather than leaving a durable promise nobody validated, so an install cannot read an empty registry while a narrower promise is being registered, and a prune cannot approve a horizon another replica is about to widen. Pruning deletes and tombstones in one statement against one frozen cutoff, so a moving boundary can never leave a deleted row without its permanent identity. Successive prunes demote the tombstones they supersede, so a subject has exactly one tombstone still claiming to be its leaf and a chain pruned twice resolves the same way every time. Re-presenting a pruned receipt byte-identically answers `unchanged` with the instant the tombstone kept, resolves that receipt's own tombstone before any rule about its subject — so an exact re-presentation is not refused as new content for a settled subject — replays the same answer under the same idempotency key by rehydrating from the tombstone when the body is gone, counts as already-resolved for the batch's duplicate-subject rule, and never recreates the row: recreating it restarted the retention clock, undid the storage the prune reclaimed, and moved the published `received_at` to a moment the consumer never filed anything at. A pruned receipt ID can never bind different content, a subject whose accepted leaf expired can never reopen — the read projection consults tombstones, not just live rows — and materialization attempt history survives as a compact terminal record, so a revision that exhausted its attempts or already succeeded does not restart at attempt 1 once its rows age out. Both conclusions — the acceptance and the fact a delivery succeeded — are folded into the live, filtered and lifecycle projections alike. Counters describe exactly the records the response emits, never more and never less, so a buyer recomputing the association cannot see `ASSOCIATED_HISTORY_INCOMPLETE`; retention therefore refuses to prune an acceptance while the resource it accepts is still readable, which is what keeps a `complete` period from having nothing to show for itself. An acceptance carries the consumer that gave it, so one consumer's pruned acceptance never settles the obligation on another's behalf — including the anonymous fail-safe consumer, which owns no acceptances at all. Pruning also removes the expired replay row before the receipts it names, keeps any receipt a surviving replay row still references — a later batch can name an earlier receipt — and keeps any materialization whose resource is still readable or that a retained receipt names as its evidence. An adjustment receipt names no materialization — it names the revision it corrects — so the readable-resource hold matches on that revision as well, or a period whose revision resource was still readable lost the adjustment acceptance behind its own `complete`. Run it from the same scheduler that runs the worker. Lifecycle receipt reads are driven from the obligation's own subjects into a `(subject_id, receipt_kind, recorded_at)` index rather than asking the global receipt table which of its rows belong to an obligation — a question that supplies neither account nor consumer, so every due obligation re-scanned the whole receipt history. The managed state digest, the obligated-consumer roster read and the permanent delivery tombstones do the same — consumer statuses carry an `obligation_id` index and materialization tombstones an `(obligation_id, reached_success, reached_success_at)` one — because that digest is read inside the apply transaction while it holds the account lock: a scan there is a scan with the lock held, which pushed concurrent Core writes past their lock timeout and failed them with 55P03.
|
|
260
|
+
|
|
261
|
+
The managed tables are additive and do not alter the Core tables. This is the schema boundary coordinated with #2943: that work owns transactional reporting notification/activity intent and the existing webhook delivery/credential plane. Managed Delivery does not create a second webhook sender, outbox, credential store, or subscriber model. Apply both feature migrations after the Core migration in either order; each owns separate tables and both reuse the Core authority.
|
|
262
|
+
|
|
263
|
+
## Transactional status notifications and account activity
|
|
264
|
+
|
|
265
|
+
Production deployments can join every health or observed-finality transition to
|
|
266
|
+
a compact account-operator activity record. Health transitions additionally
|
|
267
|
+
create one durable, schema-conformant `reporting.status_changed` intent;
|
|
268
|
+
finality-only changes remain internal activity because the AdCP event is defined
|
|
269
|
+
only for health changes:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import { createPostgresPersistentNotificationRuntime } from '@adcp/sdk/server';
|
|
273
|
+
import {
|
|
274
|
+
createPostgresReportingNotificationActivityRuntime,
|
|
275
|
+
createPostgresReportingNotificationAttemptCheckpoint,
|
|
276
|
+
PostgresReportingLedgerStore,
|
|
277
|
+
REPORTING_LEDGER_FINALITY_WRITER_FENCE_MIGRATION,
|
|
278
|
+
REPORTING_LEDGER_MIGRATION,
|
|
279
|
+
} from '@adcp/sdk/reporting/ledger';
|
|
280
|
+
|
|
281
|
+
// Build the durable pre-POST checkpoint first. The notification runtime needs
|
|
282
|
+
// it, and the activity runtime verifies it targets the same durable store —
|
|
283
|
+
// a mismatched pair checkpoints nothing and loses notifications silently, so
|
|
284
|
+
// both are refused at construction.
|
|
285
|
+
const attemptCheckpoint = createPostgresReportingNotificationAttemptCheckpoint({
|
|
286
|
+
db: pool,
|
|
287
|
+
namespace: 'seller-production',
|
|
288
|
+
});
|
|
289
|
+
const notifications = createPostgresPersistentNotificationRuntime({
|
|
290
|
+
db: pool,
|
|
291
|
+
publisherScope: 'seller-production',
|
|
292
|
+
checkpointDeliveryAttempt: attemptCheckpoint,
|
|
293
|
+
subscriptions: { acknowledgeIsolatedDatabase: true },
|
|
294
|
+
...notificationOptions, // proof, protected credentials, webhooks, authorization
|
|
295
|
+
});
|
|
296
|
+
const reportingActivity = createPostgresReportingNotificationActivityRuntime({
|
|
297
|
+
db: pool,
|
|
298
|
+
notifications,
|
|
299
|
+
// Use a deployment-unique value whenever a PostgreSQL schema is shared.
|
|
300
|
+
namespace: 'seller-production',
|
|
301
|
+
// The same checkpoint, so its store can be verified against this runtime's.
|
|
302
|
+
attemptCheckpoint,
|
|
303
|
+
// Pure host-owned mapping from an internal ledger account. Never derive
|
|
304
|
+
// this from transition data, an incoming request body, or ctx_metadata.
|
|
305
|
+
tenantScopeForAccount: accountId => durableAccountDirectory.tenantFor(accountId),
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Rolling-deployment order: ledger and notification tables, activity table,
|
|
309
|
+
// drain legacy pending transitions, then application code configured with the port.
|
|
310
|
+
await pool.query(REPORTING_LEDGER_MIGRATION);
|
|
311
|
+
for (const sql of notifications.migrations.all) await pool.query(sql);
|
|
312
|
+
for (const sql of reportingActivity.migrations.all) await pool.query(sql);
|
|
313
|
+
|
|
314
|
+
// Before enabling the port, keep legacy subscribers configured and run
|
|
315
|
+
// retryReportingStatusNotificationsV1() until listPendingTransitions() is empty.
|
|
316
|
+
// The transactional store fails closed if legacy pending rows remain.
|
|
317
|
+
|
|
318
|
+
// Last cutover step, only once no pre-SDK-14 writer is still serving: fence
|
|
319
|
+
// finality-less transitions out of the log. A surviving legacy writer now fails
|
|
320
|
+
// closed on append instead of silently adding another redundant finality event.
|
|
321
|
+
await pool.query(REPORTING_LEDGER_FINALITY_WRITER_FENCE_MIGRATION);
|
|
322
|
+
|
|
323
|
+
const store = new PostgresReportingLedgerStore(pool, {
|
|
324
|
+
acknowledgeIsolatedDatabase: true,
|
|
325
|
+
notificationActivityPort: reportingActivity.port,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Do not also pass legacy ReportingLedgerSubscriberV1 callbacks to lifecycle
|
|
329
|
+
// reconciliation. The transactional port is the sole notification handoff.
|
|
330
|
+
|
|
331
|
+
await reportingActivity.probe();
|
|
332
|
+
await notifications.probe();
|
|
333
|
+
|
|
334
|
+
// Run both bounded calls repeatedly from the deployment's durable scheduler.
|
|
335
|
+
await reportingActivity.recoverOnce({
|
|
336
|
+
ownerToken: process.env.INSTANCE_ID!,
|
|
337
|
+
onError: (error, claim) => operationalLogger.error({ error, claim }),
|
|
338
|
+
});
|
|
339
|
+
await notifications.recoverOnce({ ownerToken: process.env.INSTANCE_ID! });
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
`applyLifecycleProjection()` owns the transaction. After locking and rechecking
|
|
343
|
+
the obligation and revision evidence, the PostgreSQL store inserts the
|
|
344
|
+
transition, updates its issues, and calls `notificationActivityPort` with that
|
|
345
|
+
same transaction client. A port error rolls the whole unit back. The port does
|
|
346
|
+
no network I/O and stores no destination or authentication material. Only after
|
|
347
|
+
commit does `recoverOnce()` call the existing persistent-notification runtime,
|
|
348
|
+
which reads current subscriptions, enforces tenant/account matching and live
|
|
349
|
+
authorization, resolves opaque credential bindings, and checkpoints the normal
|
|
350
|
+
encrypted webhook outbox before POSTing. The activity queue is not a second
|
|
351
|
+
sender, credential authority, retry engine, or reporting ledger.
|
|
352
|
+
The store atomically stamps the transition's `notifiedAt` field as a durable
|
|
353
|
+
handoff marker in that same transaction. In transactional mode this field means
|
|
354
|
+
the activity intent is durable, not that a recipient matched or network delivery
|
|
355
|
+
occurred. A legacy deployment with unnotified transitions must drain or
|
|
356
|
+
explicitly resolve them before enabling the port; the lifecycle rejects the
|
|
357
|
+
cutover rather than silently abandoning those rows.
|
|
358
|
+
|
|
359
|
+
The logical notification identity is derived from the immutable transition and
|
|
360
|
+
is reused through ambiguous crashes. Intent insertion is exactly once;
|
|
361
|
+
delivery remains at least once. A crash before commit exposes neither the
|
|
362
|
+
transition nor its activity. A crash after commit leaves pending work. A crash
|
|
363
|
+
after webhook checkpointing may replay projection, but the existing webhook
|
|
364
|
+
delivery identity prevents rebinding. The bridge intentionally binds recipients
|
|
365
|
+
when the existing notification runtime checkpoints each per-subscriber webhook
|
|
366
|
+
delivery, not while the ledger transaction is open: that keeps subscription
|
|
367
|
+
credentials and destination authority out of the ledger transaction and ensures
|
|
368
|
+
a replacement or revocation that wins before checkpointing is honored. From
|
|
369
|
+
that checkpoint onward the subscriber and destination generation are stable;
|
|
370
|
+
the runtime rechecks live authorization on every attempt and suppresses stale
|
|
371
|
+
or revoked generations. Already-authorized in-flight POSTs cannot be retracted.
|
|
372
|
+
This explicit drain-time rule is what the replacement and revocation crash tests
|
|
373
|
+
assert.
|
|
374
|
+
|
|
375
|
+
Account operators can read a bounded keyset page without loading report rows:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
const page = await reportingActivity.listActivity({
|
|
379
|
+
tenantId: authenticatedTenant.id,
|
|
380
|
+
accountId: resolvedInternalAccount.id,
|
|
381
|
+
limit: 100, // 1..200
|
|
382
|
+
cursor: previousPage.nextCursor,
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Both scope values must come from authenticated server context. Cursors are
|
|
387
|
+
scope-bound and cannot be moved between accounts or tenants; the runtime also
|
|
388
|
+
revalidates the account-to-tenant mapping on every read. Pagination is a
|
|
389
|
+
newest-first operator view, not a gap-free change feed: a transaction that
|
|
390
|
+
commits after a page was read may have an earlier PostgreSQL sequence, so
|
|
391
|
+
refresh from the first page to discover concurrent late commits. Records include
|
|
392
|
+
the transition identity, health and observed-finality change, occurrence and
|
|
393
|
+
projection timestamps, issue IDs, period, and non-secret configuration/report
|
|
394
|
+
correlation references. They never embed revision rows, `sourceScope`,
|
|
395
|
+
subscriber destinations, credential handles, credentials, or `ctx_metadata`.
|
|
396
|
+
Each compact activity intent is capped at 64 KiB.
|
|
397
|
+
The runtime also applies atomic per-tenant backpressure at 100,000 pending
|
|
398
|
+
health notifications by default; tune `maxPendingPerTenant` to deployment
|
|
399
|
+
capacity and alert on the operational error instead of dropping durable intent.
|
|
400
|
+
This is an SDK/adopter API only: AdCP defines the complete health-notification
|
|
401
|
+
wire payload but no public account-activity read task, so do not expose `listActivity()`
|
|
402
|
+
as an invented wire extension.
|
|
403
|
+
|
|
404
|
+
Projected activity defaults to 90-day retention measured from projection (or
|
|
405
|
+
from commit for finality-only records that require no wire projection).
|
|
406
|
+
Abandoned activity is retained on the same schedule, measured from
|
|
407
|
+
`abandoned_at`. Override `retentionMs` only to match an explicit operator
|
|
408
|
+
policy, schedule bounded `pruneProjected()` calls — which reclaims projected and
|
|
409
|
+
abandoned rows alike — and retain pending rows until they are settled. The
|
|
410
|
+
reporting worker retries a failed projection up to `maxAttempts` (default 100)
|
|
411
|
+
and then abandons the claim; once the notification runtime has durably accepted
|
|
412
|
+
every matched subscriber, its own webhook outbox owns delivery retry and
|
|
413
|
+
retention. Supply `recoverOnce({ onError })` to report a
|
|
414
|
+
failed projection attempt without changing lease or retry semantics.
|
|
415
|
+
`matched` reports how many active subscribers were checkpointed; a projected
|
|
416
|
+
row with `matched: 0` is expected after revocation and does not claim network
|
|
417
|
+
delivery. Each recovery poll claims one row at a time so work waiting behind a
|
|
418
|
+
slow fanout is never left under an expiring pre-claimed lease.
|
|
419
|
+
The host remains responsible for a database-level retained-row/byte quota and
|
|
420
|
+
storage alerting per tenant or isolated deployment; the runtime's pending cap
|
|
421
|
+
protects delivery backlog but is not a general PostgreSQL storage quota.
|
|
422
|
+
|
|
423
|
+
### Recipient intent is frozen before any send, and revisable per recipient
|
|
424
|
+
|
|
425
|
+
The recovery worker commits its resolved recipients before anything leaves the
|
|
426
|
+
process, via `NotificationEvent.freezeRecipients`. The runtime then delivers
|
|
427
|
+
only the intersection of what is resolvable now and what was committed, so each
|
|
428
|
+
subscriber's `delivery_id` — and therefore the `idempotency_key` it dedupes on —
|
|
429
|
+
is stable across an ambiguous retry.
|
|
430
|
+
|
|
431
|
+
What makes a frozen set safely revisable is a second durable barrier:
|
|
432
|
+
`PersistentNotificationRuntimeOptions.checkpointDeliveryAttempt`, awaited on the
|
|
433
|
+
allow path of live delivery authority immediately before every external POST.
|
|
434
|
+
Wire `createPostgresReportingNotificationAttemptCheckpoint()` into it. It is a
|
|
435
|
+
runtime-level option keyed on the durable attempt context rather than a
|
|
436
|
+
per-emission closure because an emission snapshot cannot carry a function, so a
|
|
437
|
+
per-emission barrier would be skipped by the recovered outbox path — the path
|
|
438
|
+
where an ambiguous send is most likely.
|
|
439
|
+
|
|
440
|
+
The hook is runtime-wide, so the reporting checkpoint passes any event type it
|
|
441
|
+
does not own straight through. Failing closed on another subsystem's
|
|
442
|
+
notification would suppress every one of its attempts until the retry horizon
|
|
443
|
+
expired. `eventTypes` **extends** the owned set and can never shrink it —
|
|
444
|
+
`reporting.status_changed` is always owned, because a configuration that
|
|
445
|
+
silently stopped checkpointing reporting deliveries while the runtime still
|
|
446
|
+
advertised checkpoint support is the precise bug the checkpoint exists to
|
|
447
|
+
prevent.
|
|
448
|
+
|
|
449
|
+
The checkpoint is bound to the exact `(queryable, namespace, table)` it writes
|
|
450
|
+
to, and the activity runtime requires that same object as `attemptCheckpoint`.
|
|
451
|
+
It verifies in two tiers: when the port exposes `deliveryAttemptCheckpoint` —
|
|
452
|
+
the checkpoint it actually invokes — that must be the identical object, because
|
|
453
|
+
two correctly-built checkpoints can each look valid while targeting different
|
|
454
|
+
stores. When it does not, the declared store binding becomes mandatory and is
|
|
455
|
+
compared instead. Either way a mismatch is refused at construction. Configuring the two independently was
|
|
456
|
+
undetectable at runtime: the checkpoint's update matched no row, every attempt
|
|
457
|
+
was suppressed as retryable, the delivery binding eventually retired, the
|
|
458
|
+
recipient settled terminal and the activity projected — losing the notification
|
|
459
|
+
with no error anywhere.
|
|
460
|
+
|
|
461
|
+
Construction and `probe()` both fail closed unless the notification port proves
|
|
462
|
+
it runs the checkpoint. A custom `{ emit }` port must set
|
|
463
|
+
`hasDeliveryAttemptCheckpoint: true`, asserting that it forwards the event it is
|
|
464
|
+
handed to a runtime that does; otherwise a crash plus a destination replacement
|
|
465
|
+
re-addresses the notification under a second generation and idempotency key.
|
|
466
|
+
`acknowledgeMissingAttemptCheckpoint` exists only for tests that deliberately
|
|
467
|
+
demonstrate that hazard.
|
|
468
|
+
|
|
469
|
+
Declaring the capability is not sufficient, and is not trusted. Freeze and
|
|
470
|
+
checkpoint are one contract: a port that declares support but never calls
|
|
471
|
+
`freezeRecipients` leaves nothing addressable, so the checkpoint has no row to
|
|
472
|
+
mark and settlement would see zero outstanding recipients and record the
|
|
473
|
+
notification as delivered although nothing was sent. The runtime verifies that
|
|
474
|
+
the freeze actually ran and refuses to project the emission otherwise.
|
|
475
|
+
|
|
476
|
+
The checkpoint and a concurrent recipient replacement run as separate statements
|
|
477
|
+
against a pool, so neither sees the other's uncommitted work: a freeze can
|
|
478
|
+
propose a replacement generation while the original is being checkpointed, and
|
|
479
|
+
PostgreSQL keeps both rows. A partial unique index on
|
|
480
|
+
`(namespace, transition_id, subscriber_key) WHERE attempt_at IS NOT NULL` is the
|
|
481
|
+
arbiter — the second generation's checkpoint fails, so it is never POSTed, and
|
|
482
|
+
the next freeze drops it because its subscriber is already claimed.
|
|
483
|
+
|
|
484
|
+
Revisability is tracked **per recipient**, in `<activity_table>_recipients`:
|
|
485
|
+
|
|
486
|
+
- A recipient with no `attempt_at` provably never received a POST, because
|
|
487
|
+
suppression fails closed before the checkpoint. It is replaced in place when
|
|
488
|
+
it goes stale, which closes the window where a destination is replaced between
|
|
489
|
+
candidate enumeration and the first POST.
|
|
490
|
+
- A recipient with `attempt_at` is pinned. Pinning is keyed on the **subscriber**,
|
|
491
|
+
not the destination generation: once a subscriber has been addressed, a later
|
|
492
|
+
generation of that subscriber is never addressed for this notification,
|
|
493
|
+
because that would be one logical delivery under two idempotency keys.
|
|
494
|
+
- One recipient's attempt never pins a sibling. In a fanout, a subscriber
|
|
495
|
+
suppressed stale before its own first POST is still re-resolved while an
|
|
496
|
+
already-addressed sibling stays pinned.
|
|
497
|
+
- Unattempted rows are replaced rather than superseded, so a claim that retries
|
|
498
|
+
many times before any send cannot accumulate rows. A settled recipient is left
|
|
499
|
+
out of later emissions — it still gates projection, but re-addressing it would
|
|
500
|
+
be redundant traffic.
|
|
501
|
+
- Settled history is compacted to one row per subscriber, and
|
|
502
|
+
`maxRetainedRecipients` bounds **every retained row**. Counting only the
|
|
503
|
+
addressable recipients let
|
|
504
|
+
terminal rows grow for the lifetime of a claim that kept retrying while fresh
|
|
505
|
+
subscribers settled.
|
|
506
|
+
- Every mutation — the replacement delete, the insert, compaction and
|
|
507
|
+
settlement — takes a `FOR UPDATE` lock on the parent activity row before it
|
|
508
|
+
touches a recipient, in a `MATERIALIZED` CTE so that lock is the statement's
|
|
509
|
+
first act. Reading the lease without locking it only proved the lease was
|
|
510
|
+
live when the snapshot was taken: a statement that then blocked on a
|
|
511
|
+
recipient lock could resume after a successor had claimed, still see its own
|
|
512
|
+
lease in the cached snapshot, and mutate the successor's rows. Holding the
|
|
513
|
+
parent means a takeover cannot complete while a leaseholder's statement is in
|
|
514
|
+
flight, and a statement starting after one matches nothing. Lock order is
|
|
515
|
+
always parent then recipient, so the paths cannot deadlock.
|
|
516
|
+
- Every mutation is also gated on a live, matching lease, and so is the gate
|
|
517
|
+
that authorises them. A stale worker sees an empty lease source, which makes every
|
|
518
|
+
other source empty and the budget zero; gating only the row sources let that
|
|
519
|
+
empty budget satisfy the check and reap the rows a successor had already
|
|
520
|
+
frozen. After a takeover a stale worker is a strict no-op that refuses.
|
|
521
|
+
- `maxRecipients` bounds one emission's fanout and must be **at least** the
|
|
522
|
+
notification runtime's `maxFanoutCandidates`. `maxRetainedRecipients` bounds
|
|
523
|
+
every stored row — that fanout plus the pinned identities of subscribers
|
|
524
|
+
addressed and then replaced — and defaults to twice `maxRecipients`. They are
|
|
525
|
+
separate because a maximum 10,000-recipient fanout with one former subscriber
|
|
526
|
+
pinned needs 10,001 rows, which a single bound capped at the fanout ceiling
|
|
527
|
+
could never express. Setting either below what a notification legitimately
|
|
528
|
+
needs is a misconfiguration: the claim retries until `maxAttempts` (default
|
|
529
|
+
100) abandons it. An abandoned claim leaves the pending set — so it cannot
|
|
530
|
+
exhaust `maxPendingPerTenant` and start refusing writes for the whole tenant —
|
|
531
|
+
while staying visible as `notificationAbandonedAt` (the real abandonment
|
|
532
|
+
instant) in account activity. It is never recorded as delivered.
|
|
533
|
+
- The bound and the replacement are one statement, and the rows it measures are
|
|
534
|
+
taken `FOR UPDATE`. Measuring separately let a concurrent checkpoint turn a
|
|
535
|
+
revisable row into a pinned one after the budget approved the write: the
|
|
536
|
+
`DELETE` then re-checked the locked row, skipped it, and the retained set
|
|
537
|
+
landed above the bound. Compaction runs before the budget, so a bound that
|
|
538
|
+
compaction can satisfy never refuses, and a refusal mutates nothing — raise
|
|
539
|
+
`maxRecipients` and the claim self-heals on its next pass.
|
|
540
|
+
|
|
541
|
+
| Replacement lands | Outcome |
|
|
542
|
+
| --- | --- |
|
|
543
|
+
| Before candidate enumeration | New generation enumerated and delivered |
|
|
544
|
+
| Between enumeration and the first POST | Suppressed `subscription_stale`, claim released, next pass replaces that recipient with the new generation; the superseded one gets nothing |
|
|
545
|
+
| After that recipient was checkpointed | Never re-addressed; the pinned recipient settles terminally |
|
|
546
|
+
| Revoked entirely | Empty recipient set is committed and the activity settles undelivered |
|
|
547
|
+
|
|
548
|
+
### Suppression is not delivery, and delivery is not settlement
|
|
549
|
+
|
|
550
|
+
Live delivery authority fails closed before every POST. Use
|
|
551
|
+
`notificationSuppressionDisposition(reason)` to tell the two kinds apart:
|
|
552
|
+
|
|
553
|
+
- **terminal** — `subscription_missing`, `subscription_inactive`,
|
|
554
|
+
`event_not_allowed`, `authorization_denied`. The subscriber must not receive
|
|
555
|
+
this event.
|
|
556
|
+
- **retryable** — `authorization_error`, `credential_unavailable`,
|
|
557
|
+
`subscription_stale`, `attempt_checkpoint_unavailable`. Authority could not be
|
|
558
|
+
established: a store read failed, an authorization or credential callback threw
|
|
559
|
+
or timed out, the generation moved mid-flight, or the durable checkpoint could
|
|
560
|
+
not be written. Nothing was sent (`attempts: 0`).
|
|
561
|
+
|
|
562
|
+
`subscription_stale` is the one reason whose disposition depends on the caller.
|
|
563
|
+
A live emission can re-resolve the new generation, so it stays retryable. A
|
|
564
|
+
**recovered** attempt (`WebhookEmitAttempt.recovered`) is pinned to the snapshot
|
|
565
|
+
it was taken from and can never become valid for a replaced generation, so it is
|
|
566
|
+
terminal — otherwise the outbox reclaims a dead delivery until its horizon
|
|
567
|
+
expires.
|
|
568
|
+
|
|
569
|
+
A delivery that throws is classified too: a retired binding or an exhausted
|
|
570
|
+
retry horizon surfaces as `failure.reason: 'delivery_binding_retired'` with
|
|
571
|
+
`terminal: true` and settles under terminal policy. Flattening it into a
|
|
572
|
+
retryable failure left the activity pending forever and eventually exhausted the
|
|
573
|
+
tenant's pending capacity.
|
|
574
|
+
|
|
575
|
+
A retryable suppression no longer terminalizes the delivery in the webhook
|
|
576
|
+
outbox either — it releases it, exactly as a retryable exhausted HTTP result
|
|
577
|
+
does, so the only durable record of the send survives for the outbox worker.
|
|
578
|
+
|
|
579
|
+
Projection requires **every** stored recipient to have reached a terminal
|
|
580
|
+
disposition: delivered, or deliberately not delivered. An outcome that never
|
|
581
|
+
reached a subscriber — a retryable suppression, a transport error, an exhausted
|
|
582
|
+
but retryable HTTP result — leaves the claim unsettled and the activity is not
|
|
583
|
+
recorded as delivered. The bundled runtime raises
|
|
584
|
+
`ReportingNotificationRetryableSuppressionError` for a retryable suppression.
|
|
585
|
+
Custom notification runtimes must implement the same barriers and the same
|
|
586
|
+
classification.
|
|
587
|
+
|
|
588
|
+
Intent is stored relationally, one row per recipient, keyed by a bounded 64-hex
|
|
589
|
+
fingerprint; only that fingerprint is indexed, so an individual recipient
|
|
590
|
+
reference has no length limit. `maxRecipients` defaults to 10,000 — the ceiling
|
|
591
|
+
the notification runtime enforces on `maxFanoutCandidates`.
|
|
592
|
+
|
|
593
|
+
Custom ledger stores implement
|
|
594
|
+
`ReportingLedgerNotificationActivityPortV1<TTransaction>` over their existing
|
|
595
|
+
authority transaction. Their `applyLifecycleProjection` equivalent must call
|
|
596
|
+
`recordTransition({ transition, obligation }, tx)` after its compare/lock and
|
|
597
|
+
before commit, and must roll back the authoritative transition if the port
|
|
598
|
+
fails. In the same transaction they must stamp `notifiedAt` as the durable
|
|
599
|
+
handoff marker. Stores must also compare `expectedPreviousFinality` with the
|
|
600
|
+
latest stored transition before applying a finality-only projection; this field
|
|
601
|
+
is optional only so pre-v14 implementations continue to compile during
|
|
602
|
+
migration.
|
|
603
|
+
|
|
604
|
+
That comparison must read **one** committed baseline, never a freshly
|
|
605
|
+
recomputed one. Transitions written from SDK 14 onward carry their own
|
|
606
|
+
`finality`, so the baseline is read straight back off the row. Pre-v14 rows
|
|
607
|
+
carry none, and their baseline is **not** reconstructed — it resolves to `none`,
|
|
608
|
+
which the store persists via
|
|
609
|
+
`resolveTransitionFinalityBaseline(reporting_obligation_id)` under its account
|
|
610
|
+
lock. `reconcileReportingStatusLifecycleV1` calls that port before deciding the
|
|
611
|
+
transition.
|
|
612
|
+
|
|
613
|
+
Stores that omit the port must also ignore `expectedPreviousFinality`, and
|
|
614
|
+
omitting it is **not** the same as returning `none`. The lifecycle cannot
|
|
615
|
+
persist a baseline on such a store's behalf, so it uses the currently observed
|
|
616
|
+
finality instead and the comparison becomes a no-op: finality is unobservable
|
|
617
|
+
there, health transitions still fire, and finality-only ones never do — exactly
|
|
618
|
+
the behaviour from before finality existed. Assuming `none` instead would make
|
|
619
|
+
every reconciliation tick observe `none -> official` and append another
|
|
620
|
+
finality-only transition, forever. Implement the port if you want finality-only
|
|
621
|
+
activity at all.
|
|
622
|
+
|
|
623
|
+
Do not try to reconstruct a historical baseline. Nothing already stored proves
|
|
624
|
+
which revisions had committed when a pre-v14 transition was recorded:
|
|
625
|
+
|
|
626
|
+
- **Payload timestamps** (a revision's `createdAt` against the transition's
|
|
627
|
+
`occurredAt`) rank creation instants, not commits. A revision constructed
|
|
628
|
+
before the transition but committed after it counts as already observed.
|
|
629
|
+
- **Insert wall clocks** (`recorded_at`, `created_at`, anything derived from
|
|
630
|
+
`clock_timestamp()`) can repeat within a microsecond and can step backward, so
|
|
631
|
+
a revision that committed after the transition can still compare equal or
|
|
632
|
+
earlier. Comparing one against the transition's application-clock `occurredAt`
|
|
633
|
+
additionally mixes clocks, so the store and the lifecycle decision disagree
|
|
634
|
+
under skew and the compare-and-set wedges permanently.
|
|
635
|
+
|
|
636
|
+
Either rule can conclude `official`, which makes `previousFinality` equal
|
|
637
|
+
`finality` and silently suppresses the real snapshot→official transition and its
|
|
638
|
+
activity record forever. Resolving to `none` instead records at most one
|
|
639
|
+
redundant finality-only transition per obligation at upgrade, which stays
|
|
640
|
+
internal activity because the AdCP status webhook is health-only.
|
|
641
|
+
|
|
642
|
+
That bound holds only while no pre-v14 writer is still appending. During a
|
|
643
|
+
rolling deploy an old pod keeps writing finality-less transitions; each becomes
|
|
644
|
+
the latest row, gets its baseline committed as `none`, and produces another
|
|
645
|
+
redundant finality-only transition. Deploy ordering and wall clocks cannot rule
|
|
646
|
+
that out, so make it enforceable in the database.
|
|
647
|
+
`REPORTING_LEDGER_FINALITY_WRITER_FENCE_MIGRATION` adds
|
|
648
|
+
|
|
649
|
+
```sql
|
|
650
|
+
CHECK (data ? 'finality') NOT VALID
|
|
651
|
+
```
|
|
43
652
|
|
|
44
|
-
|
|
653
|
+
to `adcp_reporting_transitions`. `NOT VALID` is the whole point: PostgreSQL
|
|
654
|
+
enforces the constraint for every INSERT and UPDATE while leaving historical
|
|
655
|
+
rows unvalidated, so existing finality-less rows keep working and new
|
|
656
|
+
legacy-shaped writes are rejected. Two consequences to plan for:
|
|
657
|
+
|
|
658
|
+
- **Run it last**, after legacy pending transitions are drained and no old pod
|
|
659
|
+
remains. A surviving legacy writer will fail its appends with
|
|
660
|
+
`rejected by the finality writer fence`. That is deliberate — a loud rejection
|
|
661
|
+
beats a quietly unbounded event stream.
|
|
662
|
+
- **Any UPDATE must leave the row fence-clean.** The baseline resolver and
|
|
663
|
+
`markTransitionNotified` both write `finality` as part of their update, so a
|
|
664
|
+
historical row is repaired by the same statement that touches it.
|
|
665
|
+
|
|
666
|
+
A store that implements neither the baseline port nor the optional `finality`
|
|
667
|
+
fields is treated as unable to observe finality at all: the baseline becomes the
|
|
668
|
+
currently observed finality, so the comparison is a no-op and no finality-only
|
|
669
|
+
transition is ever written. Without that, every reconciliation tick would see
|
|
670
|
+
`none -> official` and append another one forever. Such a store behaves exactly
|
|
671
|
+
as it did before finality existed — health transitions still fire.
|
|
672
|
+
|
|
673
|
+
Custom stores carry the same obligation: after cutover, reject any transition
|
|
674
|
+
write that does not record an observed finality, and enforce it in the storage
|
|
675
|
+
engine rather than in application code — an application-level check does not
|
|
676
|
+
bind a pod running last release's binary. Repair a historical row in the same
|
|
677
|
+
statement that mutates it.
|
|
678
|
+
|
|
679
|
+
The transaction argument must be one BEGIN/COMMIT-bound connection, never a
|
|
680
|
+
pool or autocommit queryable; the per-tenant advisory transaction lock provides
|
|
681
|
+
capacity serialization under READ COMMITTED. Never call the port in a
|
|
682
|
+
post-commit subscriber callback. The bundled
|
|
683
|
+
PostgreSQL activity runtime accepts only the active queryable transaction and
|
|
684
|
+
can therefore be reused by a custom PostgreSQL ledger without adopting the SDK
|
|
685
|
+
ledger tables.
|
|
686
|
+
|
|
687
|
+
The transactional port and legacy `ReportingLedgerSubscriberV1` callbacks are
|
|
688
|
+
mutually exclusive. The bundled store exposes that mode to lifecycle
|
|
689
|
+
reconciliation and fails closed if both are supplied, preventing double fire;
|
|
690
|
+
the port's pending rows, rather than `listPendingTransitions()`, own retry.
|
|
691
|
+
|
|
692
|
+
The activity table is deliberately separate from Core revision and obligation
|
|
693
|
+
rows. Managed Delivery/Reconciled Billing work in #2944 can add immutable
|
|
694
|
+
materialization and receipt tables without changing this transition identity,
|
|
695
|
+
queue schema, or migration ordering.
|
|
45
696
|
|
|
46
697
|
`planObligations()` creates at most 1,000 obligations per call by default. Use its `account_id` and `maxObligations` options from a resumable scheduler when catching up dense or old schedules. Source executions are bounded to 10,000 objects, 1,000,000 rows, and 64 MiB per revision.
|
|
47
698
|
|
|
48
699
|
When `get_reporting_status` omits a period, the operational default horizon is the 24 hours ending at `ledger_as_of`. The `health` and `finality` arrays filter periods-view output only; they do not rewrite summary health or the underlying obligation projection.
|
|
49
700
|
|
|
50
|
-
`projectReportingObligationHealthV1` is the pure five-state projection. Before `expectedAt`, missing evidence is `waiting`; during recovery it is `delayed`; after the recovery deadline it is `action_required`; readable qualifying evidence is `healthy` for an open scope and `complete` for a closed scope. An unfiltered closed scope with no caller-owned configurations or no due periods is vacuously `complete`; an explicitly unknown configuration returns `lookup_unavailable`, and a snapshot with missing elapsed obligations fails closed. The simplified lifecycle persists deterministic issues and `reporting.status_changed` transitions
|
|
701
|
+
`projectReportingObligationHealthV1` is the pure five-state projection. Before `expectedAt`, missing evidence is `waiting`; during recovery it is `delayed`; after the recovery deadline it is `action_required`; readable qualifying evidence is `healthy` for an open scope and `complete` for a closed scope. An unfiltered closed scope with no caller-owned configurations or no due periods is vacuously `complete`; an explicitly unknown configuration returns `lookup_unavailable`, and a snapshot with missing elapsed obligations fails closed. The simplified lifecycle persists deterministic issues and `reporting.status_changed` transitions. In legacy mode it then calls only subscribers already authorized and supplied by the host; with the transactional port, finality-only changes stay in internal activity and health changes flow through the durable AdCP notification runtime. When the store implements the optional `getManagedLifecycleProjection`, the reconciler folds Managed Delivery through the same projection the read path uses, so a persisted transition and its webhook report the health a read of that obligation would return instead of Core health alone. Every managed instant is written at database precision: a JS `Date` holds milliseconds, so taking the batch instant through one truncated `recorded_at` below the microsecond watermark written from the same clock and the reconcile it should have triggered could never become due. The lifecycle projection reads each chain's leaf **as of the cutoff** — the receipt nothing recorded by then supersedes — rather than its whole history, because the leaf is the only thing the verdict uses. Asking which row is current *now* and only then applying the cutoff answered "neither" for a rejection an acceptance had since repaired, and persisted `RECEIPT_REQUIRED` over a rejection the buyer had already filed. Reading the history instead made an obligation whose subject was repaired more times than a snapshot page may carry — a state the receipt store admits — permanently unreconcilable. A leaf also stops being a leaf when its successor is pruned — the tombstone records what it superseded — or retention handed a settled subject back to the rejection its acceptance had replaced. The read path applies the same rule at the verdict: a tombstoned acceptance outranks a rejection it superseded, so `reconciliation_status` cannot report `rejected` for a subject the lifecycle considers settled. Evidence recorded after the cutoff is out of scope at that cutoff: revisions and adjustments alike are filtered by it exactly as receipts are — a revision committed after the cutoff arrives with no materialization and no receipt in scope and would read as an unmet consumer obligation, while the compare-and-set still fences on the full revision set, which is a concurrency check rather than a statement about an instant — by the store's own ordering column, not by the producer-authored `createdAt` on the body, because a producer clock running ahead otherwise hid a committed correction from the lifecycle while the public read, which orders by that column, kept demanding a receipt for it — and pruned conclusions carry the instant they concluded, so a historical reconcile cannot settle on an acceptance or a delivery that had not happened yet. The bound is now one leaf per (consumer, subject), which is the product of two dimensions the store admits independently, and crossing it — like an oversized roster or conclusion set — truncates at a deterministic boundary and reports `receiptEvidenceComplete: false` rather than failing: a bound the write path can legitimately cross must never be a hard error, and an incomplete projection is treated exactly as an unproven roster is, so it can never report an obligation reconciled. Wire counters are computed on the read path, which still sees every row. A projection that cannot be published is reported by `runWorker` as `reconcilesDeferred` rather than aborting the sweep — the durable write already committed and the obligation stays due, so the deadline sweep, which isolates and backs off per obligation, owns the retry. Reads are scoped to one authenticated consumer while a transition is account-level, so the reconciler keeps the most severe consumer: the seller's obligation is not reconciled until every consumer that owes a receipt has accepted. Consumer-specific issue codes — `RECEIPT_REQUIRED`, `RECEIPT_REJECTED`, `ADJUSTMENT_RECEIPT_REQUIRED`, `ADJUSTMENT_RECEIPT_REJECTED` — are deliberately excluded from that persisted set and from transition `issueIds`, because the issue store is keyed by obligation with no consumer dimension and reads republish persisted issues to whichever consumer is asking; publishing them would hand one consumer another's rejection state and exact receipt ingest timing. Their severity still reaches the account-level `health`, and each caller's own issues are recomputed per read. Aggregation runs over `obligatedConsumerIds`, not over whoever happens to have submitted, so a silent authorized consumer cannot vanish when another accepts. The managed tables carry no consumer dimension on destination authorizations or bindings, so the built-in PostgreSQL store cannot prove the roster is complete and reports `obligatedConsumerRosterComplete: false`; while that is false the reconciler keeps one zero-receipt consumer in the fold and never reports a `consumer_receipt` obligation reconciled. Supply the roster from your own authorization layer through the store's `obligatedConsumers` option — `(input: { reporting_obligation_id, account_id }) => Promise<{ ids, complete }>` — and return `complete: true` to get accurate reconciled transitions. Because the conservative default holds a `consumer_receipt` obligation at `action_required` from first delivery, and the per-consumer receipt issues are deliberately not persisted, the reconciler restates that state once per obligation as a `RECEIPT_REQUIRED` issue anchored to the obligation's own `expected_at`. It names no principal and carries no receipt timing, so a degraded persisted health is never unexplained and the leak stays closed.
|
|
51
702
|
|
|
52
703
|
## Consumer status ingest
|
|
53
704
|
|