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
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import type * as Log from '../../internal/Log.js'
|
|
2
|
+
import type * as Metrics from '../../Metrics.js'
|
|
3
|
+
import type * as Analytics from '../Analytics.js'
|
|
4
|
+
|
|
5
|
+
/** Columns of the `request_events` table: one compact row per API request. */
|
|
6
|
+
export type Table = {
|
|
7
|
+
/**
|
|
8
|
+
* Whether the owning org's billing was active at request time (`1`/`0`), for
|
|
9
|
+
* authenticated API-key callers; `null` otherwise. The request-metered
|
|
10
|
+
* billing gate for sandbox: only `1` rows bill.
|
|
11
|
+
*/
|
|
12
|
+
billing_active: number | null
|
|
13
|
+
/** Total request duration in milliseconds. */
|
|
14
|
+
duration_ms: number
|
|
15
|
+
/** Deployment environment that served the request. */
|
|
16
|
+
environment: string
|
|
17
|
+
/** Stable API error code, when the response was an error envelope. */
|
|
18
|
+
error_code: string | null
|
|
19
|
+
/** API-key environment for authenticated API-key callers. */
|
|
20
|
+
key_environment: 'production' | 'sandbox' | null
|
|
21
|
+
/** API key id for authenticated API-key callers. */
|
|
22
|
+
key_id: string | null
|
|
23
|
+
/** HTTP method. */
|
|
24
|
+
method: string
|
|
25
|
+
/** Organization id for authenticated API-key callers. */
|
|
26
|
+
org_id: string | null
|
|
27
|
+
/** Project id for authenticated API-key callers. */
|
|
28
|
+
project_id: string | null
|
|
29
|
+
/** Caller principal kind. */
|
|
30
|
+
principal_type: 'api_key' | 'public' | 'session' | 'super_admin' | 'unknown'
|
|
31
|
+
/** Rate-limit bucket scope, when a rate limit was applied. */
|
|
32
|
+
rate_limit_scope: string | null
|
|
33
|
+
/** Stable per-request id (`entry.requestId`); dedups the billing count via `uniqExact`. */
|
|
34
|
+
request_id: string | null
|
|
35
|
+
/** Bounded matched route pattern. */
|
|
36
|
+
route: string
|
|
37
|
+
/** Service that emitted the event. */
|
|
38
|
+
service: string
|
|
39
|
+
/** HTTP response status code. */
|
|
40
|
+
status: number
|
|
41
|
+
/** UTC request completion timestamp formatted for ClickHouse DateTime64. */
|
|
42
|
+
timestamp: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One request attribution term for usage reads. */
|
|
46
|
+
export type Attribution = {
|
|
47
|
+
/** API key ids matched by this term; absent means all keys. */
|
|
48
|
+
apiKeyIds?: readonly string[] | undefined
|
|
49
|
+
/** API-key environment matched by this term; absent means all environments. */
|
|
50
|
+
environment?: Environment | undefined
|
|
51
|
+
/**
|
|
52
|
+
* Project scope for this term: a project id matches that project, `null`
|
|
53
|
+
* matches rows without project attribution, and `undefined` matches the
|
|
54
|
+
* whole organization (any project, including unattributed rows).
|
|
55
|
+
*/
|
|
56
|
+
projectId?: string | null | undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** API-key environment for request usage. */
|
|
60
|
+
export type Environment = 'production' | 'sandbox'
|
|
61
|
+
|
|
62
|
+
/** API key usage grouped for one project. */
|
|
63
|
+
export type KeyBreakdown = {
|
|
64
|
+
/** API key id (`key_…`). */
|
|
65
|
+
apiKeyId: string
|
|
66
|
+
/** Average request duration in milliseconds. */
|
|
67
|
+
averageDurationMs: number
|
|
68
|
+
/** Requests whose status was at least 400. */
|
|
69
|
+
errors: number
|
|
70
|
+
/** Total requests. */
|
|
71
|
+
requests: number
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Time bucket size for the usage series. */
|
|
75
|
+
export type Interval = 'day' | 'hour'
|
|
76
|
+
|
|
77
|
+
/** Route usage grouped for one project. */
|
|
78
|
+
export type RouteBreakdown = {
|
|
79
|
+
/** Average request duration in milliseconds. */
|
|
80
|
+
averageDurationMs: number
|
|
81
|
+
/** Requests whose status was at least 400. */
|
|
82
|
+
errors: number
|
|
83
|
+
/** Total requests. */
|
|
84
|
+
requests: number
|
|
85
|
+
/** Matched route pattern. */
|
|
86
|
+
route: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** One usage series point. */
|
|
90
|
+
export type SeriesPoint = {
|
|
91
|
+
/** Requests whose status was at least 400. */
|
|
92
|
+
errors: number
|
|
93
|
+
/** Total requests. */
|
|
94
|
+
requests: number
|
|
95
|
+
/** Bucket start timestamp (ISO 8601). */
|
|
96
|
+
time: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Status-code usage grouped for one project. */
|
|
100
|
+
export type StatusBreakdown = {
|
|
101
|
+
/** Total requests. */
|
|
102
|
+
requests: number
|
|
103
|
+
/** HTTP status code. */
|
|
104
|
+
status: number
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Overall usage totals for one project. */
|
|
108
|
+
export type Totals = {
|
|
109
|
+
/** Average request duration in milliseconds. */
|
|
110
|
+
averageDurationMs: number
|
|
111
|
+
/** Requests whose status was at least 400. */
|
|
112
|
+
errors: number
|
|
113
|
+
/** Total requests. */
|
|
114
|
+
requests: number
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Usage read options for one project. */
|
|
118
|
+
export type ProjectUsageOptions = {
|
|
119
|
+
/** Attribution terms that define which request rows count. */
|
|
120
|
+
attributions: readonly Attribution[]
|
|
121
|
+
/** Inclusive lower timestamp bound (ISO 8601). */
|
|
122
|
+
from: string
|
|
123
|
+
/** Bucket size for the time series. */
|
|
124
|
+
interval: Interval
|
|
125
|
+
/** Organization id (`org_…`). */
|
|
126
|
+
orgId: string
|
|
127
|
+
/** Inclusive upper timestamp bound (ISO 8601). */
|
|
128
|
+
to: string
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Project usage response body. */
|
|
132
|
+
export type ProjectUsage = {
|
|
133
|
+
/** Usage grouped by API key. */
|
|
134
|
+
byKey: readonly KeyBreakdown[]
|
|
135
|
+
/** Usage grouped by route. */
|
|
136
|
+
byRoute: readonly RouteBreakdown[]
|
|
137
|
+
/** Usage grouped by status code. */
|
|
138
|
+
byStatus: readonly StatusBreakdown[]
|
|
139
|
+
/** Inclusive lower timestamp bound used for the read (ISO 8601). */
|
|
140
|
+
from: string
|
|
141
|
+
/** Bucket size used for the time series. */
|
|
142
|
+
interval: Interval
|
|
143
|
+
/** Usage grouped into small time buckets. */
|
|
144
|
+
series: readonly SeriesPoint[]
|
|
145
|
+
/** Inclusive upper timestamp bound used for the read (ISO 8601). */
|
|
146
|
+
to: string
|
|
147
|
+
/** Overall usage totals. */
|
|
148
|
+
totals: Totals
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Builds a `Metrics.cloudflare` analytics factory that enqueues one compact
|
|
153
|
+
* request row per entry onto `queue`. Pass the result directly as the
|
|
154
|
+
* backend's `analytics` option; it reuses the backend's
|
|
155
|
+
* `enabled`/`environment`/`service` context and self-disables when `enabled`
|
|
156
|
+
* is false.
|
|
157
|
+
*
|
|
158
|
+
* @param queue - The analytics queue producer.
|
|
159
|
+
* @returns The analytics factory.
|
|
160
|
+
*/
|
|
161
|
+
export function createQueueSink(queue: createQueueSink.Queue): createQueueSink.Factory {
|
|
162
|
+
return (context) => async (entry) => {
|
|
163
|
+
if (!context.enabled) return
|
|
164
|
+
const row = event(entry, context)
|
|
165
|
+
try {
|
|
166
|
+
await queue.send(row)
|
|
167
|
+
} catch {
|
|
168
|
+
// Transient Queues internal errors (e.g. code 15000) usually clear on retry.
|
|
169
|
+
await queue.send(row)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export declare namespace createQueueSink {
|
|
175
|
+
/** Analytics factory accepted by `Metrics.cloudflare`'s `analytics` option. */
|
|
176
|
+
type Factory = (context: Metrics.cloudflare.Context) => Log.Emit
|
|
177
|
+
|
|
178
|
+
/** Minimal Cloudflare Queue producer shape the sink sends rows to. */
|
|
179
|
+
type Queue = {
|
|
180
|
+
/** Enqueues one row; the result is awaited and discarded. */
|
|
181
|
+
send(message: Table): Promise<unknown>
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Inserts a batch of queued request rows, acking each message on success and
|
|
187
|
+
* retrying the batch on failure.
|
|
188
|
+
*
|
|
189
|
+
* @param analytics - The analytics store.
|
|
190
|
+
* @param messages - The queued messages.
|
|
191
|
+
*/
|
|
192
|
+
export async function insertMessages(
|
|
193
|
+
analytics: Analytics.Analytics,
|
|
194
|
+
messages: readonly insertMessages.Message[],
|
|
195
|
+
): Promise<void> {
|
|
196
|
+
if (messages.length === 0) return
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await analytics.insert(
|
|
200
|
+
'request_events',
|
|
201
|
+
messages.map((message) => message.body),
|
|
202
|
+
)
|
|
203
|
+
for (const message of messages) message.ack()
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error('ClickHouse analytics insert failed', error)
|
|
206
|
+
for (const message of messages) message.retry()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export declare namespace insertMessages {
|
|
211
|
+
/** Minimal Cloudflare Queue message shape carrying one row. */
|
|
212
|
+
type Message = {
|
|
213
|
+
/** Acknowledges the message. */
|
|
214
|
+
ack(): void
|
|
215
|
+
/** Queued row. */
|
|
216
|
+
body: Table
|
|
217
|
+
/** Marks the message for redelivery. */
|
|
218
|
+
retry(): void
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Reads request usage for one project from the `request_events` table.
|
|
224
|
+
*
|
|
225
|
+
* @param analytics - The analytics store.
|
|
226
|
+
* @param options - The usage read options.
|
|
227
|
+
* @returns The project usage.
|
|
228
|
+
*/
|
|
229
|
+
export async function readProjectUsage(
|
|
230
|
+
analytics: Analytics.Analytics,
|
|
231
|
+
options: ProjectUsageOptions,
|
|
232
|
+
): Promise<ProjectUsage> {
|
|
233
|
+
const where = usageWhere(options)
|
|
234
|
+
const average = `toFloat64(coalesce(round(avgOrNull(duration_ms), 1), 0))`
|
|
235
|
+
const bucket = options.interval === 'day' ? 'toStartOfDay(timestamp)' : 'toStartOfHour(timestamp)'
|
|
236
|
+
const [byKey, byRoute, byStatus, series, totals] = await Promise.all([
|
|
237
|
+
analytics.query<KeyRow>(
|
|
238
|
+
`
|
|
239
|
+
SELECT
|
|
240
|
+
key_id,
|
|
241
|
+
count() AS requests,
|
|
242
|
+
countIf(status >= 400) AS errors,
|
|
243
|
+
${average} AS averageDurationMs
|
|
244
|
+
FROM ${table}
|
|
245
|
+
WHERE ${where} AND key_id IS NOT NULL
|
|
246
|
+
GROUP BY key_id
|
|
247
|
+
ORDER BY requests DESC, key_id ASC
|
|
248
|
+
LIMIT 100
|
|
249
|
+
`,
|
|
250
|
+
),
|
|
251
|
+
analytics.query<RouteRow>(
|
|
252
|
+
`
|
|
253
|
+
SELECT
|
|
254
|
+
route,
|
|
255
|
+
count() AS requests,
|
|
256
|
+
countIf(status >= 400) AS errors,
|
|
257
|
+
${average} AS averageDurationMs
|
|
258
|
+
FROM ${table}
|
|
259
|
+
WHERE ${where}
|
|
260
|
+
GROUP BY route
|
|
261
|
+
ORDER BY requests DESC, route ASC
|
|
262
|
+
LIMIT 100
|
|
263
|
+
`,
|
|
264
|
+
),
|
|
265
|
+
analytics.query<StatusRow>(
|
|
266
|
+
`
|
|
267
|
+
SELECT
|
|
268
|
+
status,
|
|
269
|
+
count() AS requests
|
|
270
|
+
FROM ${table}
|
|
271
|
+
WHERE ${where}
|
|
272
|
+
GROUP BY status
|
|
273
|
+
ORDER BY status ASC
|
|
274
|
+
`,
|
|
275
|
+
),
|
|
276
|
+
analytics.query<SeriesRow>(
|
|
277
|
+
`
|
|
278
|
+
SELECT
|
|
279
|
+
formatDateTime(${bucket}, '%Y-%m-%dT%H:%i:%S.000Z', 'UTC') AS time,
|
|
280
|
+
count() AS requests,
|
|
281
|
+
countIf(status >= 400) AS errors
|
|
282
|
+
FROM ${table}
|
|
283
|
+
WHERE ${where}
|
|
284
|
+
GROUP BY ${bucket}
|
|
285
|
+
ORDER BY ${bucket} ASC
|
|
286
|
+
`,
|
|
287
|
+
),
|
|
288
|
+
analytics.query<TotalsRow>(
|
|
289
|
+
`
|
|
290
|
+
SELECT
|
|
291
|
+
count() AS requests,
|
|
292
|
+
countIf(status >= 400) AS errors,
|
|
293
|
+
${average} AS averageDurationMs
|
|
294
|
+
FROM ${table}
|
|
295
|
+
WHERE ${where}
|
|
296
|
+
`,
|
|
297
|
+
),
|
|
298
|
+
])
|
|
299
|
+
return {
|
|
300
|
+
byKey: byKey.map((row) => ({
|
|
301
|
+
apiKeyId: String(row.key_id),
|
|
302
|
+
averageDurationMs: number(row.averageDurationMs),
|
|
303
|
+
errors: number(row.errors),
|
|
304
|
+
requests: number(row.requests),
|
|
305
|
+
})),
|
|
306
|
+
byRoute: byRoute.map((row) => ({
|
|
307
|
+
averageDurationMs: number(row.averageDurationMs),
|
|
308
|
+
errors: number(row.errors),
|
|
309
|
+
requests: number(row.requests),
|
|
310
|
+
route: String(row.route),
|
|
311
|
+
})),
|
|
312
|
+
byStatus: byStatus.map((row) => ({
|
|
313
|
+
requests: number(row.requests),
|
|
314
|
+
status: number(row.status),
|
|
315
|
+
})),
|
|
316
|
+
from: options.from,
|
|
317
|
+
interval: options.interval,
|
|
318
|
+
series: series.map((row) => ({
|
|
319
|
+
errors: number(row.errors),
|
|
320
|
+
requests: number(row.requests),
|
|
321
|
+
time: String(row.time),
|
|
322
|
+
})),
|
|
323
|
+
to: options.to,
|
|
324
|
+
totals: totals[0]
|
|
325
|
+
? {
|
|
326
|
+
averageDurationMs: number(totals[0].averageDurationMs),
|
|
327
|
+
errors: number(totals[0].errors),
|
|
328
|
+
requests: number(totals[0].requests),
|
|
329
|
+
}
|
|
330
|
+
: { averageDurationMs: 0, errors: 0, requests: 0 },
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** One org's deduped billable request count for a single UTC hour. */
|
|
335
|
+
export type BillableCount = {
|
|
336
|
+
/** UTC hour start (ISO 8601). */
|
|
337
|
+
bucketStart: string
|
|
338
|
+
/** Deduped billable request count (`uniqExact(request_id)`). */
|
|
339
|
+
count: number
|
|
340
|
+
/** Organization id (`org_…`). */
|
|
341
|
+
orgId: string
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Options for {@link readBillableCounts}. */
|
|
345
|
+
export type BillableCountsOptions = {
|
|
346
|
+
/** API-key environment to bill; `sandbox` rows require request-time billing. */
|
|
347
|
+
environment: Environment
|
|
348
|
+
/** Inclusive lower timestamp bound (ISO 8601). */
|
|
349
|
+
from: string
|
|
350
|
+
/** Exclusive upper timestamp bound (ISO 8601); the seal cutoff. */
|
|
351
|
+
to: string
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Reads deduped billable request counts grouped by `(org, UTC hour)` for the
|
|
356
|
+
* request-metered billing reporter. Counts dedupe on `request_id` via
|
|
357
|
+
* `uniqExact` (the queue → ClickHouse path can duplicate rows); production rows
|
|
358
|
+
* always bill, sandbox rows only when `billing_active = 1` at request time.
|
|
359
|
+
*
|
|
360
|
+
* @param analytics - The analytics store.
|
|
361
|
+
* @param options - The read options.
|
|
362
|
+
* @returns One deduped count per `(org, hour)`.
|
|
363
|
+
*/
|
|
364
|
+
export async function readBillableCounts(
|
|
365
|
+
analytics: Analytics.Analytics,
|
|
366
|
+
options: BillableCountsOptions,
|
|
367
|
+
): Promise<BillableCount[]> {
|
|
368
|
+
const sandboxGate = options.environment === 'production' ? '' : `AND billing_active = 1`
|
|
369
|
+
const rows = await analytics.query<BillableCountRow>(
|
|
370
|
+
`
|
|
371
|
+
SELECT
|
|
372
|
+
org_id AS orgId,
|
|
373
|
+
formatDateTime(toStartOfHour(timestamp), '%Y-%m-%dT%H:%i:%S.000Z', 'UTC') AS bucketStart,
|
|
374
|
+
uniqExact(request_id) AS count
|
|
375
|
+
FROM ${table}
|
|
376
|
+
WHERE principal_type = 'api_key'
|
|
377
|
+
AND org_id IS NOT NULL
|
|
378
|
+
AND request_id IS NOT NULL
|
|
379
|
+
AND key_environment = ${literal(options.environment)}
|
|
380
|
+
${sandboxGate}
|
|
381
|
+
AND timestamp >= parseDateTime64BestEffort(${literal(options.from)}, 3, 'UTC')
|
|
382
|
+
AND timestamp < parseDateTime64BestEffort(${literal(options.to)}, 3, 'UTC')
|
|
383
|
+
GROUP BY org_id, toStartOfHour(timestamp)
|
|
384
|
+
`,
|
|
385
|
+
)
|
|
386
|
+
return rows.map((row) => ({
|
|
387
|
+
bucketStart: String(row.bucketStart),
|
|
388
|
+
count: number(row.count),
|
|
389
|
+
orgId: String(row.orgId),
|
|
390
|
+
}))
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
type BillableCountRow = {
|
|
394
|
+
bucketStart: string
|
|
395
|
+
count: number | string
|
|
396
|
+
orgId: string
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Table name queries read from; the store scopes the database. */
|
|
400
|
+
const table = 'request_events'
|
|
401
|
+
|
|
402
|
+
type KeyRow = {
|
|
403
|
+
averageDurationMs: number | string
|
|
404
|
+
errors: number | string
|
|
405
|
+
key_id: string
|
|
406
|
+
requests: number | string
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
type RouteRow = {
|
|
410
|
+
averageDurationMs: number | string
|
|
411
|
+
errors: number | string
|
|
412
|
+
requests: number | string
|
|
413
|
+
route: string
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
type SeriesRow = {
|
|
417
|
+
errors: number | string
|
|
418
|
+
requests: number | string
|
|
419
|
+
time: string
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
type StatusRow = {
|
|
423
|
+
requests: number | string
|
|
424
|
+
status: number | string
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
type TotalsRow = {
|
|
428
|
+
averageDurationMs: number | string
|
|
429
|
+
errors: number | string
|
|
430
|
+
requests: number | string
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Maps one request log entry to a stored row. */
|
|
434
|
+
function event(entry: Log.Entry, context: Metrics.cloudflare.Context): Table {
|
|
435
|
+
const principal = entry.principal
|
|
436
|
+
return {
|
|
437
|
+
billing_active: principal?.type === 'api_key' ? Number(principal.billingActive === true) : null,
|
|
438
|
+
duration_ms: entry.duration,
|
|
439
|
+
environment: context.environment ?? 'unknown',
|
|
440
|
+
error_code: entry.errorCode ?? null,
|
|
441
|
+
key_environment: principal?.type === 'api_key' ? (principal.environment ?? null) : null,
|
|
442
|
+
key_id: principal?.type === 'api_key' ? principal.id : null,
|
|
443
|
+
method: entry.method,
|
|
444
|
+
org_id: principal?.type === 'api_key' ? (principal.orgId ?? null) : null,
|
|
445
|
+
project_id: principal?.type === 'api_key' ? (principal.projectId ?? null) : null,
|
|
446
|
+
principal_type: principal?.type ?? 'unknown',
|
|
447
|
+
rate_limit_scope: entry.rateLimit?.scope ?? null,
|
|
448
|
+
request_id: entry.requestId ?? null,
|
|
449
|
+
route: entry.route,
|
|
450
|
+
service: context.service ?? 'unknown',
|
|
451
|
+
status: entry.status,
|
|
452
|
+
timestamp: timestamp(),
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function usageWhere(options: ProjectUsageOptions) {
|
|
457
|
+
const attributions = options.attributions.length
|
|
458
|
+
? options.attributions
|
|
459
|
+
.map((attribution) => `(${attributionWhere(attribution, options)})`)
|
|
460
|
+
.join(' OR ')
|
|
461
|
+
: '0'
|
|
462
|
+
return [
|
|
463
|
+
`principal_type = 'api_key'`,
|
|
464
|
+
`(${attributions})`,
|
|
465
|
+
`timestamp >= parseDateTime64BestEffort(${literal(options.from)}, 3, 'UTC')`,
|
|
466
|
+
`timestamp <= parseDateTime64BestEffort(${literal(options.to)}, 3, 'UTC')`,
|
|
467
|
+
].join(' AND ')
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function attributionWhere(attribution: Attribution, options: ProjectUsageOptions) {
|
|
471
|
+
if (attribution.projectId === null) {
|
|
472
|
+
if (!attribution.apiKeyIds?.length) return '0'
|
|
473
|
+
return [
|
|
474
|
+
`project_id IS NULL`,
|
|
475
|
+
`key_id IN (${attribution.apiKeyIds.map(literal).join(', ')})`,
|
|
476
|
+
...(attribution.environment === undefined
|
|
477
|
+
? []
|
|
478
|
+
: [keyEnvironmentWhere(attribution.environment, true)]),
|
|
479
|
+
].join(' AND ')
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Absent `projectId` scopes to the whole organization (any project).
|
|
483
|
+
if (attribution.projectId === undefined) {
|
|
484
|
+
return [
|
|
485
|
+
`org_id = ${literal(options.orgId)}`,
|
|
486
|
+
...(attribution.apiKeyIds === undefined
|
|
487
|
+
? []
|
|
488
|
+
: [`key_id IN (${attribution.apiKeyIds.map(literal).join(', ')})`]),
|
|
489
|
+
...(attribution.environment === undefined
|
|
490
|
+
? []
|
|
491
|
+
: [keyEnvironmentWhere(attribution.environment, false)]),
|
|
492
|
+
].join(' AND ')
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return [
|
|
496
|
+
`org_id = ${literal(options.orgId)}`,
|
|
497
|
+
`project_id = ${literal(attribution.projectId)}`,
|
|
498
|
+
...(attribution.apiKeyIds === undefined
|
|
499
|
+
? []
|
|
500
|
+
: [`key_id IN (${attribution.apiKeyIds.map(literal).join(', ')})`]),
|
|
501
|
+
...(attribution.environment === undefined
|
|
502
|
+
? []
|
|
503
|
+
: [keyEnvironmentWhere(attribution.environment, false)]),
|
|
504
|
+
].join(' AND ')
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function keyEnvironmentWhere(environment: Environment, allowLegacyNull: boolean) {
|
|
508
|
+
const exact = `key_environment = ${literal(environment)}`
|
|
509
|
+
if (!allowLegacyNull) return exact
|
|
510
|
+
return `(${exact} OR key_environment IS NULL)`
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function literal(value: string) {
|
|
514
|
+
return `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function number(value: number | string) {
|
|
518
|
+
return Number(value)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function timestamp() {
|
|
522
|
+
return new Date().toISOString().replace('T', ' ').replace('Z', '')
|
|
523
|
+
}
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { App, Store } from 'tapimo'
|
|
2
|
-
import { data,
|
|
2
|
+
import { data, management, relay } from 'tapimo/apps'
|
|
3
3
|
import * as TestApp from '../../test/App.js'
|
|
4
4
|
|
|
5
|
-
// Each test mounts one group and asserts its routes exist while
|
|
6
|
-
//
|
|
7
|
-
// tags
|
|
5
|
+
// Each test mounts one group and asserts its routes exist while omitted groups
|
|
6
|
+
// are absent (404 and not in the document). The management case also locks its
|
|
7
|
+
// OpenAPI group name; tags and schemes otherwise stay a superset (Decision C).
|
|
8
|
+
|
|
9
|
+
/** OpenAPI document from a composed app. */
|
|
10
|
+
async function document(app: { request: (path: string) => Promise<Response> | Response }) {
|
|
11
|
+
const res = await app.request('/openapi.json')
|
|
12
|
+
return (await res.json()) as {
|
|
13
|
+
paths: Record<string, unknown>
|
|
14
|
+
'x-tagGroups'?: readonly { name: string; tags: readonly string[] }[] | undefined
|
|
15
|
+
}
|
|
16
|
+
}
|
|
8
17
|
|
|
9
18
|
/** Paths (sans method) present in a composed app's OpenAPI document. */
|
|
10
19
|
async function paths(app: { request: (path: string) => Promise<Response> | Response }) {
|
|
11
|
-
|
|
12
|
-
const spec = (await res.json()) as { paths: Record<string, unknown> }
|
|
13
|
-
return Object.keys(spec.paths)
|
|
20
|
+
return Object.keys((await document(app)).paths)
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
describe('data()', () => {
|
|
17
|
-
test('mounts data routes and omits
|
|
24
|
+
test('mounts data routes and omits management routes', async () => {
|
|
18
25
|
const app = App.create({ db: TestApp.database() }).route('/', data())
|
|
19
26
|
|
|
20
27
|
const keys = await paths(app)
|
|
@@ -23,7 +30,7 @@ describe('data()', () => {
|
|
|
23
30
|
expect(keys.some((path) => path.startsWith('/v1/orgs'))).toBe(false)
|
|
24
31
|
expect(keys.some((path) => path.startsWith('/v1/me'))).toBe(false)
|
|
25
32
|
|
|
26
|
-
// The omitted
|
|
33
|
+
// The omitted management route resolves to a 404 (never mounted), not a 401.
|
|
27
34
|
const orgs = await app.request('/v1/orgs')
|
|
28
35
|
expect(orgs.status).toBe(404)
|
|
29
36
|
})
|
|
@@ -45,19 +52,25 @@ describe('relay()', () => {
|
|
|
45
52
|
})
|
|
46
53
|
})
|
|
47
54
|
|
|
48
|
-
describe('
|
|
49
|
-
test('mounts
|
|
55
|
+
describe('management()', () => {
|
|
56
|
+
test('mounts management routes and omits data routes', async () => {
|
|
50
57
|
const app = App.create({
|
|
51
58
|
auth: { session: { wallet: { origin: 'https://test.example' } } },
|
|
52
59
|
db: TestApp.database(),
|
|
53
60
|
kv: { store: Store.memory() },
|
|
54
|
-
}).route('/',
|
|
61
|
+
}).route('/', management())
|
|
55
62
|
|
|
56
|
-
const
|
|
63
|
+
const spec = await document(app)
|
|
64
|
+
const keys = Object.keys(spec.paths)
|
|
57
65
|
expect(keys.some((path) => path.startsWith('/v1/orgs'))).toBe(true)
|
|
58
66
|
expect(keys.some((path) => path.startsWith('/v1/me'))).toBe(true)
|
|
59
67
|
expect(keys.some((path) => path.startsWith('/v1/tokens'))).toBe(false)
|
|
60
68
|
expect(keys.some((path) => path.startsWith('/v1/blocks'))).toBe(false)
|
|
69
|
+
expect(spec['x-tagGroups']?.map(({ name }) => name)).toMatchInlineSnapshot(`
|
|
70
|
+
[
|
|
71
|
+
"Management API",
|
|
72
|
+
]
|
|
73
|
+
`)
|
|
61
74
|
|
|
62
75
|
// The omitted data route resolves to a 404 (never mounted).
|
|
63
76
|
const token = await app.request('/v1/tokens/0x20c0000000000000000000000000000000000000')
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as TestApp from '../../../test/App.js'
|
|
2
|
+
import * as Viem from '../../internal/Viem.js'
|
|
3
|
+
|
|
4
|
+
/** A supported non-mainnet chain id, exercising the sandbox testnet allowance. */
|
|
5
|
+
const testnet = Viem.chainId.testnet
|
|
6
|
+
|
|
7
|
+
/** Sandbox reader: testnet-only across the chain-data routes. */
|
|
8
|
+
const sandbox = {
|
|
9
|
+
environment: 'sandbox',
|
|
10
|
+
id: 'key_sandbox',
|
|
11
|
+
orgId: 'org_test',
|
|
12
|
+
scopes: ['data:read'],
|
|
13
|
+
token: 'secret_sandbox',
|
|
14
|
+
} satisfies TestApp.kvStore.Key
|
|
15
|
+
|
|
16
|
+
/** Production reader: unaffected by the sandbox chain guard. */
|
|
17
|
+
const production = {
|
|
18
|
+
environment: 'production',
|
|
19
|
+
id: 'key_prod',
|
|
20
|
+
orgId: 'org_test',
|
|
21
|
+
scopes: ['data:read'],
|
|
22
|
+
token: 'secret_prod',
|
|
23
|
+
} satisfies TestApp.kvStore.Key
|
|
24
|
+
|
|
25
|
+
/** RequestInit carrying a bearer token for the given key. */
|
|
26
|
+
function as(key: { token: string }) {
|
|
27
|
+
return { headers: { authorization: `Bearer ${key.token}` } } as const
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A deployment whose data group defaults to mainnet when `chainId` is absent. */
|
|
31
|
+
function mainnetDefaultClient() {
|
|
32
|
+
return TestApp.client({
|
|
33
|
+
auth: { keys: [sandbox, production] },
|
|
34
|
+
defaultChainId: Viem.chainId.mainnet,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('sandbox chain guard', () => {
|
|
39
|
+
test('rejects a sandbox key that omits `chainId` on a mainnet-default deployment', async () => {
|
|
40
|
+
const response = await mainnetDefaultClient().v1.tokenlist.$get({ query: {} }, as(sandbox))
|
|
41
|
+
expect(response.status).toBe(403)
|
|
42
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
|
|
43
|
+
'api_key_forbidden',
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('rejects a sandbox key on an explicit mainnet `chainId`', async () => {
|
|
48
|
+
const client = TestApp.client({ auth: { keys: [sandbox, production] } })
|
|
49
|
+
const response = await client.v1.tokenlist.$get(
|
|
50
|
+
{ query: { chainId: String(Viem.chainId.mainnet) } },
|
|
51
|
+
as(sandbox),
|
|
52
|
+
)
|
|
53
|
+
expect(response.status).toBe(403)
|
|
54
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
|
|
55
|
+
'api_key_forbidden',
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('allows a sandbox key on a testnet `chainId`', async () => {
|
|
60
|
+
const client = mainnetDefaultClient()
|
|
61
|
+
const response = await client.v1.tokenlist.$get(
|
|
62
|
+
{ query: { chainId: String(testnet) } },
|
|
63
|
+
as(sandbox),
|
|
64
|
+
)
|
|
65
|
+
expect(response.status).toBe(200)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('leaves production keys unaffected on a mainnet-default deployment', async () => {
|
|
69
|
+
const response = await mainnetDefaultClient().v1.tokenlist.$get({ query: {} }, as(production))
|
|
70
|
+
expect(response.status).toBe(200)
|
|
71
|
+
})
|
|
72
|
+
})
|