tapimo 0.10.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (386) hide show
  1. package/dist/App.d.ts +4896 -2166
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +2 -2
  4. package/dist/App.js.map +1 -1
  5. package/dist/Assets.d.ts +15 -0
  6. package/dist/Assets.d.ts.map +1 -1
  7. package/dist/Assets.js +7 -0
  8. package/dist/Assets.js.map +1 -1
  9. package/dist/Webhooks.d.ts +0 -75
  10. package/dist/Webhooks.d.ts.map +1 -1
  11. package/dist/Webhooks.js +1 -143
  12. package/dist/Webhooks.js.map +1 -1
  13. package/dist/admin/App.d.ts +127 -78
  14. package/dist/admin/App.d.ts.map +1 -1
  15. package/dist/admin/apps/earn-vaults.d.ts +115 -1
  16. package/dist/admin/apps/earn-vaults.d.ts.map +1 -1
  17. package/dist/admin/apps/earn-vaults.js +59 -6
  18. package/dist/admin/apps/earn-vaults.js.map +1 -1
  19. package/dist/admin/apps/organizations.d.ts +3 -3
  20. package/dist/admin/apps/organizations.d.ts.map +1 -1
  21. package/dist/admin/apps/organizations.js +4 -2
  22. package/dist/admin/apps/organizations.js.map +1 -1
  23. package/dist/admin/apps/verified-tokens.d.ts +83 -83
  24. package/dist/admin/ui.generated.d.ts +1 -1
  25. package/dist/admin/ui.generated.d.ts.map +1 -1
  26. package/dist/admin/ui.generated.js +151 -151
  27. package/dist/admin/ui.generated.js.map +1 -1
  28. package/dist/apps/data/App.d.ts +5924 -4310
  29. package/dist/apps/data/App.d.ts.map +1 -1
  30. package/dist/apps/data/App.js +61 -9
  31. package/dist/apps/data/App.js.map +1 -1
  32. package/dist/apps/data/ZoneSelection.d.ts +62 -0
  33. package/dist/apps/data/ZoneSelection.d.ts.map +1 -0
  34. package/dist/apps/data/ZoneSelection.js +44 -0
  35. package/dist/apps/data/ZoneSelection.js.map +1 -0
  36. package/dist/apps/data/routes/activities.d.ts +2368 -5
  37. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  38. package/dist/apps/data/routes/activities.js +423 -114
  39. package/dist/apps/data/routes/activities.js.map +1 -1
  40. package/dist/apps/data/routes/addresses.d.ts +131 -0
  41. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  42. package/dist/apps/data/routes/balances.d.ts +25 -25
  43. package/dist/apps/data/routes/balances.js +1 -1
  44. package/dist/apps/data/routes/balances.js.map +1 -1
  45. package/dist/apps/data/routes/blocks.d.ts +1 -1
  46. package/dist/apps/data/routes/blocks.js +1 -1
  47. package/dist/apps/data/routes/blocks.js.map +1 -1
  48. package/dist/apps/data/routes/earn.d.ts +135 -10
  49. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  50. package/dist/apps/data/routes/earn.js +157 -25
  51. package/dist/apps/data/routes/earn.js.map +1 -1
  52. package/dist/apps/data/routes/exchanges.d.ts +829 -123
  53. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  54. package/dist/apps/data/routes/exchanges.js +16 -13
  55. package/dist/apps/data/routes/exchanges.js.map +1 -1
  56. package/dist/apps/data/routes/fee-amm.d.ts +26 -26
  57. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  58. package/dist/apps/data/routes/fee-amm.js +9 -11
  59. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  60. package/dist/apps/data/routes/receipts.d.ts +54 -54
  61. package/dist/apps/data/routes/receipts.js +1 -1
  62. package/dist/apps/data/routes/receipts.js.map +1 -1
  63. package/dist/apps/data/routes/rpc.js +24 -10
  64. package/dist/apps/data/routes/rpc.js.map +1 -1
  65. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  66. package/dist/apps/data/routes/tokenlist.d.ts.map +1 -1
  67. package/dist/apps/data/routes/tokenlist.js +16 -15
  68. package/dist/apps/data/routes/tokenlist.js.map +1 -1
  69. package/dist/apps/data/routes/tokens.d.ts +17 -3
  70. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  71. package/dist/apps/data/routes/tokens.js +34 -15
  72. package/dist/apps/data/routes/tokens.js.map +1 -1
  73. package/dist/apps/data/routes/transactions.d.ts +1 -1
  74. package/dist/apps/data/routes/transactions.js +1 -1
  75. package/dist/apps/data/routes/transactions.js.map +1 -1
  76. package/dist/apps/data/routes/transfers.d.ts +13 -13
  77. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  78. package/dist/apps/data/routes/transfers.js +7 -9
  79. package/dist/apps/data/routes/transfers.js.map +1 -1
  80. package/dist/apps/data/routes/valuation.d.ts +21 -21
  81. package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
  82. package/dist/apps/data/routes/webhooks.d.ts +1011 -151
  83. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  84. package/dist/apps/data/routes/webhooks.js +126 -739
  85. package/dist/apps/data/routes/webhooks.js.map +1 -1
  86. package/dist/apps/data/routes/zones.d.ts +15 -15
  87. package/dist/apps/funding/App.d.ts +27 -27
  88. package/dist/apps/funding/routes/chains.d.ts.map +1 -1
  89. package/dist/apps/funding/routes/chains.js +5 -4
  90. package/dist/apps/funding/routes/chains.js.map +1 -1
  91. package/dist/apps/funding/routes/deposit-addresses.d.ts +18 -18
  92. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  93. package/dist/apps/funding/routes/deposit-addresses.js +8 -8
  94. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  95. package/dist/apps/funding/routes/providers.d.ts.map +1 -1
  96. package/dist/apps/funding/routes/providers.js +3 -2
  97. package/dist/apps/funding/routes/providers.js.map +1 -1
  98. package/dist/apps/funding/routes/quotes.d.ts.map +1 -1
  99. package/dist/apps/funding/routes/quotes.js +2 -2
  100. package/dist/apps/funding/routes/quotes.js.map +1 -1
  101. package/dist/apps/funding/routes/transfers.d.ts +33 -33
  102. package/dist/apps/management/App.d.ts +1433 -322
  103. package/dist/apps/management/App.d.ts.map +1 -1
  104. package/dist/apps/management/Billing.d.ts.map +1 -1
  105. package/dist/apps/management/Billing.js +19 -6
  106. package/dist/apps/management/Billing.js.map +1 -1
  107. package/dist/apps/management/routes/api-keys.d.ts +154 -94
  108. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  109. package/dist/apps/management/routes/api-keys.js +76 -30
  110. package/dist/apps/management/routes/api-keys.js.map +1 -1
  111. package/dist/apps/management/routes/billing.d.ts +103 -30
  112. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  113. package/dist/apps/management/routes/billing.js +91 -32
  114. package/dist/apps/management/routes/billing.js.map +1 -1
  115. package/dist/apps/management/routes/faucet.d.ts +12 -12
  116. package/dist/apps/management/routes/faucet.d.ts.map +1 -1
  117. package/dist/apps/management/routes/faucet.js +23 -12
  118. package/dist/apps/management/routes/faucet.js.map +1 -1
  119. package/dist/apps/management/routes/invitations.d.ts +46 -13
  120. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  121. package/dist/apps/management/routes/invitations.js +79 -29
  122. package/dist/apps/management/routes/invitations.js.map +1 -1
  123. package/dist/apps/management/routes/invite-links.d.ts +128 -83
  124. package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
  125. package/dist/apps/management/routes/invite-links.js +100 -41
  126. package/dist/apps/management/routes/invite-links.js.map +1 -1
  127. package/dist/apps/management/routes/me.d.ts +5 -5
  128. package/dist/apps/management/routes/me.d.ts.map +1 -1
  129. package/dist/apps/management/routes/me.js +14 -10
  130. package/dist/apps/management/routes/me.js.map +1 -1
  131. package/dist/apps/management/routes/members.d.ts +68 -47
  132. package/dist/apps/management/routes/members.d.ts.map +1 -1
  133. package/dist/apps/management/routes/members.js +37 -18
  134. package/dist/apps/management/routes/members.js.map +1 -1
  135. package/dist/apps/management/routes/orgs.d.ts +48 -8
  136. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  137. package/dist/apps/management/routes/orgs.js +48 -19
  138. package/dist/apps/management/routes/orgs.js.map +1 -1
  139. package/dist/apps/management/routes/projects.d.ts +130 -68
  140. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  141. package/dist/apps/management/routes/projects.js +72 -21
  142. package/dist/apps/management/routes/projects.js.map +1 -1
  143. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  144. package/dist/apps/management/routes/scopes.js +8 -5
  145. package/dist/apps/management/routes/scopes.js.map +1 -1
  146. package/dist/apps/management/routes/usage.d.ts +38 -38
  147. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  148. package/dist/apps/management/routes/usage.js +41 -31
  149. package/dist/apps/management/routes/usage.js.map +1 -1
  150. package/dist/apps/management/routes/verified-token-requests.d.ts +34 -6
  151. package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -1
  152. package/dist/apps/management/routes/verified-token-requests.js +36 -17
  153. package/dist/apps/management/routes/verified-token-requests.js.map +1 -1
  154. package/dist/apps/management/routes/webhooks.d.ts +1186 -197
  155. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  156. package/dist/apps/management/routes/webhooks.js +106 -38
  157. package/dist/apps/management/routes/webhooks.js.map +1 -1
  158. package/dist/apps/mpp/internal/Adapter.d.ts +1 -1
  159. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  160. package/dist/apps/mpp/internal/Adapter.js +12 -0
  161. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  162. package/dist/db/Db.d.ts +2 -0
  163. package/dist/db/Db.d.ts.map +1 -1
  164. package/dist/db/Db.js +61 -0
  165. package/dist/db/Db.js.map +1 -1
  166. package/dist/db/Schema.d.ts +30 -2
  167. package/dist/db/Schema.d.ts.map +1 -1
  168. package/dist/db/Schema.js +28 -3
  169. package/dist/db/Schema.js.map +1 -1
  170. package/dist/db/tables/earnVaults.d.ts +5 -1
  171. package/dist/db/tables/earnVaults.d.ts.map +1 -1
  172. package/dist/db/tables/earnVaults.js +5 -0
  173. package/dist/db/tables/earnVaults.js.map +1 -1
  174. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  175. package/dist/db/tables/webhookDeliveries.js +10 -0
  176. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  177. package/dist/db/tables/webhookQueueCompletions.d.ts +15 -0
  178. package/dist/db/tables/webhookQueueCompletions.d.ts.map +1 -0
  179. package/dist/db/tables/webhookQueueCompletions.js +32 -0
  180. package/dist/db/tables/webhookQueueCompletions.js.map +1 -0
  181. package/dist/db/tables/webhookQueueEvents.d.ts +16 -20
  182. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -1
  183. package/dist/db/tables/webhookQueueEvents.js +68 -43
  184. package/dist/db/tables/webhookQueueEvents.js.map +1 -1
  185. package/dist/db/tables/webhookSubscriptions.d.ts +47 -72
  186. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  187. package/dist/db/tables/webhookSubscriptions.js +92 -105
  188. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  189. package/dist/internal/Auth.d.ts +8 -1
  190. package/dist/internal/Auth.d.ts.map +1 -1
  191. package/dist/internal/Auth.js +13 -3
  192. package/dist/internal/Auth.js.map +1 -1
  193. package/dist/internal/Cache.d.ts.map +1 -1
  194. package/dist/internal/Cache.js +11 -7
  195. package/dist/internal/Cache.js.map +1 -1
  196. package/dist/internal/EarnVaults.d.ts +41 -2
  197. package/dist/internal/EarnVaults.d.ts.map +1 -1
  198. package/dist/internal/EarnVaults.js +167 -0
  199. package/dist/internal/EarnVaults.js.map +1 -1
  200. package/dist/internal/MetricSink.d.ts +35 -125
  201. package/dist/internal/MetricSink.d.ts.map +1 -1
  202. package/dist/internal/MetricSink.js +50 -121
  203. package/dist/internal/MetricSink.js.map +1 -1
  204. package/dist/internal/OpenApi.d.ts +26 -22
  205. package/dist/internal/OpenApi.d.ts.map +1 -1
  206. package/dist/internal/OpenApi.js +128 -39
  207. package/dist/internal/OpenApi.js.map +1 -1
  208. package/dist/internal/Response.d.ts +13 -8
  209. package/dist/internal/Response.d.ts.map +1 -1
  210. package/dist/internal/Response.js +16 -5
  211. package/dist/internal/Response.js.map +1 -1
  212. package/dist/internal/Schema.d.ts +8 -8
  213. package/dist/internal/Schema.d.ts.map +1 -1
  214. package/dist/internal/Schema.js +10 -9
  215. package/dist/internal/Schema.js.map +1 -1
  216. package/dist/internal/Store.d.ts +15 -6
  217. package/dist/internal/Store.d.ts.map +1 -1
  218. package/dist/internal/Store.js +45 -15
  219. package/dist/internal/Store.js.map +1 -1
  220. package/dist/internal/Tidx.d.ts +2 -0
  221. package/dist/internal/Tidx.d.ts.map +1 -1
  222. package/dist/internal/Tidx.js +16 -4
  223. package/dist/internal/Tidx.js.map +1 -1
  224. package/dist/internal/WebhookTransfer.d.ts +1 -55
  225. package/dist/internal/WebhookTransfer.d.ts.map +1 -1
  226. package/dist/internal/WebhookTransfer.js +1 -239
  227. package/dist/internal/WebhookTransfer.js.map +1 -1
  228. package/dist/internal/Webhooks.d.ts +112 -56
  229. package/dist/internal/Webhooks.d.ts.map +1 -1
  230. package/dist/internal/Webhooks.js +124 -93
  231. package/dist/internal/Webhooks.js.map +1 -1
  232. package/dist/internal/funding/Deposit.d.ts +2 -2
  233. package/dist/internal/funding/Deposit.d.ts.map +1 -1
  234. package/dist/internal/funding/Deposit.js +9 -8
  235. package/dist/internal/funding/Deposit.js.map +1 -1
  236. package/dist/internal/funding/DepositAddress.d.ts +4 -4
  237. package/dist/internal/funding/DepositAddress.d.ts.map +1 -1
  238. package/dist/internal/funding/DepositAddress.js +9 -8
  239. package/dist/internal/funding/DepositAddress.js.map +1 -1
  240. package/dist/internal/funding/DepositWebhook.d.ts +25 -0
  241. package/dist/internal/funding/DepositWebhook.d.ts.map +1 -0
  242. package/dist/internal/funding/DepositWebhook.js +74 -0
  243. package/dist/internal/funding/DepositWebhook.js.map +1 -0
  244. package/dist/internal/funding/Provider.d.ts.map +1 -1
  245. package/dist/internal/funding/Provider.js +13 -12
  246. package/dist/internal/funding/Provider.js.map +1 -1
  247. package/dist/internal/funding/Reconciliation.d.ts +6 -0
  248. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  249. package/dist/internal/funding/Reconciliation.js +42 -10
  250. package/dist/internal/funding/Reconciliation.js.map +1 -1
  251. package/dist/internal/funding/index.d.ts +1 -0
  252. package/dist/internal/funding/index.d.ts.map +1 -1
  253. package/dist/internal/funding/index.js +1 -0
  254. package/dist/internal/funding/index.js.map +1 -1
  255. package/dist/internal/funding/providers/stargate.d.ts +19 -0
  256. package/dist/internal/funding/providers/stargate.d.ts.map +1 -0
  257. package/dist/internal/funding/providers/stargate.js +143 -0
  258. package/dist/internal/funding/providers/stargate.js.map +1 -0
  259. package/dist/internal/webhookDestinations/slack.d.ts.map +1 -1
  260. package/dist/internal/webhookDestinations/slack.js +16 -0
  261. package/dist/internal/webhookDestinations/slack.js.map +1 -1
  262. package/dist/providers/index.d.ts +7 -0
  263. package/dist/providers/index.d.ts.map +1 -1
  264. package/dist/providers/index.js +3 -0
  265. package/dist/providers/index.js.map +1 -1
  266. package/package.json +2 -2
  267. package/src/App.test.ts +48 -0
  268. package/src/App.ts +7 -2
  269. package/src/Assets.test.ts +18 -0
  270. package/src/Assets.ts +22 -0
  271. package/src/Client.test-d.ts +287 -17
  272. package/src/Webhooks.ts +1 -220
  273. package/src/admin/apps/earn-vaults.test.ts +143 -0
  274. package/src/admin/apps/earn-vaults.ts +83 -6
  275. package/src/admin/apps/organizations.ts +6 -2
  276. package/src/admin/ui/src/routes/api-keys.tsx +36 -0
  277. package/src/admin/ui.generated.ts +151 -189
  278. package/src/apps/data/App.test.ts +31 -0
  279. package/src/apps/data/App.ts +64 -10
  280. package/src/apps/data/ZoneSelection.test.ts +196 -0
  281. package/src/apps/data/ZoneSelection.ts +108 -0
  282. package/src/apps/data/routes/activities.test.ts +435 -12
  283. package/src/apps/data/routes/activities.ts +562 -125
  284. package/src/apps/data/routes/balances.ts +1 -1
  285. package/src/apps/data/routes/blocks.ts +1 -1
  286. package/src/apps/data/routes/earn.test.ts +183 -23
  287. package/src/apps/data/routes/earn.ts +225 -29
  288. package/src/apps/data/routes/earn.zone-access.test.ts +46 -0
  289. package/src/apps/data/routes/exchanges.test.ts +31 -1
  290. package/src/apps/data/routes/exchanges.ts +19 -15
  291. package/src/apps/data/routes/fee-amm.ts +18 -11
  292. package/src/apps/data/routes/receipts.ts +1 -1
  293. package/src/apps/data/routes/rpc.test.ts +75 -0
  294. package/src/apps/data/routes/rpc.ts +23 -9
  295. package/src/apps/data/routes/tokenlist.ts +21 -18
  296. package/src/apps/data/routes/tokens.test.ts +29 -0
  297. package/src/apps/data/routes/tokens.ts +38 -15
  298. package/src/apps/data/routes/transactions.ts +1 -1
  299. package/src/apps/data/routes/transfers.test.ts +3 -3
  300. package/src/apps/data/routes/transfers.ts +12 -9
  301. package/src/apps/data/routes/webhooks.test.ts +179 -1497
  302. package/src/apps/data/routes/webhooks.ts +156 -976
  303. package/src/apps/data/routes/zones.test.ts +19 -13
  304. package/src/apps/funding/routes/chains.test.ts +211 -7
  305. package/src/apps/funding/routes/chains.ts +19 -12
  306. package/src/apps/funding/routes/deposit-addresses.test.ts +394 -0
  307. package/src/apps/funding/routes/deposit-addresses.ts +84 -72
  308. package/src/apps/funding/routes/providers.ts +9 -5
  309. package/src/apps/funding/routes/quotes.test.ts +46 -0
  310. package/src/apps/funding/routes/quotes.ts +8 -5
  311. package/src/apps/funding/routes/transfers.test.ts +35 -32
  312. package/src/apps/management/Billing.test.ts +26 -0
  313. package/src/apps/management/Billing.ts +24 -7
  314. package/src/apps/management/routes/api-keys.test.ts +178 -0
  315. package/src/apps/management/routes/api-keys.ts +203 -144
  316. package/src/apps/management/routes/billing.test.ts +209 -0
  317. package/src/apps/management/routes/billing.ts +254 -177
  318. package/src/apps/management/routes/faucet.test.ts +39 -0
  319. package/src/apps/management/routes/faucet.ts +75 -41
  320. package/src/apps/management/routes/invitations.test.ts +216 -0
  321. package/src/apps/management/routes/invitations.ts +186 -109
  322. package/src/apps/management/routes/invite-links.test.ts +263 -1
  323. package/src/apps/management/routes/invite-links.ts +295 -214
  324. package/src/apps/management/routes/me.test.ts +49 -0
  325. package/src/apps/management/routes/me.ts +42 -32
  326. package/src/apps/management/routes/members.test.ts +124 -1
  327. package/src/apps/management/routes/members.ts +81 -51
  328. package/src/apps/management/routes/orgs.test.ts +176 -0
  329. package/src/apps/management/routes/orgs.ts +101 -59
  330. package/src/apps/management/routes/projects.test.ts +183 -0
  331. package/src/apps/management/routes/projects.ts +124 -60
  332. package/src/apps/management/routes/scopes.test.ts +44 -0
  333. package/src/apps/management/routes/scopes.ts +37 -28
  334. package/src/apps/management/routes/usage.test.ts +92 -0
  335. package/src/apps/management/routes/usage.ts +186 -151
  336. package/src/apps/management/routes/verified-token-requests.test.ts +114 -0
  337. package/src/apps/management/routes/verified-token-requests.ts +166 -133
  338. package/src/apps/management/routes/webhooks.test.ts +313 -1
  339. package/src/apps/management/routes/webhooks.ts +220 -71
  340. package/src/apps/mpp/internal/Adapter.ts +13 -1
  341. package/src/db/Db.test.ts +42 -5
  342. package/src/db/Db.ts +85 -0
  343. package/src/db/Schema.ts +41 -3
  344. package/src/db/tables/earnVaults.test.ts +22 -0
  345. package/src/db/tables/earnVaults.ts +11 -1
  346. package/src/db/tables/organizations.test.ts +5 -2
  347. package/src/db/tables/webhookDeliveries.ts +13 -0
  348. package/src/db/tables/webhookQueueCompletions.ts +55 -0
  349. package/src/db/tables/webhookQueueEvents.ts +97 -57
  350. package/src/db/tables/webhookSubscriptions.ts +134 -144
  351. package/src/internal/Auth.ts +23 -5
  352. package/src/internal/Cache.ts +14 -7
  353. package/src/internal/EarnVaults.capabilities.test.ts +33 -0
  354. package/src/internal/EarnVaults.test.ts +79 -0
  355. package/src/internal/EarnVaults.ts +277 -0
  356. package/src/internal/MetricSink.test.ts +41 -373
  357. package/src/internal/MetricSink.ts +92 -273
  358. package/src/internal/OpenApi.ts +165 -49
  359. package/src/internal/Response.test.ts +95 -0
  360. package/src/internal/Response.ts +40 -2
  361. package/src/internal/Schema.test.ts +34 -0
  362. package/src/internal/Schema.ts +20 -9
  363. package/src/internal/Store.test.ts +186 -0
  364. package/src/internal/Store.ts +57 -17
  365. package/src/internal/Tidx.test.ts +19 -0
  366. package/src/internal/Tidx.ts +16 -4
  367. package/src/internal/WebhookTransfer.ts +1 -351
  368. package/src/internal/Webhooks.test.ts +222 -145
  369. package/src/internal/Webhooks.ts +234 -135
  370. package/src/internal/funding/Deposit.ts +71 -64
  371. package/src/internal/funding/DepositAddress.ts +50 -43
  372. package/src/internal/funding/DepositWebhook.test.ts +131 -0
  373. package/src/internal/funding/DepositWebhook.ts +101 -0
  374. package/src/internal/funding/Provider.ts +164 -142
  375. package/src/internal/funding/Reconciliation.test.ts +14 -0
  376. package/src/internal/funding/Reconciliation.ts +161 -86
  377. package/src/internal/funding/index.test.ts +1 -0
  378. package/src/internal/funding/index.ts +1 -0
  379. package/src/internal/funding/providers/index.test.ts +185 -0
  380. package/src/internal/funding/providers/stargate.ts +184 -0
  381. package/src/internal/webhookDestinations/slack.ts +15 -0
  382. package/src/providers/index.test.ts +1 -0
  383. package/src/providers/index.ts +9 -0
  384. package/src/Webhooks.bench.ts +0 -714
  385. package/src/Webhooks.memory.bench.ts +0 -250
  386. package/src/Webhooks.test.ts +0 -146
