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,980 @@
1
+ import { Hono } from 'hono'
2
+ import { requestId } from 'hono/request-id'
3
+ import { Mppx, tempo } from 'mppx/hono'
4
+ import { RateLimit } from 'cadent'
5
+ import * as ApiKey from '../ApiKey.js'
6
+ import * as Auth from './Auth.js'
7
+ import * as Log from './Log.js'
8
+ import * as Scope from '../Scope.js'
9
+ import * as Viem from './Viem.js'
10
+ import * as TestApp from '../../test/App.js'
11
+ import { createClient as createMppClient } from '../../test/Mppx.js'
12
+ import * as Tempo from '../../test/Tempo.js'
13
+
14
+ const key = {
15
+ environment: 'production',
16
+ id: 'key_test',
17
+ name: 'Test key',
18
+ orgId: 'org_test',
19
+ scopes: ['data:read'],
20
+ } satisfies ApiKey.ApiKey
21
+
22
+ const token = 'secret_test_key'
23
+
24
+ describe('require', () => {
25
+ describe('apiKey', () => {
26
+ test('accepts API key from tempo-api-key', async () => {
27
+ const app = createApp()
28
+ const response = await app.request('/protected', {
29
+ headers: { 'tempo-api-key': token },
30
+ })
31
+ const body = await response.json()
32
+
33
+ expect(response.status).toMatchInlineSnapshot(`200`)
34
+ expect(body).toMatchInlineSnapshot(`
35
+ {
36
+ "id": "key_test",
37
+ "orgId": "org_test",
38
+ "type": "api_key",
39
+ }
40
+ `)
41
+ })
42
+
43
+ test('accepts bearer API key when authorization is not payment', async () => {
44
+ const app = createApp()
45
+ const response = await app.request('/protected', {
46
+ headers: { authorization: `Bearer ${token}` },
47
+ })
48
+ const body = await response.json()
49
+
50
+ expect(response.status).toMatchInlineSnapshot(`200`)
51
+ expect(body).toMatchInlineSnapshot(`
52
+ {
53
+ "id": "key_test",
54
+ "orgId": "org_test",
55
+ "type": "api_key",
56
+ }
57
+ `)
58
+ })
59
+
60
+ test('does not parse payment authorization as bearer fallback', async () => {
61
+ const app = createApp()
62
+ const response = await app.request('/protected', {
63
+ headers: { authorization: 'Payment paid' },
64
+ })
65
+ const body = await response.json()
66
+
67
+ expect(response.status).toMatchInlineSnapshot(`401`)
68
+ expect(body).toMatchInlineSnapshot(`
69
+ {
70
+ "error": {
71
+ "code": "api_key_missing",
72
+ "message": "Missing API key",
73
+ },
74
+ }
75
+ `)
76
+ })
77
+
78
+ test('accepts API key from deprecated key query param', async () => {
79
+ const app = createApp()
80
+ const response = await app.request(`/protected?key=${token}`)
81
+ const body = await response.json()
82
+
83
+ expect(response.status).toMatchInlineSnapshot(`200`)
84
+ expect(body).toMatchInlineSnapshot(`
85
+ {
86
+ "id": "key_test",
87
+ "orgId": "org_test",
88
+ "type": "api_key",
89
+ }
90
+ `)
91
+ })
92
+
93
+ test('strips the deprecated key query param before route validation', async () => {
94
+ // Regression: route query schemas are strict (`z.strictObject`), so the
95
+ // `key` auth param must be removed from the URL once consumed — otherwise
96
+ // the downstream validator rejects it as `query_invalid` and the legacy
97
+ // `?key=...` lane breaks on every strict-query endpoint.
98
+ const auth = {
99
+ apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
100
+ rateLimit: RateLimit.memory(),
101
+ }
102
+ const app = new Hono<Auth.Environment>()
103
+ app.use('*', async (c, next) => {
104
+ c.set('auth', auth)
105
+ await next()
106
+ })
107
+ app.use('/strict', Auth.require({ apiKey: { scopes: ['data:read'] } }))
108
+ app.get('/strict', (c) =>
109
+ c.json({ key: c.req.query('key') ?? null, limit: c.req.query('limit') ?? null }),
110
+ )
111
+
112
+ const response = await app.request(`/strict?limit=5&key=${token}`)
113
+ const body = await response.json()
114
+
115
+ expect(response.status).toMatchInlineSnapshot(`200`)
116
+ expect(body).toMatchInlineSnapshot(`
117
+ {
118
+ "key": null,
119
+ "limit": "5",
120
+ }
121
+ `)
122
+ })
123
+
124
+ test('rejects invalid API key without falling back to public quota', async () => {
125
+ const app = createApp({
126
+ policy: {
127
+ apiKey: { scopes: ['data:read'] },
128
+ public: { rateLimit: { perMinute: 60 } },
129
+ },
130
+ })
131
+ const response = await app.request('/protected', {
132
+ headers: { 'tempo-api-key': 'wrong' },
133
+ })
134
+ const body = await response.json()
135
+
136
+ expect(response.status).toMatchInlineSnapshot(`401`)
137
+ expect(body).toMatchInlineSnapshot(`
138
+ {
139
+ "error": {
140
+ "code": "api_key_invalid",
141
+ "message": "Invalid API key",
142
+ },
143
+ }
144
+ `)
145
+ })
146
+
147
+ test('records auth error codes for request logs', async () => {
148
+ const entries: Log.Entry[] = []
149
+ const app = createApp({
150
+ logger: (entry) => void entries.push(entry),
151
+ policy: {
152
+ apiKey: { scopes: ['data:read'] },
153
+ public: { rateLimit: { perMinute: 60 } },
154
+ },
155
+ })
156
+ const response = await app.request('/protected', {
157
+ headers: { 'tempo-api-key': 'wrong' },
158
+ })
159
+
160
+ expect(response.status).toMatchInlineSnapshot(`401`)
161
+ expect(entries[0]?.errorCode).toMatchInlineSnapshot(`"api_key_invalid"`)
162
+ })
163
+
164
+ test('records resolved key principal for forbidden-scope logs', async () => {
165
+ const entries: Log.Entry[] = []
166
+ const app = createApp({
167
+ logger: (entry) => void entries.push(entry),
168
+ policy: { apiKey: { scopes: ['webhooks:write'] } },
169
+ })
170
+ const response = await app.request('/protected', {
171
+ headers: { 'tempo-api-key': token },
172
+ })
173
+
174
+ expect(response.status).toMatchInlineSnapshot(`403`)
175
+ expect(entries[0]?.errorCode).toMatchInlineSnapshot(`"api_key_forbidden"`)
176
+ expect(entries[0]?.principal).toMatchInlineSnapshot(`
177
+ {
178
+ "environment": "production",
179
+ "id": "key_test",
180
+ "orgId": "org_test",
181
+ "type": "api_key",
182
+ }
183
+ `)
184
+ })
185
+
186
+ test('rejects malformed API key', async () => {
187
+ const app = createApp()
188
+ const response = await app.request('/protected', {
189
+ headers: { authorization: 'Basic wrong' },
190
+ })
191
+ const body = await response.json()
192
+
193
+ expect(response.status).toMatchInlineSnapshot(`400`)
194
+ expect(body).toMatchInlineSnapshot(`
195
+ {
196
+ "error": {
197
+ "code": "api_key_malformed",
198
+ "message": "Malformed API key",
199
+ },
200
+ }
201
+ `)
202
+ })
203
+
204
+ test('rejects missing API key when no public or payment lane exists', async () => {
205
+ const app = createApp()
206
+ const response = await app.request('/protected')
207
+ const body = await response.json()
208
+
209
+ expect(response.status).toMatchInlineSnapshot(`401`)
210
+ expect(body).toMatchInlineSnapshot(`
211
+ {
212
+ "error": {
213
+ "code": "api_key_missing",
214
+ "message": "Missing API key",
215
+ },
216
+ }
217
+ `)
218
+ })
219
+
220
+ test('rejects missing scope', async () => {
221
+ const app = createApp({
222
+ policy: { apiKey: { scopes: ['webhooks:write'] } },
223
+ })
224
+ const response = await app.request('/protected', {
225
+ headers: { 'tempo-api-key': token },
226
+ })
227
+ const body = await response.json()
228
+
229
+ expect(response.status).toMatchInlineSnapshot(`403`)
230
+ expect(body).toMatchInlineSnapshot(`
231
+ {
232
+ "error": {
233
+ "code": "api_key_forbidden",
234
+ "message": "API key missing required scope",
235
+ },
236
+ }
237
+ `)
238
+ })
239
+
240
+ test('wildcard scope satisfies any required scope', async () => {
241
+ const app = createApp({
242
+ auth: {
243
+ apiKey: {
244
+ source: TestApp.source({
245
+ keys: [{ ...key, scopes: [Scope.wildcard], token }],
246
+ }),
247
+ },
248
+ rateLimit: RateLimit.memory(),
249
+ },
250
+ policy: {
251
+ apiKey: { scopes: ['data:read', 'webhooks:write'] },
252
+ },
253
+ })
254
+ const response = await app.request('/protected', {
255
+ headers: { 'tempo-api-key': token },
256
+ })
257
+ const body = await response.json()
258
+
259
+ expect(response.status).toMatchInlineSnapshot(`200`)
260
+ expect(body).toMatchInlineSnapshot(`
261
+ {
262
+ "id": "key_test",
263
+ "orgId": "org_test",
264
+ "type": "api_key",
265
+ }
266
+ `)
267
+ })
268
+ })
269
+
270
+ describe('environment', () => {
271
+ const sandboxToken = 'secret_sandbox'
272
+
273
+ function createChainApp(options: { public?: boolean } = {}) {
274
+ const auth: Auth.Context = {
275
+ apiKey: {
276
+ source: TestApp.source({
277
+ keys: [
278
+ { ...key, scopes: [Scope.wildcard], token },
279
+ {
280
+ environment: 'sandbox',
281
+ id: 'key_sandbox',
282
+ orgId: 'org_test',
283
+ scopes: [Scope.wildcard],
284
+ token: sandboxToken,
285
+ },
286
+ ],
287
+ }),
288
+ },
289
+ rateLimit: RateLimit.memory(),
290
+ }
291
+ const policy: Auth.require.Policy = {
292
+ apiKey: { scopes: [Scope.wildcard] },
293
+ ...(options.public === false ? {} : { public: { rateLimit: { perMinute: 60 } } }),
294
+ }
295
+ const app = new Hono<Auth.Environment>()
296
+ app.use('*', async (c, next) => {
297
+ c.set('auth', auth)
298
+ // Mirror the app default so sandbox steering toward testnet is observable.
299
+ c.set('chainId' as never, Viem.defaultChainId as never)
300
+ await next()
301
+ })
302
+ app.use('/protected', Auth.require(policy))
303
+ app.get('/protected', (c) => {
304
+ // Mirror how real handlers resolve the effective chain: explicit query
305
+ // wins, else the context default (which auth may steer to testnet).
306
+ const raw = c.req.query('chainId')
307
+ const chainId = raw ? Number(raw) : ((c.get('chainId' as never) as number | null) ?? null)
308
+ return c.json({ chainId, principal: Auth.getPrincipal(c)?.type ?? null })
309
+ })
310
+ return app
311
+ }
312
+
313
+ test('falls through to the public policy for a sandbox key on explicit mainnet', async () => {
314
+ const app = createChainApp()
315
+ const response = await app.request('/protected?chainId=4217', {
316
+ headers: { 'tempo-api-key': sandboxToken },
317
+ })
318
+ expect(response.status).toMatchInlineSnapshot(`200`)
319
+ // Treated as anonymous (public) rather than rejected; chain stays mainnet.
320
+ expect(await response.json()).toMatchInlineSnapshot(`
321
+ {
322
+ "chainId": 4217,
323
+ "principal": "public",
324
+ }
325
+ `)
326
+ })
327
+
328
+ test('falls through to the public policy for a sandbox key on the mainnet alias', async () => {
329
+ const app = createChainApp()
330
+ const response = await app.request('/protected?chainId=mainnet', {
331
+ headers: { 'tempo-api-key': sandboxToken },
332
+ })
333
+ expect(response.status).toMatchInlineSnapshot(`200`)
334
+ expect((await response.json()).principal).toMatchInlineSnapshot(`"public"`)
335
+ })
336
+
337
+ test('a sandbox key on mainnet with no public lane is rejected as missing', async () => {
338
+ const app = createChainApp({ public: false })
339
+ const response = await app.request('/protected?chainId=4217', {
340
+ headers: { 'tempo-api-key': sandboxToken },
341
+ })
342
+ // With no public lane, the public policy closes to "missing API key".
343
+ expect(response.status).toMatchInlineSnapshot(`401`)
344
+ expect(await response.json()).toMatchInlineSnapshot(`
345
+ {
346
+ "error": {
347
+ "code": "api_key_missing",
348
+ "message": "Missing API key",
349
+ },
350
+ }
351
+ `)
352
+ })
353
+
354
+ test('uses a sandbox key on an explicit non-mainnet chain', async () => {
355
+ const app = createChainApp()
356
+ const response = await app.request('/protected?chainId=42431', {
357
+ headers: { 'tempo-api-key': sandboxToken },
358
+ })
359
+ expect(response.status).toMatchInlineSnapshot(`200`)
360
+ expect(await response.json()).toMatchInlineSnapshot(`
361
+ {
362
+ "chainId": 42431,
363
+ "principal": "api_key",
364
+ }
365
+ `)
366
+ })
367
+
368
+ test('defaults a sandbox key with no chain to testnet', async () => {
369
+ const app = createChainApp()
370
+ const response = await app.request('/protected', {
371
+ headers: { 'tempo-api-key': sandboxToken },
372
+ })
373
+ expect(response.status).toMatchInlineSnapshot(`200`)
374
+ expect(await response.json()).toMatchInlineSnapshot(`
375
+ {
376
+ "chainId": 42431,
377
+ "principal": "api_key",
378
+ }
379
+ `)
380
+ })
381
+
382
+ test('uses a production key on mainnet', async () => {
383
+ const app = createChainApp()
384
+ const response = await app.request('/protected?chainId=4217', {
385
+ headers: { 'tempo-api-key': token },
386
+ })
387
+ expect(response.status).toMatchInlineSnapshot(`200`)
388
+ expect(await response.json()).toMatchInlineSnapshot(`
389
+ {
390
+ "chainId": 4217,
391
+ "principal": "api_key",
392
+ }
393
+ `)
394
+ })
395
+ })
396
+
397
+ describe('mpp', () => {
398
+ test('returns payment challenge for public over-quota requests', async () => {
399
+ const mpp = createMppServer()
400
+ const app = createApp({
401
+ auth: {
402
+ apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
403
+ mpp,
404
+ rateLimit: overQuota(),
405
+ },
406
+ policy: {
407
+ mpp: { charge: { amount: '1', description: 'Protected data' } },
408
+ public: { rateLimit: { perMinute: 1 } },
409
+ },
410
+ })
411
+ const response = await app.request('/protected')
412
+
413
+ expect(response.status).toMatchInlineSnapshot(`402`)
414
+ expect(
415
+ response.headers.get('www-authenticate')?.startsWith('Payment '),
416
+ ).toMatchInlineSnapshot(`true`)
417
+ })
418
+
419
+ test('accepts paid public retry without consuming quota', async () => {
420
+ const mpp = createMppServer()
421
+ const app = createApp({
422
+ auth: {
423
+ apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
424
+ mpp,
425
+ rateLimit: overQuotaOnce(),
426
+ },
427
+ policy: {
428
+ mpp: { charge: { amount: '1', description: 'Protected data' } },
429
+ public: { rateLimit: { perMinute: 1 } },
430
+ },
431
+ })
432
+ const response = await createMppClient(app).fetch('http://tempo-api.test/protected')
433
+ const body = await response.json()
434
+
435
+ expect(response.status).toMatchInlineSnapshot(`200`)
436
+ expect(response.headers.has('payment-receipt')).toMatchInlineSnapshot(`true`)
437
+ expect(normalizePaymentPayer(body)).toMatchInlineSnapshot(`
438
+ {
439
+ "id": "anonymous",
440
+ "payment": {
441
+ "payer": "did:pkh:eip155:...:0x...",
442
+ "reason": "public_over_quota",
443
+ "type": "mpp",
444
+ },
445
+ "type": "public",
446
+ }
447
+ `)
448
+ })
449
+
450
+ test('returns payment challenge for API-key over-quota requests', async () => {
451
+ const mpp = createMppServer()
452
+ const app = createApp({
453
+ auth: {
454
+ apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
455
+ mpp,
456
+ rateLimit: overQuota(),
457
+ },
458
+ policy: {
459
+ apiKey: {
460
+ rateLimit: { perMinute: 1 },
461
+ scopes: ['data:read'],
462
+ },
463
+ mpp: { charge: { amount: '1', description: 'Protected data' } },
464
+ },
465
+ })
466
+ const response = await app.request('/protected', {
467
+ headers: { 'tempo-api-key': token },
468
+ })
469
+
470
+ expect(response.status).toMatchInlineSnapshot(`402`)
471
+ expect(
472
+ response.headers.get('www-authenticate')?.startsWith('Payment '),
473
+ ).toMatchInlineSnapshot(`true`)
474
+ })
475
+
476
+ test('accepts paid API-key overflow retry', async () => {
477
+ const mpp = createMppServer()
478
+ const app = createApp({
479
+ auth: {
480
+ apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
481
+ mpp,
482
+ rateLimit: overQuotaOnce(),
483
+ },
484
+ policy: {
485
+ apiKey: {
486
+ rateLimit: { perMinute: 1 },
487
+ scopes: ['data:read'],
488
+ },
489
+ mpp: { charge: { amount: '1', description: 'Protected data' } },
490
+ },
491
+ })
492
+ const response = await createMppClient(app).fetch('http://tempo-api.test/protected', {
493
+ headers: {
494
+ 'tempo-api-key': token,
495
+ },
496
+ })
497
+ const body = await response.json()
498
+
499
+ expect(response.status).toMatchInlineSnapshot(`200`)
500
+ expect(response.headers.has('payment-receipt')).toMatchInlineSnapshot(`true`)
501
+ expect(normalizePaymentPayer(body)).toMatchInlineSnapshot(`
502
+ {
503
+ "id": "key_test",
504
+ "orgId": "org_test",
505
+ "payment": {
506
+ "payer": "did:pkh:eip155:...:0x...",
507
+ "reason": "api_key_over_quota",
508
+ "type": "mpp",
509
+ },
510
+ "type": "api_key",
511
+ }
512
+ `)
513
+ })
514
+ })
515
+
516
+ describe('rateLimit', () => {
517
+ test('allows anonymous public requests under quota', async () => {
518
+ const app = createApp({
519
+ policy: { public: { rateLimit: { perMinute: 60 } } },
520
+ })
521
+ const response = await app.request('/protected', {
522
+ headers: { 'x-forwarded-for': '203.0.113.10' },
523
+ })
524
+ const body = await response.json()
525
+
526
+ expect(response.status).toMatchInlineSnapshot(`200`)
527
+ expect(response.headers.get('ratelimit-limit')).toMatchInlineSnapshot(`"60"`)
528
+ expect(response.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`"59"`)
529
+ expect(body).toMatchInlineSnapshot(`
530
+ {
531
+ "id": "203.0.113.10",
532
+ "type": "public",
533
+ }
534
+ `)
535
+ })
536
+
537
+ test('returns rate-limit error without payment context', async () => {
538
+ const app = createApp({
539
+ auth: {
540
+ apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
541
+ rateLimit: overQuota(),
542
+ },
543
+ policy: { public: { rateLimit: { perMinute: 1 } } },
544
+ })
545
+ const response = await app.request('/protected')
546
+ const body = await response.json()
547
+
548
+ expect(response.status).toMatchInlineSnapshot(`429`)
549
+ expect(body).toMatchInlineSnapshot(`
550
+ {
551
+ "error": {
552
+ "code": "rate_limit_exceeded",
553
+ "message": "Rate limit exceeded",
554
+ },
555
+ }
556
+ `)
557
+ })
558
+
559
+ test('fails open when the public rate-limit store errors', async () => {
560
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {})
561
+ try {
562
+ const app = createApp({
563
+ auth: {
564
+ apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
565
+ rateLimit: {
566
+ async consume() {
567
+ throw new Error('Network connection lost.')
568
+ },
569
+ },
570
+ },
571
+ policy: { public: { rateLimit: { perMinute: 1 } } },
572
+ })
573
+ const response = await app.request('/protected', {
574
+ headers: { 'x-forwarded-for': '203.0.113.10' },
575
+ })
576
+ const body = await response.json()
577
+
578
+ expect(response.status).toMatchInlineSnapshot(`200`)
579
+ expect(response.headers.has('ratelimit-limit')).toMatchInlineSnapshot(`false`)
580
+ expect(body).toMatchInlineSnapshot(`
581
+ {
582
+ "id": "203.0.113.10",
583
+ "type": "public",
584
+ }
585
+ `)
586
+ expect(error.mock.calls.length).toMatchInlineSnapshot(`1`)
587
+ } finally {
588
+ error.mockRestore()
589
+ }
590
+ })
591
+
592
+ test('fails open when the API-key rate-limit store errors', async () => {
593
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {})
594
+ try {
595
+ const app = createApp({
596
+ auth: {
597
+ apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
598
+ rateLimit: {
599
+ async consume() {
600
+ throw new Error('Network connection lost.')
601
+ },
602
+ },
603
+ },
604
+ })
605
+ const response = await app.request('/protected', {
606
+ headers: { 'tempo-api-key': token },
607
+ })
608
+
609
+ expect(response.status).toMatchInlineSnapshot(`200`)
610
+ expect(response.headers.has('ratelimit-limit')).toMatchInlineSnapshot(`false`)
611
+ expect(error.mock.calls.length).toMatchInlineSnapshot(`1`)
612
+ } finally {
613
+ error.mockRestore()
614
+ }
615
+ })
616
+ })
617
+
618
+ describe('rateLimit per-scope', () => {
619
+ test('consumes the route scope bucket and emits the scope header', async () => {
620
+ const app = createScopedApp({
621
+ routes: [{ path: '/protected', policy: { apiKey: { scopes: ['data:read'] } } }],
622
+ })
623
+ const response = await app.request('/protected', {
624
+ headers: { 'tempo-api-key': token },
625
+ })
626
+
627
+ expect(response.status).toMatchInlineSnapshot(`200`)
628
+ expect(response.headers.get('ratelimit-scope')).toMatchInlineSnapshot(`"data:read"`)
629
+ })
630
+
631
+ test('does not share quota across scopes', async () => {
632
+ const auth = {
633
+ apiKey: {
634
+ rateLimits: { 'data:read': { perMinute: 1 }, 'webhooks:write': { perMinute: 1 } },
635
+ source: TestApp.source({
636
+ keys: [{ ...key, scopes: [Scope.wildcard], token }],
637
+ }),
638
+ },
639
+ rateLimit: RateLimit.memory(),
640
+ } satisfies Auth.Context
641
+ const app = createScopedApp({
642
+ auth,
643
+ routes: [
644
+ { path: '/read', policy: { apiKey: { scopes: ['data:read'] } } },
645
+ { path: '/write', policy: { apiKey: { scopes: ['webhooks:write'] } } },
646
+ ],
647
+ })
648
+
649
+ await app.request('/read', { headers: { 'tempo-api-key': token } })
650
+ const readAgain = await app.request('/read', { headers: { 'tempo-api-key': token } })
651
+ const write = await app.request('/write', { headers: { 'tempo-api-key': token } })
652
+
653
+ // Exhausting the `read` bucket leaves the `write` bucket untouched.
654
+ expect(readAgain.status).toMatchInlineSnapshot(`429`)
655
+ expect(write.status).toMatchInlineSnapshot(`200`)
656
+ expect(write.headers.get('ratelimit-scope')).toMatchInlineSnapshot(`"webhooks:write"`)
657
+ })
658
+
659
+ test('uses the * bucket for a zero-scope route', async () => {
660
+ const app = createScopedApp({
661
+ routes: [{ path: '/protected', policy: { apiKey: { scopes: [] } } }],
662
+ })
663
+ const response = await app.request('/protected', {
664
+ headers: { 'tempo-api-key': token },
665
+ })
666
+
667
+ expect(response.status).toMatchInlineSnapshot(`200`)
668
+ expect(response.headers.get('ratelimit-scope')).toMatchInlineSnapshot(`"*"`)
669
+ })
670
+
671
+ test('buckets a multi-scope route by its first required scope', async () => {
672
+ const app = createScopedApp({
673
+ routes: [
674
+ { path: '/protected', policy: { apiKey: { scopes: ['data:read', 'webhooks:write'] } } },
675
+ ],
676
+ })
677
+ const response = await app.request('/protected', {
678
+ headers: { 'tempo-api-key': token },
679
+ })
680
+
681
+ expect(response.status).toMatchInlineSnapshot(`200`)
682
+ expect(response.headers.get('ratelimit-scope')).toMatchInlineSnapshot(`"data:read"`)
683
+ })
684
+
685
+ test('route cap overrides the configured scope limit', async () => {
686
+ const auth = {
687
+ apiKey: {
688
+ rateLimits: { 'data:read': { perMinute: 1_000 } },
689
+ source: TestApp.source({
690
+ keys: [{ ...key, scopes: [Scope.wildcard], token }],
691
+ }),
692
+ },
693
+ rateLimit: RateLimit.memory(),
694
+ } satisfies Auth.Context
695
+ const app = createScopedApp({
696
+ auth,
697
+ routes: [
698
+ {
699
+ path: '/protected',
700
+ policy: { apiKey: { rateLimit: { perMinute: 5 }, scopes: ['data:read'] } },
701
+ },
702
+ ],
703
+ })
704
+ const response = await app.request('/protected', {
705
+ headers: { 'tempo-api-key': token },
706
+ })
707
+
708
+ expect(response.headers.get('ratelimit-limit')).toMatchInlineSnapshot(`"5"`)
709
+ expect(response.headers.get('ratelimit-scope')).toMatchInlineSnapshot(`"data:read"`)
710
+ })
711
+
712
+ test('per-key default overrides config default but not a route cap', async () => {
713
+ const source = TestApp.source({
714
+ keys: [
715
+ {
716
+ ...key,
717
+ rateLimits: { '*': { perMinute: 7 } },
718
+ scopes: [Scope.wildcard],
719
+ token,
720
+ },
721
+ ],
722
+ })
723
+
724
+ const open = createScopedApp({
725
+ auth: {
726
+ apiKey: { rateLimits: { '*': { perMinute: 1_000 } }, source },
727
+ rateLimit: RateLimit.memory(),
728
+ },
729
+ routes: [{ path: '/protected', policy: { apiKey: { scopes: ['data:read'] } } }],
730
+ })
731
+ const openResponse = await open.request('/protected', {
732
+ headers: { 'tempo-api-key': token },
733
+ })
734
+
735
+ const capped = createScopedApp({
736
+ auth: {
737
+ apiKey: { rateLimits: { '*': { perMinute: 1_000 } }, source },
738
+ rateLimit: RateLimit.memory(),
739
+ },
740
+ routes: [
741
+ {
742
+ path: '/protected',
743
+ policy: { apiKey: { rateLimit: { perMinute: 5 }, scopes: ['data:read'] } },
744
+ },
745
+ ],
746
+ })
747
+ const cappedResponse = await capped.request('/protected', {
748
+ headers: { 'tempo-api-key': token },
749
+ })
750
+
751
+ // Per-key `'*'` beats the config default (1000)…
752
+ expect(openResponse.headers.get('ratelimit-limit')).toMatchInlineSnapshot(`"7"`)
753
+ // …but a route's protective cap (5) still wins over it.
754
+ expect(cappedResponse.headers.get('ratelimit-limit')).toMatchInlineSnapshot(`"5"`)
755
+ })
756
+
757
+ test('per-key per-scope overrides everything including a route cap', async () => {
758
+ const auth = {
759
+ apiKey: {
760
+ source: TestApp.source({
761
+ keys: [
762
+ {
763
+ ...key,
764
+ rateLimits: { 'data:read': { perMinute: 9 } },
765
+ scopes: [Scope.wildcard],
766
+ token,
767
+ },
768
+ ],
769
+ }),
770
+ },
771
+ rateLimit: RateLimit.memory(),
772
+ } satisfies Auth.Context
773
+ const app = createScopedApp({
774
+ auth,
775
+ routes: [
776
+ {
777
+ path: '/protected',
778
+ policy: { apiKey: { rateLimit: { perMinute: 5 }, scopes: ['data:read'] } },
779
+ },
780
+ ],
781
+ })
782
+ const response = await app.request('/protected', {
783
+ headers: { 'tempo-api-key': token },
784
+ })
785
+
786
+ expect(response.headers.get('ratelimit-limit')).toMatchInlineSnapshot(`"9"`)
787
+ })
788
+
789
+ test('emits the scope header alongside a 429 over-quota error', async () => {
790
+ const entries: Log.Entry[] = []
791
+ const app = createApp({
792
+ auth: {
793
+ apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
794
+ rateLimit: overQuota(),
795
+ },
796
+ logger: (entry) => void entries.push(entry),
797
+ policy: { apiKey: { rateLimit: { perMinute: 1 }, scopes: ['data:read'] } },
798
+ })
799
+ const response = await app.request('/protected', {
800
+ headers: { 'tempo-api-key': token },
801
+ })
802
+
803
+ expect(response.status).toMatchInlineSnapshot(`429`)
804
+ expect(response.headers.get('ratelimit-scope')).toMatchInlineSnapshot(`"data:read"`)
805
+ expect(response.headers.get('ratelimit-limit')).toMatchInlineSnapshot(`"1"`)
806
+ expect(entries[0]?.principal).toMatchInlineSnapshot(`
807
+ {
808
+ "environment": "production",
809
+ "id": "key_test",
810
+ "orgId": "org_test",
811
+ "type": "api_key",
812
+ }
813
+ `)
814
+ })
815
+
816
+ test('still returns a protocol-native payment challenge on overflow', async () => {
817
+ const mpp = createMppServer()
818
+ const app = createApp({
819
+ auth: {
820
+ apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
821
+ mpp,
822
+ rateLimit: overQuota(),
823
+ },
824
+ policy: {
825
+ apiKey: { rateLimit: { perMinute: 1 }, scopes: ['data:read'] },
826
+ mpp: { charge: { amount: '1', description: 'Protected data' } },
827
+ },
828
+ })
829
+ const response = await app.request('/protected', {
830
+ headers: { 'tempo-api-key': token },
831
+ })
832
+
833
+ // The 402 challenge is owned by the payment protocol (mppx returns its own
834
+ // Response), so rate-limit headers ride the 429 path, not the challenge.
835
+ expect(response.status).toMatchInlineSnapshot(`402`)
836
+ expect(
837
+ response.headers.get('www-authenticate')?.startsWith('Payment '),
838
+ ).toMatchInlineSnapshot(`true`)
839
+ })
840
+ })
841
+ })
842
+
843
+ function createScopedApp(options: {
844
+ auth?: Auth.Context | undefined
845
+ routes: readonly { path: string; policy: Auth.require.Policy }[]
846
+ }) {
847
+ const auth = options.auth ?? {
848
+ apiKey: {
849
+ source: TestApp.source({ keys: [{ ...key, scopes: [Scope.wildcard], token }] }),
850
+ },
851
+ rateLimit: RateLimit.memory(),
852
+ }
853
+ const app = new Hono<Auth.Environment>()
854
+
855
+ app.use('*', async (c, next) => {
856
+ c.set('auth', auth)
857
+ await next()
858
+ })
859
+ for (const route of options.routes) {
860
+ app.use(route.path, Auth.require(route.policy))
861
+ app.get(route.path, (c) => c.json(serializePrincipal(Auth.getPrincipal(c))))
862
+ }
863
+
864
+ return app
865
+ }
866
+
867
+ type Options = {
868
+ auth?: Auth.Context | undefined
869
+ logger?: Log.Emit | undefined
870
+ policy?: Auth.require.Policy | undefined
871
+ }
872
+
873
+ function createApp(options: Options = {}) {
874
+ const auth = options.auth ?? {
875
+ apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
876
+ rateLimit: RateLimit.memory(),
877
+ }
878
+ const app = new Hono<Auth.Environment>()
879
+
880
+ if (options.logger) {
881
+ app.use('*', requestId())
882
+ app.use('*', Log.middleware({ emit: options.logger }))
883
+ }
884
+ app.use('*', async (c, next) => {
885
+ c.set('auth', auth)
886
+ await next()
887
+ })
888
+ app.use('/protected', Auth.require(options.policy ?? { apiKey: { scopes: ['data:read'] } }))
889
+ app.get('/protected', (c) => c.json(serializePrincipal(Auth.getPrincipal(c))))
890
+
891
+ return app
892
+ }
893
+
894
+ function createMppServer() {
895
+ return Mppx.create({
896
+ methods: [
897
+ tempo.charge({
898
+ chainId: Tempo.chain.id,
899
+ currency: Tempo.currency,
900
+ decimals: 6,
901
+ getClient: () => Tempo.client,
902
+ recipient: Tempo.accounts[2].address,
903
+ }),
904
+ ],
905
+ realm: 'tempo-api-test',
906
+ secretKey: 'secret_test_key_0123456789abcdef',
907
+ })
908
+ }
909
+
910
+ function overQuota(): RateLimit.Store {
911
+ return {
912
+ async consume(options) {
913
+ return {
914
+ allowed: false,
915
+ limit: options.limit.perMinute,
916
+ remaining: 0,
917
+ reset: Math.ceil(Date.now() / 1_000) + 60,
918
+ }
919
+ },
920
+ }
921
+ }
922
+
923
+ function overQuotaOnce(): RateLimit.Store {
924
+ let consumed = false
925
+ return {
926
+ async consume(options) {
927
+ if (consumed) throw new Error('rate limit should not be consumed after payment')
928
+ consumed = true
929
+ return {
930
+ allowed: false,
931
+ limit: options.limit.perMinute,
932
+ remaining: 0,
933
+ reset: Math.ceil(Date.now() / 1_000) + 60,
934
+ }
935
+ },
936
+ }
937
+ }
938
+
939
+ function serializePrincipal(principal: Auth.Principal | null) {
940
+ if (!principal) return null
941
+ const payment = principal.payment ? { payment: principal.payment } : {}
942
+
943
+ if (principal.type === 'api_key')
944
+ return {
945
+ id: principal.id,
946
+ orgId: principal.orgId,
947
+ ...payment,
948
+ type: principal.type,
949
+ }
950
+
951
+ return {
952
+ id: principal.id,
953
+ ...payment,
954
+ type: principal.type,
955
+ }
956
+ }
957
+
958
+ function normalizePaymentPayer(value: unknown) {
959
+ if (!isRecord(value) || !isRecord(value['payment'])) return value
960
+
961
+ // The payer account is generated from a random mnemonic each run. Keep the
962
+ // chain-qualified DID shape in snapshots without pinning the address.
963
+ return {
964
+ ...value,
965
+ payment: {
966
+ ...value['payment'],
967
+ payer:
968
+ typeof value['payment']['payer'] === 'string'
969
+ ? value['payment']['payer'].replace(
970
+ /^did:pkh:eip155:\d+:0x[0-9a-fA-F]{40}$/,
971
+ 'did:pkh:eip155:...:0x...',
972
+ )
973
+ : value['payment']['payer'],
974
+ },
975
+ }
976
+ }
977
+
978
+ function isRecord(value: unknown): value is Record<string, unknown> {
979
+ return !!value && typeof value === 'object' && !Array.isArray(value)
980
+ }