tapimo 0.1.0 → 0.3.0
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/README.md +57 -16
- package/dist/ApiKey.d.ts +4 -1
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +6 -2
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +98 -3
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +150 -1
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +7917 -2315
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +141 -329
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +20 -4
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +23 -4
- package/dist/Scope.js.map +1 -1
- package/dist/Webhooks.d.ts +2 -2
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +2 -2
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +203 -120
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +111 -6
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +67 -0
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/scopes.d.ts +12 -1
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/scopes.js +3 -0
- package/dist/admin/apps/scopes.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +116 -116
- package/dist/admin/apps/verified-tokens.js +1 -1
- package/dist/admin/apps/verified-tokens.js.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +13 -13
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +151 -0
- package/dist/analytics/Analytics.d.ts.map +1 -0
- package/dist/analytics/Analytics.js +212 -0
- package/dist/analytics/Analytics.js.map +1 -0
- package/dist/analytics/tables/requestEvents.d.ts +215 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -0
- package/dist/analytics/tables/requestEvents.js +264 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -0
- package/dist/apps/data/App.d.ts +19576 -0
- package/dist/apps/data/App.d.ts.map +1 -0
- package/dist/apps/data/App.js +272 -0
- package/dist/apps/data/App.js.map +1 -0
- package/dist/apps/data/index.d.ts +3 -0
- package/dist/apps/data/index.d.ts.map +1 -0
- package/dist/apps/data/index.js +3 -0
- package/dist/apps/data/index.js.map +1 -0
- package/dist/apps/{activities.d.ts → data/routes/activities.d.ts} +32 -32
- package/dist/apps/{activities.d.ts.map → data/routes/activities.d.ts.map} +1 -1
- package/dist/apps/{activities.js → data/routes/activities.js} +13 -13
- package/dist/apps/data/routes/activities.js.map +1 -0
- package/dist/apps/{addresses.d.ts → data/routes/addresses.d.ts} +22 -22
- package/dist/apps/{addresses.d.ts.map → data/routes/addresses.d.ts.map} +1 -1
- package/dist/apps/{addresses.js → data/routes/addresses.js} +5 -5
- package/dist/apps/data/routes/addresses.js.map +1 -0
- package/dist/apps/{balances.d.ts → data/routes/balances.d.ts} +4 -4
- package/dist/apps/data/routes/balances.d.ts.map +1 -0
- package/dist/apps/{balances.js → data/routes/balances.js} +42 -11
- package/dist/apps/data/routes/balances.js.map +1 -0
- package/dist/apps/{blocks.d.ts → data/routes/blocks.d.ts} +47 -47
- package/dist/apps/data/routes/blocks.d.ts.map +1 -0
- package/dist/apps/{blocks.js → data/routes/blocks.js} +13 -13
- package/dist/apps/data/routes/blocks.js.map +1 -0
- package/dist/apps/{coingecko.d.ts → data/routes/coingecko.d.ts} +8 -8
- package/dist/apps/data/routes/coingecko.d.ts.map +1 -0
- package/dist/apps/{coingecko.js → data/routes/coingecko.js} +13 -13
- package/dist/apps/data/routes/coingecko.js.map +1 -0
- package/dist/apps/{exchanges.d.ts → data/routes/exchanges.d.ts} +108 -108
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -0
- package/dist/apps/{exchanges.js → data/routes/exchanges.js} +19 -19
- package/dist/apps/data/routes/exchanges.js.map +1 -0
- package/dist/apps/{fee-amm.d.ts → data/routes/fee-amm.d.ts} +39 -39
- package/dist/apps/data/routes/fee-amm.d.ts.map +1 -0
- package/dist/apps/{fee-amm.js → data/routes/fee-amm.js} +17 -17
- package/dist/apps/data/routes/fee-amm.js.map +1 -0
- package/dist/apps/data/routes/funding.d.ts +765 -0
- package/dist/apps/data/routes/funding.d.ts.map +1 -0
- package/dist/apps/data/routes/funding.js +383 -0
- package/dist/apps/data/routes/funding.js.map +1 -0
- package/dist/apps/{indexer.d.ts → data/routes/indexer.d.ts} +4 -4
- package/dist/apps/data/routes/indexer.d.ts.map +1 -0
- package/dist/apps/{indexer.js → data/routes/indexer.js} +13 -9
- package/dist/apps/data/routes/indexer.js.map +1 -0
- package/dist/apps/{receipts.d.ts → data/routes/receipts.d.ts} +49 -49
- package/dist/apps/data/routes/receipts.d.ts.map +1 -0
- package/dist/apps/{receipts.js → data/routes/receipts.js} +14 -20
- package/dist/apps/data/routes/receipts.js.map +1 -0
- package/dist/apps/{rpc.d.ts → data/routes/rpc.d.ts} +33 -40
- package/dist/apps/data/routes/rpc.d.ts.map +1 -0
- package/dist/apps/{rpc.js → data/routes/rpc.js} +35 -7
- package/dist/apps/data/routes/rpc.js.map +1 -0
- package/dist/apps/{tokenlist.d.ts → data/routes/tokenlist.d.ts} +40 -40
- package/dist/apps/data/routes/tokenlist.d.ts.map +1 -0
- package/dist/apps/{tokenlist.js → data/routes/tokenlist.js} +8 -8
- package/dist/apps/data/routes/tokenlist.js.map +1 -0
- package/dist/apps/{tokens.d.ts → data/routes/tokens.d.ts} +468 -468
- package/dist/apps/data/routes/tokens.d.ts.map +1 -0
- package/dist/apps/{tokens.js → data/routes/tokens.js} +19 -19
- package/dist/apps/data/routes/tokens.js.map +1 -0
- package/dist/apps/{transactions.d.ts → data/routes/transactions.d.ts} +304 -304
- package/dist/apps/data/routes/transactions.d.ts.map +1 -0
- package/dist/apps/{transactions.js → data/routes/transactions.js} +13 -13
- package/dist/apps/data/routes/transactions.js.map +1 -0
- package/dist/apps/{transfers.d.ts → data/routes/transfers.d.ts} +62 -62
- package/dist/apps/data/routes/transfers.d.ts.map +1 -0
- package/dist/apps/{transfers.js → data/routes/transfers.js} +13 -13
- package/dist/apps/data/routes/transfers.js.map +1 -0
- package/dist/apps/{verified-tokens.d.ts → data/routes/verified-tokens.d.ts} +6 -6
- package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -0
- package/dist/apps/{verified-tokens.js → data/routes/verified-tokens.js} +9 -9
- package/dist/apps/data/routes/verified-tokens.js.map +1 -0
- package/dist/apps/{webhooks.d.ts → data/routes/webhooks.d.ts} +17 -17
- package/dist/apps/{webhooks.d.ts.map → data/routes/webhooks.d.ts.map} +1 -1
- package/dist/apps/{webhooks.js → data/routes/webhooks.js} +38 -34
- package/dist/apps/data/routes/webhooks.js.map +1 -0
- package/dist/apps/docs/App.d.ts +29 -0
- package/dist/apps/docs/App.d.ts.map +1 -0
- package/dist/apps/docs/App.js +54 -0
- package/dist/apps/docs/App.js.map +1 -0
- package/dist/{internal/OpenApi.fonts.d.ts → apps/docs/fonts.d.ts} +1 -1
- package/dist/apps/docs/fonts.d.ts.map +1 -0
- package/dist/{internal/OpenApi.fonts.js → apps/docs/fonts.js} +1 -1
- package/dist/apps/docs/fonts.js.map +1 -0
- package/dist/apps/docs/index.d.ts +2 -0
- package/dist/apps/docs/index.d.ts.map +1 -0
- package/dist/apps/docs/index.js +2 -0
- package/dist/apps/docs/index.js.map +1 -0
- package/dist/{internal/OpenApi.logo.d.ts → apps/docs/logo.d.ts} +1 -1
- package/dist/apps/docs/logo.d.ts.map +1 -0
- package/dist/{internal/OpenApi.logo.js → apps/docs/logo.js} +1 -1
- package/dist/apps/docs/logo.js.map +1 -0
- package/dist/apps/docs/reference.d.ts +21 -0
- package/dist/apps/docs/reference.d.ts.map +1 -0
- package/dist/apps/docs/reference.js +112 -0
- package/dist/apps/docs/reference.js.map +1 -0
- package/dist/apps/index.d.ts +5 -0
- package/dist/apps/index.d.ts.map +1 -0
- package/dist/apps/index.js +5 -0
- package/dist/apps/index.js.map +1 -0
- package/dist/apps/mcp.d.ts +76 -0
- package/dist/apps/mcp.d.ts.map +1 -0
- package/dist/apps/mcp.js +188 -0
- package/dist/apps/mcp.js.map +1 -0
- package/dist/apps/metadata.d.ts +7 -0
- package/dist/apps/metadata.d.ts.map +1 -0
- package/dist/apps/metadata.js +15 -0
- package/dist/apps/metadata.js.map +1 -0
- package/dist/apps/platform/App.d.ts +5262 -0
- package/dist/apps/platform/App.d.ts.map +1 -0
- package/dist/apps/platform/App.js +84 -0
- package/dist/apps/platform/App.js.map +1 -0
- package/dist/apps/platform/Billing.d.ts +279 -0
- package/dist/apps/platform/Billing.d.ts.map +1 -0
- package/dist/apps/platform/Billing.js +477 -0
- package/dist/apps/platform/Billing.js.map +1 -0
- package/dist/apps/platform/index.d.ts +3 -0
- package/dist/apps/platform/index.d.ts.map +1 -0
- package/dist/apps/platform/index.js +3 -0
- package/dist/apps/platform/index.js.map +1 -0
- package/dist/apps/platform/routes/api-keys.d.ts +973 -0
- package/dist/apps/platform/routes/api-keys.d.ts.map +1 -0
- package/dist/apps/platform/routes/api-keys.js +430 -0
- package/dist/apps/platform/routes/api-keys.js.map +1 -0
- package/dist/apps/platform/routes/billing.d.ts +1520 -0
- package/dist/apps/platform/routes/billing.d.ts.map +1 -0
- package/dist/apps/platform/routes/billing.js +795 -0
- package/dist/apps/platform/routes/billing.js.map +1 -0
- package/dist/apps/platform/routes/invitations.d.ts +851 -0
- package/dist/apps/platform/routes/invitations.d.ts.map +1 -0
- package/dist/apps/platform/routes/invitations.js +471 -0
- package/dist/apps/platform/routes/invitations.js.map +1 -0
- package/dist/apps/platform/routes/me.d.ts +95 -0
- package/dist/apps/platform/routes/me.d.ts.map +1 -0
- package/dist/apps/platform/routes/me.js +81 -0
- package/dist/apps/platform/routes/me.js.map +1 -0
- package/dist/apps/platform/routes/members.d.ts +554 -0
- package/dist/apps/platform/routes/members.d.ts.map +1 -0
- package/dist/apps/platform/routes/members.js +251 -0
- package/dist/apps/platform/routes/members.js.map +1 -0
- package/dist/apps/platform/routes/orgs.d.ts +596 -0
- package/dist/apps/platform/routes/orgs.d.ts.map +1 -0
- package/dist/apps/platform/routes/orgs.js +276 -0
- package/dist/apps/platform/routes/orgs.js.map +1 -0
- package/dist/apps/platform/routes/projects.d.ts +740 -0
- package/dist/apps/platform/routes/projects.d.ts.map +1 -0
- package/dist/apps/platform/routes/projects.js +286 -0
- package/dist/apps/platform/routes/projects.js.map +1 -0
- package/dist/apps/platform/routes/scopes.d.ts +107 -0
- package/dist/apps/platform/routes/scopes.d.ts.map +1 -0
- package/dist/apps/platform/routes/scopes.js +66 -0
- package/dist/apps/platform/routes/scopes.js.map +1 -0
- package/dist/apps/platform/routes/usage.d.ts +588 -0
- package/dist/apps/platform/routes/usage.d.ts.map +1 -0
- package/dist/apps/platform/routes/usage.js +391 -0
- package/dist/apps/platform/routes/usage.js.map +1 -0
- package/dist/apps/relay/App.d.ts +44 -0
- package/dist/apps/relay/App.d.ts.map +1 -0
- package/dist/apps/relay/App.js +211 -0
- package/dist/apps/relay/App.js.map +1 -0
- package/dist/apps/relay/Sponsorships.d.ts +107 -0
- package/dist/apps/relay/Sponsorships.d.ts.map +1 -0
- package/dist/apps/relay/Sponsorships.js +194 -0
- package/dist/apps/relay/Sponsorships.js.map +1 -0
- package/dist/apps/relay/index.d.ts +3 -0
- package/dist/apps/relay/index.d.ts.map +1 -0
- package/dist/apps/relay/index.js +3 -0
- package/dist/apps/relay/index.js.map +1 -0
- package/dist/cli/Cli.d.ts +9 -2
- package/dist/cli/Cli.d.ts.map +1 -1
- package/dist/cli/Cli.js +12 -4
- package/dist/cli/Cli.js.map +1 -1
- package/dist/cli/commands/admin.d.ts +5 -1
- package/dist/cli/commands/admin.d.ts.map +1 -1
- package/dist/cli/commands/admin.js +82 -3
- package/dist/cli/commands/admin.js.map +1 -1
- package/dist/db/Db.d.ts +17 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +254 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +159 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +187 -2
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/billingSettings.d.ts +55 -0
- package/dist/db/tables/billingSettings.d.ts.map +1 -0
- package/dist/db/tables/billingSettings.js +70 -0
- package/dist/db/tables/billingSettings.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +84 -0
- package/dist/db/tables/invitations.d.ts.map +1 -0
- package/dist/db/tables/invitations.js +165 -0
- package/dist/db/tables/invitations.js.map +1 -0
- package/dist/db/tables/memberships.d.ts +108 -0
- package/dist/db/tables/memberships.d.ts.map +1 -0
- package/dist/db/tables/memberships.js +153 -0
- package/dist/db/tables/memberships.js.map +1 -0
- package/dist/db/tables/organizations.d.ts +48 -0
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +97 -2
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.d.ts +65 -0
- package/dist/db/tables/projects.d.ts.map +1 -0
- package/dist/db/tables/projects.js +75 -0
- package/dist/db/tables/projects.js.map +1 -0
- package/dist/db/tables/requestUsage.d.ts +121 -0
- package/dist/db/tables/requestUsage.d.ts.map +1 -0
- package/dist/db/tables/requestUsage.js +150 -0
- package/dist/db/tables/requestUsage.js.map +1 -0
- package/dist/db/tables/sponsoredTransactions.d.ts +239 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -0
- package/dist/db/tables/sponsoredTransactions.js +314 -0
- package/dist/db/tables/sponsoredTransactions.js.map +1 -0
- package/dist/db/tables/stripeCustomers.d.ts +81 -0
- package/dist/db/tables/stripeCustomers.d.ts.map +1 -0
- package/dist/db/tables/stripeCustomers.js +120 -0
- package/dist/db/tables/stripeCustomers.js.map +1 -0
- package/dist/db/tables/users.d.ts +48 -0
- package/dist/db/tables/users.d.ts.map +1 -0
- package/dist/db/tables/users.js +65 -0
- package/dist/db/tables/users.js.map +1 -0
- package/dist/handlers/internal/executionError.d.ts.map +1 -1
- package/dist/handlers/internal/executionError.js +2 -0
- package/dist/handlers/internal/executionError.js.map +1 -1
- package/dist/handlers/internal/multisig.d.ts +2 -6
- package/dist/handlers/internal/multisig.d.ts.map +1 -1
- package/dist/handlers/internal/multisig.js +11 -36
- package/dist/handlers/internal/multisig.js.map +1 -1
- package/dist/handlers/internal/sponsorship.d.ts +35 -2
- package/dist/handlers/internal/sponsorship.d.ts.map +1 -1
- package/dist/handlers/internal/sponsorship.js +53 -14
- package/dist/handlers/internal/sponsorship.js.map +1 -1
- package/dist/handlers/internal/utils.d.ts +10 -3
- package/dist/handlers/internal/utils.d.ts.map +1 -1
- package/dist/handlers/internal/utils.js +14 -6
- package/dist/handlers/internal/utils.js.map +1 -1
- package/dist/handlers/relay.d.ts +6 -2
- package/dist/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +16 -2
- package/dist/handlers/relay.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/Auth.d.ts +222 -6
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +625 -74
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cache.d.ts.map +1 -1
- package/dist/internal/Cache.js +4 -1
- package/dist/internal/Cache.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +10 -0
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +39 -8
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/Fees.d.ts +27 -0
- package/dist/internal/Fees.d.ts.map +1 -0
- package/dist/internal/Fees.js +48 -0
- package/dist/internal/Fees.js.map +1 -0
- package/dist/internal/FundingRoutes.d.ts +183 -0
- package/dist/internal/FundingRoutes.d.ts.map +1 -0
- package/dist/internal/FundingRoutes.js +490 -0
- package/dist/internal/FundingRoutes.js.map +1 -0
- package/dist/internal/Id.d.ts +15 -0
- package/dist/internal/Id.d.ts.map +1 -0
- package/dist/internal/Id.js +22 -0
- package/dist/internal/Id.js.map +1 -0
- package/dist/internal/Log.d.ts +9 -3
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +7 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +10 -20
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +8 -58
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/RateLimit.d.ts +25 -0
- package/dist/internal/RateLimit.d.ts.map +1 -1
- package/dist/internal/RateLimit.js +51 -0
- package/dist/internal/RateLimit.js.map +1 -1
- package/dist/internal/Response.d.ts +1 -1
- package/dist/internal/Response.d.ts.map +1 -1
- package/dist/internal/Schema.d.ts +8 -0
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +7 -0
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Store.d.ts +7 -1
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +16 -2
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Viem.d.ts +84 -0
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Webhooks.d.ts +1 -1
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +3 -3
- package/dist/internal/Webhooks.js.map +1 -1
- package/package.json +33 -6
- package/src/ApiKey.ts +10 -2
- package/src/ApiKeys.test.ts +155 -1
- package/src/ApiKeys.ts +203 -1
- package/src/App.test.ts +64 -75741
- package/src/App.ts +218 -420
- package/src/Client.test-d.ts +657 -15
- package/src/Scope.ts +26 -4
- package/src/Webhooks.ts +2 -2
- package/src/admin/apps/api-keys.test.ts +51 -0
- package/src/admin/apps/api-keys.ts +84 -0
- package/src/admin/apps/scopes.test.ts +11 -1
- package/src/admin/apps/scopes.ts +3 -0
- package/src/admin/apps/verified-tokens.ts +1 -1
- package/src/admin/ui/src/components/Page.tsx +2 -3
- package/src/admin/ui/src/routes/__root.tsx +2 -2
- package/src/admin/ui/src/routes/api-keys.tsx +2 -2
- package/src/admin/ui.generated.ts +13 -13
- package/src/analytics/Analytics.test.ts +527 -0
- package/src/analytics/Analytics.ts +318 -0
- package/src/analytics/tables/requestEvents.ts +523 -0
- package/src/apps/composition.test.ts +66 -0
- package/src/apps/data/App.test.ts +72 -0
- package/src/apps/data/App.ts +330 -0
- package/src/apps/data/index.ts +2 -0
- package/src/apps/{activities.test.ts → data/routes/activities.test.ts} +3 -3
- package/src/apps/{activities.ts → data/routes/activities.ts} +15 -15
- package/src/apps/{addresses.ts → data/routes/addresses.ts} +6 -6
- package/src/apps/{balances.test.ts → data/routes/balances.test.ts} +2 -2
- package/src/apps/{balances.ts → data/routes/balances.ts} +46 -12
- package/src/apps/{blocks.test.ts → data/routes/blocks.test.ts} +3 -3
- package/src/apps/{blocks.ts → data/routes/blocks.ts} +14 -14
- package/src/apps/{coingecko.test.ts → data/routes/coingecko.test.ts} +2 -2
- package/src/apps/{coingecko.ts → data/routes/coingecko.ts} +14 -14
- package/src/apps/{exchanges.test.ts → data/routes/exchanges.test.ts} +1 -1
- package/src/apps/{exchanges.ts → data/routes/exchanges.ts} +20 -20
- package/src/apps/{fee-amm.test.ts → data/routes/fee-amm.test.ts} +3 -3
- package/src/apps/{fee-amm.ts → data/routes/fee-amm.ts} +18 -18
- package/src/apps/data/routes/funding.test.ts +214 -0
- package/src/apps/data/routes/funding.ts +584 -0
- package/src/apps/{indexer.test.ts → data/routes/indexer.test.ts} +26 -5
- package/src/apps/{indexer.ts → data/routes/indexer.ts} +13 -10
- package/src/apps/{receipts.test.ts → data/routes/receipts.test.ts} +3 -3
- package/src/apps/{receipts.ts → data/routes/receipts.ts} +15 -22
- package/src/apps/{rpc.test.ts → data/routes/rpc.test.ts} +122 -7
- package/src/apps/{rpc.ts → data/routes/rpc.ts} +38 -8
- package/src/apps/{tokenlist.test.ts → data/routes/tokenlist.test.ts} +3 -3
- package/src/apps/{tokenlist.ts → data/routes/tokenlist.ts} +9 -9
- package/src/apps/{tokens.test.ts → data/routes/tokens.test.ts} +2 -2
- package/src/apps/{tokens.ts → data/routes/tokens.ts} +20 -20
- package/src/apps/{transactions.test.ts → data/routes/transactions.test.ts} +3 -3
- package/src/apps/{transactions.ts → data/routes/transactions.ts} +15 -15
- package/src/apps/{transfers.test.ts → data/routes/transfers.test.ts} +2 -2
- package/src/apps/{transfers.ts → data/routes/transfers.ts} +15 -15
- package/src/apps/{verified-tokens.test.ts → data/routes/verified-tokens.test.ts} +3 -3
- package/src/apps/{verified-tokens.ts → data/routes/verified-tokens.ts} +10 -10
- package/src/apps/{webhooks.test.ts → data/routes/webhooks.test.ts} +7 -7
- package/src/apps/{webhooks.ts → data/routes/webhooks.ts} +50 -43
- package/src/apps/docs/App.ts +65 -0
- package/src/apps/docs/index.ts +1 -0
- package/src/apps/docs/reference.ts +132 -0
- package/src/apps/index.ts +4 -0
- package/src/apps/mcp.test.ts +206 -0
- package/src/apps/mcp.ts +250 -0
- package/src/apps/metadata.ts +23 -0
- package/src/apps/platform/App.ts +169 -0
- package/src/apps/platform/Billing.test.ts +530 -0
- package/src/apps/platform/Billing.ts +682 -0
- package/src/apps/platform/index.ts +2 -0
- package/src/apps/platform/routes/api-keys.test.ts +493 -0
- package/src/apps/platform/routes/api-keys.ts +543 -0
- package/src/apps/platform/routes/billing.test.ts +1100 -0
- package/src/apps/platform/routes/billing.ts +966 -0
- package/src/apps/platform/routes/invitations.test.ts +409 -0
- package/src/apps/platform/routes/invitations.ts +587 -0
- package/src/apps/platform/routes/me.test.ts +351 -0
- package/src/apps/platform/routes/me.ts +106 -0
- package/src/apps/platform/routes/members.test.ts +195 -0
- package/src/apps/platform/routes/members.ts +313 -0
- package/src/apps/platform/routes/orgs.test.ts +313 -0
- package/src/apps/platform/routes/orgs.ts +365 -0
- package/src/apps/platform/routes/projects.test.ts +181 -0
- package/src/apps/platform/routes/projects.ts +367 -0
- package/src/apps/platform/routes/scopes.test.ts +29 -0
- package/src/apps/platform/routes/scopes.ts +85 -0
- package/src/apps/platform/routes/usage.test.ts +422 -0
- package/src/apps/platform/routes/usage.ts +527 -0
- package/src/apps/relay/App.test.ts +933 -0
- package/src/apps/relay/App.ts +236 -0
- package/src/apps/relay/Sponsorships.test.ts +375 -0
- package/src/apps/relay/Sponsorships.ts +306 -0
- package/src/apps/relay/index.ts +2 -0
- package/src/cli/Cli.test.ts +25 -2
- package/src/cli/Cli.ts +18 -3
- package/src/cli/commands/admin.test.ts +83 -0
- package/src/cli/commands/admin.ts +91 -3
- package/src/db/Db.ts +296 -0
- package/src/db/Schema.ts +241 -2
- package/src/db/tables/billingSettings.test.ts +87 -0
- package/src/db/tables/billingSettings.ts +108 -0
- package/src/db/tables/invitations.ts +198 -0
- package/src/db/tables/memberships.ts +208 -0
- package/src/db/tables/organizations.test.ts +38 -2
- package/src/db/tables/organizations.ts +117 -2
- package/src/db/tables/projects.ts +107 -0
- package/src/db/tables/requestUsage.ts +231 -0
- package/src/db/tables/sponsoredTransactions.test.ts +325 -0
- package/src/db/tables/sponsoredTransactions.ts +464 -0
- package/src/db/tables/stripeCustomers.test.ts +109 -0
- package/src/db/tables/stripeCustomers.ts +162 -0
- package/src/db/tables/users.ts +85 -0
- package/src/handlers/internal/executionError.ts +2 -0
- package/src/handlers/internal/multisig.test.ts +84 -10
- package/src/handlers/internal/multisig.ts +15 -50
- package/src/handlers/internal/sponsorship.ts +106 -20
- package/src/handlers/internal/utils.ts +19 -7
- package/src/handlers/{internal/relay.localnet.test.ts → relay.test.ts} +2 -2
- package/src/handlers/relay.ts +23 -5
- package/src/index.ts +1 -0
- package/src/internal/Auth.test.ts +491 -25
- package/src/internal/Auth.ts +839 -84
- package/src/internal/Cache.ts +5 -1
- package/src/internal/Cursor.test.ts +3 -2
- package/src/internal/EdgeCache.test.ts +27 -4
- package/src/internal/EdgeCache.ts +47 -9
- package/src/internal/Fees.test.ts +32 -0
- package/src/internal/Fees.ts +51 -0
- package/src/internal/FundingRoutes.ts +583 -0
- package/src/internal/Id.test.ts +17 -0
- package/src/internal/Id.ts +24 -0
- package/src/internal/Log.test.ts +2 -1
- package/src/internal/Log.ts +16 -3
- package/src/internal/MetricSink.ts +1 -1
- package/src/internal/OpenApi.ts +12 -60
- package/src/internal/RateLimit.test.ts +84 -0
- package/src/internal/RateLimit.ts +70 -0
- package/src/internal/Response.ts +1 -1
- package/src/internal/Schema.ts +8 -0
- package/src/internal/Store.test.ts +52 -0
- package/src/internal/Store.ts +24 -5
- package/src/internal/Tidx.local.test.ts +1 -1
- package/src/internal/VerifiedTokens.test.ts +1 -1
- package/src/internal/WebhookDestination.test.ts +1 -1
- package/src/internal/Webhooks.test.ts +7 -7
- package/src/internal/Webhooks.ts +4 -4
- package/dist/apps/activities.js.map +0 -1
- package/dist/apps/addresses.js.map +0 -1
- package/dist/apps/balances.d.ts.map +0 -1
- package/dist/apps/balances.js.map +0 -1
- package/dist/apps/blocks.d.ts.map +0 -1
- package/dist/apps/blocks.js.map +0 -1
- package/dist/apps/coingecko.d.ts.map +0 -1
- package/dist/apps/coingecko.js.map +0 -1
- package/dist/apps/exchanges.d.ts.map +0 -1
- package/dist/apps/exchanges.js.map +0 -1
- package/dist/apps/fee-amm.d.ts.map +0 -1
- package/dist/apps/fee-amm.js.map +0 -1
- package/dist/apps/indexer.d.ts.map +0 -1
- package/dist/apps/indexer.js.map +0 -1
- package/dist/apps/receipts.d.ts.map +0 -1
- package/dist/apps/receipts.js.map +0 -1
- package/dist/apps/rpc.d.ts.map +0 -1
- package/dist/apps/rpc.js.map +0 -1
- package/dist/apps/tokenlist.d.ts.map +0 -1
- package/dist/apps/tokenlist.js.map +0 -1
- package/dist/apps/tokens.d.ts.map +0 -1
- package/dist/apps/tokens.js.map +0 -1
- package/dist/apps/transactions.d.ts.map +0 -1
- package/dist/apps/transactions.js.map +0 -1
- package/dist/apps/transfers.d.ts.map +0 -1
- package/dist/apps/transfers.js.map +0 -1
- package/dist/apps/verified-tokens.d.ts.map +0 -1
- package/dist/apps/verified-tokens.js.map +0 -1
- package/dist/apps/webhooks.js.map +0 -1
- package/dist/internal/OpenApi.fonts.d.ts.map +0 -1
- package/dist/internal/OpenApi.fonts.js.map +0 -1
- package/dist/internal/OpenApi.logo.d.ts.map +0 -1
- package/dist/internal/OpenApi.logo.js.map +0 -1
- package/src/handlers/internal/multisig.localnet.test.ts +0 -146
- /package/src/{internal/OpenApi.fonts.ts → apps/docs/fonts.ts} +0 -0
- /package/src/{internal/OpenApi.logo.ts → apps/docs/logo.ts} +0 -0
package/dist/internal/Auth.js
CHANGED
|
@@ -1,21 +1,156 @@
|
|
|
1
|
+
import * as Accounts from 'accounts/server';
|
|
2
|
+
import { Hono } from 'hono';
|
|
1
3
|
import { createMiddleware } from 'hono/factory';
|
|
2
4
|
import { matchedRoutes } from 'hono/route';
|
|
3
5
|
import { findTargetHandler } from 'hono/utils/handler';
|
|
6
|
+
import { decodeJwt } from 'jose';
|
|
4
7
|
import { Credential } from 'mppx';
|
|
5
8
|
import { Mppx, tempo } from 'mppx/hono';
|
|
9
|
+
import * as z from 'zod/mini';
|
|
6
10
|
import * as ApiKey from '../ApiKey.js';
|
|
7
11
|
import * as ApiKeys from '../ApiKeys.js';
|
|
12
|
+
import * as Db from '../db/Db.js';
|
|
13
|
+
import * as Invitations from '../db/tables/invitations.js';
|
|
14
|
+
import * as Memberships from '../db/tables/memberships.js';
|
|
15
|
+
import * as Metadata from '../apps/metadata.js';
|
|
8
16
|
import * as OpenApi from './OpenApi.js';
|
|
17
|
+
import * as Organizations from '../db/tables/organizations.js';
|
|
18
|
+
import * as Projects from '../db/tables/projects.js';
|
|
9
19
|
import * as RateLimit from './RateLimit.js';
|
|
10
20
|
import * as Response from './Response.js';
|
|
11
21
|
import * as Schema from './Schema.js';
|
|
12
22
|
import * as Scope from '../Scope.js';
|
|
23
|
+
import * as Users from '../db/tables/users.js';
|
|
13
24
|
import * as Viem from './Viem.js';
|
|
14
25
|
const policySymbol = Symbol('tempo-api.auth.policy');
|
|
15
26
|
/** Reads the authenticated principal from a Hono context. */
|
|
16
27
|
export function getPrincipal(c) {
|
|
17
28
|
return c.get('principal') ?? null;
|
|
18
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Guards org-scoped routes (`:orgId`). Sessions reach only organizations they
|
|
32
|
+
* are members of; the super admin reaches any. Missing or foreign
|
|
33
|
+
* organizations return `404`, so foreign org ids are indistinguishable from
|
|
34
|
+
* absent ones; a member below the required role gets `403` (the resource is
|
|
35
|
+
* visible to them, the action is not). The resolved organization and
|
|
36
|
+
* membership land on context — read them with {@link org} and
|
|
37
|
+
* {@link membership}.
|
|
38
|
+
*/
|
|
39
|
+
export function ensureOrg(options = {}) {
|
|
40
|
+
const { role: minimum = 'member' } = options;
|
|
41
|
+
// Typed loosely (like {@link policy}) so route chains keep their inferred
|
|
42
|
+
// schema; a concrete environment parameter collapses Hono's inference.
|
|
43
|
+
return createMiddleware(async (c, next) => {
|
|
44
|
+
const notFound = () => Response.error(c, {
|
|
45
|
+
code: 'organization_not_found',
|
|
46
|
+
message: 'Organization not found',
|
|
47
|
+
status: 404,
|
|
48
|
+
});
|
|
49
|
+
const orgId = c.req.param('orgId');
|
|
50
|
+
if (!orgId)
|
|
51
|
+
return notFound();
|
|
52
|
+
const principal = getPrincipal(c);
|
|
53
|
+
if (principal?.type !== 'session' && principal?.type !== 'super_admin')
|
|
54
|
+
return notFound();
|
|
55
|
+
const db = Db.get(contextDb(c));
|
|
56
|
+
const record = await Organizations.get(db, orgId);
|
|
57
|
+
if (!record)
|
|
58
|
+
return notFound();
|
|
59
|
+
if (principal.type !== 'super_admin') {
|
|
60
|
+
const member = await Memberships.get(db, orgId, principal.id);
|
|
61
|
+
if (!member)
|
|
62
|
+
return notFound();
|
|
63
|
+
if (Memberships.rank[member.role] < Memberships.rank[minimum])
|
|
64
|
+
return Response.error(c, {
|
|
65
|
+
code: 'forbidden',
|
|
66
|
+
message: `Requires the ${minimum} role`,
|
|
67
|
+
status: 403,
|
|
68
|
+
});
|
|
69
|
+
c.set('membership', member);
|
|
70
|
+
}
|
|
71
|
+
c.set('org', record);
|
|
72
|
+
await next();
|
|
73
|
+
return undefined;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Guards project-scoped routes (`:projectId`) nested under {@link ensureOrg},
|
|
78
|
+
* which must run first. Projects resolve only within the scoped organization;
|
|
79
|
+
* missing or cross-org projects return `404`. The resolved project lands on
|
|
80
|
+
* context — read it with {@link project}.
|
|
81
|
+
*/
|
|
82
|
+
export function ensureProject() {
|
|
83
|
+
// Typed loosely (like {@link policy}) so route chains keep their inferred
|
|
84
|
+
// schema; a concrete environment parameter collapses Hono's inference.
|
|
85
|
+
return createMiddleware(async (c, next) => {
|
|
86
|
+
const notFound = () => Response.error(c, {
|
|
87
|
+
code: 'project_not_found',
|
|
88
|
+
message: 'Project not found',
|
|
89
|
+
status: 404,
|
|
90
|
+
});
|
|
91
|
+
const projectId = c.req.param('projectId');
|
|
92
|
+
if (!projectId)
|
|
93
|
+
return notFound();
|
|
94
|
+
const scoped = c.get('org');
|
|
95
|
+
if (!scoped)
|
|
96
|
+
return notFound();
|
|
97
|
+
const record = await Projects.get(Db.get(contextDb(c)), projectId);
|
|
98
|
+
if (!record || record.orgId !== scoped.id)
|
|
99
|
+
return notFound();
|
|
100
|
+
c.set('project', record);
|
|
101
|
+
await next();
|
|
102
|
+
return undefined;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Always false at runtime, but typed as `boolean` so handlers can include a
|
|
107
|
+
* never-reached scope-error branch in Hono's inferred response union (the
|
|
108
|
+
* ensure middlewares produce these `404`s before the handler runs).
|
|
109
|
+
*/
|
|
110
|
+
export const narrowScope = false;
|
|
111
|
+
/** Typed org-scope error response for Hono client inference. Never reached at runtime. */
|
|
112
|
+
export function ensureOrgError(c) {
|
|
113
|
+
return Response.error(c, {
|
|
114
|
+
code: 'organization_not_found',
|
|
115
|
+
message: 'Organization not found',
|
|
116
|
+
status: 404,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/** Typed project-scope error response for Hono client inference. Never reached at runtime. */
|
|
120
|
+
export function ensureProjectError(c) {
|
|
121
|
+
// Project routes run the org scope first, so either not-found can surface.
|
|
122
|
+
const code = c.req.header('tempo-narrow-scope-code') === 'organization_not_found'
|
|
123
|
+
? 'organization_not_found'
|
|
124
|
+
: 'project_not_found';
|
|
125
|
+
return Response.error(c, {
|
|
126
|
+
code,
|
|
127
|
+
message: 'Project not found',
|
|
128
|
+
status: 404,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Reads the caller's membership resolved by {@link ensureOrg}; `undefined` for `super_admin`. */
|
|
132
|
+
export function membership(c) {
|
|
133
|
+
return c.get('membership');
|
|
134
|
+
}
|
|
135
|
+
/** Reads the organization resolved by {@link ensureOrg}; throws when the middleware did not run. */
|
|
136
|
+
export function org(c) {
|
|
137
|
+
const record = c.get('org');
|
|
138
|
+
if (!record)
|
|
139
|
+
throw new Error('`orgScope` middleware must run before reading the organization');
|
|
140
|
+
return record;
|
|
141
|
+
}
|
|
142
|
+
/** Reads the project resolved by {@link ensureProject}; throws when the middleware did not run. */
|
|
143
|
+
export function project(c) {
|
|
144
|
+
const record = c.get('project');
|
|
145
|
+
if (!record)
|
|
146
|
+
throw new Error('`projectScope` middleware must run before reading the project');
|
|
147
|
+
return record;
|
|
148
|
+
}
|
|
149
|
+
// Reads the app's database source from request context (set by `App.create`
|
|
150
|
+
// from its `db` option).
|
|
151
|
+
function contextDb(c) {
|
|
152
|
+
return c.get('db');
|
|
153
|
+
}
|
|
19
154
|
/**
|
|
20
155
|
* Always false at runtime, but typed as `boolean` so handlers can include a
|
|
21
156
|
* never-reached access-error branch in Hono's inferred response union.
|
|
@@ -55,6 +190,31 @@ export function accessError(c) {
|
|
|
55
190
|
status: 429,
|
|
56
191
|
});
|
|
57
192
|
}
|
|
193
|
+
/** Typed lane-less policy error response for Hono client inference. Never reached at runtime. */
|
|
194
|
+
export function superAdminAccessError(c) {
|
|
195
|
+
const status = Number(c.req.header('tempo-narrow-access-status'));
|
|
196
|
+
if (status === 400)
|
|
197
|
+
return Response.error(c, {
|
|
198
|
+
code: 'api_key_malformed',
|
|
199
|
+
message: 'Malformed API key',
|
|
200
|
+
status: 400,
|
|
201
|
+
});
|
|
202
|
+
if (status === 401) {
|
|
203
|
+
const code = c.req.header('tempo-narrow-access-code') === 'api_key_missing'
|
|
204
|
+
? 'api_key_missing'
|
|
205
|
+
: 'api_key_invalid';
|
|
206
|
+
return Response.error(c, {
|
|
207
|
+
code,
|
|
208
|
+
message: 'Missing or invalid API key',
|
|
209
|
+
status: 401,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return Response.error(c, {
|
|
213
|
+
code: 'forbidden',
|
|
214
|
+
message: 'API key not permitted for this route',
|
|
215
|
+
status: 403,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
58
218
|
/** Typed auth/payment error response for Hono client inference. Never reached at runtime. */
|
|
59
219
|
export function paidAccessError(c) {
|
|
60
220
|
const status = Number(c.req.header('tempo-narrow-access-status'));
|
|
@@ -68,7 +228,9 @@ export function paidAccessError(c) {
|
|
|
68
228
|
export function middleware(options) {
|
|
69
229
|
const { charge = {}, ...mpp } = options.mpp === false ? {} : (options.mpp ?? {});
|
|
70
230
|
const defaults = laneDefaults(options);
|
|
71
|
-
|
|
231
|
+
// Default-deny: no lanes open until a route grants them via `policy`.
|
|
232
|
+
// Otherwise the `super_admin` principal bypasses lane checks entirely.
|
|
233
|
+
const base = {};
|
|
72
234
|
// Keys resolve against the app's KV state store on request context
|
|
73
235
|
// (`App.create({ kv })`), so the middleware needs no storage wiring; when no
|
|
74
236
|
// store is configured, every token resolves to `null` (closed).
|
|
@@ -94,6 +256,10 @@ export function middleware(options) {
|
|
|
94
256
|
resolve,
|
|
95
257
|
},
|
|
96
258
|
}),
|
|
259
|
+
// The throttle ceiling for sandbox keys without active billing, so
|
|
260
|
+
// API-key-only routes (no per-route `policy.public`) still fall back to the
|
|
261
|
+
// deployment's configured public quota.
|
|
262
|
+
publicRateLimit: defaults.public.rateLimit,
|
|
97
263
|
...(options.mpp === false
|
|
98
264
|
? {}
|
|
99
265
|
: {
|
|
@@ -104,6 +270,10 @@ export function middleware(options) {
|
|
|
104
270
|
}),
|
|
105
271
|
}),
|
|
106
272
|
rateLimit: RateLimit.memory(options.rateLimit),
|
|
273
|
+
...(options.session ? { session: options.session } : {}),
|
|
274
|
+
...(options.superAdmin?.secret
|
|
275
|
+
? { superAdmin: { tokenHash: ApiKey.hash(options.superAdmin.secret) } }
|
|
276
|
+
: {}),
|
|
107
277
|
};
|
|
108
278
|
return createMiddleware((c, next) => {
|
|
109
279
|
const route = openApiRoute(c);
|
|
@@ -118,6 +288,266 @@ export function middleware(options) {
|
|
|
118
288
|
return require(endpointPolicy)(c, next);
|
|
119
289
|
});
|
|
120
290
|
}
|
|
291
|
+
/** Zod schemas documenting the session sign-in surface (`/v1/auth/siwe`). */
|
|
292
|
+
var schema;
|
|
293
|
+
(function (schema) {
|
|
294
|
+
const siweMessage = 'api.tempo.xyz wants you to sign in with your Ethereum account:\n0x0000000000000000000000000000000000000000\n\nURI: https://api.tempo.xyz\nVersion: 1\nChain ID: 0\nNonce: 3D0sZfnHqTBmc9tKR\nIssued At: 2026-01-01T00:00:00.000Z\nExpiration Time: 2026-01-01T00:10:00.000Z';
|
|
295
|
+
/** Error body returned by the sign-in handler (SDK shape, not the API error envelope). */
|
|
296
|
+
schema.HandlerError = z.object({
|
|
297
|
+
error: z
|
|
298
|
+
.string()
|
|
299
|
+
.check(z.describe('Human-readable failure reason.'), z.meta({ examples: ['domain mismatch'] })),
|
|
300
|
+
issues: z
|
|
301
|
+
.optional(z.array(z.unknown()))
|
|
302
|
+
.check(z.describe('Field-level validation issues, present on request-schema failures.')),
|
|
303
|
+
});
|
|
304
|
+
/** Schemas for `POST /v1/auth/siwe/challenge`. */
|
|
305
|
+
let challenge;
|
|
306
|
+
(function (challenge) {
|
|
307
|
+
/** Request body. Accepted fields beyond this document pass through to the SDK handler. */
|
|
308
|
+
challenge.Body = z.object({});
|
|
309
|
+
/** Response body. */
|
|
310
|
+
challenge.Response = z.object({
|
|
311
|
+
message: z
|
|
312
|
+
.string()
|
|
313
|
+
.check(z.describe('Single-use EIP-4361 (SIWE) message to sign and submit for verification.'), z.meta({ examples: [siweMessage] })),
|
|
314
|
+
});
|
|
315
|
+
})(challenge = schema.challenge || (schema.challenge = {}));
|
|
316
|
+
/** Schemas for `POST /v1/auth/siwe` (verify). */
|
|
317
|
+
let verify;
|
|
318
|
+
(function (verify) {
|
|
319
|
+
/** Request body. */
|
|
320
|
+
verify.Body = z.object({
|
|
321
|
+
address: z
|
|
322
|
+
.string()
|
|
323
|
+
.check(z.describe('Wallet address that signed the message; becomes the session subject.'), z.meta({ examples: ['0x0000000000000000000000000000000000000001'] })),
|
|
324
|
+
idToken: z.optional(z.string()).check(z.describe('Wallet-minted OIDC identity token (JWT) asserting a verified email, folded onto the session.'), // prettier-ignore
|
|
325
|
+
z.meta({ examples: ['eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIweDAwMDAifQ.c2lnbmF0dXJl'] })),
|
|
326
|
+
keyAuthorization: z.optional(z.string()).check(z.describe('RLP-serialized signed key authorization (TIP-1053) whose witness binds this message; verification recovers over its digest.'), // prettier-ignore
|
|
327
|
+
z.meta({ examples: [`0x${'aa'.repeat(60)}`] })),
|
|
328
|
+
message: z
|
|
329
|
+
.string()
|
|
330
|
+
.check(z.describe('The exact challenge message issued by `POST /v1/auth/siwe/challenge`.'), z.meta({ examples: [siweMessage] })),
|
|
331
|
+
returnToken: z.optional(z.boolean()).check(z.describe('Return the session token in the body instead of setting the session cookie.'), // prettier-ignore
|
|
332
|
+
z.meta({ examples: [true] })),
|
|
333
|
+
signature: z
|
|
334
|
+
.string()
|
|
335
|
+
.check(z.describe('Signature over the challenge message (or the key-authorization digest).'), z.meta({ examples: [`0x${'aa'.repeat(65)}`] })),
|
|
336
|
+
});
|
|
337
|
+
/** Response body. */
|
|
338
|
+
verify.Response = z.object({
|
|
339
|
+
token: z
|
|
340
|
+
.optional(z.string())
|
|
341
|
+
.check(z.describe('Bearer session token; present only when `returnToken` is true.'), z.meta({ examples: ['aa'.repeat(32)] })),
|
|
342
|
+
});
|
|
343
|
+
})(verify = schema.verify || (schema.verify = {}));
|
|
344
|
+
})(schema || (schema = {}));
|
|
345
|
+
/** `{ error }` response for a sign-in handler failure status. */
|
|
346
|
+
function handlerError(description) {
|
|
347
|
+
return {
|
|
348
|
+
content: { 'application/json': { schema: OpenApi.resolver(schema.HandlerError) } },
|
|
349
|
+
description,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Builds the built-in auth pieces from `App.create`'s `auth` option: the
|
|
354
|
+
* session-surface app to mount (when `session` is configured) and the
|
|
355
|
+
* enforcement middleware, wired with the surface's resolver. `auth: false`
|
|
356
|
+
* disables both.
|
|
357
|
+
*/
|
|
358
|
+
export function install(options) {
|
|
359
|
+
const { db, kv } = options;
|
|
360
|
+
const { session: options_session, ...options_middleware } = options.auth === false ? {} : (options.auth ?? {});
|
|
361
|
+
if (options_session && !kv)
|
|
362
|
+
throw new Error('`auth.session` requires the `kv` state store: sessions and sign-in challenges persist there.'); // prettier-ignore
|
|
363
|
+
// Session sign-in surface (challenge, verify, logout — SIWE wallet sign-in
|
|
364
|
+
// today) plus the capability the session lane resolves through. Public but
|
|
365
|
+
// policy-gated: the hidden describeRoute engages the middleware for the
|
|
366
|
+
// mounted sub-app, and the public lane rate-limits challenge-store writes.
|
|
367
|
+
const app = (() => {
|
|
368
|
+
if (!options_session || !kv)
|
|
369
|
+
return undefined;
|
|
370
|
+
const { issuer, origin, requireEmail, ttl } = (typeof options_session === 'object' ? options_session.wallet : undefined) ?? {};
|
|
371
|
+
// Case-insensitive exact-match email-domain allowlist for new-user sign-ups.
|
|
372
|
+
const emailAllowlist = (typeof options_session === 'object' ? options_session.emailAllowlist : undefined)
|
|
373
|
+
?.map((domain) => domain.trim().toLowerCase())
|
|
374
|
+
.filter(Boolean);
|
|
375
|
+
const handler = Accounts.Handler.auth({
|
|
376
|
+
...(issuer || requireEmail
|
|
377
|
+
? { identity: { ...(issuer ? { issuer } : {}), ...(requireEmail ? { required: true } : {}) } } // prettier-ignore
|
|
378
|
+
: {}),
|
|
379
|
+
// Sign-in upserts the `users` row keyed by address.
|
|
380
|
+
onAuthenticate: async ({ address }) => {
|
|
381
|
+
await Users.upsertByAddress(Db.get(db), { address });
|
|
382
|
+
},
|
|
383
|
+
// No pinned origin: on Workers the edge proxy headers are trustworthy, so
|
|
384
|
+
// opt in explicitly (the SDK's guard ignores its own Workers default).
|
|
385
|
+
// Off-Workers the SDK keeps throwing until `wallet.origin` is set.
|
|
386
|
+
...(origin
|
|
387
|
+
? { origin }
|
|
388
|
+
: globalThis.navigator?.userAgent === 'Cloudflare-Workers'
|
|
389
|
+
? { trustProxy: true }
|
|
390
|
+
: {}),
|
|
391
|
+
// The app's KV store under the SDK's Kv contract: JSON values,
|
|
392
|
+
// second-based TTLs, `auth:`-prefixed keys disjoint from API-key
|
|
393
|
+
// records. `take` is omitted; the SDK falls back to get+delete.
|
|
394
|
+
store: {
|
|
395
|
+
async delete(key) {
|
|
396
|
+
await kv.store.delete(`auth:${key}`);
|
|
397
|
+
},
|
|
398
|
+
async get(key) {
|
|
399
|
+
const value = await kv.store.get(`auth:${key}`);
|
|
400
|
+
return value === null ? undefined : JSON.parse(value);
|
|
401
|
+
},
|
|
402
|
+
async set(key, value, options) {
|
|
403
|
+
await kv.store.put(`auth:${key}`, JSON.stringify(value), options?.ttl ? { ttl: options.ttl * 1_000 } : {});
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
...(ttl ? { ttl } : {}),
|
|
407
|
+
});
|
|
408
|
+
const session = {
|
|
409
|
+
resolve: async (c) => {
|
|
410
|
+
const payload = await handler.getSession(c.req.raw);
|
|
411
|
+
if (!payload)
|
|
412
|
+
return null;
|
|
413
|
+
const database = Db.get(db);
|
|
414
|
+
const user = await Users.getByAddress(database, payload.address);
|
|
415
|
+
if (!user)
|
|
416
|
+
return null;
|
|
417
|
+
// Fold a newly verified email onto the user row: identity tokens are
|
|
418
|
+
// optional per sign-in, so the column fills lazily.
|
|
419
|
+
if (payload.email && user.email !== payload.email)
|
|
420
|
+
await Users.setEmail(database, user.id, payload.email);
|
|
421
|
+
const email = payload.email ?? user.email ?? undefined;
|
|
422
|
+
return {
|
|
423
|
+
...(email ? { email } : {}),
|
|
424
|
+
id: user.id,
|
|
425
|
+
identity: { provider: 'wallet', subject: payload.address },
|
|
426
|
+
type: 'session',
|
|
427
|
+
};
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
const policy_public = policy({ public: true });
|
|
431
|
+
const policy_session = policy({ public: true, session: true });
|
|
432
|
+
const describe = OpenApi.describeRoute({ hide: true });
|
|
433
|
+
// Sign-up gate: new addresses need an allowlisted identity email domain
|
|
434
|
+
// or a pending invitation; existing users pass. The claim decodes
|
|
435
|
+
// unverified; the mounted handler verifies the token before any session
|
|
436
|
+
// or user is created, so a forged claim never authenticates.
|
|
437
|
+
const gate_signup = createMiddleware(async (c, next) => {
|
|
438
|
+
if (!emailAllowlist?.length)
|
|
439
|
+
return next();
|
|
440
|
+
const { address, idToken } = (await c.req.json());
|
|
441
|
+
const database = Db.get(db);
|
|
442
|
+
const user = await Users.getByAddress(database, address);
|
|
443
|
+
if (!user) {
|
|
444
|
+
const email = (() => {
|
|
445
|
+
if (!idToken)
|
|
446
|
+
return undefined;
|
|
447
|
+
try {
|
|
448
|
+
const { email } = decodeJwt(idToken);
|
|
449
|
+
return typeof email === 'string' ? email.toLowerCase() : undefined;
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
})();
|
|
455
|
+
const allowed = await (async () => {
|
|
456
|
+
if (!email)
|
|
457
|
+
return false;
|
|
458
|
+
const at = email.lastIndexOf('@');
|
|
459
|
+
if (at > 0 && emailAllowlist.includes(email.slice(at + 1)))
|
|
460
|
+
return true;
|
|
461
|
+
// A pending invitation admits the email regardless of domain.
|
|
462
|
+
return (await Invitations.listByEmail(database, email)).length > 0;
|
|
463
|
+
})();
|
|
464
|
+
if (!allowed)
|
|
465
|
+
return c.json({ error: 'sign-ups are restricted to approved email domains or invited emails' }, 403); // prettier-ignore
|
|
466
|
+
}
|
|
467
|
+
return next();
|
|
468
|
+
});
|
|
469
|
+
const app = new Hono()
|
|
470
|
+
.use('/v1/auth/siwe', policy_public, describe)
|
|
471
|
+
.use('/v1/auth/siwe/*', policy_public, describe);
|
|
472
|
+
// Documented operations: pre-routes carrying the OpenAPI metadata and the
|
|
473
|
+
// envelope-shaped body validation; the mounted handler owns behavior.
|
|
474
|
+
// Statement form keeps them off the app type (`hc` surface).
|
|
475
|
+
app.post('/v1/auth/siwe/challenge', policy_public, OpenApi.validate('json', schema.challenge.Body, {
|
|
476
|
+
code: 'body_invalid',
|
|
477
|
+
message: 'Invalid request body',
|
|
478
|
+
}), OpenApi.describeRoute({
|
|
479
|
+
description: 'Issues a single-use SIWE challenge message to sign. The challenge expires after a short TTL.',
|
|
480
|
+
// Overrides the surface-wide hidden mount guard for this documented route.
|
|
481
|
+
hide: false,
|
|
482
|
+
operationId: 'createSiweChallenge',
|
|
483
|
+
responses: {
|
|
484
|
+
200: {
|
|
485
|
+
content: { 'application/json': { schema: OpenApi.resolver(schema.challenge.Response) } }, // prettier-ignore
|
|
486
|
+
description: 'The challenge message to sign.',
|
|
487
|
+
headers: { ...OpenApi.successHeaders },
|
|
488
|
+
},
|
|
489
|
+
400: OpenApi.standardError(400, 'Malformed request body.', ['body_invalid']),
|
|
490
|
+
429: OpenApi.standardError(429, 'Rate limited.'),
|
|
491
|
+
500: OpenApi.standardError(500, 'Internal server error.'),
|
|
492
|
+
},
|
|
493
|
+
summary: 'Create challenge (SIWE)',
|
|
494
|
+
tags: ['Authentication'],
|
|
495
|
+
}));
|
|
496
|
+
app.post('/v1/auth/siwe', policy_public, OpenApi.validate('json', schema.verify.Body, {
|
|
497
|
+
code: 'body_invalid',
|
|
498
|
+
message: 'Invalid request body',
|
|
499
|
+
}), gate_signup, OpenApi.describeRoute({
|
|
500
|
+
description: 'Verifies the signed challenge and establishes a session. Sets a session cookie by default; pass `returnToken` to receive a bearer token instead.',
|
|
501
|
+
hide: false,
|
|
502
|
+
operationId: 'verifySiwe',
|
|
503
|
+
responses: {
|
|
504
|
+
200: {
|
|
505
|
+
content: { 'application/json': { schema: OpenApi.resolver(schema.verify.Response) } },
|
|
506
|
+
description: 'Session established. Cookie mode sets `Set-Cookie`; token mode returns `token`.', // prettier-ignore
|
|
507
|
+
headers: {
|
|
508
|
+
...OpenApi.successHeaders,
|
|
509
|
+
'Set-Cookie': {
|
|
510
|
+
description: 'Session cookie (`accounts_auth`); omitted when `returnToken` is true.', // prettier-ignore
|
|
511
|
+
schema: { type: 'string' },
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
400: handlerError('Invalid, expired, or mismatched challenge message; or a required identity token is missing.'), // prettier-ignore
|
|
516
|
+
401: handlerError('Signature or identity verification failed.'),
|
|
517
|
+
...(emailAllowlist?.length
|
|
518
|
+
? { 403: handlerError('Sign-up rejected: the identity email is neither on an allowlisted domain nor pending an invitation.') } // prettier-ignore
|
|
519
|
+
: {}),
|
|
520
|
+
409: handlerError('Challenge nonce already used or unknown.'),
|
|
521
|
+
429: OpenApi.standardError(429, 'Rate limited.'),
|
|
522
|
+
500: OpenApi.standardError(500, 'Internal server error.'),
|
|
523
|
+
},
|
|
524
|
+
summary: 'Authenticate (SIWE)',
|
|
525
|
+
tags: ['Authentication'],
|
|
526
|
+
}));
|
|
527
|
+
app.post('/v1/auth/siwe/logout', policy_session, OpenApi.describeRoute({
|
|
528
|
+
description: 'Revokes the current session and clears the session cookie.',
|
|
529
|
+
hide: false,
|
|
530
|
+
operationId: 'siweLogout',
|
|
531
|
+
responses: {
|
|
532
|
+
204: { description: 'Session revoked and cookie cleared.' },
|
|
533
|
+
429: OpenApi.standardError(429, 'Rate limited.'),
|
|
534
|
+
500: OpenApi.standardError(500, 'Internal server error.'),
|
|
535
|
+
},
|
|
536
|
+
summary: 'Sign out',
|
|
537
|
+
tags: ['Authentication'],
|
|
538
|
+
}));
|
|
539
|
+
return Object.assign(
|
|
540
|
+
// The tag joins the `Platform API` group at `platform()`'s chosen slot;
|
|
541
|
+
// group listings filter to defined tags, so it drops out when unmounted.
|
|
542
|
+
Metadata.attach(app.route('/v1/auth/siwe', handler), {
|
|
543
|
+
tags: [{ name: 'Authentication', description: 'Authenticate into the Tempo Platform.' }],
|
|
544
|
+
}), { session });
|
|
545
|
+
})();
|
|
546
|
+
const middleware_ = options.auth === false
|
|
547
|
+
? undefined
|
|
548
|
+
: middleware({ ...options_middleware, ...(app ? { session: app.session } : {}) });
|
|
549
|
+
return { app, middleware: middleware_ };
|
|
550
|
+
}
|
|
121
551
|
/** Creates route middleware that contributes an auth policy override. */
|
|
122
552
|
export function policy(options) {
|
|
123
553
|
const middleware = createMiddleware((_, next) => next());
|
|
@@ -142,21 +572,14 @@ function laneDefaults(options) {
|
|
|
142
572
|
},
|
|
143
573
|
mpp: { charge: {} },
|
|
144
574
|
public: { rateLimit: public_?.rateLimit ?? { perMinute: 60 } },
|
|
575
|
+
session: true,
|
|
145
576
|
};
|
|
146
577
|
}
|
|
147
|
-
/**
|
|
148
|
-
* Base policy before route overrides. Default-closed except the API-key lane:
|
|
149
|
-
* a route that declares nothing is API-key-only, and must opt into the public
|
|
150
|
-
* and paid lanes explicitly. This makes a partial override fail safe — omitting
|
|
151
|
-
* a lane leaves it closed rather than silently inheriting open access.
|
|
152
|
-
*/
|
|
153
|
-
function basePolicy(options, defaults) {
|
|
154
|
-
return options.apiKey === false ? {} : { apiKey: defaults.apiKey };
|
|
155
|
-
}
|
|
156
578
|
function resolvePolicy(base, defaults, overrides, description) {
|
|
157
579
|
let apiKey = base.apiKey;
|
|
158
580
|
let mpp = base.mpp;
|
|
159
581
|
let public_ = base.public;
|
|
582
|
+
let session = base.session;
|
|
160
583
|
for (const override of overrides) {
|
|
161
584
|
if (override === false)
|
|
162
585
|
return undefined;
|
|
@@ -165,18 +588,20 @@ function resolvePolicy(base, defaults, overrides, description) {
|
|
|
165
588
|
apiKey = resolveApiKeyLane(apiKey, override.apiKey, defaults.apiKey);
|
|
166
589
|
mpp = resolveMppLane(mpp, override.mpp, defaults.mpp);
|
|
167
590
|
public_ = resolvePublicLane(public_, override.public, defaults.public);
|
|
591
|
+
session = resolveSessionLane(session, override.session, defaults.session);
|
|
168
592
|
}
|
|
169
593
|
// The OpenAPI summary/description rides along as the payment-challenge
|
|
170
594
|
// description. It is metadata, not a policy override, so it only applies when
|
|
171
595
|
// the paid lane is actually enabled and has no explicit description already.
|
|
172
596
|
if (mpp && description !== undefined && mpp.charge.description === undefined)
|
|
173
597
|
mpp = { charge: { ...mpp.charge, description } };
|
|
174
|
-
|
|
175
|
-
|
|
598
|
+
// A lane-less policy stays enforced for super admin; only an explicit
|
|
599
|
+
// `false` override opts a route out of auth entirely.
|
|
176
600
|
return {
|
|
177
601
|
...(apiKey ? { apiKey } : {}),
|
|
178
602
|
...(mpp ? { mpp } : {}),
|
|
179
603
|
...(public_ ? { public: public_ } : {}),
|
|
604
|
+
...(session ? { session } : {}),
|
|
180
605
|
};
|
|
181
606
|
}
|
|
182
607
|
function resolveApiKeyLane(current, override, default_) {
|
|
@@ -212,6 +637,13 @@ function resolvePublicLane(current, override, default_) {
|
|
|
212
637
|
const base = current ?? default_;
|
|
213
638
|
return { rateLimit: override.rateLimit ?? base.rateLimit };
|
|
214
639
|
}
|
|
640
|
+
function resolveSessionLane(current, override, default_) {
|
|
641
|
+
if (override === undefined)
|
|
642
|
+
return current;
|
|
643
|
+
if (override === false)
|
|
644
|
+
return undefined;
|
|
645
|
+
return current ?? default_;
|
|
646
|
+
}
|
|
215
647
|
function openApiRoute(c) {
|
|
216
648
|
const route = matchedRoutes(c)
|
|
217
649
|
.slice(c.req.routeIndex + 1)
|
|
@@ -248,8 +680,8 @@ function policyFromRoute(route) {
|
|
|
248
680
|
* Maps each documented operation id to its resolved access lanes by reading the
|
|
249
681
|
* `policy` overrides co-located on the app's routes. This is the single source
|
|
250
682
|
* of truth for per-operation `security` and the `402` response in the generated
|
|
251
|
-
* OpenAPI document (see `App.create`):
|
|
252
|
-
*
|
|
683
|
+
* OpenAPI document (see `App.create`): all lanes are default-closed, and an
|
|
684
|
+
* operation surfaces only the lanes its route opts into via {@link policy}.
|
|
253
685
|
*/
|
|
254
686
|
export function describeAccess(app) {
|
|
255
687
|
const groups = new Map();
|
|
@@ -268,10 +700,11 @@ export function describeAccess(app) {
|
|
|
268
700
|
for (const { operationId, overrides } of groups.values()) {
|
|
269
701
|
if (!operationId)
|
|
270
702
|
continue;
|
|
271
|
-
let apiKey =
|
|
703
|
+
let apiKey = false;
|
|
272
704
|
let scopes = [];
|
|
273
705
|
let public_ = false;
|
|
274
706
|
let mpp = false;
|
|
707
|
+
let session = false;
|
|
275
708
|
for (const override of overrides) {
|
|
276
709
|
if (override.apiKey !== undefined)
|
|
277
710
|
apiKey = override.apiKey !== false;
|
|
@@ -281,8 +714,10 @@ export function describeAccess(app) {
|
|
|
281
714
|
public_ = override.public !== false;
|
|
282
715
|
if (override.mpp !== undefined)
|
|
283
716
|
mpp = override.mpp !== false;
|
|
717
|
+
if (override.session !== undefined)
|
|
718
|
+
session = override.session !== false;
|
|
284
719
|
}
|
|
285
|
-
access[operationId] = { mpp, public: public_, scopes: apiKey ? scopes : [] };
|
|
720
|
+
access[operationId] = { apiKey, mpp, public: public_, scopes: apiKey ? scopes : [], session };
|
|
286
721
|
}
|
|
287
722
|
return access;
|
|
288
723
|
}
|
|
@@ -334,73 +769,122 @@ export function require(policy) {
|
|
|
334
769
|
});
|
|
335
770
|
const hasPayment = hasPaymentCredential(c) && isPaymentEnabled(auth, policy);
|
|
336
771
|
if (credential) {
|
|
772
|
+
// The super admin secret precedes key lookup: it is not a stored key.
|
|
773
|
+
// It must never reach KV resolution or scope/quota checks.
|
|
774
|
+
if (auth.superAdmin && ApiKey.hash(credential.token) === auth.superAdmin.tokenHash) {
|
|
775
|
+
c.set('principal', { actor: 'super_admin', id: 'super_admin', type: 'super_admin' });
|
|
776
|
+
return next();
|
|
777
|
+
}
|
|
337
778
|
const apiKey = await auth.apiKey?.resolve(c, credential.token);
|
|
338
|
-
if (!apiKey)
|
|
779
|
+
if (!apiKey) {
|
|
780
|
+
// A bearer session token is indistinguishable from an API key at the
|
|
781
|
+
// header, so a failed key lookup falls through to the session lane
|
|
782
|
+
// before rejecting.
|
|
783
|
+
if (policy.session) {
|
|
784
|
+
const session = await resolveSession(c, auth);
|
|
785
|
+
if (session) {
|
|
786
|
+
c.set('principal', session);
|
|
787
|
+
return next();
|
|
788
|
+
}
|
|
789
|
+
}
|
|
339
790
|
return jsonError(c, {
|
|
340
791
|
code: 'api_key_invalid',
|
|
341
792
|
message: 'Invalid API key',
|
|
342
793
|
status: 401,
|
|
343
794
|
});
|
|
344
|
-
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
795
|
+
}
|
|
796
|
+
// Sandbox keys are testnet-only. A sandbox key that explicitly targets
|
|
797
|
+
// mainnet (`?chainId`) is rejected with a clear message rather than
|
|
798
|
+
// steered or downgraded to anonymous, so the caller learns their key does
|
|
799
|
+
// not cover mainnet. The no-`chainId` default (mainnet) is enforced by
|
|
800
|
+
// the data group's chain guard, which knows the route is chain-scoped;
|
|
801
|
+
// the relay resolves its chain from the request body and enforces the
|
|
802
|
+
// same rule in its sponsor validation.
|
|
349
803
|
const explicitChain = explicitChainId(c);
|
|
350
|
-
const
|
|
804
|
+
const principal = principalFromApiKey(apiKey);
|
|
805
|
+
c.set('principal', principal);
|
|
806
|
+
if (apiKey.environment === 'sandbox' &&
|
|
351
807
|
explicitChain !== undefined &&
|
|
352
|
-
Viem.isMainnet(explicitChain)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
c
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
:
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
808
|
+
Viem.isMainnet(explicitChain))
|
|
809
|
+
return jsonError(c, {
|
|
810
|
+
code: 'api_key_forbidden',
|
|
811
|
+
message: 'Sandbox API keys only support testnet. Pass a testnet `chainId`.',
|
|
812
|
+
status: 403,
|
|
813
|
+
});
|
|
814
|
+
// Default-deny: routes with no API-key lane allow only the super admin.
|
|
815
|
+
// A valid key is authenticated but forbidden.
|
|
816
|
+
if (!policy.apiKey)
|
|
817
|
+
return jsonError(c, {
|
|
818
|
+
code: 'forbidden',
|
|
819
|
+
message: 'API key not permitted for this route',
|
|
820
|
+
status: 403,
|
|
821
|
+
});
|
|
822
|
+
const missing = apiKey.scopes.includes(Scope.wildcard)
|
|
823
|
+
? []
|
|
824
|
+
: (policy.apiKey?.scopes.filter((scope) => !apiKey.scopes.includes(scope)) ?? []);
|
|
825
|
+
if (missing.length > 0)
|
|
826
|
+
return jsonError(c, {
|
|
827
|
+
code: 'api_key_forbidden',
|
|
828
|
+
message: 'API key missing required scope',
|
|
829
|
+
status: 403,
|
|
830
|
+
});
|
|
831
|
+
// Sandbox keys without active billing are throttled to the public quota.
|
|
832
|
+
// `billingActive` is snapshotted on the key record (stamped at mint,
|
|
833
|
+
// re-synced when billing changes), so this needs no billing read on the
|
|
834
|
+
// request path. Resolved before the paid lane so a payment credential
|
|
835
|
+
// can't buy past the throttle: sandbox is non-billable testnet.
|
|
836
|
+
const throttled = apiKey.environment === 'sandbox' && apiKey.billingActive !== true;
|
|
837
|
+
if (hasPayment && !throttled)
|
|
838
|
+
return requirePayment(c, next, {
|
|
839
|
+
auth,
|
|
840
|
+
payment: { reason: 'api_key_over_quota', type: 'mpp' },
|
|
841
|
+
policy,
|
|
842
|
+
principal,
|
|
843
|
+
});
|
|
844
|
+
if (policy.apiKey) {
|
|
845
|
+
// A route consumes exactly one quota bucket: its single required scope,
|
|
846
|
+
// else `'*'` (zero scopes). Routes today require at most one scope; a
|
|
847
|
+
// future multi-scope route buckets against its first scope.
|
|
848
|
+
const scope = policy.apiKey.scopes[0] ?? Scope.wildcard;
|
|
849
|
+
// A throttled sandbox key shares the key's bucket but at the public
|
|
850
|
+
// ceiling, so activating billing lifts the limit on the same counter.
|
|
851
|
+
const limit = throttled
|
|
852
|
+
? (policy.public?.rateLimit ?? auth.publicRateLimit ?? { perMinute: 60 })
|
|
853
|
+
: resolveApiKeyLimit({ apiKey, auth, policy: policy.apiKey, scope });
|
|
854
|
+
const result = await consumeRateLimit(auth, {
|
|
855
|
+
key: `api_key:${apiKey.id}:scope:${scope}`,
|
|
856
|
+
limit,
|
|
857
|
+
});
|
|
858
|
+
if (result) {
|
|
859
|
+
setRateLimitHeaders(c, result);
|
|
860
|
+
// Surface the bucket so callers can correlate a 429 with the exhausted
|
|
861
|
+
// quota. Set before the over-quota branch so it rides the 429 (an MPP
|
|
862
|
+
// 402 challenge is owned by mppx and drops these headers — by design).
|
|
863
|
+
c.header('RateLimit-Scope', scope);
|
|
864
|
+
}
|
|
865
|
+
if (result && !result.allowed) {
|
|
866
|
+
// A throttled sandbox key gets a plain 429, never an MPP challenge —
|
|
867
|
+
// it must activate billing to lift the ceiling, not pay per request.
|
|
868
|
+
if (throttled)
|
|
869
|
+
return rateLimitExceeded(c, result);
|
|
870
|
+
return requirePaymentOrRateLimit(c, next, {
|
|
373
871
|
auth,
|
|
374
872
|
payment: { reason: 'api_key_over_quota', type: 'mpp' },
|
|
375
873
|
policy,
|
|
376
874
|
principal,
|
|
875
|
+
rateLimit: result,
|
|
377
876
|
});
|
|
378
|
-
if (policy.apiKey) {
|
|
379
|
-
// A route consumes exactly one quota bucket: its single required scope,
|
|
380
|
-
// else `'*'` (zero scopes). Routes today require at most one scope; a
|
|
381
|
-
// future multi-scope route buckets against its first scope.
|
|
382
|
-
const scope = policy.apiKey.scopes[0] ?? Scope.wildcard;
|
|
383
|
-
const limit = resolveApiKeyLimit({ apiKey, auth, policy: policy.apiKey, scope });
|
|
384
|
-
const result = await consumeRateLimit(auth, {
|
|
385
|
-
key: `api_key:${apiKey.id}:scope:${scope}`,
|
|
386
|
-
limit,
|
|
387
|
-
});
|
|
388
|
-
if (result) {
|
|
389
|
-
setRateLimitHeaders(c, result);
|
|
390
|
-
// Surface the bucket so callers can correlate a 429 with the exhausted
|
|
391
|
-
// quota. Set before the over-quota branch so it rides the 429 (an MPP
|
|
392
|
-
// 402 challenge is owned by mppx and drops these headers — by design).
|
|
393
|
-
c.header('RateLimit-Scope', scope);
|
|
394
|
-
}
|
|
395
|
-
if (result && !result.allowed)
|
|
396
|
-
return requirePaymentOrRateLimit(c, next, {
|
|
397
|
-
auth,
|
|
398
|
-
payment: { reason: 'api_key_over_quota', type: 'mpp' },
|
|
399
|
-
policy,
|
|
400
|
-
principal,
|
|
401
|
-
rateLimit: result,
|
|
402
|
-
});
|
|
403
877
|
}
|
|
878
|
+
}
|
|
879
|
+
return next();
|
|
880
|
+
}
|
|
881
|
+
// Cookie sessions carry no Authorization credential; resolve the session
|
|
882
|
+
// lane before the public/paid lanes so signed-in browsers skip anonymous
|
|
883
|
+
// quotas.
|
|
884
|
+
if (policy.session) {
|
|
885
|
+
const session = await resolveSession(c, auth);
|
|
886
|
+
if (session) {
|
|
887
|
+
c.set('principal', session);
|
|
404
888
|
return next();
|
|
405
889
|
}
|
|
406
890
|
}
|
|
@@ -451,16 +935,57 @@ export function require(policy) {
|
|
|
451
935
|
});
|
|
452
936
|
});
|
|
453
937
|
}
|
|
938
|
+
/**
|
|
939
|
+
* How long to wait for the rate-limit counter before failing open. A hot
|
|
940
|
+
* counter backend (e.g. a single Durable Object serving one busy quota bucket)
|
|
941
|
+
* can stall for tens of seconds during a storage-latency blip; without this cap
|
|
942
|
+
* every request for that bucket blocks on it, turning a localized backend
|
|
943
|
+
* hiccup into an API-wide latency spike. The abandoned increment still applies
|
|
944
|
+
* server-side — we just stop waiting for it.
|
|
945
|
+
*/
|
|
946
|
+
const rateLimitConsumeTimeout = 300;
|
|
454
947
|
function consumeRateLimit(auth, options) {
|
|
455
948
|
if (!auth.rateLimit)
|
|
456
949
|
return Promise.resolve(null);
|
|
457
|
-
// Fail open: a rate-limit store error (e.g. a lost Durable Object
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
|
|
950
|
+
// Fail open: a rate-limit store error (e.g. a lost Durable Object connection)
|
|
951
|
+
// or a stall past `rateLimitConsumeTimeout` degrades to unenforced quota
|
|
952
|
+
// instead of failing — or blocking — the request.
|
|
953
|
+
return withTimeout(auth.rateLimit.consume(options), rateLimitConsumeTimeout).catch((error) => {
|
|
954
|
+
// A timeout or transient Durable Object fault is an expected degradation,
|
|
955
|
+
// logged at warn to stay out of error dashboards; an unexpected store
|
|
956
|
+
// throw is a genuine error.
|
|
957
|
+
if (error instanceof RateLimitTimeoutError || isDurableObjectFault(error))
|
|
958
|
+
console.warn('[auth] rate-limit consume degraded; failing open', error);
|
|
959
|
+
else
|
|
960
|
+
console.error('[auth] rate-limit consume failed; failing open', error);
|
|
461
961
|
return null;
|
|
462
962
|
});
|
|
463
963
|
}
|
|
964
|
+
function isDurableObjectFault(error) {
|
|
965
|
+
const { message, overloaded, retryable } = (error ?? {});
|
|
966
|
+
if (overloaded === true || retryable === true)
|
|
967
|
+
return true;
|
|
968
|
+
// Cloudflare redacts internal Durable Object faults to "internal error;
|
|
969
|
+
// reference = <token>", with no marker property to detect them by.
|
|
970
|
+
return typeof message === 'string' && message.startsWith('internal error');
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Rejects with a {@link RateLimitTimeoutError} if `promise` does not settle
|
|
974
|
+
* within `ms`, clearing the timer on either outcome so it never keeps the
|
|
975
|
+
* isolate alive.
|
|
976
|
+
*/
|
|
977
|
+
function withTimeout(promise, ms) {
|
|
978
|
+
return new Promise((resolve, reject) => {
|
|
979
|
+
const timer = setTimeout(() => reject(new RateLimitTimeoutError(ms)), ms);
|
|
980
|
+
promise.then((value) => {
|
|
981
|
+
clearTimeout(timer);
|
|
982
|
+
resolve(value);
|
|
983
|
+
}, (error) => {
|
|
984
|
+
clearTimeout(timer);
|
|
985
|
+
reject(error);
|
|
986
|
+
});
|
|
987
|
+
});
|
|
988
|
+
}
|
|
464
989
|
/**
|
|
465
990
|
* Resolves the rate limit for an API-key request against a quota bucket.
|
|
466
991
|
* Most-specific wins: a per-key per-scope grant is the only thing that pierces a
|
|
@@ -571,6 +1096,7 @@ function principalFromApiKey(apiKey) {
|
|
|
571
1096
|
environment: apiKey.environment,
|
|
572
1097
|
id: apiKey.id,
|
|
573
1098
|
orgId: apiKey.orgId,
|
|
1099
|
+
...(apiKey.projectId === undefined ? {} : { projectId: apiKey.projectId }),
|
|
574
1100
|
type: 'api_key',
|
|
575
1101
|
};
|
|
576
1102
|
}
|
|
@@ -608,7 +1134,11 @@ async function requirePayment(c, next, options) {
|
|
|
608
1134
|
function requirePaymentOrRateLimit(c, next, options) {
|
|
609
1135
|
if (isPaymentEnabled(options.auth, options.policy))
|
|
610
1136
|
return requirePayment(c, next, options);
|
|
611
|
-
c
|
|
1137
|
+
return rateLimitExceeded(c, options.rateLimit);
|
|
1138
|
+
}
|
|
1139
|
+
/** Emits a `429` with a `Retry-After` derived from the window reset. */
|
|
1140
|
+
function rateLimitExceeded(c, result) {
|
|
1141
|
+
c.header('Retry-After', String(Math.max(result.reset - Math.ceil(Date.now() / 1_000), 1)));
|
|
612
1142
|
return jsonError(c, {
|
|
613
1143
|
code: 'rate_limit_exceeded',
|
|
614
1144
|
message: 'Rate limit exceeded',
|
|
@@ -625,6 +1155,20 @@ function setRateLimitHeaders(c, result) {
|
|
|
625
1155
|
function contextKv(c) {
|
|
626
1156
|
return c.get('kv');
|
|
627
1157
|
}
|
|
1158
|
+
// Resolve the session lane from the auth context's capability. Failures
|
|
1159
|
+
// resolve to `null` so the remaining lanes and the final `401` still apply.
|
|
1160
|
+
async function resolveSession(c, auth) {
|
|
1161
|
+
const session = auth.session;
|
|
1162
|
+
if (!session)
|
|
1163
|
+
return null;
|
|
1164
|
+
try {
|
|
1165
|
+
return await session.resolve(c);
|
|
1166
|
+
}
|
|
1167
|
+
catch (error) {
|
|
1168
|
+
console.error('[auth] session resolve failed; treating as no session', error);
|
|
1169
|
+
return null;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
628
1172
|
/** Maximum resolve-cache entries held per isolate before eviction. */
|
|
629
1173
|
const cachedMaxEntries = 10_000;
|
|
630
1174
|
/**
|
|
@@ -675,4 +1219,11 @@ function cachedResolve(resolve, options) {
|
|
|
675
1219
|
}
|
|
676
1220
|
};
|
|
677
1221
|
}
|
|
1222
|
+
/** Thrown when a rate-limit consume exceeds {@link rateLimitConsumeTimeout}. */
|
|
1223
|
+
export class RateLimitTimeoutError extends Error {
|
|
1224
|
+
name = 'Auth.RateLimitTimeoutError';
|
|
1225
|
+
constructor(ms) {
|
|
1226
|
+
super(`Rate-limit consume timed out after ${ms}ms; failing open.`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
678
1229
|
//# sourceMappingURL=Auth.js.map
|