tapimo 0.2.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 +31 -3
- 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 +7167 -4507
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +8 -0
- 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 +128 -118
- 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 +1024 -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/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 +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/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 +4 -2
- package/dist/apps/docs/App.d.ts.map +1 -1
- package/dist/apps/docs/App.js +27 -95
- 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 +1 -0
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +1 -0
- package/dist/apps/index.js.map +1 -1
- 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/platform/App.d.ts +3005 -728
- package/dist/apps/platform/App.d.ts.map +1 -1
- package/dist/apps/platform/App.js +13 -3
- package/dist/apps/platform/App.js.map +1 -1
- 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 +1 -0
- package/dist/apps/platform/index.d.ts.map +1 -1
- package/dist/apps/platform/index.js +1 -0
- package/dist/apps/platform/index.js.map +1 -1
- package/dist/apps/platform/routes/api-keys.d.ts +341 -49
- package/dist/apps/platform/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/platform/routes/api-keys.js +115 -13
- package/dist/apps/platform/routes/api-keys.js.map +1 -1
- 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 +201 -83
- package/dist/apps/platform/routes/invitations.d.ts.map +1 -1
- package/dist/apps/platform/routes/invitations.js +58 -0
- package/dist/apps/platform/routes/invitations.js.map +1 -1
- package/dist/apps/platform/routes/me.d.ts +17 -17
- package/dist/apps/platform/routes/orgs.d.ts +50 -36
- package/dist/apps/platform/routes/orgs.d.ts.map +1 -1
- package/dist/apps/platform/routes/orgs.js +14 -3
- package/dist/apps/platform/routes/orgs.js.map +1 -1
- package/dist/apps/platform/routes/projects.d.ts +97 -97
- 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 +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 +148 -76
- 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 +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 +4 -3
- 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 +18 -0
- package/src/Client.test-d.ts +351 -0
- 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/data/App.test.ts +72 -0
- package/src/apps/data/App.ts +35 -0
- package/src/apps/data/routes/funding.test.ts +214 -0
- package/src/apps/data/routes/funding.ts +584 -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 +29 -100
- package/src/apps/docs/reference.ts +132 -0
- package/src/apps/index.ts +1 -0
- package/src/apps/mcp.test.ts +206 -0
- package/src/apps/mcp.ts +250 -0
- package/src/apps/platform/App.ts +43 -4
- package/src/apps/platform/Billing.test.ts +530 -0
- package/src/apps/platform/Billing.ts +682 -0
- package/src/apps/platform/index.ts +1 -0
- package/src/apps/platform/routes/api-keys.test.ts +172 -9
- package/src/apps/platform/routes/api-keys.ts +136 -20
- 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 +55 -7
- package/src/apps/platform/routes/invitations.ts +68 -0
- package/src/apps/platform/routes/me.test.ts +230 -0
- package/src/apps/platform/routes/orgs.ts +26 -4
- 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 +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 +175 -86
- 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.ts +583 -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
|
@@ -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
|
+
}
|
|
@@ -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
|
+
})
|
package/src/apps/data/App.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { blocks, blocksByTimestampCompat } from './routes/blocks.js'
|
|
|
17
17
|
import { coingecko } from './routes/coingecko.js'
|
|
18
18
|
import { exchanges } from './routes/exchanges.js'
|
|
19
19
|
import { feeAmm } from './routes/fee-amm.js'
|
|
20
|
+
import { funding } from './routes/funding.js'
|
|
20
21
|
import { indexer } from './routes/indexer.js'
|
|
21
22
|
import { receipts } from './routes/receipts.js'
|
|
22
23
|
import { rpc } from './routes/rpc.js'
|
|
@@ -94,6 +95,34 @@ export function data(options: data.Options = {}) {
|
|
|
94
95
|
if (!parsed.success || supportedChainIds.has(parsed.data)) return next()
|
|
95
96
|
return Response.unsupportedChainId(c, parsed.data, supportedChainIds)
|
|
96
97
|
})
|
|
98
|
+
// Sandbox keys are testnet-only. A chain-scoped data route defaults to
|
|
99
|
+
// mainnet, so a sandbox key that omits `chainId` (or targets mainnet) is
|
|
100
|
+
// refused with a clear message telling the caller to pass a testnet
|
|
101
|
+
// `chainId`. The relay resolves its chain from the request body, not the
|
|
102
|
+
// query, so it is skipped and enforces the rule in its sponsor validation.
|
|
103
|
+
.use('*', async (c, next) => {
|
|
104
|
+
const relay = Path.join(c.get('basePath') ?? '', 'rpc/relay')
|
|
105
|
+
if (c.req.path === relay || c.req.path.startsWith(`${relay}/`)) return next()
|
|
106
|
+
|
|
107
|
+
const principal = Auth.getPrincipal(c)
|
|
108
|
+
if (principal?.type !== 'api_key' || principal.environment !== 'sandbox') return next()
|
|
109
|
+
|
|
110
|
+
const raw = c.req.query('chainId') ?? c.req.query('chain_id')
|
|
111
|
+
// Absent falls back to the group default (mainnet); a malformed value
|
|
112
|
+
// stays with the route's own validator rather than the sandbox message.
|
|
113
|
+
const chainId = (() => {
|
|
114
|
+
if (raw === undefined) return defaultChainId
|
|
115
|
+
const parsed = Schema.ChainId.safeParse(raw)
|
|
116
|
+
return parsed.success ? parsed.data : undefined
|
|
117
|
+
})()
|
|
118
|
+
if (chainId === undefined || !Viem.isMainnet(chainId)) return next()
|
|
119
|
+
|
|
120
|
+
return Response.error(c, {
|
|
121
|
+
code: 'api_key_forbidden',
|
|
122
|
+
message: 'Sandbox API keys only support testnet. Pass a testnet `chainId`.',
|
|
123
|
+
status: 403,
|
|
124
|
+
})
|
|
125
|
+
})
|
|
97
126
|
// Serve static API assets (e.g. verified-token icons) from the configured
|
|
98
127
|
// store; a no-op fall-through when no store is configured (logo URIs simply
|
|
99
128
|
// go unresolved).
|
|
@@ -111,6 +140,7 @@ export function data(options: data.Options = {}) {
|
|
|
111
140
|
.route('/', blocks())
|
|
112
141
|
.route('/', exchanges())
|
|
113
142
|
.route('/', feeAmm())
|
|
143
|
+
.route('/', funding())
|
|
114
144
|
.route('/', indexer())
|
|
115
145
|
.route('/', receipts())
|
|
116
146
|
.route('/', tokenlist())
|
|
@@ -178,6 +208,8 @@ const openapi = {
|
|
|
178
208
|
{ name: 'Blocks', description: 'The ordered batches of transactions making up the chain.' },
|
|
179
209
|
{ name: 'Exchange', description: "Tempo's built-in stablecoin exchange: pairs, swaps, orders, prices." }, // prettier-ignore
|
|
180
210
|
{ name: 'Fee AMM', description: 'Pools that convert stablecoins to pay fees.' },
|
|
211
|
+
{ name: 'Funding', description: 'Routes and inventory for moving stablecoins into Tempo.' },
|
|
212
|
+
{ name: 'Providers', description: 'Provider metadata, capabilities, and integration status.' },
|
|
181
213
|
{ name: 'Tokens', description: 'TIP-20 token details, supply, and holders.' },
|
|
182
214
|
{ name: 'Transactions', description: 'Transactions submitted to Tempo, and their receipts.' },
|
|
183
215
|
{ name: 'Transfers', description: 'Token movements from executing transactions.' },
|
|
@@ -196,6 +228,8 @@ const openapi = {
|
|
|
196
228
|
'Blocks',
|
|
197
229
|
'Exchange',
|
|
198
230
|
'Fee AMM',
|
|
231
|
+
'Funding',
|
|
232
|
+
'Providers',
|
|
199
233
|
'Tokens',
|
|
200
234
|
'Transactions',
|
|
201
235
|
'Transfers',
|
|
@@ -203,6 +237,7 @@ const openapi = {
|
|
|
203
237
|
'Webhooks',
|
|
204
238
|
'Indexer',
|
|
205
239
|
'RPC',
|
|
240
|
+
'MCP',
|
|
206
241
|
'CoinGecko',
|
|
207
242
|
],
|
|
208
243
|
},
|