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,91 @@
|
|
|
1
|
+
import * as ApiKeySource from './ApiKeySource.js'
|
|
2
|
+
import * as ApiKey from './ApiKey.js'
|
|
3
|
+
import * as Store from './internal/Store.js'
|
|
4
|
+
|
|
5
|
+
describe('schema.ApiKey', () => {
|
|
6
|
+
test('environment defaults to production when omitted', () => {
|
|
7
|
+
const parsed = ApiKey.schema.ApiKey.parse({
|
|
8
|
+
id: 'key_test',
|
|
9
|
+
orgId: 'org_test',
|
|
10
|
+
scopes: ['data:read'],
|
|
11
|
+
})
|
|
12
|
+
expect(parsed.environment).toMatchInlineSnapshot(`"production"`)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('accepts an explicit sandbox environment', () => {
|
|
16
|
+
const parsed = ApiKey.schema.ApiKey.parse({
|
|
17
|
+
environment: 'sandbox',
|
|
18
|
+
id: 'key_test',
|
|
19
|
+
orgId: 'org_test',
|
|
20
|
+
scopes: ['data:read'],
|
|
21
|
+
})
|
|
22
|
+
expect(parsed.environment).toMatchInlineSnapshot(`"sandbox"`)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('rejects an unknown environment', () => {
|
|
26
|
+
expect(
|
|
27
|
+
ApiKey.schema.ApiKey.safeParse({
|
|
28
|
+
environment: 'staging',
|
|
29
|
+
id: 'key_test',
|
|
30
|
+
orgId: 'org_test',
|
|
31
|
+
scopes: ['data:read'],
|
|
32
|
+
}).success,
|
|
33
|
+
).toBe(false)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('redact', () => {
|
|
38
|
+
test('masks tokens anywhere in a string', () => {
|
|
39
|
+
const token = `tempo:sk:${'a'.repeat(48)}`
|
|
40
|
+
expect(ApiKey.redact(`auth failed for ${token} on /tokens`)).toMatchInlineSnapshot(
|
|
41
|
+
`"auth failed for tempo:sk:… on /tokens"`,
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('masks sandbox tokens, preserving their prefix', () => {
|
|
46
|
+
const token = `tempo_sandbox:sk:${'a'.repeat(48)}`
|
|
47
|
+
expect(ApiKey.redact(`auth failed for ${token} on /tokens`)).toMatchInlineSnapshot(
|
|
48
|
+
`"auth failed for tempo_sandbox:sk:… on /tokens"`,
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('masks deprecated v1 tokens', () => {
|
|
53
|
+
const token = `tempo_${'a'.repeat(40)}`
|
|
54
|
+
expect(ApiKey.redact(`auth failed for ${token} on /tokens`)).toMatchInlineSnapshot(
|
|
55
|
+
`"auth failed for tempo_… on /tokens"`,
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('leaves non-token text untouched', () => {
|
|
60
|
+
expect(ApiKey.redact('nothing secret here')).toMatchInlineSnapshot(`"nothing secret here"`)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// The wire layout (record prefix + token hash) is the contract every persisted
|
|
65
|
+
// key is stored under; the resolver and every source's `mint` must agree on it.
|
|
66
|
+
// These assertions pin it so an accidental change fails loudly.
|
|
67
|
+
describe('keyFor', () => {
|
|
68
|
+
test('pins the record prefix and token hash (sha256 of UTF-8 bytes, hex)', () => {
|
|
69
|
+
expect(ApiKey.recordPrefix).toMatchInlineSnapshot(`"apikey:"`)
|
|
70
|
+
expect(ApiKey.keyFor('tempo:sk:test')).toMatchInlineSnapshot(
|
|
71
|
+
`"apikey:9e11781fc2ea4581de193fc7fde09dda7c01151785cdcf2f5fbef52125b225b8"`,
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('reader resolves a record written with the documented wire layout', async () => {
|
|
76
|
+
const token = `tempo:sk:${'a'.repeat(48)}`
|
|
77
|
+
const record = {
|
|
78
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
79
|
+
id: 'key_smoke',
|
|
80
|
+
orgId: 'org_test',
|
|
81
|
+
scopes: ['data:read'],
|
|
82
|
+
tokenLast4: token.slice(-4),
|
|
83
|
+
}
|
|
84
|
+
const store = Store.memory({ entries: [[ApiKey.keyFor(token), JSON.stringify(record)]] })
|
|
85
|
+
expect(await ApiKeySource.store(store).resolve(token)).toMatchObject({
|
|
86
|
+
id: 'key_smoke',
|
|
87
|
+
orgId: 'org_test',
|
|
88
|
+
scopes: ['data:read'],
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
})
|
package/src/ApiKey.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The resolved API-key resource and the reusable credential primitives every
|
|
3
|
+
* source backend shares.
|
|
4
|
+
*
|
|
5
|
+
* Holds the resolved {@link ApiKey} type and its {@link schema}, the token
|
|
6
|
+
* format/generation ({@link generateToken}/{@link generateId}), the storage-key
|
|
7
|
+
* derivation ({@link keyFor}/{@link recordPrefix}), and log {@link redact}ion.
|
|
8
|
+
* These live here — not in a single source — so every `ApiKeySource` backend
|
|
9
|
+
* (the built-in store, a future D1/Postgres source) issues and parses
|
|
10
|
+
* byte-identical credentials and cannot drift. The persisted record/mint-input
|
|
11
|
+
* shapes and provisioning (mint/list/revoke) belong to the
|
|
12
|
+
* `ApiKeySource.Source` contract, which composes these primitives with its own
|
|
13
|
+
* persistence.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { nanoid } from 'nanoid'
|
|
17
|
+
import { Hash, Hex } from 'ox'
|
|
18
|
+
import * as z from 'zod/mini'
|
|
19
|
+
|
|
20
|
+
import * as RateLimit from './internal/RateLimit.js'
|
|
21
|
+
import * as Scope from './Scope.js'
|
|
22
|
+
|
|
23
|
+
/** Zod schemas owned by the API-key module. */
|
|
24
|
+
export namespace schema {
|
|
25
|
+
/**
|
|
26
|
+
* Canonical schema for resolved API-key metadata, and the single source of
|
|
27
|
+
* truth for the {@link ApiKey} type. `ApiKeySource` derives its persisted
|
|
28
|
+
* `Record` and `MintInput` schemas from this one, so the resolved and
|
|
29
|
+
* persisted shapes cannot drift.
|
|
30
|
+
*/
|
|
31
|
+
export const ApiKey = z.object({
|
|
32
|
+
environment: z
|
|
33
|
+
._default(z.enum(['production', 'sandbox']), 'production')
|
|
34
|
+
.check(z.describe('Key environment.')),
|
|
35
|
+
id: z.string().check(z.describe('Stable key id.')),
|
|
36
|
+
name: z.optional(z.string()).check(z.describe('Human-readable key name.')),
|
|
37
|
+
orgId: z.string().check(z.describe('Owning organization id.')),
|
|
38
|
+
rateLimits: z
|
|
39
|
+
.optional(z.record(z.string(), RateLimit.schema.Limit))
|
|
40
|
+
.check(
|
|
41
|
+
z.describe(
|
|
42
|
+
"Per-key quota overrides keyed by quota scope. The reserved '*' entry is the per-key default; any other key overrides that scope. Omit to inherit server config. The most specific match wins.",
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
scopes: z
|
|
46
|
+
.readonly(z.array(z.enum([...Scope.catalog.map((entry) => entry.scope), Scope.wildcard])))
|
|
47
|
+
.check(z.describe("Granted scopes. '*' grants all scopes.")),
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** API-key metadata resolved by an auth source. */
|
|
52
|
+
export type ApiKey = z.output<typeof schema.ApiKey>
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Replaces every issued token in a string with a masked placeholder so secrets
|
|
56
|
+
* never reach logs. Apply to anything that may embed a credential — error
|
|
57
|
+
* stacks, request dumps, and the values of the `tempo-api-key` and
|
|
58
|
+
* `Authorization` headers (whose tokens match the issued-token pattern).
|
|
59
|
+
*/
|
|
60
|
+
export function redact(text: string): string {
|
|
61
|
+
return text
|
|
62
|
+
.replace(tokenPattern(), (match) => `${match.split('sk:')[0]}sk:…`)
|
|
63
|
+
.replace(legacyTokenPattern(), 'tempo_…')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Token prefix per environment. The prefix encodes the environment so a leaked
|
|
68
|
+
* sandbox token is visibly distinct from a production one; otherwise there is a
|
|
69
|
+
* single token kind. Prefixes exist purely for recognizability and leak
|
|
70
|
+
* detection (CI secret scanning, log redaction — see {@link redact}).
|
|
71
|
+
*/
|
|
72
|
+
const tokenPrefix = {
|
|
73
|
+
production: 'tempo:sk:',
|
|
74
|
+
sandbox: 'tempo_sandbox:sk:',
|
|
75
|
+
} as const
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Greppable pattern matching an issued token (`tempo:sk:`/`tempo_sandbox:sk:` +
|
|
79
|
+
* 48–64 lowercase hex chars), suitable for CI secret scanning and log
|
|
80
|
+
* redaction. A fresh instance is returned on each call so callers never share
|
|
81
|
+
* `lastIndex` state.
|
|
82
|
+
*/
|
|
83
|
+
function tokenPattern(): RegExp {
|
|
84
|
+
return /tempo(_sandbox)?:sk:[a-f0-9]{48,64}/g
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Greppable pattern matching a deprecated v1 token (`tempo_` + 40 lowercase hex
|
|
89
|
+
* chars), kept so legacy credentials are redacted from logs with the same
|
|
90
|
+
* guarantees as current tokens. A fresh instance is returned on each call so
|
|
91
|
+
* callers never share `lastIndex` state.
|
|
92
|
+
*/
|
|
93
|
+
function legacyTokenPattern(): RegExp {
|
|
94
|
+
return /tempo_[a-f0-9]{40}/g
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Generates a new plaintext token (environment prefix + 48 lowercase hex chars
|
|
99
|
+
* / 192 bits). Shared by every source backend so issued tokens are
|
|
100
|
+
* format-identical.
|
|
101
|
+
*/
|
|
102
|
+
export function generateToken(environment: ApiKey['environment'] = 'production'): string {
|
|
103
|
+
return `${tokenPrefix[environment]}${Hex.random(24).slice(2)}`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Generates a stable, opaque key id (`key_…`). */
|
|
107
|
+
export function generateId(): string {
|
|
108
|
+
return `key_${nanoid()}`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Storage-key prefix under which key records are persisted, keyed by token hash. */
|
|
112
|
+
export const recordPrefix = 'apikey:'
|
|
113
|
+
|
|
114
|
+
/** Hashes a plaintext token to its at-rest lookup hash (lowercase hex, no `0x`). */
|
|
115
|
+
function hash(token: string): string {
|
|
116
|
+
return Hash.sha256(Hex.fromString(token)).slice(2)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Storage key under which the record for a plaintext token is persisted. */
|
|
120
|
+
export function keyFor(token: string): string {
|
|
121
|
+
return `${recordPrefix}${hash(token)}`
|
|
122
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type * as ApiKeySource from './ApiKeySource.js'
|
|
2
|
+
|
|
3
|
+
describe('MintInput.scopes', () => {
|
|
4
|
+
// The scope union flows from the central `Scope.catalog`, so minting
|
|
5
|
+
// with an unissuable scope is a compile error.
|
|
6
|
+
test('accepts catalog scopes and the wildcard, rejects unknown ones', () => {
|
|
7
|
+
expectTypeOf<['data:read', 'webhooks:write']>().toExtend<ApiKeySource.MintInput['scopes']>()
|
|
8
|
+
expectTypeOf<['*']>().toExtend<ApiKeySource.MintInput['scopes']>()
|
|
9
|
+
expectTypeOf<['totally:made-up']>().not.toExtend<ApiKeySource.MintInput['scopes']>()
|
|
10
|
+
})
|
|
11
|
+
})
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import * as ApiKeySource from './ApiKeySource.js'
|
|
2
|
+
import * as Store from './internal/Store.js'
|
|
3
|
+
|
|
4
|
+
describe('store', () => {
|
|
5
|
+
test('resolves an unknown token to null', async () => {
|
|
6
|
+
const source = ApiKeySource.store(Store.memory())
|
|
7
|
+
expect(await source.resolve('tempo:sk:missing')).toBeNull()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
// Mock the system clock so timestamps are deterministic without injecting a
|
|
11
|
+
// clock into the source.
|
|
12
|
+
describe('provisioning', () => {
|
|
13
|
+
beforeEach(() => vi.useFakeTimers({ now: new Date('2026-01-01T00:00:00.000Z') }))
|
|
14
|
+
afterEach(() => vi.useRealTimers())
|
|
15
|
+
|
|
16
|
+
test('mints a key and resolves it', async () => {
|
|
17
|
+
const source = ApiKeySource.store(Store.memory())
|
|
18
|
+
|
|
19
|
+
const { record, token } = await source.mint({
|
|
20
|
+
createdBy: 'admin@tempo.xyz',
|
|
21
|
+
name: 'CI key',
|
|
22
|
+
orgId: 'org_acme',
|
|
23
|
+
scopes: ['data:read', 'webhooks:write'],
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
expect(token).toMatch(/^tempo:sk:[a-f0-9]{48}$/)
|
|
27
|
+
expect(record.tokenLast4).toBe(token.slice(-4))
|
|
28
|
+
expect({ ...record, id: '<id>', tokenLast4: '<last4>' }).toMatchInlineSnapshot(`
|
|
29
|
+
{
|
|
30
|
+
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
31
|
+
"createdBy": "admin@tempo.xyz",
|
|
32
|
+
"environment": "production",
|
|
33
|
+
"id": "<id>",
|
|
34
|
+
"name": "CI key",
|
|
35
|
+
"orgId": "org_acme",
|
|
36
|
+
"scopes": [
|
|
37
|
+
"data:read",
|
|
38
|
+
"webhooks:write",
|
|
39
|
+
],
|
|
40
|
+
"tokenLast4": "<last4>",
|
|
41
|
+
}
|
|
42
|
+
`)
|
|
43
|
+
|
|
44
|
+
const resolved = await source.resolve(token)
|
|
45
|
+
expect(resolved?.orgId).toBe('org_acme')
|
|
46
|
+
expect(resolved?.scopes).toEqual(['data:read', 'webhooks:write'])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('lists keys newest first, filtered by org', async () => {
|
|
50
|
+
const source = ApiKeySource.store(Store.memory())
|
|
51
|
+
|
|
52
|
+
// Advance the clock between mints so their createdAt timestamps differ and
|
|
53
|
+
// "newest first" is well-defined.
|
|
54
|
+
await source.mint({ name: 'a', orgId: 'org_a', scopes: ['data:read'] })
|
|
55
|
+
vi.setSystemTime(new Date('2026-01-01T00:00:01.000Z'))
|
|
56
|
+
await source.mint({ name: 'b', orgId: 'org_b', scopes: ['data:read'] })
|
|
57
|
+
|
|
58
|
+
const all = await source.list()
|
|
59
|
+
expect(all.map((record) => record.name)).toMatchInlineSnapshot(`
|
|
60
|
+
[
|
|
61
|
+
"b",
|
|
62
|
+
"a",
|
|
63
|
+
]
|
|
64
|
+
`)
|
|
65
|
+
|
|
66
|
+
const scoped = await source.list({ orgId: 'org_b' })
|
|
67
|
+
expect(scoped.map((record) => record.orgId)).toMatchInlineSnapshot(`
|
|
68
|
+
[
|
|
69
|
+
"org_b",
|
|
70
|
+
]
|
|
71
|
+
`)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('revokes a key by id', async () => {
|
|
75
|
+
const source = ApiKeySource.store(Store.memory())
|
|
76
|
+
|
|
77
|
+
const { record, token } = await source.mint({ orgId: 'org_a', scopes: ['data:read'] })
|
|
78
|
+
|
|
79
|
+
expect(await source.revoke(record.id)).toBe(true)
|
|
80
|
+
expect(await source.resolve(token)).toBeNull()
|
|
81
|
+
expect(await source.revoke(record.id)).toBe(false)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('revoke is O(1): deletes the record without scanning the prefix', async () => {
|
|
85
|
+
const store = Store.memory()
|
|
86
|
+
const source = ApiKeySource.store(store)
|
|
87
|
+
|
|
88
|
+
const { record } = await source.mint({ orgId: 'org_a', scopes: ['data:read'] })
|
|
89
|
+
|
|
90
|
+
// The id index entry maps id -> record key; revoke uses it directly.
|
|
91
|
+
expect(await source.revoke(record.id)).toBe(true)
|
|
92
|
+
// Both the record and its id index entry are gone.
|
|
93
|
+
const { keys } = await store.list({ prefix: 'apikey' })
|
|
94
|
+
expect(keys).toMatchInlineSnapshot(`[]`)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('excludes a key past its expiry from list', async () => {
|
|
98
|
+
const source = ApiKeySource.store(Store.memory())
|
|
99
|
+
|
|
100
|
+
await source.mint({
|
|
101
|
+
expiresAt: '2026-01-01T00:00:30.000Z',
|
|
102
|
+
name: 'short-lived',
|
|
103
|
+
orgId: 'org_a',
|
|
104
|
+
scopes: ['data:read'],
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
expect((await source.list()).map((record) => record.name)).toMatchInlineSnapshot(`
|
|
108
|
+
[
|
|
109
|
+
"short-lived",
|
|
110
|
+
]
|
|
111
|
+
`)
|
|
112
|
+
|
|
113
|
+
// Advance the clock past the expiry.
|
|
114
|
+
vi.setSystemTime(new Date('2026-01-01T00:01:00.000Z'))
|
|
115
|
+
expect(await source.list()).toMatchInlineSnapshot(`[]`)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('resolves before expiry, rejects after', async () => {
|
|
120
|
+
vi.useFakeTimers({ now: new Date('2026-01-01T00:00:00.000Z') })
|
|
121
|
+
try {
|
|
122
|
+
const source = ApiKeySource.store(Store.memory())
|
|
123
|
+
const { token } = await source.mint({
|
|
124
|
+
expiresAt: '2026-01-01T00:01:00.000Z',
|
|
125
|
+
orgId: 'org_a',
|
|
126
|
+
scopes: ['data:read'],
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
expect((await source.resolve(token))?.orgId).toBe('org_a')
|
|
130
|
+
|
|
131
|
+
// Advance the clock past the expiry.
|
|
132
|
+
vi.setSystemTime(new Date('2026-01-01T00:02:00.000Z'))
|
|
133
|
+
expect(await source.resolve(token)).toBeNull()
|
|
134
|
+
} finally {
|
|
135
|
+
vi.useRealTimers()
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('treats a corrupt record as absent', async () => {
|
|
140
|
+
const store = Store.memory()
|
|
141
|
+
const source = ApiKeySource.store(store)
|
|
142
|
+
const { token } = await source.mint({
|
|
143
|
+
orgId: 'org_a',
|
|
144
|
+
scopes: ['data:read'],
|
|
145
|
+
})
|
|
146
|
+
// Overwrite the stored record (keyed by token hash) with invalid JSON.
|
|
147
|
+
const { keys } = await store.list({ prefix: 'apikey:' })
|
|
148
|
+
await store.put(keys[0]!.name, 'not json')
|
|
149
|
+
expect(await source.resolve(token)).toBeNull()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('rejects a record with an unknown scope', async () => {
|
|
153
|
+
const store = Store.memory()
|
|
154
|
+
const source = ApiKeySource.store(store)
|
|
155
|
+
const { token } = await source.mint({
|
|
156
|
+
orgId: 'org_a',
|
|
157
|
+
scopes: ['data:read'],
|
|
158
|
+
})
|
|
159
|
+
const { keys } = await store.list({ prefix: 'apikey:' })
|
|
160
|
+
await store.put(
|
|
161
|
+
keys[0]!.name,
|
|
162
|
+
JSON.stringify({
|
|
163
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
164
|
+
id: 'key_x',
|
|
165
|
+
orgId: 'org_a',
|
|
166
|
+
scopes: ['data:read', 'totally:made-up'],
|
|
167
|
+
tokenLast4: 'xxxx',
|
|
168
|
+
}),
|
|
169
|
+
)
|
|
170
|
+
expect(await source.resolve(token)).toBeNull()
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
describe('cached', () => {
|
|
175
|
+
/** Wraps a source so resolve calls to the backend can be counted. */
|
|
176
|
+
function counting(source: ApiKeySource.Source) {
|
|
177
|
+
let resolves = 0
|
|
178
|
+
return {
|
|
179
|
+
resolves: () => resolves,
|
|
180
|
+
source: ApiKeySource.from({
|
|
181
|
+
...source,
|
|
182
|
+
async resolve(token) {
|
|
183
|
+
resolves++
|
|
184
|
+
return source.resolve(token)
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
test('serves repeat resolves from cache until the TTL expires', async () => {
|
|
191
|
+
const backing = ApiKeySource.store(Store.memory())
|
|
192
|
+
const { token } = await backing.mint({ orgId: 'org_acme', scopes: ['data:read'] })
|
|
193
|
+
const { resolves, source } = counting(backing)
|
|
194
|
+
let time = 0
|
|
195
|
+
const cached = ApiKeySource.cached(source, { now: () => time })
|
|
196
|
+
|
|
197
|
+
const first = await cached.resolve(token)
|
|
198
|
+
time = 59_999
|
|
199
|
+
const second = await cached.resolve(token)
|
|
200
|
+
|
|
201
|
+
expect(resolves()).toMatchInlineSnapshot(`1`)
|
|
202
|
+
expect(second).toBe(first)
|
|
203
|
+
expect({
|
|
204
|
+
...first,
|
|
205
|
+
createdAt: '<createdAt>',
|
|
206
|
+
id: '<id>',
|
|
207
|
+
tokenLast4: '<last4>',
|
|
208
|
+
}).toMatchInlineSnapshot(`
|
|
209
|
+
{
|
|
210
|
+
"createdAt": "<createdAt>",
|
|
211
|
+
"environment": "production",
|
|
212
|
+
"id": "<id>",
|
|
213
|
+
"orgId": "org_acme",
|
|
214
|
+
"scopes": [
|
|
215
|
+
"data:read",
|
|
216
|
+
],
|
|
217
|
+
"tokenLast4": "<last4>",
|
|
218
|
+
}
|
|
219
|
+
`)
|
|
220
|
+
|
|
221
|
+
time = 60_000
|
|
222
|
+
await cached.resolve(token)
|
|
223
|
+
expect(resolves()).toMatchInlineSnapshot(`2`)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test('negative-caches unknown tokens with the shorter TTL', async () => {
|
|
227
|
+
const { resolves, source } = counting(ApiKeySource.store(Store.memory()))
|
|
228
|
+
let time = 0
|
|
229
|
+
const cached = ApiKeySource.cached(source, { now: () => time })
|
|
230
|
+
|
|
231
|
+
expect(await cached.resolve('tempo:sk:missing')).toBeNull()
|
|
232
|
+
time = 9_999
|
|
233
|
+
expect(await cached.resolve('tempo:sk:missing')).toBeNull()
|
|
234
|
+
expect(resolves()).toMatchInlineSnapshot(`1`)
|
|
235
|
+
|
|
236
|
+
time = 10_000
|
|
237
|
+
expect(await cached.resolve('tempo:sk:missing')).toBeNull()
|
|
238
|
+
expect(resolves()).toMatchInlineSnapshot(`2`)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('single-flights concurrent misses for one token', async () => {
|
|
242
|
+
let calls = 0
|
|
243
|
+
const pending = deferred<null>()
|
|
244
|
+
const cached = ApiKeySource.cached(
|
|
245
|
+
ApiKeySource.from({
|
|
246
|
+
...ApiKeySource.noop,
|
|
247
|
+
async resolve() {
|
|
248
|
+
calls++
|
|
249
|
+
return pending.promise
|
|
250
|
+
},
|
|
251
|
+
}),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
const requests = [cached.resolve('tempo:sk:a'), cached.resolve('tempo:sk:a')]
|
|
255
|
+
pending.resolve(null)
|
|
256
|
+
|
|
257
|
+
expect(await Promise.all(requests)).toMatchInlineSnapshot(`
|
|
258
|
+
[
|
|
259
|
+
null,
|
|
260
|
+
null,
|
|
261
|
+
]
|
|
262
|
+
`)
|
|
263
|
+
expect(calls).toMatchInlineSnapshot(`1`)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test('does not cache resolve failures', async () => {
|
|
267
|
+
let calls = 0
|
|
268
|
+
const cached = ApiKeySource.cached(
|
|
269
|
+
ApiKeySource.from({
|
|
270
|
+
...ApiKeySource.noop,
|
|
271
|
+
async resolve() {
|
|
272
|
+
calls++
|
|
273
|
+
if (calls === 1) throw new Error('kv unavailable')
|
|
274
|
+
return null
|
|
275
|
+
},
|
|
276
|
+
}),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
await expect(cached.resolve('tempo:sk:a')).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
280
|
+
`[Error: kv unavailable]`,
|
|
281
|
+
)
|
|
282
|
+
expect(await cached.resolve('tempo:sk:a')).toBeNull()
|
|
283
|
+
expect(calls).toMatchInlineSnapshot(`2`)
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
function deferred<value>() {
|
|
288
|
+
let reject!: (cause?: unknown) => void
|
|
289
|
+
let resolve!: (value: PromiseLike<value> | value) => void
|
|
290
|
+
const promise = new Promise<value>((resolve_, reject_) => {
|
|
291
|
+
reject = reject_
|
|
292
|
+
resolve = resolve_
|
|
293
|
+
})
|
|
294
|
+
return { promise, reject, resolve }
|
|
295
|
+
}
|