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.
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/ApiKey.d.ts +65 -0
- package/dist/ApiKey.d.ts.map +1 -0
- package/dist/ApiKey.js +105 -0
- package/dist/ApiKey.js.map +1 -0
- package/dist/ApiKeySource.d.ts +166 -0
- package/dist/ApiKeySource.d.ts.map +1 -0
- package/dist/ApiKeySource.js +234 -0
- package/dist/ApiKeySource.js.map +1 -0
- package/dist/App.d.ts +19317 -0
- package/dist/App.d.ts.map +1 -0
- package/dist/App.js +612 -0
- package/dist/App.js.map +1 -0
- package/dist/Assets.d.ts +44 -0
- package/dist/Assets.d.ts.map +1 -0
- package/dist/Assets.js +23 -0
- package/dist/Assets.js.map +1 -0
- package/dist/Auth.d.ts +2 -0
- package/dist/Auth.d.ts.map +1 -0
- package/dist/Auth.js +2 -0
- package/dist/Auth.js.map +1 -0
- package/dist/Cli.d.ts +21 -0
- package/dist/Cli.d.ts.map +1 -0
- package/dist/Cli.js +13 -0
- package/dist/Cli.js.map +1 -0
- package/dist/Client.d.ts +20 -0
- package/dist/Client.d.ts.map +1 -0
- package/dist/Client.js +24 -0
- package/dist/Client.js.map +1 -0
- package/dist/Metrics.d.ts +70 -0
- package/dist/Metrics.d.ts.map +1 -0
- package/dist/Metrics.js +91 -0
- package/dist/Metrics.js.map +1 -0
- package/dist/Scope.d.ts +35 -0
- package/dist/Scope.d.ts.map +1 -0
- package/dist/Scope.js +38 -0
- package/dist/Scope.js.map +1 -0
- package/dist/Webhooks.d.ts +79 -0
- package/dist/Webhooks.d.ts.map +1 -0
- package/dist/Webhooks.js +107 -0
- package/dist/Webhooks.js.map +1 -0
- package/dist/admin/Access.d.ts +41 -0
- package/dist/admin/Access.d.ts.map +1 -0
- package/dist/admin/Access.js +75 -0
- package/dist/admin/Access.js.map +1 -0
- package/dist/admin/App.d.ts +1221 -0
- package/dist/admin/App.d.ts.map +1 -0
- package/dist/admin/App.js +113 -0
- package/dist/admin/App.js.map +1 -0
- package/dist/admin/apps/api-keys.d.ts +209 -0
- package/dist/admin/apps/api-keys.d.ts.map +1 -0
- package/dist/admin/apps/api-keys.js +129 -0
- package/dist/admin/apps/api-keys.js.map +1 -0
- package/dist/admin/apps/scopes.d.ts +46 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -0
- package/dist/admin/apps/scopes.js +41 -0
- package/dist/admin/apps/scopes.js.map +1 -0
- package/dist/admin/apps/verified-tokens.d.ts +1206 -0
- package/dist/admin/apps/verified-tokens.d.ts.map +1 -0
- package/dist/admin/apps/verified-tokens.js +485 -0
- package/dist/admin/apps/verified-tokens.js.map +1 -0
- package/dist/admin/index.d.ts +3 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +3 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/ui.generated.d.ts +11 -0
- package/dist/admin/ui.generated.d.ts.map +1 -0
- package/dist/admin/ui.generated.js +43 -0
- package/dist/admin/ui.generated.js.map +1 -0
- package/dist/apps/activities.d.ts +18725 -0
- package/dist/apps/activities.d.ts.map +1 -0
- package/dist/apps/activities.js +3138 -0
- package/dist/apps/activities.js.map +1 -0
- package/dist/apps/addresses.d.ts +3662 -0
- package/dist/apps/addresses.d.ts.map +1 -0
- package/dist/apps/addresses.js +22 -0
- package/dist/apps/addresses.js.map +1 -0
- package/dist/apps/balances.d.ts +342 -0
- package/dist/apps/balances.d.ts.map +1 -0
- package/dist/apps/balances.js +393 -0
- package/dist/apps/balances.js.map +1 -0
- package/dist/apps/blocks.d.ts +931 -0
- package/dist/apps/blocks.d.ts.map +1 -0
- package/dist/apps/blocks.js +689 -0
- package/dist/apps/blocks.js.map +1 -0
- package/dist/apps/coingecko.d.ts +1053 -0
- package/dist/apps/coingecko.d.ts.map +1 -0
- package/dist/apps/coingecko.js +1015 -0
- package/dist/apps/coingecko.js.map +1 -0
- package/dist/apps/exchanges.d.ts +3000 -0
- package/dist/apps/exchanges.d.ts.map +1 -0
- package/dist/apps/exchanges.js +3570 -0
- package/dist/apps/exchanges.js.map +1 -0
- package/dist/apps/fee-amm.d.ts +640 -0
- package/dist/apps/fee-amm.d.ts.map +1 -0
- package/dist/apps/fee-amm.js +815 -0
- package/dist/apps/fee-amm.js.map +1 -0
- package/dist/apps/indexer.d.ts +116 -0
- package/dist/apps/indexer.d.ts.map +1 -0
- package/dist/apps/indexer.js +270 -0
- package/dist/apps/indexer.js.map +1 -0
- package/dist/apps/receipts.d.ts +1438 -0
- package/dist/apps/receipts.d.ts.map +1 -0
- package/dist/apps/receipts.js +874 -0
- package/dist/apps/receipts.js.map +1 -0
- package/dist/apps/rpc.d.ts +173 -0
- package/dist/apps/rpc.d.ts.map +1 -0
- package/dist/apps/rpc.js +191 -0
- package/dist/apps/rpc.js.map +1 -0
- package/dist/apps/tokenlist.d.ts +192 -0
- package/dist/apps/tokenlist.d.ts.map +1 -0
- package/dist/apps/tokenlist.js +153 -0
- package/dist/apps/tokenlist.js.map +1 -0
- package/dist/apps/tokens.d.ts +2246 -0
- package/dist/apps/tokens.d.ts.map +1 -0
- package/dist/apps/tokens.js +1789 -0
- package/dist/apps/tokens.js.map +1 -0
- package/dist/apps/transactions.d.ts +2327 -0
- package/dist/apps/transactions.d.ts.map +1 -0
- package/dist/apps/transactions.js +975 -0
- package/dist/apps/transactions.js.map +1 -0
- package/dist/apps/transfers.d.ts +589 -0
- package/dist/apps/transfers.d.ts.map +1 -0
- package/dist/apps/transfers.js +769 -0
- package/dist/apps/transfers.js.map +1 -0
- package/dist/apps/verified-tokens.d.ts +544 -0
- package/dist/apps/verified-tokens.d.ts.map +1 -0
- package/dist/apps/verified-tokens.js +234 -0
- package/dist/apps/verified-tokens.js.map +1 -0
- package/dist/apps/webhooks.d.ts +7684 -0
- package/dist/apps/webhooks.d.ts.map +1 -0
- package/dist/apps/webhooks.js +2112 -0
- package/dist/apps/webhooks.js.map +1 -0
- package/dist/cloudflare.d.ts +32 -0
- package/dist/cloudflare.d.ts.map +1 -0
- package/dist/cloudflare.js +45 -0
- package/dist/cloudflare.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/Auth.d.ts +298 -0
- package/dist/internal/Auth.d.ts.map +1 -0
- package/dist/internal/Auth.js +618 -0
- package/dist/internal/Auth.js.map +1 -0
- package/dist/internal/Cache.d.ts +74 -0
- package/dist/internal/Cache.d.ts.map +1 -0
- package/dist/internal/Cache.js +215 -0
- package/dist/internal/Cache.js.map +1 -0
- package/dist/internal/Cursor.d.ts +79 -0
- package/dist/internal/Cursor.d.ts.map +1 -0
- package/dist/internal/Cursor.js +116 -0
- package/dist/internal/Cursor.js.map +1 -0
- package/dist/internal/EdgeCache.d.ts +59 -0
- package/dist/internal/EdgeCache.d.ts.map +1 -0
- package/dist/internal/EdgeCache.js +174 -0
- package/dist/internal/EdgeCache.js.map +1 -0
- package/dist/internal/Log.d.ts +104 -0
- package/dist/internal/Log.d.ts.map +1 -0
- package/dist/internal/Log.js +118 -0
- package/dist/internal/Log.js.map +1 -0
- package/dist/internal/MetricSink.d.ts +65 -0
- package/dist/internal/MetricSink.d.ts.map +1 -0
- package/dist/internal/MetricSink.js +146 -0
- package/dist/internal/MetricSink.js.map +1 -0
- package/dist/internal/Mpp.d.ts +71 -0
- package/dist/internal/Mpp.d.ts.map +1 -0
- package/dist/internal/Mpp.js +105 -0
- package/dist/internal/Mpp.js.map +1 -0
- package/dist/internal/OpenApi.d.ts +313 -0
- package/dist/internal/OpenApi.d.ts.map +1 -0
- package/dist/internal/OpenApi.fonts.d.ts +2 -0
- package/dist/internal/OpenApi.fonts.d.ts.map +1 -0
- package/dist/internal/OpenApi.fonts.js +39 -0
- package/dist/internal/OpenApi.fonts.js.map +1 -0
- package/dist/internal/OpenApi.js +335 -0
- package/dist/internal/OpenApi.js.map +1 -0
- package/dist/internal/OpenApi.logo.d.ts +6 -0
- package/dist/internal/OpenApi.logo.d.ts.map +1 -0
- package/dist/internal/OpenApi.logo.js +19 -0
- package/dist/internal/OpenApi.logo.js.map +1 -0
- package/dist/internal/Path.d.ts +5 -0
- package/dist/internal/Path.d.ts.map +1 -0
- package/dist/internal/Path.js +16 -0
- package/dist/internal/Path.js.map +1 -0
- package/dist/internal/RateLimit.d.ts +60 -0
- package/dist/internal/RateLimit.d.ts.map +1 -0
- package/dist/internal/RateLimit.js +48 -0
- package/dist/internal/RateLimit.js.map +1 -0
- package/dist/internal/Response.d.ts +132 -0
- package/dist/internal/Response.d.ts.map +1 -0
- package/dist/internal/Response.js +72 -0
- package/dist/internal/Response.js.map +1 -0
- package/dist/internal/Schema.d.ts +257 -0
- package/dist/internal/Schema.d.ts.map +1 -0
- package/dist/internal/Schema.js +360 -0
- package/dist/internal/Schema.js.map +1 -0
- package/dist/internal/Store.d.ts +204 -0
- package/dist/internal/Store.d.ts.map +1 -0
- package/dist/internal/Store.js +291 -0
- package/dist/internal/Store.js.map +1 -0
- package/dist/internal/Tidx.d.ts +120 -0
- package/dist/internal/Tidx.d.ts.map +1 -0
- package/dist/internal/Tidx.js +156 -0
- package/dist/internal/Tidx.js.map +1 -0
- package/dist/internal/Timing.d.ts +22 -0
- package/dist/internal/Timing.d.ts.map +1 -0
- package/dist/internal/Timing.js +32 -0
- package/dist/internal/Timing.js.map +1 -0
- package/dist/internal/Ttl.d.ts +25 -0
- package/dist/internal/Ttl.d.ts.map +1 -0
- package/dist/internal/Ttl.js +49 -0
- package/dist/internal/Ttl.js.map +1 -0
- package/dist/internal/Value.d.ts +47 -0
- package/dist/internal/Value.d.ts.map +1 -0
- package/dist/internal/Value.js +122 -0
- package/dist/internal/Value.js.map +1 -0
- package/dist/internal/VerifiedTokens.d.ts +228 -0
- package/dist/internal/VerifiedTokens.d.ts.map +1 -0
- package/dist/internal/VerifiedTokens.js +450 -0
- package/dist/internal/VerifiedTokens.js.map +1 -0
- package/dist/internal/Viem.d.ts +2082 -0
- package/dist/internal/Viem.d.ts.map +1 -0
- package/dist/internal/Viem.js +63 -0
- package/dist/internal/Viem.js.map +1 -0
- package/dist/internal/WebhookDestination.d.ts +198 -0
- package/dist/internal/WebhookDestination.d.ts.map +1 -0
- package/dist/internal/WebhookDestination.js +170 -0
- package/dist/internal/WebhookDestination.js.map +1 -0
- package/dist/internal/Webhooks.d.ts +433 -0
- package/dist/internal/Webhooks.d.ts.map +1 -0
- package/dist/internal/Webhooks.js +489 -0
- package/dist/internal/Webhooks.js.map +1 -0
- package/dist/internal/webhookDestinations/betterStack.d.ts +26 -0
- package/dist/internal/webhookDestinations/betterStack.d.ts.map +1 -0
- package/dist/internal/webhookDestinations/betterStack.js +76 -0
- package/dist/internal/webhookDestinations/betterStack.js.map +1 -0
- package/dist/internal/webhookDestinations/slack.d.ts +59 -0
- package/dist/internal/webhookDestinations/slack.d.ts.map +1 -0
- package/dist/internal/webhookDestinations/slack.js +422 -0
- package/dist/internal/webhookDestinations/slack.js.map +1 -0
- package/dist/internal/webhookDestinations/url.d.ts +17 -0
- package/dist/internal/webhookDestinations/url.d.ts.map +1 -0
- package/dist/internal/webhookDestinations/url.js +130 -0
- package/dist/internal/webhookDestinations/url.js.map +1 -0
- package/dist/openrpc.json +5302 -0
- package/package.json +46 -4
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000000.svg +1 -0
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000001.svg +1 -0
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000002.svg +1 -0
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000003.svg +1 -0
- package/seeds/31318/verified-tokens.json +30 -0
- package/seeds/4217/icons/0x20c0000000000000000000000000000000000000.svg +25 -0
- package/seeds/4217/icons/0x20c000000000000000000000048c8f36df1c9a4a.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000000a6da882d075a4c3.svg +4 -0
- package/seeds/4217/icons/0x20c000000000000000000000111111111e910f0f.svg +30 -0
- package/seeds/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000002f52d5cc21a3207b.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000003158081efd85bfc2.svg +11 -0
- package/seeds/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000383a23bacb546ab9.svg +1 -0
- package/seeds/4217/icons/0x20c00000000000000000000042109aef2f8b28e1.png +0 -0
- package/seeds/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000006fd9a167923ba194.png +0 -0
- package/seeds/4217/icons/0x20c0000000000000000000007f7ba549dd0251b9.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000008191667423f70e67.svg +5 -0
- package/seeds/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000009a4a4b17e0dc6651.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000ab02d39df30bd17e.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000ae247a1130450f09.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000aeed2ec36a54d0e5.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000bd95bfb69fbe6ce3.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000c412ec89d0c08be5.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000f047dd7018e50367.png +0 -0
- package/seeds/4217/verified-tokens.json +177 -0
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000000.svg +25 -0
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000001.svg +1 -0
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000002.svg +1 -0
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000003.svg +1 -0
- package/seeds/42431/icons/0x20c0000000000000000000009e8d7eb59b783726.svg +1 -0
- package/seeds/42431/icons/0x20c000000000000000000000d72572838bbee59c.svg +1 -0
- package/seeds/42431/verified-tokens.json +44 -0
- package/seeds/openrpc/README.md +27 -0
- package/seeds/openrpc/execution-apis.json +8989 -0
- package/src/ApiKey.test.ts +91 -0
- package/src/ApiKey.ts +122 -0
- package/src/ApiKeySource.test-d.ts +11 -0
- package/src/ApiKeySource.test.ts +295 -0
- package/src/ApiKeySource.ts +303 -0
- package/src/App.test-d.ts +38 -0
- package/src/App.test.ts +65927 -0
- package/src/App.ts +855 -0
- package/src/Assets.ts +58 -0
- package/src/Auth.ts +1 -0
- package/src/Cli.test.ts +81 -0
- package/src/Cli.ts +38 -0
- package/src/Client.test-d.ts +1438 -0
- package/src/Client.test.ts +134 -0
- package/src/Client.ts +45 -0
- package/src/Metrics.ts +154 -0
- package/src/Scope.ts +50 -0
- package/src/Webhooks.test.ts +49 -0
- package/src/Webhooks.ts +172 -0
- package/src/admin/Access.test.ts +130 -0
- package/src/admin/Access.ts +87 -0
- package/src/admin/App.test.ts +225 -0
- package/src/admin/App.ts +216 -0
- package/src/admin/apps/api-keys.test.ts +156 -0
- package/src/admin/apps/api-keys.ts +157 -0
- package/src/admin/apps/scopes.test.ts +52 -0
- package/src/admin/apps/scopes.ts +47 -0
- package/src/admin/apps/verified-tokens.test.ts +415 -0
- package/src/admin/apps/verified-tokens.ts +566 -0
- package/src/admin/index.ts +2 -0
- package/src/admin/ui/plugin.ts +135 -0
- package/src/admin/ui/src/components/Checkbox.tsx +22 -0
- package/src/admin/ui/src/components/Dialog.tsx +32 -0
- package/src/admin/ui/src/components/MobileProfile.tsx +31 -0
- package/src/admin/ui/src/components/Page.tsx +53 -0
- package/src/admin/ui/src/components/TempoLogo.tsx +37 -0
- package/src/admin/ui/src/fonts/Pilat-Demi.woff2 +0 -0
- package/src/admin/ui/src/fonts/Pilat-Regular.woff2 +0 -0
- package/src/admin/ui/src/lib/api-keys.ts +63 -0
- package/src/admin/ui/src/lib/client.ts +15 -0
- package/src/admin/ui/src/lib/config.ts +57 -0
- package/src/admin/ui/src/lib/query-client.ts +26 -0
- package/src/admin/ui/src/lib/scopes.ts +25 -0
- package/src/admin/ui/src/lib/verified-tokens.ts +89 -0
- package/src/admin/ui/src/lib/wagmi.ts +18 -0
- package/src/admin/ui/src/routeTree.gen.ts +104 -0
- package/src/admin/ui/src/router.tsx +25 -0
- package/src/admin/ui/src/routes/__root.tsx +219 -0
- package/src/admin/ui/src/routes/api-keys.tsx +329 -0
- package/src/admin/ui/src/routes/index.tsx +7 -0
- package/src/admin/ui/src/routes/verified-tokens.tsx +331 -0
- package/src/admin/ui/src/styles.css +20 -0
- package/src/admin/ui/tsconfig.json +16 -0
- package/src/admin/ui/vite.config.ts +29 -0
- package/src/admin/ui.generated.ts +60 -0
- package/src/apps/activities.test.ts +1020 -0
- package/src/apps/activities.ts +3934 -0
- package/src/apps/addresses.ts +24 -0
- package/src/apps/balances.test.ts +274 -0
- package/src/apps/balances.ts +533 -0
- package/src/apps/blocks.test.ts +385 -0
- package/src/apps/blocks.ts +896 -0
- package/src/apps/coingecko.test.ts +218 -0
- package/src/apps/coingecko.ts +1383 -0
- package/src/apps/exchanges.test.ts +958 -0
- package/src/apps/exchanges.ts +4773 -0
- package/src/apps/fee-amm.test.ts +321 -0
- package/src/apps/fee-amm.ts +1121 -0
- package/src/apps/indexer.test.ts +153 -0
- package/src/apps/indexer.ts +330 -0
- package/src/apps/receipts.test.ts +285 -0
- package/src/apps/receipts.ts +1193 -0
- package/src/apps/rpc.test.ts +302 -0
- package/src/apps/rpc.ts +222 -0
- package/src/apps/tokenlist.test.ts +80 -0
- package/src/apps/tokenlist.ts +207 -0
- package/src/apps/tokens.test.ts +1062 -0
- package/src/apps/tokens.ts +2292 -0
- package/src/apps/transactions.test.ts +408 -0
- package/src/apps/transactions.ts +1363 -0
- package/src/apps/transfers.test.ts +371 -0
- package/src/apps/transfers.ts +1116 -0
- package/src/apps/verified-tokens.test.ts +114 -0
- package/src/apps/verified-tokens.ts +295 -0
- package/src/apps/webhooks.test.ts +1937 -0
- package/src/apps/webhooks.ts +2926 -0
- package/src/cloudflare.ts +55 -0
- package/src/index.ts +15 -0
- package/src/internal/Auth.test.ts +980 -0
- package/src/internal/Auth.ts +1012 -0
- package/src/internal/Cache.test.ts +240 -0
- package/src/internal/Cache.ts +262 -0
- package/src/internal/Cursor.test.ts +100 -0
- package/src/internal/Cursor.ts +149 -0
- package/src/internal/EdgeCache.test.ts +234 -0
- package/src/internal/EdgeCache.ts +224 -0
- package/src/internal/Errors.ts +14 -0
- package/src/internal/Log.test.ts +244 -0
- package/src/internal/Log.ts +216 -0
- package/src/internal/MetricSink.ts +220 -0
- package/src/internal/Mpp.test.ts +64 -0
- package/src/internal/Mpp.ts +141 -0
- package/src/internal/OpenApi.fonts.ts +38 -0
- package/src/internal/OpenApi.logo.ts +18 -0
- package/src/internal/OpenApi.ts +488 -0
- package/src/internal/Path.test.ts +14 -0
- package/src/internal/Path.ts +15 -0
- package/src/internal/RateLimit.test.ts +93 -0
- package/src/internal/RateLimit.ts +93 -0
- package/src/internal/Response.ts +131 -0
- package/src/internal/Schema.test.ts +34 -0
- package/src/internal/Schema.ts +523 -0
- package/src/internal/Store.test.ts +545 -0
- package/src/internal/Store.ts +526 -0
- package/src/internal/Tidx.local.test.ts +33 -0
- package/src/internal/Tidx.test.ts +294 -0
- package/src/internal/Tidx.ts +238 -0
- package/src/internal/Timing.test.ts +22 -0
- package/src/internal/Timing.ts +62 -0
- package/src/internal/Ttl.test.ts +18 -0
- package/src/internal/Ttl.ts +59 -0
- package/src/internal/Value.ts +114 -0
- package/src/internal/VerifiedTokens.test.ts +333 -0
- package/src/internal/VerifiedTokens.ts +645 -0
- package/src/internal/Viem.test.ts +115 -0
- package/src/internal/Viem.ts +165 -0
- package/src/internal/WebhookDestination.test.ts +648 -0
- package/src/internal/WebhookDestination.ts +331 -0
- package/src/internal/Webhooks.test.ts +668 -0
- package/src/internal/Webhooks.ts +940 -0
- package/src/internal/webhookDestinations/betterStack.ts +91 -0
- package/src/internal/webhookDestinations/slack.ts +496 -0
- package/src/internal/webhookDestinations/url.ts +128 -0
- package/src/openrpc.json +5302 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
import * as Cache from './Cache.js'
|
|
3
|
+
|
|
4
|
+
describe('response', () => {
|
|
5
|
+
test('preserves rate-limit headers without caching them', async () => {
|
|
6
|
+
await withCaches(async () => {
|
|
7
|
+
let calls = 0
|
|
8
|
+
const app = new Hono()
|
|
9
|
+
|
|
10
|
+
app.use('*', async (c, next) => {
|
|
11
|
+
c.header('RateLimit-Remaining', calls === 0 ? '9' : '8')
|
|
12
|
+
await next()
|
|
13
|
+
})
|
|
14
|
+
app.get('/', Cache.response({ key: (c) => c.req.url, name: 'test' }), (c) =>
|
|
15
|
+
c.json({ calls: ++calls }),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const first = await app.request('/')
|
|
19
|
+
const second = await app.request('/')
|
|
20
|
+
const third = await app.request('/', {
|
|
21
|
+
headers: { 'If-None-Match': first.headers.get('etag') ?? '' },
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
expect(calls).toMatchInlineSnapshot(`1`)
|
|
25
|
+
expect(await first.json()).toMatchInlineSnapshot(`
|
|
26
|
+
{
|
|
27
|
+
"calls": 1,
|
|
28
|
+
}
|
|
29
|
+
`)
|
|
30
|
+
expect(first.headers.get('cache-control')).toMatchInlineSnapshot(
|
|
31
|
+
`"private, max-age=30, stale-while-revalidate=300"`,
|
|
32
|
+
)
|
|
33
|
+
expect(etag(first)).toMatchInlineSnapshot(`"<etag>"`)
|
|
34
|
+
expect(first.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`"9"`)
|
|
35
|
+
expect(first.headers.get('vary')).toMatchInlineSnapshot(
|
|
36
|
+
`"Accept-Encoding, Authorization, Tempo-API-Key"`,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
expect(await second.json()).toMatchInlineSnapshot(`
|
|
40
|
+
{
|
|
41
|
+
"calls": 1,
|
|
42
|
+
}
|
|
43
|
+
`)
|
|
44
|
+
expect(second.headers.get('cache-control')).toMatchInlineSnapshot(
|
|
45
|
+
`"private, max-age=30, stale-while-revalidate=300"`,
|
|
46
|
+
)
|
|
47
|
+
expect(second.headers.get('etag') === first.headers.get('etag')).toMatchInlineSnapshot(`true`)
|
|
48
|
+
expect(second.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`"8"`)
|
|
49
|
+
expect(second.headers.get('vary')).toMatchInlineSnapshot(
|
|
50
|
+
`"Accept-Encoding, Authorization, Tempo-API-Key"`,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
expect(third.status).toMatchInlineSnapshot(`304`)
|
|
54
|
+
expect(await third.text()).toMatchInlineSnapshot(`""`)
|
|
55
|
+
expect(third.headers.get('cache-control')).toMatchInlineSnapshot(
|
|
56
|
+
`"private, max-age=30, stale-while-revalidate=300"`,
|
|
57
|
+
)
|
|
58
|
+
expect(third.headers.get('etag') === first.headers.get('etag')).toMatchInlineSnapshot(`true`)
|
|
59
|
+
expect(third.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`"8"`)
|
|
60
|
+
expect(third.headers.get('vary')).toMatchInlineSnapshot(
|
|
61
|
+
`"Accept-Encoding, Authorization, Tempo-API-Key"`,
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('stores entries with the shared policy form, not the client form', async () => {
|
|
67
|
+
await withCaches(async (stores) => {
|
|
68
|
+
const app = new Hono()
|
|
69
|
+
app.get('/', Cache.response({ key: (c) => c.req.url, name: 'test' }), (c) =>
|
|
70
|
+
c.json({ ok: true }),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
await app.request('/')
|
|
74
|
+
|
|
75
|
+
const [stored] = [...(stores.get('test')?.values() ?? [])]
|
|
76
|
+
// The stored copy carries `public, s-maxage` so platform caches bound
|
|
77
|
+
// the entry's lifetime to the policy; the client copy stays private.
|
|
78
|
+
expect(stored?.headers.get('cache-control')).toMatchInlineSnapshot(
|
|
79
|
+
`"public, max-age=30, s-maxage=30, stale-while-revalidate=300"`,
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('refreshes the stored entry when the request sends `Cache-Control: no-cache`', async () => {
|
|
85
|
+
await withCaches(async () => {
|
|
86
|
+
let calls = 0
|
|
87
|
+
const app = new Hono()
|
|
88
|
+
app.get('/', Cache.response({ key: (c) => c.req.url, name: 'test' }), (c) =>
|
|
89
|
+
c.json({ calls: ++calls }),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
await app.request('/')
|
|
93
|
+
// `no-cache` purges the stored entry, re-runs the handler, and stores
|
|
94
|
+
// the fresh response, so the following plain request is served the
|
|
95
|
+
// refreshed copy.
|
|
96
|
+
const refreshed = await app.request('/', { headers: { 'cache-control': 'no-cache' } })
|
|
97
|
+
const after = await app.request('/')
|
|
98
|
+
|
|
99
|
+
expect(calls).toMatchInlineSnapshot(`2`)
|
|
100
|
+
expect(await refreshed.json()).toMatchInlineSnapshot(`
|
|
101
|
+
{
|
|
102
|
+
"calls": 2,
|
|
103
|
+
}
|
|
104
|
+
`)
|
|
105
|
+
expect(await after.json()).toMatchInlineSnapshot(`
|
|
106
|
+
{
|
|
107
|
+
"calls": 2,
|
|
108
|
+
}
|
|
109
|
+
`)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('bypasses without purging when the request sends `Cache-Control: no-store`', async () => {
|
|
114
|
+
await withCaches(async () => {
|
|
115
|
+
let calls = 0
|
|
116
|
+
const app = new Hono()
|
|
117
|
+
app.get('/', Cache.response({ key: (c) => c.req.url, name: 'test' }), (c) =>
|
|
118
|
+
c.json({ calls: ++calls }),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
await app.request('/')
|
|
122
|
+
// `no-store` skips reads and writes but leaves the stored entry alone:
|
|
123
|
+
// the bypassing caller gets a fresh response while the next plain
|
|
124
|
+
// request is still served the original copy.
|
|
125
|
+
const bypassed = await app.request('/', { headers: { 'cache-control': 'no-store' } })
|
|
126
|
+
const after = await app.request('/')
|
|
127
|
+
|
|
128
|
+
expect(calls).toMatchInlineSnapshot(`2`)
|
|
129
|
+
expect(await bypassed.json()).toMatchInlineSnapshot(`
|
|
130
|
+
{
|
|
131
|
+
"calls": 2,
|
|
132
|
+
}
|
|
133
|
+
`)
|
|
134
|
+
expect(await after.json()).toMatchInlineSnapshot(`
|
|
135
|
+
{
|
|
136
|
+
"calls": 1,
|
|
137
|
+
}
|
|
138
|
+
`)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('honors `Pragma: no-cache` when `Cache-Control` is absent', async () => {
|
|
143
|
+
await withCaches(async () => {
|
|
144
|
+
let calls = 0
|
|
145
|
+
const app = new Hono()
|
|
146
|
+
app.get('/', Cache.response({ key: (c) => c.req.url, name: 'test' }), (c) =>
|
|
147
|
+
c.json({ calls: ++calls }),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
await app.request('/')
|
|
151
|
+
const refreshed = await app.request('/', { headers: { pragma: 'no-cache' } })
|
|
152
|
+
|
|
153
|
+
expect(calls).toMatchInlineSnapshot(`2`)
|
|
154
|
+
expect(await refreshed.json()).toMatchInlineSnapshot(`
|
|
155
|
+
{
|
|
156
|
+
"calls": 2,
|
|
157
|
+
}
|
|
158
|
+
`)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('does not store responses a handler overrides to `noStore`', async () => {
|
|
163
|
+
await withCaches(async (stores) => {
|
|
164
|
+
let calls = 0
|
|
165
|
+
const app = new Hono()
|
|
166
|
+
app.get('/', Cache.response({ key: (c) => c.req.url, name: 'test' }), (c) => {
|
|
167
|
+
Cache.setPolicy(c, Cache.policies.noStore)
|
|
168
|
+
return c.json({ calls: ++calls })
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
const first = await app.request('/')
|
|
172
|
+
const second = await app.request('/')
|
|
173
|
+
|
|
174
|
+
// The `noStore` override must keep the response out of the inner cache
|
|
175
|
+
// (e.g. a pending transaction), so every request re-runs the handler.
|
|
176
|
+
expect(calls).toMatchInlineSnapshot(`2`)
|
|
177
|
+
expect(stores.get('test')?.size ?? 0).toMatchInlineSnapshot(`0`)
|
|
178
|
+
expect(first.headers.get('cache-control')).toMatchInlineSnapshot(`"no-store"`)
|
|
179
|
+
expect(second.headers.get('cache-control')).toMatchInlineSnapshot(`"no-store"`)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('leaves error response `Cache-Control` alone', async () => {
|
|
184
|
+
await withCaches(async (stores) => {
|
|
185
|
+
const app = new Hono()
|
|
186
|
+
app.get('/', Cache.response({ key: (c) => c.req.url, name: 'test' }), (c) => {
|
|
187
|
+
// Mirrors `Response.error`, which marks envelopes `no-store`.
|
|
188
|
+
c.header('Cache-Control', 'no-store')
|
|
189
|
+
return c.json({ error: true }, 404)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const first = await app.request('/')
|
|
193
|
+
const second = await app.request('/')
|
|
194
|
+
|
|
195
|
+
expect(first.status).toMatchInlineSnapshot(`404`)
|
|
196
|
+
expect(first.headers.get('cache-control')).toMatchInlineSnapshot(`"no-store"`)
|
|
197
|
+
expect(second.headers.get('cache-control')).toMatchInlineSnapshot(`"no-store"`)
|
|
198
|
+
expect(stores.get('test')?.size ?? 0).toMatchInlineSnapshot(`0`)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
function etag(response: Response) {
|
|
204
|
+
return response.headers.has('etag') ? '<etag>' : null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function withCaches(fn: (stores: Map<string, Map<string, Response>>) => Promise<void>) {
|
|
208
|
+
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'caches')
|
|
209
|
+
const stores = new Map<string, Map<string, Response>>()
|
|
210
|
+
Object.defineProperty(globalThis, 'caches', {
|
|
211
|
+
configurable: true,
|
|
212
|
+
value: {
|
|
213
|
+
async open(name: string) {
|
|
214
|
+
let store = stores.get(name)
|
|
215
|
+
if (!store) {
|
|
216
|
+
store = new Map()
|
|
217
|
+
stores.set(name, store)
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
async delete(key: string) {
|
|
221
|
+
return store.delete(key)
|
|
222
|
+
},
|
|
223
|
+
async match(key: string) {
|
|
224
|
+
return store.get(key)?.clone()
|
|
225
|
+
},
|
|
226
|
+
async put(key: string, response: Response) {
|
|
227
|
+
store.set(key, response.clone())
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
await fn(stores)
|
|
236
|
+
} finally {
|
|
237
|
+
if (descriptor) Object.defineProperty(globalThis, 'caches', descriptor)
|
|
238
|
+
else delete (globalThis as { caches?: unknown }).caches
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import type { Context, Env, MiddlewareHandler } from 'hono'
|
|
2
|
+
import { cache as hono_cache } from 'hono/cache'
|
|
3
|
+
import { etag as hono_etag, RETAINED_304_HEADERS } from 'hono/etag'
|
|
4
|
+
import type * as z from 'zod/mini'
|
|
5
|
+
|
|
6
|
+
import * as EdgeCache from './EdgeCache.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Named `Cache-Control` policies, tiered by how volatile each resource is. The
|
|
10
|
+
* `max-age` bounds how stale a shared (edge) hit can be; `stale-while-revalidate`
|
|
11
|
+
* lets clients/CDNs serve a slightly older copy while refreshing in the
|
|
12
|
+
* background. Handlers may override per response via `Cache.setPolicy` (e.g. a
|
|
13
|
+
* mined transaction is `immutable`, a pending one is `noStore`).
|
|
14
|
+
*/
|
|
15
|
+
export const policies = {
|
|
16
|
+
/**
|
|
17
|
+
* Publicly shareable binary assets (token logos). `public` because the bytes
|
|
18
|
+
* carry no per-principal data and the edge layer strips per-request headers.
|
|
19
|
+
* Deliberately NOT `immutable`: these live at a stable URL
|
|
20
|
+
* (`/tokens/:token/logo`) whose underlying R2 icon can be replaced in place
|
|
21
|
+
* (admin upload), so a long `max-age` is paired with `stale-while-revalidate`
|
|
22
|
+
* to revalidate rather than pin a replaced logo for a year. Use a versioned/
|
|
23
|
+
* content-addressed URL if true `immutable` forever-caching is needed.
|
|
24
|
+
*/
|
|
25
|
+
asset: 'public, max-age=86400, stale-while-revalidate=604800',
|
|
26
|
+
/** Fallback for routes that do not specify a policy. */
|
|
27
|
+
default: 'private, max-age=30, stale-while-revalidate=300',
|
|
28
|
+
/** Newest-first feeds (transactions, receipts, transfers): refresh quickly. */
|
|
29
|
+
feed: 'private, max-age=10, stale-while-revalidate=30',
|
|
30
|
+
/** Immutable resources (mined transactions and receipts): cache aggressively. */
|
|
31
|
+
immutable: 'private, max-age=86400, stale-while-revalidate=604800',
|
|
32
|
+
/** Token metadata and indexed listings: change slowly. */
|
|
33
|
+
metadata: 'private, max-age=60, stale-while-revalidate=300',
|
|
34
|
+
/** Transient or uncacheable responses (pending transactions). */
|
|
35
|
+
noStore: 'no-store',
|
|
36
|
+
/** Slow-changing curated data (verified tokens). */
|
|
37
|
+
stable: 'private, max-age=300, stale-while-revalidate=3600',
|
|
38
|
+
/** Current-state pages (holders, balances): moderately volatile. */
|
|
39
|
+
state: 'private, max-age=30, stale-while-revalidate=120',
|
|
40
|
+
} as const
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Overrides the cache policy for the current response, taking precedence over
|
|
44
|
+
* the route's default `Cache.response` policy. Lets a handler vary caching by
|
|
45
|
+
* outcome — e.g. cache a mined transaction as `immutable` but a pending one as
|
|
46
|
+
* `noStore`. Must be called before the handler returns.
|
|
47
|
+
*/
|
|
48
|
+
export function setPolicy<environment extends Env>(
|
|
49
|
+
c: Context<environment>,
|
|
50
|
+
cacheControl: string,
|
|
51
|
+
): void {
|
|
52
|
+
c.set('cacheControl' as never, cacheControl as never)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Hono response-cache helpers. */
|
|
56
|
+
export function response<environment extends Env>(
|
|
57
|
+
options: response.Options<environment>,
|
|
58
|
+
): MiddlewareHandler<environment> {
|
|
59
|
+
const defaultCacheControl = options.cacheControl ?? policies.default
|
|
60
|
+
const vary = options.vary ?? ['Accept-Encoding', 'Authorization', 'Tempo-API-Key']
|
|
61
|
+
// hono/cache suffixes its key with `::<directive>=<value>` for every
|
|
62
|
+
// configured vary directive (trimmed, lowercased, deduped). Mirror that
|
|
63
|
+
// normalization so the `no-cache` purge below deletes exactly the entry the
|
|
64
|
+
// wrapped middleware reads — drift here would make refreshes silently miss.
|
|
65
|
+
const varyKeyDirectives = [
|
|
66
|
+
...new Set(vary.map((name) => name.trim().toLowerCase()).filter(Boolean)),
|
|
67
|
+
]
|
|
68
|
+
const cache = hono_cache({
|
|
69
|
+
cacheName: options.name,
|
|
70
|
+
keyGenerator: options.key,
|
|
71
|
+
onCacheNotAvailable: false,
|
|
72
|
+
vary: [...vary],
|
|
73
|
+
wait: true,
|
|
74
|
+
})
|
|
75
|
+
const etag = hono_etag()
|
|
76
|
+
|
|
77
|
+
return async (c, next) => {
|
|
78
|
+
const values = response.preservedHeaders.map((name) => [name, c.res.headers.get(name)] as const)
|
|
79
|
+
|
|
80
|
+
// Honor the standard request `Cache-Control` directives (plus the HTTP/1.0
|
|
81
|
+
// `Pragma: no-cache` fallback, used only when `Cache-Control` is absent),
|
|
82
|
+
// mirroring `EdgeCache.middleware`: a refresh that bypasses the edge layer
|
|
83
|
+
// must also bypass this inner response cache, or the caller is served the
|
|
84
|
+
// inner stored copy and the "refresh" never reaches the handler.
|
|
85
|
+
// `no-cache` purges the stored entry so the wrapped hono/cache misses and
|
|
86
|
+
// re-stores the fresh response; `no-store` skips reads and writes entirely
|
|
87
|
+
// while leaving the stored entry untouched.
|
|
88
|
+
const directives = (c.req.header('Cache-Control') ?? c.req.header('Pragma') ?? '').toLowerCase()
|
|
89
|
+
const noStore = directives.includes('no-store')
|
|
90
|
+
const noCache = noStore || directives.includes('no-cache')
|
|
91
|
+
// The Cache API global is platform-provided (Cloudflare Workers, browsers)
|
|
92
|
+
// and absent from Node's type surface, so probe it structurally — on
|
|
93
|
+
// runtimes without it the wrapped hono/cache is a no-op and there is
|
|
94
|
+
// nothing to purge.
|
|
95
|
+
type Caches = { open(name: string): Promise<{ delete(key: string): Promise<unknown> }> }
|
|
96
|
+
const caches = (globalThis as { caches?: Caches }).caches
|
|
97
|
+
if (noCache && !noStore && caches) {
|
|
98
|
+
let key = await options.key(c)
|
|
99
|
+
for (const name of varyKeyDirectives)
|
|
100
|
+
key += `::${name}=${encodeURIComponent(c.req.raw.headers.get(name) ?? '')}`
|
|
101
|
+
await (await caches.open(options.name)).delete(key)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Run ETag inside Hono's cache miss path so fresh responses are hashed
|
|
105
|
+
// once before storage. Cached hits bypass Hono's ETag middleware, so this
|
|
106
|
+
// wrapper handles conditional requests from the stored ETag below.
|
|
107
|
+
const handler = async () => {
|
|
108
|
+
await etag(c, async () => {
|
|
109
|
+
await next()
|
|
110
|
+
// Rate-limit headers are per request and would be stale if stored with
|
|
111
|
+
// the shared cached body.
|
|
112
|
+
for (const name of response.preservedHeaders) c.res.headers.delete(name)
|
|
113
|
+
})
|
|
114
|
+
// Stamp the resolved policy's shared form onto 2xx responses BEFORE
|
|
115
|
+
// hono/cache stores them: the explicit `s-maxage` bounds the stored
|
|
116
|
+
// entry's lifetime to the policy (instead of the platform cache's
|
|
117
|
+
// default TTL), and a policy resolved to `noStore` (e.g. a pending
|
|
118
|
+
// transaction via `setPolicy`) becomes `no-store`, which hono/cache
|
|
119
|
+
// refuses to store. Client-facing headers are applied after storage
|
|
120
|
+
// below; non-2xx responses keep their own headers (`Response.error`
|
|
121
|
+
// marks envelopes `no-store`).
|
|
122
|
+
if (c.res.status >= 200 && c.res.status < 300) {
|
|
123
|
+
const policy = edgeCachePolicy(
|
|
124
|
+
(c.get('cacheControl' as never) as string | undefined) ?? defaultCacheControl,
|
|
125
|
+
)
|
|
126
|
+
c.res.headers.set('Cache-Control', policy ? EdgeCache.cacheControl(policy) : 'no-store')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const result = noStore ? await handler() : await cache(c, handler)
|
|
130
|
+
|
|
131
|
+
// A handler may override the policy per response (see `setPolicy`); fall back
|
|
132
|
+
// to the route default. Publishing the derived policy is what marks the
|
|
133
|
+
// response as eligible for the shared edge cache (see `EdgeCache.middleware`),
|
|
134
|
+
// so `noStore` responses are never persisted there. The edge layer reads this
|
|
135
|
+
// after the whole chain runs, so setting it here (post-handler) is in time.
|
|
136
|
+
const cacheControl =
|
|
137
|
+
(c.get('cacheControl' as never) as string | undefined) ?? defaultCacheControl
|
|
138
|
+
c.set('edgeCache' as never, edgeCachePolicy(cacheControl) as never)
|
|
139
|
+
|
|
140
|
+
let res = result instanceof Response ? new Response(result.body, result) : c.res
|
|
141
|
+
if (
|
|
142
|
+
result instanceof Response &&
|
|
143
|
+
etagMatches(res.headers.get('ETag'), c.req.header('If-None-Match'))
|
|
144
|
+
)
|
|
145
|
+
res = notModified(res)
|
|
146
|
+
|
|
147
|
+
// Apply private client cache headers after Hono cache storage; the stored
|
|
148
|
+
// copy carries the shared `public, s-maxage` form stamped above, while the
|
|
149
|
+
// client receives the policy's private form. Error responses keep their
|
|
150
|
+
// own `Cache-Control` (`no-store` from `Response.error` or the app-level
|
|
151
|
+
// safety net) instead of inheriting the route policy.
|
|
152
|
+
if ((res.status >= 200 && res.status < 300) || res.status === 304)
|
|
153
|
+
setCacheHeaders(res.headers, { cacheControl, vary })
|
|
154
|
+
for (const [name, value] of values) if (value) res.headers.set(name, value)
|
|
155
|
+
if (result instanceof Response) return res
|
|
156
|
+
return undefined
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export declare namespace response {
|
|
161
|
+
/** Options for response caching. */
|
|
162
|
+
type Options<environment extends Env> = {
|
|
163
|
+
/** Cache-control value returned to clients. */
|
|
164
|
+
cacheControl?: string | undefined
|
|
165
|
+
/** Cache name passed to Hono's cache middleware. */
|
|
166
|
+
name: string
|
|
167
|
+
/** Cache key generator. */
|
|
168
|
+
key: (c: Context<environment>) => Promise<string> | string
|
|
169
|
+
/** Vary headers used for the response and cache key. */
|
|
170
|
+
vary?: readonly string[] | undefined
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export namespace response {
|
|
175
|
+
/** Per-request headers that must not be stored in shared response caches. */
|
|
176
|
+
export const preservedHeaders = ['RateLimit-Limit', 'RateLimit-Remaining', 'RateLimit-Reset']
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Builds a cache key from the request URL after parsing query params through
|
|
181
|
+
* the given Zod schema. Defaults from the schema are baked into the key so
|
|
182
|
+
* cached entries collide regardless of param ordering, casing, or omitted
|
|
183
|
+
* defaults. Resolves an optional `chainId` query parameter against the app's
|
|
184
|
+
* default chain id when the caller did not provide one.
|
|
185
|
+
*/
|
|
186
|
+
export function urlKey<
|
|
187
|
+
environment extends { Variables: { chainId: number } },
|
|
188
|
+
schema extends z.ZodMiniType,
|
|
189
|
+
>(c: Context<environment>, schema: schema): string {
|
|
190
|
+
const url = new URL(c.req.url)
|
|
191
|
+
const query = schema.parse(Object.fromEntries(url.searchParams)) as Record<string, unknown>
|
|
192
|
+
url.search = ''
|
|
193
|
+
if (query['chainId'] === undefined) url.searchParams.set('chainId', String(c.get('chainId')))
|
|
194
|
+
for (const [key, value] of Object.entries(query)) {
|
|
195
|
+
if (value === undefined) continue
|
|
196
|
+
url.searchParams.set(key, String(value as number | string | boolean))
|
|
197
|
+
}
|
|
198
|
+
return url.toString()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function etagMatches(etag: string | null, ifNoneMatch: string | undefined) {
|
|
202
|
+
if (!etag || !ifNoneMatch) return false
|
|
203
|
+
return ifNoneMatch.split(/,\s*/).some((tag) => stripWeak(tag) === stripWeak(etag))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function notModified(response: Response) {
|
|
207
|
+
const headers = new Headers()
|
|
208
|
+
for (const name of RETAINED_304_HEADERS) {
|
|
209
|
+
const value = response.headers.get(name)
|
|
210
|
+
if (value) headers.set(name, value)
|
|
211
|
+
}
|
|
212
|
+
return new Response(null, {
|
|
213
|
+
headers,
|
|
214
|
+
status: 304,
|
|
215
|
+
statusText: 'Not Modified',
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function setCacheHeaders(
|
|
220
|
+
headers: Headers,
|
|
221
|
+
options: { cacheControl: string; vary: readonly string[] },
|
|
222
|
+
) {
|
|
223
|
+
headers.set('Cache-Control', options.cacheControl)
|
|
224
|
+
const vary = new Map(
|
|
225
|
+
(headers.get('Vary') ?? '')
|
|
226
|
+
.split(',')
|
|
227
|
+
.map((value) => value.trim())
|
|
228
|
+
.filter(Boolean)
|
|
229
|
+
.map((value) => [value.toLowerCase(), value] as const),
|
|
230
|
+
)
|
|
231
|
+
for (const value of options.vary) vary.set(value.toLowerCase(), value)
|
|
232
|
+
headers.set('Vary', Array.from(vary.values()).join(', '))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function stripWeak(etag: string) {
|
|
236
|
+
return etag.replace(/^W\//, '')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Derive the shared-cache policy from a route's client `Cache-Control`. A
|
|
240
|
+
// positive `s-maxage`/`max-age` (and no `no-store`/`no-cache`) is what makes a
|
|
241
|
+
// route eligible for the anonymous edge cache; the `private` directive only
|
|
242
|
+
// scopes the client copy and is intentionally ignored here.
|
|
243
|
+
function edgeCachePolicy(cacheControl: string): EdgeCache.Policy | undefined {
|
|
244
|
+
const directives = new Map(
|
|
245
|
+
cacheControl
|
|
246
|
+
.split(',')
|
|
247
|
+
.map((part) => part.trim().toLowerCase())
|
|
248
|
+
.filter(Boolean)
|
|
249
|
+
.map((part) => part.split('=') as [string, string | undefined]),
|
|
250
|
+
)
|
|
251
|
+
if (directives.has('no-store') || directives.has('no-cache')) return undefined
|
|
252
|
+
const maxAge = Number(directives.get('s-maxage') ?? directives.get('max-age'))
|
|
253
|
+
if (!Number.isFinite(maxAge) || maxAge <= 0) return undefined
|
|
254
|
+
const staleWhileRevalidate = Number(directives.get('stale-while-revalidate'))
|
|
255
|
+
return {
|
|
256
|
+
maxAge,
|
|
257
|
+
staleWhileRevalidate:
|
|
258
|
+
Number.isFinite(staleWhileRevalidate) && staleWhileRevalidate > 0
|
|
259
|
+
? staleWhileRevalidate
|
|
260
|
+
: undefined,
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as Cursor from './Cursor.js'
|
|
2
|
+
|
|
3
|
+
describe('Cursor', () => {
|
|
4
|
+
test('round-trips an int tuple through an opaque, URL-safe token', () => {
|
|
5
|
+
const token = Cursor.encode([123456, 7])
|
|
6
|
+
expect(token).toMatch(/^[A-Za-z0-9_-]+$/)
|
|
7
|
+
expect(Cursor.decode(token, ['int', 'int'])).toEqual([123456, 7])
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('round-trips a balance/address tuple', () => {
|
|
11
|
+
const token = Cursor.encode([
|
|
12
|
+
'1000000000000000000',
|
|
13
|
+
'0xBE058E1C4DF8A4366A387BF595B284246A93039E',
|
|
14
|
+
])
|
|
15
|
+
expect(Cursor.decode(token, ['uint', 'address'])).toEqual([
|
|
16
|
+
'1000000000000000000',
|
|
17
|
+
'0xBE058E1C4DF8A4366A387BF595B284246A93039E',
|
|
18
|
+
])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('returns undefined for malformed, mis-typed, or wrong-arity tokens', () => {
|
|
22
|
+
expect(Cursor.decode('not-base64!!', ['int', 'int'])).toBeUndefined()
|
|
23
|
+
expect(Cursor.decode(btoa('{"oops":true}'), ['int', 'int'])).toBeUndefined()
|
|
24
|
+
expect(Cursor.decode(btoa('[1]'), ['int', 'int'])).toBeUndefined()
|
|
25
|
+
expect(Cursor.decode(btoa('[1,2,3]'), ['int', 'int'])).toBeUndefined()
|
|
26
|
+
expect(Cursor.decode(btoa('[1.5,2]'), ['int', 'int'])).toBeUndefined()
|
|
27
|
+
expect(Cursor.decode(btoa('[-1,2]'), ['int', 'int'])).toBeUndefined()
|
|
28
|
+
expect(Cursor.decode(btoa('["x",2]'), ['uint', 'int'])).toBeUndefined()
|
|
29
|
+
expect(Cursor.decode(btoa('["0xnotanaddress",2]'), ['address', 'int'])).toBeUndefined()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('renders injection-safe SQL literals per field kind', () => {
|
|
33
|
+
expect(Cursor.literal(42, 'int')).toBe('42')
|
|
34
|
+
expect(Cursor.literal('1000', 'uint')).toBe('1000')
|
|
35
|
+
expect(Cursor.literal('0xAbC0000000000000000000000000000000000000', 'address')).toBe(
|
|
36
|
+
"'0xabc0000000000000000000000000000000000000'",
|
|
37
|
+
)
|
|
38
|
+
expect(Cursor.literal('2024-01-01T00:00:00Z', 'timestamp')).toBe("'2024-01-01T00:00:00Z'")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('paginates a keyset result, deriving hasMore and nextCursor from the extra row', () => {
|
|
42
|
+
const rows = [
|
|
43
|
+
{ block: 10, idx: 2 },
|
|
44
|
+
{ block: 10, idx: 1 },
|
|
45
|
+
{ block: 9, idx: 0 },
|
|
46
|
+
]
|
|
47
|
+
// Fetched `limit + 1` rows, so the trailing row signals another page.
|
|
48
|
+
const page = Cursor.paginate({ rows, limit: 2, key: (row) => [row.block, row.idx] })
|
|
49
|
+
expect(page.rows).toEqual([
|
|
50
|
+
{ block: 10, idx: 2 },
|
|
51
|
+
{ block: 10, idx: 1 },
|
|
52
|
+
])
|
|
53
|
+
expect(page.hasMore).toBe(true)
|
|
54
|
+
expect(Cursor.decode(page.nextCursor!, ['int', 'int'])).toEqual([10, 1])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('paginate reports the last page with a null nextCursor', () => {
|
|
58
|
+
const rows = [{ block: 9, idx: 0 }]
|
|
59
|
+
const page = Cursor.paginate({ rows, limit: 2, key: (row) => [row.block, row.idx] })
|
|
60
|
+
expect(page.rows).toEqual(rows)
|
|
61
|
+
expect(page.hasMore).toBe(false)
|
|
62
|
+
expect(page.nextCursor).toBeNull()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('paginate yields a null nextCursor when the last row has no sort-key', () => {
|
|
66
|
+
const rows = [{ ok: true }, { ok: true }, { ok: false }]
|
|
67
|
+
const page = Cursor.paginate({ rows, limit: 2, key: (row) => (row.ok ? [1, 2] : undefined) })
|
|
68
|
+
expect(page.hasMore).toBe(true)
|
|
69
|
+
// The boundary row (last of the page) is `{ ok: true }`, so a cursor exists.
|
|
70
|
+
expect(page.nextCursor).not.toBeNull()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('builds a row-value tuple predicate with a prunable first-column bound', () => {
|
|
74
|
+
expect(
|
|
75
|
+
Cursor.keyset([
|
|
76
|
+
{ literal: '100', name: 'block_num', order: 'desc' },
|
|
77
|
+
{ literal: '3', name: 'idx', order: 'desc' },
|
|
78
|
+
]),
|
|
79
|
+
).toBe('(block_num <= 100 AND (block_num, idx) < (100, 3))')
|
|
80
|
+
expect(
|
|
81
|
+
Cursor.keyset([
|
|
82
|
+
{ literal: '100', name: 'block_num', order: 'asc' },
|
|
83
|
+
{ literal: '3', name: 'idx', order: 'asc' },
|
|
84
|
+
]),
|
|
85
|
+
).toBe('(block_num >= 100 AND (block_num, idx) > (100, 3))')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('omits the redundant bound for a single-column keyset', () => {
|
|
89
|
+
expect(Cursor.keyset([{ literal: '100', name: 'num', order: 'desc' }])).toBe('((num) < (100))')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('falls back to an OR expansion for a mixed-direction keyset', () => {
|
|
93
|
+
expect(
|
|
94
|
+
Cursor.keyset([
|
|
95
|
+
{ literal: '1000', name: 'balance', order: 'desc' },
|
|
96
|
+
{ literal: "'0xabc'", name: 'holder', order: 'asc' },
|
|
97
|
+
]),
|
|
98
|
+
).toBe("(balance <= 1000 AND ((balance < 1000) OR (balance = 1000 AND holder > '0xabc')))")
|
|
99
|
+
})
|
|
100
|
+
})
|