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,149 @@
1
+ /**
2
+ * Opaque keyset-pagination cursor: a positional tuple encoding the sort-key of
3
+ * the last returned row (e.g. `[block, index]` for an append-only feed, or
4
+ * `[balance, address]` for a balance ranking). Pages are anchored to this
5
+ * position rather than a numeric offset, so rows arriving at the head of a feed
6
+ * cannot shift items across page boundaries (the duplicate/skip hazard of offset
7
+ * pagination over a live feed).
8
+ *
9
+ * The token is opaque on purpose: callers must treat it as a blob and pass back
10
+ * the `nextCursor` from the previous page verbatim. Because cursor values are
11
+ * inlined into upstream SQL, every field is validated on decode against its
12
+ * declared `Field` kind and re-serialized into a safe SQL literal via `literal`,
13
+ * so a malformed or forged cursor degrades to "start from the head" rather than
14
+ * reaching the query.
15
+ */
16
+ export type Cursor = (string | number)[]
17
+
18
+ /** The kind of a cursor field, determining its validation and SQL literal form. */
19
+ export type Field = 'int' | 'uint' | 'address' | 'timestamp'
20
+
21
+ /** Encodes a positional tuple as an opaque, URL-safe token. */
22
+ export function encode(values: Cursor): string {
23
+ return base64UrlEncode(JSON.stringify(values))
24
+ }
25
+
26
+ /**
27
+ * Splits a keyset query result (fetched with one extra row, `LIMIT n + 1`) into
28
+ * a page. Returns the page `rows` (capped at `limit`), whether `hasMore` rows
29
+ * follow, and the `nextCursor` anchoring the next page below the last row.
30
+ *
31
+ * `key` derives the next-page cursor tuple from the last row of the page,
32
+ * returning `undefined` when the row's sort-key can't be extracted (then
33
+ * `nextCursor` is `null`). Callers build their typed `data` from `rows`.
34
+ */
35
+ export function paginate<row>(options: {
36
+ /** The raw query result, fetched with one extra row (`LIMIT limit + 1`). */
37
+ rows: readonly row[]
38
+ /** The requested page size. */
39
+ limit: number
40
+ /** Derives the next-page cursor tuple from the last row of the page. */
41
+ key: (row: row) => Cursor | undefined
42
+ }): { hasMore: boolean; nextCursor: string | null; rows: row[] } {
43
+ const { rows, limit, key } = options
44
+ const pageRows = rows.slice(0, limit)
45
+ const hasMore = rows.length > limit
46
+ const last = pageRows.at(-1)
47
+ const tuple = hasMore && last !== undefined ? key(last) : undefined
48
+ return { hasMore, nextCursor: tuple ? encode(tuple) : null, rows: pageRows }
49
+ }
50
+
51
+ /**
52
+ * Decodes a cursor token, validating arity and each field against `fields`.
53
+ * Returns `undefined` for anything malformed/forged so a bad cursor falls back
54
+ * to the head page instead of erroring (or reaching SQL).
55
+ */
56
+ export function decode(token: string, fields: readonly Field[]): Cursor | undefined {
57
+ try {
58
+ const parsed = JSON.parse(base64UrlDecode(token)) as unknown
59
+ if (!Array.isArray(parsed) || parsed.length !== fields.length) return undefined
60
+ for (let i = 0; i < fields.length; i++) if (!isValid(parsed[i], fields[i]!)) return undefined
61
+ return parsed as Cursor
62
+ } catch {
63
+ return undefined
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Renders a (decode-validated) cursor value as a safe SQL literal: integers and
69
+ * uint256 decimals are inlined bare; addresses are lowercased and quoted;
70
+ * timestamps are quoted. Only ever call with values returned from `decode`.
71
+ */
72
+ export function literal(value: string | number, field: Field): string {
73
+ switch (field) {
74
+ case 'int':
75
+ case 'uint':
76
+ return String(value)
77
+ case 'address':
78
+ return `'${String(value).toLowerCase()}'`
79
+ case 'timestamp':
80
+ return `'${String(value)}'`
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Builds a strict lexicographic keyset predicate for the given ORDER BY columns
86
+ * and direction. Columns carry pre-rendered `literal`s.
87
+ *
88
+ * When every column sorts the same way, this emits a row-value (tuple)
89
+ * comparison, e.g. for `ORDER BY a DESC, b DESC` after `(va, vb)`:
90
+ * `((a, b) < (va, vb))`. This single-predicate form is the only one the
91
+ * indexer's signature-decoded queries accept (they reject the equivalent
92
+ * `OR`-expansion), and standard Postgres / ClickHouse support it too.
93
+ *
94
+ * When directions are mixed (e.g. a ranking `ORDER BY balance DESC, addr ASC`),
95
+ * a tuple comparison can't express it, so it falls back to the `OR` expansion:
96
+ * `((a < va) OR (a = va AND b > vb))`. Mixed-direction keysets only run against
97
+ * ClickHouse, which accepts the `OR` form.
98
+ *
99
+ * Multi-column keysets additionally AND a redundant non-strict bound on the
100
+ * first column (`a <= va`): neither the tuple nor the `OR` form alone lets the
101
+ * ClickHouse planner prune the primary-key range, so without the bare bound a
102
+ * deep cursor page scans from the tip (see the Q1 notes in `activities.ts`).
103
+ */
104
+ export function keyset(
105
+ columns: readonly { name: string; literal: string; order: 'asc' | 'desc' }[],
106
+ ): string {
107
+ const first = columns[0]!
108
+ const bound =
109
+ columns.length > 1
110
+ ? `${first.name} ${first.order === 'asc' ? '>=' : '<='} ${first.literal} AND `
111
+ : ''
112
+ const uniform = columns.every((column) => column.order === first.order)
113
+ if (uniform) {
114
+ const op = first.order === 'asc' ? '>' : '<'
115
+ const names = columns.map((column) => column.name).join(', ')
116
+ const literals = columns.map((column) => column.literal).join(', ')
117
+ return `(${bound}(${names}) ${op} (${literals}))`
118
+ }
119
+
120
+ const clauses: string[] = []
121
+ for (let i = 0; i < columns.length; i++) {
122
+ const parts = columns.slice(0, i).map((column) => `${column.name} = ${column.literal}`)
123
+ const op = columns[i]!.order === 'asc' ? '>' : '<'
124
+ parts.push(`${columns[i]!.name} ${op} ${columns[i]!.literal}`)
125
+ clauses.push(`(${parts.join(' AND ')})`)
126
+ }
127
+ return `(${bound}(${clauses.join(' OR ')}))`
128
+ }
129
+
130
+ function isValid(value: unknown, field: Field): boolean {
131
+ switch (field) {
132
+ case 'int':
133
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0
134
+ case 'uint':
135
+ return typeof value === 'string' && /^\d+$/.test(value)
136
+ case 'address':
137
+ return typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value)
138
+ case 'timestamp':
139
+ return typeof value === 'string' && !Number.isNaN(Date.parse(value))
140
+ }
141
+ }
142
+
143
+ function base64UrlEncode(value: string): string {
144
+ return btoa(value).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '')
145
+ }
146
+
147
+ function base64UrlDecode(value: string): string {
148
+ return atob(value.replaceAll('-', '+').replaceAll('_', '/'))
149
+ }
@@ -0,0 +1,234 @@
1
+ import { Hono } from 'hono'
2
+ import * as EdgeCache from './EdgeCache.js'
3
+ import type * as Store from './Store.js'
4
+
5
+ describe('middleware', () => {
6
+ test('serves GETs from cache, skipping downstream middleware', async () => {
7
+ const { app, store, downstreamCalls } = createApp()
8
+
9
+ const first = await app.request('/cached')
10
+ const second = await app.request('/cached')
11
+
12
+ // The second request is served from the edge cache, so the downstream
13
+ // (auth/rate-limit) middleware runs only once.
14
+ expect(downstreamCalls()).toMatchInlineSnapshot(`1`)
15
+ expect(store.size).toMatchInlineSnapshot(`1`)
16
+
17
+ expect(await first.json()).toMatchInlineSnapshot(`
18
+ {
19
+ "ok": true,
20
+ }
21
+ `)
22
+ expect(await second.json()).toMatchInlineSnapshot(`
23
+ {
24
+ "ok": true,
25
+ }
26
+ `)
27
+
28
+ // The miss returns the route's own (private, metered) response; the stored
29
+ // copy served on the hit is public and stripped of per-request headers.
30
+ expect(first.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`"9"`)
31
+ expect(second.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`null`)
32
+ expect(second.headers.get('server-timing')).toMatchInlineSnapshot(`null`)
33
+ expect(second.headers.get('cache-control')).toMatchInlineSnapshot(
34
+ `"public, max-age=30, s-maxage=30, stale-while-revalidate=300"`,
35
+ )
36
+ expect(second.headers.get('vary')).toMatchInlineSnapshot(`"Accept-Encoding"`)
37
+ })
38
+
39
+ test('serves authenticated requests from the same shared cache entry', async () => {
40
+ const { app, store, downstreamCalls } = createApp()
41
+
42
+ // An authenticated request warms the cache; subsequent requests — whether
43
+ // authenticated (with any key) or anonymous — are all served from the one
44
+ // shared entry without re-running downstream auth/rate-limiting.
45
+ await app.request('/cached', { headers: { authorization: 'Bearer one' } })
46
+ await app.request('/cached', { headers: { 'tempo-api-key': 'two' } })
47
+ await app.request('/cached')
48
+
49
+ expect(downstreamCalls()).toMatchInlineSnapshot(`1`)
50
+ expect(store.size).toMatchInlineSnapshot(`1`)
51
+ })
52
+
53
+ test('refreshes the entry when the request sends `Cache-Control: no-cache`', async () => {
54
+ const { app, store, downstreamCalls } = createApp()
55
+
56
+ await app.request('/cached')
57
+ // `no-cache` skips the stored hit, re-runs downstream, and overwrites the
58
+ // entry with the fresh response.
59
+ const refreshed = await app.request('/cached', {
60
+ headers: { 'cache-control': 'no-cache' },
61
+ })
62
+ const after = await app.request('/cached')
63
+
64
+ expect(downstreamCalls()).toMatchInlineSnapshot(`2`)
65
+ expect(store.size).toMatchInlineSnapshot(`1`)
66
+ // The refresh returns the route's own (private, metered) response, and the
67
+ // following request is served from the rewritten entry.
68
+ expect(refreshed.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`"9"`)
69
+ expect(after.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`null`)
70
+ })
71
+
72
+ test('refreshes the entry when the request sends `Pragma: no-cache`', async () => {
73
+ const { app, store, downstreamCalls } = createApp()
74
+
75
+ await app.request('/cached')
76
+ // `Pragma` is the HTTP/1.0 fallback for `Cache-Control: no-cache`.
77
+ await app.request('/cached', { headers: { pragma: 'no-cache' } })
78
+
79
+ expect(downstreamCalls()).toMatchInlineSnapshot(`2`)
80
+ expect(store.size).toMatchInlineSnapshot(`1`)
81
+ })
82
+
83
+ test('strips CDN telemetry headers from stored copies', async () => {
84
+ const { app } = createApp()
85
+
86
+ // The miss response carries `Age`/`CF-Cache-Status` (e.g. stamped by
87
+ // Cloudflare on an inner Cache API hit); the stored copy must not freeze
88
+ // them, or every later hit would report the same stale telemetry.
89
+ await app.request('/cached')
90
+ const hit = await app.request('/cached')
91
+
92
+ expect(hit.headers.get('age')).toMatchInlineSnapshot(`null`)
93
+ expect(hit.headers.get('cf-cache-status')).toMatchInlineSnapshot(`null`)
94
+ })
95
+
96
+ test('bypasses the cache entirely when the request sends `Cache-Control: no-store`', async () => {
97
+ const { app, store, downstreamCalls } = createApp()
98
+
99
+ await app.request('/cached')
100
+ // `no-store` neither serves nor writes the cache: the entry from the first
101
+ // request stays, but this request always runs downstream.
102
+ const bypassed = await app.request('/cached', {
103
+ headers: { 'cache-control': 'no-store' },
104
+ })
105
+
106
+ expect(downstreamCalls()).toMatchInlineSnapshot(`2`)
107
+ expect(store.size).toMatchInlineSnapshot(`1`)
108
+ expect(bypassed.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`"9"`)
109
+ })
110
+
111
+ test('does not cache non-2xx responses', async () => {
112
+ const { app, store, downstreamCalls } = createApp()
113
+
114
+ await app.request('/error')
115
+ await app.request('/error')
116
+
117
+ expect(downstreamCalls()).toMatchInlineSnapshot(`2`)
118
+ expect(store.size).toMatchInlineSnapshot(`0`)
119
+ })
120
+
121
+ test('does not cache routes that did not opt in', async () => {
122
+ const { app, store, downstreamCalls } = createApp()
123
+
124
+ await app.request('/uncached')
125
+ await app.request('/uncached')
126
+
127
+ expect(downstreamCalls()).toMatchInlineSnapshot(`2`)
128
+ expect(store.size).toMatchInlineSnapshot(`0`)
129
+ })
130
+
131
+ test('answers conditional requests from a cached ETag with 304', async () => {
132
+ const { app } = createApp()
133
+
134
+ const first = await app.request('/cached')
135
+ const etag = first.headers.get('etag') ?? ''
136
+ const conditional = await app.request('/cached', { headers: { 'if-none-match': etag } })
137
+
138
+ expect(etag).toMatchInlineSnapshot(`""abc""`)
139
+ expect(conditional.status).toMatchInlineSnapshot(`304`)
140
+ expect(await conditional.text()).toMatchInlineSnapshot(`""`)
141
+ expect(conditional.headers.get('etag')).toMatchInlineSnapshot(`""abc""`)
142
+ })
143
+
144
+ test('round-trips binary bodies through the text store without corruption', async () => {
145
+ const { app, store, downstreamCalls } = createApp()
146
+
147
+ const miss = await app.request('/binary')
148
+ const hit = await app.request('/binary')
149
+
150
+ // The second request is served from the cache (downstream ran once).
151
+ expect(downstreamCalls()).toMatchInlineSnapshot(`1`)
152
+ expect(store.size).toMatchInlineSnapshot(`1`)
153
+ expect(hit.headers.get('content-type')).toMatchInlineSnapshot(`"image/png"`)
154
+
155
+ const expected = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0x00]
156
+ expect([...new Uint8Array(await miss.arrayBuffer())]).toEqual(expected)
157
+ expect([...new Uint8Array(await hit.arrayBuffer())]).toEqual(expected)
158
+ })
159
+
160
+ test('is a no-op when no edge cache is available', async () => {
161
+ const { app, downstreamCalls } = createApp({ store: undefined })
162
+
163
+ await app.request('/cached')
164
+ await app.request('/cached')
165
+
166
+ expect(downstreamCalls()).toMatchInlineSnapshot(`2`)
167
+ })
168
+ })
169
+
170
+ type Environment = { Variables: EdgeCache.Variables }
171
+
172
+ // In-memory `Store.Store` so the edge cache and origin cache share one shape.
173
+ function memoryStore() {
174
+ const map = new Map<string, string>()
175
+ return {
176
+ async delete(key: string) {
177
+ map.delete(key)
178
+ },
179
+ async get(key: string) {
180
+ return map.get(key) ?? null
181
+ },
182
+ async list() {
183
+ return { keys: [...map.keys()].map((name) => ({ name })) }
184
+ },
185
+ async put(key: string, value: string) {
186
+ map.set(key, value)
187
+ },
188
+ get size() {
189
+ return map.size
190
+ },
191
+ type: 'state',
192
+ } satisfies Store.Store & { readonly size: number }
193
+ }
194
+
195
+ function createApp(options: { store?: Store.Store | undefined } = {}) {
196
+ const store = memoryStore()
197
+ const edgeStore = 'store' in options ? options.store : store
198
+ let downstream = 0
199
+
200
+ const app = new Hono<Environment>()
201
+ app.use('*', EdgeCache.middleware<Environment>({ store: edgeStore }))
202
+ app.use('*', async (_c, next) => {
203
+ downstream++
204
+ await next()
205
+ })
206
+ app.get('/cached', (c) => {
207
+ c.set('edgeCache', { maxAge: 30, staleWhileRevalidate: 300 })
208
+ c.header('Age', '123')
209
+ c.header('CF-Cache-Status', 'HIT')
210
+ c.header('ETag', '"abc"')
211
+ c.header('Server-Timing', 'app;dur=1')
212
+ c.header('RateLimit-Remaining', '9')
213
+ return c.json({ ok: true })
214
+ })
215
+ app.get('/error', (c) => {
216
+ c.set('edgeCache', { maxAge: 30, staleWhileRevalidate: 300 })
217
+ return c.json({ ok: false }, 500)
218
+ })
219
+ app.get('/binary', (c) => {
220
+ c.set('edgeCache', { maxAge: 30, staleWhileRevalidate: 300 })
221
+ // Bytes that are not valid UTF-8 (PNG signature) — corrupted if stored via
222
+ // `.text()` instead of base64.
223
+ return c.body(
224
+ new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0x00]),
225
+ 200,
226
+ {
227
+ 'Content-Type': 'image/png',
228
+ },
229
+ )
230
+ })
231
+ app.get('/uncached', (c) => c.json({ ok: true }))
232
+
233
+ return { app, store, downstreamCalls: () => downstream }
234
+ }
@@ -0,0 +1,224 @@
1
+ import type { Env, MiddlewareHandler } from 'hono'
2
+ import { Base64 } from 'ox'
3
+
4
+ import type * as Store from './Store.js'
5
+
6
+ /**
7
+ * Per-request edge-cache policy published by `Cache.response` for routes that
8
+ * opted into response caching. Its presence is what marks a response as safe to
9
+ * persist in the shared edge cache.
10
+ */
11
+ export type Policy = {
12
+ /** Shared max-age / s-maxage in seconds for the stored response. */
13
+ maxAge: number
14
+ /** Stale-while-revalidate window in seconds, if any. */
15
+ staleWhileRevalidate?: number | undefined
16
+ }
17
+
18
+ /** Hono variables published by the edge-cache layer. */
19
+ export type Variables = {
20
+ /** Edge-cache policy for the current route, set by `Cache.response`. */
21
+ edgeCache?: Policy | undefined
22
+ /** `'hit'` when the edge cache served the response without reaching the origin handlers. */
23
+ edgeCacheStatus?: 'hit' | undefined
24
+ }
25
+
26
+ // Per-request headers that must never be persisted in a shared cache entry.
27
+ // `age`/`cf-cache-status` are CDN telemetry stamped by Cloudflare on Cache API
28
+ // hits (e.g. the inner `Cache.response` layer); storing them would freeze a
29
+ // stale `Age` into every copy served from this cache.
30
+ const volatileHeaders = [
31
+ 'age',
32
+ 'cf-cache-status',
33
+ 'content-encoding',
34
+ 'content-length',
35
+ 'server-timing',
36
+ 'set-cookie',
37
+ 'ratelimit-limit',
38
+ 'ratelimit-remaining',
39
+ 'ratelimit-reset',
40
+ 'tempo-request-id',
41
+ ]
42
+
43
+ /**
44
+ * Serves GET responses from the edge cache store BEFORE auth and rate-limiting
45
+ * run, so cache hits skip the per-request store round-trips that dominate
46
+ * latency — for every caller, authenticated or not. Place this ahead of the
47
+ * auth middleware.
48
+ *
49
+ * Only 2xx responses from routes that opted into caching (via `Cache.response`,
50
+ * which publishes the `edgeCache` policy) are stored, and the policy is only
51
+ * published for routes whose responses are public and URL-keyed (no per-principal
52
+ * data), so a single shared entry is safe to serve to all callers. The trade-off
53
+ * is that cache hits bypass origin metering: they are not counted against
54
+ * rate-limit quotas.
55
+ *
56
+ * The cache is a plain `Store.Store` — the same store the origin read cache uses
57
+ * — holding the response serialized to a string under a URL key. It is a no-op
58
+ * when no store is supplied.
59
+ *
60
+ * Standard request `Cache-Control` directives let a caller invalidate without an
61
+ * out-of-band purge: `no-cache` forces a refresh (skip the stored hit, re-fetch,
62
+ * overwrite the entry) and `no-store` bypasses the cache entirely. Both fall
63
+ * through to the origin and are metered normally. The store is colo-local, so a
64
+ * refresh only affects the data center serving that request.
65
+ */
66
+ export function middleware<environment extends Env>(
67
+ options: middleware.Options = {},
68
+ ): MiddlewareHandler<environment> {
69
+ return async (c, next) => {
70
+ const { store } = options
71
+ const eligible =
72
+ store !== undefined && c.req.method === 'GET' && !c.req.raw.headers.has('range')
73
+ const key = eligible ? cacheKey(c.req.url) : undefined
74
+
75
+ // Honor the standard request `Cache-Control` directives so a caller can
76
+ // refresh a stale entry on the colo serving them without any out-of-band
77
+ // purge: `no-cache` skips the stored hit but still re-stores the fresh
78
+ // response (revalidate), and `no-store` bypasses the cache on both sides.
79
+ // `Pragma: no-cache` is the HTTP/1.0 fallback, honored only when
80
+ // `Cache-Control` is absent (RFC 9111 §5.4). A forced miss falls through
81
+ // to `next()` and is metered like any other origin request, so this is
82
+ // not an unmetered-access vector.
83
+ const directives = (c.req.header('cache-control') ?? c.req.header('pragma') ?? '').toLowerCase()
84
+ const noStore = directives.includes('no-store')
85
+ const noCache = noStore || directives.includes('no-cache')
86
+
87
+ if (store && key && !noCache) {
88
+ const hit = await store.get(key)
89
+ if (hit) {
90
+ // Mark the hit for the canonical request log line; routing never runs
91
+ // for hits, so this is the only signal that the cache answered.
92
+ c.set('edgeCacheStatus' as never, 'hit' as never)
93
+ return serveHit(c.req.header('if-none-match'), hit)
94
+ }
95
+ }
96
+
97
+ await next()
98
+
99
+ if (!store || !key || noStore) return
100
+ const policy = c.get('edgeCache' as never) as Policy | undefined
101
+ if (!policy) return
102
+ if (c.res.status < 200 || c.res.status >= 300) return
103
+ if (c.res.headers.has('set-cookie')) return
104
+ if (c.res.headers.get('content-type')?.includes('text/event-stream')) return
105
+
106
+ // Store a public, sanitized copy; the client still receives the original
107
+ // (private, metered) response for this miss.
108
+ const value = await serialize(c.res, policy)
109
+ const put = store.put(key, value, { ttl: policy.maxAge * 1_000 })
110
+ const waitUntil = getWaitUntil(c)
111
+ if (waitUntil) waitUntil(put)
112
+ else await put.catch(() => {})
113
+ return
114
+ }
115
+ }
116
+
117
+ export declare namespace middleware {
118
+ /** Options for the edge-cache middleware. */
119
+ type Options = {
120
+ /** Store backing the edge cache. The layer is a no-op when omitted. */
121
+ store?: Store.Store | undefined
122
+ }
123
+ }
124
+
125
+ /** A response serialized for storage in the string-keyed edge cache. */
126
+ type Stored = {
127
+ /** Response body. UTF-8 text, or base64 when `encoding` is `'base64'`. */
128
+ body: string
129
+ /** Set to `'base64'` when `body` holds base64-encoded binary bytes. */
130
+ encoding?: 'base64' | undefined
131
+ /** Response headers, already sanitized for shared caching. */
132
+ headers: [string, string][]
133
+ /** Response status code. */
134
+ status: number
135
+ }
136
+
137
+ async function serialize(response: Response, policy: Policy): Promise<string> {
138
+ const headers = new Headers(response.headers)
139
+ for (const name of volatileHeaders) headers.delete(name)
140
+ headers.set('Cache-Control', cacheControl(policy))
141
+ headers.set('Vary', 'Accept-Encoding')
142
+ // The store is text-only, so binary bodies (e.g. PNG token logos) must be
143
+ // base64-encoded or the round trip through `.text()` corrupts them. Text
144
+ // bodies (JSON, SVG, HTML) stay verbatim to avoid the 33% base64 overhead.
145
+ const textual = isTextual(headers.get('content-type'))
146
+ const stored: Stored = {
147
+ body: textual
148
+ ? await response.clone().text()
149
+ : Base64.fromBytes(new Uint8Array(await response.clone().arrayBuffer())),
150
+ ...(textual ? {} : { encoding: 'base64' as const }),
151
+ headers: [...headers],
152
+ status: response.status,
153
+ }
154
+ return JSON.stringify(stored)
155
+ }
156
+
157
+ function serveHit(ifNoneMatch: string | undefined, value: string): Response {
158
+ const { body, encoding, headers, status } = JSON.parse(value) as Stored
159
+ const responseHeaders = new Headers(headers)
160
+ const etag = responseHeaders.get('etag')
161
+ if (etag && ifNoneMatch && etagMatches(etag, ifNoneMatch)) {
162
+ const notModified = new Headers()
163
+ for (const name of ['Cache-Control', 'ETag', 'Vary']) {
164
+ const header = responseHeaders.get(name)
165
+ if (header) notModified.set(name, header)
166
+ }
167
+ return new Response(null, { headers: notModified, status: 304, statusText: 'Not Modified' })
168
+ }
169
+ if (encoding === 'base64') {
170
+ // Copy the decoded bytes into an exact `ArrayBuffer`: `Base64.toBytes`
171
+ // returns a `Uint8Array` view, which this TS lib does not accept as a
172
+ // `BodyInit`.
173
+ const bytes = Base64.toBytes(body)
174
+ const buffer = new ArrayBuffer(bytes.byteLength)
175
+ new Uint8Array(buffer).set(bytes)
176
+ return new Response(buffer, { headers: responseHeaders, status })
177
+ }
178
+ return new Response(body, { headers: responseHeaders, status })
179
+ }
180
+
181
+ // A body is safe to store as verbatim text when its content type is textual;
182
+ // everything else (images, octet-stream) is treated as binary and base64-coded.
183
+ function isTextual(contentType: string | null): boolean {
184
+ if (!contentType) return false
185
+ return /^(?:text\/|application\/(?:json|xml|javascript|[\w.-]+\+(?:json|xml))|image\/svg\+xml)/i.test(
186
+ contentType,
187
+ )
188
+ }
189
+
190
+ // Sort query params so semantically identical URLs share one cache entry.
191
+ function cacheKey(requestUrl: string) {
192
+ const url = new URL(requestUrl)
193
+ url.searchParams.sort()
194
+ return url.toString()
195
+ }
196
+
197
+ function getWaitUntil(c: { executionCtx?: { waitUntil(promise: Promise<unknown>): void } }) {
198
+ // `c.executionCtx` throws on runtimes without one (e.g. Node), so probe it
199
+ // defensively and fall back to awaiting the write inline.
200
+ try {
201
+ const ctx = c.executionCtx
202
+ return ctx ? ctx.waitUntil.bind(ctx) : undefined
203
+ } catch {
204
+ return undefined
205
+ }
206
+ }
207
+
208
+ function etagMatches(etag: string, ifNoneMatch: string) {
209
+ const strip = (tag: string) => tag.trim().replace(/^W\//, '')
210
+ return ifNoneMatch.split(',').some((tag) => strip(tag) === strip(etag))
211
+ }
212
+
213
+ /**
214
+ * Builds the shared-cache `Cache-Control` value for a stored copy: `public`
215
+ * with `max-age`/`s-maxage` bound to the policy plus an optional
216
+ * `stale-while-revalidate` window. Used for this layer's stored copies and by
217
+ * `Cache.response` to bound the inner response cache's entry lifetime.
218
+ */
219
+ export function cacheControl(policy: Policy) {
220
+ const directives = ['public', `max-age=${policy.maxAge}`, `s-maxage=${policy.maxAge}`]
221
+ if (policy.staleWhileRevalidate)
222
+ directives.push(`stale-while-revalidate=${policy.staleWhileRevalidate}`)
223
+ return directives.join(', ')
224
+ }
@@ -0,0 +1,14 @@
1
+ import { Tidx } from 'tidx.ts'
2
+
3
+ /**
4
+ * Extract a status code from a TIDX fetch error or any error that carries a
5
+ * numeric `status` property. Returns undefined when the error has none.
6
+ */
7
+ export function getStatus(cause: unknown) {
8
+ if (cause instanceof Tidx.FetchRequestError) return cause.status
9
+ if (typeof cause === 'object' && cause && 'status' in cause) {
10
+ const { status } = cause as { status?: unknown }
11
+ if (typeof status === 'number') return status
12
+ }
13
+ return undefined
14
+ }