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,648 @@
1
+ import * as WebhookDestination from './WebhookDestination.js'
2
+
3
+ const envelope = {
4
+ chainId: 4217,
5
+ createdAt: '2026-01-01T00:00:00.000Z',
6
+ data: { amount: '1000', token: '0xabc' },
7
+ id: 'evt_test',
8
+ subscriptionId: 'wh_test',
9
+ type: 'token:transfer',
10
+ } satisfies WebhookDestination.deliver.Options['envelope']
11
+
12
+ describe('url', () => {
13
+ test('builds a url destination with a deliver method', () => {
14
+ const destination = WebhookDestination.url('https://example.com/hook')
15
+ expect(destination).toMatchObject({ type: 'url', url: 'https://example.com/hook' })
16
+ expect(typeof destination.deliver).toBe('function')
17
+ // The method drops out of JSON — a destination serializes to just its data.
18
+ expect(JSON.parse(JSON.stringify(destination))).toEqual({
19
+ type: 'url',
20
+ url: 'https://example.com/hook',
21
+ })
22
+ })
23
+
24
+ test('rejects a blocked host', () => {
25
+ expect(() =>
26
+ WebhookDestination.url('https://169.254.169.254/latest/meta-data'),
27
+ ).toThrowErrorMatchingInlineSnapshot(
28
+ `[Webhooks.InvalidUrlError: Webhook URL rejected (blocked_host): https://169.254.169.254/latest/meta-data]`,
29
+ )
30
+ })
31
+ })
32
+
33
+ describe('slack', () => {
34
+ test('builds a slack destination', () => {
35
+ expect(WebhookDestination.slack('https://hooks.slack.com/services/T/B/x')).toMatchObject({
36
+ type: 'slack',
37
+ url: 'https://hooks.slack.com/services/T/B/x',
38
+ })
39
+ })
40
+
41
+ test('rejects a non-Slack host', () => {
42
+ expect(() =>
43
+ WebhookDestination.slack('https://evil.example.com/hook'),
44
+ ).toThrowErrorMatchingInlineSnapshot(
45
+ `[Webhooks.InvalidUrlError: Webhook URL rejected (slack_host): https://evil.example.com/hook]`,
46
+ )
47
+ })
48
+ })
49
+
50
+ describe('betterStack', () => {
51
+ test('builds a betterstack destination', () => {
52
+ expect(
53
+ WebhookDestination.betterStack({
54
+ token: 'src_token',
55
+ url: 'https://s1.eu-nbg-2.betterstackdata.com',
56
+ }),
57
+ ).toMatchObject({
58
+ token: 'src_token',
59
+ type: 'betterstack',
60
+ url: 'https://s1.eu-nbg-2.betterstackdata.com',
61
+ })
62
+ })
63
+
64
+ test('rejects a non-Better Stack host', () => {
65
+ expect(() =>
66
+ WebhookDestination.betterStack({ token: 'src_token', url: 'https://evil.example.com' }),
67
+ ).toThrowErrorMatchingInlineSnapshot(
68
+ `[Webhooks.InvalidUrlError: Webhook URL rejected (betterstack_host): https://evil.example.com]`,
69
+ )
70
+ })
71
+ })
72
+
73
+ describe('from', () => {
74
+ test('normalizes a bare URL string into a url destination', () => {
75
+ expect(WebhookDestination.from('https://example.com/hook')).toMatchObject({
76
+ type: 'url',
77
+ url: 'https://example.com/hook',
78
+ })
79
+ })
80
+
81
+ test('validates and returns a destination object', () => {
82
+ expect(
83
+ WebhookDestination.from({ type: 'slack', url: 'https://hooks.slack.com/services/T/B/x' }),
84
+ ).toMatchObject({ type: 'slack', url: 'https://hooks.slack.com/services/T/B/x' })
85
+ })
86
+
87
+ test('rejects an invalid destination', () => {
88
+ expect(() =>
89
+ WebhookDestination.from({ type: 'slack', url: 'https://evil.example.com' }),
90
+ ).toThrowErrorMatchingInlineSnapshot(
91
+ `[Webhooks.InvalidUrlError: Webhook URL rejected (slack_host): https://evil.example.com]`,
92
+ )
93
+ })
94
+ })
95
+
96
+ describe('url().deliver', () => {
97
+ test('signs the body and POSTs a 2xx as success', async () => {
98
+ let captured:
99
+ | { body: string; headers: Headers; method?: string | undefined; url: string }
100
+ | undefined
101
+ const fetch = (async (input, init) => {
102
+ captured = {
103
+ body: typeof init?.body === 'string' ? init.body : '',
104
+ headers: new Headers(init?.headers),
105
+ method: init?.method,
106
+ url: typeof input === 'string' ? input : '',
107
+ }
108
+ return new Response(null, { status: 200 })
109
+ }) satisfies typeof globalThis.fetch
110
+
111
+ const secret = 'whsec_test'
112
+ const result = await WebhookDestination.url('https://hooks.example.com/x').deliver({
113
+ envelope,
114
+ fetch,
115
+ now: () => 0,
116
+ secret,
117
+ timestamp: 1_000,
118
+ })
119
+
120
+ expect(result).toMatchInlineSnapshot(`
121
+ {
122
+ "durationMs": 0,
123
+ "ok": true,
124
+ "status": 200,
125
+ }
126
+ `)
127
+ expect(captured?.method).toMatchInlineSnapshot(`"POST"`)
128
+ expect(captured?.headers.get('tempo-event-id')).toMatchInlineSnapshot(`"evt_test"`)
129
+ expect(captured?.headers.get('tempo-event-type')).toMatchInlineSnapshot(`"token:transfer"`)
130
+
131
+ const signature = captured?.headers.get('tempo-signature') ?? ''
132
+ expect(
133
+ WebhookDestination.verify({ body: captured?.body ?? '', now: 1_000, secret, signature }),
134
+ ).toMatchInlineSnapshot(`true`)
135
+ })
136
+
137
+ test('classifies a non-2xx as failure with the status', async () => {
138
+ const fetch = (async () =>
139
+ new Response(null, { status: 500 })) satisfies typeof globalThis.fetch
140
+ const result = await WebhookDestination.url('https://hooks.example.com/x').deliver({
141
+ envelope,
142
+ fetch,
143
+ now: () => 0,
144
+ secret: 'whsec_test',
145
+ })
146
+
147
+ expect(result).toMatchInlineSnapshot(`
148
+ {
149
+ "durationMs": 0,
150
+ "error": "non-2xx response (500)",
151
+ "ok": false,
152
+ "status": 500,
153
+ }
154
+ `)
155
+ })
156
+
157
+ test('rejects a 3xx redirect without following it (SSRF guard)', async () => {
158
+ let redirectMode: RequestRedirect | undefined
159
+ const fetch = (async (_input, init) => {
160
+ redirectMode = init?.redirect
161
+ return new Response(null, {
162
+ headers: { location: 'https://169.254.169.254/latest' },
163
+ status: 302,
164
+ })
165
+ }) satisfies typeof globalThis.fetch
166
+
167
+ const result = await WebhookDestination.url('https://hooks.example.com/x').deliver({
168
+ envelope,
169
+ fetch,
170
+ now: () => 0,
171
+ secret: 'whsec_test',
172
+ })
173
+
174
+ expect(redirectMode).toMatchInlineSnapshot(`"manual"`)
175
+ expect(result).toMatchInlineSnapshot(`
176
+ {
177
+ "durationMs": 0,
178
+ "error": "redirect rejected (302)",
179
+ "ok": false,
180
+ "status": 302,
181
+ }
182
+ `)
183
+ })
184
+
185
+ test('fails a url delivery attempted without a secret', async () => {
186
+ let called = false
187
+ const fetch = (async () => {
188
+ called = true
189
+ return new Response(null, { status: 200 })
190
+ }) satisfies typeof globalThis.fetch
191
+
192
+ const result = await WebhookDestination.url('https://hooks.example.com/x').deliver({
193
+ envelope,
194
+ fetch,
195
+ })
196
+
197
+ expect(called).toBe(false)
198
+ expect(result).toMatchInlineSnapshot(`
199
+ {
200
+ "error": "url destination requires a secret",
201
+ "ok": false,
202
+ }
203
+ `)
204
+ })
205
+ })
206
+
207
+ describe('slack().deliver', () => {
208
+ test('POSTs an unsigned Block Kit message to the incoming-webhook URL', async () => {
209
+ let captured: { body: string; headers: Headers; url: string } | undefined
210
+ const fetch = (async (input, init) => {
211
+ captured = {
212
+ body: typeof init?.body === 'string' ? init.body : '',
213
+ headers: new Headers(init?.headers),
214
+ url: typeof input === 'string' ? input : '',
215
+ }
216
+ return new Response(null, { status: 200 })
217
+ }) satisfies typeof globalThis.fetch
218
+
219
+ const result = await WebhookDestination.slack(
220
+ 'https://hooks.slack.com/services/T000/B000/xxxx',
221
+ ).deliver({ envelope, fetch, now: () => 0 })
222
+
223
+ expect(result).toMatchInlineSnapshot(`
224
+ {
225
+ "durationMs": 0,
226
+ "ok": true,
227
+ "status": 200,
228
+ }
229
+ `)
230
+ expect(captured?.url).toBe('https://hooks.slack.com/services/T000/B000/xxxx')
231
+ expect(captured?.headers.get('tempo-signature')).toBeNull()
232
+ const payload = JSON.parse(captured?.body ?? '{}') as { blocks: unknown[]; text: string }
233
+ expect(Array.isArray(payload.blocks)).toBe(true)
234
+ expect(payload.text).toMatchInlineSnapshot(`"Token transfer · 1000 · chain 4217"`)
235
+ })
236
+
237
+ test('leads with the subscription context (title header + description section)', async () => {
238
+ let captured: { body: string } | undefined
239
+ const fetch = (async (_input, init) => {
240
+ captured = { body: typeof init?.body === 'string' ? init.body : '' }
241
+ return new Response(null, { status: 200 })
242
+ }) satisfies typeof globalThis.fetch
243
+
244
+ await WebhookDestination.slack('https://hooks.slack.com/services/T/B/x').deliver({
245
+ envelope: {
246
+ ...envelope,
247
+ context: {
248
+ description: 'Notify #ops on big settles.',
249
+ metadata: { env: 'prod', team: 'payments' },
250
+ title: 'Prod USDC transfers',
251
+ },
252
+ },
253
+ fetch,
254
+ now: () => 0,
255
+ })
256
+
257
+ const payload = JSON.parse(captured?.body ?? '{}') as {
258
+ blocks: { fields?: { text: string }[]; text?: { text: string }; type: string }[]
259
+ text: string
260
+ }
261
+ // Title drives the header + notification fallback.
262
+ expect(payload.text).toBe('Prod USDC transfers')
263
+ expect(payload.blocks[0]).toMatchObject({
264
+ text: { text: 'Prod USDC transfers' },
265
+ type: 'header',
266
+ })
267
+ // Description gets its own section, before the field grid.
268
+ expect(payload.blocks[1]).toMatchObject({
269
+ text: { text: 'Notify #ops on big settles.' },
270
+ type: 'section',
271
+ })
272
+ // Metadata renders as its own field grid.
273
+ const metadata = payload.blocks.find((block) =>
274
+ block.fields?.some((f) => f.text.startsWith('*env*')),
275
+ )
276
+ expect(metadata?.fields).toEqual([
277
+ { text: '*env*\nprod', type: 'mrkdwn' },
278
+ { text: '*team*\npayments', type: 'mrkdwn' },
279
+ ])
280
+ // The raw event type stays in the context line so identity survives the title.
281
+ const context = payload.blocks.at(-1)
282
+ expect(context?.type).toBe('context')
283
+ })
284
+
285
+ test('renders a token:transfer with a human amount and explorer links', async () => {
286
+ const payload = await captureSlack({
287
+ chainId: 4217,
288
+ createdAt: '2026-01-14T18:38:03.000Z',
289
+ data: {
290
+ address: '0x20c0000000000000000000008f5425160ebe5525',
291
+ amount: '10000000',
292
+ blockNumber: 1000002,
293
+ recipient: '0x9e39034aae71fb89f66061a2602eb6efec271754',
294
+ sender: '0xe7687128b0a808c2831ff94d4f7b2fb35c65af38',
295
+ timestamp: '2026-01-14T18:38:03.000Z',
296
+ token: { decimals: 6, symbol: 'USDC' },
297
+ transactionHash: '0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3',
298
+ },
299
+ id: 'evt_abc',
300
+ subscriptionId: 'wh_abc',
301
+ type: 'token:transfer',
302
+ })
303
+
304
+ expect(header(payload)).toBe(':money_with_wings: Token transfer')
305
+ // Human amount from token metadata, with truncated + linked sender/recipient.
306
+ expect(section(payload, 1)).toContain('*10 USDC* from')
307
+ expect(section(payload, 1)).toContain('<https://explore.tempo.xyz/address/0xe768')
308
+ expect(payload.text).toBe('Token transfer · 10 USDC · chain 4217')
309
+ // The Amount field renders the human amount only (no raw base units).
310
+ const json = JSON.stringify(payload.blocks)
311
+ expect(json).toContain('*Amount*\\n10 USDC')
312
+ expect(json).not.toContain('base units')
313
+ })
314
+
315
+ test('renders a reverted transaction with an :x: accent header', async () => {
316
+ const payload = await captureSlack({
317
+ chainId: 4217,
318
+ createdAt: '2026-01-14T18:38:03.000Z',
319
+ data: {
320
+ blockNumber: 23456789,
321
+ hash: '0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3',
322
+ meta: { receipt: { status: 'reverted' } },
323
+ recipient: '0x9e39034aae71fb89f66061a2602eb6efec271754',
324
+ sender: '0xe7687128b0a808c2831ff94d4f7b2fb35c65af38',
325
+ type: 'eip1559',
326
+ value: '0',
327
+ },
328
+ id: 'evt_tx',
329
+ subscriptionId: 'wh_tx',
330
+ type: 'transaction:included',
331
+ })
332
+
333
+ expect(header(payload)).toBe(':x: Transaction reverted')
334
+ expect(section(payload, 1)).toContain(':x: *reverted*')
335
+ expect(payload.text).toBe('Transaction included · reverted · chain 4217')
336
+ })
337
+
338
+ test('renders a decoded log with the event name in the header', async () => {
339
+ const payload = await captureSlack({
340
+ chainId: 4217,
341
+ createdAt: '2026-01-14T18:38:03.000Z',
342
+ data: {
343
+ address: '0x20c0000000000000000000008f5425160ebe5525',
344
+ args: { from: '0xe7687128b0a808c2831ff94d4f7b2fb35c65af38', value: '10000' },
345
+ blockNumber: 1000002,
346
+ event: { name: 'Transfer', signature: 'event Transfer(address,address,uint256)' },
347
+ logIndex: 0,
348
+ timestamp: '2026-01-14T18:38:03.000Z',
349
+ transactionHash: '0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3',
350
+ },
351
+ id: 'evt_log',
352
+ subscriptionId: 'wh_log',
353
+ type: 'log:emitted',
354
+ })
355
+
356
+ expect(header(payload)).toBe(':label: Log: Transfer')
357
+ expect(section(payload, 1)).toContain('*Transfer* on')
358
+ expect(payload.text).toContain('Log: Transfer')
359
+ })
360
+
361
+ test('renders a block:created header with the block number', async () => {
362
+ const payload = await captureSlack({
363
+ chainId: 4217,
364
+ createdAt: '2026-01-14T18:38:03.000Z',
365
+ data: {
366
+ gasLimit: 500000000,
367
+ gasUsed: 4340281,
368
+ hash: '0x3fe7d9e595f3d8215ec840c6ef55ac0bead58933e96fc2ead0f2aaf79715f45d',
369
+ miner: '0x0000000000000000000000000000000000000000',
370
+ number: 1000002,
371
+ transactionCount: 3,
372
+ timestamp: '2026-01-14T18:38:03.000Z',
373
+ },
374
+ id: 'evt_blk',
375
+ subscriptionId: 'wh_blk',
376
+ type: 'block:created',
377
+ })
378
+
379
+ expect(header(payload)).toBe(':link: Block created #1000002')
380
+ expect(section(payload, 1)).toContain('*3* transactions')
381
+ expect(payload.text).toBe('Block created #1000002 · 3 txns · chain 4217')
382
+ })
383
+
384
+ test('renders a ping with a wired-up confirmation lead', async () => {
385
+ const payload = await captureSlack({
386
+ chainId: 4217,
387
+ createdAt: '2026-01-14T18:38:03.000Z',
388
+ data: { ping: true },
389
+ id: 'evt_ping',
390
+ subscriptionId: 'wh_ping',
391
+ type: 'ping',
392
+ })
393
+
394
+ expect(header(payload)).toBe(':bell: Webhook test ping')
395
+ expect(section(payload, 1)).toContain('wired up correctly')
396
+ expect(payload.text).toBe('Webhook test ping · chain 4217')
397
+ })
398
+
399
+ test('classifies a non-2xx Slack response as failure', async () => {
400
+ const fetch = (async () =>
401
+ new Response('invalid_token', { status: 403 })) satisfies typeof globalThis.fetch
402
+ const result = await WebhookDestination.slack(
403
+ 'https://hooks.slack.com/services/T000/B000/xxxx',
404
+ ).deliver({ envelope, fetch, now: () => 0 })
405
+
406
+ expect(result).toMatchInlineSnapshot(`
407
+ {
408
+ "durationMs": 0,
409
+ "error": "slack non-2xx response (403)",
410
+ "ok": false,
411
+ "status": 403,
412
+ }
413
+ `)
414
+ })
415
+ })
416
+
417
+ /** A parsed Slack Block Kit payload with the loose block shapes we assert on. */
418
+ type SlackPayload = {
419
+ blocks: { elements?: { text: string }[]; text?: { text: string }; type: string }[]
420
+ text: string
421
+ }
422
+
423
+ /** Delivers an envelope through the `slack` transport and returns the parsed body. */
424
+ async function captureSlack(
425
+ envelope: WebhookDestination.deliver.Options['envelope'],
426
+ ): Promise<SlackPayload> {
427
+ let body = '{}'
428
+ const fetch = (async (_input, init) => {
429
+ body = typeof init?.body === 'string' ? init.body : '{}'
430
+ return new Response(null, { status: 200 })
431
+ }) satisfies typeof globalThis.fetch
432
+ await WebhookDestination.slack('https://hooks.slack.com/services/T/B/x').deliver({
433
+ envelope,
434
+ fetch,
435
+ now: () => 0,
436
+ })
437
+ return JSON.parse(body) as SlackPayload
438
+ }
439
+
440
+ /** The header block's text. */
441
+ function header(payload: SlackPayload): string | undefined {
442
+ return payload.blocks.find((block) => block.type === 'header')?.text?.text
443
+ }
444
+
445
+ /** The mrkdwn text of the section block at `index` (header is block 0). */
446
+ function section(payload: SlackPayload, index: number): string {
447
+ return payload.blocks[index]?.text?.text ?? ''
448
+ }
449
+
450
+ describe('betterStack().deliver', () => {
451
+ test('POSTs a bearer-authed log event to the ingest URL', async () => {
452
+ let captured: { body: string; headers: Headers; url: string } | undefined
453
+ const fetch = (async (input, init) => {
454
+ captured = {
455
+ body: typeof init?.body === 'string' ? init.body : '',
456
+ headers: new Headers(init?.headers),
457
+ url: typeof input === 'string' ? input : '',
458
+ }
459
+ return new Response(null, { status: 202 })
460
+ }) satisfies typeof globalThis.fetch
461
+
462
+ const result = await WebhookDestination.betterStack({
463
+ token: 'src_token',
464
+ url: 'https://s1234567.eu-nbg-2.betterstackdata.com',
465
+ }).deliver({ envelope, fetch, now: () => 0 })
466
+
467
+ expect(result).toMatchInlineSnapshot(`
468
+ {
469
+ "durationMs": 0,
470
+ "ok": true,
471
+ "status": 202,
472
+ }
473
+ `)
474
+ expect(captured?.headers.get('authorization')).toBe('Bearer src_token')
475
+ expect(captured?.headers.get('tempo-signature')).toBeNull()
476
+ const payload = JSON.parse(captured?.body ?? '{}') as Record<string, unknown>
477
+ expect(payload['dt']).toBe('2026-01-01T00:00:00.000Z')
478
+ expect(payload['event']).toBe('token:transfer')
479
+ })
480
+
481
+ test('maps subscription context onto the log message + description', async () => {
482
+ let captured: { body: string } | undefined
483
+ const fetch = (async (_input, init) => {
484
+ captured = { body: typeof init?.body === 'string' ? init.body : '' }
485
+ return new Response(null, { status: 202 })
486
+ }) satisfies typeof globalThis.fetch
487
+
488
+ await WebhookDestination.betterStack({
489
+ token: 'src_token',
490
+ url: 'https://s1234567.eu-nbg-2.betterstackdata.com',
491
+ }).deliver({
492
+ envelope: {
493
+ ...envelope,
494
+ context: {
495
+ description: 'Notify #ops on big settles.',
496
+ metadata: { env: 'prod', team: 'payments' },
497
+ title: 'Prod USDC transfers',
498
+ },
499
+ },
500
+ fetch,
501
+ now: () => 0,
502
+ })
503
+
504
+ const payload = JSON.parse(captured?.body ?? '{}') as Record<string, unknown>
505
+ expect(payload['message']).toBe('Prod USDC transfers')
506
+ expect(payload['description']).toBe('Notify #ops on big settles.')
507
+ expect(payload['metadata']).toEqual({ env: 'prod', team: 'payments' })
508
+ })
509
+
510
+ test('classifies a non-2xx Better Stack response as failure', async () => {
511
+ const fetch = (async () =>
512
+ new Response('Unauthorized', { status: 403 })) satisfies typeof globalThis.fetch
513
+ const result = await WebhookDestination.betterStack({
514
+ token: 'src_token',
515
+ url: 'https://s1234567.eu-nbg-2.betterstackdata.com',
516
+ }).deliver({ envelope, fetch, now: () => 0 })
517
+
518
+ expect(result).toMatchInlineSnapshot(`
519
+ {
520
+ "durationMs": 0,
521
+ "error": "betterstack non-2xx response (403)",
522
+ "ok": false,
523
+ "status": 403,
524
+ }
525
+ `)
526
+ })
527
+ })
528
+
529
+ describe('from(...).deliver', () => {
530
+ test('rebuilds a plain (persisted) destination and delivers via its own transport', async () => {
531
+ let capturedUrl: string | undefined
532
+ const fetch = (async (input) => {
533
+ capturedUrl = typeof input === 'string' ? input : ''
534
+ return new Response(null, { status: 200 })
535
+ }) satisfies typeof globalThis.fetch
536
+
537
+ // A plain destination as read back from the store — no bound `deliver`.
538
+ const destination: WebhookDestination.Destination = {
539
+ type: 'slack',
540
+ url: 'https://hooks.slack.com/services/T000/B000/xxxx',
541
+ }
542
+ const result = await WebhookDestination.from(destination).deliver({
543
+ envelope,
544
+ fetch,
545
+ now: () => 0,
546
+ })
547
+
548
+ expect(result.ok).toBe(true)
549
+ expect(capturedUrl).toBe('https://hooks.slack.com/services/T000/B000/xxxx')
550
+ })
551
+ })
552
+
553
+ describe('assertDeliverableUrl', () => {
554
+ test('accepts a plain https URL', () => {
555
+ expect(() =>
556
+ WebhookDestination.assertDeliverableUrl('https://hooks.example.com/x'),
557
+ ).not.toThrow()
558
+ })
559
+
560
+ test.each([
561
+ ['http (non-https)', 'http://hooks.example.com', 'protocol'],
562
+ ['ftp scheme', 'ftp://hooks.example.com', 'protocol'],
563
+ ['embedded credentials', 'https://user:pass@hooks.example.com', 'credentials'],
564
+ ['localhost', 'https://localhost/x', 'blocked_host'],
565
+ ['sub.localhost', 'https://api.localhost/x', 'blocked_host'],
566
+ ['loopback v4', 'https://127.0.0.1/x', 'blocked_host'],
567
+ ['private 10/8', 'https://10.1.2.3/x', 'blocked_host'],
568
+ ['private 172.16/12', 'https://172.16.0.1/x', 'blocked_host'],
569
+ ['private 192.168/16', 'https://192.168.0.1/x', 'blocked_host'],
570
+ ['link-local / metadata', 'https://169.254.169.254/x', 'blocked_host'],
571
+ ['cgnat', 'https://100.64.0.1/x', 'blocked_host'],
572
+ ['gcp metadata host', 'https://metadata.google.internal/x', 'blocked_host'],
573
+ ['ipv6 loopback', 'https://[::1]/x', 'blocked_host'],
574
+ ['ipv6 link-local', 'https://[fe80::1]/x', 'blocked_host'],
575
+ ['ipv6 unique-local', 'https://[fd00::1]/x', 'blocked_host'],
576
+ ['ipv4-mapped loopback', 'https://[::ffff:127.0.0.1]/x', 'blocked_host'],
577
+ ['malformed', 'not a url', 'malformed'],
578
+ ])('rejects %s', (_label, url, reason) => {
579
+ let error: unknown
580
+ try {
581
+ WebhookDestination.assertDeliverableUrl(url)
582
+ } catch (caught) {
583
+ error = caught
584
+ }
585
+ expect(error).toBeInstanceOf(WebhookDestination.InvalidUrlError)
586
+ expect((error as WebhookDestination.InvalidUrlError).reason).toBe(reason)
587
+ })
588
+
589
+ test('accepts a public IPv4 literal', () => {
590
+ expect(() => WebhookDestination.assertDeliverableUrl('https://93.184.216.34/x')).not.toThrow()
591
+ })
592
+ })
593
+
594
+ describe('assertSlackUrl', () => {
595
+ test('accepts a hooks.slack.com https URL', () => {
596
+ expect(() =>
597
+ WebhookDestination.assertSlackUrl('https://hooks.slack.com/services/T000/B000/xxxx'),
598
+ ).not.toThrow()
599
+ })
600
+
601
+ test('rejects a non-Slack host', () => {
602
+ expect(() =>
603
+ WebhookDestination.assertSlackUrl('https://evil.example.com/hook'),
604
+ ).toThrowErrorMatchingInlineSnapshot(
605
+ `[Webhooks.InvalidUrlError: Webhook URL rejected (slack_host): https://evil.example.com/hook]`,
606
+ )
607
+ })
608
+
609
+ test('rejects a non-https URL', () => {
610
+ expect(() =>
611
+ WebhookDestination.assertSlackUrl('http://hooks.slack.com/services/x'),
612
+ ).toThrowErrorMatchingInlineSnapshot(
613
+ `[Webhooks.InvalidUrlError: Webhook URL rejected (protocol): http://hooks.slack.com/services/x]`,
614
+ )
615
+ })
616
+ })
617
+
618
+ describe('redactSlackUrl', () => {
619
+ test('masks the secret path, keeping the origin', () => {
620
+ expect(
621
+ WebhookDestination.redactSlackUrl('https://hooks.slack.com/services/T000/B000/secrettoken'),
622
+ ).toMatchInlineSnapshot(`"https://hooks.slack.com/…"`)
623
+ })
624
+ })
625
+
626
+ describe('assertBetterstackUrl', () => {
627
+ test('accepts a betterstackdata.com https URL', () => {
628
+ expect(() =>
629
+ WebhookDestination.assertBetterstackUrl('https://s1234567.eu-nbg-2.betterstackdata.com'),
630
+ ).not.toThrow()
631
+ })
632
+
633
+ test('rejects a non-Better Stack host', () => {
634
+ expect(() =>
635
+ WebhookDestination.assertBetterstackUrl('https://evil.example.com/ingest'),
636
+ ).toThrowErrorMatchingInlineSnapshot(
637
+ `[Webhooks.InvalidUrlError: Webhook URL rejected (betterstack_host): https://evil.example.com/ingest]`,
638
+ )
639
+ })
640
+
641
+ test('rejects a non-https URL', () => {
642
+ expect(() =>
643
+ WebhookDestination.assertBetterstackUrl('http://s1.betterstackdata.com'),
644
+ ).toThrowErrorMatchingInlineSnapshot(
645
+ `[Webhooks.InvalidUrlError: Webhook URL rejected (protocol): http://s1.betterstackdata.com]`,
646
+ )
647
+ })
648
+ })