tapimo 0.0.0 → 0.0.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 (422) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/dist/ApiKey.d.ts +65 -0
  4. package/dist/ApiKey.d.ts.map +1 -0
  5. package/dist/ApiKey.js +105 -0
  6. package/dist/ApiKey.js.map +1 -0
  7. package/dist/ApiKeySource.d.ts +166 -0
  8. package/dist/ApiKeySource.d.ts.map +1 -0
  9. package/dist/ApiKeySource.js +234 -0
  10. package/dist/ApiKeySource.js.map +1 -0
  11. package/dist/App.d.ts +19317 -0
  12. package/dist/App.d.ts.map +1 -0
  13. package/dist/App.js +612 -0
  14. package/dist/App.js.map +1 -0
  15. package/dist/Assets.d.ts +44 -0
  16. package/dist/Assets.d.ts.map +1 -0
  17. package/dist/Assets.js +23 -0
  18. package/dist/Assets.js.map +1 -0
  19. package/dist/Auth.d.ts +2 -0
  20. package/dist/Auth.d.ts.map +1 -0
  21. package/dist/Auth.js +2 -0
  22. package/dist/Auth.js.map +1 -0
  23. package/dist/Cli.d.ts +21 -0
  24. package/dist/Cli.d.ts.map +1 -0
  25. package/dist/Cli.js +13 -0
  26. package/dist/Cli.js.map +1 -0
  27. package/dist/Client.d.ts +20 -0
  28. package/dist/Client.d.ts.map +1 -0
  29. package/dist/Client.js +24 -0
  30. package/dist/Client.js.map +1 -0
  31. package/dist/Metrics.d.ts +70 -0
  32. package/dist/Metrics.d.ts.map +1 -0
  33. package/dist/Metrics.js +91 -0
  34. package/dist/Metrics.js.map +1 -0
  35. package/dist/Scope.d.ts +35 -0
  36. package/dist/Scope.d.ts.map +1 -0
  37. package/dist/Scope.js +38 -0
  38. package/dist/Scope.js.map +1 -0
  39. package/dist/Webhooks.d.ts +79 -0
  40. package/dist/Webhooks.d.ts.map +1 -0
  41. package/dist/Webhooks.js +107 -0
  42. package/dist/Webhooks.js.map +1 -0
  43. package/dist/admin/Access.d.ts +41 -0
  44. package/dist/admin/Access.d.ts.map +1 -0
  45. package/dist/admin/Access.js +75 -0
  46. package/dist/admin/Access.js.map +1 -0
  47. package/dist/admin/App.d.ts +1221 -0
  48. package/dist/admin/App.d.ts.map +1 -0
  49. package/dist/admin/App.js +113 -0
  50. package/dist/admin/App.js.map +1 -0
  51. package/dist/admin/apps/api-keys.d.ts +209 -0
  52. package/dist/admin/apps/api-keys.d.ts.map +1 -0
  53. package/dist/admin/apps/api-keys.js +129 -0
  54. package/dist/admin/apps/api-keys.js.map +1 -0
  55. package/dist/admin/apps/scopes.d.ts +46 -0
  56. package/dist/admin/apps/scopes.d.ts.map +1 -0
  57. package/dist/admin/apps/scopes.js +41 -0
  58. package/dist/admin/apps/scopes.js.map +1 -0
  59. package/dist/admin/apps/verified-tokens.d.ts +1206 -0
  60. package/dist/admin/apps/verified-tokens.d.ts.map +1 -0
  61. package/dist/admin/apps/verified-tokens.js +485 -0
  62. package/dist/admin/apps/verified-tokens.js.map +1 -0
  63. package/dist/admin/index.d.ts +3 -0
  64. package/dist/admin/index.d.ts.map +1 -0
  65. package/dist/admin/index.js +3 -0
  66. package/dist/admin/index.js.map +1 -0
  67. package/dist/admin/ui.generated.d.ts +11 -0
  68. package/dist/admin/ui.generated.d.ts.map +1 -0
  69. package/dist/admin/ui.generated.js +43 -0
  70. package/dist/admin/ui.generated.js.map +1 -0
  71. package/dist/apps/activities.d.ts +18725 -0
  72. package/dist/apps/activities.d.ts.map +1 -0
  73. package/dist/apps/activities.js +3138 -0
  74. package/dist/apps/activities.js.map +1 -0
  75. package/dist/apps/addresses.d.ts +3662 -0
  76. package/dist/apps/addresses.d.ts.map +1 -0
  77. package/dist/apps/addresses.js +22 -0
  78. package/dist/apps/addresses.js.map +1 -0
  79. package/dist/apps/balances.d.ts +342 -0
  80. package/dist/apps/balances.d.ts.map +1 -0
  81. package/dist/apps/balances.js +393 -0
  82. package/dist/apps/balances.js.map +1 -0
  83. package/dist/apps/blocks.d.ts +931 -0
  84. package/dist/apps/blocks.d.ts.map +1 -0
  85. package/dist/apps/blocks.js +689 -0
  86. package/dist/apps/blocks.js.map +1 -0
  87. package/dist/apps/coingecko.d.ts +1053 -0
  88. package/dist/apps/coingecko.d.ts.map +1 -0
  89. package/dist/apps/coingecko.js +1015 -0
  90. package/dist/apps/coingecko.js.map +1 -0
  91. package/dist/apps/exchanges.d.ts +3000 -0
  92. package/dist/apps/exchanges.d.ts.map +1 -0
  93. package/dist/apps/exchanges.js +3570 -0
  94. package/dist/apps/exchanges.js.map +1 -0
  95. package/dist/apps/fee-amm.d.ts +640 -0
  96. package/dist/apps/fee-amm.d.ts.map +1 -0
  97. package/dist/apps/fee-amm.js +815 -0
  98. package/dist/apps/fee-amm.js.map +1 -0
  99. package/dist/apps/indexer.d.ts +116 -0
  100. package/dist/apps/indexer.d.ts.map +1 -0
  101. package/dist/apps/indexer.js +270 -0
  102. package/dist/apps/indexer.js.map +1 -0
  103. package/dist/apps/receipts.d.ts +1438 -0
  104. package/dist/apps/receipts.d.ts.map +1 -0
  105. package/dist/apps/receipts.js +874 -0
  106. package/dist/apps/receipts.js.map +1 -0
  107. package/dist/apps/rpc.d.ts +173 -0
  108. package/dist/apps/rpc.d.ts.map +1 -0
  109. package/dist/apps/rpc.js +191 -0
  110. package/dist/apps/rpc.js.map +1 -0
  111. package/dist/apps/tokenlist.d.ts +192 -0
  112. package/dist/apps/tokenlist.d.ts.map +1 -0
  113. package/dist/apps/tokenlist.js +153 -0
  114. package/dist/apps/tokenlist.js.map +1 -0
  115. package/dist/apps/tokens.d.ts +2246 -0
  116. package/dist/apps/tokens.d.ts.map +1 -0
  117. package/dist/apps/tokens.js +1789 -0
  118. package/dist/apps/tokens.js.map +1 -0
  119. package/dist/apps/transactions.d.ts +2327 -0
  120. package/dist/apps/transactions.d.ts.map +1 -0
  121. package/dist/apps/transactions.js +975 -0
  122. package/dist/apps/transactions.js.map +1 -0
  123. package/dist/apps/transfers.d.ts +589 -0
  124. package/dist/apps/transfers.d.ts.map +1 -0
  125. package/dist/apps/transfers.js +769 -0
  126. package/dist/apps/transfers.js.map +1 -0
  127. package/dist/apps/verified-tokens.d.ts +544 -0
  128. package/dist/apps/verified-tokens.d.ts.map +1 -0
  129. package/dist/apps/verified-tokens.js +234 -0
  130. package/dist/apps/verified-tokens.js.map +1 -0
  131. package/dist/apps/webhooks.d.ts +7684 -0
  132. package/dist/apps/webhooks.d.ts.map +1 -0
  133. package/dist/apps/webhooks.js +2112 -0
  134. package/dist/apps/webhooks.js.map +1 -0
  135. package/dist/cloudflare.d.ts +32 -0
  136. package/dist/cloudflare.d.ts.map +1 -0
  137. package/dist/cloudflare.js +45 -0
  138. package/dist/cloudflare.js.map +1 -0
  139. package/dist/index.d.ts +16 -0
  140. package/dist/index.d.ts.map +1 -0
  141. package/dist/index.js +16 -0
  142. package/dist/index.js.map +1 -0
  143. package/dist/internal/Auth.d.ts +298 -0
  144. package/dist/internal/Auth.d.ts.map +1 -0
  145. package/dist/internal/Auth.js +618 -0
  146. package/dist/internal/Auth.js.map +1 -0
  147. package/dist/internal/Cache.d.ts +74 -0
  148. package/dist/internal/Cache.d.ts.map +1 -0
  149. package/dist/internal/Cache.js +215 -0
  150. package/dist/internal/Cache.js.map +1 -0
  151. package/dist/internal/Cursor.d.ts +79 -0
  152. package/dist/internal/Cursor.d.ts.map +1 -0
  153. package/dist/internal/Cursor.js +116 -0
  154. package/dist/internal/Cursor.js.map +1 -0
  155. package/dist/internal/EdgeCache.d.ts +59 -0
  156. package/dist/internal/EdgeCache.d.ts.map +1 -0
  157. package/dist/internal/EdgeCache.js +174 -0
  158. package/dist/internal/EdgeCache.js.map +1 -0
  159. package/dist/internal/Log.d.ts +104 -0
  160. package/dist/internal/Log.d.ts.map +1 -0
  161. package/dist/internal/Log.js +118 -0
  162. package/dist/internal/Log.js.map +1 -0
  163. package/dist/internal/MetricSink.d.ts +65 -0
  164. package/dist/internal/MetricSink.d.ts.map +1 -0
  165. package/dist/internal/MetricSink.js +146 -0
  166. package/dist/internal/MetricSink.js.map +1 -0
  167. package/dist/internal/Mpp.d.ts +71 -0
  168. package/dist/internal/Mpp.d.ts.map +1 -0
  169. package/dist/internal/Mpp.js +105 -0
  170. package/dist/internal/Mpp.js.map +1 -0
  171. package/dist/internal/OpenApi.d.ts +313 -0
  172. package/dist/internal/OpenApi.d.ts.map +1 -0
  173. package/dist/internal/OpenApi.fonts.d.ts +2 -0
  174. package/dist/internal/OpenApi.fonts.d.ts.map +1 -0
  175. package/dist/internal/OpenApi.fonts.js +39 -0
  176. package/dist/internal/OpenApi.fonts.js.map +1 -0
  177. package/dist/internal/OpenApi.js +335 -0
  178. package/dist/internal/OpenApi.js.map +1 -0
  179. package/dist/internal/OpenApi.logo.d.ts +6 -0
  180. package/dist/internal/OpenApi.logo.d.ts.map +1 -0
  181. package/dist/internal/OpenApi.logo.js +19 -0
  182. package/dist/internal/OpenApi.logo.js.map +1 -0
  183. package/dist/internal/Path.d.ts +5 -0
  184. package/dist/internal/Path.d.ts.map +1 -0
  185. package/dist/internal/Path.js +16 -0
  186. package/dist/internal/Path.js.map +1 -0
  187. package/dist/internal/RateLimit.d.ts +60 -0
  188. package/dist/internal/RateLimit.d.ts.map +1 -0
  189. package/dist/internal/RateLimit.js +48 -0
  190. package/dist/internal/RateLimit.js.map +1 -0
  191. package/dist/internal/Response.d.ts +132 -0
  192. package/dist/internal/Response.d.ts.map +1 -0
  193. package/dist/internal/Response.js +72 -0
  194. package/dist/internal/Response.js.map +1 -0
  195. package/dist/internal/Schema.d.ts +257 -0
  196. package/dist/internal/Schema.d.ts.map +1 -0
  197. package/dist/internal/Schema.js +360 -0
  198. package/dist/internal/Schema.js.map +1 -0
  199. package/dist/internal/Store.d.ts +204 -0
  200. package/dist/internal/Store.d.ts.map +1 -0
  201. package/dist/internal/Store.js +291 -0
  202. package/dist/internal/Store.js.map +1 -0
  203. package/dist/internal/Tidx.d.ts +120 -0
  204. package/dist/internal/Tidx.d.ts.map +1 -0
  205. package/dist/internal/Tidx.js +156 -0
  206. package/dist/internal/Tidx.js.map +1 -0
  207. package/dist/internal/Timing.d.ts +22 -0
  208. package/dist/internal/Timing.d.ts.map +1 -0
  209. package/dist/internal/Timing.js +32 -0
  210. package/dist/internal/Timing.js.map +1 -0
  211. package/dist/internal/Ttl.d.ts +25 -0
  212. package/dist/internal/Ttl.d.ts.map +1 -0
  213. package/dist/internal/Ttl.js +49 -0
  214. package/dist/internal/Ttl.js.map +1 -0
  215. package/dist/internal/Value.d.ts +47 -0
  216. package/dist/internal/Value.d.ts.map +1 -0
  217. package/dist/internal/Value.js +122 -0
  218. package/dist/internal/Value.js.map +1 -0
  219. package/dist/internal/VerifiedTokens.d.ts +228 -0
  220. package/dist/internal/VerifiedTokens.d.ts.map +1 -0
  221. package/dist/internal/VerifiedTokens.js +450 -0
  222. package/dist/internal/VerifiedTokens.js.map +1 -0
  223. package/dist/internal/Viem.d.ts +2082 -0
  224. package/dist/internal/Viem.d.ts.map +1 -0
  225. package/dist/internal/Viem.js +63 -0
  226. package/dist/internal/Viem.js.map +1 -0
  227. package/dist/internal/WebhookDestination.d.ts +198 -0
  228. package/dist/internal/WebhookDestination.d.ts.map +1 -0
  229. package/dist/internal/WebhookDestination.js +170 -0
  230. package/dist/internal/WebhookDestination.js.map +1 -0
  231. package/dist/internal/Webhooks.d.ts +433 -0
  232. package/dist/internal/Webhooks.d.ts.map +1 -0
  233. package/dist/internal/Webhooks.js +489 -0
  234. package/dist/internal/Webhooks.js.map +1 -0
  235. package/dist/internal/webhookDestinations/betterStack.d.ts +26 -0
  236. package/dist/internal/webhookDestinations/betterStack.d.ts.map +1 -0
  237. package/dist/internal/webhookDestinations/betterStack.js +76 -0
  238. package/dist/internal/webhookDestinations/betterStack.js.map +1 -0
  239. package/dist/internal/webhookDestinations/slack.d.ts +59 -0
  240. package/dist/internal/webhookDestinations/slack.d.ts.map +1 -0
  241. package/dist/internal/webhookDestinations/slack.js +422 -0
  242. package/dist/internal/webhookDestinations/slack.js.map +1 -0
  243. package/dist/internal/webhookDestinations/url.d.ts +17 -0
  244. package/dist/internal/webhookDestinations/url.d.ts.map +1 -0
  245. package/dist/internal/webhookDestinations/url.js +130 -0
  246. package/dist/internal/webhookDestinations/url.js.map +1 -0
  247. package/dist/openrpc.json +5302 -0
  248. package/package.json +46 -4
  249. package/seeds/31318/icons/0x20c0000000000000000000000000000000000000.svg +1 -0
  250. package/seeds/31318/icons/0x20c0000000000000000000000000000000000001.svg +1 -0
  251. package/seeds/31318/icons/0x20c0000000000000000000000000000000000002.svg +1 -0
  252. package/seeds/31318/icons/0x20c0000000000000000000000000000000000003.svg +1 -0
  253. package/seeds/31318/verified-tokens.json +30 -0
  254. package/seeds/4217/icons/0x20c0000000000000000000000000000000000000.svg +25 -0
  255. package/seeds/4217/icons/0x20c000000000000000000000048c8f36df1c9a4a.svg +1 -0
  256. package/seeds/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg +1 -0
  257. package/seeds/4217/icons/0x20c0000000000000000000000a6da882d075a4c3.svg +4 -0
  258. package/seeds/4217/icons/0x20c000000000000000000000111111111e910f0f.svg +30 -0
  259. package/seeds/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg +1 -0
  260. package/seeds/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg +1 -0
  261. package/seeds/4217/icons/0x20c0000000000000000000002f52d5cc21a3207b.svg +1 -0
  262. package/seeds/4217/icons/0x20c0000000000000000000003158081efd85bfc2.svg +11 -0
  263. package/seeds/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg +1 -0
  264. package/seeds/4217/icons/0x20c000000000000000000000383a23bacb546ab9.svg +1 -0
  265. package/seeds/4217/icons/0x20c00000000000000000000042109aef2f8b28e1.png +0 -0
  266. package/seeds/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg +1 -0
  267. package/seeds/4217/icons/0x20c0000000000000000000006fd9a167923ba194.png +0 -0
  268. package/seeds/4217/icons/0x20c0000000000000000000007f7ba549dd0251b9.svg +1 -0
  269. package/seeds/4217/icons/0x20c0000000000000000000008191667423f70e67.svg +5 -0
  270. package/seeds/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg +1 -0
  271. package/seeds/4217/icons/0x20c0000000000000000000009a4a4b17e0dc6651.svg +1 -0
  272. package/seeds/4217/icons/0x20c000000000000000000000ab02d39df30bd17e.svg +1 -0
  273. package/seeds/4217/icons/0x20c000000000000000000000ae247a1130450f09.svg +1 -0
  274. package/seeds/4217/icons/0x20c000000000000000000000aeed2ec36a54d0e5.svg +1 -0
  275. package/seeds/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg +1 -0
  276. package/seeds/4217/icons/0x20c000000000000000000000bd95bfb69fbe6ce3.svg +1 -0
  277. package/seeds/4217/icons/0x20c000000000000000000000c412ec89d0c08be5.svg +1 -0
  278. package/seeds/4217/icons/0x20c000000000000000000000f047dd7018e50367.png +0 -0
  279. package/seeds/4217/verified-tokens.json +177 -0
  280. package/seeds/42431/icons/0x20c0000000000000000000000000000000000000.svg +25 -0
  281. package/seeds/42431/icons/0x20c0000000000000000000000000000000000001.svg +1 -0
  282. package/seeds/42431/icons/0x20c0000000000000000000000000000000000002.svg +1 -0
  283. package/seeds/42431/icons/0x20c0000000000000000000000000000000000003.svg +1 -0
  284. package/seeds/42431/icons/0x20c0000000000000000000009e8d7eb59b783726.svg +1 -0
  285. package/seeds/42431/icons/0x20c000000000000000000000d72572838bbee59c.svg +1 -0
  286. package/seeds/42431/verified-tokens.json +44 -0
  287. package/seeds/openrpc/README.md +27 -0
  288. package/seeds/openrpc/execution-apis.json +8989 -0
  289. package/src/ApiKey.test.ts +91 -0
  290. package/src/ApiKey.ts +122 -0
  291. package/src/ApiKeySource.test-d.ts +11 -0
  292. package/src/ApiKeySource.test.ts +295 -0
  293. package/src/ApiKeySource.ts +303 -0
  294. package/src/App.test-d.ts +38 -0
  295. package/src/App.test.ts +65927 -0
  296. package/src/App.ts +855 -0
  297. package/src/Assets.ts +58 -0
  298. package/src/Auth.ts +1 -0
  299. package/src/Cli.test.ts +81 -0
  300. package/src/Cli.ts +38 -0
  301. package/src/Client.test-d.ts +1438 -0
  302. package/src/Client.test.ts +134 -0
  303. package/src/Client.ts +45 -0
  304. package/src/Metrics.ts +154 -0
  305. package/src/Scope.ts +50 -0
  306. package/src/Webhooks.test.ts +49 -0
  307. package/src/Webhooks.ts +172 -0
  308. package/src/admin/Access.test.ts +130 -0
  309. package/src/admin/Access.ts +87 -0
  310. package/src/admin/App.test.ts +225 -0
  311. package/src/admin/App.ts +216 -0
  312. package/src/admin/apps/api-keys.test.ts +156 -0
  313. package/src/admin/apps/api-keys.ts +157 -0
  314. package/src/admin/apps/scopes.test.ts +52 -0
  315. package/src/admin/apps/scopes.ts +47 -0
  316. package/src/admin/apps/verified-tokens.test.ts +415 -0
  317. package/src/admin/apps/verified-tokens.ts +566 -0
  318. package/src/admin/index.ts +2 -0
  319. package/src/admin/ui/plugin.ts +135 -0
  320. package/src/admin/ui/src/components/Checkbox.tsx +22 -0
  321. package/src/admin/ui/src/components/Dialog.tsx +32 -0
  322. package/src/admin/ui/src/components/MobileProfile.tsx +31 -0
  323. package/src/admin/ui/src/components/Page.tsx +53 -0
  324. package/src/admin/ui/src/components/TempoLogo.tsx +37 -0
  325. package/src/admin/ui/src/fonts/Pilat-Demi.woff2 +0 -0
  326. package/src/admin/ui/src/fonts/Pilat-Regular.woff2 +0 -0
  327. package/src/admin/ui/src/lib/api-keys.ts +63 -0
  328. package/src/admin/ui/src/lib/client.ts +15 -0
  329. package/src/admin/ui/src/lib/config.ts +57 -0
  330. package/src/admin/ui/src/lib/query-client.ts +26 -0
  331. package/src/admin/ui/src/lib/scopes.ts +25 -0
  332. package/src/admin/ui/src/lib/verified-tokens.ts +89 -0
  333. package/src/admin/ui/src/lib/wagmi.ts +18 -0
  334. package/src/admin/ui/src/routeTree.gen.ts +104 -0
  335. package/src/admin/ui/src/router.tsx +25 -0
  336. package/src/admin/ui/src/routes/__root.tsx +219 -0
  337. package/src/admin/ui/src/routes/api-keys.tsx +329 -0
  338. package/src/admin/ui/src/routes/index.tsx +7 -0
  339. package/src/admin/ui/src/routes/verified-tokens.tsx +331 -0
  340. package/src/admin/ui/src/styles.css +20 -0
  341. package/src/admin/ui/tsconfig.json +16 -0
  342. package/src/admin/ui/vite.config.ts +29 -0
  343. package/src/admin/ui.generated.ts +60 -0
  344. package/src/apps/activities.test.ts +1020 -0
  345. package/src/apps/activities.ts +3934 -0
  346. package/src/apps/addresses.ts +24 -0
  347. package/src/apps/balances.test.ts +274 -0
  348. package/src/apps/balances.ts +533 -0
  349. package/src/apps/blocks.test.ts +385 -0
  350. package/src/apps/blocks.ts +896 -0
  351. package/src/apps/coingecko.test.ts +218 -0
  352. package/src/apps/coingecko.ts +1383 -0
  353. package/src/apps/exchanges.test.ts +958 -0
  354. package/src/apps/exchanges.ts +4773 -0
  355. package/src/apps/fee-amm.test.ts +321 -0
  356. package/src/apps/fee-amm.ts +1121 -0
  357. package/src/apps/indexer.test.ts +153 -0
  358. package/src/apps/indexer.ts +330 -0
  359. package/src/apps/receipts.test.ts +285 -0
  360. package/src/apps/receipts.ts +1193 -0
  361. package/src/apps/rpc.test.ts +302 -0
  362. package/src/apps/rpc.ts +222 -0
  363. package/src/apps/tokenlist.test.ts +80 -0
  364. package/src/apps/tokenlist.ts +207 -0
  365. package/src/apps/tokens.test.ts +1062 -0
  366. package/src/apps/tokens.ts +2292 -0
  367. package/src/apps/transactions.test.ts +408 -0
  368. package/src/apps/transactions.ts +1363 -0
  369. package/src/apps/transfers.test.ts +371 -0
  370. package/src/apps/transfers.ts +1116 -0
  371. package/src/apps/verified-tokens.test.ts +114 -0
  372. package/src/apps/verified-tokens.ts +295 -0
  373. package/src/apps/webhooks.test.ts +1937 -0
  374. package/src/apps/webhooks.ts +2926 -0
  375. package/src/cloudflare.ts +55 -0
  376. package/src/index.ts +15 -0
  377. package/src/internal/Auth.test.ts +980 -0
  378. package/src/internal/Auth.ts +1012 -0
  379. package/src/internal/Cache.test.ts +240 -0
  380. package/src/internal/Cache.ts +262 -0
  381. package/src/internal/Cursor.test.ts +100 -0
  382. package/src/internal/Cursor.ts +149 -0
  383. package/src/internal/EdgeCache.test.ts +234 -0
  384. package/src/internal/EdgeCache.ts +224 -0
  385. package/src/internal/Errors.ts +14 -0
  386. package/src/internal/Log.test.ts +244 -0
  387. package/src/internal/Log.ts +216 -0
  388. package/src/internal/MetricSink.ts +220 -0
  389. package/src/internal/Mpp.test.ts +64 -0
  390. package/src/internal/Mpp.ts +141 -0
  391. package/src/internal/OpenApi.fonts.ts +38 -0
  392. package/src/internal/OpenApi.logo.ts +18 -0
  393. package/src/internal/OpenApi.ts +488 -0
  394. package/src/internal/Path.test.ts +14 -0
  395. package/src/internal/Path.ts +15 -0
  396. package/src/internal/RateLimit.test.ts +93 -0
  397. package/src/internal/RateLimit.ts +93 -0
  398. package/src/internal/Response.ts +131 -0
  399. package/src/internal/Schema.test.ts +34 -0
  400. package/src/internal/Schema.ts +523 -0
  401. package/src/internal/Store.test.ts +545 -0
  402. package/src/internal/Store.ts +526 -0
  403. package/src/internal/Tidx.local.test.ts +33 -0
  404. package/src/internal/Tidx.test.ts +294 -0
  405. package/src/internal/Tidx.ts +238 -0
  406. package/src/internal/Timing.test.ts +22 -0
  407. package/src/internal/Timing.ts +62 -0
  408. package/src/internal/Ttl.test.ts +18 -0
  409. package/src/internal/Ttl.ts +59 -0
  410. package/src/internal/Value.ts +114 -0
  411. package/src/internal/VerifiedTokens.test.ts +333 -0
  412. package/src/internal/VerifiedTokens.ts +645 -0
  413. package/src/internal/Viem.test.ts +115 -0
  414. package/src/internal/Viem.ts +165 -0
  415. package/src/internal/WebhookDestination.test.ts +648 -0
  416. package/src/internal/WebhookDestination.ts +331 -0
  417. package/src/internal/Webhooks.test.ts +668 -0
  418. package/src/internal/Webhooks.ts +940 -0
  419. package/src/internal/webhookDestinations/betterStack.ts +91 -0
  420. package/src/internal/webhookDestinations/slack.ts +496 -0
  421. package/src/internal/webhookDestinations/url.ts +128 -0
  422. package/src/openrpc.json +5302 -0
