tapimo 0.10.0 → 0.10.2

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 (295) hide show
  1. package/dist/ApiKey.d.ts +2 -1
  2. package/dist/ApiKey.d.ts.map +1 -1
  3. package/dist/ApiKeys.d.ts +4 -2
  4. package/dist/ApiKeys.d.ts.map +1 -1
  5. package/dist/ApiKeys.js +26 -1
  6. package/dist/ApiKeys.js.map +1 -1
  7. package/dist/App.d.ts +6007 -5003
  8. package/dist/App.d.ts.map +1 -1
  9. package/dist/App.js +1 -0
  10. package/dist/App.js.map +1 -1
  11. package/dist/Webhooks.d.ts +1 -1
  12. package/dist/Webhooks.d.ts.map +1 -1
  13. package/dist/Webhooks.js +6 -6
  14. package/dist/Webhooks.js.map +1 -1
  15. package/dist/admin/App.d.ts +91 -84
  16. package/dist/admin/App.d.ts.map +1 -1
  17. package/dist/admin/apps/api-keys.d.ts +22 -13
  18. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  19. package/dist/admin/apps/api-keys.js +6 -0
  20. package/dist/admin/apps/api-keys.js.map +1 -1
  21. package/dist/admin/apps/organizations.d.ts +4 -2
  22. package/dist/admin/apps/organizations.d.ts.map +1 -1
  23. package/dist/admin/ui.generated.d.ts +1 -1
  24. package/dist/admin/ui.generated.js +69 -69
  25. package/dist/admin/ui.generated.js.map +1 -1
  26. package/dist/analytics/Analytics.d.ts.map +1 -1
  27. package/dist/analytics/Analytics.js +232 -0
  28. package/dist/analytics/Analytics.js.map +1 -1
  29. package/dist/analytics/tables/requestEvents.d.ts +4 -0
  30. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  31. package/dist/analytics/tables/requestEvents.js +2 -0
  32. package/dist/analytics/tables/requestEvents.js.map +1 -1
  33. package/dist/apps/Zones.d.ts +4266 -82
  34. package/dist/apps/Zones.d.ts.map +1 -1
  35. package/dist/apps/Zones.js +27 -8
  36. package/dist/apps/Zones.js.map +1 -1
  37. package/dist/apps/data/App.d.ts +3467 -2696
  38. package/dist/apps/data/App.d.ts.map +1 -1
  39. package/dist/apps/data/App.js +2 -0
  40. package/dist/apps/data/App.js.map +1 -1
  41. package/dist/apps/data/routes/activities.d.ts +2999 -100
  42. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  43. package/dist/apps/data/routes/activities.js +340 -2
  44. package/dist/apps/data/routes/activities.js.map +1 -1
  45. package/dist/apps/data/routes/addresses.d.ts +514 -0
  46. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  47. package/dist/apps/data/routes/balances.d.ts +26 -26
  48. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  49. package/dist/apps/data/routes/balances.js +7 -3
  50. package/dist/apps/data/routes/balances.js.map +1 -1
  51. package/dist/apps/data/routes/blocks.d.ts +4 -4
  52. package/dist/apps/data/routes/earn.d.ts +75 -75
  53. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  54. package/dist/apps/data/routes/earn.js +65 -36
  55. package/dist/apps/data/routes/earn.js.map +1 -1
  56. package/dist/apps/data/routes/exchanges.d.ts +124 -120
  57. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  58. package/dist/apps/data/routes/exchanges.js +115 -77
  59. package/dist/apps/data/routes/exchanges.js.map +1 -1
  60. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  61. package/dist/apps/data/routes/indexer.d.ts +2 -1
  62. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  63. package/dist/apps/data/routes/indexer.js +3 -2
  64. package/dist/apps/data/routes/indexer.js.map +1 -1
  65. package/dist/apps/data/routes/receipts.d.ts +4 -3
  66. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  67. package/dist/apps/data/routes/receipts.js +15 -14
  68. package/dist/apps/data/routes/receipts.js.map +1 -1
  69. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  70. package/dist/apps/data/routes/rpc.js +5 -1
  71. package/dist/apps/data/routes/rpc.js.map +1 -1
  72. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  73. package/dist/apps/data/routes/tokens.d.ts +1 -1
  74. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  75. package/dist/apps/data/routes/tokens.js +4 -3
  76. package/dist/apps/data/routes/tokens.js.map +1 -1
  77. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  78. package/dist/apps/data/routes/transfers.js +25 -7
  79. package/dist/apps/data/routes/transfers.js.map +1 -1
  80. package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
  81. package/dist/apps/data/routes/webhooks.d.ts +71 -1
  82. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  83. package/dist/apps/data/routes/webhooks.js +177 -23
  84. package/dist/apps/data/routes/webhooks.js.map +1 -1
  85. package/dist/apps/data/routes/zones.js +1 -1
  86. package/dist/apps/data/routes/zones.js.map +1 -1
  87. package/dist/apps/funding/App.d.ts +322 -89
  88. package/dist/apps/funding/App.d.ts.map +1 -1
  89. package/dist/apps/funding/routes/deposit-addresses.d.ts +375 -45
  90. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  91. package/dist/apps/funding/routes/deposit-addresses.js +208 -12
  92. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  93. package/dist/apps/funding/routes/transfers.d.ts +32 -32
  94. package/dist/apps/funding/routes/transfers.js +1 -1
  95. package/dist/apps/funding/routes/transfers.js.map +1 -1
  96. package/dist/apps/management/App.d.ts +418 -418
  97. package/dist/apps/management/routes/api-keys.d.ts +50 -50
  98. package/dist/apps/management/routes/billing.d.ts +72 -72
  99. package/dist/apps/management/routes/billing.js +1 -1
  100. package/dist/apps/management/routes/billing.js.map +1 -1
  101. package/dist/apps/management/routes/faucet.d.ts +8 -8
  102. package/dist/apps/management/routes/faucet.js +2 -2
  103. package/dist/apps/management/routes/faucet.js.map +1 -1
  104. package/dist/apps/management/routes/invite-links.d.ts +42 -42
  105. package/dist/apps/management/routes/me.d.ts +6 -6
  106. package/dist/apps/management/routes/members.d.ts +14 -14
  107. package/dist/apps/management/routes/projects.d.ts +40 -40
  108. package/dist/apps/management/routes/usage.d.ts +8 -8
  109. package/dist/apps/management/routes/webhooks.d.ts +49 -49
  110. package/dist/apps/mcp.d.ts.map +1 -1
  111. package/dist/apps/mcp.js +4 -1
  112. package/dist/apps/mcp.js.map +1 -1
  113. package/dist/db/Db.d.ts +2 -0
  114. package/dist/db/Db.d.ts.map +1 -1
  115. package/dist/db/Db.js +90 -0
  116. package/dist/db/Db.js.map +1 -1
  117. package/dist/db/Schema.d.ts +19 -0
  118. package/dist/db/Schema.d.ts.map +1 -1
  119. package/dist/db/Schema.js +24 -0
  120. package/dist/db/Schema.js.map +1 -1
  121. package/dist/db/tables/fundingDepositAddresses.d.ts +20 -0
  122. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  123. package/dist/db/tables/fundingDepositAddresses.js +17 -0
  124. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  125. package/dist/db/tables/fundingDeposits.d.ts +41 -0
  126. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  127. package/dist/db/tables/fundingDeposits.js +69 -0
  128. package/dist/db/tables/fundingDeposits.js.map +1 -1
  129. package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
  130. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
  131. package/dist/db/tables/webhookQueueEvents.js +160 -0
  132. package/dist/db/tables/webhookQueueEvents.js.map +1 -0
  133. package/dist/db/tables/webhookSubscriptions.d.ts +2 -0
  134. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  135. package/dist/db/tables/webhookSubscriptions.js +10 -0
  136. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  137. package/dist/internal/Auth.d.ts +3 -26
  138. package/dist/internal/Auth.d.ts.map +1 -1
  139. package/dist/internal/Auth.js +6 -37
  140. package/dist/internal/Auth.js.map +1 -1
  141. package/dist/internal/Credential.d.ts +27 -0
  142. package/dist/internal/Credential.d.ts.map +1 -0
  143. package/dist/internal/Credential.js +34 -0
  144. package/dist/internal/Credential.js.map +1 -0
  145. package/dist/internal/EarnRates.js +11 -7
  146. package/dist/internal/EarnRates.js.map +1 -1
  147. package/dist/internal/Log.d.ts +8 -0
  148. package/dist/internal/Log.d.ts.map +1 -1
  149. package/dist/internal/Log.js +4 -0
  150. package/dist/internal/Log.js.map +1 -1
  151. package/dist/internal/MetricSink.d.ts +51 -0
  152. package/dist/internal/MetricSink.d.ts.map +1 -1
  153. package/dist/internal/MetricSink.js +54 -2
  154. package/dist/internal/MetricSink.js.map +1 -1
  155. package/dist/internal/RateLimit.d.ts +4 -6
  156. package/dist/internal/RateLimit.d.ts.map +1 -1
  157. package/dist/internal/RateLimit.js +15 -14
  158. package/dist/internal/RateLimit.js.map +1 -1
  159. package/dist/internal/Store.d.ts +2 -1
  160. package/dist/internal/Store.d.ts.map +1 -1
  161. package/dist/internal/Store.js +57 -18
  162. package/dist/internal/Store.js.map +1 -1
  163. package/dist/internal/Tidx.d.ts +4 -3
  164. package/dist/internal/Tidx.d.ts.map +1 -1
  165. package/dist/internal/Tidx.js +2 -2
  166. package/dist/internal/Tidx.js.map +1 -1
  167. package/dist/internal/Value.d.ts +2 -0
  168. package/dist/internal/Value.d.ts.map +1 -1
  169. package/dist/internal/Value.js +25 -0
  170. package/dist/internal/Value.js.map +1 -1
  171. package/dist/internal/Viem.d.ts +4 -3
  172. package/dist/internal/Viem.d.ts.map +1 -1
  173. package/dist/internal/Viem.js +3 -2
  174. package/dist/internal/Viem.js.map +1 -1
  175. package/dist/internal/WebhookTransfer.d.ts +20 -0
  176. package/dist/internal/WebhookTransfer.d.ts.map +1 -1
  177. package/dist/internal/WebhookTransfer.js +7 -4
  178. package/dist/internal/WebhookTransfer.js.map +1 -1
  179. package/dist/internal/Webhooks.d.ts +141 -0
  180. package/dist/internal/Webhooks.d.ts.map +1 -1
  181. package/dist/internal/Webhooks.js +139 -10
  182. package/dist/internal/Webhooks.js.map +1 -1
  183. package/dist/internal/funding/providers/relay.d.ts +3 -4
  184. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  185. package/dist/internal/funding/providers/relay.js +59 -8
  186. package/dist/internal/funding/providers/relay.js.map +1 -1
  187. package/dist/internal/index.d.ts +1 -0
  188. package/dist/internal/index.d.ts.map +1 -1
  189. package/dist/internal/index.js +1 -0
  190. package/dist/internal/index.js.map +1 -1
  191. package/package.json +3 -3
  192. package/src/ApiKeys.test.ts +20 -0
  193. package/src/ApiKeys.ts +28 -1
  194. package/src/App.test.ts +40 -8
  195. package/src/App.ts +1 -0
  196. package/src/Client.test-d.ts +34 -1
  197. package/src/Webhooks.ts +20 -18
  198. package/src/admin/apps/api-keys.test.ts +9 -0
  199. package/src/admin/apps/api-keys.ts +15 -0
  200. package/src/admin/ui/src/lib/query-client.ts +3 -2
  201. package/src/admin/ui/src/routes/api-keys.tsx +31 -9
  202. package/src/admin/ui.generated.ts +69 -69
  203. package/src/analytics/Analytics.test.ts +42 -0
  204. package/src/analytics/Analytics.ts +232 -0
  205. package/src/analytics/tables/requestEvents.ts +6 -0
  206. package/src/apps/Zones.test.ts +20 -3
  207. package/src/apps/Zones.ts +28 -8
  208. package/src/apps/composition.test.ts +2 -0
  209. package/src/apps/data/App.test.ts +2 -2
  210. package/src/apps/data/App.ts +2 -0
  211. package/src/apps/data/routes/activities.bench.ts +41 -0
  212. package/src/apps/data/routes/activities.test.ts +103 -2
  213. package/src/apps/data/routes/activities.ts +401 -2
  214. package/src/apps/data/routes/balances.bench.ts +21 -0
  215. package/src/apps/data/routes/balances.ts +7 -3
  216. package/src/apps/data/routes/blocks.bench.ts +34 -0
  217. package/src/apps/data/routes/coingecko.bench.ts +53 -0
  218. package/src/apps/data/routes/earn.bench.ts +71 -0
  219. package/src/apps/data/routes/earn.ts +66 -36
  220. package/src/apps/data/routes/exchanges.bench.ts +85 -0
  221. package/src/apps/data/routes/exchanges.test.ts +8 -3
  222. package/src/apps/data/routes/exchanges.ts +134 -82
  223. package/src/apps/data/routes/fee-amm.bench.ts +13 -0
  224. package/src/apps/data/routes/indexer.bench.ts +8 -0
  225. package/src/apps/data/routes/indexer.test.ts +2 -1
  226. package/src/apps/data/routes/indexer.ts +3 -2
  227. package/src/apps/data/routes/receipts.bench.ts +39 -0
  228. package/src/apps/data/routes/receipts.ts +16 -14
  229. package/src/apps/data/routes/rpc.bench.ts +14 -0
  230. package/src/apps/data/routes/rpc.test.ts +27 -0
  231. package/src/apps/data/routes/rpc.ts +5 -1
  232. package/src/apps/data/routes/tokenlist.bench.ts +3 -0
  233. package/src/apps/data/routes/tokens.bench.ts +76 -0
  234. package/src/apps/data/routes/tokens.ts +3 -2
  235. package/src/apps/data/routes/transactions.bench.ts +48 -0
  236. package/src/apps/data/routes/transfers.bench.ts +28 -0
  237. package/src/apps/data/routes/transfers.ts +36 -10
  238. package/src/apps/data/routes/valuation.bench.ts +14 -0
  239. package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
  240. package/src/apps/data/routes/webhooks.bench.ts +26 -0
  241. package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
  242. package/src/apps/data/routes/webhooks.test.ts +53 -6
  243. package/src/apps/data/routes/webhooks.ts +252 -24
  244. package/src/apps/data/routes/zones.bench.ts +32 -0
  245. package/src/apps/data/routes/zones.test.ts +1 -1
  246. package/src/apps/data/routes/zones.ts +1 -1
  247. package/src/apps/funding/routes/chains.test.ts +1 -1
  248. package/src/apps/funding/routes/deposit-addresses.test.ts +217 -3
  249. package/src/apps/funding/routes/deposit-addresses.ts +255 -19
  250. package/src/apps/funding/routes/providers.test.ts +1 -1
  251. package/src/apps/funding/routes/quotes.test.ts +1 -1
  252. package/src/apps/funding/routes/transfers.test.ts +20 -8
  253. package/src/apps/funding/routes/transfers.ts +1 -1
  254. package/src/apps/management/routes/billing.ts +1 -1
  255. package/src/apps/management/routes/faucet.ts +2 -2
  256. package/src/apps/management/routes/me.test.ts +31 -0
  257. package/src/apps/mcp.ts +4 -1
  258. package/src/db/Db.ts +117 -0
  259. package/src/db/Schema.ts +32 -0
  260. package/src/db/tables/fundingDepositAddresses.test.ts +28 -0
  261. package/src/db/tables/fundingDepositAddresses.ts +39 -0
  262. package/src/db/tables/fundingDeposits.test.ts +82 -0
  263. package/src/db/tables/fundingDeposits.ts +130 -0
  264. package/src/db/tables/webhookQueueEvents.ts +285 -0
  265. package/src/db/tables/webhookSubscriptions.ts +15 -0
  266. package/src/internal/Auth.test.ts +61 -98
  267. package/src/internal/Auth.ts +9 -63
  268. package/src/internal/Credential.test.ts +51 -0
  269. package/src/internal/Credential.ts +58 -0
  270. package/src/internal/EarnRates.ts +13 -7
  271. package/src/internal/EarnVaults.test.ts +2 -2
  272. package/src/internal/Log.test.ts +3 -1
  273. package/src/internal/Log.ts +14 -0
  274. package/src/internal/MetricSink.test.ts +108 -2
  275. package/src/internal/MetricSink.ts +112 -2
  276. package/src/internal/RateLimit.test.ts +23 -18
  277. package/src/internal/RateLimit.ts +23 -15
  278. package/src/internal/Store.test.ts +99 -13
  279. package/src/internal/Store.ts +52 -17
  280. package/src/internal/Tidx.test.ts +4 -4
  281. package/src/internal/Tidx.ts +5 -4
  282. package/src/internal/Value.test.ts +25 -0
  283. package/src/internal/Value.ts +29 -0
  284. package/src/internal/Viem.test.ts +4 -0
  285. package/src/internal/Viem.ts +6 -4
  286. package/src/internal/WebhookTransfer.test.ts +89 -0
  287. package/src/internal/WebhookTransfer.ts +11 -7
  288. package/src/internal/Webhooks.test.ts +488 -0
  289. package/src/internal/Webhooks.ts +291 -10
  290. package/src/internal/funding/Provider.test.ts +1 -1
  291. package/src/internal/funding/providers/index.test.ts +73 -3
  292. package/src/internal/funding/providers/relay.test.ts +1 -5
  293. package/src/internal/funding/providers/relay.ts +56 -12
  294. package/src/internal/index.test.ts +1 -0
  295. package/src/internal/index.ts +1 -0
