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,795 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import Stripe from 'stripe';
|
|
3
|
+
import * as z from 'zod/mini';
|
|
4
|
+
import * as ApiKeys from '../../../ApiKeys.js';
|
|
5
|
+
import * as Auth from '../../../internal/Auth.js';
|
|
6
|
+
import * as BillingSettings from '../../../db/tables/billingSettings.js';
|
|
7
|
+
import * as Db from '../../../db/Db.js';
|
|
8
|
+
import * as OpenApi from '../../../internal/OpenApi.js';
|
|
9
|
+
import * as Response from '../../../internal/Response.js';
|
|
10
|
+
import * as Schema from '../../../internal/Schema.js';
|
|
11
|
+
import * as SponsoredTransactions from '../../../db/tables/sponsoredTransactions.js';
|
|
12
|
+
import * as StripeCustomers from '../../../db/tables/stripeCustomers.js';
|
|
13
|
+
import * as Viem from '../../../internal/Viem.js';
|
|
14
|
+
import * as core_Billing from '../Billing.js';
|
|
15
|
+
/** Zod schemas owned by the billing resource. */
|
|
16
|
+
export var schema;
|
|
17
|
+
(function (schema) {
|
|
18
|
+
const OrgId = z
|
|
19
|
+
.string()
|
|
20
|
+
.check(z.describe('The organization id (`org_…`).'), z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }));
|
|
21
|
+
/** Path parameters addressing one organization's billing. */
|
|
22
|
+
schema.Params = z
|
|
23
|
+
.object({ orgId: OrgId })
|
|
24
|
+
.check(z.describe("Path parameters for one organization's billing."));
|
|
25
|
+
/** Query parameters selecting the billing environment. */
|
|
26
|
+
schema.Query = z
|
|
27
|
+
.strictObject({ environment: Schema.Environment })
|
|
28
|
+
.check(z.describe("Query parameters for one organization's billing."));
|
|
29
|
+
/** Path parameters addressing one payment method. */
|
|
30
|
+
schema.MethodParams = z
|
|
31
|
+
.object({
|
|
32
|
+
methodId: z
|
|
33
|
+
.string()
|
|
34
|
+
.check(z.describe('The payment method id (`pm_…`).'), z.meta({ examples: ['pm_1NVChw2eZvKYlo2CHxiM5E2N'] })),
|
|
35
|
+
orgId: OrgId,
|
|
36
|
+
})
|
|
37
|
+
.check(z.describe('Path parameters addressing one payment method.'));
|
|
38
|
+
/** A per-period spend limit. */
|
|
39
|
+
schema.SpendLimit = Schema.describe(z.object({
|
|
40
|
+
amount: z.string().check(
|
|
41
|
+
// The lookahead rejects zero: a zero limit would refuse all
|
|
42
|
+
// sponsorship, which `null` (clear) already expresses.
|
|
43
|
+
z.regex(/^(?!0+(\.0+)?$)\d{1,9}(\.\d{1,2})?$/), z.describe('Limit as a positive decimal string in `currency` units.'), z.meta({ examples: ['250'] })),
|
|
44
|
+
currency: z
|
|
45
|
+
._default(z.enum(BillingSettings.currencies), 'usd')
|
|
46
|
+
.check(z.describe('Currency the limit is denominated in; `usd` only today.'), z.meta({ examples: ['usd'] })),
|
|
47
|
+
period: z
|
|
48
|
+
._default(z.enum(BillingSettings.periods), 'month')
|
|
49
|
+
.check(z.describe('Window the limit applies over; `month` (UTC calendar) only today.'), z.meta({ examples: ['month'] })),
|
|
50
|
+
}), 'A per-period spend limit.');
|
|
51
|
+
/** A per-transaction fee cap. */
|
|
52
|
+
schema.TxFeeLimit = Schema.describe(z.object({
|
|
53
|
+
amount: z.string().check(
|
|
54
|
+
// The lookahead rejects zero: a zero cap would refuse every
|
|
55
|
+
// transaction, which is not a supported configuration.
|
|
56
|
+
z.regex(/^(?!0+(\.0+)?$)\d{1,9}(\.\d{1,2})?$/), z.describe('Cap as a positive decimal string in `currency` units.'), z.meta({ examples: ['0.50'] })),
|
|
57
|
+
currency: z
|
|
58
|
+
._default(z.enum(BillingSettings.currencies), 'usd')
|
|
59
|
+
.check(z.describe('Currency the cap is denominated in; `usd` only today.'), z.meta({ examples: ['usd'] })),
|
|
60
|
+
}), 'A per-transaction fee cap.');
|
|
61
|
+
/** PATCH body; absent fields stay unchanged, null clears a limit. */
|
|
62
|
+
schema.Patch = Schema.describe(z.object({
|
|
63
|
+
spendLimit: z
|
|
64
|
+
.optional(z.nullable(schema.SpendLimit))
|
|
65
|
+
.check(z.describe('Spend limit per period; absent leaves it unchanged, null removes it.'), z.meta({ examples: [{ amount: '250', currency: 'usd', period: 'month' }] })),
|
|
66
|
+
txFeeLimit: z.optional(z.nullable(schema.TxFeeLimit)).check(z.describe('Per-transaction fee cap; absent leaves it unchanged, null restores the platform default.'), // prettier-ignore
|
|
67
|
+
z.meta({ examples: [{ amount: '0.50', currency: 'usd' }] })),
|
|
68
|
+
}), 'Billing settings to apply.');
|
|
69
|
+
/** An organization's billing state. */
|
|
70
|
+
schema.Billing = Schema.describe(z.object({
|
|
71
|
+
spend: z
|
|
72
|
+
.optional(z.object({
|
|
73
|
+
amount: z.string().check(z.describe('Committed spend as a decimal string: finalized fees plus in-flight fee caps.'), // prettier-ignore
|
|
74
|
+
z.meta({ examples: ['12.34'] })),
|
|
75
|
+
currency: z
|
|
76
|
+
.enum(BillingSettings.currencies)
|
|
77
|
+
.check(z.describe('Currency of the spend figure.'), z.meta({ examples: ['usd'] })),
|
|
78
|
+
period: z
|
|
79
|
+
.enum(BillingSettings.periods)
|
|
80
|
+
.check(z.describe('Window the figure covers.'), z.meta({ examples: ['month'] })),
|
|
81
|
+
}))
|
|
82
|
+
.check(z.describe('Billable production spend committed in the current window.'), z.meta({ examples: [{ amount: '12.34', currency: 'usd', period: 'month' }] })),
|
|
83
|
+
spendLimit: z
|
|
84
|
+
.optional(schema.SpendLimit)
|
|
85
|
+
.check(z.describe('Configured spend limit; absent when the organization has none.'), z.meta({ examples: [{ amount: '250', currency: 'usd', period: 'month' }] })),
|
|
86
|
+
status: z.enum(StripeCustomers.statuses).check(z.describe("Billing status derived from the organization's billing source; `active` opens production fee sponsorship."), // prettier-ignore
|
|
87
|
+
z.meta({ examples: ['active'] })),
|
|
88
|
+
txFeeLimit: z
|
|
89
|
+
.optional(schema.TxFeeLimit)
|
|
90
|
+
.check(z.describe('Configured per-transaction fee cap; absent when the platform default applies.'), z.meta({ examples: [{ amount: '0.50', currency: 'usd' }] })),
|
|
91
|
+
updatedAt: z.optional(z.string()).check(z.describe('When the billing status last changed (ISO 8601); absent before first checkout.'), // prettier-ignore
|
|
92
|
+
z.meta({ examples: ['2026-01-01T00:00:00.000Z'] })),
|
|
93
|
+
}), "An organization's billing state.");
|
|
94
|
+
/** A hosted Stripe session link. */
|
|
95
|
+
schema.Session = Schema.describe(z.object({
|
|
96
|
+
url: z
|
|
97
|
+
.url()
|
|
98
|
+
.check(z.describe('Hosted Stripe session URL; navigate the browser here.'), z.meta({ examples: ['https://checkout.stripe.com/c/pay/cs_test_a1b2c3'] })),
|
|
99
|
+
}), 'A hosted Stripe session link.');
|
|
100
|
+
/** A payment method on file. */
|
|
101
|
+
schema.PaymentMethod = Schema.describe(z.object({
|
|
102
|
+
card: z
|
|
103
|
+
.optional(z.object({
|
|
104
|
+
brand: z
|
|
105
|
+
.string()
|
|
106
|
+
.check(z.describe('Card brand (e.g. `visa`).'), z.meta({ examples: ['visa'] })),
|
|
107
|
+
expMonth: z
|
|
108
|
+
.number()
|
|
109
|
+
.check(z.describe('Expiry month (1-12).'), z.meta({ examples: [12] })),
|
|
110
|
+
expYear: z.number().check(z.describe('Expiry year.'), z.meta({ examples: [2030] })),
|
|
111
|
+
last4: z
|
|
112
|
+
.string()
|
|
113
|
+
.check(z.describe('Last four digits.'), z.meta({ examples: ['4242'] })),
|
|
114
|
+
}))
|
|
115
|
+
.check(z.describe('Card details; present when `type` is `card`.'), z.meta({ examples: [{ brand: 'visa', expMonth: 12, expYear: 2030, last4: '4242' }] })),
|
|
116
|
+
createdAt: z
|
|
117
|
+
.string()
|
|
118
|
+
.check(z.describe('When the method was attached (ISO 8601).'), z.meta({ examples: ['2026-01-01T00:00:00.000Z'] })),
|
|
119
|
+
default: z
|
|
120
|
+
.boolean()
|
|
121
|
+
.check(z.describe('Whether invoices charge this method.'), z.meta({ examples: [true] })),
|
|
122
|
+
id: z
|
|
123
|
+
.string()
|
|
124
|
+
.check(z.describe('The payment method id (`pm_…`).'), z.meta({ examples: ['pm_1NVChw2eZvKYlo2CHxiM5E2N'] })),
|
|
125
|
+
provider: z
|
|
126
|
+
.literal('stripe')
|
|
127
|
+
.check(z.describe('Payment provider; `stripe` only today.'), z.meta({ examples: ['stripe'] })),
|
|
128
|
+
type: z
|
|
129
|
+
.string()
|
|
130
|
+
.check(z.describe('Method type (e.g. `card`, `us_bank_account`).'), z.meta({ examples: ['card'] })),
|
|
131
|
+
}), 'A payment method on file.');
|
|
132
|
+
/** An organization's payment methods. */
|
|
133
|
+
schema.PaymentMethods = Schema.describe(z.object({
|
|
134
|
+
data: z.array(schema.PaymentMethod).check(z.describe('Payment methods on file, newest first.')),
|
|
135
|
+
}), "An organization's payment methods.");
|
|
136
|
+
})(schema || (schema = {}));
|
|
137
|
+
/**
|
|
138
|
+
* Mounts the billing surface: an organization's billing status. Orgs without
|
|
139
|
+
* a Stripe billing source read as `none`. Session lane; reads for members.
|
|
140
|
+
*/
|
|
141
|
+
export function billing() {
|
|
142
|
+
return new Hono()
|
|
143
|
+
.get('/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing', Auth.policy({ session: true }), Auth.ensureOrg(), OpenApi.validate('param', schema.Params, {
|
|
144
|
+
code: 'param_invalid',
|
|
145
|
+
message: 'Check the path parameters and try again.',
|
|
146
|
+
}), OpenApi.validate('query', schema.Query, {
|
|
147
|
+
code: 'query_invalid',
|
|
148
|
+
message: 'Check the query parameters and try again.',
|
|
149
|
+
}), OpenApi.describeRoute({
|
|
150
|
+
operationId: 'getBilling',
|
|
151
|
+
responses: OpenApi.responses({
|
|
152
|
+
errors: {
|
|
153
|
+
404: 'No accessible organization was found for this id.',
|
|
154
|
+
501: 'Billing is not configured on this deployment.',
|
|
155
|
+
},
|
|
156
|
+
success: { description: "The organization's billing state.", schema: schema.Billing },
|
|
157
|
+
}),
|
|
158
|
+
summary: 'Get billing',
|
|
159
|
+
tags: ['Billing'],
|
|
160
|
+
}), async (c) => {
|
|
161
|
+
if (Auth.narrowAccess)
|
|
162
|
+
return Auth.superAdminAccessError(c);
|
|
163
|
+
if (Auth.narrowScope)
|
|
164
|
+
return Auth.ensureOrgError(c);
|
|
165
|
+
if (OpenApi.narrowValidation)
|
|
166
|
+
return OpenApi.validationError(c, {
|
|
167
|
+
code: 'param_invalid',
|
|
168
|
+
message: 'Check the path parameters and try again.',
|
|
169
|
+
});
|
|
170
|
+
if (OpenApi.narrowValidation)
|
|
171
|
+
return OpenApi.validationError(c, {
|
|
172
|
+
code: 'query_invalid',
|
|
173
|
+
message: 'Check the query parameters and try again.',
|
|
174
|
+
});
|
|
175
|
+
const billing = c.get('billing');
|
|
176
|
+
if (!billing)
|
|
177
|
+
return billingUnconfigured(c);
|
|
178
|
+
const environment = c.req.valid('query').environment;
|
|
179
|
+
if (!stripeFor(billing, environment))
|
|
180
|
+
return billingUnconfigured(c);
|
|
181
|
+
try {
|
|
182
|
+
return c.json(Response.validated(schema.Billing, await billingBody(c, Auth.org(c).id, environment)), 200);
|
|
183
|
+
}
|
|
184
|
+
catch (cause) {
|
|
185
|
+
return Response.upstream(c, cause);
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
.patch('/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing', Auth.policy({ session: true }), Auth.ensureOrg({ role: 'owner' }), OpenApi.validate('param', schema.Params, {
|
|
189
|
+
code: 'param_invalid',
|
|
190
|
+
message: 'Check the path parameters and try again.',
|
|
191
|
+
}), OpenApi.validate('query', schema.Query, {
|
|
192
|
+
code: 'query_invalid',
|
|
193
|
+
message: 'Check the query parameters and try again.',
|
|
194
|
+
}), OpenApi.validate('json', schema.Patch, {
|
|
195
|
+
code: 'body_invalid',
|
|
196
|
+
message: 'Check the request body and try again.',
|
|
197
|
+
}), OpenApi.describeRoute({
|
|
198
|
+
operationId: 'updateBilling',
|
|
199
|
+
responses: OpenApi.responses({
|
|
200
|
+
errors: {
|
|
201
|
+
403: 'Requires the owner role.',
|
|
202
|
+
404: 'No accessible organization was found for this id.',
|
|
203
|
+
501: 'Billing is not configured on this deployment.',
|
|
204
|
+
},
|
|
205
|
+
success: {
|
|
206
|
+
description: "The organization's updated billing state.",
|
|
207
|
+
schema: schema.Billing,
|
|
208
|
+
},
|
|
209
|
+
}),
|
|
210
|
+
summary: 'Update billing',
|
|
211
|
+
tags: ['Billing'],
|
|
212
|
+
}), async (c) => {
|
|
213
|
+
if (Auth.narrowAccess)
|
|
214
|
+
return Auth.superAdminAccessError(c);
|
|
215
|
+
if (Auth.narrowScope)
|
|
216
|
+
return Auth.ensureOrgError(c);
|
|
217
|
+
if (OpenApi.narrowValidation)
|
|
218
|
+
return OpenApi.validationError(c, {
|
|
219
|
+
code: 'param_invalid',
|
|
220
|
+
message: 'Check the path parameters and try again.',
|
|
221
|
+
});
|
|
222
|
+
if (OpenApi.narrowValidation)
|
|
223
|
+
return OpenApi.validationError(c, {
|
|
224
|
+
code: 'query_invalid',
|
|
225
|
+
message: 'Check the query parameters and try again.',
|
|
226
|
+
});
|
|
227
|
+
if (OpenApi.narrowValidation)
|
|
228
|
+
return OpenApi.validationError(c, {
|
|
229
|
+
code: 'body_invalid',
|
|
230
|
+
message: 'Check the request body and try again.',
|
|
231
|
+
});
|
|
232
|
+
const billing = c.get('billing');
|
|
233
|
+
if (!billing)
|
|
234
|
+
return billingUnconfigured(c);
|
|
235
|
+
const environment = c.req.valid('query').environment;
|
|
236
|
+
if (!stripeFor(billing, environment))
|
|
237
|
+
return billingUnconfigured(c);
|
|
238
|
+
const body = c.req.valid('json');
|
|
239
|
+
const org = Auth.org(c);
|
|
240
|
+
const db = Db.get(c.get('db'));
|
|
241
|
+
try {
|
|
242
|
+
await BillingSettings.upsert(db, {
|
|
243
|
+
environment,
|
|
244
|
+
orgId: org.id,
|
|
245
|
+
...(body.spendLimit !== undefined
|
|
246
|
+
? body.spendLimit === null
|
|
247
|
+
? { spendLimit: null }
|
|
248
|
+
: {
|
|
249
|
+
currency: body.spendLimit.currency,
|
|
250
|
+
period: body.spendLimit.period,
|
|
251
|
+
spendLimit: body.spendLimit.amount,
|
|
252
|
+
}
|
|
253
|
+
: {}),
|
|
254
|
+
...(body.txFeeLimit !== undefined
|
|
255
|
+
? body.txFeeLimit === null
|
|
256
|
+
? { txFeeLimit: null }
|
|
257
|
+
: { currency: body.txFeeLimit.currency, txFeeLimit: body.txFeeLimit.amount }
|
|
258
|
+
: {}),
|
|
259
|
+
});
|
|
260
|
+
return c.json(Response.validated(schema.Billing, await billingBody(c, org.id, environment)), 200);
|
|
261
|
+
}
|
|
262
|
+
catch (cause) {
|
|
263
|
+
return Response.upstream(c, cause);
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
.post('/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/stripe/checkout', Auth.policy({ session: true }), Auth.ensureOrg({ role: 'owner' }), OpenApi.validate('param', schema.Params, {
|
|
267
|
+
code: 'param_invalid',
|
|
268
|
+
message: 'Check the path parameters and try again.',
|
|
269
|
+
}), OpenApi.validate('query', schema.Query, {
|
|
270
|
+
code: 'query_invalid',
|
|
271
|
+
message: 'Check the query parameters and try again.',
|
|
272
|
+
}), OpenApi.describeRoute({
|
|
273
|
+
operationId: 'createStripeCheckout',
|
|
274
|
+
responses: OpenApi.responses({
|
|
275
|
+
errors: {
|
|
276
|
+
403: 'Requires the owner role.',
|
|
277
|
+
404: 'No accessible organization was found for this id.',
|
|
278
|
+
501: 'Billing is not configured on this deployment.',
|
|
279
|
+
},
|
|
280
|
+
success: {
|
|
281
|
+
description: 'A Stripe Checkout (setup mode) session for attaching a payment method.',
|
|
282
|
+
schema: schema.Session,
|
|
283
|
+
},
|
|
284
|
+
}),
|
|
285
|
+
summary: 'Create checkout session',
|
|
286
|
+
tags: ['Billing'],
|
|
287
|
+
}), async (c) => {
|
|
288
|
+
if (Auth.narrowAccess)
|
|
289
|
+
return Auth.superAdminAccessError(c);
|
|
290
|
+
if (Auth.narrowScope)
|
|
291
|
+
return Auth.ensureOrgError(c);
|
|
292
|
+
if (OpenApi.narrowValidation)
|
|
293
|
+
return OpenApi.validationError(c, {
|
|
294
|
+
code: 'param_invalid',
|
|
295
|
+
message: 'Check the path parameters and try again.',
|
|
296
|
+
});
|
|
297
|
+
if (OpenApi.narrowValidation)
|
|
298
|
+
return OpenApi.validationError(c, {
|
|
299
|
+
code: 'query_invalid',
|
|
300
|
+
message: 'Check the query parameters and try again.',
|
|
301
|
+
});
|
|
302
|
+
const billing = c.get('billing');
|
|
303
|
+
if (!billing)
|
|
304
|
+
return billingUnconfigured(c);
|
|
305
|
+
const environment = c.req.valid('query').environment;
|
|
306
|
+
const source = stripeFor(billing, environment);
|
|
307
|
+
if (!source)
|
|
308
|
+
return billingUnconfigured(c);
|
|
309
|
+
const { client } = source;
|
|
310
|
+
const org = Auth.org(c);
|
|
311
|
+
const db = Db.get(c.get('db'));
|
|
312
|
+
try {
|
|
313
|
+
const existing = await StripeCustomers.get(db, org.id, environment);
|
|
314
|
+
const customerId = existing
|
|
315
|
+
? existing.stripeCustomerId
|
|
316
|
+
: await (async () => {
|
|
317
|
+
const customer = await client.customers.create({
|
|
318
|
+
metadata: { orgId: org.id },
|
|
319
|
+
name: org.name,
|
|
320
|
+
});
|
|
321
|
+
// The insert resolves a concurrent first-checkout race to the
|
|
322
|
+
// winning row; always use its customer, not the one just made.
|
|
323
|
+
const record = await StripeCustomers.create(db, {
|
|
324
|
+
environment,
|
|
325
|
+
orgId: org.id,
|
|
326
|
+
stripeCustomerId: customer.id,
|
|
327
|
+
});
|
|
328
|
+
return record.stripeCustomerId;
|
|
329
|
+
})();
|
|
330
|
+
const session = await client.checkout.sessions.create({
|
|
331
|
+
cancel_url: returnUrl(c, { environment, orgId: org.id, status: 'canceled', stripe: source }), // prettier-ignore
|
|
332
|
+
// Setup mode with dynamic payment methods requires a currency;
|
|
333
|
+
// fee-payer spend bills in USD.
|
|
334
|
+
currency: 'usd',
|
|
335
|
+
customer: customerId,
|
|
336
|
+
mode: 'setup',
|
|
337
|
+
success_url: returnUrl(c, { environment, orgId: org.id, status: 'success', stripe: source }), // prettier-ignore
|
|
338
|
+
});
|
|
339
|
+
if (!session.url)
|
|
340
|
+
return Response.upstream(c, new Error('checkout session has no url'));
|
|
341
|
+
return c.json(Response.validated(schema.Session, { url: session.url }), 200);
|
|
342
|
+
}
|
|
343
|
+
catch (cause) {
|
|
344
|
+
return Response.upstream(c, cause);
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
.post('/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/stripe/manage', Auth.policy({ session: true }), Auth.ensureOrg({ role: 'owner' }), OpenApi.validate('param', schema.Params, {
|
|
348
|
+
code: 'param_invalid',
|
|
349
|
+
message: 'Check the path parameters and try again.',
|
|
350
|
+
}), OpenApi.validate('query', schema.Query, {
|
|
351
|
+
code: 'query_invalid',
|
|
352
|
+
message: 'Check the query parameters and try again.',
|
|
353
|
+
}), OpenApi.describeRoute({
|
|
354
|
+
operationId: 'createStripeManage',
|
|
355
|
+
responses: OpenApi.responses({
|
|
356
|
+
errors: {
|
|
357
|
+
403: 'Requires the owner role.',
|
|
358
|
+
404: 'No accessible organization, or no billing to manage yet.',
|
|
359
|
+
501: 'Billing is not configured on this deployment.',
|
|
360
|
+
},
|
|
361
|
+
success: {
|
|
362
|
+
description: 'A Stripe billing-portal session for managing payment methods and invoices.', // prettier-ignore
|
|
363
|
+
schema: schema.Session,
|
|
364
|
+
},
|
|
365
|
+
}),
|
|
366
|
+
summary: 'Create manage session',
|
|
367
|
+
tags: ['Billing'],
|
|
368
|
+
}), async (c) => {
|
|
369
|
+
if (Auth.narrowAccess)
|
|
370
|
+
return Auth.superAdminAccessError(c);
|
|
371
|
+
if (Auth.narrowScope)
|
|
372
|
+
return Auth.ensureOrgError(c);
|
|
373
|
+
if (OpenApi.narrowValidation)
|
|
374
|
+
return OpenApi.validationError(c, {
|
|
375
|
+
code: 'param_invalid',
|
|
376
|
+
message: 'Check the path parameters and try again.',
|
|
377
|
+
});
|
|
378
|
+
if (OpenApi.narrowValidation)
|
|
379
|
+
return OpenApi.validationError(c, {
|
|
380
|
+
code: 'query_invalid',
|
|
381
|
+
message: 'Check the query parameters and try again.',
|
|
382
|
+
});
|
|
383
|
+
const billing = c.get('billing');
|
|
384
|
+
if (!billing)
|
|
385
|
+
return billingUnconfigured(c);
|
|
386
|
+
const environment = c.req.valid('query').environment;
|
|
387
|
+
const source = stripeFor(billing, environment);
|
|
388
|
+
if (!source)
|
|
389
|
+
return billingUnconfigured(c);
|
|
390
|
+
const org = Auth.org(c);
|
|
391
|
+
const db = Db.get(c.get('db'));
|
|
392
|
+
try {
|
|
393
|
+
const record = await StripeCustomers.get(db, org.id, environment);
|
|
394
|
+
if (!record)
|
|
395
|
+
return billingNotFound(c);
|
|
396
|
+
const session = await source.client.billingPortal.sessions.create({
|
|
397
|
+
customer: record.stripeCustomerId,
|
|
398
|
+
return_url: returnUrl(c, { environment, orgId: org.id, stripe: source }),
|
|
399
|
+
});
|
|
400
|
+
return c.json(Response.validated(schema.Session, { url: session.url }), 200);
|
|
401
|
+
}
|
|
402
|
+
catch (cause) {
|
|
403
|
+
return Response.upstream(c, cause);
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
.get('/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/payment-methods', Auth.policy({ session: true }), Auth.ensureOrg(), OpenApi.validate('param', schema.Params, {
|
|
407
|
+
code: 'param_invalid',
|
|
408
|
+
message: 'Check the path parameters and try again.',
|
|
409
|
+
}), OpenApi.validate('query', schema.Query, {
|
|
410
|
+
code: 'query_invalid',
|
|
411
|
+
message: 'Check the query parameters and try again.',
|
|
412
|
+
}), OpenApi.describeRoute({
|
|
413
|
+
operationId: 'getBillingPaymentMethods',
|
|
414
|
+
responses: OpenApi.responses({
|
|
415
|
+
errors: {
|
|
416
|
+
404: 'No accessible organization was found for this id.',
|
|
417
|
+
501: 'Billing is not configured on this deployment.',
|
|
418
|
+
},
|
|
419
|
+
success: {
|
|
420
|
+
description: "The organization's payment methods.",
|
|
421
|
+
schema: schema.PaymentMethods,
|
|
422
|
+
},
|
|
423
|
+
}),
|
|
424
|
+
summary: 'Get payment methods',
|
|
425
|
+
tags: ['Billing'],
|
|
426
|
+
}), async (c) => {
|
|
427
|
+
if (Auth.narrowAccess)
|
|
428
|
+
return Auth.superAdminAccessError(c);
|
|
429
|
+
if (Auth.narrowScope)
|
|
430
|
+
return Auth.ensureOrgError(c);
|
|
431
|
+
if (OpenApi.narrowValidation)
|
|
432
|
+
return OpenApi.validationError(c, {
|
|
433
|
+
code: 'param_invalid',
|
|
434
|
+
message: 'Check the path parameters and try again.',
|
|
435
|
+
});
|
|
436
|
+
if (OpenApi.narrowValidation)
|
|
437
|
+
return OpenApi.validationError(c, {
|
|
438
|
+
code: 'query_invalid',
|
|
439
|
+
message: 'Check the query parameters and try again.',
|
|
440
|
+
});
|
|
441
|
+
const billing = c.get('billing');
|
|
442
|
+
if (!billing)
|
|
443
|
+
return billingUnconfigured(c);
|
|
444
|
+
const environment = c.req.valid('query').environment;
|
|
445
|
+
const source = stripeFor(billing, environment);
|
|
446
|
+
if (!source)
|
|
447
|
+
return billingUnconfigured(c);
|
|
448
|
+
const { client } = source;
|
|
449
|
+
const org = Auth.org(c);
|
|
450
|
+
const db = Db.get(c.get('db'));
|
|
451
|
+
try {
|
|
452
|
+
// Orgs that never checked out simply have no methods yet.
|
|
453
|
+
const record = await StripeCustomers.get(db, org.id, environment);
|
|
454
|
+
if (!record)
|
|
455
|
+
return c.json(Response.validated(schema.PaymentMethods, { data: [] }), 200);
|
|
456
|
+
const customer = await client.customers.retrieve(record.stripeCustomerId);
|
|
457
|
+
if (customer.deleted)
|
|
458
|
+
return c.json(Response.validated(schema.PaymentMethods, { data: [] }), 200);
|
|
459
|
+
const fallback = customer.invoice_settings.default_payment_method;
|
|
460
|
+
const methods = await client.customers.listPaymentMethods(record.stripeCustomerId, { limit: 100 }); // prettier-ignore
|
|
461
|
+
return c.json(Response.validated(schema.PaymentMethods, {
|
|
462
|
+
data: methods.data.map((method) => ({
|
|
463
|
+
...(method.card
|
|
464
|
+
? {
|
|
465
|
+
card: {
|
|
466
|
+
brand: method.card.brand,
|
|
467
|
+
expMonth: method.card.exp_month,
|
|
468
|
+
expYear: method.card.exp_year,
|
|
469
|
+
last4: method.card.last4,
|
|
470
|
+
},
|
|
471
|
+
}
|
|
472
|
+
: {}),
|
|
473
|
+
createdAt: new Date(method.created * 1000).toISOString(),
|
|
474
|
+
default: method.id === fallback,
|
|
475
|
+
id: method.id,
|
|
476
|
+
provider: 'stripe',
|
|
477
|
+
type: method.type,
|
|
478
|
+
})),
|
|
479
|
+
}), 200);
|
|
480
|
+
}
|
|
481
|
+
catch (cause) {
|
|
482
|
+
return Response.upstream(c, cause);
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
.delete('/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/payment-methods/:methodId{pm_[A-Za-z0-9_-]+}', Auth.policy({ session: true }), Auth.ensureOrg({ role: 'owner' }), OpenApi.validate('param', schema.MethodParams, {
|
|
486
|
+
code: 'param_invalid',
|
|
487
|
+
message: 'Check the path parameters and try again.',
|
|
488
|
+
}), OpenApi.validate('query', schema.Query, {
|
|
489
|
+
code: 'query_invalid',
|
|
490
|
+
message: 'Check the query parameters and try again.',
|
|
491
|
+
}), OpenApi.describeRoute({
|
|
492
|
+
operationId: 'deleteBillingPaymentMethod',
|
|
493
|
+
responses: OpenApi.responses({
|
|
494
|
+
errors: {
|
|
495
|
+
403: 'Requires the owner role.',
|
|
496
|
+
404: 'No accessible organization, billing, or payment method for this id.',
|
|
497
|
+
501: 'Billing is not configured on this deployment.',
|
|
498
|
+
},
|
|
499
|
+
success: {
|
|
500
|
+
description: "The organization's updated billing state.",
|
|
501
|
+
schema: schema.Billing,
|
|
502
|
+
},
|
|
503
|
+
}),
|
|
504
|
+
summary: 'Remove payment method',
|
|
505
|
+
tags: ['Billing'],
|
|
506
|
+
}), async (c) => {
|
|
507
|
+
if (Auth.narrowAccess)
|
|
508
|
+
return Auth.superAdminAccessError(c);
|
|
509
|
+
if (Auth.narrowScope)
|
|
510
|
+
return Auth.ensureOrgError(c);
|
|
511
|
+
if (OpenApi.narrowValidation)
|
|
512
|
+
return OpenApi.validationError(c, {
|
|
513
|
+
code: 'param_invalid',
|
|
514
|
+
message: 'Check the path parameters and try again.',
|
|
515
|
+
});
|
|
516
|
+
if (OpenApi.narrowValidation)
|
|
517
|
+
return OpenApi.validationError(c, {
|
|
518
|
+
code: 'query_invalid',
|
|
519
|
+
message: 'Check the query parameters and try again.',
|
|
520
|
+
});
|
|
521
|
+
const billing = c.get('billing');
|
|
522
|
+
if (!billing)
|
|
523
|
+
return billingUnconfigured(c);
|
|
524
|
+
const environment = c.req.valid('query').environment;
|
|
525
|
+
const source = stripeFor(billing, environment);
|
|
526
|
+
if (!source)
|
|
527
|
+
return billingUnconfigured(c);
|
|
528
|
+
const { client } = source;
|
|
529
|
+
const org = Auth.org(c);
|
|
530
|
+
const db = Db.get(c.get('db'));
|
|
531
|
+
try {
|
|
532
|
+
const record = await StripeCustomers.get(db, org.id, environment);
|
|
533
|
+
if (!record)
|
|
534
|
+
return billingNotFound(c);
|
|
535
|
+
// Scoping the lookup to the org's own list is the ownership check;
|
|
536
|
+
// ids belonging to other customers read as absent.
|
|
537
|
+
const methods = await client.customers.listPaymentMethods(record.stripeCustomerId, { limit: 100 }); // prettier-ignore
|
|
538
|
+
const method = methods.data.find((m) => m.id === c.req.valid('param').methodId);
|
|
539
|
+
if (!method)
|
|
540
|
+
return paymentMethodNotFound(c);
|
|
541
|
+
await client.paymentMethods.detach(method.id);
|
|
542
|
+
// The metered subscription stays: accrued usage still invoices at
|
|
543
|
+
// period end, and re-adding a method reuses it.
|
|
544
|
+
const status = await deriveStatus(client, record.stripeCustomerId);
|
|
545
|
+
await StripeCustomers.setStatus(db, record.orgId, status, environment);
|
|
546
|
+
await syncKeyBilling(c, { environment, orgId: record.orgId, status });
|
|
547
|
+
return c.json(Response.validated(schema.Billing, await billingBody(c, org.id, environment)), 200);
|
|
548
|
+
}
|
|
549
|
+
catch (cause) {
|
|
550
|
+
return Response.upstream(c, cause);
|
|
551
|
+
}
|
|
552
|
+
})
|
|
553
|
+
.post('/stripe/webhook',
|
|
554
|
+
// Inbound Stripe callback: the signature is the gate; the public quota
|
|
555
|
+
// only absorbs delivery bursts. Never a partner-facing API route.
|
|
556
|
+
Auth.policy({
|
|
557
|
+
apiKey: false,
|
|
558
|
+
mpp: false,
|
|
559
|
+
public: { rateLimit: { perMinute: 600 } },
|
|
560
|
+
session: false,
|
|
561
|
+
}), OpenApi.describeRoute({ hide: true, summary: 'Stripe webhook' }), async (c) => {
|
|
562
|
+
const billing = c.get('billing');
|
|
563
|
+
if (!billing)
|
|
564
|
+
return billingUnconfigured(c);
|
|
565
|
+
const signature = c.req.header('stripe-signature');
|
|
566
|
+
if (!signature)
|
|
567
|
+
return signatureInvalid(c);
|
|
568
|
+
const payload = await c.req.text();
|
|
569
|
+
// One endpoint, two secrets: verify against the live secret, then the
|
|
570
|
+
// sandbox secret. Test and live events are signed by different endpoint
|
|
571
|
+
// secrets, so try-both is unambiguous; the customer row's environment
|
|
572
|
+
// (below) picks the client for follow-up calls.
|
|
573
|
+
const event = await verifyEvent(billing, payload, signature);
|
|
574
|
+
if (!event)
|
|
575
|
+
return signatureInvalid(c);
|
|
576
|
+
// Events are triggers, not state carriers: on every handled event the
|
|
577
|
+
// status re-derives from fetched Stripe truth, so duplicates and
|
|
578
|
+
// out-of-order delivery are harmless. Unhandled types ack immediately.
|
|
579
|
+
if (!handledEvents.has(event.type))
|
|
580
|
+
return c.json({ received: true }, 200);
|
|
581
|
+
const customer = eventCustomer(event);
|
|
582
|
+
if (!customer)
|
|
583
|
+
return c.json({ received: true }, 200);
|
|
584
|
+
const db = Db.get(c.get('db'));
|
|
585
|
+
try {
|
|
586
|
+
// Unknown customers ack with 200: other environments may share the
|
|
587
|
+
// Stripe account, and their events are not ours to fail.
|
|
588
|
+
const record = await StripeCustomers.getByCustomer(db, customer);
|
|
589
|
+
if (!record)
|
|
590
|
+
return c.json({ received: true }, 200);
|
|
591
|
+
// The row's environment picks the mode's client; a sandbox row with
|
|
592
|
+
// no sandbox source configured acks untouched.
|
|
593
|
+
const source = stripeFor(billing, record.environment);
|
|
594
|
+
if (!source)
|
|
595
|
+
return c.json({ received: true }, 200);
|
|
596
|
+
const { client } = source;
|
|
597
|
+
// A completed setup Checkout promotes its payment method to the
|
|
598
|
+
// customer default; invoicing charges the default.
|
|
599
|
+
if (event.type === 'checkout.session.completed') {
|
|
600
|
+
const session = event.data.object;
|
|
601
|
+
if (session.mode === 'setup' && typeof session.setup_intent === 'string') {
|
|
602
|
+
const intent = await client.setupIntents.retrieve(session.setup_intent);
|
|
603
|
+
if (typeof intent.payment_method === 'string')
|
|
604
|
+
await client.customers.update(customer, {
|
|
605
|
+
invoice_settings: { default_payment_method: intent.payment_method },
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const status = await deriveStatus(client, customer);
|
|
610
|
+
// Provision the metered subscription before persisting `active`, so
|
|
611
|
+
// the gate never opens (`Billing.status` reads the stored status)
|
|
612
|
+
// without a subscription to invoice the spend against. A failure
|
|
613
|
+
// here surfaces as 5xx and Stripe redelivers.
|
|
614
|
+
if (status === 'active')
|
|
615
|
+
await core_Billing.ensureSubscription(client, customer);
|
|
616
|
+
await StripeCustomers.setStatus(db, record.orgId, status, record.environment);
|
|
617
|
+
await syncKeyBilling(c, { environment: record.environment, orgId: record.orgId, status });
|
|
618
|
+
return c.json({ received: true }, 200);
|
|
619
|
+
}
|
|
620
|
+
catch (cause) {
|
|
621
|
+
return Response.upstream(c, cause);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
/** Web Crypto signature verification; Workers have no Node `crypto` module. */
|
|
626
|
+
const cryptoProvider = Stripe.createSubtleCryptoProvider();
|
|
627
|
+
/**
|
|
628
|
+
* Resolves the Stripe source for an environment: the live bag for `production`,
|
|
629
|
+
* the test-mode `sandbox` bag otherwise. Returns undefined when sandbox billing
|
|
630
|
+
* is unconfigured, which the caller maps to `billing_unconfigured` (501).
|
|
631
|
+
*/
|
|
632
|
+
function stripeFor(billing, environment) {
|
|
633
|
+
return environment === 'production' ? billing.stripe : billing.stripe.sandbox;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Re-stamps `billingActive` onto the org's sandbox key records after a status
|
|
637
|
+
* change, so the auth throttle reflects the new state without a per-request
|
|
638
|
+
* billing read. Only sandbox is gated, and re-stamping is a no-op when the KV
|
|
639
|
+
* store is unconfigured.
|
|
640
|
+
*/
|
|
641
|
+
async function syncKeyBilling(c, options) {
|
|
642
|
+
if (options.environment !== 'sandbox')
|
|
643
|
+
return;
|
|
644
|
+
const kv = c.get('kv');
|
|
645
|
+
if (!kv)
|
|
646
|
+
return;
|
|
647
|
+
await ApiKeys.setBillingActive(kv.store, {
|
|
648
|
+
active: options.status === 'active',
|
|
649
|
+
environment: 'sandbox',
|
|
650
|
+
orgId: options.orgId,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Verifies a webhook signature against the live secret, then the sandbox
|
|
655
|
+
* secret; returns the parsed event on the first match, or undefined. The
|
|
656
|
+
* client instance is irrelevant to verification (the secret is the gate), so
|
|
657
|
+
* both attempts reuse the live client.
|
|
658
|
+
*/
|
|
659
|
+
async function verifyEvent(billing, payload, signature) {
|
|
660
|
+
const secrets = [
|
|
661
|
+
billing.stripe.webhookSecret,
|
|
662
|
+
...(billing.stripe.sandbox ? [billing.stripe.sandbox.webhookSecret] : []),
|
|
663
|
+
];
|
|
664
|
+
for (const secret of secrets) {
|
|
665
|
+
try {
|
|
666
|
+
return await billing.stripe.client.webhooks.constructEventAsync(payload, signature, secret, undefined, cryptoProvider);
|
|
667
|
+
}
|
|
668
|
+
catch { }
|
|
669
|
+
}
|
|
670
|
+
return undefined;
|
|
671
|
+
}
|
|
672
|
+
/** Event types that funnel into status derivation; everything else acks untouched. */
|
|
673
|
+
const handledEvents = new Set([
|
|
674
|
+
'checkout.session.completed',
|
|
675
|
+
'customer.deleted',
|
|
676
|
+
'customer.subscription.deleted',
|
|
677
|
+
'customer.subscription.updated',
|
|
678
|
+
'customer.updated',
|
|
679
|
+
'invoice.paid',
|
|
680
|
+
'invoice.payment_failed',
|
|
681
|
+
'payment_method.attached',
|
|
682
|
+
'payment_method.detached',
|
|
683
|
+
'setup_intent.succeeded',
|
|
684
|
+
]);
|
|
685
|
+
/** Stripe customer id an event concerns; detached payment methods only carry it in `previous_attributes`. */
|
|
686
|
+
function eventCustomer(event) {
|
|
687
|
+
const object = event.data.object;
|
|
688
|
+
if (object.object === 'customer')
|
|
689
|
+
return object.id;
|
|
690
|
+
if (typeof object.customer === 'string')
|
|
691
|
+
return object.customer;
|
|
692
|
+
const previous = event.data.previous_attributes;
|
|
693
|
+
return typeof previous?.customer === 'string' ? previous.customer : undefined;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Billing status from live Stripe state: deleted customers are `canceled`; an
|
|
697
|
+
* unpaid metered subscription parks the org at `past_due`; a default payment
|
|
698
|
+
* method or any attached card is `active`; else `none`.
|
|
699
|
+
*/
|
|
700
|
+
async function deriveStatus(stripe, customerId) {
|
|
701
|
+
const customer = await stripe.customers.retrieve(customerId);
|
|
702
|
+
if (customer.deleted)
|
|
703
|
+
return 'canceled';
|
|
704
|
+
const subscriptions = await stripe.subscriptions.list({ customer: customerId, limit: 100 });
|
|
705
|
+
if (subscriptions.data.some((s) => s.status === 'past_due' || s.status === 'unpaid'))
|
|
706
|
+
return 'past_due';
|
|
707
|
+
if (customer.invoice_settings.default_payment_method)
|
|
708
|
+
return 'active';
|
|
709
|
+
const methods = await stripe.customers.listPaymentMethods(customerId, { limit: 1, type: 'card' }); // prettier-ignore
|
|
710
|
+
return methods.data.length > 0 ? 'active' : 'none';
|
|
711
|
+
}
|
|
712
|
+
/** Builds the billing response body: source status, configured limits, current-window spend. */
|
|
713
|
+
async function billingBody(c, orgId, environment) {
|
|
714
|
+
const db = Db.get(c.get('db'));
|
|
715
|
+
const [record, settings] = await Promise.all([
|
|
716
|
+
StripeCustomers.get(db, orgId, environment),
|
|
717
|
+
BillingSettings.get(db, orgId, environment),
|
|
718
|
+
]);
|
|
719
|
+
const currency = settings?.currency ?? 'usd';
|
|
720
|
+
const period = settings?.period ?? 'month';
|
|
721
|
+
// Mirrors the sponsorship gate's scope so the console shows the figure the
|
|
722
|
+
// limit is enforced against; sandbox spend is testnet, display-only.
|
|
723
|
+
const spend = await SponsoredTransactions.spend(Db.get(c.get('dbCached')), {
|
|
724
|
+
chainIds: [environment === 'production' ? Viem.chainId.mainnet : Viem.chainId.testnet],
|
|
725
|
+
environment,
|
|
726
|
+
orgId,
|
|
727
|
+
since: core_Billing.periodStart(period),
|
|
728
|
+
});
|
|
729
|
+
return {
|
|
730
|
+
spend: { amount: core_Billing.fromBaseUnits(spend), currency, period },
|
|
731
|
+
...(settings?.spendLimit != null
|
|
732
|
+
? { spendLimit: { amount: settings.spendLimit, currency, period } }
|
|
733
|
+
: {}),
|
|
734
|
+
status: record?.status ?? 'none',
|
|
735
|
+
...(settings?.txFeeLimit != null
|
|
736
|
+
? { txFeeLimit: { amount: settings.txFeeLimit, currency } }
|
|
737
|
+
: {}),
|
|
738
|
+
...(record ? { updatedAt: record.updatedAt } : {}),
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Console billing page a Stripe session returns to: the caller's `Origin` when
|
|
743
|
+
* present, else the configured console URL. Sandbox sessions carry `env=sandbox`
|
|
744
|
+
* so the full-page Stripe round-trip returns to the same console mode (the
|
|
745
|
+
* client-side `retainSearchParams` can't survive it); `status` sets `billing`.
|
|
746
|
+
*/
|
|
747
|
+
function returnUrl(c, options) {
|
|
748
|
+
const { environment, orgId, status, stripe } = options;
|
|
749
|
+
// Origin is safe to trust: SameSite=Lax session cookies never authenticate
|
|
750
|
+
// cross-site POSTs, so a forged Origin only redirects the caller themselves.
|
|
751
|
+
const origin = c.req.header('origin');
|
|
752
|
+
const base = (() => {
|
|
753
|
+
try {
|
|
754
|
+
if (origin)
|
|
755
|
+
return new URL(origin).origin;
|
|
756
|
+
}
|
|
757
|
+
catch { }
|
|
758
|
+
return (stripe.consoleUrl ?? 'https://console.tempo.xyz').replace(/\/$/, '');
|
|
759
|
+
})();
|
|
760
|
+
const url = new URL(`${base}/${orgId}/billing`);
|
|
761
|
+
if (environment === 'sandbox')
|
|
762
|
+
url.searchParams.set('env', 'sandbox');
|
|
763
|
+
if (status)
|
|
764
|
+
url.searchParams.set('billing', status);
|
|
765
|
+
return url.toString();
|
|
766
|
+
}
|
|
767
|
+
function signatureInvalid(c) {
|
|
768
|
+
return Response.error(c, {
|
|
769
|
+
code: 'signature_invalid',
|
|
770
|
+
message: 'Missing or invalid Stripe signature',
|
|
771
|
+
status: 400,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
function billingNotFound(c) {
|
|
775
|
+
return Response.error(c, {
|
|
776
|
+
code: 'billing_not_found',
|
|
777
|
+
message: 'No billing to manage; attach a payment method first',
|
|
778
|
+
status: 404,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
function billingUnconfigured(c) {
|
|
782
|
+
return Response.error(c, {
|
|
783
|
+
code: 'billing_unconfigured',
|
|
784
|
+
message: 'Billing is not configured on this deployment',
|
|
785
|
+
status: 501,
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
function paymentMethodNotFound(c) {
|
|
789
|
+
return Response.error(c, {
|
|
790
|
+
code: 'payment_method_not_found',
|
|
791
|
+
message: 'No payment method was found for this id',
|
|
792
|
+
status: 404,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
//# sourceMappingURL=billing.js.map
|