@@ -3,6 +3,7 @@ import { Hash, Hex } from 'ox'
3
3
  import type * as app_Webhooks from '../apps/data/routes/webhooks.js'
4
4
  import type * as Db from '../db/Db.js'
5
5
  import * as WebhookDeliveries from '../db/tables/webhookDeliveries.js'
6
+ import * as WebhookQueueCompletions from '../db/tables/webhookQueueCompletions.js'
6
7
  import * as WebhookQueueEvents from '../db/tables/webhookQueueEvents.js'
7
8
  import * as WebhookSubscriptions from '../db/tables/webhookSubscriptions.js'
8
9
  import type * as Metrics from '../Metrics.js'
@@ -49,6 +50,16 @@ export type Owner =
49
50
  type: 'mpp'
50
51
  }
51
52
 
53
+ /** API-key scope applied when managing private-resource subscriptions. */
54
+ export type Access = {
55
+ /** API-key environment. */
56
+ environment: 'production' | 'sandbox'
57
+ /** Project attribution, when the key is project-scoped. */
58
+ projectId?: string | undefined
59
+ /** API-key scopes used to authorize private resource reads. */
60
+ scopes: readonly string[]
61
+ }
62
+
52
63
  /**
53
64
  * Optional human context describing what a subscription is for. Carried onto
54
65
  * every delivered {@link Envelope} and surfaced by destinations (Slack header +
@@ -73,6 +84,8 @@ export type Subscription = {
73
84
  createdAt: string
74
85
  /** Where matched events are delivered (URL or Slack channel). */
