@adcp/sdk 5.25.1 → 6.0.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.
Files changed (288) hide show
  1. package/README.md +45 -7
  2. package/dist/lib/compliance-fixtures/index.d.ts +1 -1
  3. package/dist/lib/compliance-fixtures/index.js +1 -1
  4. package/dist/lib/core/AgentClient.d.ts.map +1 -1
  5. package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
  6. package/dist/lib/core/SingleAgentClient.js +15 -0
  7. package/dist/lib/core/SingleAgentClient.js.map +1 -1
  8. package/dist/lib/core/TaskExecutor.d.ts +7 -0
  9. package/dist/lib/core/TaskExecutor.d.ts.map +1 -1
  10. package/dist/lib/core/TaskExecutor.js +9 -2
  11. package/dist/lib/core/TaskExecutor.js.map +1 -1
  12. package/dist/lib/index.d.ts +1 -1
  13. package/dist/lib/index.d.ts.map +1 -1
  14. package/dist/lib/index.js +7 -8
  15. package/dist/lib/index.js.map +1 -1
  16. package/dist/lib/schemas/index.d.ts +1 -1
  17. package/dist/lib/schemas/index.js +1 -1
  18. package/dist/lib/server/create-adcp-server.d.ts +129 -11
  19. package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
  20. package/dist/lib/server/create-adcp-server.js +112 -2
  21. package/dist/lib/server/create-adcp-server.js.map +1 -1
  22. package/dist/lib/server/ctx-metadata/backends/memory.d.ts +27 -0
  23. package/dist/lib/server/ctx-metadata/backends/memory.d.ts.map +1 -0
  24. package/dist/lib/server/ctx-metadata/backends/memory.js +72 -0
  25. package/dist/lib/server/ctx-metadata/backends/memory.js.map +1 -0
  26. package/dist/lib/server/ctx-metadata/backends/pg.d.ts +62 -0
  27. package/dist/lib/server/ctx-metadata/backends/pg.d.ts.map +1 -0
  28. package/dist/lib/server/ctx-metadata/backends/pg.js +145 -0
  29. package/dist/lib/server/ctx-metadata/backends/pg.js.map +1 -0
  30. package/dist/lib/server/ctx-metadata/index.d.ts +15 -0
  31. package/dist/lib/server/ctx-metadata/index.d.ts.map +1 -0
  32. package/dist/lib/server/ctx-metadata/index.js +28 -0
  33. package/dist/lib/server/ctx-metadata/index.js.map +1 -0
  34. package/dist/lib/server/ctx-metadata/store.d.ts +177 -0
  35. package/dist/lib/server/ctx-metadata/store.d.ts.map +1 -0
  36. package/dist/lib/server/ctx-metadata/store.js +327 -0
  37. package/dist/lib/server/ctx-metadata/store.js.map +1 -0
  38. package/dist/lib/server/ctx-metadata/wire-shape.d.ts +55 -0
  39. package/dist/lib/server/ctx-metadata/wire-shape.d.ts.map +1 -0
  40. package/dist/lib/server/ctx-metadata/wire-shape.js +121 -0
  41. package/dist/lib/server/ctx-metadata/wire-shape.js.map +1 -0
  42. package/dist/lib/server/decisioning/account.d.ts +309 -0
  43. package/dist/lib/server/decisioning/account.d.ts.map +1 -0
  44. package/dist/lib/server/decisioning/account.js +102 -0
  45. package/dist/lib/server/decisioning/account.js.map +1 -0
  46. package/dist/lib/server/decisioning/admin-router.d.ts +75 -0
  47. package/dist/lib/server/decisioning/admin-router.d.ts.map +1 -0
  48. package/dist/lib/server/decisioning/admin-router.js +120 -0
  49. package/dist/lib/server/decisioning/admin-router.js.map +1 -0
  50. package/dist/lib/server/decisioning/assembly-helpers.d.ts +204 -0
  51. package/dist/lib/server/decisioning/assembly-helpers.d.ts.map +1 -0
  52. package/dist/lib/server/decisioning/assembly-helpers.js +173 -0
  53. package/dist/lib/server/decisioning/assembly-helpers.js.map +1 -0
  54. package/dist/lib/server/decisioning/async-outcome.d.ts +154 -0
  55. package/dist/lib/server/decisioning/async-outcome.d.ts.map +1 -0
  56. package/dist/lib/server/decisioning/async-outcome.js +239 -0
  57. package/dist/lib/server/decisioning/async-outcome.js.map +1 -0
  58. package/dist/lib/server/decisioning/capabilities.d.ts +251 -0
  59. package/dist/lib/server/decisioning/capabilities.d.ts.map +1 -0
  60. package/dist/lib/server/decisioning/capabilities.js +16 -0
  61. package/dist/lib/server/decisioning/capabilities.js.map +1 -0
  62. package/dist/lib/server/decisioning/context.d.ts +212 -0
  63. package/dist/lib/server/decisioning/context.d.ts.map +1 -0
  64. package/dist/lib/server/decisioning/context.js +26 -0
  65. package/dist/lib/server/decisioning/context.js.map +1 -0
  66. package/dist/lib/server/decisioning/errors-typed.d.ts +104 -0
  67. package/dist/lib/server/decisioning/errors-typed.d.ts.map +1 -0
  68. package/dist/lib/server/decisioning/errors-typed.js +304 -0
  69. package/dist/lib/server/decisioning/errors-typed.js.map +1 -0
  70. package/dist/lib/server/decisioning/helpers.d.ts +131 -0
  71. package/dist/lib/server/decisioning/helpers.d.ts.map +1 -0
  72. package/dist/lib/server/decisioning/helpers.js +134 -0
  73. package/dist/lib/server/decisioning/helpers.js.map +1 -0
  74. package/dist/lib/server/decisioning/index.d.ts +46 -0
  75. package/dist/lib/server/decisioning/index.d.ts.map +1 -0
  76. package/dist/lib/server/decisioning/index.js +120 -0
  77. package/dist/lib/server/decisioning/index.js.map +1 -0
  78. package/dist/lib/server/decisioning/list-helpers.d.ts +53 -0
  79. package/dist/lib/server/decisioning/list-helpers.d.ts.map +1 -0
  80. package/dist/lib/server/decisioning/list-helpers.js +96 -0
  81. package/dist/lib/server/decisioning/list-helpers.js.map +1 -0
  82. package/dist/lib/server/decisioning/manifest-helpers.d.ts +56 -0
  83. package/dist/lib/server/decisioning/manifest-helpers.d.ts.map +1 -0
  84. package/dist/lib/server/decisioning/manifest-helpers.js +78 -0
  85. package/dist/lib/server/decisioning/manifest-helpers.js.map +1 -0
  86. package/dist/lib/server/decisioning/pagination.d.ts +21 -0
  87. package/dist/lib/server/decisioning/pagination.d.ts.map +1 -0
  88. package/dist/lib/server/decisioning/pagination.js +12 -0
  89. package/dist/lib/server/decisioning/pagination.js.map +1 -0
  90. package/dist/lib/server/decisioning/platform.d.ts +188 -0
  91. package/dist/lib/server/decisioning/platform.d.ts.map +1 -0
  92. package/dist/lib/server/decisioning/platform.js +19 -0
  93. package/dist/lib/server/decisioning/platform.js.map +1 -0
  94. package/dist/lib/server/decisioning/runtime/from-platform.d.ts +510 -0
  95. package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -0
  96. package/dist/lib/server/decisioning/runtime/from-platform.js +2196 -0
  97. package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -0
  98. package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts +114 -0
  99. package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts.map +1 -0
  100. package/dist/lib/server/decisioning/runtime/postgres-task-registry.js +247 -0
  101. package/dist/lib/server/decisioning/runtime/postgres-task-registry.js.map +1 -0
  102. package/dist/lib/server/decisioning/runtime/protocol-for-tool.d.ts +32 -0
  103. package/dist/lib/server/decisioning/runtime/protocol-for-tool.d.ts.map +1 -0
  104. package/dist/lib/server/decisioning/runtime/protocol-for-tool.js +127 -0
  105. package/dist/lib/server/decisioning/runtime/protocol-for-tool.js.map +1 -0
  106. package/dist/lib/server/decisioning/runtime/task-registry.d.ts +105 -0
  107. package/dist/lib/server/decisioning/runtime/task-registry.d.ts.map +1 -0
  108. package/dist/lib/server/decisioning/runtime/task-registry.js +96 -0
  109. package/dist/lib/server/decisioning/runtime/task-registry.js.map +1 -0
  110. package/dist/lib/server/decisioning/runtime/to-context.d.ts +54 -0
  111. package/dist/lib/server/decisioning/runtime/to-context.d.ts.map +1 -0
  112. package/dist/lib/server/decisioning/runtime/to-context.js +166 -0
  113. package/dist/lib/server/decisioning/runtime/to-context.js.map +1 -0
  114. package/dist/lib/server/decisioning/runtime/validate-platform.d.ts +20 -0
  115. package/dist/lib/server/decisioning/runtime/validate-platform.d.ts.map +1 -0
  116. package/dist/lib/server/decisioning/runtime/validate-platform.js +93 -0
  117. package/dist/lib/server/decisioning/runtime/validate-platform.js.map +1 -0
  118. package/dist/lib/server/decisioning/specialisms/audiences.d.ts +72 -0
  119. package/dist/lib/server/decisioning/specialisms/audiences.d.ts.map +1 -0
  120. package/dist/lib/server/decisioning/specialisms/audiences.js +15 -0
  121. package/dist/lib/server/decisioning/specialisms/audiences.js.map +1 -0
  122. package/dist/lib/server/decisioning/specialisms/brand-rights.d.ts +92 -0
  123. package/dist/lib/server/decisioning/specialisms/brand-rights.d.ts.map +1 -0
  124. package/dist/lib/server/decisioning/specialisms/brand-rights.js +28 -0
  125. package/dist/lib/server/decisioning/specialisms/brand-rights.js.map +1 -0
  126. package/dist/lib/server/decisioning/specialisms/campaign-governance.d.ts +67 -0
  127. package/dist/lib/server/decisioning/specialisms/campaign-governance.d.ts.map +1 -0
  128. package/dist/lib/server/decisioning/specialisms/campaign-governance.js +31 -0
  129. package/dist/lib/server/decisioning/specialisms/campaign-governance.js.map +1 -0
  130. package/dist/lib/server/decisioning/specialisms/content-standards.d.ts +78 -0
  131. package/dist/lib/server/decisioning/specialisms/content-standards.d.ts.map +1 -0
  132. package/dist/lib/server/decisioning/specialisms/content-standards.js +35 -0
  133. package/dist/lib/server/decisioning/specialisms/content-standards.js.map +1 -0
  134. package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.ts +81 -0
  135. package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.ts.map +1 -0
  136. package/dist/lib/server/decisioning/specialisms/creative-ad-server.js +28 -0
  137. package/dist/lib/server/decisioning/specialisms/creative-ad-server.js.map +1 -0
  138. package/dist/lib/server/decisioning/specialisms/creative.d.ts +144 -0
  139. package/dist/lib/server/decisioning/specialisms/creative.d.ts.map +1 -0
  140. package/dist/lib/server/decisioning/specialisms/creative.js +19 -0
  141. package/dist/lib/server/decisioning/specialisms/creative.js.map +1 -0
  142. package/dist/lib/server/decisioning/specialisms/lists.d.ts +61 -0
  143. package/dist/lib/server/decisioning/specialisms/lists.d.ts.map +1 -0
  144. package/dist/lib/server/decisioning/specialisms/lists.js +30 -0
  145. package/dist/lib/server/decisioning/specialisms/lists.js.map +1 -0
  146. package/dist/lib/server/decisioning/specialisms/sales.d.ts +163 -0
  147. package/dist/lib/server/decisioning/specialisms/sales.d.ts.map +1 -0
  148. package/dist/lib/server/decisioning/specialisms/sales.js +64 -0
  149. package/dist/lib/server/decisioning/specialisms/sales.js.map +1 -0
  150. package/dist/lib/server/decisioning/specialisms/signals.d.ts +64 -0
  151. package/dist/lib/server/decisioning/specialisms/signals.d.ts.map +1 -0
  152. package/dist/lib/server/decisioning/specialisms/signals.js +28 -0
  153. package/dist/lib/server/decisioning/specialisms/signals.js.map +1 -0
  154. package/dist/lib/server/decisioning/start-time.d.ts +76 -0
  155. package/dist/lib/server/decisioning/start-time.d.ts.map +1 -0
  156. package/dist/lib/server/decisioning/start-time.js +81 -0
  157. package/dist/lib/server/decisioning/start-time.js.map +1 -0
  158. package/dist/lib/server/decisioning/status-changes.d.ts +165 -0
  159. package/dist/lib/server/decisioning/status-changes.d.ts.map +1 -0
  160. package/dist/lib/server/decisioning/status-changes.js +131 -0
  161. package/dist/lib/server/decisioning/status-changes.js.map +1 -0
  162. package/dist/lib/server/decisioning/status-mappers.d.ts +46 -0
  163. package/dist/lib/server/decisioning/status-mappers.d.ts.map +1 -0
  164. package/dist/lib/server/decisioning/status-mappers.js +46 -0
  165. package/dist/lib/server/decisioning/status-mappers.js.map +1 -0
  166. package/dist/lib/server/decisioning/tenant-registry.d.ts +289 -0
  167. package/dist/lib/server/decisioning/tenant-registry.d.ts.map +1 -0
  168. package/dist/lib/server/decisioning/tenant-registry.js +503 -0
  169. package/dist/lib/server/decisioning/tenant-registry.js.map +1 -0
  170. package/dist/lib/server/express-adapter.d.ts +1 -1
  171. package/dist/lib/server/express-adapter.js +1 -1
  172. package/dist/lib/server/governance.d.ts +1 -1
  173. package/dist/lib/server/governance.js +1 -1
  174. package/dist/lib/server/idempotency/store.d.ts +1 -1
  175. package/dist/lib/server/idempotency/store.js +1 -1
  176. package/dist/lib/server/index.d.ts +9 -2
  177. package/dist/lib/server/index.d.ts.map +1 -1
  178. package/dist/lib/server/index.js +79 -4
  179. package/dist/lib/server/index.js.map +1 -1
  180. package/dist/lib/server/legacy/v5/index.d.ts +38 -0
  181. package/dist/lib/server/legacy/v5/index.d.ts.map +1 -0
  182. package/dist/lib/server/legacy/v5/index.js +60 -0
  183. package/dist/lib/server/legacy/v5/index.js.map +1 -0
  184. package/dist/lib/server/normalize-errors.d.ts +88 -0
  185. package/dist/lib/server/normalize-errors.d.ts.map +1 -0
  186. package/dist/lib/server/normalize-errors.js +146 -0
  187. package/dist/lib/server/normalize-errors.js.map +1 -0
  188. package/dist/lib/server/pick-safe-details.d.ts +90 -0
  189. package/dist/lib/server/pick-safe-details.d.ts.map +1 -0
  190. package/dist/lib/server/pick-safe-details.js +148 -0
  191. package/dist/lib/server/pick-safe-details.js.map +1 -0
  192. package/dist/lib/server/postgres-state-store.d.ts +1 -1
  193. package/dist/lib/server/postgres-state-store.js +1 -1
  194. package/dist/lib/server/responses.d.ts +38 -0
  195. package/dist/lib/server/responses.d.ts.map +1 -1
  196. package/dist/lib/server/responses.js +38 -0
  197. package/dist/lib/server/responses.js.map +1 -1
  198. package/dist/lib/server/state-store.d.ts +1 -1
  199. package/dist/lib/server/state-store.js +1 -1
  200. package/dist/lib/server/test-controller.d.ts +10 -3
  201. package/dist/lib/server/test-controller.d.ts.map +1 -1
  202. package/dist/lib/server/test-controller.js +10 -3
  203. package/dist/lib/server/test-controller.js.map +1 -1
  204. package/dist/lib/testing/comply-controller.d.ts +47 -1
  205. package/dist/lib/testing/comply-controller.d.ts.map +1 -1
  206. package/dist/lib/testing/comply-controller.js +11 -4
  207. package/dist/lib/testing/comply-controller.js.map +1 -1
  208. package/dist/lib/testing/index.d.ts +1 -1
  209. package/dist/lib/testing/index.d.ts.map +1 -1
  210. package/dist/lib/testing/index.js.map +1 -1
  211. package/dist/lib/testing/personas/index.d.ts +143 -0
  212. package/dist/lib/testing/personas/index.d.ts.map +1 -0
  213. package/dist/lib/testing/personas/index.js +190 -0
  214. package/dist/lib/testing/personas/index.js.map +1 -0
  215. package/dist/lib/testing/storyboard/index.d.ts +1 -1
  216. package/dist/lib/testing/storyboard/index.d.ts.map +1 -1
  217. package/dist/lib/testing/storyboard/index.js +3 -2
  218. package/dist/lib/testing/storyboard/index.js.map +1 -1
  219. package/dist/lib/testing/storyboard/runner.d.ts +13 -0
  220. package/dist/lib/testing/storyboard/runner.d.ts.map +1 -1
  221. package/dist/lib/testing/storyboard/runner.js +179 -7
  222. package/dist/lib/testing/storyboard/runner.js.map +1 -1
  223. package/dist/lib/types/asset-instances.d.ts +1 -0
  224. package/dist/lib/types/asset-instances.d.ts.map +1 -1
  225. package/dist/lib/types/core.generated.d.ts +203 -98
  226. package/dist/lib/types/core.generated.d.ts.map +1 -1
  227. package/dist/lib/types/core.generated.js +1 -1
  228. package/dist/lib/types/index.d.ts +1 -0
  229. package/dist/lib/types/index.d.ts.map +1 -1
  230. package/dist/lib/types/index.js.map +1 -1
  231. package/dist/lib/types/schemas.generated.d.ts +599 -159
  232. package/dist/lib/types/schemas.generated.d.ts.map +1 -1
  233. package/dist/lib/types/schemas.generated.js +175 -94
  234. package/dist/lib/types/schemas.generated.js.map +1 -1
  235. package/dist/lib/types/tools.generated.d.ts +315 -46
  236. package/dist/lib/types/tools.generated.d.ts.map +1 -1
  237. package/dist/lib/utils/capabilities.d.ts +1 -1
  238. package/dist/lib/utils/capabilities.d.ts.map +1 -1
  239. package/dist/lib/utils/capabilities.js +6 -0
  240. package/dist/lib/utils/capabilities.js.map +1 -1
  241. package/dist/lib/validation/schema-validator.d.ts +13 -0
  242. package/dist/lib/validation/schema-validator.d.ts.map +1 -1
  243. package/dist/lib/validation/schema-validator.js +240 -3
  244. package/dist/lib/validation/schema-validator.js.map +1 -1
  245. package/dist/lib/version.d.ts +3 -3
  246. package/dist/lib/version.d.ts.map +1 -1
  247. package/dist/lib/version.js +3 -3
  248. package/dist/lib/version.js.map +1 -1
  249. package/docs/guides/BUILD-AN-AGENT.md +30 -5
  250. package/docs/llms.txt +28 -17
  251. package/examples/README.md +3 -1
  252. package/examples/decisioning-platform-broadcast-tv.ts +300 -0
  253. package/examples/decisioning-platform-identity-graph.ts +214 -0
  254. package/examples/decisioning-platform-mock-seller.ts +332 -0
  255. package/examples/decisioning-platform-multi-tenant.ts +128 -0
  256. package/examples/decisioning-platform-programmatic.ts +254 -0
  257. package/examples/signals-agent.ts +1 -1
  258. package/package.json +13 -2
  259. package/skills/build-brand-rights-agent/SKILL.md +10 -3
  260. package/skills/build-creative-agent/SKILL.md +94 -64
  261. package/skills/build-decisioning-creative-template/SKILL.md +554 -0
  262. package/skills/build-decisioning-platform/SKILL.md +304 -0
  263. package/skills/build-decisioning-platform/advanced/BRAND-RIGHTS.md +25 -0
  264. package/skills/build-decisioning-platform/advanced/COMPLIANCE.md +23 -0
  265. package/skills/build-decisioning-platform/advanced/GOVERNANCE.md +24 -0
  266. package/skills/build-decisioning-platform/advanced/HITL.md +34 -0
  267. package/skills/build-decisioning-platform/advanced/IDEMPOTENCY.md +52 -0
  268. package/skills/build-decisioning-platform/advanced/MULTI-TENANT.md +47 -0
  269. package/skills/build-decisioning-platform/advanced/OAUTH.md +22 -0
  270. package/skills/build-decisioning-platform/advanced/REFERENCE.md +991 -0
  271. package/skills/build-decisioning-platform/advanced/SANDBOX.md +24 -0
  272. package/skills/build-decisioning-platform/advanced/STATE-MACHINE.md +52 -0
  273. package/skills/build-decisioning-signal-marketplace/SKILL.md +269 -0
  274. package/skills/build-generative-seller-agent/SKILL.md +89 -53
  275. package/skills/build-governance-agent/SKILL.md +76 -45
  276. package/skills/build-retail-media-agent/SKILL.md +87 -62
  277. package/skills/build-seller-agent/SKILL.md +384 -255
  278. package/skills/build-seller-agent/deployment.md +5 -3
  279. package/skills/build-seller-agent/specialisms/audience-sync.md +0 -2
  280. package/skills/build-seller-agent/specialisms/sales-broadcast-tv.md +0 -2
  281. package/skills/build-seller-agent/specialisms/sales-guaranteed.md +0 -2
  282. package/skills/build-seller-agent/specialisms/sales-non-guaranteed.md +0 -2
  283. package/skills/build-seller-agent/specialisms/sales-proposal-mode.md +0 -2
  284. package/skills/build-seller-agent/specialisms/sales-social.md +0 -2
  285. package/skills/build-seller-agent/specialisms/signed-requests.md +0 -2
  286. package/skills/build-si-agent/SKILL.md +40 -32
  287. package/skills/build-signals-agent/SKILL.md +139 -92
  288. 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