@@ -804,6 +804,25 @@ describe('delivery log', () => {
804
804
  `)
805
805
  })
806
806
 
807
+ test('deliverAndRecord records envelope serialization failures', async () => {
808
+ const db = TestApp.database()
809
+ const created = await Webhooks.createSubscription(db, createInput())
810
+ const envelope = {
811
+ chainId: 4217,
812
+ createdAt: '2026-01-01T00:00:00.000Z',
813
+ data: { amount: 1n },
814
+ id: 'evt_invalid',
815
+ subscriptionId: created.id,
816
+ type: 'token:transfer',
817
+ } satisfies Webhooks.Envelope
818
+
819
+ const result = await Webhooks.deliverAndRecord(db, created, envelope, { now: fixedNow })
820
+
821
+ expect(result).toEqual({ error: 'delivery failed before request', ok: false })
822
+ const after = await Webhooks.getSubscription(db, apiKeyOwner, created.id)
823
+ expect(after?.failureCount).toBe(1)
824
+ })
825
+
807
826
  test('deliverCurrentAndRecord skips paused and deleted queued subscriptions', async () => {
808
827
  const db = TestApp.database()
809
828
  const paused = await Webhooks.createSubscription(db, createInput())
@@ -909,6 +928,475 @@ describe('delivery log', () => {
909
928
  })
910
929
  })
911
930
 
931
+ async function stagedObligation(db: ReturnType<typeof TestApp.database>, logIndex = 4) {
932
+ const subscription = await Webhooks.createSubscription(db, createInput())
933
+ const envelope = Webhooks.buildEnvelope({
934
+ blockNumber: 123,
935
+ createdAt: new Date(),
936
+ data: { amount: '1' },
937
+ logIndex,
938
+ subscription,
939
+ })
940
+ const { created, references } = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }])
941
+ return { created, envelope, reference: references[0]!, subscription }
942
+ }
943
+
944
+ describe('ensureQueueEvents', () => {
945
+ test('keeps oversized envelopes behind compact idempotent Queue references', async () => {
946
+ const db = TestApp.database()
947
+ const subscription = await Webhooks.createSubscription(
948
+ db,
949
+ createInput({ eventType: 'transaction:included' }),
950
+ )
951
+ const input = `0x${'ab'.repeat(70_000)}`
952
+ const envelope = Webhooks.buildEnvelope({
953
+ blockNumber: 123,
954
+ createdAt: fixedNow(),
955
+ data: { input, meta: { rpc: { input } } },
956
+ logIndex: 4,
957
+ subscription,
958
+ })
959
+ const dispatchable = { envelope, subscription }
960
+
961
+ const { references: [reference] } = await Webhooks.ensureQueueEvents(db, [dispatchable]) // prettier-ignore
962
+ await Webhooks.ensureQueueEvents(db, [dispatchable])
963
+
964
+ expect(
965
+ new TextEncoder().encode(JSON.stringify({ body: dispatchable })).byteLength,
966
+ ).toBeGreaterThan(128_000)
967
+ expect(new TextEncoder().encode(JSON.stringify({ body: reference })).byteLength).toBeLessThan(
968
+ 1_000,
969
+ )
970
+ expect((await Webhooks.getQueueEvent(db, reference!))?.envelope).toEqual(envelope)
971
+ })
972
+
973
+ test('delivers a staged oversized envelope from its compact reference', async () => {
974
+ const db = TestApp.database()
975
+ const subscription = await Webhooks.createSubscription(
976
+ db,
977
+ createInput({ eventType: 'transaction:included' }),
978
+ )
979
+ const input = `0x${'ab'.repeat(70_000)}`
980
+ const envelope = Webhooks.buildEnvelope({
981
+ blockNumber: 123,
982
+ data: { input, meta: { rpc: { input } } },
983
+ logIndex: 4,
984
+ subscription,
985
+ })
986
+ const { references: [reference] } = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }]) // prettier-ignore
987
+ const histograms = new Map<string, number>()
988
+ const metrics = Metrics.from({
989
+ count() {},
990
+ flush() {},
991
+ gauge() {},
992
+ histogram(name, value) {
993
+ histograms.set(name, value)
994
+ },
995
+ })
996
+ let deliveredBytes = 0
997
+ const fetch = (async (_url, init) => {
998
+ deliveredBytes = new TextEncoder().encode(
999
+ typeof init?.body === 'string' ? init.body : '',
1000
+ ).byteLength
1001
+ return new Response(null, { status: 200 })
1002
+ }) satisfies typeof globalThis.fetch
1003
+
1004
+ const result = await Webhooks.deliverQueueEventAndRecord(db, reference!, {
1005
+ fetch,
1006
+ metrics,
1007
+ queuedAt: Date.now(),
1008
+ })
1009
+
1010
+ expect(result.status).toBe('delivered')
1011
+ expect(deliveredBytes).toBeGreaterThan(128_000)
1012
+ expect(histograms.has('webhook_delivery_processing_ms')).toBe(true)
1013
+ expect(histograms.has('webhook_delivery_queue_wait_ms')).toBe(true)
1014
+ expect(histograms.has('webhook_delivery_subscription_read_ms')).toBe(true)
1015
+ })
1016
+
1017
+ test('skips a staged event after its subscription expires', async () => {
1018
+ vi.useFakeTimers({ now: fixedNow(), toFake: ['Date'] })
1019
+ try {
1020
+ const db = TestApp.database()
1021
+ const subscription = await Webhooks.createSubscription(
1022
+ db,
1023
+ createInput({ eventType: 'transaction:included', owner: mppOwner, ttl: 60_000 }),
1024
+ { now: fixedNow },
1025
+ )
1026
+ const envelope = Webhooks.buildEnvelope({
1027
+ blockNumber: 123,
1028
+ data: {},
1029
+ logIndex: 4,
1030
+ subscription,
1031
+ })
1032
+ const { references: [reference] } = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }]) // prettier-ignore
1033
+
1034
+ vi.setSystemTime(new Date('2026-01-01T00:01:00.000Z'))
1035
+
1036
+ await expect(Webhooks.deliverQueueEventAndRecord(db, reference!)).resolves
1037
+ .toMatchInlineSnapshot(`
1038
+ {
1039
+ "status": "skipped",
1040
+ }
1041
+ `)
1042
+ } finally {
1043
+ vi.useRealTimers()
1044
+ }
1045
+ })
1046
+
1047
+ test('stages once and keeps the first envelope', async () => {
1048
+ const db = TestApp.database()
1049
+ const { created, envelope, reference, subscription } = await stagedObligation(db)
1050
+ const replacement = { ...envelope, data: { amount: '2' } }
1051
+
1052
+ const second = await Webhooks.ensureQueueEvents(db, [{ envelope: replacement, subscription }])
1053
+
1054
+ expect(created).toBe(1)
1055
+ expect(second.created).toBe(0)
1056
+ expect((await Webhooks.getQueueEvent(db, reference))?.envelope).toEqual(envelope)
1057
+ })
1058
+
1059
+ test('never resurrects a terminal obligation', async () => {
1060
+ const db = TestApp.database()
1061
+ const { envelope, reference, subscription } = await stagedObligation(db)
1062
+ const claim = await Webhooks.claimQueueEvent(db, reference)
1063
+ expect(claim.type).toBe('claimed')
1064
+ if (claim.type !== 'claimed') return
1065
+ expect(
1066
+ await Webhooks.completeQueueEvent(db, reference, {
1067
+ claimedAt: claim.record.attemptingAt!,
1068
+ status: 'succeeded',
1069
+ }),
1070
+ ).toBe(true)
1071
+
1072
+ const again = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }])
1073
+ expect(again.created).toBe(0)
1074
+ await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({
1075
+ status: 'succeeded',
1076
+ type: 'terminal',
1077
+ })
1078
+ // Terminal rows read as absent, so no consumer path can redeliver them.
1079
+ expect(await Webhooks.getQueueEvent(db, reference)).toBeNull()
1080
+ })
1081
+
1082
+ test('replaces an expired terminal row so replays stay deliverable', async () => {
1083
+ vi.useFakeTimers({ toFake: ['Date'] })
1084
+ vi.setSystemTime(fixedNow())
1085
+ try {
1086
+ const db = TestApp.database()
1087
+ const { envelope, reference, subscription } = await stagedObligation(db)
1088
+ const claim = await Webhooks.claimQueueEvent(db, reference)
1089
+ expect(claim.type).toBe('claimed')
1090
+ if (claim.type !== 'claimed') return
1091
+ expect(
1092
+ await Webhooks.completeQueueEvent(db, reference, {
1093
+ claimedAt: claim.record.attemptingAt!,
1094
+ status: 'succeeded',
1095
+ }),
1096
+ ).toBe(true)
1097
+
1098
+ // Past the dedupe window an unpruned terminal row must not suppress a
1099
+ // replay.
1100
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryRetentionMs + 1_000))
1101
+ const replay = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }])
1102
+ expect(replay.created).toBe(1)
1103
+ await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toMatchObject({
1104
+ type: 'claimed',
1105
+ })
1106
+ } finally {
1107
+ vi.useRealTimers()
1108
+ }
1109
+ })
1110
+ })
1111
+
1112
+ describe('claimQueueEvent', () => {
1113
+ beforeEach(() => {
1114
+ vi.useFakeTimers({ toFake: ['Date'] })
1115
+ vi.setSystemTime(fixedNow())
1116
+ })
1117
+
1118
+ afterEach(() => {
1119
+ vi.useRealTimers()
1120
+ })
1121
+
1122
+ test('claims a due obligation once and reclaims after staleness', async () => {
1123
+ const db = TestApp.database()
1124
+ const { reference } = await stagedObligation(db)
1125
+
1126
+ const first = await Webhooks.claimQueueEvent(db, reference)
1127
+ expect(first.type).toBe('claimed')
1128
+ if (first.type !== 'claimed') return
1129
+ expect(first.record.attemptCount).toBe(1)
1130
+
1131
+ await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({
1132
+ type: 'attempting',
1133
+ })
1134
+
1135
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryAttemptStaleMs + 1))
1136
+ const reclaimed = await Webhooks.claimQueueEvent(db, reference)
1137
+ expect(reclaimed.type).toBe('claimed')
1138
+ if (reclaimed.type !== 'claimed') return
1139
+ expect(reclaimed.record.attemptCount).toBe(2)
1140
+ })
1141
+
1142
+ test('classifies missing, scheduled, and terminal obligations', async () => {
1143
+ const db = TestApp.database()
1144
+ const { reference } = await stagedObligation(db)
1145
+
1146
+ await expect(
1147
+ Webhooks.claimQueueEvent(db, { eventId: 'evt_absent', subscriptionId: 'wh_absent' }),
1148
+ ).resolves.toEqual({ type: 'missing' })
1149
+
1150
+ const claim = await Webhooks.claimQueueEvent(db, reference)
1151
+ expect(claim.type).toBe('claimed')
1152
+ if (claim.type !== 'claimed') return
1153
+ expect(
1154
+ await Webhooks.scheduleQueueEventRetry(db, reference, {
1155
+ attemptCount: 1,
1156
+ claimedAt: claim.record.attemptingAt!,
1157
+ }),
1158
+ ).toBe(true)
1159
+ await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({
1160
+ nextAttemptAt: new Date(fixedNow().getTime() + Webhooks.retryDelayMs(1)).toISOString(),
1161
+ type: 'scheduled',
1162
+ })
1163
+
1164
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.retryDelayMs(1) + 1))
1165
+ const retried = await Webhooks.claimQueueEvent(db, reference)
1166
+ expect(retried.type).toBe('claimed')
1167
+ if (retried.type !== 'claimed') return
1168
+ expect(
1169
+ await Webhooks.completeQueueEvent(db, reference, {
1170
+ claimedAt: retried.record.attemptingAt!,
1171
+ status: 'failed',
1172
+ }),
1173
+ ).toBe(true)
1174
+ await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({
1175
+ status: 'failed',
1176
+ type: 'terminal',
1177
+ })
1178
+ })
1179
+
1180
+ test('keeps owed work claimable indefinitely', async () => {
1181
+ const db = TestApp.database()
1182
+ const { reference } = await stagedObligation(db)
1183
+
1184
+ // Pending obligations never expire; only terminal transitions stamp a
1185
+ // dedupe deadline.
1186
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryRetentionMs * 3))
1187
+ await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toMatchObject({
1188
+ type: 'claimed',
1189
+ })
1190
+ })
1191
+
1192
+ test('never prunes pending work', async () => {
1193
+ const db = TestApp.database()
1194
+ const { reference } = await stagedObligation(db)
1195
+
1196
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryRetentionMs + 1_000))
1197
+ await Webhooks.pruneExpired(db)
1198
+ const claim = await Webhooks.claimQueueEvent(db, reference)
1199
+ expect(claim.type).toBe('claimed')
1200
+ if (claim.type !== 'claimed') return
1201
+ expect(
1202
+ await Webhooks.completeQueueEvent(db, reference, {
1203
+ claimedAt: claim.record.attemptingAt!,
1204
+ status: 'succeeded',
1205
+ }),
1206
+ ).toBe(true)
1207
+ })
1208
+
1209
+ test('fences a stale claimant after a reclaim', async () => {
1210
+ const db = TestApp.database()
1211
+ const { reference } = await stagedObligation(db)
1212
+ const stale = await Webhooks.claimQueueEvent(db, reference)
1213
+ expect(stale.type).toBe('claimed')
1214
+ if (stale.type !== 'claimed') return
1215
+
1216
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryAttemptStaleMs + 1))
1217
+ const current = await Webhooks.claimQueueEvent(db, reference)
1218
+ expect(current.type).toBe('claimed')
1219
+ if (current.type !== 'claimed') return
1220
+
1221
+ const staleClaimedAt = stale.record.attemptingAt!
1222
+ expect(
1223
+ await Webhooks.completeQueueEvent(db, reference, {
1224
+ claimedAt: staleClaimedAt,
1225
+ status: 'failed',
1226
+ }),
1227
+ ).toBe(false)
1228
+ expect(
1229
+ await Webhooks.scheduleQueueEventRetry(db, reference, {
1230
+ attemptCount: 1,
1231
+ claimedAt: staleClaimedAt,
1232
+ }),
1233
+ ).toBe(false)
1234
+ expect(
1235
+ await Webhooks.completeQueueEvent(db, reference, {
1236
+ claimedAt: current.record.attemptingAt!,
1237
+ status: 'succeeded',
1238
+ }),
1239
+ ).toBe(true)
1240
+ })
1241
+ })
1242
+
1243
+ describe('retryDelayMs', () => {
1244
+ test('backs off exponentially and caps hourly', () => {
1245
+ expect([1, 2, 3, 4, 5, 6, 10].map(Webhooks.retryDelayMs)).toMatchInlineSnapshot(`
1246
+ [
1247
+ 5000,
1248
+ 25000,
1249
+ 125000,
1250
+ 625000,
1251
+ 3125000,
1252
+ 3600000,
1253
+ 3600000,
1254
+ ]
1255
+ `)
1256
+ })
1257
+ })
1258
+
1259
+ describe('cursorBlock', () => {
1260
+ test('decodes one- and two-part cursors and rejects the rest', () => {
1261
+ expect(Webhooks.cursorBlock(null)).toBeUndefined()
1262
+ expect(Webhooks.cursorBlock('not-a-cursor')).toBeUndefined()
1263
+ expect(Webhooks.cursorBlock(Cursor.encode([123]))).toBe(123)
1264
+ expect(Webhooks.cursorBlock(Cursor.encode([456, 7]))).toBe(456)
1265
+ })
1266
+ })
1267
+
1268
+ describe('dueQueueEvents', () => {
1269
+ beforeEach(() => {
1270
+ vi.useFakeTimers({ toFake: ['Date'] })
1271
+ vi.setSystemTime(fixedNow())
1272
+ })
1273
+
1274
+ afterEach(() => {
1275
+ vi.useRealTimers()
1276
+ })
1277
+
1278
+ test('lists due pending work and skips in-flight, scheduled, and terminal rows', async () => {
1279
+ const db = TestApp.database()
1280
+ // Distinct staging times so the due order (next_attempt_at) is deterministic.
1281
+ const due = await stagedObligation(db, 1)
1282
+ vi.setSystemTime(new Date(fixedNow().getTime() + 1))
1283
+ const inFlight = await stagedObligation(db, 2)
1284
+ vi.setSystemTime(new Date(fixedNow().getTime() + 2))
1285
+ const scheduled = await stagedObligation(db, 3)
1286
+ vi.setSystemTime(new Date(fixedNow().getTime() + 3))
1287
+ const terminal = await stagedObligation(db, 4)
1288
+
1289
+ expect((await Webhooks.claimQueueEvent(db, inFlight.reference)).type).toBe('claimed')
1290
+ const scheduledClaim = await Webhooks.claimQueueEvent(db, scheduled.reference)
1291
+ expect(scheduledClaim.type).toBe('claimed')
1292
+ if (scheduledClaim.type !== 'claimed') return
1293
+ expect(
1294
+ await Webhooks.scheduleQueueEventRetry(db, scheduled.reference, {
1295
+ attemptCount: 3,
1296
+ claimedAt: scheduledClaim.record.attemptingAt!,
1297
+ }),
1298
+ ).toBe(true)
1299
+ const terminalClaim = await Webhooks.claimQueueEvent(db, terminal.reference)
1300
+ expect(terminalClaim.type).toBe('claimed')
1301
+ if (terminalClaim.type !== 'claimed') return
1302
+ expect(
1303
+ await Webhooks.completeQueueEvent(db, terminal.reference, {
1304
+ claimedAt: terminalClaim.record.attemptingAt!,
1305
+ status: 'succeeded',
1306
+ }),
1307
+ ).toBe(true)
1308
+
1309
+ await expect(Webhooks.dueQueueEvents(db, 10)).resolves.toEqual([due.reference])
1310
+
1311
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryAttemptStaleMs + 10))
1312
+ await expect(Webhooks.dueQueueEvents(db, 10)).resolves.toEqual([
1313
+ due.reference,
1314
+ inFlight.reference,
1315
+ ])
1316
+ })
1317
+ })
1318
+
1319
+ describe('pendingQueueEvents', () => {
1320
+ beforeEach(() => {
1321
+ vi.useFakeTimers({ toFake: ['Date'] })
1322
+ vi.setSystemTime(fixedNow())
1323
+ })
1324
+
1325
+ afterEach(() => {
1326
+ vi.useRealTimers()
1327
+ })
1328
+
1329
+ test('counts pending obligations and reports the oldest staging time', async () => {
1330
+ const db = TestApp.database()
1331
+ await stagedObligation(db, 1)
1332
+ vi.setSystemTime(new Date(fixedNow().getTime() + 1_000))
1333
+ const done = await stagedObligation(db, 2)
1334
+ const doneClaim = await Webhooks.claimQueueEvent(db, done.reference)
1335
+ expect(doneClaim.type).toBe('claimed')
1336
+ if (doneClaim.type !== 'claimed') return
1337
+ expect(
1338
+ await Webhooks.completeQueueEvent(db, done.reference, {
1339
+ claimedAt: doneClaim.record.attemptingAt!,
1340
+ status: 'succeeded',
1341
+ }),
1342
+ ).toBe(true)
1343
+
1344
+ await expect(Webhooks.pendingQueueEvents(db)).resolves.toEqual({
1345
+ count: 1,
1346
+ dueCount: 1,
1347
+ oldestAt: fixedNow().toISOString(),
1348
+ oldestDueAt: fixedNow().toISOString(),
1349
+ })
1350
+ })
1351
+
1352
+ test('excludes in-flight claims and scheduled retries from due work', async () => {
1353
+ const db = TestApp.database()
1354
+ const inFlight = await stagedObligation(db, 1)
1355
+ const claim = await Webhooks.claimQueueEvent(db, inFlight.reference)
1356
+ expect(claim.type).toBe('claimed')
1357
+
1358
+ await expect(Webhooks.pendingQueueEvents(db)).resolves.toMatchObject({
1359
+ count: 1,
1360
+ dueCount: 0,
1361
+ oldestDueAt: null,
1362
+ })
1363
+ })
1364
+ })
1365
+
1366
+ describe('deliverClaimedAndRecord', () => {
1367
+ test('delivers a claimed envelope and skips an inactive subscription', async () => {
1368
+ const db = TestApp.database()
1369
+ const { envelope, reference, subscription } = await stagedObligation(db)
1370
+ const claim = await Webhooks.claimQueueEvent(db, reference)
1371
+ expect(claim.type).toBe('claimed')
1372
+ if (claim.type !== 'claimed') return
1373
+
1374
+ let delivered: unknown
1375
+ const fetch = (async (_url, init) => {
1376
+ delivered = JSON.parse(typeof init?.body === 'string' ? init.body : '{}')
1377
+ return new Response(null, { status: 200 })
1378
+ }) satisfies typeof globalThis.fetch
1379
+
1380
+ const delivery = await Webhooks.deliverClaimedAndRecord(
1381
+ db,
1382
+ { envelope: claim.record.envelope, subscriptionId: reference.subscriptionId },
1383
+ { fetch },
1384
+ )
1385
+ expect(delivery.status).toBe('delivered')
1386
+ if (delivery.status !== 'delivered') return
1387
+ expect(delivery.result.ok).toBe(true)
1388
+ expect(delivered).toEqual(envelope)
1389
+
1390
+ await Webhooks.deleteSubscription(db, apiKeyOwner, subscription.id)
1391
+ await expect(
1392
+ Webhooks.deliverClaimedAndRecord(db, {
1393
+ envelope: claim.record.envelope,
1394
+ subscriptionId: reference.subscriptionId,
1395
+ }),
1396
+ ).resolves.toEqual({ status: 'skipped' })
1397
+ })
1398
+ })
1399
+
912
1400
  describe('buildPingEnvelope', () => {
913
1401
  test('builds a synthetic ping envelope with a deterministic id from the nonce', async () => {
914
1402
  const db = TestApp.database()