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,648 @@
|
|
|
1
|
+
import * as WebhookDestination from './WebhookDestination.js'
|
|
2
|
+
|
|
3
|
+
const envelope = {
|
|
4
|
+
chainId: 4217,
|
|
5
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
6
|
+
data: { amount: '1000', token: '0xabc' },
|
|
7
|
+
id: 'evt_test',
|
|
8
|
+
subscriptionId: 'wh_test',
|
|
9
|
+
type: 'token:transfer',
|
|
10
|
+
} satisfies WebhookDestination.deliver.Options['envelope']
|
|
11
|
+
|
|
12
|
+
describe('url', () => {
|
|
13
|
+
test('builds a url destination with a deliver method', () => {
|
|
14
|
+
const destination = WebhookDestination.url('https://example.com/hook')
|
|
15
|
+
expect(destination).toMatchObject({ type: 'url', url: 'https://example.com/hook' })
|
|
16
|
+
expect(typeof destination.deliver).toBe('function')
|
|
17
|
+
// The method drops out of JSON — a destination serializes to just its data.
|
|
18
|
+
expect(JSON.parse(JSON.stringify(destination))).toEqual({
|
|
19
|
+
type: 'url',
|
|
20
|
+
url: 'https://example.com/hook',
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('rejects a blocked host', () => {
|
|
25
|
+
expect(() =>
|
|
26
|
+
WebhookDestination.url('https://169.254.169.254/latest/meta-data'),
|
|
27
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
28
|
+
`[Webhooks.InvalidUrlError: Webhook URL rejected (blocked_host): https://169.254.169.254/latest/meta-data]`,
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('slack', () => {
|
|
34
|
+
test('builds a slack destination', () => {
|
|
35
|
+
expect(WebhookDestination.slack('https://hooks.slack.com/services/T/B/x')).toMatchObject({
|
|
36
|
+
type: 'slack',
|
|
37
|
+
url: 'https://hooks.slack.com/services/T/B/x',
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('rejects a non-Slack host', () => {
|
|
42
|
+
expect(() =>
|
|
43
|
+
WebhookDestination.slack('https://evil.example.com/hook'),
|
|
44
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
45
|
+
`[Webhooks.InvalidUrlError: Webhook URL rejected (slack_host): https://evil.example.com/hook]`,
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('betterStack', () => {
|
|
51
|
+
test('builds a betterstack destination', () => {
|
|
52
|
+
expect(
|
|
53
|
+
WebhookDestination.betterStack({
|
|
54
|
+
token: 'src_token',
|
|
55
|
+
url: 'https://s1.eu-nbg-2.betterstackdata.com',
|
|
56
|
+
}),
|
|
57
|
+
).toMatchObject({
|
|
58
|
+
token: 'src_token',
|
|
59
|
+
type: 'betterstack',
|
|
60
|
+
url: 'https://s1.eu-nbg-2.betterstackdata.com',
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('rejects a non-Better Stack host', () => {
|
|
65
|
+
expect(() =>
|
|
66
|
+
WebhookDestination.betterStack({ token: 'src_token', url: 'https://evil.example.com' }),
|
|
67
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
68
|
+
`[Webhooks.InvalidUrlError: Webhook URL rejected (betterstack_host): https://evil.example.com]`,
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('from', () => {
|
|
74
|
+
test('normalizes a bare URL string into a url destination', () => {
|
|
75
|
+
expect(WebhookDestination.from('https://example.com/hook')).toMatchObject({
|
|
76
|
+
type: 'url',
|
|
77
|
+
url: 'https://example.com/hook',
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('validates and returns a destination object', () => {
|
|
82
|
+
expect(
|
|
83
|
+
WebhookDestination.from({ type: 'slack', url: 'https://hooks.slack.com/services/T/B/x' }),
|
|
84
|
+
).toMatchObject({ type: 'slack', url: 'https://hooks.slack.com/services/T/B/x' })
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('rejects an invalid destination', () => {
|
|
88
|
+
expect(() =>
|
|
89
|
+
WebhookDestination.from({ type: 'slack', url: 'https://evil.example.com' }),
|
|
90
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
91
|
+
`[Webhooks.InvalidUrlError: Webhook URL rejected (slack_host): https://evil.example.com]`,
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('url().deliver', () => {
|
|
97
|
+
test('signs the body and POSTs a 2xx as success', async () => {
|
|
98
|
+
let captured:
|
|
99
|
+
| { body: string; headers: Headers; method?: string | undefined; url: string }
|
|
100
|
+
| undefined
|
|
101
|
+
const fetch = (async (input, init) => {
|
|
102
|
+
captured = {
|
|
103
|
+
body: typeof init?.body === 'string' ? init.body : '',
|
|
104
|
+
headers: new Headers(init?.headers),
|
|
105
|
+
method: init?.method,
|
|
106
|
+
url: typeof input === 'string' ? input : '',
|
|
107
|
+
}
|
|
108
|
+
return new Response(null, { status: 200 })
|
|
109
|
+
}) satisfies typeof globalThis.fetch
|
|
110
|
+
|
|
111
|
+
const secret = 'whsec_test'
|
|
112
|
+
const result = await WebhookDestination.url('https://hooks.example.com/x').deliver({
|
|
113
|
+
envelope,
|
|
114
|
+
fetch,
|
|
115
|
+
now: () => 0,
|
|
116
|
+
secret,
|
|
117
|
+
timestamp: 1_000,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
expect(result).toMatchInlineSnapshot(`
|
|
121
|
+
{
|
|
122
|
+
"durationMs": 0,
|
|
123
|
+
"ok": true,
|
|
124
|
+
"status": 200,
|
|
125
|
+
}
|
|
126
|
+
`)
|
|
127
|
+
expect(captured?.method).toMatchInlineSnapshot(`"POST"`)
|
|
128
|
+
expect(captured?.headers.get('tempo-event-id')).toMatchInlineSnapshot(`"evt_test"`)
|
|
129
|
+
expect(captured?.headers.get('tempo-event-type')).toMatchInlineSnapshot(`"token:transfer"`)
|
|
130
|
+
|
|
131
|
+
const signature = captured?.headers.get('tempo-signature') ?? ''
|
|
132
|
+
expect(
|
|
133
|
+
WebhookDestination.verify({ body: captured?.body ?? '', now: 1_000, secret, signature }),
|
|
134
|
+
).toMatchInlineSnapshot(`true`)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('classifies a non-2xx as failure with the status', async () => {
|
|
138
|
+
const fetch = (async () =>
|
|
139
|
+
new Response(null, { status: 500 })) satisfies typeof globalThis.fetch
|
|
140
|
+
const result = await WebhookDestination.url('https://hooks.example.com/x').deliver({
|
|
141
|
+
envelope,
|
|
142
|
+
fetch,
|
|
143
|
+
now: () => 0,
|
|
144
|
+
secret: 'whsec_test',
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
expect(result).toMatchInlineSnapshot(`
|
|
148
|
+
{
|
|
149
|
+
"durationMs": 0,
|
|
150
|
+
"error": "non-2xx response (500)",
|
|
151
|
+
"ok": false,
|
|
152
|
+
"status": 500,
|
|
153
|
+
}
|
|
154
|
+
`)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('rejects a 3xx redirect without following it (SSRF guard)', async () => {
|
|
158
|
+
let redirectMode: RequestRedirect | undefined
|
|
159
|
+
const fetch = (async (_input, init) => {
|
|
160
|
+
redirectMode = init?.redirect
|
|
161
|
+
return new Response(null, {
|
|
162
|
+
headers: { location: 'https://169.254.169.254/latest' },
|
|
163
|
+
status: 302,
|
|
164
|
+
})
|
|
165
|
+
}) satisfies typeof globalThis.fetch
|
|
166
|
+
|
|
167
|
+
const result = await WebhookDestination.url('https://hooks.example.com/x').deliver({
|
|
168
|
+
envelope,
|
|
169
|
+
fetch,
|
|
170
|
+
now: () => 0,
|
|
171
|
+
secret: 'whsec_test',
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
expect(redirectMode).toMatchInlineSnapshot(`"manual"`)
|
|
175
|
+
expect(result).toMatchInlineSnapshot(`
|
|
176
|
+
{
|
|
177
|
+
"durationMs": 0,
|
|
178
|
+
"error": "redirect rejected (302)",
|
|
179
|
+
"ok": false,
|
|
180
|
+
"status": 302,
|
|
181
|
+
}
|
|
182
|
+
`)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('fails a url delivery attempted without a secret', async () => {
|
|
186
|
+
let called = false
|
|
187
|
+
const fetch = (async () => {
|
|
188
|
+
called = true
|
|
189
|
+
return new Response(null, { status: 200 })
|
|
190
|
+
}) satisfies typeof globalThis.fetch
|
|
191
|
+
|
|
192
|
+
const result = await WebhookDestination.url('https://hooks.example.com/x').deliver({
|
|
193
|
+
envelope,
|
|
194
|
+
fetch,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
expect(called).toBe(false)
|
|
198
|
+
expect(result).toMatchInlineSnapshot(`
|
|
199
|
+
{
|
|
200
|
+
"error": "url destination requires a secret",
|
|
201
|
+
"ok": false,
|
|
202
|
+
}
|
|
203
|
+
`)
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('slack().deliver', () => {
|
|
208
|
+
test('POSTs an unsigned Block Kit message to the incoming-webhook URL', async () => {
|
|
209
|
+
let captured: { body: string; headers: Headers; url: string } | undefined
|
|
210
|
+
const fetch = (async (input, init) => {
|
|
211
|
+
captured = {
|
|
212
|
+
body: typeof init?.body === 'string' ? init.body : '',
|
|
213
|
+
headers: new Headers(init?.headers),
|
|
214
|
+
url: typeof input === 'string' ? input : '',
|
|
215
|
+
}
|
|
216
|
+
return new Response(null, { status: 200 })
|
|
217
|
+
}) satisfies typeof globalThis.fetch
|
|
218
|
+
|
|
219
|
+
const result = await WebhookDestination.slack(
|
|
220
|
+
'https://hooks.slack.com/services/T000/B000/xxxx',
|
|
221
|
+
).deliver({ envelope, fetch, now: () => 0 })
|
|
222
|
+
|
|
223
|
+
expect(result).toMatchInlineSnapshot(`
|
|
224
|
+
{
|
|
225
|
+
"durationMs": 0,
|
|
226
|
+
"ok": true,
|
|
227
|
+
"status": 200,
|
|
228
|
+
}
|
|
229
|
+
`)
|
|
230
|
+
expect(captured?.url).toBe('https://hooks.slack.com/services/T000/B000/xxxx')
|
|
231
|
+
expect(captured?.headers.get('tempo-signature')).toBeNull()
|
|
232
|
+
const payload = JSON.parse(captured?.body ?? '{}') as { blocks: unknown[]; text: string }
|
|
233
|
+
expect(Array.isArray(payload.blocks)).toBe(true)
|
|
234
|
+
expect(payload.text).toMatchInlineSnapshot(`"Token transfer · 1000 · chain 4217"`)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test('leads with the subscription context (title header + description section)', async () => {
|
|
238
|
+
let captured: { body: string } | undefined
|
|
239
|
+
const fetch = (async (_input, init) => {
|
|
240
|
+
captured = { body: typeof init?.body === 'string' ? init.body : '' }
|
|
241
|
+
return new Response(null, { status: 200 })
|
|
242
|
+
}) satisfies typeof globalThis.fetch
|
|
243
|
+
|
|
244
|
+
await WebhookDestination.slack('https://hooks.slack.com/services/T/B/x').deliver({
|
|
245
|
+
envelope: {
|
|
246
|
+
...envelope,
|
|
247
|
+
context: {
|
|
248
|
+
description: 'Notify #ops on big settles.',
|
|
249
|
+
metadata: { env: 'prod', team: 'payments' },
|
|
250
|
+
title: 'Prod USDC transfers',
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
fetch,
|
|
254
|
+
now: () => 0,
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const payload = JSON.parse(captured?.body ?? '{}') as {
|
|
258
|
+
blocks: { fields?: { text: string }[]; text?: { text: string }; type: string }[]
|
|
259
|
+
text: string
|
|
260
|
+
}
|
|
261
|
+
// Title drives the header + notification fallback.
|
|
262
|
+
expect(payload.text).toBe('Prod USDC transfers')
|
|
263
|
+
expect(payload.blocks[0]).toMatchObject({
|
|
264
|
+
text: { text: 'Prod USDC transfers' },
|
|
265
|
+
type: 'header',
|
|
266
|
+
})
|
|
267
|
+
// Description gets its own section, before the field grid.
|
|
268
|
+
expect(payload.blocks[1]).toMatchObject({
|
|
269
|
+
text: { text: 'Notify #ops on big settles.' },
|
|
270
|
+
type: 'section',
|
|
271
|
+
})
|
|
272
|
+
// Metadata renders as its own field grid.
|
|
273
|
+
const metadata = payload.blocks.find((block) =>
|
|
274
|
+
block.fields?.some((f) => f.text.startsWith('*env*')),
|
|
275
|
+
)
|
|
276
|
+
expect(metadata?.fields).toEqual([
|
|
277
|
+
{ text: '*env*\nprod', type: 'mrkdwn' },
|
|
278
|
+
{ text: '*team*\npayments', type: 'mrkdwn' },
|
|
279
|
+
])
|
|
280
|
+
// The raw event type stays in the context line so identity survives the title.
|
|
281
|
+
const context = payload.blocks.at(-1)
|
|
282
|
+
expect(context?.type).toBe('context')
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('renders a token:transfer with a human amount and explorer links', async () => {
|
|
286
|
+
const payload = await captureSlack({
|
|
287
|
+
chainId: 4217,
|
|
288
|
+
createdAt: '2026-01-14T18:38:03.000Z',
|
|
289
|
+
data: {
|
|
290
|
+
address: '0x20c0000000000000000000008f5425160ebe5525',
|
|
291
|
+
amount: '10000000',
|
|
292
|
+
blockNumber: 1000002,
|
|
293
|
+
recipient: '0x9e39034aae71fb89f66061a2602eb6efec271754',
|
|
294
|
+
sender: '0xe7687128b0a808c2831ff94d4f7b2fb35c65af38',
|
|
295
|
+
timestamp: '2026-01-14T18:38:03.000Z',
|
|
296
|
+
token: { decimals: 6, symbol: 'USDC' },
|
|
297
|
+
transactionHash: '0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3',
|
|
298
|
+
},
|
|
299
|
+
id: 'evt_abc',
|
|
300
|
+
subscriptionId: 'wh_abc',
|
|
301
|
+
type: 'token:transfer',
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
expect(header(payload)).toBe(':money_with_wings: Token transfer')
|
|
305
|
+
// Human amount from token metadata, with truncated + linked sender/recipient.
|
|
306
|
+
expect(section(payload, 1)).toContain('*10 USDC* from')
|
|
307
|
+
expect(section(payload, 1)).toContain('<https://explore.tempo.xyz/address/0xe768')
|
|
308
|
+
expect(payload.text).toBe('Token transfer · 10 USDC · chain 4217')
|
|
309
|
+
// The Amount field renders the human amount only (no raw base units).
|
|
310
|
+
const json = JSON.stringify(payload.blocks)
|
|
311
|
+
expect(json).toContain('*Amount*\\n10 USDC')
|
|
312
|
+
expect(json).not.toContain('base units')
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
test('renders a reverted transaction with an :x: accent header', async () => {
|
|
316
|
+
const payload = await captureSlack({
|
|
317
|
+
chainId: 4217,
|
|
318
|
+
createdAt: '2026-01-14T18:38:03.000Z',
|
|
319
|
+
data: {
|
|
320
|
+
blockNumber: 23456789,
|
|
321
|
+
hash: '0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3',
|
|
322
|
+
meta: { receipt: { status: 'reverted' } },
|
|
323
|
+
recipient: '0x9e39034aae71fb89f66061a2602eb6efec271754',
|
|
324
|
+
sender: '0xe7687128b0a808c2831ff94d4f7b2fb35c65af38',
|
|
325
|
+
type: 'eip1559',
|
|
326
|
+
value: '0',
|
|
327
|
+
},
|
|
328
|
+
id: 'evt_tx',
|
|
329
|
+
subscriptionId: 'wh_tx',
|
|
330
|
+
type: 'transaction:included',
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
expect(header(payload)).toBe(':x: Transaction reverted')
|
|
334
|
+
expect(section(payload, 1)).toContain(':x: *reverted*')
|
|
335
|
+
expect(payload.text).toBe('Transaction included · reverted · chain 4217')
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
test('renders a decoded log with the event name in the header', async () => {
|
|
339
|
+
const payload = await captureSlack({
|
|
340
|
+
chainId: 4217,
|
|
341
|
+
createdAt: '2026-01-14T18:38:03.000Z',
|
|
342
|
+
data: {
|
|
343
|
+
address: '0x20c0000000000000000000008f5425160ebe5525',
|
|
344
|
+
args: { from: '0xe7687128b0a808c2831ff94d4f7b2fb35c65af38', value: '10000' },
|
|
345
|
+
blockNumber: 1000002,
|
|
346
|
+
event: { name: 'Transfer', signature: 'event Transfer(address,address,uint256)' },
|
|
347
|
+
logIndex: 0,
|
|
348
|
+
timestamp: '2026-01-14T18:38:03.000Z',
|
|
349
|
+
transactionHash: '0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3',
|
|
350
|
+
},
|
|
351
|
+
id: 'evt_log',
|
|
352
|
+
subscriptionId: 'wh_log',
|
|
353
|
+
type: 'log:emitted',
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
expect(header(payload)).toBe(':label: Log: Transfer')
|
|
357
|
+
expect(section(payload, 1)).toContain('*Transfer* on')
|
|
358
|
+
expect(payload.text).toContain('Log: Transfer')
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
test('renders a block:created header with the block number', async () => {
|
|
362
|
+
const payload = await captureSlack({
|
|
363
|
+
chainId: 4217,
|
|
364
|
+
createdAt: '2026-01-14T18:38:03.000Z',
|
|
365
|
+
data: {
|
|
366
|
+
gasLimit: 500000000,
|
|
367
|
+
gasUsed: 4340281,
|
|
368
|
+
hash: '0x3fe7d9e595f3d8215ec840c6ef55ac0bead58933e96fc2ead0f2aaf79715f45d',
|
|
369
|
+
miner: '0x0000000000000000000000000000000000000000',
|
|
370
|
+
number: 1000002,
|
|
371
|
+
transactionCount: 3,
|
|
372
|
+
timestamp: '2026-01-14T18:38:03.000Z',
|
|
373
|
+
},
|
|
374
|
+
id: 'evt_blk',
|
|
375
|
+
subscriptionId: 'wh_blk',
|
|
376
|
+
type: 'block:created',
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
expect(header(payload)).toBe(':link: Block created #1000002')
|
|
380
|
+
expect(section(payload, 1)).toContain('*3* transactions')
|
|
381
|
+
expect(payload.text).toBe('Block created #1000002 · 3 txns · chain 4217')
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
test('renders a ping with a wired-up confirmation lead', async () => {
|
|
385
|
+
const payload = await captureSlack({
|
|
386
|
+
chainId: 4217,
|
|
387
|
+
createdAt: '2026-01-14T18:38:03.000Z',
|
|
388
|
+
data: { ping: true },
|
|
389
|
+
id: 'evt_ping',
|
|
390
|
+
subscriptionId: 'wh_ping',
|
|
391
|
+
type: 'ping',
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
expect(header(payload)).toBe(':bell: Webhook test ping')
|
|
395
|
+
expect(section(payload, 1)).toContain('wired up correctly')
|
|
396
|
+
expect(payload.text).toBe('Webhook test ping · chain 4217')
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
test('classifies a non-2xx Slack response as failure', async () => {
|
|
400
|
+
const fetch = (async () =>
|
|
401
|
+
new Response('invalid_token', { status: 403 })) satisfies typeof globalThis.fetch
|
|
402
|
+
const result = await WebhookDestination.slack(
|
|
403
|
+
'https://hooks.slack.com/services/T000/B000/xxxx',
|
|
404
|
+
).deliver({ envelope, fetch, now: () => 0 })
|
|
405
|
+
|
|
406
|
+
expect(result).toMatchInlineSnapshot(`
|
|
407
|
+
{
|
|
408
|
+
"durationMs": 0,
|
|
409
|
+
"error": "slack non-2xx response (403)",
|
|
410
|
+
"ok": false,
|
|
411
|
+
"status": 403,
|
|
412
|
+
}
|
|
413
|
+
`)
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
/** A parsed Slack Block Kit payload with the loose block shapes we assert on. */
|
|
418
|
+
type SlackPayload = {
|
|
419
|
+
blocks: { elements?: { text: string }[]; text?: { text: string }; type: string }[]
|
|
420
|
+
text: string
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Delivers an envelope through the `slack` transport and returns the parsed body. */
|
|
424
|
+
async function captureSlack(
|
|
425
|
+
envelope: WebhookDestination.deliver.Options['envelope'],
|
|
426
|
+
): Promise<SlackPayload> {
|
|
427
|
+
let body = '{}'
|
|
428
|
+
const fetch = (async (_input, init) => {
|
|
429
|
+
body = typeof init?.body === 'string' ? init.body : '{}'
|
|
430
|
+
return new Response(null, { status: 200 })
|
|
431
|
+
}) satisfies typeof globalThis.fetch
|
|
432
|
+
await WebhookDestination.slack('https://hooks.slack.com/services/T/B/x').deliver({
|
|
433
|
+
envelope,
|
|
434
|
+
fetch,
|
|
435
|
+
now: () => 0,
|
|
436
|
+
})
|
|
437
|
+
return JSON.parse(body) as SlackPayload
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** The header block's text. */
|
|
441
|
+
function header(payload: SlackPayload): string | undefined {
|
|
442
|
+
return payload.blocks.find((block) => block.type === 'header')?.text?.text
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** The mrkdwn text of the section block at `index` (header is block 0). */
|
|
446
|
+
function section(payload: SlackPayload, index: number): string {
|
|
447
|
+
return payload.blocks[index]?.text?.text ?? ''
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
describe('betterStack().deliver', () => {
|
|
451
|
+
test('POSTs a bearer-authed log event to the ingest URL', async () => {
|
|
452
|
+
let captured: { body: string; headers: Headers; url: string } | undefined
|
|
453
|
+
const fetch = (async (input, init) => {
|
|
454
|
+
captured = {
|
|
455
|
+
body: typeof init?.body === 'string' ? init.body : '',
|
|
456
|
+
headers: new Headers(init?.headers),
|
|
457
|
+
url: typeof input === 'string' ? input : '',
|
|
458
|
+
}
|
|
459
|
+
return new Response(null, { status: 202 })
|
|
460
|
+
}) satisfies typeof globalThis.fetch
|
|
461
|
+
|
|
462
|
+
const result = await WebhookDestination.betterStack({
|
|
463
|
+
token: 'src_token',
|
|
464
|
+
url: 'https://s1234567.eu-nbg-2.betterstackdata.com',
|
|
465
|
+
}).deliver({ envelope, fetch, now: () => 0 })
|
|
466
|
+
|
|
467
|
+
expect(result).toMatchInlineSnapshot(`
|
|
468
|
+
{
|
|
469
|
+
"durationMs": 0,
|
|
470
|
+
"ok": true,
|
|
471
|
+
"status": 202,
|
|
472
|
+
}
|
|
473
|
+
`)
|
|
474
|
+
expect(captured?.headers.get('authorization')).toBe('Bearer src_token')
|
|
475
|
+
expect(captured?.headers.get('tempo-signature')).toBeNull()
|
|
476
|
+
const payload = JSON.parse(captured?.body ?? '{}') as Record<string, unknown>
|
|
477
|
+
expect(payload['dt']).toBe('2026-01-01T00:00:00.000Z')
|
|
478
|
+
expect(payload['event']).toBe('token:transfer')
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
test('maps subscription context onto the log message + description', async () => {
|
|
482
|
+
let captured: { body: string } | undefined
|
|
483
|
+
const fetch = (async (_input, init) => {
|
|
484
|
+
captured = { body: typeof init?.body === 'string' ? init.body : '' }
|
|
485
|
+
return new Response(null, { status: 202 })
|
|
486
|
+
}) satisfies typeof globalThis.fetch
|
|
487
|
+
|
|
488
|
+
await WebhookDestination.betterStack({
|
|
489
|
+
token: 'src_token',
|
|
490
|
+
url: 'https://s1234567.eu-nbg-2.betterstackdata.com',
|
|
491
|
+
}).deliver({
|
|
492
|
+
envelope: {
|
|
493
|
+
...envelope,
|
|
494
|
+
context: {
|
|
495
|
+
description: 'Notify #ops on big settles.',
|
|
496
|
+
metadata: { env: 'prod', team: 'payments' },
|
|
497
|
+
title: 'Prod USDC transfers',
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
fetch,
|
|
501
|
+
now: () => 0,
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
const payload = JSON.parse(captured?.body ?? '{}') as Record<string, unknown>
|
|
505
|
+
expect(payload['message']).toBe('Prod USDC transfers')
|
|
506
|
+
expect(payload['description']).toBe('Notify #ops on big settles.')
|
|
507
|
+
expect(payload['metadata']).toEqual({ env: 'prod', team: 'payments' })
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
test('classifies a non-2xx Better Stack response as failure', async () => {
|
|
511
|
+
const fetch = (async () =>
|
|
512
|
+
new Response('Unauthorized', { status: 403 })) satisfies typeof globalThis.fetch
|
|
513
|
+
const result = await WebhookDestination.betterStack({
|
|
514
|
+
token: 'src_token',
|
|
515
|
+
url: 'https://s1234567.eu-nbg-2.betterstackdata.com',
|
|
516
|
+
}).deliver({ envelope, fetch, now: () => 0 })
|
|
517
|
+
|
|
518
|
+
expect(result).toMatchInlineSnapshot(`
|
|
519
|
+
{
|
|
520
|
+
"durationMs": 0,
|
|
521
|
+
"error": "betterstack non-2xx response (403)",
|
|
522
|
+
"ok": false,
|
|
523
|
+
"status": 403,
|
|
524
|
+
}
|
|
525
|
+
`)
|
|
526
|
+
})
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
describe('from(...).deliver', () => {
|
|
530
|
+
test('rebuilds a plain (persisted) destination and delivers via its own transport', async () => {
|
|
531
|
+
let capturedUrl: string | undefined
|
|
532
|
+
const fetch = (async (input) => {
|
|
533
|
+
capturedUrl = typeof input === 'string' ? input : ''
|
|
534
|
+
return new Response(null, { status: 200 })
|
|
535
|
+
}) satisfies typeof globalThis.fetch
|
|
536
|
+
|
|
537
|
+
// A plain destination as read back from the store — no bound `deliver`.
|
|
538
|
+
const destination: WebhookDestination.Destination = {
|
|
539
|
+
type: 'slack',
|
|
540
|
+
url: 'https://hooks.slack.com/services/T000/B000/xxxx',
|
|
541
|
+
}
|
|
542
|
+
const result = await WebhookDestination.from(destination).deliver({
|
|
543
|
+
envelope,
|
|
544
|
+
fetch,
|
|
545
|
+
now: () => 0,
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
expect(result.ok).toBe(true)
|
|
549
|
+
expect(capturedUrl).toBe('https://hooks.slack.com/services/T000/B000/xxxx')
|
|
550
|
+
})
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
describe('assertDeliverableUrl', () => {
|
|
554
|
+
test('accepts a plain https URL', () => {
|
|
555
|
+
expect(() =>
|
|
556
|
+
WebhookDestination.assertDeliverableUrl('https://hooks.example.com/x'),
|
|
557
|
+
).not.toThrow()
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
test.each([
|
|
561
|
+
['http (non-https)', 'http://hooks.example.com', 'protocol'],
|
|
562
|
+
['ftp scheme', 'ftp://hooks.example.com', 'protocol'],
|
|
563
|
+
['embedded credentials', 'https://user:pass@hooks.example.com', 'credentials'],
|
|
564
|
+
['localhost', 'https://localhost/x', 'blocked_host'],
|
|
565
|
+
['sub.localhost', 'https://api.localhost/x', 'blocked_host'],
|
|
566
|
+
['loopback v4', 'https://127.0.0.1/x', 'blocked_host'],
|
|
567
|
+
['private 10/8', 'https://10.1.2.3/x', 'blocked_host'],
|
|
568
|
+
['private 172.16/12', 'https://172.16.0.1/x', 'blocked_host'],
|
|
569
|
+
['private 192.168/16', 'https://192.168.0.1/x', 'blocked_host'],
|
|
570
|
+
['link-local / metadata', 'https://169.254.169.254/x', 'blocked_host'],
|
|
571
|
+
['cgnat', 'https://100.64.0.1/x', 'blocked_host'],
|
|
572
|
+
['gcp metadata host', 'https://metadata.google.internal/x', 'blocked_host'],
|
|
573
|
+
['ipv6 loopback', 'https://[::1]/x', 'blocked_host'],
|
|
574
|
+
['ipv6 link-local', 'https://[fe80::1]/x', 'blocked_host'],
|
|
575
|
+
['ipv6 unique-local', 'https://[fd00::1]/x', 'blocked_host'],
|
|
576
|
+
['ipv4-mapped loopback', 'https://[::ffff:127.0.0.1]/x', 'blocked_host'],
|
|
577
|
+
['malformed', 'not a url', 'malformed'],
|
|
578
|
+
])('rejects %s', (_label, url, reason) => {
|
|
579
|
+
let error: unknown
|
|
580
|
+
try {
|
|
581
|
+
WebhookDestination.assertDeliverableUrl(url)
|
|
582
|
+
} catch (caught) {
|
|
583
|
+
error = caught
|
|
584
|
+
}
|
|
585
|
+
expect(error).toBeInstanceOf(WebhookDestination.InvalidUrlError)
|
|
586
|
+
expect((error as WebhookDestination.InvalidUrlError).reason).toBe(reason)
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
test('accepts a public IPv4 literal', () => {
|
|
590
|
+
expect(() => WebhookDestination.assertDeliverableUrl('https://93.184.216.34/x')).not.toThrow()
|
|
591
|
+
})
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
describe('assertSlackUrl', () => {
|
|
595
|
+
test('accepts a hooks.slack.com https URL', () => {
|
|
596
|
+
expect(() =>
|
|
597
|
+
WebhookDestination.assertSlackUrl('https://hooks.slack.com/services/T000/B000/xxxx'),
|
|
598
|
+
).not.toThrow()
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
test('rejects a non-Slack host', () => {
|
|
602
|
+
expect(() =>
|
|
603
|
+
WebhookDestination.assertSlackUrl('https://evil.example.com/hook'),
|
|
604
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
605
|
+
`[Webhooks.InvalidUrlError: Webhook URL rejected (slack_host): https://evil.example.com/hook]`,
|
|
606
|
+
)
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
test('rejects a non-https URL', () => {
|
|
610
|
+
expect(() =>
|
|
611
|
+
WebhookDestination.assertSlackUrl('http://hooks.slack.com/services/x'),
|
|
612
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
613
|
+
`[Webhooks.InvalidUrlError: Webhook URL rejected (protocol): http://hooks.slack.com/services/x]`,
|
|
614
|
+
)
|
|
615
|
+
})
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
describe('redactSlackUrl', () => {
|
|
619
|
+
test('masks the secret path, keeping the origin', () => {
|
|
620
|
+
expect(
|
|
621
|
+
WebhookDestination.redactSlackUrl('https://hooks.slack.com/services/T000/B000/secrettoken'),
|
|
622
|
+
).toMatchInlineSnapshot(`"https://hooks.slack.com/…"`)
|
|
623
|
+
})
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
describe('assertBetterstackUrl', () => {
|
|
627
|
+
test('accepts a betterstackdata.com https URL', () => {
|
|
628
|
+
expect(() =>
|
|
629
|
+
WebhookDestination.assertBetterstackUrl('https://s1234567.eu-nbg-2.betterstackdata.com'),
|
|
630
|
+
).not.toThrow()
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
test('rejects a non-Better Stack host', () => {
|
|
634
|
+
expect(() =>
|
|
635
|
+
WebhookDestination.assertBetterstackUrl('https://evil.example.com/ingest'),
|
|
636
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
637
|
+
`[Webhooks.InvalidUrlError: Webhook URL rejected (betterstack_host): https://evil.example.com/ingest]`,
|
|
638
|
+
)
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
test('rejects a non-https URL', () => {
|
|
642
|
+
expect(() =>
|
|
643
|
+
WebhookDestination.assertBetterstackUrl('http://s1.betterstackdata.com'),
|
|
644
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
645
|
+
`[Webhooks.InvalidUrlError: Webhook URL rejected (protocol): http://s1.betterstackdata.com]`,
|
|
646
|
+
)
|
|
647
|
+
})
|
|
648
|
+
})
|