@adcp/sdk 14.0.0-rc.38 → 14.0.0-rc.39

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