@adcp/sdk 5.25.1 → 6.1.0
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/README.md +45 -7
- package/dist/lib/compliance-fixtures/index.d.ts +1 -1
- package/dist/lib/compliance-fixtures/index.js +1 -1
- package/dist/lib/core/AgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.js +27 -0
- package/dist/lib/core/SingleAgentClient.js.map +1 -1
- package/dist/lib/core/TaskExecutor.d.ts +21 -0
- package/dist/lib/core/TaskExecutor.d.ts.map +1 -1
- package/dist/lib/core/TaskExecutor.js +25 -2
- package/dist/lib/core/TaskExecutor.js.map +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +7 -8
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/schemas/index.d.ts +1 -1
- package/dist/lib/schemas/index.js +1 -1
- package/dist/lib/schemas-data/v2.5/_provenance.json +8 -0
- package/dist/lib/schemas-data/v2.5/adagents.json +494 -0
- package/dist/lib/schemas-data/v2.5/core/activation-key.json +50 -0
- package/dist/lib/schemas-data/v2.5/core/assets/audio-asset.json +32 -0
- package/dist/lib/schemas-data/v2.5/core/assets/css-asset.json +21 -0
- package/dist/lib/schemas-data/v2.5/core/assets/daast-asset.json +87 -0
- package/dist/lib/schemas-data/v2.5/core/assets/html-asset.json +21 -0
- package/dist/lib/schemas-data/v2.5/core/assets/image-asset.json +38 -0
- package/dist/lib/schemas-data/v2.5/core/assets/javascript-asset.json +21 -0
- package/dist/lib/schemas-data/v2.5/core/assets/markdown-asset.json +31 -0
- package/dist/lib/schemas-data/v2.5/core/assets/text-asset.json +21 -0
- package/dist/lib/schemas-data/v2.5/core/assets/url-asset.json +26 -0
- package/dist/lib/schemas-data/v2.5/core/assets/vast-asset.json +87 -0
- package/dist/lib/schemas-data/v2.5/core/assets/video-asset.json +44 -0
- package/dist/lib/schemas-data/v2.5/core/assets/webhook-asset.json +71 -0
- package/dist/lib/schemas-data/v2.5/core/async-response-data.json +88 -0
- package/dist/lib/schemas-data/v2.5/core/brand-manifest-ref.json +33 -0
- package/dist/lib/schemas-data/v2.5/core/brand-manifest.json +409 -0
- package/dist/lib/schemas-data/v2.5/core/context.json +8 -0
- package/dist/lib/schemas-data/v2.5/core/creative-asset.json +125 -0
- package/dist/lib/schemas-data/v2.5/core/creative-assignment.json +31 -0
- package/dist/lib/schemas-data/v2.5/core/creative-filters.json +111 -0
- package/dist/lib/schemas-data/v2.5/core/creative-manifest.json +72 -0
- package/dist/lib/schemas-data/v2.5/core/creative-policy.json +27 -0
- package/dist/lib/schemas-data/v2.5/core/delivery-metrics.json +171 -0
- package/dist/lib/schemas-data/v2.5/core/deployment.json +93 -0
- package/dist/lib/schemas-data/v2.5/core/destination.json +53 -0
- package/dist/lib/schemas-data/v2.5/core/error.json +40 -0
- package/dist/lib/schemas-data/v2.5/core/ext.json +8 -0
- package/dist/lib/schemas-data/v2.5/core/format-id.json +47 -0
- package/dist/lib/schemas-data/v2.5/core/format.json +324 -0
- package/dist/lib/schemas-data/v2.5/core/frequency-cap.json +18 -0
- package/dist/lib/schemas-data/v2.5/core/mcp-webhook-payload.json +152 -0
- package/dist/lib/schemas-data/v2.5/core/measurement.json +48 -0
- package/dist/lib/schemas-data/v2.5/core/media-buy.json +62 -0
- package/dist/lib/schemas-data/v2.5/core/package.json +72 -0
- package/dist/lib/schemas-data/v2.5/core/performance-feedback.json +90 -0
- package/dist/lib/schemas-data/v2.5/core/placement.json +34 -0
- package/dist/lib/schemas-data/v2.5/core/pricing-option.json +35 -0
- package/dist/lib/schemas-data/v2.5/core/product-filters.json +102 -0
- package/dist/lib/schemas-data/v2.5/core/product.json +153 -0
- package/dist/lib/schemas-data/v2.5/core/promoted-offerings.json +115 -0
- package/dist/lib/schemas-data/v2.5/core/promoted-products.json +67 -0
- package/dist/lib/schemas-data/v2.5/core/property-id.json +14 -0
- package/dist/lib/schemas-data/v2.5/core/property-tag.json +16 -0
- package/dist/lib/schemas-data/v2.5/core/property.json +62 -0
- package/dist/lib/schemas-data/v2.5/core/protocol-envelope.json +146 -0
- package/dist/lib/schemas-data/v2.5/core/publisher-property-selector.json +92 -0
- package/dist/lib/schemas-data/v2.5/core/push-notification-config.json +48 -0
- package/dist/lib/schemas-data/v2.5/core/reporting-capabilities.json +71 -0
- package/dist/lib/schemas-data/v2.5/core/response.json +24 -0
- package/dist/lib/schemas-data/v2.5/core/signal-filters.json +35 -0
- package/dist/lib/schemas-data/v2.5/core/start-timing.json +18 -0
- package/dist/lib/schemas-data/v2.5/core/sub-asset.json +79 -0
- package/dist/lib/schemas-data/v2.5/core/targeting.json +50 -0
- package/dist/lib/schemas-data/v2.5/core/tasks-get-request.json +43 -0
- package/dist/lib/schemas-data/v2.5/core/tasks-get-response.json +166 -0
- package/dist/lib/schemas-data/v2.5/core/tasks-list-request.json +192 -0
- package/dist/lib/schemas-data/v2.5/core/tasks-list-response.json +183 -0
- package/dist/lib/schemas-data/v2.5/creative/asset-types/index.json +101 -0
- package/dist/lib/schemas-data/v2.5/creative/list-creative-formats-request.json +73 -0
- package/dist/lib/schemas-data/v2.5/creative/list-creative-formats-response.json +61 -0
- package/dist/lib/schemas-data/v2.5/creative/preview-creative-request.json +164 -0
- package/dist/lib/schemas-data/v2.5/creative/preview-creative-response.json +245 -0
- package/dist/lib/schemas-data/v2.5/creative/preview-render.json +225 -0
- package/dist/lib/schemas-data/v2.5/enums/adcp-domain.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/asset-content-type.json +22 -0
- package/dist/lib/schemas-data/v2.5/enums/auth-scheme.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/available-metric.json +18 -0
- package/dist/lib/schemas-data/v2.5/enums/channels.json +18 -0
- package/dist/lib/schemas-data/v2.5/enums/co-branding-requirement.json +12 -0
- package/dist/lib/schemas-data/v2.5/enums/creative-action.json +14 -0
- package/dist/lib/schemas-data/v2.5/enums/creative-agent-capability.json +13 -0
- package/dist/lib/schemas-data/v2.5/enums/creative-sort-field.json +15 -0
- package/dist/lib/schemas-data/v2.5/enums/creative-status.json +19 -0
- package/dist/lib/schemas-data/v2.5/enums/daast-tracking-event.json +20 -0
- package/dist/lib/schemas-data/v2.5/enums/daast-version.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/delivery-type.json +15 -0
- package/dist/lib/schemas-data/v2.5/enums/dimension-unit.json +13 -0
- package/dist/lib/schemas-data/v2.5/enums/feed-format.json +12 -0
- package/dist/lib/schemas-data/v2.5/enums/feedback-source.json +13 -0
- package/dist/lib/schemas-data/v2.5/enums/format-category.json +16 -0
- package/dist/lib/schemas-data/v2.5/enums/format-id-parameter.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/frequency-cap-scope.json +13 -0
- package/dist/lib/schemas-data/v2.5/enums/history-entry-type.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/http-method.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/identifier-types.json +34 -0
- package/dist/lib/schemas-data/v2.5/enums/javascript-module-type.json +12 -0
- package/dist/lib/schemas-data/v2.5/enums/landing-page-requirement.json +12 -0
- package/dist/lib/schemas-data/v2.5/enums/markdown-flavor.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/media-buy-status.json +19 -0
- package/dist/lib/schemas-data/v2.5/enums/metric-type.json +17 -0
- package/dist/lib/schemas-data/v2.5/enums/notification-type.json +13 -0
- package/dist/lib/schemas-data/v2.5/enums/pacing.json +17 -0
- package/dist/lib/schemas-data/v2.5/enums/preview-output-format.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/pricing-model.json +25 -0
- package/dist/lib/schemas-data/v2.5/enums/property-type.json +16 -0
- package/dist/lib/schemas-data/v2.5/enums/publisher-identifier-types.json +19 -0
- package/dist/lib/schemas-data/v2.5/enums/reporting-frequency.json +12 -0
- package/dist/lib/schemas-data/v2.5/enums/signal-catalog-type.json +12 -0
- package/dist/lib/schemas-data/v2.5/enums/sort-direction.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/standard-format-ids.json +103 -0
- package/dist/lib/schemas-data/v2.5/enums/task-status.json +29 -0
- package/dist/lib/schemas-data/v2.5/enums/task-type.json +27 -0
- package/dist/lib/schemas-data/v2.5/enums/update-frequency.json +13 -0
- package/dist/lib/schemas-data/v2.5/enums/url-asset-type.json +12 -0
- package/dist/lib/schemas-data/v2.5/enums/validation-mode.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/vast-tracking-event.json +25 -0
- package/dist/lib/schemas-data/v2.5/enums/vast-version.json +14 -0
- package/dist/lib/schemas-data/v2.5/enums/webhook-response-type.json +13 -0
- package/dist/lib/schemas-data/v2.5/enums/webhook-security-method.json +12 -0
- package/dist/lib/schemas-data/v2.5/index.json +604 -0
- package/dist/lib/schemas-data/v2.5/media-buy/build-creative-request.json +31 -0
- package/dist/lib/schemas-data/v2.5/media-buy/build-creative-response.json +65 -0
- package/dist/lib/schemas-data/v2.5/media-buy/create-media-buy-async-response-input-required.json +31 -0
- package/dist/lib/schemas-data/v2.5/media-buy/create-media-buy-async-response-submitted.json +16 -0
- package/dist/lib/schemas-data/v2.5/media-buy/create-media-buy-async-response-working.json +36 -0
- package/dist/lib/schemas-data/v2.5/media-buy/create-media-buy-request.json +126 -0
- package/dist/lib/schemas-data/v2.5/media-buy/create-media-buy-response.json +97 -0
- package/dist/lib/schemas-data/v2.5/media-buy/get-media-buy-delivery-request.json +54 -0
- package/dist/lib/schemas-data/v2.5/media-buy/get-media-buy-delivery-response.json +285 -0
- package/dist/lib/schemas-data/v2.5/media-buy/get-products-async-response-input-required.json +38 -0
- package/dist/lib/schemas-data/v2.5/media-buy/get-products-async-response-submitted.json +21 -0
- package/dist/lib/schemas-data/v2.5/media-buy/get-products-async-response-working.json +34 -0
- package/dist/lib/schemas-data/v2.5/media-buy/get-products-request.json +28 -0
- package/dist/lib/schemas-data/v2.5/media-buy/get-products-response.json +33 -0
- package/dist/lib/schemas-data/v2.5/media-buy/list-authorized-properties-request.json +26 -0
- package/dist/lib/schemas-data/v2.5/media-buy/list-authorized-properties-response.json +70 -0
- package/dist/lib/schemas-data/v2.5/media-buy/list-creative-formats-request.json +58 -0
- package/dist/lib/schemas-data/v2.5/media-buy/list-creative-formats-response.json +61 -0
- package/dist/lib/schemas-data/v2.5/media-buy/list-creatives-request.json +137 -0
- package/dist/lib/schemas-data/v2.5/media-buy/list-creatives-response.json +437 -0
- package/dist/lib/schemas-data/v2.5/media-buy/package-request.json +80 -0
- package/dist/lib/schemas-data/v2.5/media-buy/provide-performance-feedback-request.json +88 -0
- package/dist/lib/schemas-data/v2.5/media-buy/provide-performance-feedback-response.json +66 -0
- package/dist/lib/schemas-data/v2.5/media-buy/sync-creatives-async-response-input-required.json +25 -0
- package/dist/lib/schemas-data/v2.5/media-buy/sync-creatives-async-response-submitted.json +16 -0
- package/dist/lib/schemas-data/v2.5/media-buy/sync-creatives-async-response-working.json +46 -0
- package/dist/lib/schemas-data/v2.5/media-buy/sync-creatives-request.json +178 -0
- package/dist/lib/schemas-data/v2.5/media-buy/sync-creatives-response.json +149 -0
- package/dist/lib/schemas-data/v2.5/media-buy/update-media-buy-async-response-input-required.json +24 -0
- package/dist/lib/schemas-data/v2.5/media-buy/update-media-buy-async-response-submitted.json +16 -0
- package/dist/lib/schemas-data/v2.5/media-buy/update-media-buy-async-response-working.json +36 -0
- package/dist/lib/schemas-data/v2.5/media-buy/update-media-buy-request.json +129 -0
- package/dist/lib/schemas-data/v2.5/media-buy/update-media-buy-response.json +99 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/cpc-option.json +52 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/cpcv-option.json +52 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/cpm-auction-option.json +81 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/cpm-fixed-option.json +52 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/cpp-option.json +73 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/cpv-option.json +88 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/flat-rate-option.json +93 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/vcpm-auction-option.json +81 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/vcpm-fixed-option.json +52 -0
- package/dist/lib/schemas-data/v2.5/protocols/adcp-extension.json +33 -0
- package/dist/lib/schemas-data/v2.5/signals/activate-signal-request.json +32 -0
- package/dist/lib/schemas-data/v2.5/signals/activate-signal-response.json +68 -0
- package/dist/lib/schemas-data/v2.5/signals/get-signals-request.json +59 -0
- package/dist/lib/schemas-data/v2.5/signals/get-signals-response.json +100 -0
- package/dist/lib/server/create-adcp-server.d.ts +129 -11
- package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
- package/dist/lib/server/create-adcp-server.js +127 -2
- package/dist/lib/server/create-adcp-server.js.map +1 -1
- package/dist/lib/server/ctx-metadata/backends/memory.d.ts +27 -0
- package/dist/lib/server/ctx-metadata/backends/memory.d.ts.map +1 -0
- package/dist/lib/server/ctx-metadata/backends/memory.js +72 -0
- package/dist/lib/server/ctx-metadata/backends/memory.js.map +1 -0
- package/dist/lib/server/ctx-metadata/backends/pg.d.ts +62 -0
- package/dist/lib/server/ctx-metadata/backends/pg.d.ts.map +1 -0
- package/dist/lib/server/ctx-metadata/backends/pg.js +145 -0
- package/dist/lib/server/ctx-metadata/backends/pg.js.map +1 -0
- package/dist/lib/server/ctx-metadata/index.d.ts +15 -0
- package/dist/lib/server/ctx-metadata/index.d.ts.map +1 -0
- package/dist/lib/server/ctx-metadata/index.js +28 -0
- package/dist/lib/server/ctx-metadata/index.js.map +1 -0
- package/dist/lib/server/ctx-metadata/store.d.ts +177 -0
- package/dist/lib/server/ctx-metadata/store.d.ts.map +1 -0
- package/dist/lib/server/ctx-metadata/store.js +327 -0
- package/dist/lib/server/ctx-metadata/store.js.map +1 -0
- package/dist/lib/server/ctx-metadata/wire-shape.d.ts +55 -0
- package/dist/lib/server/ctx-metadata/wire-shape.d.ts.map +1 -0
- package/dist/lib/server/ctx-metadata/wire-shape.js +121 -0
- package/dist/lib/server/ctx-metadata/wire-shape.js.map +1 -0
- package/dist/lib/server/decisioning/account.d.ts +309 -0
- package/dist/lib/server/decisioning/account.d.ts.map +1 -0
- package/dist/lib/server/decisioning/account.js +102 -0
- package/dist/lib/server/decisioning/account.js.map +1 -0
- package/dist/lib/server/decisioning/admin-router.d.ts +75 -0
- package/dist/lib/server/decisioning/admin-router.d.ts.map +1 -0
- package/dist/lib/server/decisioning/admin-router.js +120 -0
- package/dist/lib/server/decisioning/admin-router.js.map +1 -0
- package/dist/lib/server/decisioning/assembly-helpers.d.ts +204 -0
- package/dist/lib/server/decisioning/assembly-helpers.d.ts.map +1 -0
- package/dist/lib/server/decisioning/assembly-helpers.js +173 -0
- package/dist/lib/server/decisioning/assembly-helpers.js.map +1 -0
- package/dist/lib/server/decisioning/async-outcome.d.ts +154 -0
- package/dist/lib/server/decisioning/async-outcome.d.ts.map +1 -0
- package/dist/lib/server/decisioning/async-outcome.js +239 -0
- package/dist/lib/server/decisioning/async-outcome.js.map +1 -0
- package/dist/lib/server/decisioning/capabilities.d.ts +251 -0
- package/dist/lib/server/decisioning/capabilities.d.ts.map +1 -0
- package/dist/lib/server/decisioning/capabilities.js +16 -0
- package/dist/lib/server/decisioning/capabilities.js.map +1 -0
- package/dist/lib/server/decisioning/context.d.ts +212 -0
- package/dist/lib/server/decisioning/context.d.ts.map +1 -0
- package/dist/lib/server/decisioning/context.js +26 -0
- package/dist/lib/server/decisioning/context.js.map +1 -0
- package/dist/lib/server/decisioning/errors-typed.d.ts +104 -0
- package/dist/lib/server/decisioning/errors-typed.d.ts.map +1 -0
- package/dist/lib/server/decisioning/errors-typed.js +304 -0
- package/dist/lib/server/decisioning/errors-typed.js.map +1 -0
- package/dist/lib/server/decisioning/helpers.d.ts +131 -0
- package/dist/lib/server/decisioning/helpers.d.ts.map +1 -0
- package/dist/lib/server/decisioning/helpers.js +134 -0
- package/dist/lib/server/decisioning/helpers.js.map +1 -0
- package/dist/lib/server/decisioning/index.d.ts +46 -0
- package/dist/lib/server/decisioning/index.d.ts.map +1 -0
- package/dist/lib/server/decisioning/index.js +120 -0
- package/dist/lib/server/decisioning/index.js.map +1 -0
- package/dist/lib/server/decisioning/list-helpers.d.ts +53 -0
- package/dist/lib/server/decisioning/list-helpers.d.ts.map +1 -0
- package/dist/lib/server/decisioning/list-helpers.js +96 -0
- package/dist/lib/server/decisioning/list-helpers.js.map +1 -0
- package/dist/lib/server/decisioning/manifest-helpers.d.ts +56 -0
- package/dist/lib/server/decisioning/manifest-helpers.d.ts.map +1 -0
- package/dist/lib/server/decisioning/manifest-helpers.js +78 -0
- package/dist/lib/server/decisioning/manifest-helpers.js.map +1 -0
- package/dist/lib/server/decisioning/pagination.d.ts +21 -0
- package/dist/lib/server/decisioning/pagination.d.ts.map +1 -0
- package/dist/lib/server/decisioning/pagination.js +12 -0
- package/dist/lib/server/decisioning/pagination.js.map +1 -0
- package/dist/lib/server/decisioning/platform.d.ts +188 -0
- package/dist/lib/server/decisioning/platform.d.ts.map +1 -0
- package/dist/lib/server/decisioning/platform.js +19 -0
- package/dist/lib/server/decisioning/platform.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/from-platform.d.ts +510 -0
- package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/from-platform.js +2196 -0
- package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts +114 -0
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.js +247 -0
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.d.ts +32 -0
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.js +127 -0
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/task-registry.d.ts +105 -0
- package/dist/lib/server/decisioning/runtime/task-registry.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/task-registry.js +96 -0
- package/dist/lib/server/decisioning/runtime/task-registry.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/to-context.d.ts +54 -0
- package/dist/lib/server/decisioning/runtime/to-context.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/to-context.js +166 -0
- package/dist/lib/server/decisioning/runtime/to-context.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/validate-platform.d.ts +20 -0
- package/dist/lib/server/decisioning/runtime/validate-platform.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/validate-platform.js +93 -0
- package/dist/lib/server/decisioning/runtime/validate-platform.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/audiences.d.ts +72 -0
- package/dist/lib/server/decisioning/specialisms/audiences.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/audiences.js +15 -0
- package/dist/lib/server/decisioning/specialisms/audiences.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/brand-rights.d.ts +92 -0
- package/dist/lib/server/decisioning/specialisms/brand-rights.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/brand-rights.js +28 -0
- package/dist/lib/server/decisioning/specialisms/brand-rights.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/campaign-governance.d.ts +67 -0
- package/dist/lib/server/decisioning/specialisms/campaign-governance.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/campaign-governance.js +31 -0
- package/dist/lib/server/decisioning/specialisms/campaign-governance.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/content-standards.d.ts +78 -0
- package/dist/lib/server/decisioning/specialisms/content-standards.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/content-standards.js +35 -0
- package/dist/lib/server/decisioning/specialisms/content-standards.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.ts +81 -0
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.js +28 -0
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/creative.d.ts +144 -0
- package/dist/lib/server/decisioning/specialisms/creative.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/creative.js +19 -0
- package/dist/lib/server/decisioning/specialisms/creative.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/lists.d.ts +61 -0
- package/dist/lib/server/decisioning/specialisms/lists.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/lists.js +30 -0
- package/dist/lib/server/decisioning/specialisms/lists.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/sales.d.ts +163 -0
- package/dist/lib/server/decisioning/specialisms/sales.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/sales.js +64 -0
- package/dist/lib/server/decisioning/specialisms/sales.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/signals.d.ts +64 -0
- package/dist/lib/server/decisioning/specialisms/signals.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/signals.js +28 -0
- package/dist/lib/server/decisioning/specialisms/signals.js.map +1 -0
- package/dist/lib/server/decisioning/start-time.d.ts +76 -0
- package/dist/lib/server/decisioning/start-time.d.ts.map +1 -0
- package/dist/lib/server/decisioning/start-time.js +81 -0
- package/dist/lib/server/decisioning/start-time.js.map +1 -0
- package/dist/lib/server/decisioning/status-changes.d.ts +165 -0
- package/dist/lib/server/decisioning/status-changes.d.ts.map +1 -0
- package/dist/lib/server/decisioning/status-changes.js +131 -0
- package/dist/lib/server/decisioning/status-changes.js.map +1 -0
- package/dist/lib/server/decisioning/status-mappers.d.ts +46 -0
- package/dist/lib/server/decisioning/status-mappers.d.ts.map +1 -0
- package/dist/lib/server/decisioning/status-mappers.js +46 -0
- package/dist/lib/server/decisioning/status-mappers.js.map +1 -0
- package/dist/lib/server/decisioning/tenant-registry.d.ts +289 -0
- package/dist/lib/server/decisioning/tenant-registry.d.ts.map +1 -0
- package/dist/lib/server/decisioning/tenant-registry.js +503 -0
- package/dist/lib/server/decisioning/tenant-registry.js.map +1 -0
- package/dist/lib/server/express-adapter.d.ts +1 -1
- package/dist/lib/server/express-adapter.js +1 -1
- package/dist/lib/server/governance.d.ts +1 -1
- package/dist/lib/server/governance.js +1 -1
- package/dist/lib/server/idempotency/store.d.ts +1 -1
- package/dist/lib/server/idempotency/store.js +1 -1
- package/dist/lib/server/index.d.ts +9 -2
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +79 -4
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/server/legacy/v5/index.d.ts +38 -0
- package/dist/lib/server/legacy/v5/index.d.ts.map +1 -0
- package/dist/lib/server/legacy/v5/index.js +60 -0
- package/dist/lib/server/legacy/v5/index.js.map +1 -0
- package/dist/lib/server/normalize-errors.d.ts +88 -0
- package/dist/lib/server/normalize-errors.d.ts.map +1 -0
- package/dist/lib/server/normalize-errors.js +146 -0
- package/dist/lib/server/normalize-errors.js.map +1 -0
- package/dist/lib/server/pick-safe-details.d.ts +90 -0
- package/dist/lib/server/pick-safe-details.d.ts.map +1 -0
- package/dist/lib/server/pick-safe-details.js +148 -0
- package/dist/lib/server/pick-safe-details.js.map +1 -0
- package/dist/lib/server/postgres-state-store.d.ts +1 -1
- package/dist/lib/server/postgres-state-store.js +1 -1
- package/dist/lib/server/responses.d.ts +38 -0
- package/dist/lib/server/responses.d.ts.map +1 -1
- package/dist/lib/server/responses.js +38 -0
- package/dist/lib/server/responses.js.map +1 -1
- package/dist/lib/server/state-store.d.ts +1 -1
- package/dist/lib/server/state-store.js +1 -1
- package/dist/lib/server/test-controller.d.ts +10 -3
- package/dist/lib/server/test-controller.d.ts.map +1 -1
- package/dist/lib/server/test-controller.js +10 -3
- package/dist/lib/server/test-controller.js.map +1 -1
- package/dist/lib/testing/comply-controller.d.ts +47 -1
- package/dist/lib/testing/comply-controller.d.ts.map +1 -1
- package/dist/lib/testing/comply-controller.js +11 -4
- package/dist/lib/testing/comply-controller.js.map +1 -1
- package/dist/lib/testing/index.d.ts +1 -1
- package/dist/lib/testing/index.d.ts.map +1 -1
- package/dist/lib/testing/index.js.map +1 -1
- package/dist/lib/testing/personas/index.d.ts +143 -0
- package/dist/lib/testing/personas/index.d.ts.map +1 -0
- package/dist/lib/testing/personas/index.js +190 -0
- package/dist/lib/testing/personas/index.js.map +1 -0
- package/dist/lib/testing/storyboard/index.d.ts +1 -1
- package/dist/lib/testing/storyboard/index.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/index.js +3 -2
- package/dist/lib/testing/storyboard/index.js.map +1 -1
- package/dist/lib/testing/storyboard/runner.d.ts +13 -0
- package/dist/lib/testing/storyboard/runner.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/runner.js +260 -7
- package/dist/lib/testing/storyboard/runner.js.map +1 -1
- package/dist/lib/types/asset-instances.d.ts +1 -0
- package/dist/lib/types/asset-instances.d.ts.map +1 -1
- package/dist/lib/types/core.generated.d.ts +203 -98
- package/dist/lib/types/core.generated.d.ts.map +1 -1
- package/dist/lib/types/core.generated.js +1 -1
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/types/schemas.generated.d.ts +599 -159
- package/dist/lib/types/schemas.generated.d.ts.map +1 -1
- package/dist/lib/types/schemas.generated.js +175 -94
- package/dist/lib/types/schemas.generated.js.map +1 -1
- package/dist/lib/types/tools.generated.d.ts +315 -46
- package/dist/lib/types/tools.generated.d.ts.map +1 -1
- package/dist/lib/utils/capabilities.d.ts +1 -1
- package/dist/lib/utils/capabilities.d.ts.map +1 -1
- package/dist/lib/utils/capabilities.js +6 -0
- package/dist/lib/utils/capabilities.js.map +1 -1
- package/dist/lib/utils/creative-adapter.d.ts +32 -2
- package/dist/lib/utils/creative-adapter.d.ts.map +1 -1
- package/dist/lib/utils/creative-adapter.js +42 -6
- package/dist/lib/utils/creative-adapter.js.map +1 -1
- package/dist/lib/validation/schema-loader.d.ts.map +1 -1
- package/dist/lib/validation/schema-loader.js +20 -2
- package/dist/lib/validation/schema-loader.js.map +1 -1
- package/dist/lib/validation/schema-validator.d.ts +13 -0
- package/dist/lib/validation/schema-validator.d.ts.map +1 -1
- package/dist/lib/validation/schema-validator.js +240 -3
- package/dist/lib/validation/schema-validator.js.map +1 -1
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.d.ts.map +1 -1
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.js.map +1 -1
- package/docs/guides/BUILD-AN-AGENT.md +30 -5
- package/docs/llms.txt +28 -17
- package/examples/README.md +3 -1
- package/examples/decisioning-platform-broadcast-tv.ts +300 -0
- package/examples/decisioning-platform-identity-graph.ts +214 -0
- package/examples/decisioning-platform-mock-seller.ts +332 -0
- package/examples/decisioning-platform-multi-tenant.ts +128 -0
- package/examples/decisioning-platform-programmatic.ts +254 -0
- package/examples/signals-agent.ts +1 -1
- package/package.json +18 -5
- package/skills/build-brand-rights-agent/SKILL.md +10 -3
- package/skills/build-creative-agent/SKILL.md +94 -64
- package/skills/build-decisioning-creative-template/SKILL.md +554 -0
- package/skills/build-decisioning-platform/SKILL.md +304 -0
- package/skills/build-decisioning-platform/advanced/BRAND-RIGHTS.md +25 -0
- package/skills/build-decisioning-platform/advanced/COMPLIANCE.md +23 -0
- package/skills/build-decisioning-platform/advanced/GOVERNANCE.md +24 -0
- package/skills/build-decisioning-platform/advanced/HITL.md +34 -0
- package/skills/build-decisioning-platform/advanced/IDEMPOTENCY.md +52 -0
- package/skills/build-decisioning-platform/advanced/MULTI-TENANT.md +47 -0
- package/skills/build-decisioning-platform/advanced/OAUTH.md +22 -0
- package/skills/build-decisioning-platform/advanced/REFERENCE.md +991 -0
- package/skills/build-decisioning-platform/advanced/SANDBOX.md +24 -0
- package/skills/build-decisioning-platform/advanced/STATE-MACHINE.md +52 -0
- package/skills/build-decisioning-signal-marketplace/SKILL.md +269 -0
- package/skills/build-generative-seller-agent/SKILL.md +89 -53
- package/skills/build-governance-agent/SKILL.md +76 -45
- package/skills/build-retail-media-agent/SKILL.md +87 -62
- package/skills/build-seller-agent/SKILL.md +384 -255
- package/skills/build-seller-agent/deployment.md +5 -3
- package/skills/build-seller-agent/specialisms/audience-sync.md +0 -2
- package/skills/build-seller-agent/specialisms/sales-broadcast-tv.md +0 -2
- package/skills/build-seller-agent/specialisms/sales-guaranteed.md +0 -2
- package/skills/build-seller-agent/specialisms/sales-non-guaranteed.md +0 -2
- package/skills/build-seller-agent/specialisms/sales-proposal-mode.md +0 -2
- package/skills/build-seller-agent/specialisms/sales-social.md +0 -2
- package/skills/build-seller-agent/specialisms/signed-requests.md +0 -2
- package/skills/build-si-agent/SKILL.md +40 -32
- package/skills/build-signals-agent/SKILL.md +139 -92
- package/skills/call-adcp-agent.previous/SKILL.md +5 -0
|
@@ -0,0 +1,2196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Build an `AdcpServer` from a `DecisioningPlatform` impl.
|
|
4
|
+
*
|
|
5
|
+
* v6.0 alpha entry point. Translates the per-specialism platform interface
|
|
6
|
+
* into the framework's existing handler-style config and delegates to
|
|
7
|
+
* `createAdcpServer()`. This means every framework primitive — idempotency,
|
|
8
|
+
* RFC 9421 signing, governance, schema validation, state store, MCP/A2A
|
|
9
|
+
* wire mapping, sandbox boundary — applies unchanged. The new code is the
|
|
10
|
+
* adapter shim, not a forked runtime.
|
|
11
|
+
*
|
|
12
|
+
* **Adopter shape (unified hybrid):** each HITL-eligible tool is a single
|
|
13
|
+
* method. The method returns the wire success arm (sync fast path) OR
|
|
14
|
+
* `ctx.handoffToTask(fn)` to promote the call to a background task (HITL
|
|
15
|
+
* slow path). Adopters branch per-call; framework detects the `TaskHandoff`
|
|
16
|
+
* marker and dispatches accordingly:
|
|
17
|
+
*
|
|
18
|
+
* - Sync path: framework awaits the return value in foreground; projects
|
|
19
|
+
* it to the wire success arm. `throw new AdcpError(...)` projects to
|
|
20
|
+
* the wire `adcp_error` envelope.
|
|
21
|
+
* - HITL path: framework detects the `TaskHandoff` marker, allocates
|
|
22
|
+
* `taskId`, returns the submitted envelope to the buyer immediately,
|
|
23
|
+
* then runs the handoff function in background. The function's return
|
|
24
|
+
* value becomes the task's terminal `result`; thrown `AdcpError` becomes
|
|
25
|
+
* the terminal `error`.
|
|
26
|
+
*
|
|
27
|
+
* Generic thrown errors (`Error`, `TypeError`) fall through to the
|
|
28
|
+
* framework's `SERVICE_UNAVAILABLE` mapping.
|
|
29
|
+
*
|
|
30
|
+
* **Wired surface (6.0):** `SalesPlatform` (14 tools — 3 required core +
|
|
31
|
+
* 11 optional; unified hybrid on `create_media_buy` / `sync_creatives`),
|
|
32
|
+
* `CreativeBuilderPlatform` (build_creative / sync_creatives unified
|
|
33
|
+
* hybrid, optional preview_creative sync-only, optional refineCreative),
|
|
34
|
+
* `AudiencePlatform.syncAudiences`, `SignalsPlatform` (activate_signal,
|
|
35
|
+
* list_signals), `AccountStore` (reportUsage, getAccountFinancials),
|
|
36
|
+
* `ContentStandardsPlatform`, `CampaignGovernancePlatform`,
|
|
37
|
+
* `TenantRegistry` (multi-tenant health), `createPostgresTaskRegistry`,
|
|
38
|
+
* `tasks/get` wire handler, per-server + module-level `publishStatusChange`.
|
|
39
|
+
*
|
|
40
|
+
* **Still deferred (rc.1+):** MCP Resources subscription projection for
|
|
41
|
+
* `publishStatusChange`; `resolveAccount(undefined, { authInfo, toolName })`
|
|
42
|
+
* refactor for `provide_performance_feedback` / `list_creative_formats`
|
|
43
|
+
* no-account path in `'explicit'`-mode adopters (see `SalesPlatform` JSDoc).
|
|
44
|
+
*
|
|
45
|
+
* Status: Preview / 6.0. Not yet exported from the public `./server`
|
|
46
|
+
* subpath; reach in via `@adcp/sdk/server/decisioning/runtime` for
|
|
47
|
+
* spike experimentation only.
|
|
48
|
+
*
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.createAdcpServerFromPlatform = createAdcpServerFromPlatform;
|
|
53
|
+
exports._resetMergeSeamDedupe = _resetMergeSeamDedupe;
|
|
54
|
+
exports.getAllAdcpMigrations = getAllAdcpMigrations;
|
|
55
|
+
const node_crypto_1 = require("node:crypto");
|
|
56
|
+
const create_adcp_server_1 = require("../../create-adcp-server");
|
|
57
|
+
const account_1 = require("../account");
|
|
58
|
+
const async_outcome_1 = require("../async-outcome");
|
|
59
|
+
const errors_1 = require("../../errors");
|
|
60
|
+
const validate_platform_1 = require("./validate-platform");
|
|
61
|
+
const to_context_1 = require("./to-context");
|
|
62
|
+
const ctx_metadata_1 = require("../../ctx-metadata");
|
|
63
|
+
const idempotency_1 = require("../../idempotency");
|
|
64
|
+
const pg_1 = require("../../idempotency/backends/pg");
|
|
65
|
+
const postgres_task_registry_1 = require("./postgres-task-registry");
|
|
66
|
+
const async_outcome_2 = require("../async-outcome");
|
|
67
|
+
const zod_1 = require("zod");
|
|
68
|
+
const task_registry_1 = require("./task-registry");
|
|
69
|
+
const protocol_for_tool_1 = require("./protocol-for-tool");
|
|
70
|
+
/**
|
|
71
|
+
* Default logger when adopters don't supply `opts.logger`. `debug` /
|
|
72
|
+
* `info` are no-op; `warn` / `error` route to console so framework
|
|
73
|
+
* warnings (merge-seam collisions, SSRF rejections, observability hook
|
|
74
|
+
* misuse) surface during development. Adopters wiring `pino`/`bunyan`
|
|
75
|
+
* supply all four levels via `opts.logger`.
|
|
76
|
+
*/
|
|
77
|
+
const DEFAULT_FRAMEWORK_LOGGER = {
|
|
78
|
+
debug: () => { },
|
|
79
|
+
info: () => { },
|
|
80
|
+
// eslint-disable-next-line no-console
|
|
81
|
+
warn: console.warn.bind(console),
|
|
82
|
+
// eslint-disable-next-line no-console
|
|
83
|
+
error: console.error.bind(console),
|
|
84
|
+
};
|
|
85
|
+
const status_changes_1 = require("../status-changes");
|
|
86
|
+
const comply_controller_1 = require("../../../testing/comply-controller");
|
|
87
|
+
const normalize_errors_1 = require("../../normalize-errors");
|
|
88
|
+
/**
|
|
89
|
+
* Apply `normalizeErrors` to a sync_creatives row's optional `errors`
|
|
90
|
+
* field. Adopters often return errors as bare strings, native Error
|
|
91
|
+
* instances, or vendor-specific shapes; the wire schema requires
|
|
92
|
+
* `Error[]` with `{ code, message, ... }`. Normalizing at the
|
|
93
|
+
* projection seam means every adopter's syncCreatives method gets
|
|
94
|
+
* coerced to wire shape — the row passes strict response validation
|
|
95
|
+
* even when the adopter doesn't hand-shape every error.
|
|
96
|
+
*/
|
|
97
|
+
function normalizeRowErrors(row) {
|
|
98
|
+
if (row?.errors == null)
|
|
99
|
+
return row;
|
|
100
|
+
return { ...row, errors: (0, normalize_errors_1.normalizeErrors)(row.errors) };
|
|
101
|
+
}
|
|
102
|
+
// Use `DecisioningPlatform<any, any>` for the generic constraint. The default
|
|
103
|
+
// `TCtxMeta = Record<string, unknown>` doesn't accept adopter metadata interfaces
|
|
104
|
+
// without an index signature (e.g., `interface MyMeta { brand_id: string }`),
|
|
105
|
+
// which is a needless friction point — adopter metadata is opaque to the
|
|
106
|
+
// framework, so we don't need to constrain it here.
|
|
107
|
+
function createAdcpServerFromPlatform(platform, opts) {
|
|
108
|
+
(0, validate_platform_1.validatePlatform)(platform);
|
|
109
|
+
// Compliance-testing capability/adapter consistency.
|
|
110
|
+
//
|
|
111
|
+
// Two failure modes the framework refuses to ship:
|
|
112
|
+
// 1. Capability declared, no adapter — discovery field projects to
|
|
113
|
+
// `get_adcp_capabilities` but the wire tool has no implementation
|
|
114
|
+
// behind it. Conformance harnesses would dispatch and crash.
|
|
115
|
+
// 2. Adapter wired, capability not declared — discovery field is
|
|
116
|
+
// missing from `get_adcp_capabilities` so buyers / harnesses can't
|
|
117
|
+
// tell the agent supports compliance testing.
|
|
118
|
+
//
|
|
119
|
+
// Both throw at construction with `PlatformConfigError`. Adopters who
|
|
120
|
+
// want one without the other are doing it wrong; the right escape hatch
|
|
121
|
+
// is to set `compliance_testing.scenarios = []` (a noop block, but the
|
|
122
|
+
// spec disallows empty `scenarios` so this should never come up in
|
|
123
|
+
// practice — included only to make the constraint explicit).
|
|
124
|
+
const capHasComplianceTesting = platform.capabilities.compliance_testing != null;
|
|
125
|
+
const optsHasComplyTest = opts.complyTest != null;
|
|
126
|
+
if (capHasComplianceTesting && !optsHasComplyTest) {
|
|
127
|
+
throw new validate_platform_1.PlatformConfigError(`capabilities.compliance_testing is declared but opts.complyTest is missing. ` +
|
|
128
|
+
`Either supply complyTest (the ComplyControllerConfig adapter set) or remove ` +
|
|
129
|
+
`the compliance_testing capability block — the framework refuses to advertise ` +
|
|
130
|
+
`comply_test_controller without an implementation.`);
|
|
131
|
+
}
|
|
132
|
+
if (optsHasComplyTest && !capHasComplianceTesting) {
|
|
133
|
+
throw new validate_platform_1.PlatformConfigError(`opts.complyTest is supplied but capabilities.compliance_testing is not declared. ` +
|
|
134
|
+
`Add 'compliance_testing: {}' to your platform.capabilities — the framework needs ` +
|
|
135
|
+
`the discovery block to project comply_test_controller scenarios on get_adcp_capabilities. ` +
|
|
136
|
+
`Scenarios auto-derive from your supplied adapters; explicit 'scenarios: [...]' is optional.`);
|
|
137
|
+
}
|
|
138
|
+
// Sec-M2: warn when `signed-requests` is claimed but a custom
|
|
139
|
+
// taskWebhookEmitter is wired without acknowledging signing posture.
|
|
140
|
+
// The default emitter (bound to `serve({ webhooks })`) signs; a custom
|
|
141
|
+
// emitter that doesn't declare `unsigned: true` and doesn't delegate
|
|
142
|
+
// to the framework's signed pipeline ships unsigned webhooks to buyers
|
|
143
|
+
// who expect signatures.
|
|
144
|
+
//
|
|
145
|
+
// Gate via DEV_ALLOWLIST inversion (matches feedback_node_env_allowlist):
|
|
146
|
+
// warn when NODE_ENV is NOT in {test, development} AND no explicit ack
|
|
147
|
+
// env. Catches NODE_ENV unset, 'staging', 'prod', 'live' — common
|
|
148
|
+
// production deployments that the previous `=== 'production'` check
|
|
149
|
+
// failed open on.
|
|
150
|
+
// Footgun guard for `allowPrivateWebhookUrls` — same DEV_ALLOWLIST
|
|
151
|
+
// inversion pattern as the unsigned-emitter check below. Adopters
|
|
152
|
+
// intentionally relaxing the SSRF gate for sandbox testing aren't
|
|
153
|
+
// doing anything wrong; production deployments that flip this on
|
|
154
|
+
// accidentally are. Warn on construction when the flag is true AND
|
|
155
|
+
// NODE_ENV is unset / 'staging' / 'prod' / 'live' AND no explicit
|
|
156
|
+
// ack env. Keeps the relaxation usable for real local-test setups
|
|
157
|
+
// without letting it sneak into production.
|
|
158
|
+
if (opts.allowPrivateWebhookUrls === true) {
|
|
159
|
+
const env = process.env.NODE_ENV;
|
|
160
|
+
const isDevOrTest = env === 'test' || env === 'development';
|
|
161
|
+
const ack = process.env.ADCP_DECISIONING_ALLOW_PRIVATE_WEBHOOK_URLS === '1';
|
|
162
|
+
if (!isDevOrTest && !ack) {
|
|
163
|
+
// eslint-disable-next-line no-console
|
|
164
|
+
console.warn('[adcp/decisioning] allowPrivateWebhookUrls: true relaxes the loopback / private-IP ' +
|
|
165
|
+
'guard on push_notification_config.url. NODE_ENV is not test/development and ' +
|
|
166
|
+
'ADCP_DECISIONING_ALLOW_PRIVATE_WEBHOOK_URLS is not set — accepting private ' +
|
|
167
|
+
'destinations in production is a SSRF / cloud-metadata exfiltration path. ' +
|
|
168
|
+
'For sandbox/local testing, scope this on your own NODE_ENV check.');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (opts.taskWebhookEmitter && !opts.taskWebhookEmitter.unsigned) {
|
|
172
|
+
const claimsSigned = platform.capabilities?.specialisms?.includes('signed-requests');
|
|
173
|
+
const env = process.env.NODE_ENV;
|
|
174
|
+
const isDevOrTest = env === 'test' || env === 'development';
|
|
175
|
+
const ackUnsignedTestEmitter = process.env.ADCP_DECISIONING_ALLOW_UNSIGNED_TEST_EMITTER === '1';
|
|
176
|
+
if (claimsSigned && !isDevOrTest && !ackUnsignedTestEmitter) {
|
|
177
|
+
// eslint-disable-next-line no-console
|
|
178
|
+
console.warn('[adcp/decisioning] taskWebhookEmitter wired without unsigned:true while ' +
|
|
179
|
+
"platform.capabilities.specialisms claims 'signed-requests'. " +
|
|
180
|
+
'Buyers expecting RFC 9421 signatures will receive unsigned webhooks ' +
|
|
181
|
+
'unless your custom emitter delegates to the framework signing path. ' +
|
|
182
|
+
'If this is intentional (your emitter signs internally), set ' +
|
|
183
|
+
'unsigned: true on the emitter. For dev/test fakes, set ' +
|
|
184
|
+
'ADCP_DECISIONING_ALLOW_UNSIGNED_TEST_EMITTER=1 or NODE_ENV=test.');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Pool shortcut: when `opts.pool` is wired and a specific store/registry
|
|
188
|
+
// is NOT explicitly set, derive that store from pool with sensible
|
|
189
|
+
// defaults. Explicit per-store opts always win — this is "fill the gaps,"
|
|
190
|
+
// not "override what the adopter passed."
|
|
191
|
+
const pooledIdempotency = opts.pool && opts.idempotency === undefined ? (0, idempotency_1.createIdempotencyStore)({ backend: (0, pg_1.pgBackend)(opts.pool) }) : undefined;
|
|
192
|
+
const pooledCtxMetadata = opts.pool && opts.ctxMetadata === undefined
|
|
193
|
+
? (0, ctx_metadata_1.createCtxMetadataStore)({ backend: (0, ctx_metadata_1.pgCtxMetadataStore)(opts.pool) })
|
|
194
|
+
: undefined;
|
|
195
|
+
const pooledTaskRegistry = opts.pool && opts.taskRegistry === undefined ? (0, postgres_task_registry_1.createPostgresTaskRegistry)({ pool: opts.pool }) : undefined;
|
|
196
|
+
// Effective resolved values. Explicit > pooled > default.
|
|
197
|
+
const effectiveIdempotency = opts.idempotency ?? pooledIdempotency;
|
|
198
|
+
const effectiveCtxMetadata = opts.ctxMetadata ?? pooledCtxMetadata;
|
|
199
|
+
const taskRegistry = opts.taskRegistry ?? pooledTaskRegistry ?? buildDefaultTaskRegistry();
|
|
200
|
+
const baseBus = opts.statusChangeBus ?? (0, status_changes_1.createInMemoryStatusChangeBus)();
|
|
201
|
+
const taskWebhookEmit = opts.taskWebhookEmitter?.emit;
|
|
202
|
+
// Wrap the status-change bus so every publish fires onStatusChangePublish.
|
|
203
|
+
// Subscribers / recent-buffer behavior pass through unchanged — the wrap
|
|
204
|
+
// is only for the publish side. Hook-throw is caught + logged so adopter
|
|
205
|
+
// telemetry mistakes don't break event delivery.
|
|
206
|
+
const observability = opts.observability;
|
|
207
|
+
const statusChangeBus = observability?.onStatusChangePublish
|
|
208
|
+
? wrapBusWithObservability(baseBus, observability)
|
|
209
|
+
: baseBus;
|
|
210
|
+
const fwLogger = opts.logger ?? DEFAULT_FRAMEWORK_LOGGER;
|
|
211
|
+
const mergeOpts = { mode: opts.mergeSeam ?? 'warn', logger: fwLogger };
|
|
212
|
+
// Project per-domain capability blocks declared on the platform onto
|
|
213
|
+
// get_adcp_capabilities via createAdcpServer's `overrides.media_buy`
|
|
214
|
+
// deep-merge seam. Adopters declare audience_targeting /
|
|
215
|
+
// conversion_tracking / content_standards on `platform.capabilities`;
|
|
216
|
+
// the framework wires the deep-merge so buyers see the discovery
|
|
217
|
+
// fields without an opaque custom get_adcp_capabilities tool (which
|
|
218
|
+
// the framework refuses anyway).
|
|
219
|
+
//
|
|
220
|
+
// Each rich block also forces the corresponding `media_buy.features.*`
|
|
221
|
+
// boolean to `true`. Buyers gating on `features.audience_targeting`
|
|
222
|
+
// (which the framework auto-derives as `false` by default) would
|
|
223
|
+
// otherwise skip the rich block sitting next to it.
|
|
224
|
+
const at = platform.capabilities.audience_targeting;
|
|
225
|
+
const ct = platform.capabilities.conversion_tracking;
|
|
226
|
+
const cs = platform.capabilities.content_standards;
|
|
227
|
+
const hasMediaBuyProjection = at != null || ct != null || cs != null;
|
|
228
|
+
const mediaBuyOverrides = {
|
|
229
|
+
...(at != null && { audience_targeting: at }),
|
|
230
|
+
...(ct != null && { conversion_tracking: ct }),
|
|
231
|
+
...(cs != null && { content_standards: cs }),
|
|
232
|
+
...(hasMediaBuyProjection && {
|
|
233
|
+
features: {
|
|
234
|
+
...(at != null && { audience_targeting: true }),
|
|
235
|
+
...(ct != null && { conversion_tracking: true }),
|
|
236
|
+
...(cs != null && { content_standards: true }),
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
// Brand-protocol capability projection. Adopters who declare
|
|
241
|
+
// `capabilities.brand` get the block projected via `overrides.brand`.
|
|
242
|
+
// When `BrandRightsPlatform` is supplied, `rights: true` is auto-
|
|
243
|
+
// derived (the framework knows the wire tools are available); adopter-
|
|
244
|
+
// declared `right_types` / `available_uses` / `generation_providers` /
|
|
245
|
+
// `description` ride the deep-merge.
|
|
246
|
+
const adopterBrand = platform.capabilities.brand;
|
|
247
|
+
const hasBrandRightsImpl = platform.brandRights != null;
|
|
248
|
+
const hasBrandProjection = adopterBrand != null || hasBrandRightsImpl;
|
|
249
|
+
const brandOverrides = {
|
|
250
|
+
...(hasBrandRightsImpl && { rights: true }),
|
|
251
|
+
...adopterBrand,
|
|
252
|
+
};
|
|
253
|
+
// Account-mode capability projection. Two redundant adopter signals
|
|
254
|
+
// resolve into the same wire bit:
|
|
255
|
+
// - `capabilities.requireOperatorAuth: true` — explicit override
|
|
256
|
+
// - `accounts.resolution: 'explicit'` — derived from the account-store
|
|
257
|
+
// model (operators authenticate independently with the seller; the
|
|
258
|
+
// buyer discovers accounts via `list_accounts`, NOT `sync_accounts`).
|
|
259
|
+
// Either, taken alone, projects to `account.require_operator_auth: true`.
|
|
260
|
+
// The conformance storyboard runner reads this bit at step time and
|
|
261
|
+
// grades `sync_accounts` steps as `'not_applicable'` (rather than the
|
|
262
|
+
// misleading `'missing_tool'`) for explicit-mode adopters who correctly
|
|
263
|
+
// don't implement that tool. See storyboard runner.ts account-mode gate.
|
|
264
|
+
//
|
|
265
|
+
// `supportedBillings` projects onto the parallel `account.supported_billing`
|
|
266
|
+
// wire field — buyers pre-flight check whether the seller bills the
|
|
267
|
+
// operator (retail-media model) or the buying agent (pass-through). Without
|
|
268
|
+
// the projection, retail-media adopters that declared `['operator']` saw
|
|
269
|
+
// their buyers default-route to agent-billed flows.
|
|
270
|
+
const requireOperatorAuth = platform.capabilities.requireOperatorAuth ?? platform.accounts.resolution === 'explicit';
|
|
271
|
+
const supportedBillings = platform.capabilities.supportedBillings;
|
|
272
|
+
const hasAccountProjection = requireOperatorAuth === true || (supportedBillings?.length ?? 0) > 0;
|
|
273
|
+
const accountOverrides = {
|
|
274
|
+
...(requireOperatorAuth === true && { require_operator_auth: true }),
|
|
275
|
+
...(supportedBillings?.length && { supported_billing: [...supportedBillings] }),
|
|
276
|
+
};
|
|
277
|
+
// Compliance-testing scenarios projection. Adopters who claim the
|
|
278
|
+
// `compliance_testing` capability AND wire `complyTest` adapters
|
|
279
|
+
// expect buyers to discover which scenarios they implement via
|
|
280
|
+
// `get_adcp_capabilities.compliance_testing.scenarios`. Without
|
|
281
|
+
// projection the wire response carried an empty `compliance_testing: {}`
|
|
282
|
+
// block, the comply-track runner emitted a warning on every call,
|
|
283
|
+
// and adopters saw an actionable-looking message pointing at
|
|
284
|
+
// something they'd already done correctly. Auto-derive scenario names
|
|
285
|
+
// from the wired adapters; let an explicit
|
|
286
|
+
// `capabilities.compliance_testing.scenarios` override the
|
|
287
|
+
// auto-derivation when adopters want to advertise a subset.
|
|
288
|
+
const declaredCT = platform.capabilities.compliance_testing;
|
|
289
|
+
const wiredComplyTest = opts.complyTest;
|
|
290
|
+
const hasComplianceTestingProjection = declaredCT != null && wiredComplyTest != null;
|
|
291
|
+
const complianceTestingOverrides = hasComplianceTestingProjection
|
|
292
|
+
? {
|
|
293
|
+
scenarios: declaredCT.scenarios ? [...declaredCT.scenarios] : deriveScenariosFromAdapters(wiredComplyTest),
|
|
294
|
+
}
|
|
295
|
+
: undefined;
|
|
296
|
+
const projectedCapabilitiesConfig = hasMediaBuyProjection || hasBrandProjection || hasAccountProjection || hasComplianceTestingProjection
|
|
297
|
+
? {
|
|
298
|
+
overrides: {
|
|
299
|
+
...(hasMediaBuyProjection && { media_buy: mediaBuyOverrides }),
|
|
300
|
+
...(hasBrandProjection && { brand: brandOverrides }),
|
|
301
|
+
...(hasAccountProjection && { account: accountOverrides }),
|
|
302
|
+
...(hasComplianceTestingProjection &&
|
|
303
|
+
complianceTestingOverrides != null && { compliance_testing: complianceTestingOverrides }),
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
: undefined;
|
|
307
|
+
// Per-server `ctxFor` closure; threads the effective ctx-metadata store
|
|
308
|
+
// (explicit > pooled > none) into `buildRequestContext` so handlers see
|
|
309
|
+
// `ctx.ctxMetadata` as an account-scoped accessor. Multi-tenant hosts
|
|
310
|
+
// (TenantRegistry) get one closure per server, so per-tenant store
|
|
311
|
+
// routing is preserved.
|
|
312
|
+
const ctxFor = makeCtxFor(effectiveCtxMetadata);
|
|
313
|
+
// Construction-time warn: when the default `resolveIdempotencyPrincipal`
|
|
314
|
+
// is used (no explicit hook), the chain falls through:
|
|
315
|
+
// ctx.authInfo.clientId → ctx.sessionKey → ctx.account.id → undefined
|
|
316
|
+
// The `account.id` fallback collapses unauthenticated buyers into one
|
|
317
|
+
// shared idempotency namespace per account — fine for single-tenant
|
|
318
|
+
// deployments where every buyer authenticates, dangerous for multi-
|
|
319
|
+
// tenant hosts serving unauthenticated traffic over a shared
|
|
320
|
+
// account_id. Gate via the dev-allowlist pattern: warn unless
|
|
321
|
+
// NODE_ENV ∈ {test, development} OR the operator explicitly acks via
|
|
322
|
+
// ADCP_DECISIONING_ALLOW_ACCOUNT_ID_PRINCIPAL=1.
|
|
323
|
+
if (opts.resolveIdempotencyPrincipal === undefined) {
|
|
324
|
+
const env = process.env.NODE_ENV;
|
|
325
|
+
const inDevAllowlist = env === 'test' || env === 'development';
|
|
326
|
+
const acked = process.env.ADCP_DECISIONING_ALLOW_ACCOUNT_ID_PRINCIPAL === '1';
|
|
327
|
+
if (!inDevAllowlist && !acked) {
|
|
328
|
+
// eslint-disable-next-line no-console
|
|
329
|
+
console.warn(`[adcp/decisioning] resolveIdempotencyPrincipal not explicitly wired. ` +
|
|
330
|
+
`Default falls through: authInfo.clientId → sessionKey → account.id → undefined. ` +
|
|
331
|
+
`The account.id fallback collapses unauthenticated buyers into one shared idempotency ` +
|
|
332
|
+
`namespace per account. SAFE for single-tenant deployments where every buyer ` +
|
|
333
|
+
`authenticates; UNSAFE for multi-tenant hosts serving unauthenticated traffic over a ` +
|
|
334
|
+
`shared account_id. Wire \`authenticate\` on serve() (verifyApiKey / verifyIntrospection) ` +
|
|
335
|
+
`OR pass an explicit \`resolveIdempotencyPrincipal\` in opts. ` +
|
|
336
|
+
`Set ADCP_DECISIONING_ALLOW_ACCOUNT_ID_PRINCIPAL=1 to silence this warning.`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const config = {
|
|
340
|
+
...opts,
|
|
341
|
+
...(projectedCapabilitiesConfig != null && { capabilities: projectedCapabilitiesConfig }),
|
|
342
|
+
// Pool-derived stores override the spread above when adopters supplied
|
|
343
|
+
// `pool` but no explicit per-store opt. Explicit values still win.
|
|
344
|
+
...(effectiveIdempotency !== undefined && { idempotency: effectiveIdempotency }),
|
|
345
|
+
// v6 default principal resolver: every mutating tool requires an
|
|
346
|
+
// idempotency principal (the v5 createAdcpServer surface returns
|
|
347
|
+
// SERVICE_UNAVAILABLE when one isn't wired). v6 platform adopters
|
|
348
|
+
// who skip the explicit hook get a sensible default — auth client
|
|
349
|
+
// id when present (multi-tenant: each buyer owns its own
|
|
350
|
+
// idempotency namespace), else session key, else account id
|
|
351
|
+
// (single-tenant fallback). Adopters override by passing
|
|
352
|
+
// resolveIdempotencyPrincipal in opts; the spread above keeps
|
|
353
|
+
// explicit values winning. Closed by the Emma matrix surfacing
|
|
354
|
+
// SERVICE_UNAVAILABLE on every v6 mutating call.
|
|
355
|
+
resolveIdempotencyPrincipal: opts.resolveIdempotencyPrincipal ??
|
|
356
|
+
(ctx => ctx.authInfo?.clientId ?? ctx.sessionKey ?? ctx.account?.id ?? undefined),
|
|
357
|
+
resolveAccount: async (ref, ctx) => {
|
|
358
|
+
const start = Date.now();
|
|
359
|
+
let resolved = false;
|
|
360
|
+
let resolvedAccountId;
|
|
361
|
+
try {
|
|
362
|
+
const account = await platform.accounts.resolve(ref, {
|
|
363
|
+
...(ctx.authInfo !== undefined && { authInfo: ctx.authInfo }),
|
|
364
|
+
toolName: ctx.toolName,
|
|
365
|
+
});
|
|
366
|
+
resolved = account != null;
|
|
367
|
+
resolvedAccountId = account?.id;
|
|
368
|
+
return account;
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
if (err instanceof account_1.AccountNotFoundError)
|
|
372
|
+
return null;
|
|
373
|
+
throw err;
|
|
374
|
+
}
|
|
375
|
+
finally {
|
|
376
|
+
safeFire(observability?.onAccountResolve, {
|
|
377
|
+
tool: ctx.toolName,
|
|
378
|
+
durationMs: Date.now() - start,
|
|
379
|
+
resolved,
|
|
380
|
+
fromAuth: false,
|
|
381
|
+
...(resolvedAccountId !== undefined && { accountId: resolvedAccountId }),
|
|
382
|
+
}, 'onAccountResolve', fwLogger);
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
// Auth-derived path: framework calls this for tools whose wire request
|
|
386
|
+
// doesn't carry an `account` field (`provide_performance_feedback`,
|
|
387
|
+
// `list_creative_formats`, `tasks_get`). The platform's resolver runs
|
|
388
|
+
// with `undefined` ref + `authInfo` available — adopters of any
|
|
389
|
+
// `resolution` mode can return a non-null Account here:
|
|
390
|
+
//
|
|
391
|
+
// - `'derived'` — return the singleton.
|
|
392
|
+
// - `'implicit'` — look up by `ctx.authInfo.clientId`.
|
|
393
|
+
// - `'explicit'` — also handle the `undefined` ref branch by
|
|
394
|
+
// looking up via `ctx.authInfo.clientId` (or whichever principal
|
|
395
|
+
// field your auth wires). The framework calls this resolver
|
|
396
|
+
// regardless of declared `resolution` mode; only adopters who
|
|
397
|
+
// intentionally don't model these tools return null.
|
|
398
|
+
//
|
|
399
|
+
// A `null` return is legal — handler runs with `ctx.account`
|
|
400
|
+
// undefined. Appropriate for tools that don't need tenant scoping
|
|
401
|
+
// (publisher-wide format catalogs).
|
|
402
|
+
resolveAccountFromAuth: async (ctx) => {
|
|
403
|
+
const start = Date.now();
|
|
404
|
+
let resolved = false;
|
|
405
|
+
let resolvedAccountId;
|
|
406
|
+
try {
|
|
407
|
+
const account = await platform.accounts.resolve(undefined, {
|
|
408
|
+
...(ctx.authInfo !== undefined && { authInfo: ctx.authInfo }),
|
|
409
|
+
toolName: ctx.toolName,
|
|
410
|
+
});
|
|
411
|
+
resolved = account != null;
|
|
412
|
+
resolvedAccountId = account?.id;
|
|
413
|
+
return account;
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
if (err instanceof account_1.AccountNotFoundError)
|
|
417
|
+
return null;
|
|
418
|
+
throw err;
|
|
419
|
+
}
|
|
420
|
+
finally {
|
|
421
|
+
safeFire(observability?.onAccountResolve, {
|
|
422
|
+
tool: ctx.toolName,
|
|
423
|
+
durationMs: Date.now() - start,
|
|
424
|
+
resolved,
|
|
425
|
+
fromAuth: true,
|
|
426
|
+
...(resolvedAccountId !== undefined && { accountId: resolvedAccountId }),
|
|
427
|
+
}, 'onAccountResolve', fwLogger);
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
// Merge: platform-derived handlers WIN per-key over adopter-supplied
|
|
431
|
+
// custom handlers. Adopter handlers fill gaps for tools the v6 platform
|
|
432
|
+
// doesn't yet model (getMediaBuys, listCreativeFormats, content-standards
|
|
433
|
+
// CRUD, sync_event_sources, etc.). See `CreateAdcpServerFromPlatformOptions`
|
|
434
|
+
// JSDoc for the migration-seam contract.
|
|
435
|
+
mediaBuy: mergeHandlers(opts.mediaBuy, buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observability, fwLogger, {
|
|
436
|
+
allowPrivateWebhookUrls: opts.allowPrivateWebhookUrls === true,
|
|
437
|
+
autoEmitCompletionWebhooks: opts.autoEmitCompletionWebhooks !== false,
|
|
438
|
+
}, ctxFor, effectiveCtxMetadata), 'mediaBuy', mergeOpts),
|
|
439
|
+
creative: mergeHandlers(opts.creative, buildCreativeHandlers(platform, taskRegistry, taskWebhookEmit, observability, fwLogger, {
|
|
440
|
+
allowPrivateWebhookUrls: opts.allowPrivateWebhookUrls === true,
|
|
441
|
+
autoEmitCompletionWebhooks: opts.autoEmitCompletionWebhooks !== false,
|
|
442
|
+
}, ctxFor), 'creative', mergeOpts),
|
|
443
|
+
eventTracking: mergeHandlers(opts.eventTracking, buildEventTrackingHandlers(platform, ctxFor), 'eventTracking', mergeOpts),
|
|
444
|
+
signals: mergeHandlers(opts.signals, buildSignalsHandlers(platform, ctxFor, effectiveCtxMetadata, fwLogger), 'signals', mergeOpts),
|
|
445
|
+
governance: mergeHandlers(opts.governance, buildGovernanceHandlers(platform, ctxFor), 'governance', mergeOpts),
|
|
446
|
+
accounts: mergeHandlers(opts.accounts, buildAccountHandlers(platform, ctxFor), 'accounts', mergeOpts),
|
|
447
|
+
brandRights: mergeHandlers(opts.brandRights, buildBrandRightsHandlers(platform, ctxFor, effectiveCtxMetadata, fwLogger), 'brandRights', mergeOpts),
|
|
448
|
+
customTools: {
|
|
449
|
+
...opts.customTools,
|
|
450
|
+
tasks_get: buildTasksGetTool(platform, taskRegistry),
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
const server = (0, create_adcp_server_1.createAdcpServer)(config);
|
|
454
|
+
// Wire `comply_test_controller` if the adopter supplied adapters.
|
|
455
|
+
// `createComplyController` builds the tool definition + handler + raw
|
|
456
|
+
// dispatch; `register(server)` calls server.registerTool. Sandbox
|
|
457
|
+
// gating is the adopter's job (per-request via complyTest.sandboxGate
|
|
458
|
+
// or environment-level by guarding the createAdcpServerFromPlatform
|
|
459
|
+
// call site itself).
|
|
460
|
+
if (opts.complyTest != null) {
|
|
461
|
+
const controller = (0, comply_controller_1.createComplyController)(opts.complyTest);
|
|
462
|
+
controller.register(server);
|
|
463
|
+
}
|
|
464
|
+
return Object.assign(server, {
|
|
465
|
+
getTaskState: async (taskId, expectedAccountId) => {
|
|
466
|
+
const record = await taskRegistry.getTask(taskId);
|
|
467
|
+
if (record == null)
|
|
468
|
+
return null;
|
|
469
|
+
// Tenant boundary: if caller specified the expected account and the
|
|
470
|
+
// task's owner doesn't match, treat as not-found. Returning null
|
|
471
|
+
// here mirrors the "not-found / cross-tenant" envelope and avoids
|
|
472
|
+
// principal-enumeration via task_id probing.
|
|
473
|
+
if (expectedAccountId !== undefined && record.accountId !== expectedAccountId) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
return record;
|
|
477
|
+
},
|
|
478
|
+
awaitTask: (taskId) => taskRegistry.awaitTask(taskId),
|
|
479
|
+
statusChange: statusChangeBus,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
// `tasks_get` polling tool — buyer-facing wire path for HITL task lifecycle
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
/**
|
|
486
|
+
* Custom tool exposing `taskRegistry.getTask(taskId)` over the wire so
|
|
487
|
+
* buyer agents can poll HITL task state. Snake-case `tasks_get` (MCP tool
|
|
488
|
+
* names disallow `/`) approximates the spec's `tasks/get` method.
|
|
489
|
+
*
|
|
490
|
+
* Native MCP `tasks/get` integration via the SDK's experimental
|
|
491
|
+
* `registerToolTask` path lands in v6.1 — that registers HITL tools
|
|
492
|
+
* (`create_media_buy`, `sync_creatives`) as MCP task tools and the SDK
|
|
493
|
+
* handles the protocol-level `tasks/get` natively. Until then, this
|
|
494
|
+
* custom tool is the buyer-facing polling surface.
|
|
495
|
+
*
|
|
496
|
+
* **Tenant scoping.** The tool reads from `taskRegistry.getTask`
|
|
497
|
+
* directly. Adopters with multi-tenant deployments MUST pass `account`
|
|
498
|
+
* in the request so the framework's account-resolution flow scopes the
|
|
499
|
+
* read; the handler then verifies `record.accountId` matches the
|
|
500
|
+
* resolved account before returning the task. Single-tenant agents
|
|
501
|
+
* (`resolution: 'derived'`) get scoping for free via the auth-derived
|
|
502
|
+
* resolver.
|
|
503
|
+
*/
|
|
504
|
+
function buildTasksGetTool(platform, taskRegistry) {
|
|
505
|
+
const inputShape = {
|
|
506
|
+
// Cap task_id length: framework-issued task ids are
|
|
507
|
+
// `task_<UUIDv4>` = 41 chars. Cap at 128 so a malicious buyer can't
|
|
508
|
+
// hand us megabytes of string for a parameterized read query.
|
|
509
|
+
task_id: zod_1.z
|
|
510
|
+
.string()
|
|
511
|
+
.min(1)
|
|
512
|
+
.max(128)
|
|
513
|
+
.describe('Task identifier returned in the submitted envelope of a HITL tool call.'),
|
|
514
|
+
// `AccountReference.account_id` per `core/account-ref.json`. Stricter
|
|
515
|
+
// than `passthrough()` — must be a string when present. We don't
|
|
516
|
+
// accept the `{ brand, operator }` arm here because tenant scoping
|
|
517
|
+
// for `tasks_get` is by resolved account id, not by brand identity.
|
|
518
|
+
account: zod_1.z
|
|
519
|
+
.object({ account_id: zod_1.z.string().min(1).optional() })
|
|
520
|
+
.strict()
|
|
521
|
+
.optional()
|
|
522
|
+
.describe('Optional account reference for tenant scoping. Required for multi-tenant deployments.'),
|
|
523
|
+
};
|
|
524
|
+
return {
|
|
525
|
+
description: 'Call this when you receive `{ status: "submitted", task_id }` from create_media_buy ' +
|
|
526
|
+
'or sync_creatives — pass the same `task_id` plus your `account` to retrieve the ' +
|
|
527
|
+
'terminal lifecycle state. Returns the spec-flat tasks-get-response shape ' +
|
|
528
|
+
'(`status`, `result` on completed, `error: { code, message }` on failed). ' +
|
|
529
|
+
"Snake-case substitute for the spec's `tasks/get` method (MCP tool names disallow " +
|
|
530
|
+
'`/`); native MCP method dispatch lands in v6.1. Webhook delivery is the push-based ' +
|
|
531
|
+
'alternative when the buyer set `push_notification_config` on the original request.',
|
|
532
|
+
title: 'Get Task State',
|
|
533
|
+
inputSchema: inputShape,
|
|
534
|
+
annotations: { readOnlyHint: true },
|
|
535
|
+
// Handler receives the MCP RequestHandlerExtra as second arg — carries
|
|
536
|
+
// the caller's `authInfo` extracted by `serve({ authenticate })`. Thread
|
|
537
|
+
// it through `accounts.resolve(ref, ctx)` so adopters' `'explicit'`-mode
|
|
538
|
+
// resolvers can authorize the resolution against the principal — without
|
|
539
|
+
// this, an attacker passing `{ account: { account_id: 'tenant_B' } }`
|
|
540
|
+
// gets tenant B's account back from a naive `findById(ref.account_id)`
|
|
541
|
+
// resolver and reads tenant B's task. Same threading as the regular
|
|
542
|
+
// `resolveAccount` dispatch flow in `create-adcp-server.ts:2380-2398`.
|
|
543
|
+
handler: async (args, extra) => {
|
|
544
|
+
const ref = args.account;
|
|
545
|
+
const resolveCtx = {
|
|
546
|
+
...(extra?.authInfo !== undefined && { authInfo: extra.authInfo }),
|
|
547
|
+
toolName: 'tasks_get',
|
|
548
|
+
};
|
|
549
|
+
let resolvedAccountId;
|
|
550
|
+
if (ref) {
|
|
551
|
+
try {
|
|
552
|
+
const resolved = await platform.accounts.resolve(ref, resolveCtx);
|
|
553
|
+
if (resolved)
|
|
554
|
+
resolvedAccountId = resolved.id;
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
if (!(err instanceof account_1.AccountNotFoundError))
|
|
558
|
+
throw err;
|
|
559
|
+
}
|
|
560
|
+
if (!resolvedAccountId) {
|
|
561
|
+
return (0, errors_1.adcpError)('ACCOUNT_NOT_FOUND', {
|
|
562
|
+
message: 'The specified account does not exist',
|
|
563
|
+
field: 'account',
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
try {
|
|
569
|
+
const resolved = await platform.accounts.resolve(undefined, resolveCtx);
|
|
570
|
+
if (resolved)
|
|
571
|
+
resolvedAccountId = resolved.id;
|
|
572
|
+
}
|
|
573
|
+
catch (err) {
|
|
574
|
+
if (!(err instanceof account_1.AccountNotFoundError))
|
|
575
|
+
throw err;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const record = await taskRegistry.getTask(args.task_id);
|
|
579
|
+
if (record == null) {
|
|
580
|
+
return (0, errors_1.adcpError)('REFERENCE_NOT_FOUND', {
|
|
581
|
+
message: `Task ${args.task_id} not found`,
|
|
582
|
+
field: 'task_id',
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
// Tenant boundary. Two checks to close the auth-derived bypass:
|
|
586
|
+
// 1. If we resolved an account, the task's owning account must match.
|
|
587
|
+
// 2. If we did NOT resolve an account but the task IS owned by an
|
|
588
|
+
// account, refuse to leak. This catches the unauthenticated /
|
|
589
|
+
// `'explicit'`-mode-misconfigured caller that hits the auth-derived
|
|
590
|
+
// branch and would otherwise read any task by id.
|
|
591
|
+
if (resolvedAccountId === undefined && record.accountId) {
|
|
592
|
+
return (0, errors_1.adcpError)('REFERENCE_NOT_FOUND', {
|
|
593
|
+
message: `Task ${args.task_id} not found`,
|
|
594
|
+
field: 'task_id',
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
if (resolvedAccountId !== undefined && record.accountId !== resolvedAccountId) {
|
|
598
|
+
return (0, errors_1.adcpError)('REFERENCE_NOT_FOUND', {
|
|
599
|
+
message: `Task ${args.task_id} not found`,
|
|
600
|
+
field: 'task_id',
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
// Spec shape: `tasks-get-response.json` requires task_id, task_type,
|
|
604
|
+
// status, created_at, updated_at, protocol. Optional: completed_at
|
|
605
|
+
// (terminal states), error (failed tasks — top-level, NOT inside
|
|
606
|
+
// result), result (success-arm body for completed tasks),
|
|
607
|
+
// has_webhook (whether buyer wired push_notification_config).
|
|
608
|
+
const payload = {
|
|
609
|
+
task_id: record.taskId,
|
|
610
|
+
task_type: record.tool,
|
|
611
|
+
status: record.status,
|
|
612
|
+
protocol: (0, protocol_for_tool_1.protocolForTool)(record.tool),
|
|
613
|
+
has_webhook: record.hasWebhook === true,
|
|
614
|
+
created_at: record.createdAt,
|
|
615
|
+
updated_at: record.updatedAt,
|
|
616
|
+
};
|
|
617
|
+
// Terminal states get `completed_at` per spec (covers completed,
|
|
618
|
+
// failed, canceled). The framework writes terminal-state transitions
|
|
619
|
+
// by stamping `updated_at`, so the two coincide today.
|
|
620
|
+
if (record.status === 'completed' || record.status === 'failed' || record.status === 'canceled') {
|
|
621
|
+
payload.completed_at = record.updatedAt;
|
|
622
|
+
}
|
|
623
|
+
if (record.statusMessage)
|
|
624
|
+
payload.message = record.statusMessage;
|
|
625
|
+
if (record.status === 'completed' && record.result !== undefined) {
|
|
626
|
+
payload.result = record.result;
|
|
627
|
+
}
|
|
628
|
+
if (record.status === 'failed' && record.error) {
|
|
629
|
+
// Spec shape: top-level `error: { code, message, details? }` —
|
|
630
|
+
// matches `tasks-get-response.json`'s required `code` + `message`
|
|
631
|
+
// shape with optional `details` carrying the structured-error
|
|
632
|
+
// tail (`recovery`, `field`, `suggestion`, `retry_after`,
|
|
633
|
+
// adopter-supplied `details`).
|
|
634
|
+
const { code, message, ...details } = record.error;
|
|
635
|
+
payload.error = {
|
|
636
|
+
code,
|
|
637
|
+
message,
|
|
638
|
+
...(Object.keys(details).length > 0 && { details }),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
content: [{ type: 'text', text: `Task ${record.taskId} status: ${record.status}` }],
|
|
643
|
+
structuredContent: payload,
|
|
644
|
+
};
|
|
645
|
+
},
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
// Module-level dedupe set for `'log-once'` mode. Keyed on
|
|
649
|
+
// `${domain}|${sortedColliders}` so two constructions hitting the same
|
|
650
|
+
// collision pattern only log once across the lifetime of the process.
|
|
651
|
+
// Cleared via `_resetMergeSeamDedupe()` in tests.
|
|
652
|
+
const mergeSeamLoggedKeys = new Set();
|
|
653
|
+
/** @internal — reset the log-once dedupe set. Tests only. */
|
|
654
|
+
function _resetMergeSeamDedupe() {
|
|
655
|
+
mergeSeamLoggedKeys.clear();
|
|
656
|
+
}
|
|
657
|
+
function mergeHandlers(custom, platform, domain, opts) {
|
|
658
|
+
if (!custom && !platform)
|
|
659
|
+
return undefined;
|
|
660
|
+
if (!custom)
|
|
661
|
+
return platform;
|
|
662
|
+
if (!platform)
|
|
663
|
+
return custom;
|
|
664
|
+
if (opts.mode !== 'silent') {
|
|
665
|
+
const collisions = [];
|
|
666
|
+
for (const key of Object.keys(platform)) {
|
|
667
|
+
if (key in custom)
|
|
668
|
+
collisions.push(key);
|
|
669
|
+
}
|
|
670
|
+
if (collisions.length > 0) {
|
|
671
|
+
// Sort for stable dedupe key — same collision set logs the same key
|
|
672
|
+
// regardless of declaration order across constructions.
|
|
673
|
+
const dedupeKey = `${domain}|${[...collisions].sort().join(',')}`;
|
|
674
|
+
const shouldLog = opts.mode !== 'log-once' || !mergeSeamLoggedKeys.has(dedupeKey);
|
|
675
|
+
const message = `[adcp/decisioning] opts.${domain}.{${collisions.join(', ')}} ` +
|
|
676
|
+
`${collisions.length === 1 ? 'is' : 'are'} shadowed by platform-derived handlers. ` +
|
|
677
|
+
`The merge seam is for tools the platform doesn't model yet — once a tool has a native ` +
|
|
678
|
+
`platform method, move the logic there and remove the opts override.`;
|
|
679
|
+
if (opts.mode === 'strict') {
|
|
680
|
+
throw new validate_platform_1.PlatformConfigError(message);
|
|
681
|
+
}
|
|
682
|
+
if (shouldLog) {
|
|
683
|
+
opts.logger.warn(message);
|
|
684
|
+
if (opts.mode === 'log-once')
|
|
685
|
+
mergeSeamLoggedKeys.add(dedupeKey);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return { ...custom, ...platform };
|
|
690
|
+
}
|
|
691
|
+
// ---------------------------------------------------------------------------
|
|
692
|
+
// Observability hook plumbing
|
|
693
|
+
// ---------------------------------------------------------------------------
|
|
694
|
+
/**
|
|
695
|
+
* Fire an observability callback with throw-safe semantics — both sync
|
|
696
|
+
* throws AND rejected promises are caught + logged so a buggy span/metric
|
|
697
|
+
* callback never breaks dispatch. The framework does NOT await the
|
|
698
|
+
* callback; if you need async tracer work, do it inside the callback and
|
|
699
|
+
* the framework will not hold the dispatch path waiting for it.
|
|
700
|
+
*/
|
|
701
|
+
/**
|
|
702
|
+
* Derive scenario names from a wired `ComplyControllerConfig` adapter
|
|
703
|
+
* set. Each adapter slot maps to one wire scenario name. Order is
|
|
704
|
+
* stable (force → simulate) so adopter wire-fixture snapshots don't
|
|
705
|
+
* churn between releases. Used by the projection seam to populate
|
|
706
|
+
* `get_adcp_capabilities.compliance_testing.scenarios` when the adopter
|
|
707
|
+
* doesn't supply an explicit subset.
|
|
708
|
+
*
|
|
709
|
+
* Seed scenarios (`seed_product`, `seed_creative`, etc.) are
|
|
710
|
+
* deliberately NOT advertised on the wire — the spec scopes the
|
|
711
|
+
* `compliance_testing.scenarios` enum to forces + simulates, and the
|
|
712
|
+
* controller's own `list_scenarios` response follows the same rule.
|
|
713
|
+
* Adopters who wire seed adapters get them dispatched correctly at
|
|
714
|
+
* runtime; they just don't appear in capability discovery.
|
|
715
|
+
*/
|
|
716
|
+
function deriveScenariosFromAdapters(cfg) {
|
|
717
|
+
const out = [];
|
|
718
|
+
if (cfg.force?.creative_status)
|
|
719
|
+
out.push('force_creative_status');
|
|
720
|
+
if (cfg.force?.account_status)
|
|
721
|
+
out.push('force_account_status');
|
|
722
|
+
if (cfg.force?.media_buy_status)
|
|
723
|
+
out.push('force_media_buy_status');
|
|
724
|
+
if (cfg.force?.session_status)
|
|
725
|
+
out.push('force_session_status');
|
|
726
|
+
if (cfg.simulate?.delivery)
|
|
727
|
+
out.push('simulate_delivery');
|
|
728
|
+
if (cfg.simulate?.budget_spend)
|
|
729
|
+
out.push('simulate_budget_spend');
|
|
730
|
+
return out;
|
|
731
|
+
}
|
|
732
|
+
function safeFire(fn, arg, hookName, logger) {
|
|
733
|
+
if (!fn)
|
|
734
|
+
return;
|
|
735
|
+
let result;
|
|
736
|
+
try {
|
|
737
|
+
result = fn(arg);
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
logger.warn(`[adcp/decisioning] observability hook ${hookName} threw — telemetry callbacks must never throw. ` +
|
|
741
|
+
`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
// Catch rejected promises from accidentally-async callbacks. Without
|
|
745
|
+
// this an `async () => { throw ... }` hook would surface as
|
|
746
|
+
// UnhandledPromiseRejection and on `--unhandled-rejections=strict`
|
|
747
|
+
// would crash the process. `Promise.resolve()` coerces both real
|
|
748
|
+
// promises and user-land thenables (a thenable returning sync values
|
|
749
|
+
// is wrapped, a true Promise is returned as-is) — safer than the
|
|
750
|
+
// duck-typed `typeof .catch === 'function'` check.
|
|
751
|
+
if (result !== undefined && result !== null) {
|
|
752
|
+
Promise.resolve(result).catch((err) => {
|
|
753
|
+
logger.warn(`[adcp/decisioning] observability hook ${hookName} returned a rejected promise — ` +
|
|
754
|
+
`telemetry callbacks must never reject. ` +
|
|
755
|
+
`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Wrap a `StatusChangeBus` so every publish fires `onStatusChangePublish`
|
|
761
|
+
* after the underlying bus persists the event. Subscribers + recent-buffer
|
|
762
|
+
* pass through; only the publish side is observed.
|
|
763
|
+
*/
|
|
764
|
+
function wrapBusWithObservability(bus, observability) {
|
|
765
|
+
return {
|
|
766
|
+
publish(eventOpts) {
|
|
767
|
+
bus.publish(eventOpts);
|
|
768
|
+
if (observability.onStatusChangePublish) {
|
|
769
|
+
try {
|
|
770
|
+
observability.onStatusChangePublish({
|
|
771
|
+
accountId: eventOpts.account_id,
|
|
772
|
+
resourceType: eventOpts.resource_type,
|
|
773
|
+
resourceId: eventOpts.resource_id,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
catch (err) {
|
|
777
|
+
// eslint-disable-next-line no-console
|
|
778
|
+
console.warn(`[adcp/decisioning] observability hook onStatusChangePublish threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
subscribe: bus.subscribe.bind(bus),
|
|
783
|
+
recent: bus.recent.bind(bus),
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
// ---------------------------------------------------------------------------
|
|
787
|
+
// Default task registry — gated by NODE_ENV
|
|
788
|
+
// ---------------------------------------------------------------------------
|
|
789
|
+
/**
|
|
790
|
+
* Build the default in-memory task registry, gated by NODE_ENV.
|
|
791
|
+
*
|
|
792
|
+
* The in-memory registry loses task state on process restart — fine for
|
|
793
|
+
* tests and local dev, NOT fine for production. Gate via allowlist:
|
|
794
|
+
* `NODE_ENV` must be `'test'` or `'development'`, OR the operator must
|
|
795
|
+
* explicitly opt in via `ADCP_DECISIONING_ALLOW_INMEMORY_TASKS=1`.
|
|
796
|
+
*
|
|
797
|
+
* Pattern follows `feedback_node_env_allowlist.md`: never compare
|
|
798
|
+
* `=== 'production'` (production may unset NODE_ENV entirely); always
|
|
799
|
+
* allowlist the safe modes.
|
|
800
|
+
*/
|
|
801
|
+
/**
|
|
802
|
+
* Combined DDL for all framework persistence tables: idempotency cache,
|
|
803
|
+
* ctx-metadata cache, and decisioning task registry. Run once per database
|
|
804
|
+
* during deployment / boot. Idempotent — safe to re-run.
|
|
805
|
+
*
|
|
806
|
+
* Use with the `pool` shortcut on `createAdcpServerFromPlatform`:
|
|
807
|
+
*
|
|
808
|
+
* ```ts
|
|
809
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
810
|
+
* await pool.query(getAllAdcpMigrations());
|
|
811
|
+
*
|
|
812
|
+
* createAdcpServerFromPlatform(myPlatform, {
|
|
813
|
+
* name: '...', version: '...',
|
|
814
|
+
* pool,
|
|
815
|
+
* });
|
|
816
|
+
* ```
|
|
817
|
+
*
|
|
818
|
+
* Adopters who don't use the `pool` shortcut should call the per-store
|
|
819
|
+
* migration helpers (`getIdempotencyMigration`, `getCtxMetadataMigration`,
|
|
820
|
+
* `getDecisioningTaskRegistryMigration`) only for the stores they wire.
|
|
821
|
+
*
|
|
822
|
+
* @public
|
|
823
|
+
*/
|
|
824
|
+
function getAllAdcpMigrations() {
|
|
825
|
+
return [(0, pg_1.getIdempotencyMigration)(), (0, ctx_metadata_1.getCtxMetadataMigration)(), (0, postgres_task_registry_1.getDecisioningTaskRegistryMigration)()].join('\n\n');
|
|
826
|
+
}
|
|
827
|
+
function buildDefaultTaskRegistry() {
|
|
828
|
+
const env = process.env.NODE_ENV;
|
|
829
|
+
const safe = env === 'test' || env === 'development';
|
|
830
|
+
const ack = process.env.ADCP_DECISIONING_ALLOW_INMEMORY_TASKS === '1';
|
|
831
|
+
if (!safe && !ack) {
|
|
832
|
+
throw new Error('createAdcpServerFromPlatform: in-memory task registry refused outside ' +
|
|
833
|
+
'{NODE_ENV=test, NODE_ENV=development}. Production deployments need a ' +
|
|
834
|
+
'durable task registry — pick one of:\n' +
|
|
835
|
+
' 1. (Recommended) Pass `taskRegistry: createPostgresTaskRegistry({ pool })` ' +
|
|
836
|
+
'to keep HITL tasks across restarts. See `@adcp/sdk/server/decisioning` ' +
|
|
837
|
+
'for `getDecisioningTaskRegistryMigration()` — run it once against your DB.\n' +
|
|
838
|
+
' 2. Pass `taskRegistry: createInMemoryTaskRegistry()` explicitly if you ' +
|
|
839
|
+
'accept that in-flight tasks are lost on process restart. The explicit ' +
|
|
840
|
+
'pass-in is the contract — saying "yes I want in-memory in production" ' +
|
|
841
|
+
'in code is the right shape.\n' +
|
|
842
|
+
' 3. ADCP_DECISIONING_ALLOW_INMEMORY_TASKS=1 env flag is the ops escape ' +
|
|
843
|
+
'hatch (same effect as #2 but config-only); prefer #2 in adopter code.');
|
|
844
|
+
}
|
|
845
|
+
return (0, task_registry_1.createInMemoryTaskRegistry)();
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Project a sync platform call onto the wire dispatch shape. `AdcpError`
|
|
849
|
+
* throws → wire `adcp_error` envelope; other thrown errors bubble to the
|
|
850
|
+
* framework's `SERVICE_UNAVAILABLE` mapping.
|
|
851
|
+
*/
|
|
852
|
+
async function projectSync(fn, mapResult) {
|
|
853
|
+
try {
|
|
854
|
+
const result = await fn();
|
|
855
|
+
const wire = mapResult(result);
|
|
856
|
+
// Single-chokepoint runtime strip: ctx_metadata MUST NEVER cross to the
|
|
857
|
+
// buyer. Defense-in-depth (compile-time WireShape<T> + runtime walk).
|
|
858
|
+
// Mutates `wire` in place — every handler builds a fresh response per
|
|
859
|
+
// call so mutation is safe. Runs BEFORE the framework wraps in envelope
|
|
860
|
+
// / caches in idempotency, so cached replays stay clean too.
|
|
861
|
+
if (wire != null && typeof wire === 'object') {
|
|
862
|
+
(0, ctx_metadata_1.stripCtxMetadata)(wire);
|
|
863
|
+
}
|
|
864
|
+
return wire;
|
|
865
|
+
}
|
|
866
|
+
catch (err) {
|
|
867
|
+
if (err instanceof async_outcome_1.AdcpError) {
|
|
868
|
+
return (0, errors_1.adcpError)(err.code, {
|
|
869
|
+
message: err.message,
|
|
870
|
+
recovery: err.recovery,
|
|
871
|
+
...(err.field !== undefined && { field: err.field }),
|
|
872
|
+
...(err.suggestion !== undefined && { suggestion: err.suggestion }),
|
|
873
|
+
...(err.retry_after !== undefined && { retry_after: err.retry_after }),
|
|
874
|
+
...(err.details !== undefined && { details: err.details }),
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
// AccountNotFoundError is documented as throwable only from
|
|
878
|
+
// AccountStore.resolve(); but adopters new to the framework
|
|
879
|
+
// sometimes throw it from a specialism method body. Project to
|
|
880
|
+
// ACCOUNT_NOT_FOUND so the wire envelope is right either way —
|
|
881
|
+
// closes the silent-SERVICE_UNAVAILABLE foot-gun the security
|
|
882
|
+
// review flagged. Adopters are still encouraged to handle account
|
|
883
|
+
// not-found inside resolve() (canonical) — this is a guardrail.
|
|
884
|
+
if (err instanceof account_1.AccountNotFoundError) {
|
|
885
|
+
return (0, errors_1.adcpError)('ACCOUNT_NOT_FOUND', {
|
|
886
|
+
message: 'Account not found',
|
|
887
|
+
field: 'account',
|
|
888
|
+
recovery: 'terminal',
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
throw err;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Route a unified-shape return value: if it's a `TaskHandoff` marker,
|
|
896
|
+
* dispatch through `dispatchHitl`; otherwise pass through the sync
|
|
897
|
+
* projection.
|
|
898
|
+
*
|
|
899
|
+
* Adopter return type is `Success | TaskHandoff<Success>`. Each
|
|
900
|
+
* specialism dispatcher uses this helper so all three call sites
|
|
901
|
+
* (`createMediaBuy`, `sales.syncCreatives`, `creative.syncCreatives`)
|
|
902
|
+
* route through a single seam — closes round-6 CR-1 (drift across
|
|
903
|
+
* three near-identical `isTaskHandoff` branches).
|
|
904
|
+
*
|
|
905
|
+
* `project` shapes both arms identically — for `syncCreatives`,
|
|
906
|
+
* `rows → { creatives: rows }`; for `createMediaBuy`, identity.
|
|
907
|
+
*/
|
|
908
|
+
async function routeIfHandoff(taskRegistry, opts, result, project) {
|
|
909
|
+
if ((0, async_outcome_2.isTaskHandoff)(result)) {
|
|
910
|
+
const taskFn = (0, async_outcome_2._extractTaskFn)(result);
|
|
911
|
+
if (!taskFn) {
|
|
912
|
+
// Forgery — adopter constructed something with the brand symbol
|
|
913
|
+
// but didn't go through ctx.handoffToTask. Treat as a sync
|
|
914
|
+
// success arm with an empty body (caller-supplied projection
|
|
915
|
+
// shapes the result; this branch is defensive).
|
|
916
|
+
return project(result);
|
|
917
|
+
}
|
|
918
|
+
return dispatchHitl(taskRegistry, opts, async (taskId) => {
|
|
919
|
+
const inner = await taskFn((0, to_context_1.buildHandoffContext)(taskRegistry, taskId));
|
|
920
|
+
return project(inner);
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
// Catch the most common LLM-scaffolded mistake: hand-rolling a
|
|
924
|
+
// `{status: 'submitted', task_id: '...'}` envelope instead of returning
|
|
925
|
+
// `ctx.handoffToTask(fn)`. The framework owns the submitted envelope —
|
|
926
|
+
// adopters either return the sync-success arm or a TaskHandoff marker.
|
|
927
|
+
// A bare submitted-shape return here would slip past dispatch and fail
|
|
928
|
+
// response-schema validation downstream with a generic shape error;
|
|
929
|
+
// pointing at the right SDK primitive up-front saves the debug round-trip.
|
|
930
|
+
if (result != null &&
|
|
931
|
+
typeof result === 'object' &&
|
|
932
|
+
result.status === 'submitted' &&
|
|
933
|
+
'task_id' in result) {
|
|
934
|
+
throw new Error(`Specialism handler returned a hand-rolled \`{status: 'submitted', task_id}\` ` +
|
|
935
|
+
`envelope. The framework owns the submitted envelope — return ` +
|
|
936
|
+
`\`ctx.handoffToTask(async (taskCtx) => { ... })\` from the handler ` +
|
|
937
|
+
`and the framework will issue the task_id, persist the handoff, and ` +
|
|
938
|
+
`wrap the wire envelope. Returning a bare submitted shape skips the ` +
|
|
939
|
+
`task registry and the buyer ends up polling a task_id the framework ` +
|
|
940
|
+
`never registered.`);
|
|
941
|
+
}
|
|
942
|
+
const projected = project(result);
|
|
943
|
+
if (opts.autoEmitCompletion === true && opts.pushNotificationUrl) {
|
|
944
|
+
// Auto-emit completion webhook on sync-success arm — fire-and-forget.
|
|
945
|
+
// Awaiting inline would let an attacker-controlled
|
|
946
|
+
// `push_notification_config.url` (e.g., a slowloris receiver) hold
|
|
947
|
+
// the seller's request worker for the full retry budget — minutes-
|
|
948
|
+
// to-tens-of-minutes per call, by spec-conformant payload. The buyer
|
|
949
|
+
// already has the result inline; webhook delivery is purely a
|
|
950
|
+
// notification convenience and shares the SPEC_WEBHOOK_TASK_TYPES
|
|
951
|
+
// gate with the HITL path. Errors land on the framework logger and
|
|
952
|
+
// the `onWebhookEmit` observability hook for operator alerting.
|
|
953
|
+
void emitSyncCompletionWebhook(opts, projected).catch((err) => {
|
|
954
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
955
|
+
opts.logger.warn(`[adcp/decisioning] sync completion webhook background-error: ${msg}`);
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
return projected;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Auto-fire the post-success webhook for the sync arm of a mutating
|
|
962
|
+
* tool. Mirrors `emitTaskWebhook` (HITL path) but synthesizes a
|
|
963
|
+
* `task_id` since sync responses don't allocate a registry task.
|
|
964
|
+
* Buyers correlate via the resource IDs embedded in `result`
|
|
965
|
+
* (`media_buy_id`, `creative_id`, etc.) — `task_id` is informational
|
|
966
|
+
* for the spec's required-field shape.
|
|
967
|
+
*
|
|
968
|
+
* Webhook delivery failures are logged-and-swallowed: the sync
|
|
969
|
+
* response succeeded and the buyer has the result inline, so blocking
|
|
970
|
+
* the response on a flaky webhook receiver would be strictly worse
|
|
971
|
+
* than the buyer eventually polling.
|
|
972
|
+
*/
|
|
973
|
+
async function emitSyncCompletionWebhook(opts, result) {
|
|
974
|
+
if (!opts.emitWebhook || !opts.pushNotificationUrl)
|
|
975
|
+
return;
|
|
976
|
+
if (!protocol_for_tool_1.SPEC_WEBHOOK_TASK_TYPES.has(opts.tool)) {
|
|
977
|
+
opts.logger.warn(`[adcp/decisioning] sync completion webhook for ${opts.tool} skipped — ` +
|
|
978
|
+
`tool not in spec task-type enum (closed 20-value set per enums/task-type.json). ` +
|
|
979
|
+
`Use publishStatusChange for long-running ${opts.tool} state.`);
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
const taskId = `sync-${(0, node_crypto_1.randomUUID)()}`;
|
|
983
|
+
const wirePayload = buildTaskWebhookPayload(opts, taskId, 'completed', { result });
|
|
984
|
+
const start = Date.now();
|
|
985
|
+
let success = false;
|
|
986
|
+
let errorMessages;
|
|
987
|
+
let errorCode;
|
|
988
|
+
try {
|
|
989
|
+
const r = await opts.emitWebhook({
|
|
990
|
+
url: opts.pushNotificationUrl,
|
|
991
|
+
payload: wirePayload,
|
|
992
|
+
operation_id: `${opts.tool}.${taskId}`,
|
|
993
|
+
});
|
|
994
|
+
success = r?.delivered === true;
|
|
995
|
+
if (r && Array.isArray(r.errors) && r.errors.length > 0) {
|
|
996
|
+
errorMessages = r.errors;
|
|
997
|
+
errorCode = bucketWebhookError(r.errors[0] ?? '');
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
catch (err) {
|
|
1001
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1002
|
+
errorMessages = [msg];
|
|
1003
|
+
errorCode = bucketWebhookError(msg);
|
|
1004
|
+
opts.logger.warn(`[adcp/decisioning] sync completion webhook for ${opts.tool} failed: ${msg}`);
|
|
1005
|
+
}
|
|
1006
|
+
finally {
|
|
1007
|
+
safeFire(opts.observability?.onWebhookEmit, {
|
|
1008
|
+
taskId,
|
|
1009
|
+
tool: opts.tool,
|
|
1010
|
+
status: 'completed',
|
|
1011
|
+
url: opts.pushNotificationUrl,
|
|
1012
|
+
success,
|
|
1013
|
+
durationMs: Date.now() - start,
|
|
1014
|
+
...(errorCode && { errorCode }),
|
|
1015
|
+
...(errorMessages && { errorMessages }),
|
|
1016
|
+
}, 'onWebhookEmit', opts.logger);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async function dispatchHitl(taskRegistry, opts, taskFn) {
|
|
1020
|
+
const createStart = Date.now();
|
|
1021
|
+
const { taskId } = await taskRegistry.create({
|
|
1022
|
+
tool: opts.tool,
|
|
1023
|
+
accountId: opts.accountId,
|
|
1024
|
+
hasWebhook: opts.pushNotificationUrl !== undefined,
|
|
1025
|
+
});
|
|
1026
|
+
safeFire(opts.observability?.onTaskCreate, {
|
|
1027
|
+
tool: opts.tool,
|
|
1028
|
+
taskId,
|
|
1029
|
+
accountId: opts.accountId,
|
|
1030
|
+
durationMs: Date.now() - createStart,
|
|
1031
|
+
}, 'onTaskCreate', opts.logger);
|
|
1032
|
+
const taskStart = Date.now();
|
|
1033
|
+
// Single helper for the four `onTaskTransition` fire sites.
|
|
1034
|
+
const fireTransition = (status, errorCode) => {
|
|
1035
|
+
safeFire(opts.observability?.onTaskTransition, {
|
|
1036
|
+
taskId,
|
|
1037
|
+
tool: opts.tool,
|
|
1038
|
+
accountId: opts.accountId,
|
|
1039
|
+
status,
|
|
1040
|
+
durationMs: Date.now() - taskStart,
|
|
1041
|
+
...(errorCode !== undefined && { errorCode }),
|
|
1042
|
+
}, 'onTaskTransition', opts.logger);
|
|
1043
|
+
};
|
|
1044
|
+
// Three failure surfaces:
|
|
1045
|
+
// 1. taskFn throws → record failure → emit failed webhook
|
|
1046
|
+
// 2. taskFn succeeds but registry write fails (DB outage) → log only;
|
|
1047
|
+
// do NOT emit webhook (buyer doesn't know task succeeded), do NOT
|
|
1048
|
+
// try to fail() the task (taskFn DID succeed; mismatch would
|
|
1049
|
+
// mislead operator reconciliation)
|
|
1050
|
+
// 3. taskFn fails AND registry fail-write also fails → log only;
|
|
1051
|
+
// do not emit webhook (registry state is inconsistent)
|
|
1052
|
+
//
|
|
1053
|
+
// Webhook delivery is gated on the registry write succeeding so the
|
|
1054
|
+
// buyer's view (via webhook OR getTaskState) is always consistent.
|
|
1055
|
+
const completion = (async () => {
|
|
1056
|
+
let result;
|
|
1057
|
+
let taskFnError;
|
|
1058
|
+
try {
|
|
1059
|
+
result = await taskFn(taskId);
|
|
1060
|
+
}
|
|
1061
|
+
catch (err) {
|
|
1062
|
+
taskFnError = err;
|
|
1063
|
+
}
|
|
1064
|
+
if (taskFnError === undefined) {
|
|
1065
|
+
// Success path
|
|
1066
|
+
try {
|
|
1067
|
+
await taskRegistry.complete(taskId, result);
|
|
1068
|
+
}
|
|
1069
|
+
catch (registryErr) {
|
|
1070
|
+
opts.logger.error(`[adcp/decisioning] task ${taskId} (${opts.tool}) completed but registry write failed — ` +
|
|
1071
|
+
`manual reconciliation required. Webhook not emitted; buyer state will diverge until resolved. ` +
|
|
1072
|
+
`Error: ${registryErr instanceof Error ? registryErr.message : String(registryErr)}`);
|
|
1073
|
+
fireTransition('failed', 'REGISTRY_WRITE_FAILED');
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
fireTransition('completed');
|
|
1077
|
+
await emitTaskWebhook(opts, {
|
|
1078
|
+
task: { task_id: taskId, status: 'completed', result },
|
|
1079
|
+
});
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
// Failure path
|
|
1083
|
+
const structured = taskFnError instanceof async_outcome_1.AdcpError
|
|
1084
|
+
? taskFnError.toStructuredError()
|
|
1085
|
+
: {
|
|
1086
|
+
code: 'SERVICE_UNAVAILABLE',
|
|
1087
|
+
recovery: 'transient',
|
|
1088
|
+
message: taskFnError instanceof Error ? taskFnError.message : String(taskFnError),
|
|
1089
|
+
};
|
|
1090
|
+
try {
|
|
1091
|
+
await taskRegistry.fail(taskId, structured);
|
|
1092
|
+
}
|
|
1093
|
+
catch (registryErr) {
|
|
1094
|
+
opts.logger.error(`[adcp/decisioning] task ${taskId} (${opts.tool}) failed AND registry fail-write also failed — ` +
|
|
1095
|
+
`manual reconciliation required. Webhook not emitted. ` +
|
|
1096
|
+
`taskFn error: ${structured.message}; registry error: ${registryErr instanceof Error ? registryErr.message : String(registryErr)}`);
|
|
1097
|
+
fireTransition('failed', 'REGISTRY_WRITE_FAILED');
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
fireTransition('failed', structured.code);
|
|
1101
|
+
await emitTaskWebhook(opts, {
|
|
1102
|
+
task: { task_id: taskId, status: 'failed', error: structured },
|
|
1103
|
+
});
|
|
1104
|
+
})();
|
|
1105
|
+
taskRegistry._registerBackground(taskId, completion);
|
|
1106
|
+
return { status: 'submitted', task_id: taskId };
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* AdCP wire spec puts task / status / context fields on the webhook envelope
|
|
1110
|
+
* top level (`mcp-webhook-payload.json`); the success / failure body lives on
|
|
1111
|
+
* `result`. This helper builds that shape from the v6 task lifecycle data.
|
|
1112
|
+
*
|
|
1113
|
+
* Spec requires top-level: `idempotency_key`, `task_id`, `task_type`, `status`,
|
|
1114
|
+
* `timestamp`. Optional: `protocol`, `context_id`, `message`, `result`,
|
|
1115
|
+
* `operation_id`. We add `validation_token` (echoed from
|
|
1116
|
+
* `push_notification_config.token`) outside the spec but consistent with the
|
|
1117
|
+
* intent — receivers that don't expect it ignore the extra property.
|
|
1118
|
+
*
|
|
1119
|
+
* The webhook emitter generates `idempotency_key` internally from
|
|
1120
|
+
* `operation_id`; we mirror it on the payload body so receivers running
|
|
1121
|
+
* spec-conformant `mcp-webhook-payload.json` validation see the required
|
|
1122
|
+
* field. Same value is on the HTTP `Idempotency-Key` header for HTTP-level
|
|
1123
|
+
* dedup.
|
|
1124
|
+
*/
|
|
1125
|
+
function buildTaskWebhookPayload(opts, taskId, status, artifact) {
|
|
1126
|
+
const idempotencyKey = (0, node_crypto_1.randomUUID)();
|
|
1127
|
+
const payload = {
|
|
1128
|
+
idempotency_key: idempotencyKey,
|
|
1129
|
+
task_id: taskId,
|
|
1130
|
+
task_type: opts.tool,
|
|
1131
|
+
status,
|
|
1132
|
+
timestamp: new Date().toISOString(),
|
|
1133
|
+
protocol: (0, protocol_for_tool_1.protocolForTool)(opts.tool),
|
|
1134
|
+
};
|
|
1135
|
+
// Spec doesn't define a wire field name for the echoed token. Buyers
|
|
1136
|
+
// pass `push_notification_config.token` on the request; we echo it back
|
|
1137
|
+
// under the same name `token` so receivers verifying via the request
|
|
1138
|
+
// field can find it. (Earlier preview drops named this `validation_token`
|
|
1139
|
+
// — that wasn't spec-aligned and won't be picked up by buyers wiring
|
|
1140
|
+
// against the spec request shape.)
|
|
1141
|
+
if (opts.pushNotificationToken !== undefined) {
|
|
1142
|
+
payload.token = opts.pushNotificationToken;
|
|
1143
|
+
}
|
|
1144
|
+
// `result` is the AdCP async-response-data union — for completed it's
|
|
1145
|
+
// the success-arm body; for failed it carries `errors: AdcpStructuredError[]`
|
|
1146
|
+
// alongside the empty success shape.
|
|
1147
|
+
if (status === 'completed' && artifact.result !== undefined) {
|
|
1148
|
+
payload.result = artifact.result;
|
|
1149
|
+
}
|
|
1150
|
+
if (status === 'failed' && artifact.error !== undefined) {
|
|
1151
|
+
payload.result = { errors: [artifact.error] };
|
|
1152
|
+
payload.message = artifact.error.message;
|
|
1153
|
+
}
|
|
1154
|
+
return payload;
|
|
1155
|
+
}
|
|
1156
|
+
// `protocolForTool` and `SPEC_WEBHOOK_TASK_TYPES` are exported from
|
|
1157
|
+
// `protocol-for-tool.ts` — see import at top of file.
|
|
1158
|
+
async function emitTaskWebhook(opts, source) {
|
|
1159
|
+
if (!opts.emitWebhook || !opts.pushNotificationUrl)
|
|
1160
|
+
return;
|
|
1161
|
+
const taskId = source.task.task_id;
|
|
1162
|
+
// Spec gate: `enums/task-type.json` is a closed 20-value enum at AdCP
|
|
1163
|
+
// 3.0 GA. Spec-validating receivers reject envelopes with a non-spec
|
|
1164
|
+
// `task_type` value. The framework dispatches a wider tool surface
|
|
1165
|
+
// than the spec-listed task types — for those, skip webhook delivery
|
|
1166
|
+
// (adopters surface long-running state via `publishStatusChange`
|
|
1167
|
+
// instead). Tracking spec issue to widen the enum.
|
|
1168
|
+
if (!protocol_for_tool_1.SPEC_WEBHOOK_TASK_TYPES.has(opts.tool)) {
|
|
1169
|
+
opts.logger.warn(`[adcp/decisioning] task webhook for ${taskId} (${opts.tool}) skipped — ` +
|
|
1170
|
+
`tool not in spec task-type enum (closed 20-value set per enums/task-type.json). ` +
|
|
1171
|
+
`Use publishStatusChange for long-running ${opts.tool} state.`);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
const wirePayload = buildTaskWebhookPayload(opts, taskId, source.task.status, {
|
|
1175
|
+
...(source.task.result !== undefined && { result: source.task.result }),
|
|
1176
|
+
...(source.task.error !== undefined && { error: source.task.error }),
|
|
1177
|
+
});
|
|
1178
|
+
const start = Date.now();
|
|
1179
|
+
let success = false;
|
|
1180
|
+
let errorMessages;
|
|
1181
|
+
let errorCode;
|
|
1182
|
+
try {
|
|
1183
|
+
const result = await opts.emitWebhook({
|
|
1184
|
+
url: opts.pushNotificationUrl,
|
|
1185
|
+
payload: wirePayload,
|
|
1186
|
+
operation_id: `${opts.tool}.${taskId}`,
|
|
1187
|
+
});
|
|
1188
|
+
success = result?.delivered === true;
|
|
1189
|
+
if (result && Array.isArray(result.errors) && result.errors.length > 0) {
|
|
1190
|
+
errorMessages = result.errors;
|
|
1191
|
+
errorCode = bucketWebhookError(result.errors[0] ?? '');
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
catch (err) {
|
|
1195
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1196
|
+
errorMessages = [msg];
|
|
1197
|
+
errorCode = bucketWebhookError(msg);
|
|
1198
|
+
// Webhook failures don't fail the task — registry already records the
|
|
1199
|
+
// terminal state. URL redacted from log to avoid leaking buyer-supplied
|
|
1200
|
+
// attacker-controllable values into operator log aggregators.
|
|
1201
|
+
opts.logger.warn(`[adcp/decisioning] task webhook for ${taskId} (${opts.tool}, status=${source.task.status}) ` + `failed: ${msg}`);
|
|
1202
|
+
}
|
|
1203
|
+
finally {
|
|
1204
|
+
safeFire(opts.observability?.onWebhookEmit, {
|
|
1205
|
+
taskId,
|
|
1206
|
+
tool: opts.tool,
|
|
1207
|
+
status: source.task.status,
|
|
1208
|
+
url: opts.pushNotificationUrl,
|
|
1209
|
+
success,
|
|
1210
|
+
durationMs: Date.now() - start,
|
|
1211
|
+
...(errorCode && { errorCode }),
|
|
1212
|
+
...(errorMessages && { errorMessages }),
|
|
1213
|
+
}, 'onWebhookEmit', opts.logger);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Bucket a free-text webhook error into a metric-tag-safe code. Matches
|
|
1218
|
+
* `DecisioningObservabilityHooks.onWebhookEmit.errorCode` documented enum
|
|
1219
|
+
* (`'TIMEOUT'`, `'CONNECTION_REFUSED'`, `'HTTP_4XX'`, `'HTTP_5XX'`,
|
|
1220
|
+
* `'SIGNATURE_FAILURE'`, `'UNKNOWN'`). Adopters tag DD/Prom by the bucket;
|
|
1221
|
+
* `errorMessages` carries the raw text for log adopters.
|
|
1222
|
+
*/
|
|
1223
|
+
function bucketWebhookError(msg) {
|
|
1224
|
+
const lower = msg.toLowerCase();
|
|
1225
|
+
if (lower.includes('timeout') || lower.includes('etimedout'))
|
|
1226
|
+
return 'TIMEOUT';
|
|
1227
|
+
if (lower.includes('econnrefused') || lower.includes('connection refused'))
|
|
1228
|
+
return 'CONNECTION_REFUSED';
|
|
1229
|
+
if (lower.includes('signature') || lower.includes('rfc 9421'))
|
|
1230
|
+
return 'SIGNATURE_FAILURE';
|
|
1231
|
+
// Find ALL 3-digit HTTP-status-shaped tokens. Take the LARGEST one —
|
|
1232
|
+
// operator triage cares about the most-severe status, not the
|
|
1233
|
+
// left-most occurrence. Fixes "upstream 502 (proxy received 401)"
|
|
1234
|
+
// which would mis-bucket as HTTP_4XX under a first-match policy.
|
|
1235
|
+
const matches = lower.match(/\b[45]\d\d\b/g);
|
|
1236
|
+
if (matches && matches.length > 0) {
|
|
1237
|
+
const codes = matches.map(m => parseInt(m, 10));
|
|
1238
|
+
const max = Math.max(...codes);
|
|
1239
|
+
return max >= 500 ? 'HTTP_5XX' : 'HTTP_4XX';
|
|
1240
|
+
}
|
|
1241
|
+
return 'UNKNOWN';
|
|
1242
|
+
}
|
|
1243
|
+
function makeCtxFor(ctxMetadataStore) {
|
|
1244
|
+
return handlerCtx => (0, to_context_1.buildRequestContext)(handlerCtx, ctxMetadataStore);
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Auto-store helper. After a publisher returns resources from a discovery
|
|
1248
|
+
* tool (`getProducts`, `getMediaBuys`, etc.), persist each resource's wire
|
|
1249
|
+
* shape (minus `ctx_metadata`) alongside the publisher's `ctx_metadata`
|
|
1250
|
+
* blob. Subsequent calls referencing the same id by string can be hydrated
|
|
1251
|
+
* (publisher sees the full resource as `req.packages[i].product`).
|
|
1252
|
+
*
|
|
1253
|
+
* Failures are logged + swallowed — auto-store must NEVER break a
|
|
1254
|
+
* successful response.
|
|
1255
|
+
*/
|
|
1256
|
+
async function autoStoreResources(store, accountId, kind, resources, idField, logger) {
|
|
1257
|
+
if (!store || !accountId || !resources)
|
|
1258
|
+
return;
|
|
1259
|
+
let skippedMissingId = 0;
|
|
1260
|
+
for (const r of resources) {
|
|
1261
|
+
if (r == null || typeof r !== 'object')
|
|
1262
|
+
continue;
|
|
1263
|
+
const obj = r;
|
|
1264
|
+
const id = obj[idField];
|
|
1265
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
1266
|
+
// The id field is wire-required on every resource the framework
|
|
1267
|
+
// auto-stores (e.g. `signal_agent_segment_id` on a signal,
|
|
1268
|
+
// `product_id` on a product). Silently skipping leaves buyers with
|
|
1269
|
+
// no way to reference the resource on a downstream mutating call —
|
|
1270
|
+
// a strong indicator the handler returned a misshaped response.
|
|
1271
|
+
skippedMissingId++;
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
const ctxMeta = obj['ctx_metadata'];
|
|
1275
|
+
// Strip ctx_metadata from the resource before storing — round-trip
|
|
1276
|
+
// restores it on hydration. Keeping a pristine wire copy in `resource`.
|
|
1277
|
+
const { ctx_metadata: _stripped, ...wireResource } = obj;
|
|
1278
|
+
void _stripped;
|
|
1279
|
+
try {
|
|
1280
|
+
// Use `setResource` (not `setEntry`) so a publisher's prior
|
|
1281
|
+
// `ctx.ctxMetadata.set(kind, id, blob)` is preserved when the
|
|
1282
|
+
// publisher returns the resource WITHOUT `ctx_metadata` inline.
|
|
1283
|
+
// Auto-store should never clobber adopter-managed state.
|
|
1284
|
+
await store.setResource(accountId, kind, id, wireResource, ctxMeta);
|
|
1285
|
+
}
|
|
1286
|
+
catch (err) {
|
|
1287
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1288
|
+
logger.warn(`[adcp/decisioning] auto-store ${kind} ${id} failed: ${msg}`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (skippedMissingId > 0) {
|
|
1292
|
+
logger.warn(`[adcp/decisioning] auto-store skipped ${skippedMissingId} ${kind} ` +
|
|
1293
|
+
`record(s) missing required '${idField}' — buyers will not be able ` +
|
|
1294
|
+
`to reference these resources on a subsequent mutating call.`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Auto-hydrate helper. Before invoking a publisher's mutating handler,
|
|
1299
|
+
* walk the request's resource references and attach the full wire
|
|
1300
|
+
* resource (including `ctx_metadata`) to each. Mutates the request in
|
|
1301
|
+
* place — adds an extra typed field alongside the original id.
|
|
1302
|
+
*
|
|
1303
|
+
* For `createMediaBuy`: walks `req.packages`, hydrates each with
|
|
1304
|
+
* `pkg.product = { ...resource, ctx_metadata }` keyed by `pkg.product_id`.
|
|
1305
|
+
*
|
|
1306
|
+
* Failures are logged + swallowed; the publisher still receives the
|
|
1307
|
+
* un-hydrated request and can fall back to its own DB.
|
|
1308
|
+
*/
|
|
1309
|
+
async function hydratePackagesWithProducts(store, accountId, packages, logger) {
|
|
1310
|
+
if (!store || !accountId || !packages || packages.length === 0)
|
|
1311
|
+
return;
|
|
1312
|
+
const refs = [];
|
|
1313
|
+
for (const pkg of packages) {
|
|
1314
|
+
if (pkg == null || typeof pkg !== 'object')
|
|
1315
|
+
continue;
|
|
1316
|
+
const productId = pkg['product_id'];
|
|
1317
|
+
if (typeof productId === 'string' && productId.length > 0) {
|
|
1318
|
+
refs.push({ kind: 'product', id: productId });
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
if (refs.length === 0)
|
|
1322
|
+
return;
|
|
1323
|
+
let entries;
|
|
1324
|
+
try {
|
|
1325
|
+
entries = await store.bulkGetEntries(accountId, refs);
|
|
1326
|
+
}
|
|
1327
|
+
catch (err) {
|
|
1328
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1329
|
+
logger.warn(`[adcp/decisioning] auto-hydrate bulkGet failed: ${msg}`);
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
for (const pkg of packages) {
|
|
1333
|
+
if (pkg == null || typeof pkg !== 'object')
|
|
1334
|
+
continue;
|
|
1335
|
+
const productId = pkg['product_id'];
|
|
1336
|
+
if (typeof productId !== 'string')
|
|
1337
|
+
continue;
|
|
1338
|
+
const entry = entries.get(`product:${productId}`);
|
|
1339
|
+
if (!entry?.resource || typeof entry.resource !== 'object')
|
|
1340
|
+
continue;
|
|
1341
|
+
const hydrated = { ...entry.resource };
|
|
1342
|
+
if (entry.value !== null && entry.value !== undefined) {
|
|
1343
|
+
hydrated['ctx_metadata'] = entry.value;
|
|
1344
|
+
}
|
|
1345
|
+
Object.defineProperty(hydrated, '__adcp_hydrated__', {
|
|
1346
|
+
value: true,
|
|
1347
|
+
enumerable: false,
|
|
1348
|
+
writable: false,
|
|
1349
|
+
configurable: false,
|
|
1350
|
+
});
|
|
1351
|
+
// Non-enumerable: see hydrateSingleResource for rationale (no leak via
|
|
1352
|
+
// JSON.stringify / spread / Object.entries; direct access works).
|
|
1353
|
+
Object.defineProperty(pkg, 'product', {
|
|
1354
|
+
value: hydrated,
|
|
1355
|
+
enumerable: false,
|
|
1356
|
+
writable: true,
|
|
1357
|
+
configurable: true,
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Auto-hydrate one resource referenced by id at the top level of a request.
|
|
1363
|
+
*
|
|
1364
|
+
* Generalization of {@link hydratePackagesWithProducts} for verbs whose
|
|
1365
|
+
* primary resource lives directly on the request body (`update_media_buy`,
|
|
1366
|
+
* `provide_performance_feedback`, `activate_signal`, `acquire_rights`).
|
|
1367
|
+
* Walks the store for `(kind, id)`, attaches `target[attachField] =
|
|
1368
|
+
* { ...resource, ctx_metadata }` when the entry has a wire resource.
|
|
1369
|
+
*
|
|
1370
|
+
* ## Error contract on missing references
|
|
1371
|
+
*
|
|
1372
|
+
* **Misses are silent. The handler runs anyway with `target[attachField]`
|
|
1373
|
+
* undefined.** This is deliberate — the framework cache is a *hint*, not
|
|
1374
|
+
* the source of truth. A miss can mean any of:
|
|
1375
|
+
*
|
|
1376
|
+
* 1. The buyer never called the discovery verb in this session (cold
|
|
1377
|
+
* start, fresh tenant). Hydration is purely additive context; the
|
|
1378
|
+
* publisher's own DB is authoritative for whether the id exists.
|
|
1379
|
+
* 2. The cache evicted (TTL, LRU). Same: publisher's DB stays the
|
|
1380
|
+
* source of truth.
|
|
1381
|
+
* 3. The buyer truly referenced an unknown id. The publisher SHOULD
|
|
1382
|
+
* reject this — see the handler-side guard pattern below.
|
|
1383
|
+
*
|
|
1384
|
+
* Adopters who want strict existence checks (option 1: framework throws
|
|
1385
|
+
* `PRODUCT_NOT_FOUND` / `MEDIA_BUY_NOT_FOUND` and the handler never runs)
|
|
1386
|
+
* implement that check inside the handler:
|
|
1387
|
+
*
|
|
1388
|
+
* ```ts
|
|
1389
|
+
* updateMediaBuy: async (id, patch, ctx) => {
|
|
1390
|
+
* // Hydration miss + DB miss = unknown to this seller.
|
|
1391
|
+
* if (!patch.media_buy && !(await db.findMediaBuy(id))) {
|
|
1392
|
+
* throw new MediaBuyNotFoundError({ message: `media_buy ${id} not found` });
|
|
1393
|
+
* }
|
|
1394
|
+
* // ...
|
|
1395
|
+
* }
|
|
1396
|
+
* ```
|
|
1397
|
+
*
|
|
1398
|
+
* The framework cannot distinguish (1)/(2) from (3) without consulting the
|
|
1399
|
+
* publisher's DB, which is exactly what the handler does. Erroring at the
|
|
1400
|
+
* framework layer would force every adopter to manage cache warmth or
|
|
1401
|
+
* pre-load every media_buy into the cache before serving traffic — wrong
|
|
1402
|
+
* default for a hint-based cache.
|
|
1403
|
+
*
|
|
1404
|
+
* ## Field semantics on the hydrated value
|
|
1405
|
+
*
|
|
1406
|
+
* The attached field is **non-enumerable** so accidental serialization
|
|
1407
|
+
* (`JSON.stringify(req)`, spread `{...req}`, `Object.entries(req)`)
|
|
1408
|
+
* doesn't leak the publisher's `ctx_metadata` blob into request-side audit
|
|
1409
|
+
* sinks. Direct property access (`req.media_buy.ctx_metadata`) still
|
|
1410
|
+
* works; the field is invisible only to enumeration-based serializers.
|
|
1411
|
+
*
|
|
1412
|
+
* Hydrated fields carry a `__adcp_hydrated__: true` non-enumerable marker
|
|
1413
|
+
* so handler authors and middleware can disambiguate "publisher passed it"
|
|
1414
|
+
* from "framework attached it" — the field is **advisory context only**;
|
|
1415
|
+
* the wire contract is defined by the spec request fields, not by what
|
|
1416
|
+
* the SDK happens to attach.
|
|
1417
|
+
*
|
|
1418
|
+
* Store-fetch failures (Postgres unavailable, etc.) are logged + swallowed.
|
|
1419
|
+
* Hydration must NEVER break a successful dispatch — same posture as a
|
|
1420
|
+
* cache miss.
|
|
1421
|
+
*/
|
|
1422
|
+
async function hydrateSingleResource(store, accountId, kind, id, attachField, target, logger) {
|
|
1423
|
+
if (!store || !accountId || !id || target == null || typeof target !== 'object')
|
|
1424
|
+
return;
|
|
1425
|
+
let entry;
|
|
1426
|
+
try {
|
|
1427
|
+
entry = await store.getEntry(accountId, kind, id);
|
|
1428
|
+
}
|
|
1429
|
+
catch (err) {
|
|
1430
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1431
|
+
logger.warn(`[adcp/decisioning] auto-hydrate ${kind}:${id} failed: ${msg}`);
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
if (!entry?.resource || typeof entry.resource !== 'object')
|
|
1435
|
+
return;
|
|
1436
|
+
const hydrated = { ...entry.resource };
|
|
1437
|
+
if (entry.value !== null && entry.value !== undefined) {
|
|
1438
|
+
hydrated['ctx_metadata'] = entry.value;
|
|
1439
|
+
}
|
|
1440
|
+
Object.defineProperty(hydrated, '__adcp_hydrated__', {
|
|
1441
|
+
value: true,
|
|
1442
|
+
enumerable: false,
|
|
1443
|
+
writable: false,
|
|
1444
|
+
configurable: false,
|
|
1445
|
+
});
|
|
1446
|
+
// Attach as non-enumerable so JSON.stringify(req), spread {...req}, and
|
|
1447
|
+
// Object.entries(req) do NOT carry the publisher's ctx_metadata blob into
|
|
1448
|
+
// log lines, audit sinks, or replay payloads. Direct access (req.foo)
|
|
1449
|
+
// still works.
|
|
1450
|
+
Object.defineProperty(target, attachField, {
|
|
1451
|
+
value: hydrated,
|
|
1452
|
+
enumerable: false,
|
|
1453
|
+
writable: true,
|
|
1454
|
+
configurable: true,
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Extract the buyer's push-notification webhook config from a request body
|
|
1459
|
+
* and validate the URL + token against SSRF / replay primitives.
|
|
1460
|
+
*
|
|
1461
|
+
* AdCP wire requests for HITL tools carry `push_notification_config: { url,
|
|
1462
|
+
* token? }`. The buyer-supplied URL is attacker-controllable — without
|
|
1463
|
+
* validation, a buyer with `create_media_buy` access can force the agent
|
|
1464
|
+
* process to POST signed payloads to internal admin endpoints, AWS metadata
|
|
1465
|
+
* (`http://169.254.169.254/`), or RFC 1918 private ranges. Validation
|
|
1466
|
+
* gates here:
|
|
1467
|
+
*
|
|
1468
|
+
* 1. **Scheme**: `https://` only in production; `http://` allowed when
|
|
1469
|
+
* `NODE_ENV` ∈ {test, development} OR the operator opts in via
|
|
1470
|
+
* `ADCP_DECISIONING_ALLOW_HTTP_WEBHOOKS=1`. Same allowlist pattern as
|
|
1471
|
+
* the in-memory task registry.
|
|
1472
|
+
* 2. **Host**: rejects IP-literals targeting RFC 1918 private ranges
|
|
1473
|
+
* (10/8, 172.16/12, 192.168/16), link-local (169.254/16, fe80::/10),
|
|
1474
|
+
* loopback (127/8, ::1), CGNAT (100.64/10), and unspecified addresses
|
|
1475
|
+
* (0.0.0.0, ::). Rejects bare hostnames `localhost` / `0`.
|
|
1476
|
+
* 3. **Token shape**: rejects tokens longer than 255 chars or containing
|
|
1477
|
+
* control characters. Tokens past 255 chars combined with a malicious
|
|
1478
|
+
* URL would inflate webhook payload size; control characters break log
|
|
1479
|
+
* redaction.
|
|
1480
|
+
*
|
|
1481
|
+
* Adopters who need stricter rules (host allowlist) can re-validate
|
|
1482
|
+
* inside their `taskWebhookEmitter` impl. Adopters needing to relax for
|
|
1483
|
+
* legitimate internal-network test setups use the env-var ack.
|
|
1484
|
+
*
|
|
1485
|
+
* **DNS rebinding caveat.** This validator only inspects the literal
|
|
1486
|
+
* hostname/IP in the URL. A buyer registers `https://rebind.attacker.com/`
|
|
1487
|
+
* whose A-record returns `8.8.8.8` at validate time and `127.0.0.1` /
|
|
1488
|
+
* `169.254.169.254` at fetch time bypasses this layer. The framework's
|
|
1489
|
+
* own `serve({ webhooks })`-wired emitter (RFC 9421-signed delivery via
|
|
1490
|
+
* `WebhookManager`) re-resolves the host at fetch time and DOES NOT
|
|
1491
|
+
* pin-and-bind to the validate-time IP. Adopters wiring a custom
|
|
1492
|
+
* `taskWebhookEmitter` SHOULD pin the resolved IP at validate time and
|
|
1493
|
+
* connect to that specific IP, OR run all webhook delivery through a
|
|
1494
|
+
* forward proxy with an egress allowlist. Tracking issue:
|
|
1495
|
+
* `adcp-client#TBD` for framework-side pin-and-bind.
|
|
1496
|
+
*
|
|
1497
|
+
* On rejection, throws `AdcpError('INVALID_REQUEST', { field })` so the
|
|
1498
|
+
* buyer sees the bad config at the request boundary. Previous silent-skip
|
|
1499
|
+
* posture lost buyer visibility.
|
|
1500
|
+
*/
|
|
1501
|
+
function extractPushConfig(params, _logger, opts = {}) {
|
|
1502
|
+
if (!params || typeof params !== 'object')
|
|
1503
|
+
return {};
|
|
1504
|
+
const cfg = params.push_notification_config;
|
|
1505
|
+
if (!cfg || typeof cfg !== 'object')
|
|
1506
|
+
return {};
|
|
1507
|
+
const rawUrl = cfg.url;
|
|
1508
|
+
const rawToken = cfg.token;
|
|
1509
|
+
let url;
|
|
1510
|
+
if (typeof rawUrl === 'string') {
|
|
1511
|
+
const validation = validatePushNotificationUrl(rawUrl, { allowPrivate: opts.allowPrivateWebhookUrls === true });
|
|
1512
|
+
if (!validation.ok) {
|
|
1513
|
+
// Fail fast: buyers thought they wired push and never saw it under
|
|
1514
|
+
// the previous silent-skip posture. Rejecting upfront with
|
|
1515
|
+
// `INVALID_REQUEST` and `field: 'push_notification_config.url'`
|
|
1516
|
+
// surfaces the problem at the request boundary so buyers can fix
|
|
1517
|
+
// their config before relying on webhooks. Buyers can still poll
|
|
1518
|
+
// via `tasks_get` if they need a fallback path.
|
|
1519
|
+
throw new async_outcome_1.AdcpError('INVALID_REQUEST', {
|
|
1520
|
+
message: `push_notification_config.url rejected: ${validation.reason}`,
|
|
1521
|
+
field: 'push_notification_config.url',
|
|
1522
|
+
recovery: 'terminal',
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
url = rawUrl;
|
|
1526
|
+
}
|
|
1527
|
+
let token;
|
|
1528
|
+
if (typeof rawToken === 'string') {
|
|
1529
|
+
const validation = validatePushNotificationToken(rawToken);
|
|
1530
|
+
if (!validation.ok) {
|
|
1531
|
+
throw new async_outcome_1.AdcpError('INVALID_REQUEST', {
|
|
1532
|
+
message: `push_notification_config.token rejected: ${validation.reason}`,
|
|
1533
|
+
field: 'push_notification_config.token',
|
|
1534
|
+
recovery: 'terminal',
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
token = rawToken;
|
|
1538
|
+
}
|
|
1539
|
+
return {
|
|
1540
|
+
...(url !== undefined && { url }),
|
|
1541
|
+
...(token !== undefined && { token }),
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
function validatePushNotificationUrl(rawUrl, opts = {}) {
|
|
1545
|
+
let parsed;
|
|
1546
|
+
try {
|
|
1547
|
+
parsed = new URL(rawUrl);
|
|
1548
|
+
}
|
|
1549
|
+
catch {
|
|
1550
|
+
return { ok: false, reason: 'malformed URL' };
|
|
1551
|
+
}
|
|
1552
|
+
const allowHttp = process.env.NODE_ENV === 'test' ||
|
|
1553
|
+
process.env.NODE_ENV === 'development' ||
|
|
1554
|
+
process.env.ADCP_DECISIONING_ALLOW_HTTP_WEBHOOKS === '1';
|
|
1555
|
+
if (parsed.protocol === 'http:' && !allowHttp) {
|
|
1556
|
+
return {
|
|
1557
|
+
ok: false,
|
|
1558
|
+
reason: 'http:// scheme not allowed (use https:// or set ADCP_DECISIONING_ALLOW_HTTP_WEBHOOKS=1)',
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
1562
|
+
return { ok: false, reason: `unsupported scheme "${parsed.protocol}" (only http: / https:)` };
|
|
1563
|
+
}
|
|
1564
|
+
// Adopter-set flag bypasses ONLY the private-IP / loopback rejection.
|
|
1565
|
+
// Malformed-URL and scheme checks above always fire — the relaxation
|
|
1566
|
+
// is scoped to "let sandbox webhook receivers bind to loopback" not
|
|
1567
|
+
// "trust everything." See CreateAdcpServerFromPlatformOptions.
|
|
1568
|
+
if (opts.allowPrivate === true) {
|
|
1569
|
+
return { ok: true };
|
|
1570
|
+
}
|
|
1571
|
+
// Node's `URL.hostname` returns IPv6 literals WITH brackets — so
|
|
1572
|
+
// `https://[::1]/` yields `[::1]`. Strip them so our IPv6 checks below
|
|
1573
|
+
// can match the unbracketed form. (`URL.host` includes the port, which
|
|
1574
|
+
// we don't want here.)
|
|
1575
|
+
let host = parsed.hostname.toLowerCase();
|
|
1576
|
+
if (host.startsWith('[') && host.endsWith(']')) {
|
|
1577
|
+
host = host.slice(1, -1);
|
|
1578
|
+
}
|
|
1579
|
+
if (host === '' || host === 'localhost' || host === '0') {
|
|
1580
|
+
return { ok: false, reason: `host "${host}" rejected (loopback / unspecified)` };
|
|
1581
|
+
}
|
|
1582
|
+
// Note on IPv4 alternate forms (integer `2130706433`, hex `0x7f000001`,
|
|
1583
|
+
// octal `0177.0.0.1`): Node's WHATWG URL parser canonicalizes all of
|
|
1584
|
+
// these to dotted-decimal before we see `parsed.hostname`. So
|
|
1585
|
+
// `https://2130706433/` arrives here with host `127.0.0.1` and falls
|
|
1586
|
+
// through to the dotted-decimal range check below. Defense-in-depth
|
|
1587
|
+
// alternate-form regex rejectors are not needed at this layer.
|
|
1588
|
+
// IPv4 dotted-decimal check
|
|
1589
|
+
const ipv4Match = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
1590
|
+
if (ipv4Match) {
|
|
1591
|
+
const a = Number(ipv4Match[1]);
|
|
1592
|
+
const b = Number(ipv4Match[2]);
|
|
1593
|
+
if (a === 10)
|
|
1594
|
+
return { ok: false, reason: 'RFC 1918 private range 10/8 rejected' };
|
|
1595
|
+
if (a === 127)
|
|
1596
|
+
return { ok: false, reason: 'loopback range 127/8 rejected' };
|
|
1597
|
+
if (a === 0)
|
|
1598
|
+
return { ok: false, reason: 'unspecified range 0/8 rejected' };
|
|
1599
|
+
if (a === 169 && b === 254)
|
|
1600
|
+
return { ok: false, reason: 'link-local 169.254/16 rejected (cloud metadata)' };
|
|
1601
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
1602
|
+
return { ok: false, reason: 'RFC 1918 private range 172.16/12 rejected' };
|
|
1603
|
+
if (a === 192 && b === 168)
|
|
1604
|
+
return { ok: false, reason: 'RFC 1918 private range 192.168/16 rejected' };
|
|
1605
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
1606
|
+
return { ok: false, reason: 'CGNAT range 100.64/10 rejected' };
|
|
1607
|
+
if (a >= 224)
|
|
1608
|
+
return { ok: false, reason: 'multicast / reserved range rejected' };
|
|
1609
|
+
}
|
|
1610
|
+
// IPv6 literal check — Node strips brackets so we match unbracketed forms.
|
|
1611
|
+
// A hostname containing `:` is an IPv6 literal (DNS hostnames disallow `:`).
|
|
1612
|
+
if (host.includes(':')) {
|
|
1613
|
+
// Loopback + unspecified — exact match
|
|
1614
|
+
if (host === '::1')
|
|
1615
|
+
return { ok: false, reason: 'IPv6 loopback ::1 rejected' };
|
|
1616
|
+
if (host === '::')
|
|
1617
|
+
return { ok: false, reason: 'IPv6 unspecified :: rejected' };
|
|
1618
|
+
// Link-local fe80::/10 — match strict prefix `fe80:` (not just `fe80`)
|
|
1619
|
+
if (host.startsWith('fe80:')) {
|
|
1620
|
+
return { ok: false, reason: 'IPv6 link-local fe80::/10 rejected' };
|
|
1621
|
+
}
|
|
1622
|
+
// Unique-local fc00::/7 — match strict prefix `fc` or `fd` followed
|
|
1623
|
+
// by exactly one more hex char (so fc00:, fcab:, fd00:, fdef:) and
|
|
1624
|
+
// then `:`. Old check `host.startsWith('fc')` would match the
|
|
1625
|
+
// hostname-not-IP `fc-cdn.example.com`; since IPv6 hostnames always
|
|
1626
|
+
// contain `:`, restrict to literals where char[2] is hex + `:`.
|
|
1627
|
+
if (/^f[cd][0-9a-f]{2}:/.test(host)) {
|
|
1628
|
+
return { ok: false, reason: 'IPv6 unique-local fc00::/7 rejected' };
|
|
1629
|
+
}
|
|
1630
|
+
// IPv4-mapped IPv6 — `::ffff:127.0.0.1` or `::ffff:7f00:0001`. Either
|
|
1631
|
+
// form: extract the embedded IPv4 (if dotted) or the last 32 bits and
|
|
1632
|
+
// re-run the IPv4 range checks. Simpler: reject any `::ffff:` prefix
|
|
1633
|
+
// pointing to a private IPv4 by recursive validation.
|
|
1634
|
+
const v4MappedMatch = host.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
1635
|
+
if (v4MappedMatch) {
|
|
1636
|
+
const inner = v4MappedMatch[1];
|
|
1637
|
+
const innerCheck = validatePushNotificationUrl(`http://${inner}/`);
|
|
1638
|
+
if (!innerCheck.ok) {
|
|
1639
|
+
return { ok: false, reason: `IPv4-mapped IPv6 ${host}: ${innerCheck.reason}` };
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
// Hex IPv4-mapped form (`::ffff:7f00:0001`): match the 32-bit
|
|
1643
|
+
// suffix and convert to dotted form for re-check.
|
|
1644
|
+
const v4MappedHexMatch = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
1645
|
+
if (v4MappedHexMatch) {
|
|
1646
|
+
const high = parseInt(v4MappedHexMatch[1], 16);
|
|
1647
|
+
const low = parseInt(v4MappedHexMatch[2], 16);
|
|
1648
|
+
const dotted = `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
|
|
1649
|
+
const innerCheck = validatePushNotificationUrl(`http://${dotted}/`);
|
|
1650
|
+
if (!innerCheck.ok) {
|
|
1651
|
+
return { ok: false, reason: `IPv4-mapped IPv6 ${host}: ${innerCheck.reason}` };
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return { ok: true };
|
|
1656
|
+
}
|
|
1657
|
+
const TOKEN_MAX_LENGTH = 255;
|
|
1658
|
+
const TOKEN_CONTROL_CHAR_RE = /[\x00-\x1f\x7f]/;
|
|
1659
|
+
function validatePushNotificationToken(token) {
|
|
1660
|
+
if (token.length === 0) {
|
|
1661
|
+
return { ok: false, reason: 'token is empty' };
|
|
1662
|
+
}
|
|
1663
|
+
if (token.length > TOKEN_MAX_LENGTH) {
|
|
1664
|
+
return { ok: false, reason: `token longer than ${TOKEN_MAX_LENGTH} chars` };
|
|
1665
|
+
}
|
|
1666
|
+
if (TOKEN_CONTROL_CHAR_RE.test(token)) {
|
|
1667
|
+
return { ok: false, reason: 'token contains control characters' };
|
|
1668
|
+
}
|
|
1669
|
+
return { ok: true };
|
|
1670
|
+
}
|
|
1671
|
+
function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observability, logger, pushOpts, ctxFor, ctxMetadataStore) {
|
|
1672
|
+
const sales = platform.sales;
|
|
1673
|
+
if (!sales)
|
|
1674
|
+
return undefined;
|
|
1675
|
+
return {
|
|
1676
|
+
getProducts: async (params, ctx) => {
|
|
1677
|
+
const reqCtx = ctxFor(ctx);
|
|
1678
|
+
return projectSync(async () => {
|
|
1679
|
+
const result = await sales.getProducts(params, reqCtx);
|
|
1680
|
+
// Auto-store products: persist each Product's wire shape +
|
|
1681
|
+
// ctx_metadata so subsequent createMediaBuy / updateMediaBuy
|
|
1682
|
+
// calls referencing product_id can hydrate the full Product
|
|
1683
|
+
// automatically (publisher sees `req.packages[i].product`).
|
|
1684
|
+
await autoStoreResources(ctxMetadataStore, reqCtx.account?.id, 'product', result?.products, 'product_id', logger);
|
|
1685
|
+
return result;
|
|
1686
|
+
}, r => r);
|
|
1687
|
+
},
|
|
1688
|
+
createMediaBuy: async (params, ctx) => {
|
|
1689
|
+
const reqCtx = ctxFor(ctx);
|
|
1690
|
+
// Auto-hydrate: walk `params.packages`, attach the full Product object
|
|
1691
|
+
// (including `ctx_metadata`) at `pkg.product`. Publisher reads
|
|
1692
|
+
// `pkg.product.format_ids`, `pkg.product.ctx_metadata?.gam?.ad_unit_ids`
|
|
1693
|
+
// directly — no separate lookup, no boilerplate.
|
|
1694
|
+
await hydratePackagesWithProducts(ctxMetadataStore, reqCtx.account?.id, params?.packages, logger);
|
|
1695
|
+
return projectSync(async () => {
|
|
1696
|
+
const push = extractPushConfig(params, logger, { allowPrivateWebhookUrls: pushOpts.allowPrivateWebhookUrls });
|
|
1697
|
+
const result = await sales.createMediaBuy(params, reqCtx);
|
|
1698
|
+
return routeIfHandoff(taskRegistry, {
|
|
1699
|
+
tool: 'create_media_buy',
|
|
1700
|
+
accountId: reqCtx.account.id,
|
|
1701
|
+
pushNotificationUrl: push.url,
|
|
1702
|
+
pushNotificationToken: push.token,
|
|
1703
|
+
emitWebhook: taskWebhookEmit ?? ctx.emitWebhook,
|
|
1704
|
+
autoEmitCompletion: pushOpts.autoEmitCompletionWebhooks,
|
|
1705
|
+
observability,
|
|
1706
|
+
logger,
|
|
1707
|
+
}, result, r => r // identity projection for createMediaBuy
|
|
1708
|
+
);
|
|
1709
|
+
}, r => r);
|
|
1710
|
+
},
|
|
1711
|
+
updateMediaBuy: async (params, ctx) => {
|
|
1712
|
+
const reqCtx = ctxFor(ctx);
|
|
1713
|
+
// `media_buy_id` is required on the wire schema, but `validation: 'off'`
|
|
1714
|
+
// mode skips the schema parse — guard at the seam so platform code can
|
|
1715
|
+
// trust the value rather than re-checking. Also catches buyers calling
|
|
1716
|
+
// with the param missing under an off-spec server config.
|
|
1717
|
+
const { media_buy_id } = params;
|
|
1718
|
+
if (!media_buy_id) {
|
|
1719
|
+
return (0, errors_1.adcpError)('INVALID_REQUEST', {
|
|
1720
|
+
message: 'update_media_buy requires media_buy_id',
|
|
1721
|
+
field: 'media_buy_id',
|
|
1722
|
+
recovery: 'correctable',
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
// Auto-hydrate: attach the full MediaBuy (wire shape + ctx_metadata)
|
|
1726
|
+
// at `req.media_buy`. Publisher reads `req.media_buy.ctx_metadata?.gam`
|
|
1727
|
+
// directly — no separate lookup. Misses are silent; publisher falls
|
|
1728
|
+
// back to its own DB.
|
|
1729
|
+
await hydrateSingleResource(ctxMetadataStore, reqCtx.account?.id, 'media_buy', media_buy_id, 'media_buy', params, logger);
|
|
1730
|
+
return projectSync(async () => {
|
|
1731
|
+
const push = extractPushConfig(params, logger, {
|
|
1732
|
+
allowPrivateWebhookUrls: pushOpts.allowPrivateWebhookUrls,
|
|
1733
|
+
});
|
|
1734
|
+
const result = await sales.updateMediaBuy(media_buy_id, params, reqCtx);
|
|
1735
|
+
// F12 sync auto-emit. updateMediaBuy is sync-only on the
|
|
1736
|
+
// platform interface (no TaskHandoff arm — spec response
|
|
1737
|
+
// doesn't include Submitted), so we don't route through
|
|
1738
|
+
// routeIfHandoff. Fire-and-forget to keep slowloris webhook
|
|
1739
|
+
// receivers from blocking the sync response.
|
|
1740
|
+
if (pushOpts.autoEmitCompletionWebhooks && push.url) {
|
|
1741
|
+
const emitOpts = {
|
|
1742
|
+
tool: 'update_media_buy',
|
|
1743
|
+
accountId: reqCtx.account.id,
|
|
1744
|
+
pushNotificationUrl: push.url,
|
|
1745
|
+
...(push.token !== undefined && { pushNotificationToken: push.token }),
|
|
1746
|
+
emitWebhook: taskWebhookEmit ?? ctx.emitWebhook,
|
|
1747
|
+
...(observability && { observability }),
|
|
1748
|
+
logger,
|
|
1749
|
+
};
|
|
1750
|
+
void emitSyncCompletionWebhook(emitOpts, result).catch((err) => {
|
|
1751
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1752
|
+
logger.warn(`[adcp/decisioning] sync completion webhook background-error: ${msg}`);
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
return result;
|
|
1756
|
+
}, r => r);
|
|
1757
|
+
},
|
|
1758
|
+
syncCreatives: async (params, ctx) => {
|
|
1759
|
+
const reqCtx = ctxFor(ctx);
|
|
1760
|
+
const creatives = params.creatives ?? [];
|
|
1761
|
+
if (!sales.syncCreatives) {
|
|
1762
|
+
return (0, errors_1.adcpError)('UNSUPPORTED_FEATURE', {
|
|
1763
|
+
message: 'sync_creatives not supported by this sales platform',
|
|
1764
|
+
recovery: 'terminal',
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
return projectSync(async () => {
|
|
1768
|
+
const push = extractPushConfig(params, logger, { allowPrivateWebhookUrls: pushOpts.allowPrivateWebhookUrls });
|
|
1769
|
+
const result = await sales.syncCreatives(creatives, reqCtx);
|
|
1770
|
+
return routeIfHandoff(taskRegistry, {
|
|
1771
|
+
tool: 'sync_creatives',
|
|
1772
|
+
accountId: reqCtx.account.id,
|
|
1773
|
+
pushNotificationUrl: push.url,
|
|
1774
|
+
pushNotificationToken: push.token,
|
|
1775
|
+
emitWebhook: taskWebhookEmit ?? ctx.emitWebhook,
|
|
1776
|
+
autoEmitCompletion: pushOpts.autoEmitCompletionWebhooks,
|
|
1777
|
+
observability,
|
|
1778
|
+
logger,
|
|
1779
|
+
}, result, rows => ({ creatives: rows.map(normalizeRowErrors) }));
|
|
1780
|
+
}, r => r);
|
|
1781
|
+
},
|
|
1782
|
+
getMediaBuyDelivery: async (params, ctx) => {
|
|
1783
|
+
const reqCtx = ctxFor(ctx);
|
|
1784
|
+
return projectSync(() => sales.getMediaBuyDelivery(params, reqCtx), actuals => actuals);
|
|
1785
|
+
},
|
|
1786
|
+
// Optional methods — return UNSUPPORTED_FEATURE when the platform omits
|
|
1787
|
+
// them. Adopters that haven't migrated to the v6 platform interface for
|
|
1788
|
+
// these specific tools can still pass raw handlers via opts.mediaBuy
|
|
1789
|
+
// (merge seam) — the merge runs AFTER buildMediaBuyHandlers, so opts
|
|
1790
|
+
// handlers fill in for the methods this platform omits.
|
|
1791
|
+
// `getMediaBuys` is REQUIRED at the type level, but we keep a runtime
|
|
1792
|
+
// guard for the merge-seam migration path: legacy adopters wire it via
|
|
1793
|
+
// `opts.mediaBuy.getMediaBuys` rather than on the platform interface.
|
|
1794
|
+
// Once the migration completes (every adopter implements it natively),
|
|
1795
|
+
// this conditional spreads can collapse — but for now, omitting the
|
|
1796
|
+
// platform-derived handler when absent lets `mergeHandlers` pick up the
|
|
1797
|
+
// adopter's custom handler from `opts.mediaBuy` instead of throwing
|
|
1798
|
+
// `sales.getMediaBuys is not a function`.
|
|
1799
|
+
...(sales.getMediaBuys && {
|
|
1800
|
+
getMediaBuys: async (params, ctx) => {
|
|
1801
|
+
const reqCtx = ctxFor(ctx);
|
|
1802
|
+
return projectSync(async () => {
|
|
1803
|
+
const result = await sales.getMediaBuys(params, reqCtx);
|
|
1804
|
+
await autoStoreResources(ctxMetadataStore, reqCtx.account?.id, 'media_buy', result?.media_buys, 'media_buy_id', logger);
|
|
1805
|
+
return result;
|
|
1806
|
+
}, r => r);
|
|
1807
|
+
},
|
|
1808
|
+
}),
|
|
1809
|
+
...(sales.providePerformanceFeedback && {
|
|
1810
|
+
providePerformanceFeedback: async (params, ctx) => {
|
|
1811
|
+
const reqCtx = ctxFor(ctx);
|
|
1812
|
+
// Auto-hydrate `req.media_buy` from the prior createMediaBuy /
|
|
1813
|
+
// getMediaBuys store entry, plus `req.creative` when the buyer
|
|
1814
|
+
// scoped feedback to a specific creative. Both fields are optional
|
|
1815
|
+
// hydration targets — adopters who only care about the feedback
|
|
1816
|
+
// payload itself can ignore them.
|
|
1817
|
+
const accountId = reqCtx.account?.id;
|
|
1818
|
+
await hydrateSingleResource(ctxMetadataStore, accountId, 'media_buy', params.media_buy_id, 'media_buy', params, logger);
|
|
1819
|
+
const creativeId = params.creative_id;
|
|
1820
|
+
if (creativeId) {
|
|
1821
|
+
await hydrateSingleResource(ctxMetadataStore, accountId, 'creative', creativeId, 'creative', params, logger);
|
|
1822
|
+
}
|
|
1823
|
+
return projectSync(() => sales.providePerformanceFeedback(params, reqCtx), r => r);
|
|
1824
|
+
},
|
|
1825
|
+
}),
|
|
1826
|
+
...(sales.listCreativeFormats && {
|
|
1827
|
+
listCreativeFormats: async (params, ctx) => {
|
|
1828
|
+
const reqCtx = ctxFor(ctx);
|
|
1829
|
+
return projectSync(() => sales.listCreativeFormats(params, reqCtx), r => r);
|
|
1830
|
+
},
|
|
1831
|
+
}),
|
|
1832
|
+
...(sales.listCreatives && {
|
|
1833
|
+
listCreatives: async (params, ctx) => {
|
|
1834
|
+
const reqCtx = ctxFor(ctx);
|
|
1835
|
+
return projectSync(() => sales.listCreatives(params, reqCtx), r => r);
|
|
1836
|
+
},
|
|
1837
|
+
}),
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Project an adopter `buildCreative` return value into the wire response
|
|
1842
|
+
* shape. Handles the four legal adopter shapes per `BuildCreativeReturn`:
|
|
1843
|
+
*
|
|
1844
|
+
* - Already-shaped Single envelope (`creative_manifest` field present) →
|
|
1845
|
+
* passthrough. Adopter set `sandbox` / `expires_at` / `preview` themselves.
|
|
1846
|
+
* - Already-shaped Multi envelope (`creative_manifests` field present) →
|
|
1847
|
+
* passthrough. Same metadata-controlled case for multi-format requests.
|
|
1848
|
+
* - Bare array → wrap as `{ creative_manifests: <array> }` (multi, no metadata).
|
|
1849
|
+
* - Plain `CreativeManifest` → wrap as `{ creative_manifest: <obj> }`
|
|
1850
|
+
* (single, no metadata).
|
|
1851
|
+
*
|
|
1852
|
+
* The discriminator order matters: check shaped envelopes first so an
|
|
1853
|
+
* adopter that returned `{ creative_manifest, sandbox: true }` (a Single
|
|
1854
|
+
* envelope) doesn't get re-wrapped into
|
|
1855
|
+
* `{ creative_manifest: { creative_manifest, sandbox: true } }`. Same
|
|
1856
|
+
* concern applies symmetrically for Multi.
|
|
1857
|
+
*/
|
|
1858
|
+
function projectBuildCreativeReturn(ret) {
|
|
1859
|
+
if (ret != null && typeof ret === 'object' && !Array.isArray(ret)) {
|
|
1860
|
+
if ('creative_manifest' in ret)
|
|
1861
|
+
return ret;
|
|
1862
|
+
if ('creative_manifests' in ret)
|
|
1863
|
+
return ret;
|
|
1864
|
+
}
|
|
1865
|
+
if (Array.isArray(ret)) {
|
|
1866
|
+
return { creative_manifests: ret };
|
|
1867
|
+
}
|
|
1868
|
+
return { creative_manifest: ret };
|
|
1869
|
+
}
|
|
1870
|
+
function buildCreativeHandlers(platform, taskRegistry, taskWebhookEmit, observability, logger, pushOpts, ctxFor) {
|
|
1871
|
+
const creative = platform.creative;
|
|
1872
|
+
if (!creative)
|
|
1873
|
+
return undefined;
|
|
1874
|
+
return {
|
|
1875
|
+
buildCreative: async (params, ctx) => {
|
|
1876
|
+
const reqCtx = ctxFor(ctx);
|
|
1877
|
+
return projectSync(() => creative.buildCreative(params, reqCtx), ret => projectBuildCreativeReturn(ret));
|
|
1878
|
+
},
|
|
1879
|
+
previewCreative: async (params, ctx) => {
|
|
1880
|
+
if (!('previewCreative' in creative)) {
|
|
1881
|
+
return (0, errors_1.adcpError)('UNSUPPORTED_FEATURE', {
|
|
1882
|
+
message: 'preview_creative not supported by this platform',
|
|
1883
|
+
recovery: 'terminal',
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
const reqCtx = ctxFor(ctx);
|
|
1887
|
+
return projectSync(() => creative.previewCreative(params, reqCtx), preview => preview);
|
|
1888
|
+
},
|
|
1889
|
+
syncCreatives: async (params, ctx) => {
|
|
1890
|
+
const reqCtx = ctxFor(ctx);
|
|
1891
|
+
const creatives = params.creatives ?? [];
|
|
1892
|
+
if (!creative.syncCreatives) {
|
|
1893
|
+
return (0, errors_1.adcpError)('UNSUPPORTED_FEATURE', {
|
|
1894
|
+
message: 'sync_creatives not supported by this creative platform',
|
|
1895
|
+
recovery: 'terminal',
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
return projectSync(async () => {
|
|
1899
|
+
const push = extractPushConfig(params, logger, { allowPrivateWebhookUrls: pushOpts.allowPrivateWebhookUrls });
|
|
1900
|
+
const result = await creative.syncCreatives(creatives, reqCtx);
|
|
1901
|
+
return routeIfHandoff(taskRegistry, {
|
|
1902
|
+
tool: 'sync_creatives',
|
|
1903
|
+
accountId: reqCtx.account.id,
|
|
1904
|
+
pushNotificationUrl: push.url,
|
|
1905
|
+
pushNotificationToken: push.token,
|
|
1906
|
+
emitWebhook: taskWebhookEmit ?? ctx.emitWebhook,
|
|
1907
|
+
autoEmitCompletion: pushOpts.autoEmitCompletionWebhooks,
|
|
1908
|
+
observability,
|
|
1909
|
+
logger,
|
|
1910
|
+
}, result, rows => ({ creatives: rows.map(normalizeRowErrors) }));
|
|
1911
|
+
}, r => r);
|
|
1912
|
+
},
|
|
1913
|
+
// Ad-server-specialism methods. Only the CreativeAdServerPlatform variant
|
|
1914
|
+
// implements these; framework returns UNSUPPORTED_FEATURE for the other
|
|
1915
|
+
// archetypes (template, generative).
|
|
1916
|
+
listCreatives: async (params, ctx) => {
|
|
1917
|
+
if (!('listCreatives' in creative)) {
|
|
1918
|
+
return (0, errors_1.adcpError)('UNSUPPORTED_FEATURE', {
|
|
1919
|
+
message: 'list_creatives not supported by this platform',
|
|
1920
|
+
recovery: 'terminal',
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
const reqCtx = ctxFor(ctx);
|
|
1924
|
+
return projectSync(() => creative.listCreatives(params, reqCtx), r => r);
|
|
1925
|
+
},
|
|
1926
|
+
getCreativeDelivery: async (params, ctx) => {
|
|
1927
|
+
if (!('getCreativeDelivery' in creative)) {
|
|
1928
|
+
return (0, errors_1.adcpError)('UNSUPPORTED_FEATURE', {
|
|
1929
|
+
message: 'get_creative_delivery not supported by this platform',
|
|
1930
|
+
recovery: 'terminal',
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
const reqCtx = ctxFor(ctx);
|
|
1934
|
+
return projectSync(() => creative.getCreativeDelivery(params, reqCtx), r => r);
|
|
1935
|
+
},
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
function buildEventTrackingHandlers(platform, ctxFor) {
|
|
1939
|
+
const audiences = platform.audiences;
|
|
1940
|
+
const sales = platform.sales;
|
|
1941
|
+
// Retail-media adopters (sales-catalog-driven) implement sync_catalogs /
|
|
1942
|
+
// log_event / sync_event_sources on `SalesPlatform`. The wire spec routes
|
|
1943
|
+
// these through the `event-tracking` framework category so the handlers
|
|
1944
|
+
// land on `EventTrackingHandlers` regardless of which specialism owns
|
|
1945
|
+
// them on the platform side.
|
|
1946
|
+
if (!audiences && !sales)
|
|
1947
|
+
return undefined;
|
|
1948
|
+
const handlers = {};
|
|
1949
|
+
if (audiences) {
|
|
1950
|
+
handlers.syncAudiences = async (params, ctx) => {
|
|
1951
|
+
const reqCtx = ctxFor(ctx);
|
|
1952
|
+
const audienceList = (params.audiences ?? []);
|
|
1953
|
+
return projectSync(() => audiences.syncAudiences(audienceList, reqCtx), rows => ({ audiences: rows }));
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
if (sales?.syncCatalogs) {
|
|
1957
|
+
handlers.syncCatalogs = async (params, ctx) => {
|
|
1958
|
+
const reqCtx = ctxFor(ctx);
|
|
1959
|
+
return projectSync(() => sales.syncCatalogs(params, reqCtx), r => r);
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
if (sales?.logEvent) {
|
|
1963
|
+
handlers.logEvent = async (params, ctx) => {
|
|
1964
|
+
const reqCtx = ctxFor(ctx);
|
|
1965
|
+
return projectSync(() => sales.logEvent(params, reqCtx), r => r);
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
if (sales?.syncEventSources) {
|
|
1969
|
+
handlers.syncEventSources = async (params, ctx) => {
|
|
1970
|
+
const reqCtx = ctxFor(ctx);
|
|
1971
|
+
return projectSync(() => sales.syncEventSources(params, reqCtx), r => r);
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
return Object.keys(handlers).length > 0 ? handlers : undefined;
|
|
1975
|
+
}
|
|
1976
|
+
function buildSignalsHandlers(platform, ctxFor, ctxMetadataStore, logger) {
|
|
1977
|
+
const signals = platform.signals;
|
|
1978
|
+
if (!signals)
|
|
1979
|
+
return undefined;
|
|
1980
|
+
return {
|
|
1981
|
+
getSignals: async (params, ctx) => {
|
|
1982
|
+
const reqCtx = ctxFor(ctx);
|
|
1983
|
+
return projectSync(async () => {
|
|
1984
|
+
const result = await signals.getSignals(params, reqCtx);
|
|
1985
|
+
// Auto-store signals so subsequent activate_signal can hydrate
|
|
1986
|
+
// `req.signal` from the publisher's prior catalog entry.
|
|
1987
|
+
await autoStoreResources(ctxMetadataStore, reqCtx.account?.id, 'signal', result?.signals, 'signal_agent_segment_id', logger);
|
|
1988
|
+
return result;
|
|
1989
|
+
}, r => r);
|
|
1990
|
+
},
|
|
1991
|
+
activateSignal: async (params, ctx) => {
|
|
1992
|
+
const reqCtx = ctxFor(ctx);
|
|
1993
|
+
// Auto-hydrate `req.signal` from the prior getSignals store entry —
|
|
1994
|
+
// publisher reads pricing options, agent segment id, ctx_metadata
|
|
1995
|
+
// directly without the buyer round-tripping the full signal object.
|
|
1996
|
+
await hydrateSingleResource(ctxMetadataStore, reqCtx.account?.id, 'signal', params.signal_agent_segment_id, 'signal', params, logger);
|
|
1997
|
+
return projectSync(() => signals.activateSignal(params, reqCtx), r => r);
|
|
1998
|
+
},
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
function buildBrandRightsHandlers(platform, ctxFor, ctxMetadataStore, logger) {
|
|
2002
|
+
const br = platform.brandRights;
|
|
2003
|
+
if (!br)
|
|
2004
|
+
return undefined;
|
|
2005
|
+
return {
|
|
2006
|
+
getBrandIdentity: async (params, ctx) => {
|
|
2007
|
+
const reqCtx = ctxFor(ctx);
|
|
2008
|
+
return projectSync(() => br.getBrandIdentity(params, reqCtx), r => r);
|
|
2009
|
+
},
|
|
2010
|
+
getRights: async (params, ctx) => {
|
|
2011
|
+
const reqCtx = ctxFor(ctx);
|
|
2012
|
+
return projectSync(async () => {
|
|
2013
|
+
const result = await br.getRights(params, reqCtx);
|
|
2014
|
+
// Auto-store rights offerings so subsequent acquire_rights can
|
|
2015
|
+
// hydrate `req.rights` (pricing_options + ctx_metadata) without
|
|
2016
|
+
// a separate publisher lookup.
|
|
2017
|
+
await autoStoreResources(ctxMetadataStore, reqCtx.account?.id, 'rights_grant', result?.rights, 'rights_id', logger);
|
|
2018
|
+
return result;
|
|
2019
|
+
}, r => r);
|
|
2020
|
+
},
|
|
2021
|
+
// `acquire_rights` has 3 native wire-spec arms (Acquired / PendingApproval /
|
|
2022
|
+
// Rejected) handled by the platform directly. No framework task envelope —
|
|
2023
|
+
// adopters return the spec-defined arm. Async delivery for the
|
|
2024
|
+
// PendingApproval arm rides the buyer's `push_notification_config` webhook
|
|
2025
|
+
// (the spec doesn't define a polling tool for `acquire_rights`).
|
|
2026
|
+
acquireRights: async (params, ctx) => {
|
|
2027
|
+
const reqCtx = ctxFor(ctx);
|
|
2028
|
+
// Auto-hydrate `req.rights` from the prior getRights catalog entry.
|
|
2029
|
+
// Publisher reads selected pricing option + ctx_metadata directly.
|
|
2030
|
+
await hydrateSingleResource(ctxMetadataStore, reqCtx.account?.id, 'rights_grant', params.rights_id, 'rights', params, logger);
|
|
2031
|
+
return projectSync(() => br.acquireRights(params, reqCtx), r => r);
|
|
2032
|
+
},
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
function buildGovernanceHandlers(platform, ctxFor) {
|
|
2036
|
+
const cg = platform.campaignGovernance;
|
|
2037
|
+
const pl = platform.propertyLists;
|
|
2038
|
+
const cl = platform.collectionLists;
|
|
2039
|
+
const cs = platform.contentStandards;
|
|
2040
|
+
if (!cg && !pl && !cl && !cs)
|
|
2041
|
+
return undefined;
|
|
2042
|
+
const handlers = {};
|
|
2043
|
+
if (cg) {
|
|
2044
|
+
handlers.checkGovernance = async (params, ctx) => {
|
|
2045
|
+
const reqCtx = ctxFor(ctx);
|
|
2046
|
+
return projectSync(() => cg.checkGovernance(params, reqCtx), r => r);
|
|
2047
|
+
};
|
|
2048
|
+
handlers.syncPlans = async (params, ctx) => {
|
|
2049
|
+
const reqCtx = ctxFor(ctx);
|
|
2050
|
+
return projectSync(() => cg.syncPlans(params, reqCtx), r => r);
|
|
2051
|
+
};
|
|
2052
|
+
handlers.reportPlanOutcome = async (params, ctx) => {
|
|
2053
|
+
const reqCtx = ctxFor(ctx);
|
|
2054
|
+
return projectSync(() => cg.reportPlanOutcome(params, reqCtx), r => r);
|
|
2055
|
+
};
|
|
2056
|
+
handlers.getPlanAuditLogs = async (params, ctx) => {
|
|
2057
|
+
const reqCtx = ctxFor(ctx);
|
|
2058
|
+
return projectSync(() => cg.getPlanAuditLogs(params, reqCtx), r => r);
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
if (pl) {
|
|
2062
|
+
handlers.createPropertyList = async (params, ctx) => {
|
|
2063
|
+
const reqCtx = ctxFor(ctx);
|
|
2064
|
+
return projectSync(() => pl.createPropertyList(params, reqCtx), r => r);
|
|
2065
|
+
};
|
|
2066
|
+
handlers.updatePropertyList = async (params, ctx) => {
|
|
2067
|
+
const reqCtx = ctxFor(ctx);
|
|
2068
|
+
return projectSync(() => pl.updatePropertyList(params, reqCtx), r => r);
|
|
2069
|
+
};
|
|
2070
|
+
handlers.getPropertyList = async (params, ctx) => {
|
|
2071
|
+
const reqCtx = ctxFor(ctx);
|
|
2072
|
+
return projectSync(() => pl.getPropertyList(params, reqCtx), r => r);
|
|
2073
|
+
};
|
|
2074
|
+
handlers.listPropertyLists = async (params, ctx) => {
|
|
2075
|
+
const reqCtx = ctxFor(ctx);
|
|
2076
|
+
return projectSync(() => pl.listPropertyLists(params, reqCtx), r => r);
|
|
2077
|
+
};
|
|
2078
|
+
handlers.deletePropertyList = async (params, ctx) => {
|
|
2079
|
+
const reqCtx = ctxFor(ctx);
|
|
2080
|
+
return projectSync(() => pl.deletePropertyList(params, reqCtx), r => r);
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
if (cl) {
|
|
2084
|
+
handlers.createCollectionList = async (params, ctx) => {
|
|
2085
|
+
const reqCtx = ctxFor(ctx);
|
|
2086
|
+
return projectSync(() => cl.createCollectionList(params, reqCtx), r => r);
|
|
2087
|
+
};
|
|
2088
|
+
handlers.updateCollectionList = async (params, ctx) => {
|
|
2089
|
+
const reqCtx = ctxFor(ctx);
|
|
2090
|
+
return projectSync(() => cl.updateCollectionList(params, reqCtx), r => r);
|
|
2091
|
+
};
|
|
2092
|
+
handlers.getCollectionList = async (params, ctx) => {
|
|
2093
|
+
const reqCtx = ctxFor(ctx);
|
|
2094
|
+
return projectSync(() => cl.getCollectionList(params, reqCtx), r => r);
|
|
2095
|
+
};
|
|
2096
|
+
handlers.listCollectionLists = async (params, ctx) => {
|
|
2097
|
+
const reqCtx = ctxFor(ctx);
|
|
2098
|
+
return projectSync(() => cl.listCollectionLists(params, reqCtx), r => r);
|
|
2099
|
+
};
|
|
2100
|
+
handlers.deleteCollectionList = async (params, ctx) => {
|
|
2101
|
+
const reqCtx = ctxFor(ctx);
|
|
2102
|
+
return projectSync(() => cl.deleteCollectionList(params, reqCtx), r => r);
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
if (cs) {
|
|
2106
|
+
handlers.listContentStandards = async (params, ctx) => {
|
|
2107
|
+
const reqCtx = ctxFor(ctx);
|
|
2108
|
+
return projectSync(() => cs.listContentStandards(params, reqCtx), r => r);
|
|
2109
|
+
};
|
|
2110
|
+
handlers.getContentStandards = async (params, ctx) => {
|
|
2111
|
+
const reqCtx = ctxFor(ctx);
|
|
2112
|
+
return projectSync(() => cs.getContentStandards(params, reqCtx), r => r);
|
|
2113
|
+
};
|
|
2114
|
+
handlers.createContentStandards = async (params, ctx) => {
|
|
2115
|
+
const reqCtx = ctxFor(ctx);
|
|
2116
|
+
return projectSync(() => cs.createContentStandards(params, reqCtx), r => r);
|
|
2117
|
+
};
|
|
2118
|
+
handlers.updateContentStandards = async (params, ctx) => {
|
|
2119
|
+
const reqCtx = ctxFor(ctx);
|
|
2120
|
+
return projectSync(() => cs.updateContentStandards(params, reqCtx), r => r);
|
|
2121
|
+
};
|
|
2122
|
+
handlers.calibrateContent = async (params, ctx) => {
|
|
2123
|
+
const reqCtx = ctxFor(ctx);
|
|
2124
|
+
return projectSync(() => cs.calibrateContent(params, reqCtx), r => r);
|
|
2125
|
+
};
|
|
2126
|
+
handlers.validateContentDelivery = async (params, ctx) => {
|
|
2127
|
+
const reqCtx = ctxFor(ctx);
|
|
2128
|
+
return projectSync(() => cs.validateContentDelivery(params, reqCtx), r => r);
|
|
2129
|
+
};
|
|
2130
|
+
if (cs.getMediaBuyArtifacts) {
|
|
2131
|
+
handlers.getMediaBuyArtifacts = async (params, ctx) => {
|
|
2132
|
+
const reqCtx = ctxFor(ctx);
|
|
2133
|
+
return projectSync(() => cs.getMediaBuyArtifacts(params, reqCtx), r => r);
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
if (cs.getCreativeFeatures) {
|
|
2137
|
+
handlers.getCreativeFeatures = async (params, ctx) => {
|
|
2138
|
+
const reqCtx = ctxFor(ctx);
|
|
2139
|
+
return projectSync(() => cs.getCreativeFeatures(params, reqCtx), r => r);
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
return handlers;
|
|
2144
|
+
}
|
|
2145
|
+
function buildAccountHandlers(platform, ctxFor) {
|
|
2146
|
+
const accounts = platform.accounts;
|
|
2147
|
+
// Only emit framework-derived handlers for methods the platform actually
|
|
2148
|
+
// implements. Emitting an UNSUPPORTED_FEATURE stub for an undefined
|
|
2149
|
+
// method shadows adopter-supplied `opts.accounts.{syncAccounts,listAccounts}`
|
|
2150
|
+
// fillers under the merge seam (platform-derived wins per-key), so the
|
|
2151
|
+
// adopter's working handler silently never runs. This pattern matches the
|
|
2152
|
+
// gating already used for `reportUsage` / `getAccountFinancials` below.
|
|
2153
|
+
// Adopters who claim sync_accounts / list_accounts capability without
|
|
2154
|
+
// implementing `accounts.upsert` / `accounts.list` AND without supplying a
|
|
2155
|
+
// merge-seam override get framework's "tool not registered" path —
|
|
2156
|
+
// closer to the truth than a fabricated UNSUPPORTED_FEATURE envelope.
|
|
2157
|
+
const handlers = {};
|
|
2158
|
+
if (accounts.upsert) {
|
|
2159
|
+
handlers.syncAccounts = async (params, _ctx) => {
|
|
2160
|
+
const refs = (params.accounts ?? []);
|
|
2161
|
+
return projectSync(() => accounts.upsert(refs), rows => ({ accounts: rows }));
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
if (accounts.list) {
|
|
2165
|
+
handlers.listAccounts = async (params, _ctx) => {
|
|
2166
|
+
const filter = params;
|
|
2167
|
+
// Wrap in projectSync so adopter `throw new AdcpError('PERMISSION_DENIED', ...)`
|
|
2168
|
+
// from the list impl projects to the structured wire envelope rather
|
|
2169
|
+
// than falling through to the framework's `SERVICE_UNAVAILABLE` mapping.
|
|
2170
|
+
return projectSync(() => accounts.list(filter), page => ({
|
|
2171
|
+
accounts: page.items.map(account_1.toWireAccount),
|
|
2172
|
+
...(page.nextCursor != null && { next_cursor: page.nextCursor }),
|
|
2173
|
+
}));
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
if (accounts.reportUsage) {
|
|
2177
|
+
handlers.reportUsage = async (params, ctx) => {
|
|
2178
|
+
const resolveCtx = {
|
|
2179
|
+
...(ctx.authInfo !== undefined && { authInfo: ctx.authInfo }),
|
|
2180
|
+
toolName: 'report_usage',
|
|
2181
|
+
};
|
|
2182
|
+
return projectSync(() => accounts.reportUsage(params, resolveCtx), r => r);
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
if (accounts.getAccountFinancials) {
|
|
2186
|
+
handlers.getAccountFinancials = async (params, ctx) => {
|
|
2187
|
+
const resolveCtx = {
|
|
2188
|
+
...(ctx.authInfo !== undefined && { authInfo: ctx.authInfo }),
|
|
2189
|
+
toolName: 'get_account_financials',
|
|
2190
|
+
};
|
|
2191
|
+
return projectSync(() => accounts.getAccountFinancials(params, resolveCtx), r => r);
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
return handlers;
|
|
2195
|
+
}
|
|
2196
|
+
//# sourceMappingURL=from-platform.js.map
|