75
86
  destination: WebhookDestination.Destination
87
+ /** API-key environment captured when the subscription was created. */
88
+ environment?: 'production' | 'sandbox' | undefined
76
89
  /** Event type the subscription listens to. */
77
90
  eventType: EventType
78
91
  /** ISO expiry timestamp; set for MPP-owned subscriptions (TTL-bounded). */
@@ -83,15 +96,19 @@ export type Subscription = {
83
96
  filters: Record<string, unknown>
84
97
  /** Subscription id (`wh_…`). */
85
98
  id: string
86
- /** ISO timestamp of the last successful delivery, if any. */
99
+ /** ISO timestamp of the last successful delivery, if any. Display only:
100
+ * refreshed at most once per precision window. */
87
101
  lastDeliveryAt?: string | undefined
88
102
  /** Authenticated owner. */
89
103
  owner: Owner
104
+ /** Project attribution captured from the creating API key. */
105
+ projectId?: string | undefined
90
106
  /** HMAC signing secret. Returned once at creation; never exposed by reads. */
91
107
  secret: string
92
108
  /** Lifecycle status. */
93
109
  status: Status
94
- /** ISO timestamp of the last mutation. */
110
+ /** ISO timestamp of the last mutation. May lag repeat deliveries; see
111
+ * `lastDeliveryAt`. */
95
112
  updatedAt: string
96
113
  }
