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,130 @@
|
|
|
1
|
+
import * as jose from 'jose'
|
|
2
|
+
|
|
3
|
+
import * as Access from './Access.js'
|
|
4
|
+
|
|
5
|
+
const teamDomain = 'https://tempo.cloudflareaccess.com'
|
|
6
|
+
const certsUrl = `${teamDomain}/cdn-cgi/access/certs`
|
|
7
|
+
const kid = 'test-key-1'
|
|
8
|
+
|
|
9
|
+
/** Generates an ES256 keypair and the JWKS the team domain would serve. */
|
|
10
|
+
async function makeKeys() {
|
|
11
|
+
const { privateKey, publicKey } = await jose.generateKeyPair('ES256', { extractable: true })
|
|
12
|
+
const jwk = { ...(await jose.exportJWK(publicKey)), kid }
|
|
13
|
+
return { jwks: { keys: [jwk] }, privateKey }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Signs an Access-style JWT with the given claims/overrides. */
|
|
17
|
+
async function sign(
|
|
18
|
+
privateKey: jose.CryptoKey,
|
|
19
|
+
options: {
|
|
20
|
+
audience?: string | undefined
|
|
21
|
+
email?: string | undefined
|
|
22
|
+
expirationTime?: string | number | undefined
|
|
23
|
+
issuer?: string | undefined
|
|
24
|
+
} = {},
|
|
25
|
+
) {
|
|
26
|
+
const payload = options.email === undefined ? {} : { email: options.email }
|
|
27
|
+
return new jose.SignJWT(payload)
|
|
28
|
+
.setProtectedHeader({ alg: 'ES256', kid })
|
|
29
|
+
.setIssuer(options.issuer ?? teamDomain)
|
|
30
|
+
.setAudience(options.audience ?? 'test-access-aud-tag')
|
|
31
|
+
.setIssuedAt()
|
|
32
|
+
.setExpirationTime(options.expirationTime ?? '1h')
|
|
33
|
+
.sign(privateKey)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('cloudflareAccess', () => {
|
|
37
|
+
let privateKey: jose.CryptoKey
|
|
38
|
+
let jwks: { keys: readonly unknown[] }
|
|
39
|
+
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
;({ jwks, privateKey } = await makeKeys())
|
|
42
|
+
// Serve the public JWKS from the team-domain certs URL.
|
|
43
|
+
vi.stubGlobal(
|
|
44
|
+
'fetch',
|
|
45
|
+
vi.fn(async (input: string | URL | Request) => {
|
|
46
|
+
const url =
|
|
47
|
+
typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
|
|
48
|
+
if (url === certsUrl) return new Response(JSON.stringify(jwks))
|
|
49
|
+
throw new Error(`unexpected fetch: ${url}`)
|
|
50
|
+
}),
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
vi.unstubAllGlobals()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('valid JWT in header → identity', async () => {
|
|
59
|
+
const identify = Access.cloudflareAccess({ teamDomain })
|
|
60
|
+
const token = await sign(privateKey, { email: 'admin@tempo.xyz' })
|
|
61
|
+
const request = new Request('https://admin.tempo.xyz/api-keys', {
|
|
62
|
+
headers: { 'cf-access-jwt-assertion': token },
|
|
63
|
+
})
|
|
64
|
+
expect(await identify(request)).toEqual({ email: 'admin@tempo.xyz' })
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('valid JWT in CF_Authorization cookie → identity', async () => {
|
|
68
|
+
const identify = Access.cloudflareAccess({ teamDomain })
|
|
69
|
+
const token = await sign(privateKey, { email: 'admin@tempo.xyz' })
|
|
70
|
+
const request = new Request('https://admin.tempo.xyz/api-keys', {
|
|
71
|
+
headers: { cookie: `foo=bar; CF_Authorization=${token}` },
|
|
72
|
+
})
|
|
73
|
+
expect(await identify(request)).toEqual({ email: 'admin@tempo.xyz' })
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('missing assertion → null', async () => {
|
|
77
|
+
const identify = Access.cloudflareAccess({ teamDomain })
|
|
78
|
+
const request = new Request('https://admin.tempo.xyz/api-keys')
|
|
79
|
+
expect(await identify(request)).toBeNull()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('any audience accepted (issuer still enforced)', async () => {
|
|
83
|
+
const identify = Access.cloudflareAccess({ teamDomain })
|
|
84
|
+
const token = await sign(privateKey, { audience: 'some-other-aud', email: 'admin@tempo.xyz' })
|
|
85
|
+
const request = new Request('https://admin.tempo.xyz/api-keys', {
|
|
86
|
+
headers: { 'cf-access-jwt-assertion': token },
|
|
87
|
+
})
|
|
88
|
+
expect(await identify(request)).toEqual({ email: 'admin@tempo.xyz' })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('wrong issuer → null', async () => {
|
|
92
|
+
const identify = Access.cloudflareAccess({ teamDomain })
|
|
93
|
+
const token = await sign(privateKey, {
|
|
94
|
+
email: 'admin@tempo.xyz',
|
|
95
|
+
issuer: 'https://evil.cloudflareaccess.com',
|
|
96
|
+
})
|
|
97
|
+
const request = new Request('https://admin.tempo.xyz/api-keys', {
|
|
98
|
+
headers: { 'cf-access-jwt-assertion': token },
|
|
99
|
+
})
|
|
100
|
+
expect(await identify(request)).toBeNull()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('expired JWT → null', async () => {
|
|
104
|
+
const identify = Access.cloudflareAccess({ teamDomain })
|
|
105
|
+
const token = await sign(privateKey, { email: 'admin@tempo.xyz', expirationTime: '-1h' })
|
|
106
|
+
const request = new Request('https://admin.tempo.xyz/api-keys', {
|
|
107
|
+
headers: { 'cf-access-jwt-assertion': token },
|
|
108
|
+
})
|
|
109
|
+
expect(await identify(request)).toBeNull()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('valid signature but no email claim → null', async () => {
|
|
113
|
+
const identify = Access.cloudflareAccess({ teamDomain })
|
|
114
|
+
const token = await sign(privateKey)
|
|
115
|
+
const request = new Request('https://admin.tempo.xyz/api-keys', {
|
|
116
|
+
headers: { 'cf-access-jwt-assertion': token },
|
|
117
|
+
})
|
|
118
|
+
expect(await identify(request)).toBeNull()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('signature from an unknown key → null', async () => {
|
|
122
|
+
const identify = Access.cloudflareAccess({ teamDomain })
|
|
123
|
+
const other = await makeKeys()
|
|
124
|
+
const token = await sign(other.privateKey, { email: 'admin@tempo.xyz' })
|
|
125
|
+
const request = new Request('https://admin.tempo.xyz/api-keys', {
|
|
126
|
+
headers: { 'cf-access-jwt-assertion': token },
|
|
127
|
+
})
|
|
128
|
+
expect(await identify(request)).toBeNull()
|
|
129
|
+
})
|
|
130
|
+
})
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default {@link App.create.Options.identify} hook: verifies a Cloudflare
|
|
3
|
+
* Access JWT in-worker, so a request sent directly to the worker URL (bypassing
|
|
4
|
+
* the edge gate) still can't reach the admin routes (defense in depth).
|
|
5
|
+
*
|
|
6
|
+
* The only admin module that imports `jose`, keeping that dependency out of the
|
|
7
|
+
* IdP-agnostic core. Self-hosters not on Access supply their own `identify`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as jose from 'jose'
|
|
11
|
+
|
|
12
|
+
import type * as App from './App.js'
|
|
13
|
+
|
|
14
|
+
/** Header Cloudflare Access sets with the signed identity assertion (a JWT). */
|
|
15
|
+
const headerName = 'cf-access-jwt-assertion'
|
|
16
|
+
/** Cookie Cloudflare Access sets as a fallback carrier for the same JWT. */
|
|
17
|
+
const cookieName = 'CF_Authorization'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Builds an {@link App.create.Options.identify} hook that verifies the Cloudflare
|
|
21
|
+
* Access JWT against the team-domain JWKS (fetched lazily, cached by `jose`). Any
|
|
22
|
+
* verification failure resolves to `null` (deny) rather than throwing.
|
|
23
|
+
*
|
|
24
|
+
* Verifies the signature and issuer (the team domain). The audience (`aud`) is
|
|
25
|
+
* not checked, so any token minted for any app in the team's org is accepted —
|
|
26
|
+
* fine when everyone with org access is trusted.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* import { App, Access } from 'tapimo/admin'
|
|
31
|
+
*
|
|
32
|
+
* const source = {} as App.create.Options['apiKey']['source']
|
|
33
|
+
*
|
|
34
|
+
* App.create({
|
|
35
|
+
* apiKey: { source },
|
|
36
|
+
* identify: Access.cloudflareAccess({
|
|
37
|
+
* teamDomain: 'https://tempo.cloudflareaccess.com',
|
|
38
|
+
* }),
|
|
39
|
+
* })
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function cloudflareAccess(
|
|
43
|
+
options: cloudflareAccess.Options,
|
|
44
|
+
): (request: Request) => Promise<App.Identity | null> {
|
|
45
|
+
const { teamDomain } = options
|
|
46
|
+
|
|
47
|
+
// Created once and reused: `jose` caches keys and refetches on unknown `kid`.
|
|
48
|
+
const issuer = teamDomain.replace(/\/+$/, '')
|
|
49
|
+
const jwks = jose.createRemoteJWKSet(new URL(`${issuer}/cdn-cgi/access/certs`))
|
|
50
|
+
|
|
51
|
+
return async (request) => {
|
|
52
|
+
const token = readToken(request)
|
|
53
|
+
if (!token) return null
|
|
54
|
+
try {
|
|
55
|
+
const { payload } = await jose.jwtVerify(token, jwks, { issuer })
|
|
56
|
+
const email = payload['email']
|
|
57
|
+
return typeof email === 'string' && email ? { email } : null
|
|
58
|
+
} catch {
|
|
59
|
+
// Any verification failure is a denial, not an error to surface.
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export declare namespace cloudflareAccess {
|
|
66
|
+
/** Options for {@link cloudflareAccess}. */
|
|
67
|
+
type Options = {
|
|
68
|
+
/** Team domain (e.g. `https://<team>.cloudflareaccess.com`); the token issuer and JWKS host. */
|
|
69
|
+
teamDomain: string
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extracts the Access JWT: the `Cf-Access-Jwt-Assertion` header, falling back to
|
|
75
|
+
* the `CF_Authorization` cookie. Returns `undefined` when neither is present.
|
|
76
|
+
*/
|
|
77
|
+
function readToken(request: Request): string | undefined {
|
|
78
|
+
const header = request.headers.get(headerName)
|
|
79
|
+
if (header) return header
|
|
80
|
+
const cookie = request.headers.get('cookie')
|
|
81
|
+
if (!cookie) return undefined
|
|
82
|
+
for (const part of cookie.split(';')) {
|
|
83
|
+
const [name, ...rest] = part.trim().split('=')
|
|
84
|
+
if (name === cookieName && rest.length) return rest.join('=')
|
|
85
|
+
}
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
import * as TestAdmin from '../../test/Admin.js'
|
|
3
|
+
|
|
4
|
+
// Route behavior (mint/list/revoke, the scope catalog) is covered in
|
|
5
|
+
// `./apps/api-keys.test.ts` and `./apps/scopes.test.ts`; this file covers the
|
|
6
|
+
// app-level wiring: the auth gate, cache policy, and the SPA shell.
|
|
7
|
+
describe('create', () => {
|
|
8
|
+
describe('authentication', () => {
|
|
9
|
+
test('denied identity → 401 on config + the data API', async () => {
|
|
10
|
+
const { app } = TestAdmin.setup({ identify: () => null })
|
|
11
|
+
const routes = [
|
|
12
|
+
new Request('http://admin/config.json'),
|
|
13
|
+
new Request('http://admin/scopes'),
|
|
14
|
+
new Request('http://admin/api-keys'),
|
|
15
|
+
new Request('http://admin/api-keys', {
|
|
16
|
+
body: JSON.stringify({ orgId: 'org', scopes: ['data:read'] }),
|
|
17
|
+
headers: { 'content-type': 'application/json' },
|
|
18
|
+
method: 'POST',
|
|
19
|
+
}),
|
|
20
|
+
new Request('http://admin/api-keys/key_x', { method: 'DELETE' }),
|
|
21
|
+
new Request('http://admin/verified-tokens?chainId=42431', {
|
|
22
|
+
body: JSON.stringify({ tokens: [] }),
|
|
23
|
+
headers: { 'content-type': 'application/json' },
|
|
24
|
+
method: 'PUT',
|
|
25
|
+
}),
|
|
26
|
+
]
|
|
27
|
+
for (const request of routes) {
|
|
28
|
+
const response = await app.request(request)
|
|
29
|
+
expect(response.status).toBe(401)
|
|
30
|
+
expect(response.headers.get('cache-control')).toBe('no-store')
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('denied identity → shell + assets stay public (so the SPA can show login)', async () => {
|
|
35
|
+
const { app } = TestAdmin.setup({ identify: () => null, ui: { loginUrl: '/login' } })
|
|
36
|
+
const shell = await app.request('http://admin/')
|
|
37
|
+
expect(shell.status).toBe(200)
|
|
38
|
+
// The shell inlines `identity: null` so the SPA renders login on first paint.
|
|
39
|
+
const html = await shell.text()
|
|
40
|
+
const json = html.match(/window\.__TEMPO_API_CONFIG__=({.*?})<\/script>/)?.[1]
|
|
41
|
+
expect(JSON.parse(json!)).toEqual({ auth: { identity: null, loginUrl: '/login' } })
|
|
42
|
+
const assetPath = html.match(/\.\/(assets\/[^"]+\.js)/)?.[1]
|
|
43
|
+
expect((await app.request(`http://admin/${assetPath}`)).status).toBe(200)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('public auth routes mounted ahead of the admin app bypass the gate', async () => {
|
|
47
|
+
// The consumer owns auth endpoints: mount them on a parent app before the
|
|
48
|
+
// admin app, so they never reach the gate while the admin surface stays gated.
|
|
49
|
+
const { app: admin } = TestAdmin.setup({ identify: () => null })
|
|
50
|
+
const app = new Hono().get('/api/auth/ping', (c) => c.text('ok')).route('/', admin)
|
|
51
|
+
expect((await app.request('http://admin/api/auth/ping')).status).toBe(200)
|
|
52
|
+
expect((await app.request('http://admin/api-keys')).status).toBe(401)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('verified identity is recorded as createdBy on minted keys', async () => {
|
|
56
|
+
const { app } = TestAdmin.setup({ identify: () => ({ email: 'ops@tempo.xyz' }) })
|
|
57
|
+
const response = await app.request('http://admin/api-keys', {
|
|
58
|
+
body: JSON.stringify({ orgId: 'org', scopes: ['data:read'] }),
|
|
59
|
+
headers: { 'content-type': 'application/json' },
|
|
60
|
+
method: 'POST',
|
|
61
|
+
})
|
|
62
|
+
const body = (await response.json()) as { createdBy?: string }
|
|
63
|
+
expect(body.createdBy).toBe('ops@tempo.xyz')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('cache policy', () => {
|
|
68
|
+
test('every response is no-store (admin data is per-principal and mutating)', async () => {
|
|
69
|
+
const { app } = TestAdmin.setup()
|
|
70
|
+
for (const path of ['/scopes', '/api-keys', '/config.json']) {
|
|
71
|
+
const response = await app.request(`http://admin${path}`)
|
|
72
|
+
expect(response.headers.get('cache-control')).toBe('no-store')
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('GET /config.json', () => {
|
|
78
|
+
test('reports the signed-in identity and sign-in/out URLs', async () => {
|
|
79
|
+
const { app } = TestAdmin.setup({
|
|
80
|
+
identify: () => ({ email: 'ops@tempo.xyz' }),
|
|
81
|
+
ui: { loginUrl: '/login', logoutUrl: '/logout' },
|
|
82
|
+
})
|
|
83
|
+
const response = await app.request('http://admin/config.json')
|
|
84
|
+
expect(response.status).toBe(200)
|
|
85
|
+
expect(response.headers.get('cache-control')).toBe('no-store')
|
|
86
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
87
|
+
{
|
|
88
|
+
"auth": {
|
|
89
|
+
"identity": {
|
|
90
|
+
"email": "ops@tempo.xyz",
|
|
91
|
+
},
|
|
92
|
+
"loginUrl": "/login",
|
|
93
|
+
"logoutUrl": "/logout",
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
`)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('omits sign-in/out URLs when no ui config is provided', async () => {
|
|
100
|
+
const { app } = TestAdmin.setup({ identify: () => ({ email: 'ops@tempo.xyz' }) })
|
|
101
|
+
const response = await app.request('http://admin/config.json')
|
|
102
|
+
expect(response.status).toBe(200)
|
|
103
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
104
|
+
{
|
|
105
|
+
"auth": {
|
|
106
|
+
"identity": {
|
|
107
|
+
"email": "ops@tempo.xyz",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
`)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('GET / (SPA shell)', () => {
|
|
116
|
+
test('serves the prerendered SPA shell by default', async () => {
|
|
117
|
+
const { app } = TestAdmin.setup()
|
|
118
|
+
const response = await app.request('http://admin/')
|
|
119
|
+
expect(response.status).toBe(200)
|
|
120
|
+
expect(response.headers.get('content-type')).toContain('text/html')
|
|
121
|
+
const html = await response.text()
|
|
122
|
+
expect(html).toContain('<div id="root">')
|
|
123
|
+
// Standalone mount → `<base href="/">` so relative asset/API URLs resolve.
|
|
124
|
+
expect(html).toContain('<base href="/"/>')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('inlines the gated auth config into the shell (no bootstrap round trip)', async () => {
|
|
128
|
+
const { app } = TestAdmin.setup({
|
|
129
|
+
ui: { loginUrl: 'https://sso/login', logoutUrl: 'https://sso/logout' },
|
|
130
|
+
})
|
|
131
|
+
const html = await (await app.request('http://admin/')).text()
|
|
132
|
+
const json = html.match(/window\.__TEMPO_API_CONFIG__=({.*?})<\/script>/)?.[1]
|
|
133
|
+
expect(json).toBeDefined()
|
|
134
|
+
expect(JSON.parse(json!)).toEqual({
|
|
135
|
+
auth: {
|
|
136
|
+
identity: { email: 'admin@tempo.xyz' },
|
|
137
|
+
loginUrl: 'https://sso/login',
|
|
138
|
+
logoutUrl: 'https://sso/logout',
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('HTML navigation to a data-route path (e.g. /api-keys) serves the shell', async () => {
|
|
144
|
+
const { app } = TestAdmin.setup()
|
|
145
|
+
// `/api-keys` is both an SPA route and a data endpoint; a browser hard
|
|
146
|
+
// reload (Accept: text/html) must get the shell, not the JSON list.
|
|
147
|
+
const response = await app.request('http://admin/api-keys', {
|
|
148
|
+
headers: { Accept: 'text/html,application/xhtml+xml' },
|
|
149
|
+
})
|
|
150
|
+
expect(response.status).toBe(200)
|
|
151
|
+
expect(response.headers.get('content-type')).toContain('text/html')
|
|
152
|
+
expect(await response.text()).toContain('<div id="root">')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('fetch/RPC to a data-route path still returns JSON (not the shell)', async () => {
|
|
156
|
+
const { app } = TestAdmin.setup()
|
|
157
|
+
const response = await app.request('http://admin/api-keys')
|
|
158
|
+
expect(response.headers.get('content-type')).toContain('application/json')
|
|
159
|
+
expect((await response.json()) as { data: unknown[] }).toEqual({ data: [] })
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('any non-API GET falls back to the shell (client-side routing)', async () => {
|
|
163
|
+
const { app } = TestAdmin.setup()
|
|
164
|
+
const response = await app.request('http://admin/verified-tokens-ui')
|
|
165
|
+
expect(response.status).toBe(200)
|
|
166
|
+
expect(await response.text()).toContain('<div id="root">')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test('path-mounted: shell gets a `<base href>` of the mount root', async () => {
|
|
170
|
+
const { app } = TestAdmin.setup()
|
|
171
|
+
const api = new Hono().route('/admin', app)
|
|
172
|
+
const response = await api.request('http://host/admin/some/deep/link')
|
|
173
|
+
expect(response.status).toBe(200)
|
|
174
|
+
// The base href is the mount root, not the deep link, so assets/API
|
|
175
|
+
// resolve under `/admin/` from any in-app route.
|
|
176
|
+
expect(await response.text()).toContain('<base href="/admin/"/>')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('ui: false → no shell; `/` returns the JSON 404', async () => {
|
|
180
|
+
const { app } = TestAdmin.setup({ ui: false })
|
|
181
|
+
const response = await app.request('http://admin/')
|
|
182
|
+
expect(response.status).toBe(404)
|
|
183
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe('not_found')
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('GET /assets/* (SPA assets)', () => {
|
|
188
|
+
test('serves a hashed asset with an immutable cache header', async () => {
|
|
189
|
+
const { app } = TestAdmin.setup()
|
|
190
|
+
// Drive the asset name from the generated shell so the test survives UI
|
|
191
|
+
// churn (hashes change on every build).
|
|
192
|
+
const shell = await (await app.request('http://admin/')).text()
|
|
193
|
+
const assetPath = shell.match(/\.\/(assets\/[^"]+\.js)/)?.[1]
|
|
194
|
+
expect(assetPath).toBeDefined()
|
|
195
|
+
const response = await app.request(`http://admin/${assetPath}`)
|
|
196
|
+
expect(response.status).toBe(200)
|
|
197
|
+
expect(response.headers.get('content-type')).toContain('text/javascript')
|
|
198
|
+
expect(response.headers.get('cache-control')).toContain('immutable')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('unknown asset → JSON 404', async () => {
|
|
202
|
+
const { app } = TestAdmin.setup()
|
|
203
|
+
const response = await app.request('http://admin/assets/does-not-exist.js')
|
|
204
|
+
expect(response.status).toBe(404)
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe('not found', () => {
|
|
209
|
+
test('ui: false → unknown routes get the JSON 404 envelope', async () => {
|
|
210
|
+
const { app } = TestAdmin.setup({ ui: false })
|
|
211
|
+
const response = await app.request('http://admin/nope')
|
|
212
|
+
const body = (await response.json()) as { error: { code: string } }
|
|
213
|
+
expect(response.status).toBe(404)
|
|
214
|
+
expect(body.error.code).toBe('not_found')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('non-GET unknown routes get the JSON 404 envelope (shell only serves GET)', async () => {
|
|
218
|
+
const { app } = TestAdmin.setup()
|
|
219
|
+
const response = await app.request('http://admin/nope', { method: 'POST' })
|
|
220
|
+
const body = (await response.json()) as { error: { code: string } }
|
|
221
|
+
expect(response.status).toBe(404)
|
|
222
|
+
expect(body.error.code).toBe('not_found')
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
})
|
package/src/admin/App.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { type Context, Hono, type Next } from 'hono'
|
|
2
|
+
import { HTTPException } from 'hono/http-exception'
|
|
3
|
+
import { requestId, type RequestIdVariables } from 'hono/request-id'
|
|
4
|
+
|
|
5
|
+
import * as ApiKey from '../ApiKey.js'
|
|
6
|
+
import * as ApiKeySource from '../ApiKeySource.js'
|
|
7
|
+
import type * as Assets from '../Assets.js'
|
|
8
|
+
import * as Response from '../internal/Response.js'
|
|
9
|
+
import type * as Store from '../internal/Store.js'
|
|
10
|
+
|
|
11
|
+
import { apiKeys } from './apps/api-keys.js'
|
|
12
|
+
import { scopes } from './apps/scopes.js'
|
|
13
|
+
import { verifiedTokens } from './apps/verified-tokens.js'
|
|
14
|
+
import * as Ui from './ui.generated.js'
|
|
15
|
+
|
|
16
|
+
/** A verified admin identity, as returned by {@link create.Options.identify}. */
|
|
17
|
+
export type Identity = {
|
|
18
|
+
/** The admin's verified email, recorded as `createdBy` on every minted key. */
|
|
19
|
+
email: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Hono environment for the admin app. */
|
|
23
|
+
export type Environment = {
|
|
24
|
+
Variables: RequestIdVariables & {
|
|
25
|
+
/** Static-asset writer (e.g. the R2 icon bucket), when configured. */
|
|
26
|
+
assets: create.Assets | undefined
|
|
27
|
+
/** The admin identity, or `null` when unauthenticated (gated routes reject). */
|
|
28
|
+
identity: Identity | null
|
|
29
|
+
/** The API-key source provisioned against (mint/list/revoke). */
|
|
30
|
+
source: ApiKeySource.Source
|
|
31
|
+
/** Verified-token state store, when management is enabled. */
|
|
32
|
+
verifiedTokens: { store: Store.State } | undefined
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The admin portal Hono app. Inferred from {@link create}. */
|
|
37
|
+
export type App = ReturnType<typeof create>
|
|
38
|
+
|
|
39
|
+
/** Creates the admin portal Hono app. */
|
|
40
|
+
export function create(options: create.Options) {
|
|
41
|
+
const { identify } = options
|
|
42
|
+
const { source } = options.apiKey
|
|
43
|
+
// `false` (disabled) collapses to `undefined` so only a real config surfaces
|
|
44
|
+
// `loginUrl`/`logoutUrl` in `/config.json`.
|
|
45
|
+
const ui = options.ui || undefined
|
|
46
|
+
|
|
47
|
+
const app = new Hono<Environment>()
|
|
48
|
+
|
|
49
|
+
app.use('*', requestId({ headerName: 'tempo-request-id' }))
|
|
50
|
+
|
|
51
|
+
// Public hashed assets (no identity needed); before identify so it's skipped.
|
|
52
|
+
if (options.ui !== false) app.get('/assets/*', serveAsset)
|
|
53
|
+
|
|
54
|
+
// Resolve identity for every request, but don't reject here; the gate does.
|
|
55
|
+
app.use('*', async (c, next) => {
|
|
56
|
+
// Admin data is per-principal and mutating: never cache.
|
|
57
|
+
c.header('Cache-Control', 'no-store')
|
|
58
|
+
c.set('source', source)
|
|
59
|
+
c.set('assets', options.assets)
|
|
60
|
+
c.set('verifiedTokens', options.verifiedTokens)
|
|
61
|
+
c.set('identity', await identify(c.req.raw))
|
|
62
|
+
return next()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Auth contract the UI reads (identity + sign-in/out URLs). Served at
|
|
66
|
+
// `/config.json` and inlined into the shell, so the UI reads it synchronously.
|
|
67
|
+
function authConfig(identity: Identity | null) {
|
|
68
|
+
return {
|
|
69
|
+
auth: {
|
|
70
|
+
identity,
|
|
71
|
+
...(ui?.loginUrl ? { loginUrl: ui.loginUrl } : {}),
|
|
72
|
+
...(ui?.logoutUrl ? { logoutUrl: ui.logoutUrl } : {}),
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Renders the public SPA shell: injects a `<base href>` of the mount root (so
|
|
78
|
+
// relative asset/API URLs resolve) and the auth config (read synchronously,
|
|
79
|
+
// `identity` is `null` when logged out). `<` is escaped to keep `<script>`.
|
|
80
|
+
const serveShell = (c: Context<Environment>) => {
|
|
81
|
+
const mount = c.req.routePath.replace(/\*$/, '').replace(/\/$/, '')
|
|
82
|
+
const config = JSON.stringify(authConfig(c.get('identity'))).replaceAll('<', '\\u003c')
|
|
83
|
+
return c.html(
|
|
84
|
+
Ui.shell.replace(
|
|
85
|
+
'<head>',
|
|
86
|
+
`<head><base href="${mount}/"/><script>window.__TEMPO_API_CONFIG__=${config}</script>`,
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Browser navigations (`Accept: text/html`) get the public shell before the
|
|
92
|
+
// data routes match, so SPA routes that share a path with a data endpoint
|
|
93
|
+
// (e.g. `/api-keys`) still load on hard reload. `fetch`/RPC (`*/*`) fall
|
|
94
|
+
// through to the JSON API below.
|
|
95
|
+
if (options.ui !== false)
|
|
96
|
+
app.get('*', (c, next) =>
|
|
97
|
+
c.req.header('accept')?.includes('text/html') ? serveShell(c) : next(),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// Gate config + data API (`identity` → `null` ⇒ 401); shell/assets stay public.
|
|
101
|
+
const requireAuth = async (c: Context<Environment>, next: Next) => {
|
|
102
|
+
if (!c.get('identity'))
|
|
103
|
+
return Response.error(c, {
|
|
104
|
+
code: 'unauthorized',
|
|
105
|
+
message: 'Admin authentication required.',
|
|
106
|
+
status: 401,
|
|
107
|
+
})
|
|
108
|
+
return next()
|
|
109
|
+
}
|
|
110
|
+
for (const path of [
|
|
111
|
+
'/config.json',
|
|
112
|
+
'/api-keys',
|
|
113
|
+
'/api-keys/*',
|
|
114
|
+
'/scopes',
|
|
115
|
+
'/scopes/*',
|
|
116
|
+
'/verified-tokens',
|
|
117
|
+
'/verified-tokens/*',
|
|
118
|
+
])
|
|
119
|
+
app.use(path, requireAuth)
|
|
120
|
+
|
|
121
|
+
const routed = app
|
|
122
|
+
.get('/config.json', (c) => c.json(authConfig(c.get('identity')), 200))
|
|
123
|
+
.route('/api-keys', apiKeys())
|
|
124
|
+
.route('/scopes', scopes())
|
|
125
|
+
.route('/verified-tokens', verifiedTokens())
|
|
126
|
+
|
|
127
|
+
// Any other non-API GET renders the public SPA shell (catch-all last, so data
|
|
128
|
+
// routes win); the HTML-navigation handler above covers shared-path reloads.
|
|
129
|
+
if (options.ui !== false) app.get('*', serveShell)
|
|
130
|
+
|
|
131
|
+
app.notFound((c) =>
|
|
132
|
+
Response.error(c, { code: 'not_found', message: 'Route not found', status: 404 }),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
app.onError((cause, c) => {
|
|
136
|
+
if (cause instanceof HTTPException) return cause.getResponse()
|
|
137
|
+
// Redact any leaked API-key token from the logged error/stack.
|
|
138
|
+
console.error(
|
|
139
|
+
ApiKey.redact(cause instanceof Error ? (cause.stack ?? cause.message) : String(cause)),
|
|
140
|
+
)
|
|
141
|
+
return Response.error(c, {
|
|
142
|
+
code: 'internal_error',
|
|
143
|
+
message: 'Internal server error',
|
|
144
|
+
status: 500,
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return routed
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Serves a hashed SPA asset with an immutable cache (override the global
|
|
152
|
+
// `no-store`). Filename comes from the path, so it resolves under any mount.
|
|
153
|
+
function serveAsset(c: Context<Environment>) {
|
|
154
|
+
const asset = Ui.assets[`assets/${c.req.path.split('/assets/').at(-1)}`]
|
|
155
|
+
if (!asset)
|
|
156
|
+
return Response.error(c, { code: 'not_found', message: 'Route not found', status: 404 })
|
|
157
|
+
c.header('Cache-Control', 'public, max-age=31536000, immutable')
|
|
158
|
+
c.header('Content-Type', asset.contentType)
|
|
159
|
+
return c.body(
|
|
160
|
+
asset.encoding === 'base64'
|
|
161
|
+
? Uint8Array.from(atob(asset.content), (ch) => ch.charCodeAt(0))
|
|
162
|
+
: asset.content,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export declare namespace create {
|
|
167
|
+
/**
|
|
168
|
+
* Static-asset store for the deployment (e.g. `Assets.cloudflareR2(...)`,
|
|
169
|
+
* shared with the main API). Keyed `<chainId>/<path>`; `get` enables logo
|
|
170
|
+
* previews, `put` enables logo uploads.
|
|
171
|
+
*/
|
|
172
|
+
type Assets = Assets.Assets
|
|
173
|
+
|
|
174
|
+
/** Options for {@link create}. */
|
|
175
|
+
type Options = {
|
|
176
|
+
/** API-key provisioning config (mirrors the API app's `apiKey` option). */
|
|
177
|
+
apiKey: {
|
|
178
|
+
/** Source to provision against (mint/list/revoke). */
|
|
179
|
+
source: ApiKeySource.Source
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Static-asset writer for logo uploads (shared with the main API's `assets`
|
|
183
|
+
* loader) to enable `PUT /verified-tokens/:address/logo`; omit for `404`.
|
|
184
|
+
*/
|
|
185
|
+
assets?: Assets | undefined
|
|
186
|
+
/**
|
|
187
|
+
* Extracts the verified admin identity from a request, or `null` to deny.
|
|
188
|
+
* Must only return an identity for an already-authenticated request (a
|
|
189
|
+
* verified Access JWT, or a header set by a trusted proxy).
|
|
190
|
+
*/
|
|
191
|
+
identify: (request: Request) => Identity | null | Promise<Identity | null>
|
|
192
|
+
/**
|
|
193
|
+
* Admin UI options, or `false` to disable the bundled UI (JSON API only).
|
|
194
|
+
* `loginUrl`/`logoutUrl` are surfaced in `GET /config.json`.
|
|
195
|
+
*/
|
|
196
|
+
ui?: Ui | false | undefined
|
|
197
|
+
/**
|
|
198
|
+
* Verified-token management config (store shared with the main API's reads)
|
|
199
|
+
* to enable the `/verified-tokens` routes; omit to serve them as `404`.
|
|
200
|
+
*/
|
|
201
|
+
verifiedTokens?:
|
|
202
|
+
| {
|
|
203
|
+
/** Verified-token state store (shared with the main API's reads). */
|
|
204
|
+
store: Store.State
|
|
205
|
+
}
|
|
206
|
+
| undefined
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Admin UI options surfaced to the SPA via `GET /config.json`. */
|
|
210
|
+
type Ui = {
|
|
211
|
+
/** Where the deployer's sign-in page lives; shown by the UI on a `401`. */
|
|
212
|
+
loginUrl?: string | undefined
|
|
213
|
+
/** Where to send the user to sign out (e.g. Cloudflare Access logout). */
|
|
214
|
+
logoutUrl?: string | undefined
|
|
215
|
+
}
|
|
216
|
+
}
|