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
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
|
+
import type Stripe from 'stripe'
|
|
2
3
|
|
|
3
4
|
import type * as App from '../../App.js'
|
|
4
5
|
import * as Metadata from '../metadata.js'
|
|
5
6
|
|
|
6
7
|
import { apiKeys } from './routes/api-keys.js'
|
|
8
|
+
import { billing } from './routes/billing.js'
|
|
7
9
|
import { invitations } from './routes/invitations.js'
|
|
8
10
|
import { me } from './routes/me.js'
|
|
9
11
|
import { members } from './routes/members.js'
|
|
10
12
|
import { orgs } from './routes/orgs.js'
|
|
11
13
|
import { projects } from './routes/projects.js'
|
|
14
|
+
import { scopes } from './routes/scopes.js'
|
|
15
|
+
import { usage } from './routes/usage.js'
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* Minimal transactional email sender. Cloudflare deployments wrap the Email
|
|
@@ -18,7 +22,7 @@ import { projects } from './routes/projects.js'
|
|
|
18
22
|
export type Email = {
|
|
19
23
|
/** Console URL embedded in email links (e.g. sign-in URLs). Defaults to the hosted console; self-hosts point it at their own. */
|
|
20
24
|
consoleUrl?: string | undefined
|
|
21
|
-
/** Sender address for
|
|
25
|
+
/** Sender address for management emails (e.g. `noreply@tempo.xyz`). */
|
|
22
26
|
from: string
|
|
23
27
|
/** Sends one message. */
|
|
24
28
|
send: (message: Email.Message) => Promise<unknown>
|
|
@@ -40,60 +44,91 @@ export declare namespace Email {
|
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
/** Billing capability, one bag per billing source; Stripe is the sole source today. */
|
|
48
|
+
export type Billing = {
|
|
49
|
+
/**
|
|
50
|
+
* Stripe billing source.
|
|
51
|
+
*/
|
|
52
|
+
stripe: {
|
|
53
|
+
/** Constructed Stripe client. */
|
|
54
|
+
client: Stripe
|
|
55
|
+
/** Console URL for Checkout/portal return links when the request carries no `Origin`. Defaults to the hosted console. */
|
|
56
|
+
consoleUrl?: string | undefined
|
|
57
|
+
/** Test-mode Stripe backing sandbox billing; omit to keep sandbox billing 501. */
|
|
58
|
+
sandbox?:
|
|
59
|
+
| {
|
|
60
|
+
/** Constructed test-mode Stripe client. */
|
|
61
|
+
client: Stripe
|
|
62
|
+
/** Test-mode webhook endpoint signing secret (`whsec_…`). */
|
|
63
|
+
webhookSecret: string
|
|
64
|
+
}
|
|
65
|
+
| undefined
|
|
66
|
+
/** Webhook endpoint signing secret (`whsec_…`). */
|
|
67
|
+
webhookSecret: string
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
43
71
|
/**
|
|
44
|
-
* Request environment for
|
|
45
|
-
*
|
|
46
|
-
* `App.Environment`, since
|
|
72
|
+
* Request environment for management routes: the shared app environment plus the
|
|
73
|
+
* capabilities this group publishes on context (`billing`, `email`). Owned
|
|
74
|
+
* here, not in core `App.Environment`, since these are management concerns.
|
|
47
75
|
*/
|
|
48
76
|
export type Environment = {
|
|
49
77
|
Variables: App.Environment['Variables'] & {
|
|
50
|
-
/**
|
|
78
|
+
/** Stripe billing capability; undefined when billing is unconfigured. */
|
|
79
|
+
billing: Billing | undefined
|
|
80
|
+
/** Transactional email sender for management emails; undefined when dispatch is disabled. */
|
|
51
81
|
email: Email | undefined
|
|
52
82
|
}
|
|
53
83
|
}
|
|
54
84
|
|
|
55
85
|
/**
|
|
56
|
-
* The
|
|
86
|
+
* The management route group: the current user, orgs, projects, members,
|
|
57
87
|
* invitations, and API-key management. Every route reads its dependencies from
|
|
58
88
|
* request context set by {@link App.create}.
|
|
59
89
|
*
|
|
60
90
|
* The session-auth surface (`/v1/auth/siwe` + `session` capability) is owned by
|
|
61
|
-
* {@link App.create}, so
|
|
91
|
+
* {@link App.create}, so management routes gate on a session without owning it.
|
|
62
92
|
*
|
|
63
93
|
* ```ts
|
|
64
|
-
* App.create(options).route('/',
|
|
94
|
+
* App.create(options).route('/', management())
|
|
65
95
|
* ```
|
|
66
96
|
*/
|
|
67
|
-
export function
|
|
97
|
+
export function management(options: management.Options = {}) {
|
|
68
98
|
const app = new Hono<Environment>()
|
|
69
|
-
// Publish
|
|
70
|
-
// (invitations read it off context); undefined disables dispatch.
|
|
99
|
+
// Publish this group's capabilities for handlers; undefined disables each.
|
|
71
100
|
.use('*', async (c, next) => {
|
|
101
|
+
c.set('billing', options.billing)
|
|
72
102
|
c.set('email', options.email)
|
|
73
103
|
await next()
|
|
74
104
|
})
|
|
75
105
|
.route('/', apiKeys({ enabled: options.apiKeys !== false }))
|
|
106
|
+
.route('/', billing())
|
|
76
107
|
.route('/', invitations())
|
|
77
108
|
.route('/', me())
|
|
78
109
|
.route('/', members())
|
|
79
110
|
.route('/', orgs())
|
|
80
111
|
.route('/', projects())
|
|
112
|
+
.route('/', scopes())
|
|
113
|
+
.route('/', usage())
|
|
81
114
|
return Metadata.attach(app, openapi)
|
|
82
115
|
}
|
|
83
116
|
|
|
84
|
-
export declare namespace
|
|
85
|
-
/** Options for the
|
|
117
|
+
export declare namespace management {
|
|
118
|
+
/** Options for the management route group. */
|
|
86
119
|
type Options = {
|
|
87
120
|
/** Expose the API-key management routes in the OpenAPI document. @default true */
|
|
88
121
|
apiKeys?: boolean | undefined
|
|
89
|
-
/**
|
|
122
|
+
/** Stripe billing capability; omit to disable billing routes (501) and keep production sponsorship closed. */
|
|
123
|
+
billing?: Billing | undefined
|
|
124
|
+
/** Transactional email sender for management emails (e.g. invitations); omit to disable dispatch — notified flows still work in-product. */
|
|
90
125
|
email?: Email | undefined
|
|
91
126
|
}
|
|
92
127
|
}
|
|
93
128
|
|
|
94
129
|
/**
|
|
95
|
-
* The OpenAPI metadata this group owns:
|
|
96
|
-
* `session` security scheme. Tagged onto the {@link
|
|
130
|
+
* The OpenAPI metadata this group owns: management operation tags and the
|
|
131
|
+
* `session` security scheme. Tagged onto the {@link management} instance so
|
|
97
132
|
* `App.create` collects it.
|
|
98
133
|
*/
|
|
99
134
|
const openapi = {
|
|
@@ -107,22 +142,26 @@ const openapi = {
|
|
|
107
142
|
},
|
|
108
143
|
tags: [
|
|
109
144
|
{ name: 'API Keys', description: 'Credentials for accessing the Tempo API.' },
|
|
145
|
+
{ name: 'Billing', description: 'Organization billing backed by Stripe.' },
|
|
110
146
|
{ name: 'Invitations', description: 'Pending organization invitations.' },
|
|
111
147
|
{ name: 'Members', description: 'Organization team membership.' },
|
|
112
148
|
{ name: 'Organizations', description: 'Teams that own application workspaces and members.' },
|
|
113
149
|
{ name: 'Projects', description: 'Application workspaces within an organization.' },
|
|
150
|
+
{ name: 'Usage', description: 'Request and sponsorship usage for organizations.' },
|
|
114
151
|
{ name: 'Users', description: 'Users on the Tempo Platform.' },
|
|
115
152
|
],
|
|
116
153
|
'x-tagGroups': [
|
|
117
154
|
{
|
|
118
|
-
name: '
|
|
155
|
+
name: 'Management API',
|
|
119
156
|
tags: [
|
|
120
157
|
'API Keys',
|
|
121
158
|
'Authentication',
|
|
159
|
+
'Billing',
|
|
122
160
|
'Invitations',
|
|
123
161
|
'Members',
|
|
124
162
|
'Organizations',
|
|
125
163
|
'Projects',
|
|
164
|
+
'Usage',
|
|
126
165
|
'Users',
|
|
127
166
|
],
|
|
128
167
|
},
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
2
|
+
import type Stripe from 'stripe'
|
|
3
|
+
|
|
4
|
+
import { postgresUrl } from '../../../test/containers.js'
|
|
5
|
+
import * as ApiKeys from '../../ApiKeys.js'
|
|
6
|
+
import * as Db from '../../db/Db.js'
|
|
7
|
+
import * as RequestUsage from '../../db/tables/requestUsage.js'
|
|
8
|
+
import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
|
|
9
|
+
import * as StripeCustomers from '../../db/tables/stripeCustomers.js'
|
|
10
|
+
import * as Store from '../../internal/Store.js'
|
|
11
|
+
import * as Viem from '../../internal/Viem.js'
|
|
12
|
+
import * as Billing from './Billing.js'
|
|
13
|
+
|
|
14
|
+
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
15
|
+
|
|
16
|
+
/** Baseline billable mainnet sponsorship input; tests override per case. */
|
|
17
|
+
const input = {
|
|
18
|
+
apiKeyId: 'key_1',
|
|
19
|
+
billable: true,
|
|
20
|
+
chainId: Viem.chainId.mainnet,
|
|
21
|
+
environment: 'production',
|
|
22
|
+
orgId: 'org_1',
|
|
23
|
+
projectId: 'prj_1',
|
|
24
|
+
transaction: `0x76${'cc'.repeat(16)}`,
|
|
25
|
+
} satisfies Omit<SponsoredTransactions.upsert.Input, 'signPayload'>
|
|
26
|
+
|
|
27
|
+
/** Inserts and finalizes one billable row; `seed` derives a unique sign payload. */
|
|
28
|
+
async function finalized(
|
|
29
|
+
db: Db.Db,
|
|
30
|
+
options: { feeAmount: string; orgId?: string; seed: string },
|
|
31
|
+
): Promise<SponsoredTransactions.Record> {
|
|
32
|
+
const row = await SponsoredTransactions.upsert(db, {
|
|
33
|
+
...input,
|
|
34
|
+
...(options.orgId ? { orgId: options.orgId } : {}),
|
|
35
|
+
signPayload: `0x${options.seed.repeat(32)}`,
|
|
36
|
+
})
|
|
37
|
+
await SponsoredTransactions.finalize(db, row.id, {
|
|
38
|
+
feeAmount: options.feeAmount,
|
|
39
|
+
finalizedAt: new Date().toISOString(),
|
|
40
|
+
})
|
|
41
|
+
return row
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Managed meter/price ids per kind, keyed by lookup key. */
|
|
45
|
+
const priceByLookup: Record<string, string> = {
|
|
46
|
+
[Billing.meters.apiRequests.lookupKey]: 'price_req',
|
|
47
|
+
[Billing.meters.feePayerSpend.lookupKey]: 'price_fee',
|
|
48
|
+
}
|
|
49
|
+
const meterByEvent: Record<string, string> = {
|
|
50
|
+
[Billing.meters.apiRequests.eventName]: 'mtr_req',
|
|
51
|
+
[Billing.meters.feePayerSpend.eventName]: 'mtr_fee',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A stubbed subscription: live status plus which managed kinds it carries as items. */
|
|
55
|
+
type StubSubscription = { items?: readonly Billing.MeterKind[]; status: string }
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A Stripe stub covering the reporter/subscription surface: fixtures resolve
|
|
59
|
+
* per kind, subscriptions carry per-kind items, and meter-event and
|
|
60
|
+
* item/subscription creation are captured for assertions.
|
|
61
|
+
*/
|
|
62
|
+
function stubStripe(options: {
|
|
63
|
+
meterEvent?: ((params: { identifier: string }) => void) | undefined
|
|
64
|
+
subscriptions?: readonly StubSubscription[] | undefined
|
|
65
|
+
}) {
|
|
66
|
+
const meterEvents: {
|
|
67
|
+
event_name: string
|
|
68
|
+
identifier: string
|
|
69
|
+
payload?: { stripe_customer_id: string; value: string }
|
|
70
|
+
timestamp?: number
|
|
71
|
+
}[] = []
|
|
72
|
+
const subscriptionsCreated: { items: { price: string }[] }[] = []
|
|
73
|
+
const itemsCreated: { price: string; subscription: string }[] = []
|
|
74
|
+
const expand = (subscription: StubSubscription) => ({
|
|
75
|
+
id: `sub_${subscriptionsCreated.length}_${Math.random().toString(36).slice(2, 8)}`,
|
|
76
|
+
items: {
|
|
77
|
+
data: (subscription.items ?? ['feePayerSpend']).map((kind) => ({
|
|
78
|
+
price: { id: priceByLookup[Billing.meters[kind].lookupKey] },
|
|
79
|
+
})),
|
|
80
|
+
},
|
|
81
|
+
status: subscription.status,
|
|
82
|
+
})
|
|
83
|
+
const subscriptions = (options.subscriptions ?? []).map(expand)
|
|
84
|
+
const stripe = {
|
|
85
|
+
billing: {
|
|
86
|
+
meterEvents: {
|
|
87
|
+
create: async (params: {
|
|
88
|
+
event_name: string
|
|
89
|
+
identifier: string
|
|
90
|
+
payload?: { stripe_customer_id: string; value: string }
|
|
91
|
+
timestamp?: number
|
|
92
|
+
}) => {
|
|
93
|
+
options.meterEvent?.(params)
|
|
94
|
+
meterEvents.push(params)
|
|
95
|
+
return {}
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
meters: {
|
|
99
|
+
create: async (params: { event_name: string }) => ({
|
|
100
|
+
event_name: params.event_name,
|
|
101
|
+
id: meterByEvent[params.event_name] ?? 'mtr_new',
|
|
102
|
+
}),
|
|
103
|
+
list: async () => ({
|
|
104
|
+
data: Object.entries(meterByEvent).map(([event_name, id]) => ({ event_name, id })),
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
prices: {
|
|
109
|
+
create: async (params: { lookup_key: string }) => ({
|
|
110
|
+
id: priceByLookup[params.lookup_key] ?? 'price_new',
|
|
111
|
+
}),
|
|
112
|
+
list: async (params: { lookup_keys: string[] }) => ({
|
|
113
|
+
data: params.lookup_keys
|
|
114
|
+
.map((key) => priceByLookup[key])
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.map((id) => ({ id })),
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
subscriptionItems: {
|
|
120
|
+
create: async (params: { price: string; subscription: string }) => {
|
|
121
|
+
itemsCreated.push(params)
|
|
122
|
+
return {}
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
subscriptions: {
|
|
126
|
+
create: async (params: { items: { price: string }[] }) => {
|
|
127
|
+
subscriptionsCreated.push(params)
|
|
128
|
+
return {}
|
|
129
|
+
},
|
|
130
|
+
list: async () => ({ data: subscriptions }),
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
return { itemsCreated, meterEvents, stripe: stripe as never as Stripe, subscriptionsCreated }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
describe('periodStart', () => {
|
|
137
|
+
test('behavior: anchors to the UTC calendar month start', () => {
|
|
138
|
+
expect(Billing.periodStart('month', new Date('2026-03-15T12:34:56.789Z'))).toBe('2026-03-01T00:00:00.000Z') // prettier-ignore
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('behavior: month boundaries, year rollovers, and leap years resolve in UTC', () => {
|
|
142
|
+
expect(Billing.periodStart('month', new Date('2025-12-31T23:59:59.999Z'))).toBe('2025-12-01T00:00:00.000Z') // prettier-ignore
|
|
143
|
+
expect(Billing.periodStart('month', new Date('2026-01-01T00:00:00.000Z'))).toBe('2026-01-01T00:00:00.000Z') // prettier-ignore
|
|
144
|
+
expect(Billing.periodStart('month', new Date('2028-02-29T10:00:00.000Z'))).toBe('2028-02-01T00:00:00.000Z') // prettier-ignore
|
|
145
|
+
// A UTC month starts while western timezones still sit in the prior one.
|
|
146
|
+
expect(Billing.periodStart('month', new Date('2026-07-01T03:00:00.000Z'))).toBe('2026-07-01T00:00:00.000Z') // prettier-ignore
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('toBaseUnits', () => {
|
|
151
|
+
test('behavior: round-trips decimal strings through fee-token base units', () => {
|
|
152
|
+
expect(Billing.toBaseUnits('0.35')).toBe(350_000n)
|
|
153
|
+
expect(Billing.toBaseUnits('250')).toBe(250_000_000n)
|
|
154
|
+
expect(Billing.fromBaseUnits(350_000n)).toBe('0.35')
|
|
155
|
+
expect(Billing.fromBaseUnits(250_000_000n)).toBe('250')
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('status', () => {
|
|
160
|
+
test('behavior: reflects the stored billing source status', async () => {
|
|
161
|
+
const db = create()
|
|
162
|
+
await db.migrate()
|
|
163
|
+
expect(await Billing.status(db, 'org_1')).toBeUndefined()
|
|
164
|
+
expect(await Billing.active(db, 'org_1')).toBe(false)
|
|
165
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
166
|
+
await StripeCustomers.setStatus(db, 'org_1', 'past_due')
|
|
167
|
+
expect(await Billing.status(db, 'org_1')).toBe('past_due')
|
|
168
|
+
expect(await Billing.active(db, 'org_1')).toBe(false)
|
|
169
|
+
await StripeCustomers.setStatus(db, 'org_1', 'active')
|
|
170
|
+
expect(await Billing.active(db, 'org_1')).toBe(true)
|
|
171
|
+
await db.close()
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('createReporter', () => {
|
|
176
|
+
test('behavior: a poisoned row never stalls the queue', async () => {
|
|
177
|
+
const db = create()
|
|
178
|
+
await db.migrate()
|
|
179
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
180
|
+
const poison = await finalized(db, { feeAmount: '100000', seed: '01' })
|
|
181
|
+
const good = await finalized(db, { feeAmount: '250000', seed: '02' })
|
|
182
|
+
|
|
183
|
+
const { stripe } = stubStripe({
|
|
184
|
+
meterEvent: (params) => {
|
|
185
|
+
if (params.identifier === poison.id) throw new Error('boom')
|
|
186
|
+
},
|
|
187
|
+
subscriptions: [{ items: ['feePayerSpend', 'apiRequests'], status: 'active' }],
|
|
188
|
+
})
|
|
189
|
+
const reporter = Billing.createReporter({ db, stripe })
|
|
190
|
+
expect(await reporter.tick()).toStrictEqual({ failed: 1, reported: 1, skipped: 0 })
|
|
191
|
+
// The good row marked; the poisoned row stays unmarked for retry.
|
|
192
|
+
expect((await SponsoredTransactions.get(db, good.id))?.meterReportedAt).not.toBeNull()
|
|
193
|
+
expect((await SponsoredTransactions.get(db, poison.id))?.meterReportedAt).toBeNull()
|
|
194
|
+
expect(await reporter.tick()).toStrictEqual({ failed: 1, reported: 0, skipped: 0 })
|
|
195
|
+
await db.close()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('behavior: canceled customers skip without meter events', async () => {
|
|
199
|
+
const db = create()
|
|
200
|
+
await db.migrate()
|
|
201
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
202
|
+
await StripeCustomers.setStatus(db, 'org_1', 'canceled')
|
|
203
|
+
const row = await finalized(db, { feeAmount: '100000', seed: '03' })
|
|
204
|
+
|
|
205
|
+
const { meterEvents, stripe } = stubStripe({ subscriptions: [{ status: 'active' }] })
|
|
206
|
+
expect(await Billing.createReporter({ db, stripe }).tick()).toStrictEqual({
|
|
207
|
+
failed: 0,
|
|
208
|
+
reported: 0,
|
|
209
|
+
skipped: 1,
|
|
210
|
+
})
|
|
211
|
+
expect(meterEvents).toHaveLength(0)
|
|
212
|
+
// Skipped rows still mark: nobody remains to invoice, ever.
|
|
213
|
+
expect((await SponsoredTransactions.get(db, row.id))?.meterReportedAt).not.toBeNull()
|
|
214
|
+
await db.close()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('behavior: ensures the metered subscription once and stamps its backlog at now', async () => {
|
|
218
|
+
const db = create()
|
|
219
|
+
await db.migrate()
|
|
220
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
221
|
+
await finalized(db, { feeAmount: '100000', seed: '04' })
|
|
222
|
+
await finalized(db, { feeAmount: '250000', seed: '05' })
|
|
223
|
+
|
|
224
|
+
const { meterEvents, stripe, subscriptionsCreated } = stubStripe({ subscriptions: [] })
|
|
225
|
+
expect(await Billing.createReporter({ db, stripe }).tick()).toStrictEqual({
|
|
226
|
+
failed: 0,
|
|
227
|
+
reported: 2,
|
|
228
|
+
skipped: 0,
|
|
229
|
+
})
|
|
230
|
+
// One ensure per org per tick, not per row.
|
|
231
|
+
expect(subscriptionsCreated).toHaveLength(1)
|
|
232
|
+
// The just-created subscription bills nothing before its start, so the
|
|
233
|
+
// backlog carries no timestamp and lands on the first invoice.
|
|
234
|
+
expect(meterEvents).toHaveLength(2)
|
|
235
|
+
for (const event of meterEvents) expect(event.timestamp).toBeUndefined()
|
|
236
|
+
await db.close()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('behavior: an existing subscription keeps finalization timestamps', async () => {
|
|
240
|
+
const db = create()
|
|
241
|
+
await db.migrate()
|
|
242
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
243
|
+
const row = await finalized(db, { feeAmount: '100000', seed: '06' })
|
|
244
|
+
|
|
245
|
+
const { meterEvents, stripe, subscriptionsCreated } = stubStripe({
|
|
246
|
+
subscriptions: [{ items: ['feePayerSpend', 'apiRequests'], status: 'active' }],
|
|
247
|
+
})
|
|
248
|
+
expect(await Billing.createReporter({ db, stripe }).tick()).toStrictEqual({
|
|
249
|
+
failed: 0,
|
|
250
|
+
reported: 1,
|
|
251
|
+
skipped: 0,
|
|
252
|
+
})
|
|
253
|
+
expect(subscriptionsCreated).toHaveLength(0)
|
|
254
|
+
const finalizedAt = new Date((await SponsoredTransactions.get(db, row.id))!.finalizedAt!)
|
|
255
|
+
expect(meterEvents[0]?.timestamp).toBe(Math.floor(finalizedAt.getTime() / 1000))
|
|
256
|
+
await db.close()
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
describe('createKeyBillingSyncer', () => {
|
|
261
|
+
test('behavior: converges drifted sandbox key snapshots to the billing table', async () => {
|
|
262
|
+
const db = create()
|
|
263
|
+
await db.migrate()
|
|
264
|
+
const kv = Store.memory()
|
|
265
|
+
|
|
266
|
+
// A sandbox key minted while billing was inactive; billing later activates
|
|
267
|
+
// but the webhook re-sync never lands, so the record snapshot is stale.
|
|
268
|
+
const { token } = await ApiKeys.mint(kv, {
|
|
269
|
+
billingActive: false,
|
|
270
|
+
environment: 'sandbox',
|
|
271
|
+
orgId: 'org_1',
|
|
272
|
+
scopes: ['data:read'],
|
|
273
|
+
})
|
|
274
|
+
await StripeCustomers.create(db, {
|
|
275
|
+
environment: 'sandbox',
|
|
276
|
+
orgId: 'org_1',
|
|
277
|
+
stripeCustomerId: 'cus_1',
|
|
278
|
+
})
|
|
279
|
+
await StripeCustomers.setStatus(db, 'org_1', 'active', 'sandbox')
|
|
280
|
+
|
|
281
|
+
const syncer = Billing.createKeyBillingSyncer({ db, kv })
|
|
282
|
+
expect(await syncer.tick()).toStrictEqual({ checked: 1, updated: 1 })
|
|
283
|
+
expect((await ApiKeys.resolve(kv, token))?.billingActive).toBe(true)
|
|
284
|
+
|
|
285
|
+
// A converged fleet re-reads but writes nothing.
|
|
286
|
+
expect(await syncer.tick()).toStrictEqual({ checked: 1, updated: 0 })
|
|
287
|
+
await db.close()
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
test('behavior: reconciles a lapse back to the public quota', async () => {
|
|
291
|
+
const db = create()
|
|
292
|
+
await db.migrate()
|
|
293
|
+
const kv = Store.memory()
|
|
294
|
+
|
|
295
|
+
const { token } = await ApiKeys.mint(kv, {
|
|
296
|
+
billingActive: true,
|
|
297
|
+
environment: 'sandbox',
|
|
298
|
+
orgId: 'org_1',
|
|
299
|
+
scopes: ['data:read'],
|
|
300
|
+
})
|
|
301
|
+
await StripeCustomers.create(db, {
|
|
302
|
+
environment: 'sandbox',
|
|
303
|
+
orgId: 'org_1',
|
|
304
|
+
stripeCustomerId: 'cus_1',
|
|
305
|
+
})
|
|
306
|
+
await StripeCustomers.setStatus(db, 'org_1', 'past_due', 'sandbox')
|
|
307
|
+
|
|
308
|
+
expect(await Billing.createKeyBillingSyncer({ db, kv }).tick()).toStrictEqual({
|
|
309
|
+
checked: 1,
|
|
310
|
+
updated: 1,
|
|
311
|
+
})
|
|
312
|
+
// Non-`active` billing throttles: the snapshot flips back to false.
|
|
313
|
+
expect((await ApiKeys.resolve(kv, token))?.billingActive).toBe(false)
|
|
314
|
+
await db.close()
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
test('behavior: leaves production billing sources untouched', async () => {
|
|
318
|
+
const db = create()
|
|
319
|
+
await db.migrate()
|
|
320
|
+
const kv = Store.memory()
|
|
321
|
+
|
|
322
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
323
|
+
await StripeCustomers.setStatus(db, 'org_1', 'active')
|
|
324
|
+
|
|
325
|
+
// The default syncer reconciles sandbox only; a production-only org is
|
|
326
|
+
// outside its driver set.
|
|
327
|
+
expect(await Billing.createKeyBillingSyncer({ db, kv }).tick()).toStrictEqual({
|
|
328
|
+
checked: 0,
|
|
329
|
+
updated: 0,
|
|
330
|
+
})
|
|
331
|
+
await db.close()
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
describe('ensureSubscription', () => {
|
|
336
|
+
test('behavior: adds the missing item to a live subscription', async () => {
|
|
337
|
+
const { itemsCreated, stripe, subscriptionsCreated } = stubStripe({
|
|
338
|
+
subscriptions: [{ items: ['feePayerSpend'], status: 'active' }],
|
|
339
|
+
})
|
|
340
|
+
const result = await Billing.ensureSubscription(stripe, 'cus_1', [
|
|
341
|
+
'feePayerSpend',
|
|
342
|
+
'apiRequests',
|
|
343
|
+
])
|
|
344
|
+
expect(result.created).toBe(false)
|
|
345
|
+
expect(subscriptionsCreated).toHaveLength(0)
|
|
346
|
+
expect(itemsCreated).toEqual([{ price: 'price_req', subscription: expect.any(String) }])
|
|
347
|
+
expect(result.items.apiRequests?.created).toBe(true)
|
|
348
|
+
expect(result.items.feePayerSpend?.created).toBe(false)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('behavior: creates a subscription carrying every requested item', async () => {
|
|
352
|
+
const { stripe, subscriptionsCreated } = stubStripe({ subscriptions: [] })
|
|
353
|
+
const result = await Billing.ensureSubscription(stripe, 'cus_1', [
|
|
354
|
+
'feePayerSpend',
|
|
355
|
+
'apiRequests',
|
|
356
|
+
])
|
|
357
|
+
expect(result.created).toBe(true)
|
|
358
|
+
expect(subscriptionsCreated).toHaveLength(1)
|
|
359
|
+
expect(subscriptionsCreated[0]!.items.map((item) => item.price).sort()).toEqual([
|
|
360
|
+
'price_fee',
|
|
361
|
+
'price_req',
|
|
362
|
+
])
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('behavior: fails closed on multiple live managed subscriptions', async () => {
|
|
366
|
+
const { stripe } = stubStripe({
|
|
367
|
+
subscriptions: [
|
|
368
|
+
{ items: ['feePayerSpend'], status: 'active' },
|
|
369
|
+
{ items: ['apiRequests'], status: 'active' },
|
|
370
|
+
],
|
|
371
|
+
})
|
|
372
|
+
await expect(
|
|
373
|
+
Billing.ensureSubscription(stripe, 'cus_1', ['feePayerSpend', 'apiRequests']),
|
|
374
|
+
).rejects.toBeInstanceOf(Billing.DuplicateSubscriptionError)
|
|
375
|
+
})
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
describe('createRequestUsageReporter', () => {
|
|
379
|
+
// A closed hour inside the settlement window for the fixed clock below.
|
|
380
|
+
const bucketStart = '2026-01-01T12:00:00.000Z'
|
|
381
|
+
const now = () => Date.parse('2026-01-02T00:20:00.000Z')
|
|
382
|
+
|
|
383
|
+
test('behavior: seals the initial count, then bills only the delta on growth', async () => {
|
|
384
|
+
const db = create()
|
|
385
|
+
await db.migrate()
|
|
386
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
387
|
+
await StripeCustomers.setStatus(db, 'org_1', 'active')
|
|
388
|
+
|
|
389
|
+
// Tick 1: fresh customer, initial seal of 5. The just-created item bills at
|
|
390
|
+
// now (no backdate).
|
|
391
|
+
const first = stubStripe({ subscriptions: [] })
|
|
392
|
+
const reporter1 = Billing.createRequestUsageReporter({
|
|
393
|
+
db,
|
|
394
|
+
now,
|
|
395
|
+
readCounts: async () => [{ bucketStart, count: 5, orgId: 'org_1' }],
|
|
396
|
+
stripe: first.stripe,
|
|
397
|
+
})
|
|
398
|
+
expect(await reporter1.tick()).toStrictEqual({ failed: 0, reported: 1, skipped: 0 })
|
|
399
|
+
expect(first.meterEvents).toEqual([
|
|
400
|
+
{ event_name: 'api_request_count', identifier: expect.any(String), payload: expect.objectContaining({ value: '5' }) }, // prettier-ignore
|
|
401
|
+
])
|
|
402
|
+
expect(first.meterEvents[0]!.timestamp).toBeUndefined()
|
|
403
|
+
expect((await RequestUsage.getBucket(db, { bucketStart, environment: 'production', orgId: 'org_1' }))?.reportedCount).toBe(5) // prettier-ignore
|
|
404
|
+
|
|
405
|
+
// Tick 2: count grows to 8; only the delta of 3 bills, backdated (item exists).
|
|
406
|
+
const second = stubStripe({
|
|
407
|
+
subscriptions: [{ items: ['feePayerSpend', 'apiRequests'], status: 'active' }],
|
|
408
|
+
})
|
|
409
|
+
const reporter2 = Billing.createRequestUsageReporter({
|
|
410
|
+
db,
|
|
411
|
+
now,
|
|
412
|
+
readCounts: async () => [{ bucketStart, count: 8, orgId: 'org_1' }],
|
|
413
|
+
stripe: second.stripe,
|
|
414
|
+
})
|
|
415
|
+
expect(await reporter2.tick()).toStrictEqual({ failed: 0, reported: 1, skipped: 0 })
|
|
416
|
+
expect(second.meterEvents[0]?.payload).toMatchObject({ value: '3' })
|
|
417
|
+
expect(second.meterEvents[0]?.timestamp).toBe(Math.floor(Date.parse(bucketStart) / 1000))
|
|
418
|
+
expect((await RequestUsage.getBucket(db, { bucketStart, environment: 'production', orgId: 'org_1' }))?.reportedCount).toBe(8) // prettier-ignore
|
|
419
|
+
|
|
420
|
+
// Tick 3: no growth; nothing bills.
|
|
421
|
+
const third = stubStripe({
|
|
422
|
+
subscriptions: [{ items: ['feePayerSpend', 'apiRequests'], status: 'active' }],
|
|
423
|
+
})
|
|
424
|
+
expect(
|
|
425
|
+
await Billing.createRequestUsageReporter({
|
|
426
|
+
db,
|
|
427
|
+
now,
|
|
428
|
+
readCounts: async () => [{ bucketStart, count: 8, orgId: 'org_1' }],
|
|
429
|
+
stripe: third.stripe,
|
|
430
|
+
}).tick(),
|
|
431
|
+
).toStrictEqual({ failed: 0, reported: 0, skipped: 0 })
|
|
432
|
+
expect(third.meterEvents).toHaveLength(0)
|
|
433
|
+
await db.close()
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
test('behavior: seals only hours older than the settlement delay', async () => {
|
|
437
|
+
const db = create()
|
|
438
|
+
await db.migrate()
|
|
439
|
+
const readCounts = vi.fn(async () => [] as { bucketStart: string; count: number; orgId: string }[]) // prettier-ignore
|
|
440
|
+
const { stripe } = stubStripe({ subscriptions: [] })
|
|
441
|
+
await Billing.createRequestUsageReporter({ db, now, readCounts, stripe }).tick()
|
|
442
|
+
// Cutoff is the start of the hour 15 minutes before `now`.
|
|
443
|
+
expect(readCounts).toHaveBeenCalledWith({
|
|
444
|
+
environment: 'production',
|
|
445
|
+
from: '2025-12-31T00:00:00.000Z',
|
|
446
|
+
to: '2026-01-02T00:00:00.000Z',
|
|
447
|
+
})
|
|
448
|
+
await db.close()
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
test('behavior: an org without a billing source is skipped and never sealed', async () => {
|
|
452
|
+
const db = create()
|
|
453
|
+
await db.migrate()
|
|
454
|
+
const { meterEvents, stripe } = stubStripe({ subscriptions: [] })
|
|
455
|
+
expect(
|
|
456
|
+
await Billing.createRequestUsageReporter({
|
|
457
|
+
db,
|
|
458
|
+
now,
|
|
459
|
+
readCounts: async () => [{ bucketStart, count: 4, orgId: 'org_1' }],
|
|
460
|
+
stripe,
|
|
461
|
+
}).tick(),
|
|
462
|
+
).toStrictEqual({ failed: 0, reported: 0, skipped: 1 })
|
|
463
|
+
expect(meterEvents).toHaveLength(0)
|
|
464
|
+
expect(await RequestUsage.getBucket(db, { bucketStart, environment: 'production', orgId: 'org_1' })).toBeUndefined() // prettier-ignore
|
|
465
|
+
await db.close()
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
test('behavior: bills sandbox usage on the sandbox client', async () => {
|
|
469
|
+
const db = create()
|
|
470
|
+
await db.migrate()
|
|
471
|
+
await StripeCustomers.create(db, {
|
|
472
|
+
environment: 'sandbox',
|
|
473
|
+
orgId: 'org_1',
|
|
474
|
+
stripeCustomerId: 'cus_sandbox',
|
|
475
|
+
})
|
|
476
|
+
await StripeCustomers.setStatus(db, 'org_1', 'active', 'sandbox')
|
|
477
|
+
|
|
478
|
+
const production = stubStripe({ subscriptions: [] })
|
|
479
|
+
const sandbox = stubStripe({ subscriptions: [] })
|
|
480
|
+
const reporter = Billing.createRequestUsageReporter({
|
|
481
|
+
db,
|
|
482
|
+
now,
|
|
483
|
+
readCounts: async ({ environment }) =>
|
|
484
|
+
environment === 'sandbox' ? [{ bucketStart, count: 7, orgId: 'org_1' }] : [],
|
|
485
|
+
sandboxStripe: sandbox.stripe,
|
|
486
|
+
stripe: production.stripe,
|
|
487
|
+
})
|
|
488
|
+
expect(await reporter.tick()).toStrictEqual({ failed: 0, reported: 1, skipped: 0 })
|
|
489
|
+
expect(production.meterEvents).toHaveLength(0)
|
|
490
|
+
expect(sandbox.meterEvents[0]?.payload).toMatchObject({
|
|
491
|
+
stripe_customer_id: 'cus_sandbox',
|
|
492
|
+
value: '7',
|
|
493
|
+
})
|
|
494
|
+
expect((await RequestUsage.getBucket(db, { bucketStart, environment: 'sandbox', orgId: 'org_1' }))?.reportedCount).toBe(7) // prettier-ignore
|
|
495
|
+
await db.close()
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
test('behavior: re-sends a stranded pending event and advances the watermark', async () => {
|
|
499
|
+
const db = create()
|
|
500
|
+
await db.migrate()
|
|
501
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
502
|
+
await StripeCustomers.setStatus(db, 'org_1', 'active')
|
|
503
|
+
// Simulate a crash after sealing but before the Stripe ack.
|
|
504
|
+
await RequestUsage.insertPending(db, {
|
|
505
|
+
bucketStart,
|
|
506
|
+
deltaCount: 6,
|
|
507
|
+
environment: 'production',
|
|
508
|
+
identifier: 'rq_production_deadbeef0001_0_0',
|
|
509
|
+
orgId: 'org_1',
|
|
510
|
+
sequence: 0,
|
|
511
|
+
stripeCustomerId: 'cus_1',
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
const { meterEvents, stripe } = stubStripe({
|
|
515
|
+
subscriptions: [{ items: ['feePayerSpend', 'apiRequests'], status: 'active' }],
|
|
516
|
+
})
|
|
517
|
+
expect(
|
|
518
|
+
await Billing.createRequestUsageReporter({
|
|
519
|
+
db,
|
|
520
|
+
now,
|
|
521
|
+
// No new counts; recovery must still drain the pending event.
|
|
522
|
+
readCounts: async () => [],
|
|
523
|
+
stripe,
|
|
524
|
+
}).tick(),
|
|
525
|
+
).toStrictEqual({ failed: 0, reported: 1, skipped: 0 })
|
|
526
|
+
expect(meterEvents[0]?.payload).toMatchObject({ value: '6' })
|
|
527
|
+
expect((await RequestUsage.getBucket(db, { bucketStart, environment: 'production', orgId: 'org_1' }))?.reportedCount).toBe(6) // prettier-ignore
|
|
528
|
+
await db.close()
|
|
529
|
+
})
|
|
530
|
+
})
|