97
114
 
@@ -103,12 +120,16 @@ export type CreateInput = {
103
120
  context?: Context | undefined
104
121
  /** Where matched events are delivered. `url` destinations are SSRF-validated. */
105
122
  destination: WebhookDestination.Destination
123
+ /** API-key environment used to isolate private resource events. */
124
+ environment?: 'production' | 'sandbox' | undefined
106
125
  /** Event type to listen to. */
107
126
  eventType: EventType
108
127
  /** Event-type-specific filter predicates. */
109
128
  filters?: Record<string, unknown> | undefined
110
129
  /** Owner derived from the request principal. */
111
130
  owner: Owner
131
+ /** Project attribution used to isolate private resource events. */
132
+ projectId?: string | undefined
112
133
  /** Time-to-live in milliseconds; set the subscription to expire (MPP TTL). */
113
134
  ttl?: number | undefined
114
135
  }
@@ -181,7 +202,7 @@ export async function createSubscription(
181
202
  const now = (options.now ?? (() => new Date()))()
182
203
  const iso = now.toISOString()
183
204
  // A one-field checkpoint records the live creation head without claiming it
184
- // was scanned. The poller expands it using its confirmation depth.
205
+ // was scanned; it anchors the fanout boundary for the new subscription.
185
206
  const pollerCursor =
186
207
  options.startBlockNumber === undefined ? undefined : Cursor.encode([options.startBlockNumber])
187
208
 
@@ -190,11 +211,13 @@ export async function createSubscription(
190
211
  ...(input.context === undefined ? {} : { context: input.context }),
191
212
  createdAt: iso,
192
213
  destination: input.destination,
214
+ ...(input.environment === undefined ? {} : { environment: input.environment }),
193
215
  eventType: input.eventType,
194
216
  failureCount: 0,
195
217
  filters: input.filters ?? {},
196
218
  id: generateId(now),
197
219
  owner: input.owner,
220
+ ...(input.projectId === undefined ? {} : { projectId: input.projectId }),
198
221
  secret: generateSecret(),
199
222
  status: 'active',
200
223
  updatedAt: iso,
@@ -236,8 +259,14 @@ export async function getSubscription(
236
259
  db: Db.Db,
237
260
  owner: Owner,
238
261
  id: string,
262
+ options: getSubscription.Options = {},
239
263
  ): Promise<Subscription | null> {
240
- return WebhookSubscriptions.get(db, owner, id, new Date().toISOString())
264
+ return WebhookSubscriptions.get(db, owner, id, new Date().toISOString(), options.access)
265
+ }
266
+
267
+ export declare namespace getSubscription {
268
+ /** Private-resource visibility options. */
269
+ type Options = { access?: Access | undefined }
241
270
  }
242
271
 
243
272
  /**
@@ -255,6 +284,8 @@ export async function listSubscriptions(
255
284
  export declare namespace listSubscriptions {
256
285
  /** Options for {@link listSubscriptions}. */
257
286
  type Options = {
287
+ /** Private-resource visibility scope. */
288
+ access?: Access | undefined
258
289
  /** Return subscriptions older than this id (keyset paging, newest first). */
259
290
  cursor?: string | undefined
260
291
  /** Maximum subscriptions to return. */
@@ -269,13 +300,22 @@ export declare namespace listSubscriptions {
269
300
  * since the per-owner set is bounded by `maxPerOwner`. Feeds the opt-in
270
301
  * `meta.totalCount` on `GET /webhooks`.
271
302
  */
272
- export async function countSubscriptions(db: Db.Db, owner: Owner): Promise<number> {
273
- return WebhookSubscriptions.count(db, owner, new Date().toISOString())
303
+ export async function countSubscriptions(
304
+ db: Db.Db,
305
+ owner: Owner,
306
+ options: countSubscriptions.Options = {},
307
+ ): Promise<number> {
308
+ return WebhookSubscriptions.count(db, owner, new Date().toISOString(), options.access)
309
+ }
310
+
311
+ export declare namespace countSubscriptions {
312
+ /** Private-resource visibility options. */
313
+ type Options = { access?: Access | undefined }
274
314
  }
275
315
 
276
316
  /**
277
317
  * Patches an owner's subscription, re-validating the URL when changed.
278
- * Returns null when absent. The status column drives the poller's partial
318
+ * Returns null when absent. The status column drives the partial active
279
319
  * index, so no secondary-index maintenance is needed.
280
320
  */
281
321
  export async function updateSubscription(
@@ -285,7 +325,12 @@ export async function updateSubscription(
285
325
  patch: PatchInput,
286
326
  options: updateSubscription.Options = {},
287
327
  ): Promise<Subscription | null> {
288
- const current = await getSubscription(db, owner, id)
328
+ const current = await getSubscription(
329
+ db,
330
+ owner,
331
+ id,
332
+ options.access === undefined ? {} : { access: options.access },
333
+ )
289
334
  if (!current) return null
290
335
 
291
336
  if (current.destination.type !== 'url' && patch.destination?.type === 'url')
@@ -307,43 +352,29 @@ export async function updateSubscription(
307
352
  export declare namespace updateSubscription {
308
353
  /** Options for {@link updateSubscription}. */
309
354
  type Options = {
355
+ /** Private-resource visibility scope. */
356
+ access?: Access | undefined
310
357
  /** Clock used for timestamps (injectable for tests). */
311
358
  now?: (() => Date) | undefined
312
359
  }
313
360
  }
314
361
 
315
362
  /** Deletes an owner's subscription; its delivery rows cascade via the FK. */
316
- export async function deleteSubscription(db: Db.Db, owner: Owner, id: string): Promise<boolean> {
317
- return WebhookSubscriptions.remove(db, owner, id, new Date().toISOString())
318
- }
319
-
320
- /**
321
- * Lists active subscriptions for a `(chainId, eventType)` stream. Used by the
322
- * poller; one indexed query (the partial status index replaces the old
323
- * secondary key space).
324
- */
325
- export async function listActive(
363
+ export async function deleteSubscription(
326
364
  db: Db.Db,
327
- chainId: number,
328
- eventType: EventType,
329
- ): Promise<Subscription[]> {
330
- const pairs = await listActiveWithCursors(db, chainId, eventType)
331
- return pairs.map((pair) => pair.subscription)
365
+ owner: Owner,
366
+ id: string,
367
+ options: deleteSubscription.Options = {},
368
+ ): Promise<boolean> {
369
+ return WebhookSubscriptions.remove(db, owner, id, new Date().toISOString(), options.access)
332
370
  }
333
371
 
334
- /**
335
- * Lists a stream's live active subscriptions paired with their poller cursors,
336
- * from one query. The poller's per-tick lookup.
337
- */
338
- export async function listActiveWithCursors(
339
- db: Db.Db,
340
- chainId: number,
341
- eventType: EventType,
342
- ): Promise<{ cursor: string | null; subscription: Subscription }[]> {
343
- return WebhookSubscriptions.listActive(db, chainId, eventType, new Date().toISOString())
372
+ export declare namespace deleteSubscription {
373
+ /** Private-resource visibility options. */
374
+ type Options = { access?: Access | undefined }
344
375
  }
345
376
 
346
- /** Reads every live active subscription on a chain in one poller query. */
377
+ /** Reads every live active subscription on a chain in one query. */
347
378
  export async function listActiveForChain(
348
379
  db: Db.Db,
349
380
  options: listActiveForChain.Options,
@@ -356,29 +387,10 @@ export async function listActiveForChain(
356
387
  }
357
388
 
358
389
  export declare namespace listActiveForChain {
359
- /** Filters for the poller's chain-level subscription read. */
390
+ /** Filters for the chain-level subscription read. */
360
391
  type Options = Pick<WebhookSubscriptions.listActiveForChain.Options, 'chainId' | 'eventTypes'>
361
392
  }
362
393
 
363
- /**
364
- * Reads the freshest cursor block across a chain's active subscriptions, or
365
- * null when none carry a cursor. Creation checkpoints count at their recorded
366
- * head, so a brand-new subscription reads as fresh.
367
- */
368
- export async function maxCursorBlock(db: Db.Db, chainId: number): Promise<number | null> {
369
- const cursors = await WebhookSubscriptions.listActiveCursors(
370
- db,
371
- chainId,
372
- new Date().toISOString(),
373
- )
374
- let max: number | null = null
375
- for (const cursor of cursors) {
376
- const block = cursorBlock(cursor)
377
- if (block !== undefined && (max === null || block > max)) max = block
378
- }
379
- return max
380
- }
381
-
382
394
  /** Decodes the block component of a subscription cursor, if any. */
383
395
  export function cursorBlock(cursor: string | null): number | undefined {
384
396
  if (cursor === null) return undefined
@@ -392,47 +404,23 @@ export async function getCursor(db: Db.Db, subscriptionId: string): Promise<stri
392
404
  return WebhookSubscriptions.getCursor(db, subscriptionId)
393
405
  }
394
406
 
395
- /** Writes a cursor only when its stored value matches the expected cursor. */
396
- export async function compareAndSetCursor(
397
- db: Db.Db,
398
- options: compareAndSetCursor.Options,
399
- ): Promise<boolean> {
400
- return WebhookSubscriptions.compareAndSetCursor(db, options)
401
- }
402
-
403
- export declare namespace compareAndSetCursor {
404
- /** Options for {@link compareAndSetCursor}. */
405
- type Options = WebhookSubscriptions.compareAndSetCursor.Options
406
- }
407
-
408
- /** Writes cursors whose stored values match their expected cursors. */
409
- export async function compareAndSetCursors(
410
- db: Db.Db,
411
- options: compareAndSetCursors.Options,
412
- ): Promise<Set<string>> {
413
- return WebhookSubscriptions.compareAndSetCursors(db, options)
414
- }
415
-
416
- export declare namespace compareAndSetCursors {
417
- /** Options for {@link compareAndSetCursors}. */
418
- type Options = WebhookSubscriptions.compareAndSetCursors.Options
419
- }
420
-
421
- /** Writes a subscription's keyset cursor. */
422
- export async function setCursor(db: Db.Db, subscriptionId: string, cursor: string): Promise<void> {
423
- await WebhookSubscriptions.setCursor(db, subscriptionId, cursor)
424
- }
425
-
426
407
  /**
427
408
  * Deletes expired MPP-owned subscriptions (delivery rows cascade) and
428
- * deliveries past retention. Reads already filter expiry at query time this
429
- * is the janitor reclaiming space, called best-effort per poller tick.
409
+ * deliveries past retention. Reads already filter expiry at query time; this
410
+ * is the janitor reclaiming space, run best-effort on a schedule.
430
411
  */
431
- export async function pruneExpired(db: Db.Db, options: pruneExpired.Options = {}): Promise<void> {
412
+ export async function pruneExpired(
413
+ db: Db.Db,
414
+ options: pruneExpired.Options = {},
415
+ ): Promise<pruneExpired.Result> {
432
416
  const iso = (options.now ?? (() => new Date()))().toISOString()
433
- await WebhookDeliveries.pruneExpired(db, iso)
434
- await WebhookQueueEvents.pruneExpired(db, iso)
435
- await WebhookSubscriptions.pruneExpired(db, iso)
417
+ // Sequential on purpose: the subscription delete cascades into the other
418
+ // tables, so overlapping it with their prunes risks deadlocks.
419
+ return {
420
+ completions: await WebhookQueueCompletions.pruneExpired(db, iso),
421
+ deliveries: await WebhookDeliveries.pruneExpired(db, iso),
422
+ subscriptions: await WebhookSubscriptions.pruneExpired(db, iso),
423
+ }
436
424
  }
437
425
 
438
426
  export declare namespace pruneExpired {
@@ -441,6 +429,16 @@ export declare namespace pruneExpired {
441
429
  /** Clock used for expiry evaluation; injectable for tests. */
442
430
  now?: (() => Date) | undefined
443
431
  }
432
+
433
+ /** Rows deleted per table; a count pinned at the batch bound means expiry is outpacing pruning. */
434
+ type Result = {
435
+ /** Deleted `webhook_queue_completions` rows. */
436
+ completions: number
437
+ /** Deleted `webhook_deliveries` rows. */
438
+ deliveries: number
439
+ /** Deleted `webhook_subscriptions` rows. */
440
+ subscriptions: number
441
+ }
444
442
  }
445
443
 
446
444
  /**
@@ -449,6 +447,13 @@ export declare namespace pruneExpired {
449
447
  */
450
448
  export const deliveryRetentionMs = 7 * 24 * 60 * 60 * 1_000
451
449
 
450
+ /**
451
+ * Completions dedupe window: replays of a completed obligation are suppressed
452
+ * this long. Deliberately its own literal so per-deployment log-retention
453
+ * tuning cannot move it.
454
+ */
455
+ export const dedupeRetentionMs = 7 * 24 * 60 * 60 * 1_000
456
+
452
457
  /** A compact Cloudflare Queue message referencing a durable staged envelope. */
453
458
  export type QueueReference = {
454
459
  /** Idempotent event id of the staged envelope. */
@@ -463,8 +468,6 @@ export type QueueEvent = QueueReference & {
463
468
  createdAt: string
464
469
  /** Full envelope loaded by the Queue consumer. */
465
470
  envelope: Envelope
466
- /** ISO timestamp after which an unconsumed envelope may be pruned. */
467
- expiresAt: string
468
471
  }
469
472
 
470
473
  /** A claim newer than this counts as an in-flight attempt; above the 10s destination timeout default. */
@@ -489,16 +492,17 @@ export async function ensureQueueEvents(
489
492
  options: ensureQueueEvents.Options = {},
490
493
  ): Promise<ensureQueueEvents.Result> {
491
494
  const now = (options.now ?? (() => new Date()))()
492
- const expiresAt = new Date(now.getTime() + (options.ttl ?? deliveryRetentionMs)).toISOString()
495
+ // Carried so the delivery worker can measure observation-to-attempt without
496
+ // re-reading the block; replays have no observation and stay null.
497
+ const observedAt =
498
+ options.observedAt === undefined ? null : new Date(options.observedAt).toISOString()
493
499
  const events = dispatchables.map(({ envelope, subscription }) => ({
494
500
  createdAt: now.toISOString(),
495
501
  envelope,
496
502
  eventId: envelope.id,
497
- // Inert while pending; the meaningful dedupe deadline is stamped at
498
- // terminal transitions.
499
- expiresAt,
500
503
  // Due immediately; the value orders the sweep so it is never null here.
501
504
  nextAttemptAt: now.toISOString(),
505
+ observedAt,
502
506
  subscriptionId: subscription.id,
503
507
  }))
504
508
  return {
@@ -516,12 +520,12 @@ export declare namespace ensureQueueEvents {
516
520
  subscription: Subscription
517
521
  }
518
522
 
519
- /** Staged-envelope retention options. */
523
+ /** Staging options. */
520
524
  type Options = {
521
525
  /** Clock used for staging timestamps. */
522
526
  now?: (() => Date) | undefined
523
- /** Staged-envelope TTL in ms; defaults to {@link deliveryRetentionMs}. */
524
- ttl?: number | undefined
527
+ /** Epoch ms the head owing these events was observed; absent on replays. */
528
+ observedAt?: number | undefined
525
529
  }
526
530
 
527
531
  /** Created-row count plus references for every requested obligation. */
@@ -557,7 +561,7 @@ export function completeQueueEvent(
557
561
  ): Promise<boolean> {
558
562
  return WebhookQueueEvents.complete(db, reference, {
559
563
  ...options,
560
- expiresAt: new Date(Date.now() + deliveryRetentionMs).toISOString(),
564
+ expiresAt: new Date(Date.now() + dedupeRetentionMs).toISOString(),
561
565
  })
562
566
  }
563
567
 
@@ -567,7 +571,7 @@ export declare namespace completeQueueEvent {
567
571
  /** Claim marker from the claimed record's `attemptingAt`. */
568
572
  claimedAt: string
569
573
  /** Terminal obligation outcome. */
570
- status: WebhookQueueEvents.TerminalStatus
574
+ status: WebhookQueueCompletions.TerminalStatus
571
575
  }
572
576
  }
573
577
 
@@ -652,10 +656,13 @@ export async function deliverClaimedAndRecord(
652
656
  new Date().toISOString(),
653
657
  )
654
658
  if (!subscription || subscription.status !== 'active') return { status: 'skipped' }
659
+ // A staged row predating the observation column, or a replay, reads null.
660
+ const observedAt = claimed.observedAt ? Date.parse(claimed.observedAt) : Number.NaN
655
661
  return {
656
662
  result: await deliverAndRecord(db, subscription, claimed.envelope, {
657
663
  ...options,
658
664
  dequeuedAt,
665
+ ...(Number.isNaN(observedAt) ? {} : { observedAt }),
659
666
  subscriptionReadMs: Date.now() - subscriptionReadStartedAt,
660
667
  }),
661
668
  status: 'delivered',
@@ -667,6 +674,8 @@ export declare namespace deliverClaimedAndRecord {
667
674
  type Claimed = {
668
675
  /** Immutable event envelope to deliver. */
669
676
  envelope: Envelope
677
+ /** ISO head-observation stamp carried by the staged row; null on replays. */
678
+ observedAt?: string | null | undefined
670
679
  /** Subscription re-resolved for current status and secret. */
671
680
  subscriptionId: string
672
681
  }
@@ -714,7 +723,7 @@ export async function recordDelivery(
714
723
  ): Promise<void> {
715
724
  const ttl = options.ttl ?? deliveryRetentionMs
716
725
  // The retention deadline replaces the old store TTL: reads filter on it at
717
- // query time and the poller prunes past it. Anchored to write time (not the
726
+ // query time and pruning reclaims past it. Anchored to write time (not the
718
727
  // row's `createdAt`, which tests pin) — exactly like the old `put` TTL.
719
728
  const expiresAt = new Date(Date.now() + ttl).toISOString()
720
729
  await WebhookDeliveries.insert(db, delivery, expiresAt)
@@ -772,14 +781,21 @@ export async function getDelivery(
772
781
 
773
782
  /**
774
783
  * Default number of consecutive delivery failures after which a subscription
775
- * auto-disables (and is dropped from the poller's active index).
784
+ * auto-disables (and is dropped from the partial active index).
776
785
  */
777
786
  export const maxFailures = 10
778
787
 
788
+ /**
789
+ * How stale `lastDeliveryAt` may get before a success rewrites it. Read only
790
+ * for display, so exactness is traded for not rewriting the row per delivery.
791
+ */
792
+ const lastDeliveryPrecisionMs = 60_000
793
+
779
794
  /**
780
795
  * Records a successful delivery: resets the consecutive-failure counter and
781
- * stamps `lastDeliveryAt`. A column-targeted update, so a concurrent patch is
782
- * never clobbered by the stale copy carried through the delivery queue.
796
+ * refreshes `lastDeliveryAt` at most once per precision window. Returns the
797
+ * written row, or the caller's snapshot when the write was skipped or the row
798
+ * deleted.
783
799
  */
784
800
  export async function recordSuccess(
785
801
  db: Db.Db,
@@ -787,11 +803,13 @@ export async function recordSuccess(
787
803
  options: recordSuccess.Options = {},
788
804
  ): Promise<Subscription> {
789
805
  const now = (options.now ?? (() => new Date()))()
790
- const iso = now.toISOString()
791
- const next = await WebhookSubscriptions.recordSuccess(db, subscription.id, iso)
792
- // A concurrently deleted subscription is not resurrected; return the
793
- // optimistic next state so the caller still observes the outcome.
794
- return next ?? { ...subscription, failureCount: 0, lastDeliveryAt: iso, updatedAt: iso }
806
+ const next = await WebhookSubscriptions.recordSuccess(db, subscription.id, {
807
+ now: now.toISOString(),
808
+ staleBefore: new Date(now.getTime() - lastDeliveryPrecisionMs).toISOString(),
809
+ })
810
+ // No row means the write was skipped or the row was deleted; neither
811
+ // resurrects, so keep the caller's timestamps.
812
+ return next ?? { ...subscription, failureCount: 0 }
795
813
  }
796
814
 
797
815
  export declare namespace recordSuccess {
@@ -805,8 +823,10 @@ export declare namespace recordSuccess {
805
823
  /**
806
824
  * Records a failed delivery: atomically increments the consecutive-failure
807
825
  * counter and, once it reaches `maxFailures`, sets `status: 'disabled'` so the
808
- * subscription drops out of the poller's partial index (surfaced via the API).
809
- * Returns the updated subscription.
826
+ * subscription drops out of the partial active index (surfaced via the API).
827
+ * Once disabled at the cap the row is left alone, so an outage's in-flight
828
+ * failures stop contending on its lock. Returns the written row, or the
829
+ * caller's snapshot advanced optimistically.
810
830
  */
811
831
  export async function recordFailure(
812
832
  db: Db.Db,
@@ -818,14 +838,14 @@ export async function recordFailure(
818
838
  const max = options.maxFailures ?? maxFailures
819
839
  const next = await WebhookSubscriptions.recordFailure(db, subscription.id, max, iso)
820
840
  if (next) return next
821
- // A concurrently deleted subscription is not resurrected; return the
822
- // optimistic next state so the caller still observes the outcome.
823
- const failureCount = subscription.failureCount + 1
841
+ // No row means skipped (already disabled at the cap) or deleted; neither
842
+ // resurrects, so keep the caller's timestamps. Capped so chained skips
843
+ // cannot grow the counter past what the store would hold.
844
+ const failureCount = Math.min(subscription.failureCount + 1, max)
824
845
  return {
825
846
  ...subscription,
826
847
  failureCount,
827
848
  ...(failureCount >= max ? { status: 'disabled' as const } : {}),
828
- updatedAt: iso,
829
849
  }
830
850
  }
831
851
 
@@ -868,23 +888,32 @@ export async function deliverAndRecord(
868
888
  return { error: 'delivery failed before request', ok: false }
869
889
  }
870
890
  })()
871
- const stateWriteStartedAt = Date.now()
872
- if (result.ok) await recordSuccess(db, subscription, options)
873
- else await recordFailure(db, subscription, options)
874
- const stateWriteMs = Date.now() - stateWriteStartedAt
875
-
891
+ // Overlapped: the two writes touch different rows and the log carries the
892
+ // immutable pre-attempt subscription, so it never reads the state write's
893
+ // result. Sequentially they cost two Postgres round trips per delivery.
894
+ const writeStartedAt = Date.now()
876
895
  // Append a delivery-log row. Best-effort: a logging failure must never change
877
896
  // the delivery outcome the caller (Queue consumer / inline path) acts on.
878
- const deliveryLogStartedAt = Date.now()
879
- try {
880
- await recordDelivery(
881
- db,
882
- buildDelivery({ attempt, envelope, result, subscription }, options),
883
- options.deliveryRetentionMs === undefined ? {} : { ttl: options.deliveryRetentionMs },
884
- )
885
- } catch {
886
- // delivery logging is best-effort
887
- }
897
+ const deliveryLog = (async () => {
898
+ try {
899
+ await recordDelivery(
900
+ db,
901
+ buildDelivery({ attempt, envelope, result, subscription }, options),
902
+ options.deliveryRetentionMs === undefined ? {} : { ttl: options.deliveryRetentionMs },
903
+ )
904
+ } catch {
905
+ // delivery logging is best-effort
906
+ }
907
+ return Date.now() - writeStartedAt
908
+ })()
909
+ const stateWrite = (async () => {
910
+ if (result.ok) await recordSuccess(db, subscription, options)
911
+ else await recordFailure(db, subscription, options)
912
+ return Date.now() - writeStartedAt
913
+ })()
914
+ // A state-write rejection still propagates to the caller's retry path; the
915
+ // log promise never rejects, so it cannot mask it or go unhandled.
916
+ const [stateWriteMs, deliveryLogMs] = await Promise.all([stateWrite, deliveryLog])
888
917
  const completedAt = Date.now()
889
918
 
890
919
  // Emit after all delivery work so the timings cover every stage. The
@@ -901,19 +930,34 @@ export async function deliverAndRecord(
901
930
  subscription,
902
931
  attempt,
903
932
  timings: {
904
- deliveryLogMs: completedAt - deliveryLogStartedAt,
933
+ deliveryLogMs,
905
934
  envelopeToQueueMs:
906
935
  options.queuedAt === undefined || Number.isNaN(envelopeAt)
907
936
  ? undefined
908
937
  : Math.max(0, options.queuedAt - envelopeAt),
909
938
  endToEndMs: eventAt === undefined ? undefined : Math.max(0, completedAt - eventAt),
939
+ // The attempt leaving, not bookkeeping finishing: everything after
940
+ // `startedAt` is either the subscriber's time or our own recording,
941
+ // neither of which delays the event reaching them. Gated on a request
942
+ // actually being made, so a failure before send (formatting, invalid
943
+ // destination) does not report an attempt that never left.
944
+ eventToAttemptMs:
945
+ eventAt === undefined || result.durationMs === undefined
946
+ ? undefined
947
+ : Math.max(0, startedAt - eventAt),
910
948
  eventToEnvelopeMs:
911
949
  eventAt === undefined || Number.isNaN(envelopeAt)
912
950
  ? undefined
913
951
  : Math.max(0, envelopeAt - eventAt),
952
+ observedToAttemptMs:
953
+ options.observedAt === undefined || result.durationMs === undefined
954
+ ? undefined
955
+ : Math.max(0, startedAt - options.observedAt),
956
+ preflightMs: Math.max(0, startedAt - dequeuedAt),
914
957
  processingMs: Math.max(0, completedAt - dequeuedAt),
915
958
  queueWaitMs:
916
959
  options.queuedAt === undefined ? undefined : Math.max(0, dequeuedAt - options.queuedAt),
960
+ settleMs: Math.max(0, completedAt - writeStartedAt),
917
961
  stateWriteMs,
918
962
  subscriptionReadMs: options.subscriptionReadMs,
919
963
  },
@@ -960,6 +1004,8 @@ export declare namespace deliverAndRecord {
960
1004
  metrics?: Metrics.Metrics | undefined
961
1005
  /** Clock used for timestamps; injectable for tests. */
962
1006
  now?: (() => Date) | undefined
1007
+ /** Epoch ms the head owing this event was observed; absent on replays. */
1008
+ observedAt?: number | undefined
963
1009
  /** Cloudflare Queue delivery attempt, starting at one; absent for inline delivery paths. */
964
1010
  queueAttempt?: number | undefined
965
1011
  /** Queue enqueue time as epoch milliseconds, used to isolate queue wait. */
@@ -1044,8 +1090,11 @@ function buildDelivery(
1044
1090
  * re-deliveries are dedupable by the receiver.
1045
1091
  */
1046
1092
  export function eventId(options: eventId.Options): string {
1047
- const input = `${options.chainId}:${options.eventType}:${options.blockNumber}:${options.logIndex}`
1048
- return `evt_${Hash.sha256(Hex.fromString(input)).slice(2)}`
1093
+ return eventIdFromKey({
1094
+ chainId: options.chainId,
1095
+ eventType: options.eventType,
1096
+ key: `${options.blockNumber}:${options.logIndex}`,
1097
+ })
1049
1098
  }
1050
1099
 
1051
1100
  export declare namespace eventId {
@@ -1062,6 +1111,24 @@ export declare namespace eventId {
1062
1111
  }
1063
1112
  }
1064
1113
 
1114
+ /** Computes a stable event id from an event-type-specific identity key. */
1115
+ export function eventIdFromKey(options: eventIdFromKey.Options): string {
1116
+ const input = `${options.chainId}:${options.eventType}:${options.key}`
1117
+ return `evt_${Hash.sha256(Hex.fromString(input)).slice(2)}`
1118
+ }
1119
+
1120
+ export declare namespace eventIdFromKey {
1121
+ /** Stable identity inputs for a webhook event. */
1122
+ type Options = {
1123
+ /** Chain the event belongs to. */
1124
+ chainId: number
1125
+ /** Event type. */
1126
+ eventType: EventType
1127
+ /** Stable event-type-specific identity. */
1128
+ key: string
1129
+ }
1130
+ }
1131
+
1065
1132
  /** The JSON envelope delivered to subscribers. */
1066
1133
  export type Envelope = {
1067
1134
  /** Chain the event occurred on. */
@@ -1115,6 +1182,38 @@ export declare namespace buildEnvelope {
1115
1182
  }
1116
1183
  }
1117
1184
 
1185
+ /** Builds a delivery envelope from a stable application-event identity. */
1186
+ export function buildKeyedEnvelope(options: buildKeyedEnvelope.Options): Envelope {
1187
+ const { subscription } = options
1188
+ return {
1189
+ chainId: subscription.chainId,
1190
+ ...(subscription.context === undefined ? {} : { context: subscription.context }),
1191
+ createdAt: (options.createdAt ?? new Date()).toISOString(),
1192
+ data: options.data,
1193
+ id: eventIdFromKey({
1194
+ chainId: subscription.chainId,
1195
+ eventType: subscription.eventType,
1196
+ key: options.key,
1197
+ }),
1198
+ subscriptionId: subscription.id,
1199
+ type: subscription.eventType,
1200
+ }
1201
+ }
1202
+
1203
+ export declare namespace buildKeyedEnvelope {
1204
+ /** Application-event envelope fields. */
1205
+ type Options = {
1206
+ /** Timestamp to stamp on the envelope; defaults to now. */
1207
+ createdAt?: Date | undefined
1208
+ /** Public event payload. */
1209
+ data: unknown
1210
+ /** Stable event-type-specific identity. */
1211
+ key: string
1212
+ /** Subscription the event matched. */
1213
+ subscription: Subscription
1214
+ }
1215
+ }
1216
+
1118
1217
  /**
1119
1218
  * Builds a synthetic `ping` envelope so an owner can test a subscription's
1120
1219
  * endpoint (connectivity, TLS, signature verification) without waiting for a real