@@ -0,0 +1,668 @@
1
+ import { Store, Webhooks } from 'cadent'
2
+
3
+ const apiKeyOwner = { orgId: 'org_1', type: 'api_key' } as const
4
+ const mppOwner = { payer: 'did:pkh:eip155:1:0xabc', type: 'mpp' } as const
5
+
6
+ /** Fixed clock so created/updated timestamps snapshot deterministically. */
7
+ const fixedNow = () => new Date('2026-01-01T00:00:00.000Z')
8
+
9
+ /** Asserts the random id/secret shape, then redacts them for a stable snapshot. */
10
+ function redact(subscription: Webhooks.Subscription | null) {
11
+ expect(subscription).not.toBeNull()
12
+ expect(subscription?.id).toMatch(/^wh_\d{15}_[A-Za-z0-9_-]{21}$/)
13
+ expect(subscription?.secret).toMatch(/^whsec_[0-9a-f]{64}$/)
14
+ return { ...subscription, id: '<id>', secret: '<secret>' }
15
+ }
16
+
17
+ function createInput(overrides: Partial<Webhooks.CreateInput> = {}): Webhooks.CreateInput {
18
+ return {
19
+ chainId: 4217,
20
+ destination: { type: 'url', url: 'https://hooks.example.com/endpoint' },
21
+ eventType: 'token:transfer',
22
+ owner: apiKeyOwner,
23
+ ...overrides,
24
+ }
25
+ }
26
+
27
+ describe('createSubscription', () => {
28
+ test('persists a subscription with generated id and secret', async () => {
29
+ const store = Store.memory()
30
+ const subscription = await Webhooks.createSubscription(store, createInput(), { now: fixedNow })
31
+
32
+ expect(redact(subscription)).toMatchInlineSnapshot(`
33
+ {
34
+ "chainId": 4217,
35
+ "createdAt": "2026-01-01T00:00:00.000Z",
36
+ "destination": {
37
+ "type": "url",
38
+ "url": "https://hooks.example.com/endpoint",
39
+ },
40
+ "eventType": "token:transfer",
41
+ "failureCount": 0,
42
+ "filters": {},
43
+ "id": "<id>",
44
+ "owner": {
45
+ "orgId": "org_1",
46
+ "type": "api_key",
47
+ },
48
+ "secret": "<secret>",
49
+ "status": "active",
50
+ "updatedAt": "2026-01-01T00:00:00.000Z",
51
+ }
52
+ `)
53
+
54
+ const read = await Webhooks.getSubscription(store, apiKeyOwner, subscription.id)
55
+ expect(read).toEqual(subscription)
56
+ })
57
+
58
+ test('sets expiresAt from ttl for MPP-owned subscriptions', async () => {
59
+ const store = Store.memory()
60
+ const subscription = await Webhooks.createSubscription(
61
+ store,
62
+ createInput({ owner: mppOwner, ttl: 60_000 }),
63
+ { now: fixedNow },
64
+ )
65
+
66
+ expect(subscription.expiresAt).toMatchInlineSnapshot(`"2026-01-01T00:01:00.000Z"`)
67
+ })
68
+
69
+ test('enforces a per-owner cap', async () => {
70
+ const store = Store.memory()
71
+ await Webhooks.createSubscription(store, createInput(), { maxPerOwner: 1 })
72
+
73
+ await expect(
74
+ Webhooks.createSubscription(store, createInput(), { maxPerOwner: 1 }),
75
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
76
+ `[Webhooks.LimitExceededError: Webhook subscription limit reached (1).]`,
77
+ )
78
+ })
79
+
80
+ test('rejects an undeliverable URL before persisting', async () => {
81
+ const store = Store.memory()
82
+ await expect(
83
+ Webhooks.createSubscription(
84
+ store,
85
+ createInput({ destination: { type: 'url', url: 'http://localhost/x' } }),
86
+ ),
87
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
88
+ `[Webhooks.InvalidUrlError: Webhook URL rejected (protocol): http://localhost/x]`,
89
+ )
90
+
91
+ expect(await Webhooks.listSubscriptions(store, apiKeyOwner)).toMatchInlineSnapshot(`[]`)
92
+ })
93
+ })
94
+
95
+ describe('owner isolation', () => {
96
+ test('an owner cannot read or list another owner subscriptions', async () => {
97
+ const store = Store.memory()
98
+ const mine = await Webhooks.createSubscription(store, createInput({ owner: apiKeyOwner }))
99
+ await Webhooks.createSubscription(store, createInput({ owner: mppOwner }))
100
+
101
+ expect(await Webhooks.getSubscription(store, mppOwner, mine.id)).toMatchInlineSnapshot(`null`)
102
+
103
+ const apiKeyList = await Webhooks.listSubscriptions(store, apiKeyOwner)
104
+ expect(apiKeyList.map((s) => s.id)).toEqual([mine.id])
105
+
106
+ const mppList = await Webhooks.listSubscriptions(store, mppOwner)
107
+ expect(mppList).toHaveLength(1)
108
+ expect(mppList[0]?.id).not.toBe(mine.id)
109
+ })
110
+
111
+ test('an owner cannot delete another owner subscription', async () => {
112
+ const store = Store.memory()
113
+ const mine = await Webhooks.createSubscription(store, createInput({ owner: apiKeyOwner }))
114
+
115
+ expect(await Webhooks.deleteSubscription(store, mppOwner, mine.id)).toMatchInlineSnapshot(
116
+ `false`,
117
+ )
118
+ expect(await Webhooks.getSubscription(store, apiKeyOwner, mine.id)).not.toBeNull()
119
+ })
120
+ })
121
+
122
+ describe('listSubscriptions', () => {
123
+ test('lists subscriptions newest-first with keyset paging', async () => {
124
+ const store = Store.memory()
125
+ const created = []
126
+ for (const minute of [0, 1, 2]) {
127
+ const now = () => new Date(`2026-01-01T00:0${minute}:00.000Z`)
128
+ created.push(await Webhooks.createSubscription(store, createInput(), { now }))
129
+ }
130
+ const [first, second, third] = created
131
+
132
+ // Newest first: most recently created subscription leads.
133
+ const all = await Webhooks.listSubscriptions(store, apiKeyOwner)
134
+ expect(all.map((s) => s.id)).toEqual([third!.id, second!.id, first!.id])
135
+
136
+ // First page of two, then resume after the last id for the older remainder.
137
+ const page = await Webhooks.listSubscriptions(store, apiKeyOwner, { limit: 2 })
138
+ expect(page.map((s) => s.id)).toEqual([third!.id, second!.id])
139
+
140
+ const next = await Webhooks.listSubscriptions(store, apiKeyOwner, {
141
+ cursor: page.at(-1)?.id,
142
+ limit: 2,
143
+ })
144
+ expect(next.map((s) => s.id)).toEqual([first!.id])
145
+ })
146
+ })
147
+
148
+ describe('updateSubscription', () => {
149
+ test('patches fields and bumps updatedAt', async () => {
150
+ const store = Store.memory()
151
+ const created = await Webhooks.createSubscription(store, createInput(), { now: fixedNow })
152
+
153
+ const updated = await Webhooks.updateSubscription(
154
+ store,
155
+ apiKeyOwner,
156
+ created.id,
157
+ {
158
+ destination: { type: 'url', url: 'https://hooks.example.com/v2' },
159
+ filters: { token: '0xtoken' },
160
+ },
161
+ { now: () => new Date('2026-01-02T00:00:00.000Z') },
162
+ )
163
+
164
+ expect(redact(updated)).toMatchInlineSnapshot(`
165
+ {
166
+ "chainId": 4217,
167
+ "createdAt": "2026-01-01T00:00:00.000Z",
168
+ "destination": {
169
+ "type": "url",
170
+ "url": "https://hooks.example.com/v2",
171
+ },
172
+ "eventType": "token:transfer",
173
+ "failureCount": 0,
174
+ "filters": {
175
+ "token": "0xtoken",
176
+ },
177
+ "id": "<id>",
178
+ "owner": {
179
+ "orgId": "org_1",
180
+ "type": "api_key",
181
+ },
182
+ "secret": "<secret>",
183
+ "status": "active",
184
+ "updatedAt": "2026-01-02T00:00:00.000Z",
185
+ }
186
+ `)
187
+ })
188
+
189
+ test('returns null for a missing subscription', async () => {
190
+ const store = Store.memory()
191
+ expect(
192
+ await Webhooks.updateSubscription(store, apiKeyOwner, 'wh_missing', { status: 'paused' }),
193
+ ).toMatchInlineSnapshot(`null`)
194
+ })
195
+
196
+ test('rejects a patched URL that fails validation', async () => {
197
+ const store = Store.memory()
198
+ const created = await Webhooks.createSubscription(store, createInput())
199
+
200
+ await expect(
201
+ Webhooks.updateSubscription(store, apiKeyOwner, created.id, {
202
+ destination: { type: 'url', url: 'http://10.0.0.1' },
203
+ }),
204
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
205
+ `[Webhooks.InvalidUrlError: Webhook URL rejected (protocol): http://10.0.0.1]`,
206
+ )
207
+ })
208
+ })
209
+
210
+ describe('listActive', () => {
211
+ test('returns only active subscriptions for a stream', async () => {
212
+ const store = Store.memory()
213
+ const a = await Webhooks.createSubscription(store, createInput())
214
+ const b = await Webhooks.createSubscription(store, createInput({ owner: mppOwner }))
215
+ await Webhooks.createSubscription(store, createInput({ eventType: 'transaction:included' }))
216
+
217
+ let active = await Webhooks.listActive(store, 4217, 'token:transfer')
218
+ expect(active.map((s) => s.id).sort()).toEqual([a.id, b.id].sort())
219
+
220
+ await Webhooks.updateSubscription(store, apiKeyOwner, a.id, { status: 'paused' })
221
+ active = await Webhooks.listActive(store, 4217, 'token:transfer')
222
+ expect(active.map((s) => s.id)).toEqual([b.id])
223
+ })
224
+
225
+ test('drops a deleted subscription from the index', async () => {
226
+ const store = Store.memory()
227
+ const a = await Webhooks.createSubscription(store, createInput())
228
+
229
+ await Webhooks.deleteSubscription(store, apiKeyOwner, a.id)
230
+ expect(await Webhooks.listActive(store, 4217, 'token:transfer')).toMatchInlineSnapshot(`[]`)
231
+ })
232
+ })
233
+
234
+ describe('cursors', () => {
235
+ test('round-trips a per-subscription cursor', async () => {
236
+ const store = Store.memory()
237
+ expect(await Webhooks.getCursor(store, 'wh_1')).toMatchInlineSnapshot(`null`)
238
+
239
+ await Webhooks.setCursor(store, 'wh_1', '12345:7')
240
+ expect(await Webhooks.getCursor(store, 'wh_1')).toMatchInlineSnapshot(`"12345:7"`)
241
+ })
242
+ })
243
+
244
+ describe('recordSuccess / recordFailure', () => {
245
+ test('recordSuccess resets failures and stamps lastDeliveryAt', async () => {
246
+ const store = Store.memory()
247
+ const created = await Webhooks.createSubscription(store, createInput())
248
+ const failed = await Webhooks.recordFailure(store, created, { now: fixedNow })
249
+ expect(failed.failureCount).toMatchInlineSnapshot(`1`)
250
+
251
+ const ok = await Webhooks.recordSuccess(store, failed, { now: fixedNow })
252
+ expect({ failureCount: ok.failureCount, lastDeliveryAt: ok.lastDeliveryAt })
253
+ .toMatchInlineSnapshot(`
254
+ {
255
+ "failureCount": 0,
256
+ "lastDeliveryAt": "2026-01-01T00:00:00.000Z",
257
+ }
258
+ `)
259
+
260
+ // Persisted, not just returned.
261
+ const read = await Webhooks.getSubscription(store, apiKeyOwner, created.id)
262
+ expect(read?.lastDeliveryAt).toBe('2026-01-01T00:00:00.000Z')
263
+ })
264
+
265
+ test('recordFailure auto-disables at the threshold and drops from the active index', async () => {
266
+ const store = Store.memory()
267
+ let subscription = await Webhooks.createSubscription(store, createInput())
268
+
269
+ // Below the threshold: stays active.
270
+ subscription = await Webhooks.recordFailure(store, subscription, { maxFailures: 3 })
271
+ subscription = await Webhooks.recordFailure(store, subscription, { maxFailures: 3 })
272
+ expect(subscription.status).toMatchInlineSnapshot(`"active"`)
273
+ expect((await Webhooks.listActive(store, 4217, 'token:transfer')).map((s) => s.id)).toEqual([
274
+ subscription.id,
275
+ ])
276
+
277
+ // Hitting the threshold disables it and removes it from the poller's index.
278
+ subscription = await Webhooks.recordFailure(store, subscription, { maxFailures: 3 })
279
+ expect({ failureCount: subscription.failureCount, status: subscription.status })
280
+ .toMatchInlineSnapshot(`
281
+ {
282
+ "failureCount": 3,
283
+ "status": "disabled",
284
+ }
285
+ `)
286
+ expect(await Webhooks.listActive(store, 4217, 'token:transfer')).toMatchInlineSnapshot(`[]`)
287
+ })
288
+ })
289
+
290
+ describe('sign / verify', () => {
291
+ test('signs a body deterministically', () => {
292
+ const signature = Webhooks.sign({
293
+ body: JSON.stringify({ hello: 'world' }),
294
+ secret: 'whsec_test',
295
+ timestamp: 1_000,
296
+ })
297
+
298
+ expect(signature).toMatchInlineSnapshot(
299
+ `"t=1000,v1=b345b1f6668bbb1b5f1a02cc5a2de10334d1281ba860bbdeadf4681c4719563d"`,
300
+ )
301
+ })
302
+
303
+ test('verifies a freshly signed body', () => {
304
+ const secret = 'whsec_test'
305
+ const body = JSON.stringify({ hello: 'world' })
306
+ const signature = Webhooks.sign({ body, secret, timestamp: 1_000 })
307
+
308
+ expect(Webhooks.verify({ body, now: 1_000, secret, signature })).toMatchInlineSnapshot(`true`)
309
+ })
310
+
311
+ test('rejects a tampered body', () => {
312
+ const secret = 'whsec_test'
313
+ const signature = Webhooks.sign({ body: 'original', secret, timestamp: 1_000 })
314
+
315
+ expect(
316
+ Webhooks.verify({ body: 'tampered', now: 1_000, secret, signature }),
317
+ ).toMatchInlineSnapshot(`false`)
318
+ })
319
+
320
+ test('rejects the wrong secret', () => {
321
+ const signature = Webhooks.sign({ body: 'x', secret: 'a', timestamp: 1_000 })
322
+ expect(
323
+ Webhooks.verify({ body: 'x', now: 1_000, secret: 'b', signature }),
324
+ ).toMatchInlineSnapshot(`false`)
325
+ })
326
+
327
+ test('rejects a stale signature beyond tolerance', () => {
328
+ const secret = 'whsec_test'
329
+ const signature = Webhooks.sign({ body: 'x', secret, timestamp: 1_000 })
330
+
331
+ expect(
332
+ Webhooks.verify({ body: 'x', now: 2_000, secret, signature, tolerance: 300 }),
333
+ ).toMatchInlineSnapshot(`false`)
334
+ expect(
335
+ Webhooks.verify({ body: 'x', now: 1_200, secret, signature, tolerance: 300 }),
336
+ ).toMatchInlineSnapshot(`true`)
337
+ })
338
+
339
+ test('rejects a malformed signature', () => {
340
+ expect(
341
+ Webhooks.verify({ body: 'x', secret: 'a', signature: 'nonsense' }),
342
+ ).toMatchInlineSnapshot(`false`)
343
+ })
344
+ })
345
+
346
+ describe('eventId / buildEnvelope', () => {
347
+ test('eventId is stable for the same coordinates', () => {
348
+ const a = Webhooks.eventId({
349
+ blockNumber: 100,
350
+ chainId: 4217,
351
+ eventType: 'token:transfer',
352
+ logIndex: 3,
353
+ })
354
+ const b = Webhooks.eventId({
355
+ blockNumber: 100,
356
+ chainId: 4217,
357
+ eventType: 'token:transfer',
358
+ logIndex: 3,
359
+ })
360
+ const different = Webhooks.eventId({
361
+ blockNumber: 100,
362
+ chainId: 4217,
363
+ eventType: 'token:transfer',
364
+ logIndex: 4,
365
+ })
366
+
367
+ expect(a).toMatchInlineSnapshot(
368
+ `"evt_d7c75afd0abccdc90a391d9313762a957c7e4383cac1fae76e5de56e49c9d3f8"`,
369
+ )
370
+ expect(a).toBe(b)
371
+ expect(a).not.toBe(different)
372
+ })
373
+
374
+ test('buildEnvelope embeds the data row and a stable id', async () => {
375
+ const store = Store.memory()
376
+ const subscription = await Webhooks.createSubscription(store, createInput())
377
+
378
+ const envelope = Webhooks.buildEnvelope({
379
+ blockNumber: 100,
380
+ createdAt: new Date('2026-01-01T00:00:00.000Z'),
381
+ data: { amount: '1', to: '0x1' },
382
+ logIndex: 3,
383
+ subscription,
384
+ })
385
+
386
+ expect(envelope.subscriptionId).toBe(subscription.id)
387
+ expect({ ...envelope, subscriptionId: '<subscriptionId>' }).toMatchInlineSnapshot(`
388
+ {
389
+ "chainId": 4217,
390
+ "createdAt": "2026-01-01T00:00:00.000Z",
391
+ "data": {
392
+ "amount": "1",
393
+ "to": "0x1",
394
+ },
395
+ "id": "evt_d7c75afd0abccdc90a391d9313762a957c7e4383cac1fae76e5de56e49c9d3f8",
396
+ "subscriptionId": "<subscriptionId>",
397
+ "type": "token:transfer",
398
+ }
399
+ `)
400
+ })
401
+ })
402
+
403
+ describe('delivery log', () => {
404
+ /** Asserts the time-sortable delivery id shape and redacts non-deterministic fields. */
405
+ function redactDelivery(delivery: Webhooks.Delivery) {
406
+ expect(delivery.id).toMatch(/^whd_\d{15}_[A-Za-z0-9_-]{21}$/)
407
+ expect(delivery.subscriptionId).toMatch(/^wh_\d{15}_[A-Za-z0-9_-]{21}$/)
408
+ return {
409
+ ...delivery,
410
+ envelope: { ...delivery.envelope, subscriptionId: '<subscriptionId>' },
411
+ id: '<id>',
412
+ subscriptionId: '<subscriptionId>',
413
+ ...(delivery.responseMs === undefined ? {} : { responseMs: '<ms>' }),
414
+ }
415
+ }
416
+
417
+ /** A delivery row with an explicit, lexically-sortable id for ordering tests. */
418
+ function row(subscriptionId: string, n: number): Webhooks.Delivery {
419
+ return {
420
+ attempt: n,
421
+ createdAt: new Date(1_700_000_000_000 + n * 1_000).toISOString(),
422
+ envelope: {
423
+ chainId: 4217,
424
+ createdAt: new Date(1_700_000_000_000 + n * 1_000).toISOString(),
425
+ data: { amount: String(n) },
426
+ id: `evt_${n}`,
427
+ subscriptionId,
428
+ type: 'token:transfer',
429
+ },
430
+ eventId: `evt_${n}`,
431
+ id: `whd_${String(1_700_000_000_000 + n * 1_000).padStart(15, '0')}_${'0'.repeat(16)}`,
432
+ requestUrl: 'https://hooks.example.com/endpoint',
433
+ status: 'succeeded',
434
+ subscriptionId,
435
+ }
436
+ }
437
+
438
+ test('lists rows newest-first with keyset paging', async () => {
439
+ const store = Store.memory()
440
+ const created = await Webhooks.createSubscription(store, createInput())
441
+ for (const n of [1, 2, 3]) await Webhooks.recordDelivery(store, row(created.id, n))
442
+
443
+ // Newest first.
444
+ const all = await Webhooks.listDeliveries(store, created.id)
445
+ expect(all.map((d) => d.eventId)).toMatchInlineSnapshot(`
446
+ [
447
+ "evt_3",
448
+ "evt_2",
449
+ "evt_1",
450
+ ]
451
+ `)
452
+
453
+ // First page of two, then resume after the last id for the older remainder.
454
+ const page = await Webhooks.listDeliveries(store, created.id, { limit: 2 })
455
+ expect(page.map((d) => d.eventId)).toEqual(['evt_3', 'evt_2'])
456
+
457
+ const next = await Webhooks.listDeliveries(store, created.id, {
458
+ cursor: page.at(-1)?.id,
459
+ limit: 2,
460
+ })
461
+ expect(next.map((d) => d.eventId)).toEqual(['evt_1'])
462
+ })
463
+
464
+ test('deliverAndRecord appends a succeeded row', async () => {
465
+ const store = Store.memory()
466
+ const created = await Webhooks.createSubscription(store, createInput())
467
+ const envelope = {
468
+ chainId: 4217,
469
+ createdAt: '2026-01-01T00:00:00.000Z',
470
+ data: { amount: '1' },
471
+ id: 'evt_ok',
472
+ subscriptionId: created.id,
473
+ type: 'token:transfer',
474
+ } satisfies Webhooks.Envelope
475
+
476
+ const fetch = (async () =>
477
+ new Response(null, { status: 200 })) satisfies typeof globalThis.fetch
478
+ await Webhooks.deliverAndRecord(store, created, envelope, { fetch, now: fixedNow })
479
+
480
+ const [delivery] = await Webhooks.listDeliveries(store, created.id)
481
+ expect(redactDelivery(delivery!)).toMatchInlineSnapshot(`
482
+ {
483
+ "attempt": 1,
484
+ "createdAt": "2026-01-01T00:00:00.000Z",
485
+ "envelope": {
486
+ "chainId": 4217,
487
+ "createdAt": "2026-01-01T00:00:00.000Z",
488
+ "data": {
489
+ "amount": "1",
490
+ },
491
+ "id": "evt_ok",
492
+ "subscriptionId": "<subscriptionId>",
493
+ "type": "token:transfer",
494
+ },
495
+ "eventId": "evt_ok",
496
+ "id": "<id>",
497
+ "requestUrl": "https://hooks.example.com/endpoint",
498
+ "responseMs": "<ms>",
499
+ "responseStatus": 200,
500
+ "status": "succeeded",
501
+ "subscriptionId": "<subscriptionId>",
502
+ }
503
+ `)
504
+ })
505
+
506
+ test('deliverAndRecord appends a failed row with the error and status', async () => {
507
+ const store = Store.memory()
508
+ const created = await Webhooks.createSubscription(store, createInput())
509
+ const envelope = {
510
+ chainId: 4217,
511
+ createdAt: '2026-01-01T00:00:00.000Z',
512
+ data: { amount: '1' },
513
+ id: 'evt_fail',
514
+ subscriptionId: created.id,
515
+ type: 'token:transfer',
516
+ } satisfies Webhooks.Envelope
517
+
518
+ const fetch = (async () =>
519
+ new Response(null, { status: 500 })) satisfies typeof globalThis.fetch
520
+ await Webhooks.deliverAndRecord(store, created, envelope, { fetch, now: fixedNow })
521
+
522
+ const [delivery] = await Webhooks.listDeliveries(store, created.id)
523
+ expect(redactDelivery(delivery!)).toMatchInlineSnapshot(`
524
+ {
525
+ "attempt": 1,
526
+ "createdAt": "2026-01-01T00:00:00.000Z",
527
+ "envelope": {
528
+ "chainId": 4217,
529
+ "createdAt": "2026-01-01T00:00:00.000Z",
530
+ "data": {
531
+ "amount": "1",
532
+ },
533
+ "id": "evt_fail",
534
+ "subscriptionId": "<subscriptionId>",
535
+ "type": "token:transfer",
536
+ },
537
+ "error": "non-2xx response (500)",
538
+ "eventId": "evt_fail",
539
+ "id": "<id>",
540
+ "requestUrl": "https://hooks.example.com/endpoint",
541
+ "responseMs": "<ms>",
542
+ "responseStatus": 500,
543
+ "status": "failed",
544
+ "subscriptionId": "<subscriptionId>",
545
+ }
546
+ `)
547
+ })
548
+
549
+ test('deleteSubscription removes the delivery log', async () => {
550
+ const store = Store.memory()
551
+ const created = await Webhooks.createSubscription(store, createInput())
552
+ for (const n of [1, 2]) await Webhooks.recordDelivery(store, row(created.id, n))
553
+ expect(await Webhooks.listDeliveries(store, created.id)).toHaveLength(2)
554
+
555
+ await Webhooks.deleteSubscription(store, apiKeyOwner, created.id)
556
+ expect(await Webhooks.listDeliveries(store, created.id)).toMatchInlineSnapshot(`[]`)
557
+ })
558
+
559
+ test('getDelivery loads one row by id and returns null when absent', async () => {
560
+ const store = Store.memory()
561
+ const created = await Webhooks.createSubscription(store, createInput())
562
+ await Webhooks.recordDelivery(store, row(created.id, 1))
563
+ const id = row(created.id, 1).id
564
+
565
+ const found = await Webhooks.getDelivery(store, created.id, id)
566
+ expect(found?.eventId).toMatchInlineSnapshot(`"evt_1"`)
567
+ // The persisted row carries the full envelope for replay.
568
+ expect(found?.envelope.id).toMatchInlineSnapshot(`"evt_1"`)
569
+
570
+ expect(await Webhooks.getDelivery(store, created.id, 'whd_missing')).toMatchInlineSnapshot(
571
+ `null`,
572
+ )
573
+ })
574
+ })
575
+
576
+ describe('buildPingEnvelope', () => {
577
+ test('builds a synthetic ping envelope with a deterministic id from the nonce', async () => {
578
+ const store = Store.memory()
579
+ const created = await Webhooks.createSubscription(store, createInput())
580
+ const envelope = Webhooks.buildPingEnvelope(created, {
581
+ createdAt: fixedNow(),
582
+ nonce: 'test-nonce',
583
+ })
584
+ // The id hashes the (random) subscription id, so assert its shape and redact.
585
+ expect(envelope.id).toMatch(/^evt_[0-9a-f]{64}$/)
586
+ expect({ ...envelope, id: '<id>', subscriptionId: '<subscriptionId>' }).toMatchInlineSnapshot(`
587
+ {
588
+ "chainId": 4217,
589
+ "createdAt": "2026-01-01T00:00:00.000Z",
590
+ "data": {
591
+ "ping": true,
592
+ },
593
+ "id": "<id>",
594
+ "subscriptionId": "<subscriptionId>",
595
+ "type": "ping",
596
+ }
597
+ `)
598
+ })
599
+
600
+ test('randomizes the id per call so repeated pings are not deduped', async () => {
601
+ const store = Store.memory()
602
+ const created = await Webhooks.createSubscription(store, createInput())
603
+ const a = Webhooks.buildPingEnvelope(created)
604
+ const b = Webhooks.buildPingEnvelope(created)
605
+ expect(a.id).not.toBe(b.id)
606
+ })
607
+ })
608
+
609
+ describe('ping', () => {
610
+ test('delivers, records a log row, and leaves the subscription state untouched', async () => {
611
+ const store = Store.memory()
612
+ const created = await Webhooks.createSubscription(store, createInput())
613
+
614
+ const captured: { body: string; signature: string | null } = { body: '', signature: null }
615
+ const fetch = (async (_url, init) => {
616
+ captured.body = typeof init?.body === 'string' ? init.body : ''
617
+ captured.signature = new Headers(init?.headers).get(Webhooks.signatureHeader)
618
+ return new Response(null, { status: 200 })
619
+ }) satisfies typeof globalThis.fetch
620
+
621
+ const { envelope, result } = await Webhooks.ping(store, created, {
622
+ fetch,
623
+ nonce: 'test-nonce',
624
+ now: fixedNow,
625
+ })
626
+
627
+ expect(result.ok).toBe(true)
628
+ // The delivered body is signed with the subscription secret.
629
+ expect(
630
+ Webhooks.verify({
631
+ body: captured.body,
632
+ now: 0,
633
+ secret: created.secret,
634
+ signature: captured.signature ?? '',
635
+ tolerance: Number.POSITIVE_INFINITY,
636
+ }),
637
+ ).toBe(true)
638
+
639
+ // A ping is observable in the delivery log.
640
+ const [delivery] = await Webhooks.listDeliveries(store, created.id)
641
+ expect(delivery?.eventId).toBe(envelope.id)
642
+ expect(delivery?.status).toMatchInlineSnapshot(`"succeeded"`)
643
+
644
+ // A ping never mutates the subscription's failure/lifecycle state.
645
+ const after = await Webhooks.getSubscription(store, apiKeyOwner, created.id)
646
+ expect(after?.failureCount).toMatchInlineSnapshot(`0`)
647
+ expect(after?.status).toMatchInlineSnapshot(`"active"`)
648
+ expect(after?.lastDeliveryAt).toMatchInlineSnapshot(`undefined`)
649
+ })
650
+
651
+ test('a failed ping records a failed row without disabling the subscription', async () => {
652
+ const store = Store.memory()
653
+ const created = await Webhooks.createSubscription(store, createInput())
654
+
655
+ const fetch = (async () =>
656
+ new Response(null, { status: 500 })) satisfies typeof globalThis.fetch
657
+ const { result } = await Webhooks.ping(store, created, { fetch, now: fixedNow })
658
+
659
+ expect(result.ok).toBe(false)
660
+ const [delivery] = await Webhooks.listDeliveries(store, created.id)
661
+ expect(delivery?.status).toMatchInlineSnapshot(`"failed"`)
662
+
663
+ // Crucially, a failed test ping must NOT count toward auto-disable.
664
+ const after = await Webhooks.getSubscription(store, apiKeyOwner, created.id)
665
+ expect(after?.failureCount).toMatchInlineSnapshot(`0`)
666
+ expect(after?.status).toMatchInlineSnapshot(`"active"`)
667
+ })
668
+ })