tapimo 0.2.0 → 0.4.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 +36 -8
- package/dist/ApiKey.d.ts +1 -0
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +5 -2
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +28 -1
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +34 -1
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +7532 -4850
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +11 -3
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +11 -2
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +14 -3
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +63 -53
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +7 -0
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/scopes.d.ts +8 -0
- 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 +57 -57
- 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 +1049 -624
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +36 -0
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingRouteProviders.d.ts +54 -0
- package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -0
- package/dist/apps/data/FundingRouteProviders.js +355 -0
- package/dist/apps/data/FundingRouteProviders.js.map +1 -0
- package/dist/apps/data/index.d.ts +1 -0
- package/dist/apps/data/index.d.ts.map +1 -1
- package/dist/apps/data/index.js +1 -0
- package/dist/apps/data/index.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +45 -45
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/exchanges.d.ts +455 -455
- package/dist/apps/data/routes/fee-amm.d.ts +101 -101
- package/dist/apps/data/routes/funding.d.ts +843 -0
- package/dist/apps/data/routes/funding.d.ts.map +1 -0
- package/dist/apps/data/routes/funding.js +443 -0
- package/dist/apps/data/routes/funding.js.map +1 -0
- package/dist/apps/data/routes/indexer.d.ts +5 -5
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +2 -8
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts +29 -36
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +29 -1
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +36 -36
- package/dist/apps/data/routes/transfers.d.ts +56 -56
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/data/routes/webhooks.d.ts +45 -45
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +8 -8
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/docs/App.d.ts +5 -3
- package/dist/apps/docs/App.d.ts.map +1 -1
- package/dist/apps/docs/App.js +28 -96
- package/dist/apps/docs/App.js.map +1 -1
- 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 +3 -2
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +3 -2
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/{platform → management}/App.d.ts +3015 -738
- package/dist/apps/management/App.d.ts.map +1 -0
- package/dist/apps/{platform → management}/App.js +20 -10
- package/dist/apps/management/App.js.map +1 -0
- package/dist/apps/management/Billing.d.ts +279 -0
- package/dist/apps/management/Billing.d.ts.map +1 -0
- package/dist/apps/management/Billing.js +477 -0
- package/dist/apps/management/Billing.js.map +1 -0
- package/dist/apps/management/index.d.ts +3 -0
- package/dist/apps/management/index.d.ts.map +1 -0
- package/dist/apps/management/index.js +3 -0
- package/dist/apps/management/index.js.map +1 -0
- package/dist/apps/{platform → management}/routes/api-keys.d.ts +341 -49
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -0
- package/dist/apps/{platform → management}/routes/api-keys.js +116 -14
- package/dist/apps/management/routes/api-keys.js.map +1 -0
- package/dist/apps/management/routes/billing.d.ts +1520 -0
- package/dist/apps/management/routes/billing.d.ts.map +1 -0
- package/dist/apps/management/routes/billing.js +795 -0
- package/dist/apps/management/routes/billing.js.map +1 -0
- package/dist/apps/{platform → management}/routes/invitations.d.ts +202 -84
- package/dist/apps/management/routes/invitations.d.ts.map +1 -0
- package/dist/apps/{platform → management}/routes/invitations.js +59 -1
- package/dist/apps/management/routes/invitations.js.map +1 -0
- package/dist/apps/{platform → management}/routes/me.d.ts +17 -17
- package/dist/apps/management/routes/me.d.ts.map +1 -0
- package/dist/apps/management/routes/me.js.map +1 -0
- package/dist/apps/management/routes/members.d.ts.map +1 -0
- package/dist/apps/management/routes/members.js.map +1 -0
- package/dist/apps/{platform → management}/routes/orgs.d.ts +50 -36
- package/dist/apps/management/routes/orgs.d.ts.map +1 -0
- package/dist/apps/{platform → management}/routes/orgs.js +14 -3
- package/dist/apps/management/routes/orgs.js.map +1 -0
- package/dist/apps/{platform → management}/routes/projects.d.ts +97 -97
- package/dist/apps/{platform → management}/routes/projects.d.ts.map +1 -1
- package/dist/apps/management/routes/projects.js.map +1 -0
- package/dist/apps/management/routes/scopes.d.ts +107 -0
- package/dist/apps/management/routes/scopes.d.ts.map +1 -0
- package/dist/apps/management/routes/scopes.js +66 -0
- package/dist/apps/management/routes/scopes.js.map +1 -0
- package/dist/apps/management/routes/usage.d.ts +588 -0
- package/dist/apps/management/routes/usage.d.ts.map +1 -0
- package/dist/apps/management/routes/usage.js +391 -0
- package/dist/apps/management/routes/usage.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/relay/App.d.ts +4 -1
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +118 -10
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
- package/dist/apps/relay/Sponsorships.js +2 -1
- package/dist/apps/relay/Sponsorships.js.map +1 -1
- package/dist/cli/Cli.d.ts +8 -1
- package/dist/cli/Cli.d.ts.map +1 -1
- package/dist/cli/Cli.js +11 -3
- package/dist/cli/Cli.js.map +1 -1
- package/dist/cli/commands/admin.d.ts +4 -0
- package/dist/cli/commands/admin.d.ts.map +1 -1
- package/dist/cli/commands/admin.js +80 -1
- package/dist/cli/commands/admin.js.map +1 -1
- package/dist/db/Db.d.ts +7 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +126 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +79 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +88 -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.js +2 -2
- package/dist/db/tables/invitations.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +6 -2
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +10 -4
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.js +2 -2
- package/dist/db/tables/projects.js.map +1 -1
- 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 +119 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +155 -2
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- 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.js +2 -2
- package/dist/db/tables/users.js.map +1 -1
- package/dist/handlers/internal/sponsorship.d.ts +5 -2
- package/dist/handlers/internal/sponsorship.d.ts.map +1 -1
- package/dist/handlers/internal/sponsorship.js +19 -5
- 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/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 +18 -4
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +149 -77
- 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 +915 -0
- package/dist/internal/FundingRoutes.d.ts.map +1 -0
- package/dist/internal/FundingRoutes.js +625 -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 +2 -0
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +3 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +10 -1
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +8 -1
- 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/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +3 -3
- package/dist/internal/Webhooks.js.map +1 -1
- package/package.json +8 -7
- package/src/ApiKey.ts +9 -2
- package/src/ApiKeys.test.ts +51 -0
- package/src/ApiKeys.ts +50 -1
- package/src/App.test.ts +33 -86718
- package/src/App.ts +26 -8
- package/src/Client.test-d.ts +353 -2
- package/src/Scope.ts +16 -3
- package/src/admin/apps/scopes.test.ts +6 -0
- package/src/admin/apps/scopes.ts +3 -0
- 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 +26 -13
- package/src/apps/data/App.test.ts +72 -0
- package/src/apps/data/App.ts +37 -0
- package/src/apps/data/FundingRouteProviders.test.ts +281 -0
- package/src/apps/data/FundingRouteProviders.ts +459 -0
- package/src/apps/data/index.ts +1 -0
- package/src/apps/data/routes/funding.test.ts +314 -0
- package/src/apps/data/routes/funding.ts +698 -0
- package/src/apps/data/routes/receipts.ts +2 -9
- package/src/apps/data/routes/rpc.test.ts +78 -2
- package/src/apps/data/routes/rpc.ts +31 -1
- package/src/apps/data/routes/webhooks.test.ts +1 -1
- package/src/apps/data/routes/webhooks.ts +11 -8
- package/src/apps/docs/App.ts +30 -101
- package/src/apps/docs/reference.ts +132 -0
- package/src/apps/index.ts +3 -2
- package/src/apps/{platform → management}/App.ts +56 -17
- package/src/apps/management/Billing.test.ts +530 -0
- package/src/apps/management/Billing.ts +682 -0
- package/src/apps/management/index.ts +2 -0
- package/src/apps/{platform → management}/routes/api-keys.test.ts +172 -9
- package/src/apps/{platform → management}/routes/api-keys.ts +137 -21
- package/src/apps/management/routes/billing.test.ts +1100 -0
- package/src/apps/management/routes/billing.ts +966 -0
- package/src/apps/{platform → management}/routes/invitations.test.ts +55 -7
- package/src/apps/{platform → management}/routes/invitations.ts +69 -1
- package/src/apps/management/routes/me.test.ts +351 -0
- package/src/apps/{platform → management}/routes/orgs.ts +26 -4
- package/src/apps/management/routes/scopes.test.ts +29 -0
- package/src/apps/management/routes/scopes.ts +85 -0
- package/src/apps/management/routes/usage.test.ts +422 -0
- package/src/apps/management/routes/usage.ts +527 -0
- package/src/apps/mcp.test.ts +206 -0
- package/src/apps/mcp.ts +250 -0
- package/src/apps/relay/App.test.ts +457 -8
- package/src/apps/relay/App.ts +120 -12
- package/src/apps/relay/Sponsorships.test.ts +1 -1
- package/src/apps/relay/Sponsorships.ts +2 -1
- package/src/cli/Cli.test.ts +25 -2
- package/src/cli/Cli.ts +17 -2
- package/src/cli/commands/admin.test.ts +83 -0
- package/src/cli/commands/admin.ts +89 -1
- package/src/db/Db.ts +137 -0
- package/src/db/Schema.ts +104 -2
- package/src/db/tables/billingSettings.test.ts +87 -0
- package/src/db/tables/billingSettings.ts +108 -0
- package/src/db/tables/invitations.ts +2 -2
- package/src/db/tables/organizations.test.ts +1 -1
- package/src/db/tables/organizations.ts +15 -5
- package/src/db/tables/projects.ts +2 -2
- package/src/db/tables/requestUsage.ts +231 -0
- package/src/db/tables/sponsoredTransactions.test.ts +108 -1
- package/src/db/tables/sponsoredTransactions.ts +230 -2
- package/src/db/tables/stripeCustomers.test.ts +109 -0
- package/src/db/tables/stripeCustomers.ts +162 -0
- package/src/db/tables/users.ts +2 -2
- package/src/handlers/internal/sponsorship.ts +34 -7
- package/src/handlers/internal/utils.ts +19 -7
- package/src/index.ts +1 -0
- package/src/internal/Auth.test.ts +175 -25
- package/src/internal/Auth.ts +176 -87
- package/src/internal/Cache.ts +5 -1
- 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.test.ts +213 -0
- package/src/internal/FundingRoutes.ts +898 -0
- package/src/internal/Id.test.ts +17 -0
- package/src/internal/Id.ts +24 -0
- package/src/internal/Log.test.ts +1 -0
- package/src/internal/Log.ts +5 -0
- package/src/internal/OpenApi.ts +12 -1
- 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/Webhooks.test.ts +3 -3
- package/src/internal/Webhooks.ts +3 -3
- package/dist/apps/platform/App.d.ts.map +0 -1
- package/dist/apps/platform/App.js.map +0 -1
- package/dist/apps/platform/index.d.ts +0 -2
- package/dist/apps/platform/index.d.ts.map +0 -1
- package/dist/apps/platform/index.js +0 -2
- package/dist/apps/platform/index.js.map +0 -1
- package/dist/apps/platform/routes/api-keys.d.ts.map +0 -1
- package/dist/apps/platform/routes/api-keys.js.map +0 -1
- package/dist/apps/platform/routes/invitations.d.ts.map +0 -1
- package/dist/apps/platform/routes/invitations.js.map +0 -1
- package/dist/apps/platform/routes/me.d.ts.map +0 -1
- package/dist/apps/platform/routes/me.js.map +0 -1
- package/dist/apps/platform/routes/members.d.ts.map +0 -1
- package/dist/apps/platform/routes/members.js.map +0 -1
- package/dist/apps/platform/routes/orgs.d.ts.map +0 -1
- package/dist/apps/platform/routes/orgs.js.map +0 -1
- package/dist/apps/platform/routes/projects.js.map +0 -1
- package/src/apps/platform/index.ts +0 -1
- package/src/apps/platform/routes/me.test.ts +0 -121
- /package/dist/apps/{platform → management}/routes/me.js +0 -0
- /package/dist/apps/{platform → management}/routes/members.d.ts +0 -0
- /package/dist/apps/{platform → management}/routes/members.js +0 -0
- /package/dist/apps/{platform → management}/routes/projects.js +0 -0
- /package/src/apps/{platform → management}/routes/me.ts +0 -0
- /package/src/apps/{platform → management}/routes/members.test.ts +0 -0
- /package/src/apps/{platform → management}/routes/members.ts +0 -0
- /package/src/apps/{platform → management}/routes/orgs.test.ts +0 -0
- /package/src/apps/{platform → management}/routes/projects.test.ts +0 -0
- /package/src/apps/{platform → management}/routes/projects.ts +0 -0
package/src/apps/relay/App.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
2
|
import { createMiddleware } from 'hono/factory'
|
|
3
|
-
import {
|
|
3
|
+
import { RpcResponse } from 'ox'
|
|
4
|
+
import { Addresses, TokenId, Transaction } from 'viem/tempo'
|
|
4
5
|
|
|
5
6
|
import type * as App from '../../App.js'
|
|
7
|
+
import * as BillingSettings from '../../db/tables/billingSettings.js'
|
|
6
8
|
import * as Db from '../../db/Db.js'
|
|
9
|
+
import * as Fees from '../../internal/Fees.js'
|
|
7
10
|
import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
|
|
11
|
+
import * as Billing from '../management/Billing.js'
|
|
8
12
|
import * as Handler from '../../Handler.js'
|
|
9
13
|
import * as Auth from '../../internal/Auth.js'
|
|
10
14
|
import * as OpenApi from '../../internal/OpenApi.js'
|
|
@@ -43,10 +47,13 @@ export declare namespace relay {
|
|
|
43
47
|
/** Options for the relay route group. */
|
|
44
48
|
type Options = {
|
|
45
49
|
feePayer?:
|
|
46
|
-
| Pick<
|
|
50
|
+
| (Pick<
|
|
47
51
|
NonNullable<Handler.relay.Options['feePayer']>,
|
|
48
52
|
'account' | 'feeToken' | 'name' | 'url'
|
|
49
|
-
>
|
|
53
|
+
> & {
|
|
54
|
+
/** Per-transaction fee cap (decimal USD string); orgs tighten it, never loosen. @default '1.00' */
|
|
55
|
+
txFeeLimit?: string | undefined
|
|
56
|
+
})
|
|
50
57
|
| undefined
|
|
51
58
|
}
|
|
52
59
|
}
|
|
@@ -56,6 +63,10 @@ export declare namespace relay {
|
|
|
56
63
|
// than closed over group locals; relay traffic is low-QPS and the viem
|
|
57
64
|
// clients are memoized upstream, so per-request handler construction is cheap.
|
|
58
65
|
function middleware(options: relay.Options = {}) {
|
|
66
|
+
// Platform-wide per-transaction cap; org settings tighten it, never loosen.
|
|
67
|
+
const txFeeLimit = options.feePayer
|
|
68
|
+
? Billing.toBaseUnits(options.feePayer.txFeeLimit ?? '1.00')
|
|
69
|
+
: 0n
|
|
59
70
|
return createMiddleware<App.Environment>(async (c) => {
|
|
60
71
|
const verifiedTokens = c.get('verifiedTokens')
|
|
61
72
|
|
|
@@ -76,20 +87,97 @@ function middleware(options: relay.Options = {}) {
|
|
|
76
87
|
principal?.type === 'api_key' &&
|
|
77
88
|
(principal.apiKey.scopes.includes('rpc-relay:sponsor') ||
|
|
78
89
|
principal.apiKey.scopes.includes(Scope.wildcard))
|
|
90
|
+
// Lifted so validation and the sponsored-token default read one value.
|
|
91
|
+
const feeToken_sponsor = options.feePayer.feeToken ?? Addresses.pathUsd
|
|
79
92
|
return {
|
|
80
93
|
...options.feePayer,
|
|
81
|
-
feeToken:
|
|
82
|
-
validate: (
|
|
94
|
+
feeToken: feeToken_sponsor,
|
|
95
|
+
validate: async (transaction) => {
|
|
83
96
|
// Allowlist the chains this deployment serves rather than denylisting
|
|
84
97
|
// only mainnet: the relay resolves chainId from the request body, so
|
|
85
|
-
// the query-string chain guard never sees it.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
98
|
+
// the query-string chain guard never sees it.
|
|
99
|
+
const id = Number(transaction.chainId)
|
|
100
|
+
if (!attribution || !sponsorable || !c.get('supportedChainIds').has(id)) return false
|
|
101
|
+
if (!Viem.isMainnet(id)) return true
|
|
102
|
+
// Mainnet is production-only: a sandbox key resolves chainId from the
|
|
103
|
+
// request body, so the auth-layer sandbox/mainnet guard never sees
|
|
104
|
+
// it. Refuse here, or sandbox spend would sponsor mainnet unbilled.
|
|
105
|
+
if (attribution.environment !== 'production') return false
|
|
106
|
+
|
|
107
|
+
// Enforcement reads hit the primary, never the cached replica: a
|
|
108
|
+
// stale `past_due`, a lowered limit, or another isolate's pending
|
|
109
|
+
// spend must not keep the gate open.
|
|
110
|
+
const db = Db.get(c.get('db'))
|
|
111
|
+
// Mainnet sponsorship requires an active billing source; deployments
|
|
112
|
+
// without billing configured have no rows, so mainnet stays closed.
|
|
113
|
+
// `past_due` gets its own reason: the caller must settle an invoice,
|
|
114
|
+
// not attach a card.
|
|
115
|
+
const billing = await Billing.status(db, attribution.orgId)
|
|
116
|
+
if (billing === 'past_due') return 'billing_past_due'
|
|
117
|
+
if (billing !== 'active') return 'billing_required'
|
|
118
|
+
|
|
119
|
+
const feeToken = (() => {
|
|
120
|
+
try {
|
|
121
|
+
return TokenId.toAddress(transaction.feeToken ?? feeToken_sponsor)
|
|
122
|
+
} catch {
|
|
123
|
+
return undefined
|
|
124
|
+
}
|
|
125
|
+
})()
|
|
126
|
+
const verified = feeToken
|
|
127
|
+
? (await VerifiedTokens.read(Db.get(c.get('dbCached')), id))?.byAddress.get(feeToken.toLowerCase()) // prettier-ignore
|
|
128
|
+
: undefined
|
|
129
|
+
if (
|
|
130
|
+
!verified ||
|
|
131
|
+
verified.currency.toLowerCase() !== 'usd' ||
|
|
132
|
+
verified.decimals !== Fees.tokenDecimals
|
|
133
|
+
)
|
|
134
|
+
// prettier-ignore
|
|
135
|
+
return 'fee_token_unsupported'
|
|
136
|
+
|
|
137
|
+
// Fail closed: a transaction whose signed cap cannot be computed
|
|
138
|
+
// cannot be bounded, so it never sponsors on mainnet.
|
|
139
|
+
const feeMax = Fees.maxOf(transaction)
|
|
140
|
+
if (feeMax === undefined) return 'tx_fee_limit_exceeded'
|
|
141
|
+
const settings = await BillingSettings.get(db, attribution.orgId)
|
|
142
|
+
const cap = (() => {
|
|
143
|
+
if (settings?.txFeeLimit == null) return txFeeLimit
|
|
144
|
+
const org = Billing.toBaseUnits(settings.txFeeLimit)
|
|
145
|
+
return org < txFeeLimit ? org : txFeeLimit
|
|
146
|
+
})()
|
|
147
|
+
if (feeMax > cap) return 'tx_fee_limit_exceeded'
|
|
148
|
+
// The period spend limit is enforced atomically at recording time
|
|
149
|
+
// (`onSponsored` → `SponsoredTransactions.reserve`); a check here
|
|
150
|
+
// would be a check-then-act race a burst could slip through.
|
|
151
|
+
return true
|
|
89
152
|
},
|
|
90
153
|
onSponsored: async (event) => {
|
|
91
154
|
if (!attribution) return
|
|
92
|
-
|
|
155
|
+
const mainnet = Viem.isMainnet(event.chainId)
|
|
156
|
+
// Defense in depth: `validate` already refuses non-production
|
|
157
|
+
// mainnet, so reaching here with that pair is a wiring bug.
|
|
158
|
+
if (mainnet && attribution.environment !== 'production')
|
|
159
|
+
throw new RpcResponse.InvalidParamsError({ message: 'Sponsorship rejected.' })
|
|
160
|
+
|
|
161
|
+
// The cap recomputes from canonical envelope bytes, mirroring the
|
|
162
|
+
// reconciliation convention; pending rows charge it to spend limits.
|
|
163
|
+
const feeMax = Fees.maxOf(Transaction.deserialize(event.transaction as `0x76${string}`))
|
|
164
|
+
const db = Db.get(c.get('db'))
|
|
165
|
+
|
|
166
|
+
const feeToken = (() => {
|
|
167
|
+
try {
|
|
168
|
+
return event.feeToken !== undefined ? TokenId.toAddress(event.feeToken) : undefined
|
|
169
|
+
} catch {
|
|
170
|
+
return undefined
|
|
171
|
+
}
|
|
172
|
+
})()
|
|
173
|
+
|
|
174
|
+
const currency = feeToken
|
|
175
|
+
? (await VerifiedTokens.read(Db.get(c.get('dbCached')), event.chainId))?.byAddress
|
|
176
|
+
.get(feeToken.toLowerCase())
|
|
177
|
+
?.currency.toLowerCase()
|
|
178
|
+
: undefined
|
|
179
|
+
|
|
180
|
+
const input = {
|
|
93
181
|
apiKeyId: attribution.apiKeyId,
|
|
94
182
|
billable: attribution.environment === 'production',
|
|
95
183
|
chainId: event.chainId,
|
|
@@ -98,9 +186,29 @@ function middleware(options: relay.Options = {}) {
|
|
|
98
186
|
projectId: attribution.projectId,
|
|
99
187
|
signPayload: event.signPayload,
|
|
100
188
|
transaction: event.transaction,
|
|
101
|
-
...(
|
|
189
|
+
...(currency ? { currency } : {}),
|
|
190
|
+
...(feeMax !== undefined ? { feeMax: feeMax.toString() } : {}),
|
|
191
|
+
...(feeToken ? { feeToken } : {}),
|
|
102
192
|
...(event.transactionHash ? { transactionHash: event.transactionHash } : {}),
|
|
103
|
-
}
|
|
193
|
+
}
|
|
194
|
+
// Re-read settings on the authoritative path: the limit may have
|
|
195
|
+
// changed since `validate`, and only mainnet spend is limited.
|
|
196
|
+
const settings = mainnet ? await BillingSettings.get(db, attribution.orgId) : undefined
|
|
197
|
+
if (settings?.spendLimit == null) return void (await SponsoredTransactions.upsert(db, input)) // prettier-ignore
|
|
198
|
+
try {
|
|
199
|
+
await SponsoredTransactions.reserve(db, input, {
|
|
200
|
+
chainIds: [Viem.chainId.mainnet],
|
|
201
|
+
max: Billing.toBaseUnits(settings.spendLimit),
|
|
202
|
+
since: Billing.periodStart(settings.period),
|
|
203
|
+
})
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof SponsoredTransactions.PeriodSpendLimitError)
|
|
206
|
+
throw new RpcResponse.InvalidParamsError({
|
|
207
|
+
data: { code: 'spend_limit_exceeded' },
|
|
208
|
+
message: 'Spend limit exceeded.',
|
|
209
|
+
})
|
|
210
|
+
throw error
|
|
211
|
+
}
|
|
104
212
|
},
|
|
105
213
|
} satisfies Handler.relay.Options['feePayer']
|
|
106
214
|
})()
|
|
@@ -49,7 +49,7 @@ describe('finalize', () => {
|
|
|
49
49
|
const finalized = await SponsoredTransactions.get(db, record.id)
|
|
50
50
|
expect(finalized?.status).toMatchInlineSnapshot(`"finalized"`)
|
|
51
51
|
// 21000 gas × 3 = 63000 base units.
|
|
52
|
-
expect(finalized?.feeAmount).toMatchInlineSnapshot(`"
|
|
52
|
+
expect(finalized?.feeAmount).toMatchInlineSnapshot(`"1"`)
|
|
53
53
|
expect(finalized?.finalizedAt).not.toBeNull()
|
|
54
54
|
await db.close()
|
|
55
55
|
})
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { getTransactionReceipt } from 'viem/actions'
|
|
11
11
|
|
|
12
12
|
import * as Db from '../../db/Db.js'
|
|
13
|
+
import * as Fees from '../../internal/Fees.js'
|
|
13
14
|
import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
|
|
14
15
|
import * as Tidx from '../../internal/Tidx.js'
|
|
15
16
|
import * as Viem from '../../internal/Viem.js'
|
|
@@ -67,7 +68,7 @@ export async function finalize(db: Db.Db, options: finalize.Options): Promise<fi
|
|
|
67
68
|
|
|
68
69
|
if (receipt) {
|
|
69
70
|
// A reverted transaction still finalizes: the fee payer paid its fees.
|
|
70
|
-
const feeAmount = receipt.gasUsed
|
|
71
|
+
const feeAmount = Fees.fromGas(receipt.gasUsed, receipt.effectiveGasPrice)
|
|
71
72
|
await SponsoredTransactions.finalize(db, row.id, {
|
|
72
73
|
feeAmount: feeAmount.toString(),
|
|
73
74
|
finalizedAt: now().toISOString(),
|
package/src/cli/Cli.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Cli as incur_Cli, z } from 'incur'
|
|
1
2
|
import { Cli } from 'tapimo'
|
|
2
3
|
|
|
3
4
|
import * as TestApp from '../../test/App.js'
|
|
@@ -10,7 +11,7 @@ describe('from', () => {
|
|
|
10
11
|
const fetch = mockHostedApi()
|
|
11
12
|
|
|
12
13
|
try {
|
|
13
|
-
const cli = Cli.from(apiUrl, { version: '1.2.3' })
|
|
14
|
+
const cli = Cli.from(apiUrl, { docs: false, version: '1.2.3' })
|
|
14
15
|
await serve(cli, ['--help'])
|
|
15
16
|
|
|
16
17
|
expect(cli.name).toMatchInlineSnapshot(`"tapimo"`)
|
|
@@ -28,7 +29,7 @@ describe('from', () => {
|
|
|
28
29
|
const fetch = mockHostedApi(TestApp.create({ db }))
|
|
29
30
|
|
|
30
31
|
try {
|
|
31
|
-
const cli = Cli.from(apiUrl)
|
|
32
|
+
const cli = Cli.from(apiUrl, { docs: false })
|
|
32
33
|
// `/v1/tokens?verified=true` now serves the curated verified list through
|
|
33
34
|
// the standard listing endpoint, so the CLI surfaces the filter as a
|
|
34
35
|
// `--verified` flag on the `v1 tokens get` command instead of routing
|
|
@@ -52,6 +53,28 @@ describe('from', () => {
|
|
|
52
53
|
fetch.mockRestore()
|
|
53
54
|
}
|
|
54
55
|
})
|
|
56
|
+
|
|
57
|
+
test('mounts the docs MCP source as a command group', async () => {
|
|
58
|
+
const fetch = mockHostedApi()
|
|
59
|
+
|
|
60
|
+
// Stub remote docs MCP server: an in-process incur CLI serving `/mcp`.
|
|
61
|
+
const remote = incur_Cli.create('docs-remote', { version: '1.0.0' }).command('search', {
|
|
62
|
+
description: 'Search docs',
|
|
63
|
+
options: z.object({ query: z.string().describe('Search query') }),
|
|
64
|
+
run: (context) => ({ hits: [context.options.query] }),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const cli = Cli.from(apiUrl, {
|
|
69
|
+
docs: { fetch: (request) => remote.fetch(request), url: 'http://docs.local/mcp' },
|
|
70
|
+
})
|
|
71
|
+
const output = await serve(cli, ['docs', 'search', '--query', 'fees', '--format', 'json'])
|
|
72
|
+
|
|
73
|
+
expect(JSON.parse(output)).toEqual({ hits: ['fees'] })
|
|
74
|
+
} finally {
|
|
75
|
+
fetch.mockRestore()
|
|
76
|
+
}
|
|
77
|
+
})
|
|
55
78
|
})
|
|
56
79
|
|
|
57
80
|
function mockHostedApi(app = TestApp.create()) {
|
package/src/cli/Cli.ts
CHANGED
|
@@ -1,29 +1,42 @@
|
|
|
1
1
|
import { Cli as incur_Cli, Fetch, type Openapi } from 'incur'
|
|
2
2
|
|
|
3
|
+
import type { mcp } from '../apps/mcp.js'
|
|
3
4
|
import * as Admin from './commands/admin.js'
|
|
4
5
|
|
|
6
|
+
/** Tool patterns hidden from MCP clients: super admin, partner-shaped gecko mirrors, browser session flows. */
|
|
7
|
+
const excludedTools = ['admin_*', 'gecko_*', 'v1_auth_*']
|
|
8
|
+
|
|
9
|
+
/** Usage guidance surfaced to MCP clients on initialize (`<cli> --mcp`). */
|
|
10
|
+
const instructions =
|
|
11
|
+
'Tools mirror the hosted Tempo API. Authenticated tools accept an `authorization` input (`Bearer <api key>`). List endpoints paginate via `cursor`/`nextCursor`. `docs_*` tools search the Tempo documentation.'
|
|
12
|
+
|
|
5
13
|
/**
|
|
6
14
|
* Creates an incur CLI backed by a hosted Tempo API, plus the super admin
|
|
7
|
-
* commands (`<cli> admin …`).
|
|
15
|
+
* commands (`<cli> admin …`) and the remote Tempo docs tools (`<cli> docs …`).
|
|
8
16
|
*/
|
|
9
17
|
export function from(url: string | URL, options: from.Options = {}) {
|
|
10
18
|
const {
|
|
11
19
|
description = 'Tempo API CLI',
|
|
20
|
+
docs = 'https://mcp.tempo.xyz/mcp',
|
|
12
21
|
name = 'tapimo',
|
|
13
22
|
openapi = 'openapi.json',
|
|
14
23
|
request,
|
|
15
24
|
version = '0.0.0',
|
|
16
25
|
} = options
|
|
17
26
|
|
|
18
|
-
|
|
27
|
+
const cli = incur_Cli
|
|
19
28
|
.create(name, {
|
|
20
29
|
description,
|
|
21
30
|
fetch: Fetch.fromRequest(url, request),
|
|
31
|
+
mcp: { instructions, tools: { exclude: excludedTools } },
|
|
22
32
|
openapi,
|
|
23
33
|
openapiConfig: options.openapiConfig ?? { mode: 'namespace' },
|
|
24
34
|
version,
|
|
25
35
|
})
|
|
26
36
|
.command(Admin.admin)
|
|
37
|
+
if (docs !== false)
|
|
38
|
+
cli.command('docs', { description: 'Search the Tempo documentation.', mcp: docs })
|
|
39
|
+
return cli
|
|
27
40
|
}
|
|
28
41
|
|
|
29
42
|
export declare namespace from {
|
|
@@ -31,6 +44,8 @@ export declare namespace from {
|
|
|
31
44
|
type Options = {
|
|
32
45
|
/** CLI description. */
|
|
33
46
|
description?: string | undefined
|
|
47
|
+
/** Remote docs MCP source mounted as the `docs` command group, or `false` to omit it. @default 'https://mcp.tempo.xyz/mcp' */
|
|
48
|
+
docs?: mcp.Source | false | undefined
|
|
34
49
|
/** CLI name. */
|
|
35
50
|
name?: string | undefined
|
|
36
51
|
/** OpenAPI document source. Relative paths resolve from the API URL. */
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid'
|
|
2
2
|
|
|
3
3
|
import * as TestCli from '../../../test/cli.js'
|
|
4
|
+
import * as Analytics from '../../analytics/Analytics.js'
|
|
5
|
+
|
|
6
|
+
afterEach(() => vi.unstubAllGlobals())
|
|
4
7
|
|
|
5
8
|
describe('admin', () => {
|
|
6
9
|
describe('migrate', () => {
|
|
@@ -67,5 +70,85 @@ describe('admin', () => {
|
|
|
67
70
|
expect(code).toBe(1)
|
|
68
71
|
expect(envelope.error.code).toBe('migrate_failed')
|
|
69
72
|
})
|
|
73
|
+
|
|
74
|
+
test('behavior: applies ClickHouse analytics migrations when configured', async () => {
|
|
75
|
+
const fetch = vi
|
|
76
|
+
.fn<typeof globalThis.fetch>()
|
|
77
|
+
.mockResolvedValue(new Response('', { status: 200 }))
|
|
78
|
+
vi.stubGlobal('fetch', fetch)
|
|
79
|
+
|
|
80
|
+
const { code, envelope } = await TestCli.run(
|
|
81
|
+
['admin', 'migrate', '--db-schema', `t_${nanoid()}`],
|
|
82
|
+
{
|
|
83
|
+
CLICKHOUSE_DATABASE: 'tempo_api',
|
|
84
|
+
CLICKHOUSE_MIGRATE_PASSWORD: 'ddl-secret',
|
|
85
|
+
CLICKHOUSE_MIGRATE_USER: 'ddl',
|
|
86
|
+
CLICKHOUSE_URL: 'https://clickhouse.test',
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
expect(code).toBe(0)
|
|
90
|
+
expect(envelope).toEqual({ data: { analytics: true, migrated: true }, ok: true })
|
|
91
|
+
const url = new URL(String(fetch.mock.calls[0]![0]))
|
|
92
|
+
expect(url.searchParams.get('database')).toBe('tempo_api')
|
|
93
|
+
const bodies = fetch.mock.calls.map((call) => String(call[1]?.body))
|
|
94
|
+
for (const migration of Analytics.migrations) expect(bodies).toContain(migration.sql)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('behavior: skips analytics migrations when ClickHouse env is unset or empty', async () => {
|
|
98
|
+
const fetch = vi.fn<typeof globalThis.fetch>()
|
|
99
|
+
vi.stubGlobal('fetch', fetch)
|
|
100
|
+
|
|
101
|
+
const { code, envelope } = await TestCli.run(
|
|
102
|
+
['admin', 'migrate', '--db-schema', `t_${nanoid()}`],
|
|
103
|
+
{
|
|
104
|
+
CLICKHOUSE_DATABASE: '',
|
|
105
|
+
CLICKHOUSE_MIGRATE_PASSWORD: '',
|
|
106
|
+
CLICKHOUSE_MIGRATE_USER: '',
|
|
107
|
+
CLICKHOUSE_URL: '',
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
expect(code).toBe(0)
|
|
111
|
+
expect(envelope).toEqual({ data: { migrated: true }, ok: true })
|
|
112
|
+
expect(fetch).not.toHaveBeenCalled()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('error: partial ClickHouse config exits 1', async () => {
|
|
116
|
+
const { code, envelope } = await TestCli.run(['admin', 'migrate'], {
|
|
117
|
+
CLICKHOUSE_MIGRATE_PASSWORD: 'ddl-secret',
|
|
118
|
+
})
|
|
119
|
+
expect(code).toBe(1)
|
|
120
|
+
expect(envelope).toEqual({
|
|
121
|
+
error: {
|
|
122
|
+
code: 'missing_clickhouse_config',
|
|
123
|
+
message:
|
|
124
|
+
'ClickHouse analytics migration is partially configured; set CLICKHOUSE_DATABASE, CLICKHOUSE_MIGRATE_USER, CLICKHOUSE_URL.',
|
|
125
|
+
},
|
|
126
|
+
ok: false,
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('error: a ClickHouse migration failure exits 1', async () => {
|
|
131
|
+
vi.stubGlobal(
|
|
132
|
+
'fetch',
|
|
133
|
+
vi.fn<typeof globalThis.fetch>().mockResolvedValue(new Response('denied', { status: 403 })),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
const { code, envelope } = await TestCli.run([
|
|
137
|
+
'admin',
|
|
138
|
+
'migrate',
|
|
139
|
+
'--clickhouse-database',
|
|
140
|
+
'tempo_api',
|
|
141
|
+
'--clickhouse-migrate-password',
|
|
142
|
+
'ddl-secret',
|
|
143
|
+
'--clickhouse-migrate-user',
|
|
144
|
+
'ddl',
|
|
145
|
+
'--clickhouse-url',
|
|
146
|
+
'https://clickhouse.test',
|
|
147
|
+
'--db-schema',
|
|
148
|
+
`t_${nanoid()}`,
|
|
149
|
+
])
|
|
150
|
+
expect(code).toBe(1)
|
|
151
|
+
expect(envelope.error.code).toBe('migrate_failed')
|
|
152
|
+
})
|
|
70
153
|
})
|
|
71
154
|
})
|
|
@@ -1,21 +1,53 @@
|
|
|
1
1
|
import { Cli as incur_Cli, z } from 'incur'
|
|
2
2
|
|
|
3
|
+
import * as Analytics from '../../analytics/Analytics.js'
|
|
3
4
|
import * as Db from '../../db/Db.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Applies pending Postgres migrations, connecting directly via
|
|
7
8
|
* `--database-url` or `DATABASE_URL` (never through Hyperdrive — the migrate
|
|
8
9
|
* advisory lock needs session semantics); `--db-schema` scopes the session.
|
|
10
|
+
* When ClickHouse migrate credentials are configured, also applies analytics
|
|
11
|
+
* schema migrations ({@link Analytics.migrations}) over the HTTP interface.
|
|
9
12
|
*/
|
|
10
13
|
const migrate = incur_Cli.create('migrate', {
|
|
11
14
|
description: 'Apply pending database migrations.',
|
|
12
15
|
env: z.object({
|
|
16
|
+
CLICKHOUSE_DATABASE: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('ClickHouse database for analytics migrations.'),
|
|
20
|
+
CLICKHOUSE_MIGRATE_PASSWORD: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Password for the DDL-capable ClickHouse user.'),
|
|
24
|
+
CLICKHOUSE_MIGRATE_USER: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('DDL-capable ClickHouse user for analytics migrations.'),
|
|
28
|
+
CLICKHOUSE_URL: z.string().optional().describe('ClickHouse HTTPS endpoint.'),
|
|
13
29
|
DATABASE_URL: z
|
|
14
30
|
.string()
|
|
15
31
|
.optional()
|
|
16
32
|
.describe('Postgres connection string. Used when --database-url is omitted.'),
|
|
17
33
|
}),
|
|
18
34
|
options: z.object({
|
|
35
|
+
clickhouseDatabase: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('ClickHouse database. Overrides CLICKHOUSE_DATABASE.'),
|
|
39
|
+
clickhouseMigratePassword: z
|
|
40
|
+
.string()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('ClickHouse DDL password. Overrides CLICKHOUSE_MIGRATE_PASSWORD.'),
|
|
43
|
+
clickhouseMigrateUser: z
|
|
44
|
+
.string()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('ClickHouse DDL user. Overrides CLICKHOUSE_MIGRATE_USER.'),
|
|
47
|
+
clickhouseUrl: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe('ClickHouse HTTPS endpoint. Overrides CLICKHOUSE_URL.'),
|
|
19
51
|
databaseUrl: z
|
|
20
52
|
.string()
|
|
21
53
|
.optional()
|
|
@@ -29,13 +61,25 @@ const migrate = incur_Cli.create('migrate', {
|
|
|
29
61
|
code: 'missing_database_url',
|
|
30
62
|
message: 'Set --database-url or DATABASE_URL to the Postgres connection string.',
|
|
31
63
|
})
|
|
64
|
+
|
|
65
|
+
const analytics = clickhouseConfig({
|
|
66
|
+
database: c.options.clickhouseDatabase ?? c.env.CLICKHOUSE_DATABASE,
|
|
67
|
+
password: c.options.clickhouseMigratePassword ?? c.env.CLICKHOUSE_MIGRATE_PASSWORD,
|
|
68
|
+
url: c.options.clickhouseUrl ?? c.env.CLICKHOUSE_URL,
|
|
69
|
+
user: c.options.clickhouseMigrateUser ?? c.env.CLICKHOUSE_MIGRATE_USER,
|
|
70
|
+
})
|
|
71
|
+
if (analytics?.missing)
|
|
72
|
+
return c.error({
|
|
73
|
+
code: 'missing_clickhouse_config',
|
|
74
|
+
message: `ClickHouse analytics migration is partially configured; set ${analytics.missing.join(', ')}.`,
|
|
75
|
+
})
|
|
76
|
+
|
|
32
77
|
const db = Db.postgres({
|
|
33
78
|
connectionString,
|
|
34
79
|
...(c.options.dbSchema ? { schema: c.options.dbSchema } : {}),
|
|
35
80
|
})
|
|
36
81
|
try {
|
|
37
82
|
await db.migrate()
|
|
38
|
-
return { migrated: true }
|
|
39
83
|
} catch (error) {
|
|
40
84
|
/* v8 ignore next */
|
|
41
85
|
const message = error instanceof Error ? error.message : 'Migration failed.'
|
|
@@ -46,6 +90,19 @@ const migrate = incur_Cli.create('migrate', {
|
|
|
46
90
|
} finally {
|
|
47
91
|
await db.close()
|
|
48
92
|
}
|
|
93
|
+
if (!analytics) return { migrated: true }
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await Analytics.clickhouse(analytics.options).migrate()
|
|
97
|
+
} catch (error) {
|
|
98
|
+
/* v8 ignore next */
|
|
99
|
+
const message = error instanceof Error ? error.message : 'Migration failed.'
|
|
100
|
+
return c.error({
|
|
101
|
+
code: 'migrate_failed',
|
|
102
|
+
message,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
return { analytics: true, migrated: true }
|
|
49
106
|
},
|
|
50
107
|
})
|
|
51
108
|
|
|
@@ -58,3 +115,34 @@ export const admin = incur_Cli
|
|
|
58
115
|
description: 'Super admin commands for a Tempo API deployment.',
|
|
59
116
|
})
|
|
60
117
|
.command(migrate)
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Resolves ClickHouse migrate config. Credentials gate the migration (absent
|
|
121
|
+
* → skip); empty values count as unset since CI renders missing secrets as
|
|
122
|
+
* empty strings. Partial config returns the missing variable names instead.
|
|
123
|
+
*/
|
|
124
|
+
function clickhouseConfig(input: {
|
|
125
|
+
database?: string | undefined
|
|
126
|
+
password?: string | undefined
|
|
127
|
+
url?: string | undefined
|
|
128
|
+
user?: string | undefined
|
|
129
|
+
}):
|
|
130
|
+
| { missing: readonly string[]; options?: undefined }
|
|
131
|
+
| { missing?: undefined; options: Analytics.clickhouse.Options }
|
|
132
|
+
| undefined {
|
|
133
|
+
const database = input.database || undefined
|
|
134
|
+
const password = input.password || undefined
|
|
135
|
+
const url = input.url || undefined
|
|
136
|
+
const user = input.user || undefined
|
|
137
|
+
if (password === undefined && user === undefined) return undefined
|
|
138
|
+
if (database === undefined || password === undefined || url === undefined || user === undefined)
|
|
139
|
+
return {
|
|
140
|
+
missing: [
|
|
141
|
+
...(database === undefined ? ['CLICKHOUSE_DATABASE'] : []),
|
|
142
|
+
...(password === undefined ? ['CLICKHOUSE_MIGRATE_PASSWORD'] : []),
|
|
143
|
+
...(user === undefined ? ['CLICKHOUSE_MIGRATE_USER'] : []),
|
|
144
|
+
...(url === undefined ? ['CLICKHOUSE_URL'] : []),
|
|
145
|
+
],
|
|
146
|
+
}
|
|
147
|
+
return { options: { database, password, url, user } }
|
|
148
|
+
}
|