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,940 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
2
|
+
import { Hash, Hex } from 'ox'
|
|
3
|
+
|
|
4
|
+
import type * as app_Webhooks from '../apps/webhooks.js'
|
|
5
|
+
import type * as Metrics from '../Metrics.js'
|
|
6
|
+
import * as MetricSink from './MetricSink.js'
|
|
7
|
+
import * as Store from './Store.js'
|
|
8
|
+
import * as WebhookDestination from './WebhookDestination.js'
|
|
9
|
+
|
|
10
|
+
// The destination layer lives in `./WebhookDestination.js`. Re-export only the
|
|
11
|
+
// pieces that belong on the public `Webhooks.*` surface: the `Destination` and
|
|
12
|
+
// `Result` types, the `InvalidUrlError` thrown by subscription writes, and the
|
|
13
|
+
// signature-verification API a consumer uses to authenticate a delivery
|
|
14
|
+
// (`verify` + `signatureHeader`, with `sign` as its pair). The transport-specific
|
|
15
|
+
// validators/log helpers stay internal to `WebhookDestination`.
|
|
16
|
+
export type { Destination, Result } from './WebhookDestination.js'
|
|
17
|
+
export { InvalidUrlError, sign, signatureHeader, verify } from './WebhookDestination.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Event types a subscription can listen to. The canonical set lives in the apps
|
|
21
|
+
* layer ({@link app_Webhooks.eventTypes}), co-located with the per-type schemas
|
|
22
|
+
* and descriptions that reference apps-layer read schemas; this storage layer
|
|
23
|
+
* only consumes the type.
|
|
24
|
+
*/
|
|
25
|
+
export type EventType = app_Webhooks.EventType
|
|
26
|
+
|
|
27
|
+
/** Lifecycle status of a subscription. */
|
|
28
|
+
export type Status = 'active' | 'disabled' | 'paused'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Authenticated owner of a subscription. Derived from the request principal:
|
|
32
|
+
* API keys are org-scoped; MPP payers are identified by their stable DID.
|
|
33
|
+
*/
|
|
34
|
+
export type Owner =
|
|
35
|
+
| {
|
|
36
|
+
/** Owning organization id (any key in the org manages its webhooks). */
|
|
37
|
+
orgId: string
|
|
38
|
+
/** Owner kind. */
|
|
39
|
+
type: 'api_key'
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
/** Verified MPP payer identifier (e.g. `did:pkh:…`). */
|
|
43
|
+
payer: string
|
|
44
|
+
/** Owner kind. */
|
|
45
|
+
type: 'mpp'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Optional human context describing what a subscription is for. Carried onto
|
|
50
|
+
* every delivered {@link Envelope} and surfaced by destinations (Slack header +
|
|
51
|
+
* description, Better Stack log fields, and the raw `url` payload).
|
|
52
|
+
*/
|
|
53
|
+
export type Context = {
|
|
54
|
+
/** Longer description of the subscription's purpose. */
|
|
55
|
+
description?: string | undefined
|
|
56
|
+
/** Arbitrary key/value labels echoed on every delivery and rendered by destinations. */
|
|
57
|
+
metadata?: Record<string, string> | undefined
|
|
58
|
+
/** Short label for the subscription (e.g. `Prod USDC large transfers`). */
|
|
59
|
+
title?: string | undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A durable webhook subscription record. */
|
|
63
|
+
export type Subscription = {
|
|
64
|
+
/** Chain the subscription listens on. */
|
|
65
|
+
chainId: number
|
|
66
|
+
/** Optional human context describing what the subscription is for. */
|
|
67
|
+
context?: Context | undefined
|
|
68
|
+
/** ISO timestamp of creation. */
|
|
69
|
+
createdAt: string
|
|
70
|
+
/** Where matched events are delivered (URL or Slack channel). */
|
|
71
|
+
destination: WebhookDestination.Destination
|
|
72
|
+
/** Event type the subscription listens to. */
|
|
73
|
+
eventType: EventType
|
|
74
|
+
/** ISO expiry timestamp; set for MPP-owned subscriptions (TTL-bounded). */
|
|
75
|
+
expiresAt?: string | undefined
|
|
76
|
+
/** Consecutive delivery failures; drives auto-disable. */
|
|
77
|
+
failureCount: number
|
|
78
|
+
/** Event-type-specific filter predicates. */
|
|
79
|
+
filters: Record<string, unknown>
|
|
80
|
+
/** Subscription id (`wh_…`). */
|
|
81
|
+
id: string
|
|
82
|
+
/** ISO timestamp of the last successful delivery, if any. */
|
|
83
|
+
lastDeliveryAt?: string | undefined
|
|
84
|
+
/** Authenticated owner. */
|
|
85
|
+
owner: Owner
|
|
86
|
+
/** HMAC signing secret. Returned once at creation; never exposed by reads. */
|
|
87
|
+
secret: string
|
|
88
|
+
/** Lifecycle status. */
|
|
89
|
+
status: Status
|
|
90
|
+
/** ISO timestamp of the last mutation. */
|
|
91
|
+
updatedAt: string
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Fields accepted when creating a subscription. */
|
|
95
|
+
export type CreateInput = {
|
|
96
|
+
/** Chain to listen on. */
|
|
97
|
+
chainId: number
|
|
98
|
+
/** Optional human context describing what the subscription is for. */
|
|
99
|
+
context?: Context | undefined
|
|
100
|
+
/** Where matched events are delivered. `url` destinations are SSRF-validated. */
|
|
101
|
+
destination: WebhookDestination.Destination
|
|
102
|
+
/** Event type to listen to. */
|
|
103
|
+
eventType: EventType
|
|
104
|
+
/** Event-type-specific filter predicates. */
|
|
105
|
+
filters?: Record<string, unknown> | undefined
|
|
106
|
+
/** Owner derived from the request principal. */
|
|
107
|
+
owner: Owner
|
|
108
|
+
/** Time-to-live in milliseconds; set the subscription to expire (MPP TTL). */
|
|
109
|
+
ttl?: number | undefined
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Mutable fields accepted when patching a subscription. */
|
|
113
|
+
export type PatchInput = {
|
|
114
|
+
/** New human context, or `null` to clear it. */
|
|
115
|
+
context?: Context | null | undefined
|
|
116
|
+
/** New delivery destination. `url` destinations are re-validated for SSRF. */
|
|
117
|
+
destination?: WebhookDestination.Destination | undefined
|
|
118
|
+
/** New filter predicates. */
|
|
119
|
+
filters?: Record<string, unknown> | undefined
|
|
120
|
+
/** New lifecycle status (`active`/`paused`/`disabled`). */
|
|
121
|
+
status?: Status | undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Outcome status of a delivery-log row. */
|
|
125
|
+
export type DeliveryStatus = 'failed' | 'pending' | 'succeeded'
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A durable delivery-log row recording one delivery attempt. Persisted by
|
|
129
|
+
* {@link deliverAndRecord} so deliveries are observable and replayable (Phase
|
|
130
|
+
* C); the read endpoint returns these verbatim.
|
|
131
|
+
*/
|
|
132
|
+
export type Delivery = {
|
|
133
|
+
/** Consecutive attempt number (1-based) at the time of this delivery. */
|
|
134
|
+
attempt: number
|
|
135
|
+
/** ISO timestamp the attempt was recorded. */
|
|
136
|
+
createdAt: string
|
|
137
|
+
/**
|
|
138
|
+
* The exact envelope that was sent, persisted so the delivery can be replayed
|
|
139
|
+
* verbatim (manual retry). Internal-only: the public delivery row omits it.
|
|
140
|
+
*/
|
|
141
|
+
envelope: Envelope
|
|
142
|
+
/** Failure reason when `status` is `failed`. */
|
|
143
|
+
error?: string | undefined
|
|
144
|
+
/** Idempotent event id this delivery carried (`evt_…`). */
|
|
145
|
+
eventId: string
|
|
146
|
+
/** Delivery id (`whd_…`); time-sortable so keys order chronologically. */
|
|
147
|
+
id: string
|
|
148
|
+
/** Subscriber callback URL the attempt targeted. */
|
|
149
|
+
requestUrl: string
|
|
150
|
+
/** Wall-clock duration of the attempt in ms, when a request was made. */
|
|
151
|
+
responseMs?: number | undefined
|
|
152
|
+
/** HTTP response status, when a response was received. */
|
|
153
|
+
responseStatus?: number | undefined
|
|
154
|
+
/** Outcome status. */
|
|
155
|
+
status: DeliveryStatus
|
|
156
|
+
/** Owning subscription id. */
|
|
157
|
+
subscriptionId: string
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Creates a subscription, validating its URL and (optionally) enforcing a
|
|
162
|
+
* per-owner cap, then persists it and indexes it for the poller.
|
|
163
|
+
*/
|
|
164
|
+
export async function createSubscription(
|
|
165
|
+
store: Store.State,
|
|
166
|
+
input: CreateInput,
|
|
167
|
+
options: createSubscription.Options = {},
|
|
168
|
+
): Promise<Subscription> {
|
|
169
|
+
WebhookDestination.assertDestination(input.destination)
|
|
170
|
+
|
|
171
|
+
if (options.maxPerOwner !== undefined) {
|
|
172
|
+
const existing = await listSubscriptions(store, input.owner)
|
|
173
|
+
if (existing.length >= options.maxPerOwner) throw new LimitExceededError(options.maxPerOwner)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const now = (options.now ?? (() => new Date()))()
|
|
177
|
+
const iso = now.toISOString()
|
|
178
|
+
const subscription: Subscription = {
|
|
179
|
+
chainId: input.chainId,
|
|
180
|
+
...(input.context === undefined ? {} : { context: input.context }),
|
|
181
|
+
createdAt: iso,
|
|
182
|
+
destination: input.destination,
|
|
183
|
+
eventType: input.eventType,
|
|
184
|
+
failureCount: 0,
|
|
185
|
+
filters: input.filters ?? {},
|
|
186
|
+
id: generateId(now),
|
|
187
|
+
owner: input.owner,
|
|
188
|
+
secret: generateSecret(),
|
|
189
|
+
status: 'active',
|
|
190
|
+
updatedAt: iso,
|
|
191
|
+
...(input.ttl === undefined
|
|
192
|
+
? {}
|
|
193
|
+
: { expiresAt: new Date(now.getTime() + input.ttl).toISOString() }),
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await writeSubscription(store, subscription)
|
|
197
|
+
return subscription
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export declare namespace createSubscription {
|
|
201
|
+
/** Options for {@link createSubscription}. */
|
|
202
|
+
type Options = {
|
|
203
|
+
/** Reject creation once the owner has this many subscriptions. */
|
|
204
|
+
maxPerOwner?: number | undefined
|
|
205
|
+
/** Clock used for timestamps (injectable for tests). */
|
|
206
|
+
now?: (() => Date) | undefined
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Reads a single subscription scoped to its owner. Returns null when absent. */
|
|
211
|
+
export async function getSubscription(
|
|
212
|
+
store: Store.State,
|
|
213
|
+
owner: Owner,
|
|
214
|
+
id: string,
|
|
215
|
+
): Promise<Subscription | null> {
|
|
216
|
+
return readSubscription(store, subscriptionKey(owner, id))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Lists an owner's subscriptions, newest first, with optional keyset paging.
|
|
221
|
+
* Subscription ids embed a timestamp, so lexical key order is chronological.
|
|
222
|
+
*/
|
|
223
|
+
export async function listSubscriptions(
|
|
224
|
+
store: Store.State,
|
|
225
|
+
owner: Owner,
|
|
226
|
+
options: listSubscriptions.Options = {},
|
|
227
|
+
): Promise<Subscription[]> {
|
|
228
|
+
const prefix = subscriptionPrefix(owner)
|
|
229
|
+
const { keys } = await store.list({ prefix })
|
|
230
|
+
|
|
231
|
+
// Newest first: ids sort chronologically ascending, so reverse for descending.
|
|
232
|
+
let names = keys
|
|
233
|
+
.map((key) => key.name)
|
|
234
|
+
.sort()
|
|
235
|
+
.reverse()
|
|
236
|
+
if (options.cursor) names = names.filter((name) => name < `${prefix}${options.cursor}`)
|
|
237
|
+
if (options.offset) names = names.slice(options.offset)
|
|
238
|
+
if (options.limit !== undefined) names = names.slice(0, options.limit)
|
|
239
|
+
|
|
240
|
+
// Hydrate the page in parallel: the reads are independent, and a serial
|
|
241
|
+
// loop costs one store round trip per row (an N+1 against the production
|
|
242
|
+
// Durable Object).
|
|
243
|
+
const subscriptions = await Promise.all(names.map((name) => readSubscription(store, name)))
|
|
244
|
+
return subscriptions.filter((subscription) => subscription !== null)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export declare namespace listSubscriptions {
|
|
248
|
+
/** Options for {@link listSubscriptions}. */
|
|
249
|
+
type Options = {
|
|
250
|
+
/** Return subscriptions older than this id (keyset paging, newest first). */
|
|
251
|
+
cursor?: string | undefined
|
|
252
|
+
/** Maximum subscriptions to return. */
|
|
253
|
+
limit?: number | undefined
|
|
254
|
+
/** Rows to skip from the head (positional pagination; exclusive with `cursor`). */
|
|
255
|
+
offset?: number | undefined
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Counts an owner's subscriptions. Reads only the key set under the owner
|
|
261
|
+
* prefix (no row hydration), so it's a single store `list` — exact and cheap,
|
|
262
|
+
* since the per-owner set is bounded by `maxPerOwner`. Feeds the opt-in
|
|
263
|
+
* `meta.totalCount` on `GET /webhooks`.
|
|
264
|
+
*/
|
|
265
|
+
export async function countSubscriptions(store: Store.State, owner: Owner): Promise<number> {
|
|
266
|
+
const { keys } = await store.list({ prefix: subscriptionPrefix(owner) })
|
|
267
|
+
return keys.length
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Patches an owner's subscription, re-validating the URL when changed and
|
|
272
|
+
* re-indexing for the poller when the status changes. Returns null when absent.
|
|
273
|
+
*/
|
|
274
|
+
export async function updateSubscription(
|
|
275
|
+
store: Store.State,
|
|
276
|
+
owner: Owner,
|
|
277
|
+
id: string,
|
|
278
|
+
patch: PatchInput,
|
|
279
|
+
options: updateSubscription.Options = {},
|
|
280
|
+
): Promise<Subscription | null> {
|
|
281
|
+
const current = await getSubscription(store, owner, id)
|
|
282
|
+
if (!current) return null
|
|
283
|
+
|
|
284
|
+
if (patch.destination !== undefined) WebhookDestination.assertDestination(patch.destination)
|
|
285
|
+
|
|
286
|
+
const now = (options.now ?? (() => new Date()))()
|
|
287
|
+
const next: Subscription = {
|
|
288
|
+
...current,
|
|
289
|
+
// `context: null` clears it (stored as undefined; drops from JSON + reads).
|
|
290
|
+
...(patch.context === undefined
|
|
291
|
+
? {}
|
|
292
|
+
: { context: patch.context === null ? undefined : patch.context }),
|
|
293
|
+
...(patch.destination === undefined ? {} : { destination: patch.destination }),
|
|
294
|
+
...(patch.filters === undefined ? {} : { filters: patch.filters }),
|
|
295
|
+
...(patch.status === undefined ? {} : { status: patch.status }),
|
|
296
|
+
updatedAt: now.toISOString(),
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
await writeSubscription(store, next)
|
|
300
|
+
return next
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export declare namespace updateSubscription {
|
|
304
|
+
/** Options for {@link updateSubscription}. */
|
|
305
|
+
type Options = {
|
|
306
|
+
/** Clock used for timestamps (injectable for tests). */
|
|
307
|
+
now?: (() => Date) | undefined
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Deletes an owner's subscription and its index/cursor keys. */
|
|
312
|
+
export async function deleteSubscription(
|
|
313
|
+
store: Store.State,
|
|
314
|
+
owner: Owner,
|
|
315
|
+
id: string,
|
|
316
|
+
): Promise<boolean> {
|
|
317
|
+
const current = await getSubscription(store, owner, id)
|
|
318
|
+
if (!current) return false
|
|
319
|
+
|
|
320
|
+
await store.delete(subscriptionKey(owner, id))
|
|
321
|
+
await store.delete(activeKey(current.chainId, current.eventType, id))
|
|
322
|
+
await store.delete(cursorKey(id))
|
|
323
|
+
|
|
324
|
+
// Drop the delivery log so a deleted subscription leaves no orphaned rows.
|
|
325
|
+
const { keys } = await store.list({ prefix: deliveryPrefix(id) })
|
|
326
|
+
for (const { name } of keys) await store.delete(name)
|
|
327
|
+
return true
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Lists active subscriptions for a `(chainId, eventType)` stream. Used by the
|
|
332
|
+
* poller; reads the secondary index, then hydrates each subscription.
|
|
333
|
+
*/
|
|
334
|
+
export async function listActive(
|
|
335
|
+
store: Store.State,
|
|
336
|
+
chainId: number,
|
|
337
|
+
eventType: EventType,
|
|
338
|
+
): Promise<Subscription[]> {
|
|
339
|
+
const { keys } = await store.list({ prefix: activePrefix(chainId, eventType) })
|
|
340
|
+
|
|
341
|
+
const subscriptions: Subscription[] = []
|
|
342
|
+
for (const { name } of keys) {
|
|
343
|
+
const subKey = await store.get(name)
|
|
344
|
+
if (!subKey) continue
|
|
345
|
+
const subscription = await readSubscription(store, subKey)
|
|
346
|
+
if (subscription) subscriptions.push(subscription)
|
|
347
|
+
}
|
|
348
|
+
return subscriptions
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Reads a subscription's keyset cursor. Returns null when unset. */
|
|
352
|
+
export async function getCursor(
|
|
353
|
+
store: Store.State,
|
|
354
|
+
subscriptionId: string,
|
|
355
|
+
): Promise<string | null> {
|
|
356
|
+
return store.get(cursorKey(subscriptionId))
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Writes a subscription's keyset cursor. */
|
|
360
|
+
export async function setCursor(
|
|
361
|
+
store: Store.State,
|
|
362
|
+
subscriptionId: string,
|
|
363
|
+
cursor: string,
|
|
364
|
+
): Promise<void> {
|
|
365
|
+
await store.put(cursorKey(subscriptionId), cursor)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Default delivery-log retention. Rows are written with this TTL so a busy
|
|
370
|
+
* subscription's log doesn't grow unbounded; tune per deployment.
|
|
371
|
+
*/
|
|
372
|
+
export const deliveryRetentionMs = 7 * 24 * 60 * 60 * 1_000
|
|
373
|
+
|
|
374
|
+
/** Records one delivery-log row (stateful, owner-agnostic; scoped per subscription). */
|
|
375
|
+
export async function recordDelivery(
|
|
376
|
+
store: Store.State,
|
|
377
|
+
delivery: Delivery,
|
|
378
|
+
options: recordDelivery.Options = {},
|
|
379
|
+
): Promise<void> {
|
|
380
|
+
const ttl = options.ttl ?? deliveryRetentionMs
|
|
381
|
+
await store.put(deliveryKey(delivery.subscriptionId, delivery.id), JSON.stringify(delivery), {
|
|
382
|
+
ttl,
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export declare namespace recordDelivery {
|
|
387
|
+
/** Options for {@link recordDelivery}. */
|
|
388
|
+
type Options = {
|
|
389
|
+
/** Row time-to-live in ms; defaults to {@link deliveryRetentionMs}. */
|
|
390
|
+
ttl?: number | undefined
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Lists a subscription's delivery log, newest first, with optional keyset
|
|
396
|
+
* paging. Delivery ids embed a timestamp, so lexical key order is chronological.
|
|
397
|
+
*/
|
|
398
|
+
export async function listDeliveries(
|
|
399
|
+
store: Store.State,
|
|
400
|
+
subscriptionId: string,
|
|
401
|
+
options: listDeliveries.Options = {},
|
|
402
|
+
): Promise<Delivery[]> {
|
|
403
|
+
const prefix = deliveryPrefix(subscriptionId)
|
|
404
|
+
const { keys } = await store.list({ prefix })
|
|
405
|
+
|
|
406
|
+
// Newest first: ids sort chronologically ascending, so reverse for descending.
|
|
407
|
+
let names = keys
|
|
408
|
+
.map((key) => key.name)
|
|
409
|
+
.sort()
|
|
410
|
+
.reverse()
|
|
411
|
+
if (options.cursor) names = names.filter((name) => name < `${prefix}${options.cursor}`)
|
|
412
|
+
if (options.offset) names = names.slice(options.offset)
|
|
413
|
+
if (options.limit !== undefined) names = names.slice(0, options.limit)
|
|
414
|
+
|
|
415
|
+
// Hydrate the page in parallel: the reads are independent, and a serial
|
|
416
|
+
// loop costs one store round trip per row (an N+1 against the production
|
|
417
|
+
// Durable Object).
|
|
418
|
+
const deliveries = await Promise.all(names.map((name) => readDelivery(store, name)))
|
|
419
|
+
return deliveries.filter((delivery) => delivery !== null)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export declare namespace listDeliveries {
|
|
423
|
+
/** Options for {@link listDeliveries}. */
|
|
424
|
+
type Options = {
|
|
425
|
+
/** Return deliveries whose id sorts before this one (older entries). */
|
|
426
|
+
cursor?: string | undefined
|
|
427
|
+
/** Maximum deliveries to return. */
|
|
428
|
+
limit?: number | undefined
|
|
429
|
+
/** Rows to skip from the head (positional pagination; exclusive with `cursor`). */
|
|
430
|
+
offset?: number | undefined
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Counts a subscription's delivery-log entries. Reads only the key set under
|
|
436
|
+
* the delivery prefix (no row hydration), so it's a single store `list` —
|
|
437
|
+
* exact, and bounded by the delivery retention TTL. Feeds the opt-in
|
|
438
|
+
* `meta.totalCount` on `GET /webhooks/:id/deliveries`.
|
|
439
|
+
*/
|
|
440
|
+
export async function countDeliveries(store: Store.State, subscriptionId: string): Promise<number> {
|
|
441
|
+
const { keys } = await store.list({ prefix: deliveryPrefix(subscriptionId) })
|
|
442
|
+
return keys.length
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Loads a single delivery-log row (`null` when absent), scoped to its subscription. */
|
|
446
|
+
export async function getDelivery(
|
|
447
|
+
store: Store.State,
|
|
448
|
+
subscriptionId: string,
|
|
449
|
+
deliveryId: string,
|
|
450
|
+
): Promise<Delivery | null> {
|
|
451
|
+
return readDelivery(store, deliveryKey(subscriptionId, deliveryId))
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Default number of consecutive delivery failures after which a subscription
|
|
456
|
+
* auto-disables (and is dropped from the poller's active index).
|
|
457
|
+
*/
|
|
458
|
+
export const maxFailures = 10
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Records a successful delivery: resets the consecutive-failure counter and
|
|
462
|
+
* stamps `lastDeliveryAt`. Returns the updated subscription (re-persisted so the
|
|
463
|
+
* poller's active index and TTL stay correct).
|
|
464
|
+
*/
|
|
465
|
+
export async function recordSuccess(
|
|
466
|
+
store: Store.State,
|
|
467
|
+
subscription: Subscription,
|
|
468
|
+
options: recordSuccess.Options = {},
|
|
469
|
+
): Promise<Subscription> {
|
|
470
|
+
const now = (options.now ?? (() => new Date()))()
|
|
471
|
+
const next: Subscription = {
|
|
472
|
+
...subscription,
|
|
473
|
+
failureCount: 0,
|
|
474
|
+
lastDeliveryAt: now.toISOString(),
|
|
475
|
+
updatedAt: now.toISOString(),
|
|
476
|
+
}
|
|
477
|
+
await writeSubscription(store, next)
|
|
478
|
+
return next
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export declare namespace recordSuccess {
|
|
482
|
+
/** Options for {@link recordSuccess}. */
|
|
483
|
+
type Options = {
|
|
484
|
+
/** Clock used for timestamps; injectable for tests. */
|
|
485
|
+
now?: (() => Date) | undefined
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Records a failed delivery: increments the consecutive-failure counter and,
|
|
491
|
+
* once it reaches `maxFailures`, sets `status: 'disabled'` so the subscription
|
|
492
|
+
* drops out of the poller's active index (surfaced via the API). Returns the
|
|
493
|
+
* updated subscription.
|
|
494
|
+
*/
|
|
495
|
+
export async function recordFailure(
|
|
496
|
+
store: Store.State,
|
|
497
|
+
subscription: Subscription,
|
|
498
|
+
options: recordFailure.Options = {},
|
|
499
|
+
): Promise<Subscription> {
|
|
500
|
+
const now = (options.now ?? (() => new Date()))()
|
|
501
|
+
const failureCount = subscription.failureCount + 1
|
|
502
|
+
const disabled = failureCount >= (options.maxFailures ?? maxFailures)
|
|
503
|
+
const next: Subscription = {
|
|
504
|
+
...subscription,
|
|
505
|
+
failureCount,
|
|
506
|
+
...(disabled ? { status: 'disabled' } : {}),
|
|
507
|
+
updatedAt: now.toISOString(),
|
|
508
|
+
}
|
|
509
|
+
await writeSubscription(store, next)
|
|
510
|
+
return next
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export declare namespace recordFailure {
|
|
514
|
+
/** Options for {@link recordFailure}. */
|
|
515
|
+
type Options = {
|
|
516
|
+
/** Consecutive failures before auto-disabling (default {@link maxFailures}). */
|
|
517
|
+
maxFailures?: number | undefined
|
|
518
|
+
/** Clock used for timestamps; injectable for tests. */
|
|
519
|
+
now?: (() => Date) | undefined
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Delivers an envelope to its subscription and records the outcome: success
|
|
525
|
+
* resets the failure counter and stamps `lastDeliveryAt`; failure increments it
|
|
526
|
+
* and auto-disables after {@link maxFailures}. This is the single delivery entry
|
|
527
|
+
* point shared by the Cloudflare Queue consumer and the self-host inline path.
|
|
528
|
+
*/
|
|
529
|
+
export async function deliverAndRecord(
|
|
530
|
+
store: Store.State,
|
|
531
|
+
subscription: Subscription,
|
|
532
|
+
envelope: Envelope,
|
|
533
|
+
options: deliverAndRecord.Options = {},
|
|
534
|
+
): Promise<WebhookDestination.Result> {
|
|
535
|
+
// Snapshot the attempt before recordSuccess/recordFailure mutate the counter.
|
|
536
|
+
const attempt = subscription.failureCount + 1
|
|
537
|
+
const result = await WebhookDestination.from(subscription.destination).deliver({
|
|
538
|
+
envelope,
|
|
539
|
+
secret: subscription.secret,
|
|
540
|
+
...(options.fetch === undefined ? {} : { fetch: options.fetch }),
|
|
541
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
542
|
+
})
|
|
543
|
+
if (result.ok) await recordSuccess(store, subscription, options)
|
|
544
|
+
else await recordFailure(store, subscription, options)
|
|
545
|
+
|
|
546
|
+
// Emit a `webhook:delivery` metric. recordSuccess/recordFailure are immutable
|
|
547
|
+
// (they spread into a new record), so `subscription.failureCount` here is still
|
|
548
|
+
// the pre-mutation snapshot the sink needs to derive the attempt bucket.
|
|
549
|
+
// Best-effort, like delivery logging.
|
|
550
|
+
const metrics = options.metrics ? MetricSink.webhooks(options.metrics) : undefined
|
|
551
|
+
if (metrics) {
|
|
552
|
+
metrics.record({
|
|
553
|
+
envelope,
|
|
554
|
+
result,
|
|
555
|
+
subscription,
|
|
556
|
+
trigger: options.trigger ?? 'queue',
|
|
557
|
+
type: 'webhook:delivery',
|
|
558
|
+
})
|
|
559
|
+
metrics.flush()
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Append a delivery-log row. Best-effort: a logging failure must never change
|
|
563
|
+
// the delivery outcome the caller (Queue consumer / inline path) acts on.
|
|
564
|
+
try {
|
|
565
|
+
await recordDelivery(
|
|
566
|
+
store,
|
|
567
|
+
buildDelivery({ attempt, envelope, result, subscription }, options),
|
|
568
|
+
options.deliveryRetentionMs === undefined ? {} : { ttl: options.deliveryRetentionMs },
|
|
569
|
+
)
|
|
570
|
+
} catch {
|
|
571
|
+
// delivery logging is best-effort
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return result
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export declare namespace deliverAndRecord {
|
|
578
|
+
/** Options for {@link deliverAndRecord}. */
|
|
579
|
+
type Options = {
|
|
580
|
+
/** Delivery-log row TTL in ms; defaults to {@link deliveryRetentionMs}. */
|
|
581
|
+
deliveryRetentionMs?: number | undefined
|
|
582
|
+
/** `fetch` implementation passed through to {@link deliver}. */
|
|
583
|
+
fetch?: typeof globalThis.fetch | undefined
|
|
584
|
+
/** Consecutive failures before auto-disabling (default {@link maxFailures}). */
|
|
585
|
+
maxFailures?: number | undefined
|
|
586
|
+
/** Metrics backend; emits one `webhook:delivery` event per attempt. */
|
|
587
|
+
metrics?: Metrics.Metrics | undefined
|
|
588
|
+
/** Clock used for timestamps; injectable for tests. */
|
|
589
|
+
now?: (() => Date) | undefined
|
|
590
|
+
/** Delivery timeout in ms passed through to {@link deliver}. */
|
|
591
|
+
timeoutMs?: number | undefined
|
|
592
|
+
/** Where this attempt originated; tags the `webhook:delivery` event (default `queue`). */
|
|
593
|
+
trigger?: MetricSink.Trigger | undefined
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Builds a delivery-log row from an attempt's envelope and classified result. */
|
|
598
|
+
function buildDelivery(
|
|
599
|
+
input: {
|
|
600
|
+
attempt: number
|
|
601
|
+
envelope: Envelope
|
|
602
|
+
result: WebhookDestination.Result
|
|
603
|
+
subscription: Subscription
|
|
604
|
+
},
|
|
605
|
+
options: { now?: (() => Date) | undefined } = {},
|
|
606
|
+
): Delivery {
|
|
607
|
+
const { attempt, envelope, result, subscription } = input
|
|
608
|
+
const now = (options.now ?? (() => new Date()))()
|
|
609
|
+
return {
|
|
610
|
+
attempt,
|
|
611
|
+
createdAt: now.toISOString(),
|
|
612
|
+
envelope,
|
|
613
|
+
eventId: envelope.id,
|
|
614
|
+
id: generateDeliveryId(now),
|
|
615
|
+
requestUrl: WebhookDestination.destinationLabel(subscription.destination),
|
|
616
|
+
status: result.ok ? 'succeeded' : 'failed',
|
|
617
|
+
subscriptionId: subscription.id,
|
|
618
|
+
...(result.error === undefined ? {} : { error: result.error }),
|
|
619
|
+
...(result.durationMs === undefined ? {} : { responseMs: result.durationMs }),
|
|
620
|
+
...(result.status === undefined ? {} : { responseStatus: result.status }),
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Computes a stable, idempotent event id from its on-chain coordinates so
|
|
626
|
+
* re-deliveries are dedupable by the receiver.
|
|
627
|
+
*/
|
|
628
|
+
export function eventId(options: eventId.Options): string {
|
|
629
|
+
const input = `${options.chainId}:${options.eventType}:${options.blockNumber}:${options.logIndex}`
|
|
630
|
+
return `evt_${Hash.sha256(Hex.fromString(input)).slice(2)}`
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export declare namespace eventId {
|
|
634
|
+
/** Options for {@link eventId}. */
|
|
635
|
+
type Options = {
|
|
636
|
+
/** Block number the event was indexed in. */
|
|
637
|
+
blockNumber: bigint | number | string
|
|
638
|
+
/** Chain the event occurred on. */
|
|
639
|
+
chainId: number
|
|
640
|
+
/** Event type. */
|
|
641
|
+
eventType: EventType
|
|
642
|
+
/** Log index within the block (or transaction index for transaction events). */
|
|
643
|
+
logIndex: bigint | number | string
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** The JSON envelope delivered to subscribers. */
|
|
648
|
+
export type Envelope = {
|
|
649
|
+
/** Chain the event occurred on. */
|
|
650
|
+
chainId: number
|
|
651
|
+
/** Human context copied from the subscription, when set. */
|
|
652
|
+
context?: Context | undefined
|
|
653
|
+
/** ISO timestamp the envelope was built. */
|
|
654
|
+
createdAt: string
|
|
655
|
+
/** The same row shape the corresponding read endpoint returns. */
|
|
656
|
+
data: unknown
|
|
657
|
+
/** Stable, idempotent event id (`evt_…`). */
|
|
658
|
+
id: string
|
|
659
|
+
/** Originating subscription id. */
|
|
660
|
+
subscriptionId: string
|
|
661
|
+
/** Event type; `ping` marks a synthetic test delivery (see {@link buildPingEnvelope}). */
|
|
662
|
+
type: EventType | 'ping'
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Builds a delivery envelope for an event matched against a subscription. */
|
|
666
|
+
export function buildEnvelope(options: buildEnvelope.Options): Envelope {
|
|
667
|
+
const { subscription } = options
|
|
668
|
+
return {
|
|
669
|
+
chainId: subscription.chainId,
|
|
670
|
+
...(subscription.context === undefined ? {} : { context: subscription.context }),
|
|
671
|
+
createdAt: (options.createdAt ?? new Date()).toISOString(),
|
|
672
|
+
data: options.data,
|
|
673
|
+
id: eventId({
|
|
674
|
+
blockNumber: options.blockNumber,
|
|
675
|
+
chainId: subscription.chainId,
|
|
676
|
+
eventType: subscription.eventType,
|
|
677
|
+
logIndex: options.logIndex,
|
|
678
|
+
}),
|
|
679
|
+
subscriptionId: subscription.id,
|
|
680
|
+
type: subscription.eventType,
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export declare namespace buildEnvelope {
|
|
685
|
+
/** Options for {@link buildEnvelope}. */
|
|
686
|
+
type Options = {
|
|
687
|
+
/** Block number the event was indexed in. */
|
|
688
|
+
blockNumber: bigint | number | string
|
|
689
|
+
/** Timestamp to stamp on the envelope; defaults to now. */
|
|
690
|
+
createdAt?: Date | undefined
|
|
691
|
+
/** Decoded event row (same shape the read endpoint returns). */
|
|
692
|
+
data: unknown
|
|
693
|
+
/** Log index within the block. */
|
|
694
|
+
logIndex: bigint | number | string
|
|
695
|
+
/** Subscription the event matched. */
|
|
696
|
+
subscription: Subscription
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Builds a synthetic `ping` envelope so an owner can test a subscription's
|
|
702
|
+
* endpoint (connectivity, TLS, signature verification) without waiting for a real
|
|
703
|
+
* on-chain event. The id is randomized per call so repeated pings are never
|
|
704
|
+
* deduped by the receiver; `type` is `ping` and `data` is a recognizable
|
|
705
|
+
* `{ ping: true }` marker rather than an event row, so receivers can tell a test
|
|
706
|
+
* delivery apart from a real event.
|
|
707
|
+
*/
|
|
708
|
+
export function buildPingEnvelope(
|
|
709
|
+
subscription: Subscription,
|
|
710
|
+
options: buildPingEnvelope.Options = {},
|
|
711
|
+
): Envelope {
|
|
712
|
+
const now = options.createdAt ?? new Date()
|
|
713
|
+
const nonce = options.nonce ?? Hex.random(16).slice(2)
|
|
714
|
+
return {
|
|
715
|
+
chainId: subscription.chainId,
|
|
716
|
+
...(subscription.context === undefined ? {} : { context: subscription.context }),
|
|
717
|
+
createdAt: now.toISOString(),
|
|
718
|
+
data: { ping: true },
|
|
719
|
+
id: `evt_${Hash.sha256(Hex.fromString(`ping:${subscription.id}:${nonce}`)).slice(2)}`,
|
|
720
|
+
subscriptionId: subscription.id,
|
|
721
|
+
type: 'ping',
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export declare namespace buildPingEnvelope {
|
|
726
|
+
/** Options for {@link buildPingEnvelope}. */
|
|
727
|
+
type Options = {
|
|
728
|
+
/** Timestamp to stamp on the envelope; defaults to now. */
|
|
729
|
+
createdAt?: Date | undefined
|
|
730
|
+
/** Deterministic nonce for the event id; defaults to random (tests only). */
|
|
731
|
+
nonce?: string | undefined
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Delivers a synthetic {@link buildPingEnvelope} to a subscription's endpoint and
|
|
737
|
+
* logs the attempt. Unlike {@link deliverAndRecord} it deliberately does NOT
|
|
738
|
+
* touch the subscription's `failureCount`/`status`: a test ping must never
|
|
739
|
+
* auto-disable a healthy subscription or reset a real failure streak. The
|
|
740
|
+
* delivery row is still appended (best-effort) so the ping is observable via the
|
|
741
|
+
* delivery log.
|
|
742
|
+
*/
|
|
743
|
+
export async function ping(
|
|
744
|
+
store: Store.State,
|
|
745
|
+
subscription: Subscription,
|
|
746
|
+
options: ping.Options = {},
|
|
747
|
+
): Promise<ping.Result> {
|
|
748
|
+
const now = options.now ?? (() => new Date())
|
|
749
|
+
const envelope = buildPingEnvelope(subscription, {
|
|
750
|
+
createdAt: now(),
|
|
751
|
+
...(options.nonce === undefined ? {} : { nonce: options.nonce }),
|
|
752
|
+
})
|
|
753
|
+
const result = await WebhookDestination.from(subscription.destination).deliver({
|
|
754
|
+
envelope,
|
|
755
|
+
secret: subscription.secret,
|
|
756
|
+
...(options.fetch === undefined ? {} : { fetch: options.fetch }),
|
|
757
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
// Best-effort: a logging failure must never change the result the caller sees.
|
|
761
|
+
const delivery = buildDelivery({ attempt: 1, envelope, result, subscription }, { now })
|
|
762
|
+
try {
|
|
763
|
+
await recordDelivery(
|
|
764
|
+
store,
|
|
765
|
+
delivery,
|
|
766
|
+
options.deliveryRetentionMs === undefined ? {} : { ttl: options.deliveryRetentionMs },
|
|
767
|
+
)
|
|
768
|
+
} catch {
|
|
769
|
+
// delivery logging is best-effort
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return { delivery, envelope, result }
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export declare namespace ping {
|
|
776
|
+
/** Options for {@link ping}. */
|
|
777
|
+
type Options = {
|
|
778
|
+
/** Delivery-log row TTL in ms; defaults to {@link deliveryRetentionMs}. */
|
|
779
|
+
deliveryRetentionMs?: number | undefined
|
|
780
|
+
/** `fetch` implementation passed through to {@link deliver}. */
|
|
781
|
+
fetch?: typeof globalThis.fetch | undefined
|
|
782
|
+
/** Deterministic event-id nonce; defaults to random (tests only). */
|
|
783
|
+
nonce?: string | undefined
|
|
784
|
+
/** Clock used for timestamps; injectable for tests. */
|
|
785
|
+
now?: (() => Date) | undefined
|
|
786
|
+
/** Delivery timeout in ms passed through to {@link deliver}. */
|
|
787
|
+
timeoutMs?: number | undefined
|
|
788
|
+
}
|
|
789
|
+
/** Outcome of a ping: the classified delivery result plus the logged row. */
|
|
790
|
+
type Result = {
|
|
791
|
+
/** The delivery-log row that was recorded. */
|
|
792
|
+
delivery: Delivery
|
|
793
|
+
/** The synthetic envelope that was sent. */
|
|
794
|
+
envelope: Envelope
|
|
795
|
+
/** The classified transport result. */
|
|
796
|
+
result: WebhookDestination.Result
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function ownerKey(owner: Owner): string {
|
|
801
|
+
return owner.type === 'api_key'
|
|
802
|
+
? `api_key:${encodeURIComponent(owner.orgId)}`
|
|
803
|
+
: `mpp:${encodeURIComponent(owner.payer)}`
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function subscriptionPrefix(owner: Owner): string {
|
|
807
|
+
return `webhook:sub:${ownerKey(owner)}:`
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function subscriptionKey(owner: Owner, id: string): string {
|
|
811
|
+
return `${subscriptionPrefix(owner)}${id}`
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function cursorKey(subscriptionId: string): string {
|
|
815
|
+
return `webhook:cursor:${subscriptionId}`
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function deliveryPrefix(subscriptionId: string): string {
|
|
819
|
+
return `webhook:delivery:${subscriptionId}:`
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function deliveryKey(subscriptionId: string, deliveryId: string): string {
|
|
823
|
+
return `${deliveryPrefix(subscriptionId)}${deliveryId}`
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function activePrefix(chainId: number, eventType: EventType): string {
|
|
827
|
+
return `webhook:active:${chainId}:${eventType}:`
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function activeKey(chainId: number, eventType: EventType, id: string): string {
|
|
831
|
+
return `${activePrefix(chainId, eventType)}${id}`
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function readSubscription(store: Store.State, key: string): Promise<Subscription | null> {
|
|
835
|
+
const raw = await store.get(key)
|
|
836
|
+
if (!raw) return null
|
|
837
|
+
try {
|
|
838
|
+
return JSON.parse(raw) as Subscription
|
|
839
|
+
} catch {
|
|
840
|
+
// Corrupt entries are treated as absent.
|
|
841
|
+
return null
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function readDelivery(store: Store.State, key: string): Promise<Delivery | null> {
|
|
846
|
+
const raw = await store.get(key)
|
|
847
|
+
if (!raw) return null
|
|
848
|
+
try {
|
|
849
|
+
return JSON.parse(raw) as Delivery
|
|
850
|
+
} catch {
|
|
851
|
+
// Corrupt entries are treated as absent.
|
|
852
|
+
return null
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async function writeSubscription(store: Store.State, subscription: Subscription): Promise<void> {
|
|
857
|
+
const ttl = ttlFor(subscription)
|
|
858
|
+
const key = subscriptionKey(subscription.owner, subscription.id)
|
|
859
|
+
await store.put(key, JSON.stringify(subscription), { ttl })
|
|
860
|
+
|
|
861
|
+
// Mirror status into the poller's secondary index: only `active`
|
|
862
|
+
// subscriptions are polled. The index value is the subscription's storage key
|
|
863
|
+
// so the poller can hydrate without knowing the owner.
|
|
864
|
+
const indexKey = activeKey(subscription.chainId, subscription.eventType, subscription.id)
|
|
865
|
+
if (subscription.status === 'active') await store.put(indexKey, key, { ttl })
|
|
866
|
+
else await store.delete(indexKey)
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function ttlFor(subscription: Subscription): number | undefined {
|
|
870
|
+
if (!subscription.expiresAt) return undefined
|
|
871
|
+
return Math.max(1, new Date(subscription.expiresAt).getTime() - Date.now())
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function generateId(now: Date): string {
|
|
875
|
+
// Zero-pad the epoch-ms so ids sort chronologically by string comparison
|
|
876
|
+
// (mirroring delivery ids), letting listSubscriptions page newest-first; 15
|
|
877
|
+
// digits leaves headroom well past the year 5138.
|
|
878
|
+
const timestamp = now.getTime().toString().padStart(15, '0')
|
|
879
|
+
return `wh_${timestamp}_${nanoid()}`
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function generateDeliveryId(now: Date): string {
|
|
883
|
+
// Zero-pad the epoch-ms so ids sort chronologically by string comparison; 15
|
|
884
|
+
// digits leaves headroom well past the year 5138.
|
|
885
|
+
const timestamp = now.getTime().toString().padStart(15, '0')
|
|
886
|
+
return `whd_${timestamp}_${nanoid()}`
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function generateSecret(): string {
|
|
890
|
+
return `whsec_${Hex.random(32).slice(2)}`
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
export class LimitExceededError extends Error {
|
|
894
|
+
/** The cap that was reached. */
|
|
895
|
+
limit: number
|
|
896
|
+
|
|
897
|
+
constructor(limit: number) {
|
|
898
|
+
super(`Webhook subscription limit reached (${limit}).`)
|
|
899
|
+
this.name = 'Webhooks.LimitExceededError'
|
|
900
|
+
this.limit = limit
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Thrown when a subscription's `filters` fail to validate against the schema for
|
|
906
|
+
* its `eventType`. The detection path treats stored filters as **fail-closed**:
|
|
907
|
+
* an invalid filter never degrades to a firehose, so the API rejects bad filters
|
|
908
|
+
* at create/patch time rather than silently widening the match set later.
|
|
909
|
+
*/
|
|
910
|
+
export class InvalidFilterError extends Error {
|
|
911
|
+
/** Event type whose filters failed validation. */
|
|
912
|
+
eventType: string
|
|
913
|
+
/** Per-field validation issues, suitable for an API error `details` array. */
|
|
914
|
+
details: readonly InvalidFilterError.Detail[]
|
|
915
|
+
|
|
916
|
+
constructor(eventType: string, details: readonly InvalidFilterError.Detail[] = []) {
|
|
917
|
+
super(`Invalid filters for "${eventType}" webhook subscription.`)
|
|
918
|
+
this.name = 'Webhooks.InvalidFilterError'
|
|
919
|
+
this.eventType = eventType
|
|
920
|
+
this.details = details
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
export declare namespace InvalidFilterError {
|
|
925
|
+
/** A single validation issue: a message plus the path to the invalid value. */
|
|
926
|
+
type Detail = {
|
|
927
|
+
/** Human-readable validation message. */
|
|
928
|
+
message: string
|
|
929
|
+
/** Path to the invalid value within the `filters` object. */
|
|
930
|
+
path?: readonly (string | number)[] | undefined
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/** Thrown when webhook operations run without a configured state store. */
|
|
935
|
+
export class UnconfiguredError extends Error {
|
|
936
|
+
constructor() {
|
|
937
|
+
super('Webhooks are not configured for this deployment.')
|
|
938
|
+
this.name = 'Webhooks.UnconfiguredError'
|
|
939
|
+
}
|
|
940
|
+
}
|