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

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