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.
Files changed (313) hide show
  1. package/README.md +31 -3
  2. package/dist/ApiKey.d.ts +1 -0
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +5 -2
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +28 -1
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +34 -1
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +7167 -4507
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +8 -0
  13. package/dist/App.js.map +1 -1
  14. package/dist/Scope.d.ts +11 -2
  15. package/dist/Scope.d.ts.map +1 -1
  16. package/dist/Scope.js +14 -3
  17. package/dist/Scope.js.map +1 -1
  18. package/dist/admin/App.d.ts +128 -118
  19. package/dist/admin/App.d.ts.map +1 -1
  20. package/dist/admin/apps/api-keys.d.ts +7 -0
  21. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  22. package/dist/admin/apps/scopes.d.ts +8 -0
  23. package/dist/admin/apps/scopes.d.ts.map +1 -1
  24. package/dist/admin/apps/scopes.js +3 -0
  25. package/dist/admin/apps/scopes.js.map +1 -1
  26. package/dist/admin/apps/verified-tokens.d.ts +57 -57
  27. package/dist/admin/ui.generated.d.ts +1 -1
  28. package/dist/admin/ui.generated.js +13 -13
  29. package/dist/admin/ui.generated.js.map +1 -1
  30. package/dist/analytics/Analytics.d.ts +151 -0
  31. package/dist/analytics/Analytics.d.ts.map +1 -0
  32. package/dist/analytics/Analytics.js +212 -0
  33. package/dist/analytics/Analytics.js.map +1 -0
  34. package/dist/analytics/tables/requestEvents.d.ts +215 -0
  35. package/dist/analytics/tables/requestEvents.d.ts.map +1 -0
  36. package/dist/analytics/tables/requestEvents.js +264 -0
  37. package/dist/analytics/tables/requestEvents.js.map +1 -0
  38. package/dist/apps/data/App.d.ts +1024 -624
  39. package/dist/apps/data/App.d.ts.map +1 -1
  40. package/dist/apps/data/App.js +36 -0
  41. package/dist/apps/data/App.js.map +1 -1
  42. package/dist/apps/data/routes/balances.d.ts +45 -45
  43. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  44. package/dist/apps/data/routes/exchanges.d.ts +455 -455
  45. package/dist/apps/data/routes/fee-amm.d.ts +101 -101
  46. package/dist/apps/data/routes/funding.d.ts +765 -0
  47. package/dist/apps/data/routes/funding.d.ts.map +1 -0
  48. package/dist/apps/data/routes/funding.js +383 -0
  49. package/dist/apps/data/routes/funding.js.map +1 -0
  50. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  51. package/dist/apps/data/routes/receipts.js +2 -8
  52. package/dist/apps/data/routes/receipts.js.map +1 -1
  53. package/dist/apps/data/routes/rpc.d.ts +29 -36
  54. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  55. package/dist/apps/data/routes/rpc.js +29 -1
  56. package/dist/apps/data/routes/rpc.js.map +1 -1
  57. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  58. package/dist/apps/data/routes/transfers.d.ts +56 -56
  59. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  60. package/dist/apps/data/routes/webhooks.d.ts +45 -45
  61. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  62. package/dist/apps/data/routes/webhooks.js +8 -8
  63. package/dist/apps/data/routes/webhooks.js.map +1 -1
  64. package/dist/apps/docs/App.d.ts +4 -2
  65. package/dist/apps/docs/App.d.ts.map +1 -1
  66. package/dist/apps/docs/App.js +27 -95
  67. package/dist/apps/docs/App.js.map +1 -1
  68. package/dist/apps/docs/reference.d.ts +21 -0
  69. package/dist/apps/docs/reference.d.ts.map +1 -0
  70. package/dist/apps/docs/reference.js +112 -0
  71. package/dist/apps/docs/reference.js.map +1 -0
  72. package/dist/apps/index.d.ts +1 -0
  73. package/dist/apps/index.d.ts.map +1 -1
  74. package/dist/apps/index.js +1 -0
  75. package/dist/apps/index.js.map +1 -1
  76. package/dist/apps/mcp.d.ts +76 -0
  77. package/dist/apps/mcp.d.ts.map +1 -0
  78. package/dist/apps/mcp.js +188 -0
  79. package/dist/apps/mcp.js.map +1 -0
  80. package/dist/apps/platform/App.d.ts +3005 -728
  81. package/dist/apps/platform/App.d.ts.map +1 -1
  82. package/dist/apps/platform/App.js +13 -3
  83. package/dist/apps/platform/App.js.map +1 -1
  84. package/dist/apps/platform/Billing.d.ts +279 -0
  85. package/dist/apps/platform/Billing.d.ts.map +1 -0
  86. package/dist/apps/platform/Billing.js +477 -0
  87. package/dist/apps/platform/Billing.js.map +1 -0
  88. package/dist/apps/platform/index.d.ts +1 -0
  89. package/dist/apps/platform/index.d.ts.map +1 -1
  90. package/dist/apps/platform/index.js +1 -0
  91. package/dist/apps/platform/index.js.map +1 -1
  92. package/dist/apps/platform/routes/api-keys.d.ts +341 -49
  93. package/dist/apps/platform/routes/api-keys.d.ts.map +1 -1
  94. package/dist/apps/platform/routes/api-keys.js +115 -13
  95. package/dist/apps/platform/routes/api-keys.js.map +1 -1
  96. package/dist/apps/platform/routes/billing.d.ts +1520 -0
  97. package/dist/apps/platform/routes/billing.d.ts.map +1 -0
  98. package/dist/apps/platform/routes/billing.js +795 -0
  99. package/dist/apps/platform/routes/billing.js.map +1 -0
  100. package/dist/apps/platform/routes/invitations.d.ts +201 -83
  101. package/dist/apps/platform/routes/invitations.d.ts.map +1 -1
  102. package/dist/apps/platform/routes/invitations.js +58 -0
  103. package/dist/apps/platform/routes/invitations.js.map +1 -1
  104. package/dist/apps/platform/routes/me.d.ts +17 -17
  105. package/dist/apps/platform/routes/orgs.d.ts +50 -36
  106. package/dist/apps/platform/routes/orgs.d.ts.map +1 -1
  107. package/dist/apps/platform/routes/orgs.js +14 -3
  108. package/dist/apps/platform/routes/orgs.js.map +1 -1
  109. package/dist/apps/platform/routes/projects.d.ts +97 -97
  110. package/dist/apps/platform/routes/scopes.d.ts +107 -0
  111. package/dist/apps/platform/routes/scopes.d.ts.map +1 -0
  112. package/dist/apps/platform/routes/scopes.js +66 -0
  113. package/dist/apps/platform/routes/scopes.js.map +1 -0
  114. package/dist/apps/platform/routes/usage.d.ts +588 -0
  115. package/dist/apps/platform/routes/usage.d.ts.map +1 -0
  116. package/dist/apps/platform/routes/usage.js +391 -0
  117. package/dist/apps/platform/routes/usage.js.map +1 -0
  118. package/dist/apps/relay/App.d.ts +4 -1
  119. package/dist/apps/relay/App.d.ts.map +1 -1
  120. package/dist/apps/relay/App.js +118 -10
  121. package/dist/apps/relay/App.js.map +1 -1
  122. package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
  123. package/dist/apps/relay/Sponsorships.js +2 -1
  124. package/dist/apps/relay/Sponsorships.js.map +1 -1
  125. package/dist/cli/Cli.d.ts +8 -1
  126. package/dist/cli/Cli.d.ts.map +1 -1
  127. package/dist/cli/Cli.js +11 -3
  128. package/dist/cli/Cli.js.map +1 -1
  129. package/dist/cli/commands/admin.d.ts +4 -0
  130. package/dist/cli/commands/admin.d.ts.map +1 -1
  131. package/dist/cli/commands/admin.js +80 -1
  132. package/dist/cli/commands/admin.js.map +1 -1
  133. package/dist/db/Db.d.ts +7 -0
  134. package/dist/db/Db.d.ts.map +1 -1
  135. package/dist/db/Db.js +126 -0
  136. package/dist/db/Db.js.map +1 -1
  137. package/dist/db/Schema.d.ts +79 -0
  138. package/dist/db/Schema.d.ts.map +1 -1
  139. package/dist/db/Schema.js +88 -2
  140. package/dist/db/Schema.js.map +1 -1
  141. package/dist/db/tables/billingSettings.d.ts +55 -0
  142. package/dist/db/tables/billingSettings.d.ts.map +1 -0
  143. package/dist/db/tables/billingSettings.js +70 -0
  144. package/dist/db/tables/billingSettings.js.map +1 -0
  145. package/dist/db/tables/invitations.js +2 -2
  146. package/dist/db/tables/invitations.js.map +1 -1
  147. package/dist/db/tables/organizations.d.ts +6 -2
  148. package/dist/db/tables/organizations.d.ts.map +1 -1
  149. package/dist/db/tables/organizations.js +10 -4
  150. package/dist/db/tables/organizations.js.map +1 -1
  151. package/dist/db/tables/projects.js +2 -2
  152. package/dist/db/tables/projects.js.map +1 -1
  153. package/dist/db/tables/requestUsage.d.ts +121 -0
  154. package/dist/db/tables/requestUsage.d.ts.map +1 -0
  155. package/dist/db/tables/requestUsage.js +150 -0
  156. package/dist/db/tables/requestUsage.js.map +1 -0
  157. package/dist/db/tables/sponsoredTransactions.d.ts +119 -0
  158. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  159. package/dist/db/tables/sponsoredTransactions.js +155 -2
  160. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  161. package/dist/db/tables/stripeCustomers.d.ts +81 -0
  162. package/dist/db/tables/stripeCustomers.d.ts.map +1 -0
  163. package/dist/db/tables/stripeCustomers.js +120 -0
  164. package/dist/db/tables/stripeCustomers.js.map +1 -0
  165. package/dist/db/tables/users.js +2 -2
  166. package/dist/db/tables/users.js.map +1 -1
  167. package/dist/handlers/internal/sponsorship.d.ts +5 -2
  168. package/dist/handlers/internal/sponsorship.d.ts.map +1 -1
  169. package/dist/handlers/internal/sponsorship.js +19 -5
  170. package/dist/handlers/internal/sponsorship.js.map +1 -1
  171. package/dist/handlers/internal/utils.d.ts +10 -3
  172. package/dist/handlers/internal/utils.d.ts.map +1 -1
  173. package/dist/handlers/internal/utils.js +14 -6
  174. package/dist/handlers/internal/utils.js.map +1 -1
  175. package/dist/index.d.ts +1 -0
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +1 -0
  178. package/dist/index.js.map +1 -1
  179. package/dist/internal/Auth.d.ts +18 -4
  180. package/dist/internal/Auth.d.ts.map +1 -1
  181. package/dist/internal/Auth.js +148 -76
  182. package/dist/internal/Auth.js.map +1 -1
  183. package/dist/internal/Cache.d.ts.map +1 -1
  184. package/dist/internal/Cache.js +4 -1
  185. package/dist/internal/Cache.js.map +1 -1
  186. package/dist/internal/EdgeCache.d.ts +10 -0
  187. package/dist/internal/EdgeCache.d.ts.map +1 -1
  188. package/dist/internal/EdgeCache.js +39 -8
  189. package/dist/internal/EdgeCache.js.map +1 -1
  190. package/dist/internal/Fees.d.ts +27 -0
  191. package/dist/internal/Fees.d.ts.map +1 -0
  192. package/dist/internal/Fees.js +48 -0
  193. package/dist/internal/Fees.js.map +1 -0
  194. package/dist/internal/FundingRoutes.d.ts +183 -0
  195. package/dist/internal/FundingRoutes.d.ts.map +1 -0
  196. package/dist/internal/FundingRoutes.js +490 -0
  197. package/dist/internal/FundingRoutes.js.map +1 -0
  198. package/dist/internal/Id.d.ts +15 -0
  199. package/dist/internal/Id.d.ts.map +1 -0
  200. package/dist/internal/Id.js +22 -0
  201. package/dist/internal/Id.js.map +1 -0
  202. package/dist/internal/Log.d.ts +2 -0
  203. package/dist/internal/Log.d.ts.map +1 -1
  204. package/dist/internal/Log.js +3 -0
  205. package/dist/internal/Log.js.map +1 -1
  206. package/dist/internal/OpenApi.d.ts +10 -1
  207. package/dist/internal/OpenApi.d.ts.map +1 -1
  208. package/dist/internal/OpenApi.js +8 -1
  209. package/dist/internal/OpenApi.js.map +1 -1
  210. package/dist/internal/RateLimit.d.ts +25 -0
  211. package/dist/internal/RateLimit.d.ts.map +1 -1
  212. package/dist/internal/RateLimit.js +51 -0
  213. package/dist/internal/RateLimit.js.map +1 -1
  214. package/dist/internal/Response.d.ts +1 -1
  215. package/dist/internal/Response.d.ts.map +1 -1
  216. package/dist/internal/Schema.d.ts +8 -0
  217. package/dist/internal/Schema.d.ts.map +1 -1
  218. package/dist/internal/Schema.js +7 -0
  219. package/dist/internal/Schema.js.map +1 -1
  220. package/dist/internal/Webhooks.d.ts.map +1 -1
  221. package/dist/internal/Webhooks.js +3 -3
  222. package/dist/internal/Webhooks.js.map +1 -1
  223. package/package.json +4 -3
  224. package/src/ApiKey.ts +9 -2
  225. package/src/ApiKeys.test.ts +51 -0
  226. package/src/ApiKeys.ts +50 -1
  227. package/src/App.test.ts +33 -86718
  228. package/src/App.ts +18 -0
  229. package/src/Client.test-d.ts +351 -0
  230. package/src/Scope.ts +16 -3
  231. package/src/admin/apps/scopes.test.ts +6 -0
  232. package/src/admin/apps/scopes.ts +3 -0
  233. package/src/admin/ui/src/components/Page.tsx +2 -3
  234. package/src/admin/ui/src/routes/__root.tsx +2 -2
  235. package/src/admin/ui/src/routes/api-keys.tsx +2 -2
  236. package/src/admin/ui.generated.ts +13 -13
  237. package/src/analytics/Analytics.test.ts +527 -0
  238. package/src/analytics/Analytics.ts +318 -0
  239. package/src/analytics/tables/requestEvents.ts +523 -0
  240. package/src/apps/data/App.test.ts +72 -0
  241. package/src/apps/data/App.ts +35 -0
  242. package/src/apps/data/routes/funding.test.ts +214 -0
  243. package/src/apps/data/routes/funding.ts +584 -0
  244. package/src/apps/data/routes/receipts.ts +2 -9
  245. package/src/apps/data/routes/rpc.test.ts +78 -2
  246. package/src/apps/data/routes/rpc.ts +31 -1
  247. package/src/apps/data/routes/webhooks.test.ts +1 -1
  248. package/src/apps/data/routes/webhooks.ts +11 -8
  249. package/src/apps/docs/App.ts +29 -100
  250. package/src/apps/docs/reference.ts +132 -0
  251. package/src/apps/index.ts +1 -0
  252. package/src/apps/mcp.test.ts +206 -0
  253. package/src/apps/mcp.ts +250 -0
  254. package/src/apps/platform/App.ts +43 -4
  255. package/src/apps/platform/Billing.test.ts +530 -0
  256. package/src/apps/platform/Billing.ts +682 -0
  257. package/src/apps/platform/index.ts +1 -0
  258. package/src/apps/platform/routes/api-keys.test.ts +172 -9
  259. package/src/apps/platform/routes/api-keys.ts +136 -20
  260. package/src/apps/platform/routes/billing.test.ts +1100 -0
  261. package/src/apps/platform/routes/billing.ts +966 -0
  262. package/src/apps/platform/routes/invitations.test.ts +55 -7
  263. package/src/apps/platform/routes/invitations.ts +68 -0
  264. package/src/apps/platform/routes/me.test.ts +230 -0
  265. package/src/apps/platform/routes/orgs.ts +26 -4
  266. package/src/apps/platform/routes/scopes.test.ts +29 -0
  267. package/src/apps/platform/routes/scopes.ts +85 -0
  268. package/src/apps/platform/routes/usage.test.ts +422 -0
  269. package/src/apps/platform/routes/usage.ts +527 -0
  270. package/src/apps/relay/App.test.ts +457 -8
  271. package/src/apps/relay/App.ts +120 -12
  272. package/src/apps/relay/Sponsorships.test.ts +1 -1
  273. package/src/apps/relay/Sponsorships.ts +2 -1
  274. package/src/cli/Cli.test.ts +25 -2
  275. package/src/cli/Cli.ts +17 -2
  276. package/src/cli/commands/admin.test.ts +83 -0
  277. package/src/cli/commands/admin.ts +89 -1
  278. package/src/db/Db.ts +137 -0
  279. package/src/db/Schema.ts +104 -2
  280. package/src/db/tables/billingSettings.test.ts +87 -0
  281. package/src/db/tables/billingSettings.ts +108 -0
  282. package/src/db/tables/invitations.ts +2 -2
  283. package/src/db/tables/organizations.test.ts +1 -1
  284. package/src/db/tables/organizations.ts +15 -5
  285. package/src/db/tables/projects.ts +2 -2
  286. package/src/db/tables/requestUsage.ts +231 -0
  287. package/src/db/tables/sponsoredTransactions.test.ts +108 -1
  288. package/src/db/tables/sponsoredTransactions.ts +230 -2
  289. package/src/db/tables/stripeCustomers.test.ts +109 -0
  290. package/src/db/tables/stripeCustomers.ts +162 -0
  291. package/src/db/tables/users.ts +2 -2
  292. package/src/handlers/internal/sponsorship.ts +34 -7
  293. package/src/handlers/internal/utils.ts +19 -7
  294. package/src/index.ts +1 -0
  295. package/src/internal/Auth.test.ts +175 -25
  296. package/src/internal/Auth.ts +175 -86
  297. package/src/internal/Cache.ts +5 -1
  298. package/src/internal/EdgeCache.test.ts +27 -4
  299. package/src/internal/EdgeCache.ts +47 -9
  300. package/src/internal/Fees.test.ts +32 -0
  301. package/src/internal/Fees.ts +51 -0
  302. package/src/internal/FundingRoutes.ts +583 -0
  303. package/src/internal/Id.test.ts +17 -0
  304. package/src/internal/Id.ts +24 -0
  305. package/src/internal/Log.test.ts +1 -0
  306. package/src/internal/Log.ts +5 -0
  307. package/src/internal/OpenApi.ts +12 -1
  308. package/src/internal/RateLimit.test.ts +84 -0
  309. package/src/internal/RateLimit.ts +70 -0
  310. package/src/internal/Response.ts +1 -1
  311. package/src/internal/Schema.ts +8 -0
  312. package/src/internal/Webhooks.test.ts +3 -3
  313. package/src/internal/Webhooks.ts +3 -3
@@ -0,0 +1,682 @@
1
+ import { Hash, Hex, Value } from 'ox'
2
+ import type Stripe from 'stripe'
3
+
4
+ import type * as Store from '../../internal/Store.js'
5
+ import * as ApiKeys from '../../ApiKeys.js'
6
+ import type * as BillingSettings from '../../db/tables/billingSettings.js'
7
+ import * as Db from '../../db/Db.js'
8
+ import * as Fees from '../../internal/Fees.js'
9
+ import * as RequestUsage from '../../db/tables/requestUsage.js'
10
+ import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
11
+ import * as StripeCustomers from '../../db/tables/stripeCustomers.js'
12
+ import * as Viem from '../../internal/Viem.js'
13
+
14
+ /**
15
+ * Whether the organization has any active billing source — the production
16
+ * sponsorship gate's read. Storage stays one table per source; this resolver
17
+ * is the union across them (Stripe today).
18
+ *
19
+ * @param db - The database.
20
+ * @param orgId - The organization id (`org_…`).
21
+ * @returns Whether any billing source is active.
22
+ */
23
+ export async function active(
24
+ db: Db.Db,
25
+ orgId: string,
26
+ environment: StripeCustomers.Record['environment'] = 'production',
27
+ ): Promise<boolean> {
28
+ return (await status(db, orgId, environment)) === 'active'
29
+ }
30
+
31
+ /**
32
+ * The organization's billing status derived from its stored billing source,
33
+ * or undefined when it has none. The sponsorship gate maps non-`active`
34
+ * statuses to distinct refusal reasons.
35
+ *
36
+ * @param db - The database.
37
+ * @param orgId - The organization id (`org_…`).
38
+ * @returns The stored status, or undefined.
39
+ */
40
+ export async function status(
41
+ db: Db.Db,
42
+ orgId: string,
43
+ environment: StripeCustomers.Record['environment'] = 'production',
44
+ ): Promise<StripeCustomers.Record['status'] | undefined> {
45
+ const record = await StripeCustomers.get(db, orgId, environment)
46
+ return record?.status
47
+ }
48
+
49
+ /**
50
+ * Converts a decimal currency string (e.g. `'0.50'`) to fee-token base units.
51
+ *
52
+ * @param amount - The decimal string; at most {@link Fees.tokenDecimals} fraction digits.
53
+ * @returns The amount in base units.
54
+ */
55
+ export function toBaseUnits(amount: string): bigint {
56
+ return Value.from(amount, Fees.tokenDecimals)
57
+ }
58
+
59
+ /**
60
+ * Converts fee-token base units to a decimal currency string with trailing
61
+ * zeros trimmed (e.g. `350000n` → `'0.35'`).
62
+ *
63
+ * @param units - The amount in base units.
64
+ * @returns The decimal string.
65
+ */
66
+ export function fromBaseUnits(units: bigint): string {
67
+ return Value.format(units, Fees.tokenDecimals)
68
+ }
69
+
70
+ /**
71
+ * Start of the limit window containing `now` (ISO 8601); `month` is the UTC
72
+ * calendar month.
73
+ *
74
+ * @param period - The configured window.
75
+ * @param now - Reference time; defaults to now.
76
+ * @returns The window start.
77
+ */
78
+ export function periodStart(period: BillingSettings.Record['period'], now = new Date()): string {
79
+ // Single-member today; a switch lands with the second period.
80
+ void period
81
+ return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString()
82
+ }
83
+
84
+ /**
85
+ * A metered billing dimension: a Stripe meter plus its metered monthly price.
86
+ * Each kind bills as a distinct invoice line item on one shared subscription.
87
+ */
88
+ export type MeterKind = 'apiRequests' | 'feePayerSpend'
89
+
90
+ /** Descriptor for one metered dimension. */
91
+ export type MeterDescriptor = {
92
+ /** Human-readable meter/product display name. */
93
+ displayName: string
94
+ /** Meter event name; the Stripe meter aggregates `sum(value)` per customer per period. */
95
+ eventName: string
96
+ /** Versioned price lookup key; changing economics ships a new key, never a mutated price. */
97
+ lookupKey: string
98
+ /** Price per meter unit in cents (`unit_amount_decimal`). */
99
+ unitAmountDecimal: string
100
+ }
101
+
102
+ /**
103
+ * The metered dimensions and their unit economics. A price pins cents per meter
104
+ * unit; changing them ships a new versioned `lookupKey`, never a mutated price.
105
+ */
106
+ export const meters = {
107
+ apiRequests: {
108
+ displayName: 'API Requests',
109
+ eventName: 'api_request_count',
110
+ lookupKey: 'api-request-count-v1',
111
+ // $0.0001/request → 0.01¢ per unit; the meter value is the integer request count.
112
+ unitAmountDecimal: '0.01',
113
+ },
114
+ feePayerSpend: {
115
+ displayName: 'Sponsor Usage',
116
+ eventName: 'fee_payer_spend',
117
+ lookupKey: 'fee-payer-spend-v1',
118
+ // Meter values are 6-decimal base units: 100¢ per 1e6 units.
119
+ unitAmountDecimal: '0.0001',
120
+ },
121
+ } as const satisfies Record<MeterKind, MeterDescriptor>
122
+
123
+ /**
124
+ * The metered dimensions billed in an environment. Production bills sponsorship
125
+ * spend and request counts; sandbox bills request counts only.
126
+ *
127
+ * @param environment - The environment.
128
+ * @returns The metered dimensions for that environment.
129
+ */
130
+ export function kindsFor(environment: StripeCustomers.Record['environment']): readonly MeterKind[] {
131
+ return environment === 'production' ? ['feePayerSpend', 'apiRequests'] : ['apiRequests']
132
+ }
133
+
134
+ /** Back-compat: the fee-payer meter event name. */
135
+ export const meterEventName = meters.feePayerSpend.eventName
136
+
137
+ /** Back-compat: the fee-payer metered price lookup key. */
138
+ export const priceLookupKey = meters.feePayerSpend.lookupKey
139
+
140
+ /** Fixture promises memoized per client, per kind; a failed ensure retries on the next call. */
141
+ const fixturesByClient = new WeakMap<Stripe, Map<MeterKind, Promise<ensureFixtures.Fixtures>>>()
142
+
143
+ /**
144
+ * Ensures the account-level metering fixtures exist for a kind: its meter and
145
+ * metered monthly price. Idempotent by lookup (meter `event_name`, price
146
+ * `lookup_key`) and memoized per `(client, kind)`.
147
+ *
148
+ * @param stripe - The Stripe client.
149
+ * @param kind - The metered dimension; defaults to fee-payer spend.
150
+ * @returns The fixture ids.
151
+ */
152
+ export function ensureFixtures(
153
+ stripe: Stripe,
154
+ kind: MeterKind = 'feePayerSpend',
155
+ ): Promise<ensureFixtures.Fixtures> {
156
+ let byKind = fixturesByClient.get(stripe)
157
+ if (!byKind) {
158
+ byKind = new Map()
159
+ fixturesByClient.set(stripe, byKind)
160
+ }
161
+ const pending = byKind.get(kind)
162
+ if (pending) return pending
163
+ const descriptor = meters[kind]
164
+ const created = (async () => {
165
+ const meterList = await stripe.billing.meters.list({ limit: 100, status: 'active' })
166
+ const meter =
167
+ meterList.data.find((meter) => meter.event_name === descriptor.eventName) ??
168
+ (await stripe.billing.meters.create({
169
+ customer_mapping: { event_payload_key: 'stripe_customer_id', type: 'by_id' },
170
+ default_aggregation: { formula: 'sum' },
171
+ display_name: descriptor.displayName,
172
+ event_name: descriptor.eventName,
173
+ value_settings: { event_payload_key: 'value' },
174
+ }))
175
+ const prices = await stripe.prices.list({ limit: 1, lookup_keys: [descriptor.lookupKey] })
176
+ const price =
177
+ prices.data[0] ??
178
+ (await stripe.prices.create({
179
+ billing_scheme: 'per_unit',
180
+ currency: 'usd',
181
+ lookup_key: descriptor.lookupKey,
182
+ product_data: { name: descriptor.displayName },
183
+ recurring: { interval: 'month', meter: meter.id, usage_type: 'metered' },
184
+ unit_amount_decimal: descriptor.unitAmountDecimal,
185
+ }))
186
+ return { meterId: meter.id, priceId: price.id }
187
+ })()
188
+ byKind.set(kind, created)
189
+ created.catch(() => byKind.delete(kind))
190
+ return created
191
+ }
192
+
193
+ export declare namespace ensureFixtures {
194
+ /** Account-level metering fixture ids. */
195
+ type Fixtures = {
196
+ /** Spend meter id (`mtr_…`). */
197
+ meterId: string
198
+ /** Metered price id (`price_…`). */
199
+ priceId: string
200
+ }
201
+ }
202
+
203
+ /** Subscription statuses that no longer bill; a new subscription must be created. */
204
+ const terminalStatuses = ['canceled', 'incomplete_expired']
205
+
206
+ /**
207
+ * Ensures the customer holds exactly one live subscription carrying one metered
208
+ * item per requested {@link MeterKind}, creating the subscription or adding
209
+ * missing items as needed. Metered usage bills the customer's default payment
210
+ * method monthly.
211
+ *
212
+ * Fail-closed on more than one live managed subscription: duplicates would
213
+ * double-bill the same meter usage, so the caller must reconcile manually.
214
+ *
215
+ * @param stripe - The Stripe client.
216
+ * @param customerId - The Stripe customer id.
217
+ * @param kinds - The metered dimensions to ensure; defaults to fee-payer spend.
218
+ * @returns The per-kind creation state and whether a subscription was created.
219
+ */
220
+ export async function ensureSubscription(
221
+ stripe: Stripe,
222
+ customerId: string,
223
+ kinds: readonly MeterKind[] = ['feePayerSpend'],
224
+ ): Promise<ensureSubscription.Result> {
225
+ const requested = kinds.length ? kinds : (['feePayerSpend'] as const)
226
+ // Resolve every requested kind's price; managed prices are what we reconcile.
227
+ const fixtures = new Map<MeterKind, ensureFixtures.Fixtures>()
228
+ for (const kind of requested) fixtures.set(kind, await ensureFixtures(stripe, kind))
229
+ const priceToKind = new Map<string, MeterKind>()
230
+ for (const [kind, fixture] of fixtures) priceToKind.set(fixture.priceId, kind)
231
+
232
+ const subscriptions = await stripe.subscriptions.list({
233
+ customer: customerId,
234
+ limit: 100,
235
+ status: 'all',
236
+ })
237
+ const liveManaged = subscriptions.data.filter(
238
+ (subscription) =>
239
+ !terminalStatuses.includes(subscription.status) &&
240
+ subscription.items.data.some((item) => priceToKind.has(item.price.id)),
241
+ )
242
+ if (liveManaged.length > 1) throw new DuplicateSubscriptionError(customerId)
243
+
244
+ const items: Record<string, ensureSubscription.ItemResult> = {}
245
+ const subscription = liveManaged[0]
246
+ if (subscription) {
247
+ const present = new Set(
248
+ subscription.items.data
249
+ .map((item) => priceToKind.get(item.price.id))
250
+ .filter((kind): kind is MeterKind => kind !== undefined),
251
+ )
252
+ for (const kind of requested) {
253
+ if (present.has(kind)) {
254
+ items[kind] = { created: false }
255
+ continue
256
+ }
257
+ // Add the missing item; the idempotency key absorbs concurrent adds.
258
+ await stripe.subscriptionItems.create(
259
+ { price: fixtures.get(kind)!.priceId, subscription: subscription.id },
260
+ { idempotencyKey: `${customerId}-item-${meters[kind].lookupKey}` },
261
+ )
262
+ items[kind] = { created: true }
263
+ }
264
+ return { created: false, items }
265
+ }
266
+
267
+ // The idempotency key absorbs concurrent activations: a duplicate metered
268
+ // subscription would double-bill the same meter usage.
269
+ await stripe.subscriptions.create(
270
+ {
271
+ collection_method: 'charge_automatically',
272
+ customer: customerId,
273
+ items: requested.map((kind) => ({ price: fixtures.get(kind)!.priceId })),
274
+ },
275
+ { idempotencyKey: `${customerId}-metered-sub-v2` },
276
+ )
277
+ for (const kind of requested) items[kind] = { created: true }
278
+ return { created: true, items }
279
+ }
280
+
281
+ export declare namespace ensureSubscription {
282
+ /** Per-kind item reconciliation state. */
283
+ type ItemResult = {
284
+ /** Whether this call added the item (a fresh item bills nothing before its start). */
285
+ created: boolean
286
+ }
287
+
288
+ /** Outcome of {@link ensureSubscription}. */
289
+ type Result = {
290
+ /** Whether this call created the subscription (none was live before). */
291
+ created: boolean
292
+ /** Per-kind item state, keyed by {@link MeterKind}. */
293
+ items: Partial<Record<MeterKind, ItemResult>>
294
+ }
295
+ }
296
+
297
+ /** More than one live managed subscription for a customer; requires manual reconcile. */
298
+ export class DuplicateSubscriptionError extends Error {
299
+ constructor(customerId: string) {
300
+ super(`multiple live metered subscriptions for customer ${customerId}`)
301
+ this.name = 'Billing.DuplicateSubscriptionError'
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Creates the metering job: each tick reports finalized, unreported billable
307
+ * mainnet spend to the Stripe meter and marks rows reported. A host runtime
308
+ * drives {@link createReporter.Reporter.tick} on a timer, like the
309
+ * sponsorship finalizer.
310
+ */
311
+ export function createReporter(options: createReporter.Options): createReporter.Reporter {
312
+ const { batchSize = 100, chainIds = [Viem.chainId.mainnet], db, stripe } = options
313
+ return {
314
+ async tick() {
315
+ // Resolve per tick so Workers factories build a fresh Hyperdrive-pooled
316
+ // connection each run.
317
+ const database = Db.get(db)
318
+ const rows = await SponsoredTransactions.listUnreported(database, {
319
+ chainIds,
320
+ limit: batchSize,
321
+ })
322
+ // One customer lookup and one subscription ensure per org per tick.
323
+ const customers = new Map<string, StripeCustomers.Record | undefined>()
324
+ const subscriptions = new Map<string, ensureSubscription.Result>()
325
+ let failed = 0
326
+ let reported = 0
327
+ let skipped = 0
328
+ for (const row of rows) {
329
+ // One poisoned row must not stall the queue: it stays unmarked for
330
+ // retry while the rest keep reporting.
331
+ try {
332
+ let customer = customers.get(row.orgId)
333
+ if (!customers.has(row.orgId)) {
334
+ customer = await StripeCustomers.get(database, row.orgId)
335
+ customers.set(row.orgId, customer)
336
+ }
337
+ if (customer && customer.status !== 'canceled') {
338
+ // Self-heal: guarantee the metered subscription before reporting;
339
+ // meter events without one never reach an invoice. Pass the full
340
+ // production kind set so concurrent creates share idempotency.
341
+ let subscription = subscriptions.get(row.orgId)
342
+ if (!subscription) {
343
+ subscription = await ensureSubscription(
344
+ stripe,
345
+ customer.stripeCustomerId,
346
+ kindsFor('production'),
347
+ )
348
+ subscriptions.set(row.orgId, subscription)
349
+ }
350
+ // Meter timestamps must fall inside Stripe's 35-day ingestion
351
+ // window; older backlog bills into the current period instead. A
352
+ // just-created fee-payer item bills nothing before its start, so
353
+ // its backlog stamps at now too.
354
+ const finalizedAt = new Date(row.finalizedAt!).getTime()
355
+ const recent = Date.now() - finalizedAt < 34 * 24 * 60 * 60 * 1000
356
+ const stamp = recent && !subscription.items.feePayerSpend?.created
357
+ // The row id makes short-window retries idempotent on Stripe's
358
+ // side; `meter_reported_at` is the durable exactly-once guard.
359
+ await stripe.billing.meterEvents.create({
360
+ event_name: meterEventName,
361
+ identifier: row.id,
362
+ payload: { stripe_customer_id: customer.stripeCustomerId, value: row.feeAmount! },
363
+ ...(stamp ? { timestamp: Math.floor(finalizedAt / 1000) } : {}),
364
+ })
365
+ reported++
366
+ } else {
367
+ // Deleted orgs and Stripe-deleted (canceled) customers keep their
368
+ // ledger rows; nobody remains to invoice.
369
+ skipped++
370
+ }
371
+ // Mark only after the Stripe ack (or a definitive no-customer skip).
372
+ await SponsoredTransactions.markReported(database, row.id, new Date().toISOString())
373
+ } catch (error) {
374
+ failed++
375
+ console.error(`Billing.createReporter: reporting row ${row.id} failed`, error)
376
+ }
377
+ }
378
+ return { failed, reported, skipped }
379
+ },
380
+ }
381
+ }
382
+
383
+ export declare namespace createReporter {
384
+ /** Options for {@link createReporter}. */
385
+ type Options = {
386
+ /** Maximum rows reported per tick. @default 100 */
387
+ batchSize?: number | undefined
388
+ /** Chains whose spend is metered. @default mainnet */
389
+ chainIds?: readonly number[] | undefined
390
+ /** Database holding sponsorship rows, or a factory resolved per tick (Workers/Hyperdrive). */
391
+ db: Db.Source
392
+ /** Stripe client used to send meter events. */
393
+ stripe: Stripe
394
+ }
395
+
396
+ /** A scheduled metering job. */
397
+ type Reporter = {
398
+ /** Runs one reporting pass. Failed rows stay unmarked and retry next tick. */
399
+ tick(): Promise<{ failed: number; reported: number; skipped: number }>
400
+ }
401
+ }
402
+
403
+ /** Default settlement delay: only seal buckets older than this, absorbing normal ingest lag. */
404
+ const defaultSettlementDelayMs = 15 * 60 * 1000
405
+
406
+ /** Default correction window: how far back a tick reconciles late-arriving deltas. */
407
+ const defaultWindowMs = 48 * 60 * 60 * 1000
408
+
409
+ /** One hour in milliseconds. */
410
+ const hourMs = 60 * 60 * 1000
411
+
412
+ /**
413
+ * Creates the request-metered billing job: each tick reports deduped billable
414
+ * request counts to the `api_request_count` meter, per environment, using that
415
+ * environment's Stripe client. Exactly-once from the request analytics stream
416
+ * via a Postgres outbox with frozen counts:
417
+ *
418
+ * - Deduped counts come from `request_events` (`uniqExact(request_id)`), never
419
+ * a raw `count()`, so queue retries never over-bill.
420
+ * - Sandbox rows bill only when billing was active at request time
421
+ * (`billing_active = 1`); production rows always bill.
422
+ * - Each tick first re-sends any `pending` events (crash recovery, idempotent
423
+ * on `identifier`), then seals new additive deltas for buckets older than the
424
+ * settlement delay whose count has grown since the last report.
425
+ *
426
+ * A host runtime drives {@link createRequestUsageReporter.Reporter.tick} on a
427
+ * timer, like the fee-payer meter reporter.
428
+ */
429
+ export function createRequestUsageReporter(
430
+ options: createRequestUsageReporter.Options,
431
+ ): createRequestUsageReporter.Reporter {
432
+ const {
433
+ db,
434
+ now = () => Date.now(),
435
+ readCounts,
436
+ sandboxStripe,
437
+ settlementDelayMs = defaultSettlementDelayMs,
438
+ stripe,
439
+ windowMs = defaultWindowMs,
440
+ } = options
441
+ const clients: { client: Stripe; environment: StripeCustomers.Record['environment'] }[] = [
442
+ { client: stripe, environment: 'production' },
443
+ ...(sandboxStripe ? [{ client: sandboxStripe, environment: 'sandbox' as const }] : []),
444
+ ]
445
+ return {
446
+ async tick() {
447
+ const database = Db.get(db)
448
+ let failed = 0
449
+ let reported = 0
450
+ let skipped = 0
451
+ for (const { client, environment } of clients) {
452
+ const nowMs = now()
453
+ const sealCutoff = startOfHour(nowMs - settlementDelayMs)
454
+ const windowStart = new Date(new Date(sealCutoff).getTime() - windowMs).toISOString()
455
+
456
+ // Pass A — crash recovery: re-send frozen pending events. Idempotent on
457
+ // `identifier` within Stripe's dedup window; the minutely cadence
458
+ // re-marks well inside it. Buckets that still fail stay blocked so the
459
+ // seal pass never stacks a second delta on them.
460
+ const blocked = new Set<string>()
461
+ for (const event of await RequestUsage.listPending(database, environment)) {
462
+ try {
463
+ await client.billing.meterEvents.create({
464
+ event_name: meters.apiRequests.eventName,
465
+ identifier: event.identifier,
466
+ payload: {
467
+ stripe_customer_id: event.stripeCustomerId,
468
+ value: String(event.deltaCount),
469
+ },
470
+ })
471
+ if (await RequestUsage.settle(database, event.identifier, new Date().toISOString()))
472
+ reported++
473
+ } catch (error) {
474
+ failed++
475
+ blocked.add(bucketKey(event.orgId, event.bucketStart))
476
+ await RequestUsage.markError(database, event.identifier, String(error))
477
+ }
478
+ }
479
+
480
+ // Pass B — seal new deltas for closed buckets whose count has grown.
481
+ const counts = await readCounts({ environment, from: windowStart, to: sealCutoff })
482
+ const customers = new Map<string, StripeCustomers.Record | undefined>()
483
+ const subscriptions = new Map<string, ensureSubscription.Result>()
484
+ for (const { bucketStart, count, orgId } of counts) {
485
+ const key = { bucketStart, environment, orgId }
486
+ if (blocked.has(bucketKey(orgId, bucketStart))) continue
487
+ try {
488
+ const watermark = await RequestUsage.getBucket(database, key)
489
+ const delta = count - (watermark?.reportedCount ?? 0)
490
+ if (delta <= 0) continue
491
+
492
+ let customer = customers.get(orgId)
493
+ if (!customers.has(orgId)) {
494
+ customer = await StripeCustomers.get(database, orgId, environment)
495
+ customers.set(orgId, customer)
496
+ }
497
+ if (!customer || customer.status === 'canceled') {
498
+ skipped++
499
+ continue
500
+ }
501
+
502
+ let subscription = subscriptions.get(orgId)
503
+ if (!subscription) {
504
+ subscription = await ensureSubscription(
505
+ client,
506
+ customer.stripeCustomerId,
507
+ kindsFor(environment),
508
+ )
509
+ subscriptions.set(orgId, subscription)
510
+ }
511
+
512
+ const sequence = await RequestUsage.countReported(database, key)
513
+ const identifier = usageIdentifier(environment, orgId, bucketStart, sequence)
514
+ const inserted = await RequestUsage.insertPending(database, {
515
+ ...key,
516
+ deltaCount: delta,
517
+ identifier,
518
+ sequence,
519
+ stripeCustomerId: customer.stripeCustomerId,
520
+ })
521
+ // A concurrent tick already claimed this delta; leave it to that run.
522
+ if (!inserted) continue
523
+
524
+ // Backdate to the bucket hour unless the item was just created (it
525
+ // bills nothing before its start, so its backlog stamps at now).
526
+ const stamp = !subscription.items.apiRequests?.created
527
+ await client.billing.meterEvents.create({
528
+ event_name: meters.apiRequests.eventName,
529
+ identifier,
530
+ payload: { stripe_customer_id: customer.stripeCustomerId, value: String(delta) },
531
+ ...(stamp ? { timestamp: Math.floor(new Date(bucketStart).getTime() / 1000) } : {}),
532
+ })
533
+ if (await RequestUsage.settle(database, identifier, new Date().toISOString()))
534
+ reported++
535
+ } catch (error) {
536
+ failed++
537
+ console.error(
538
+ `Billing.createRequestUsageReporter: reporting ${environment} bucket ${orgId}/${bucketStart} failed`,
539
+ error,
540
+ )
541
+ }
542
+ }
543
+ }
544
+ return { failed, reported, skipped }
545
+ },
546
+ }
547
+ }
548
+
549
+ export declare namespace createRequestUsageReporter {
550
+ /** Deduped billable request count for one `(org, hour)` bucket. */
551
+ type Count = {
552
+ /** UTC hour start (ISO 8601). */
553
+ bucketStart: string
554
+ /** Deduped billable request count. */
555
+ count: number
556
+ /** Organization id (`org_…`). */
557
+ orgId: string
558
+ }
559
+
560
+ /** Options for {@link createRequestUsageReporter}. */
561
+ type Options = {
562
+ /** Database holding the outbox/watermarks, or a factory resolved per tick (Workers/Hyperdrive). */
563
+ db: Db.Source
564
+ /** Clock seam for tests; defaults to `Date.now`. */
565
+ now?: (() => number) | undefined
566
+ /** Reads deduped billable counts per `(org, hour)` for an environment window. */
567
+ readCounts: (options: {
568
+ environment: StripeCustomers.Record['environment']
569
+ from: string
570
+ to: string
571
+ }) => Promise<readonly Count[]>
572
+ /** Sandbox (test-mode) Stripe client; omit to skip sandbox billing. */
573
+ sandboxStripe?: Stripe | undefined
574
+ /** Only seal buckets older than this. @default 15min */
575
+ settlementDelayMs?: number | undefined
576
+ /** Production Stripe client. */
577
+ stripe: Stripe
578
+ /** How far back a tick reconciles late deltas. @default 48h */
579
+ windowMs?: number | undefined
580
+ }
581
+
582
+ /** A scheduled request-metering job. */
583
+ type Reporter = {
584
+ /** Runs one reporting pass across production (and sandbox when configured). */
585
+ tick(): Promise<{ failed: number; reported: number; skipped: number }>
586
+ }
587
+ }
588
+
589
+ /** Composite key for the in-tick blocked-bucket set. */
590
+ function bucketKey(orgId: string, bucketStart: string) {
591
+ return `${orgId}|${bucketStart}`
592
+ }
593
+
594
+ /** Start of the UTC hour containing `ms` (ISO 8601). */
595
+ function startOfHour(ms: number) {
596
+ return new Date(Math.floor(ms / hourMs) * hourMs).toISOString()
597
+ }
598
+
599
+ /**
600
+ * Deterministic meter-event identifier, bounded under Stripe's 100-char limit:
601
+ * `rq_${env}_${sha(orgId)[:12]}_${bucketEpochHour}_${sequence}`.
602
+ */
603
+ function usageIdentifier(
604
+ environment: StripeCustomers.Record['environment'],
605
+ orgId: string,
606
+ bucketStart: string,
607
+ sequence: number,
608
+ ) {
609
+ const hash = Hash.keccak256(Hex.fromString(orgId)).slice(2, 14)
610
+ const epochHour = Math.floor(new Date(bucketStart).getTime() / hourMs)
611
+ return `rq_${environment}_${hash}_${epochHour}_${sequence}`
612
+ }
613
+
614
+ /**
615
+ * Every organization's billing-active flag in an environment — the driver set
616
+ * for reconciling cached snapshots against this table. Provider-agnostic seam:
617
+ * callers reconcile against `{ orgId, active }`, never the Stripe source shape.
618
+ *
619
+ * @param db - The database.
620
+ * @param environment - The environment to enumerate.
621
+ * @returns One `{ orgId, active }` per billing source in the environment.
622
+ */
623
+ export async function listActive(
624
+ db: Db.Db,
625
+ environment: StripeCustomers.Record['environment'],
626
+ ): Promise<{ active: boolean; orgId: string }[]> {
627
+ const records = await StripeCustomers.listByEnvironment(db, environment)
628
+ return records.map((record) => ({ active: record.status === 'active', orgId: record.orgId }))
629
+ }
630
+
631
+ /**
632
+ * Creates the snapshot reconciler: each tick re-stamps the `billingActive`
633
+ * flag cached on API-key records from the authoritative billing table, so a
634
+ * missed or failed webhook re-sync self-heals within a tick. A host runtime
635
+ * drives {@link createKeyBillingSyncer.Syncer.tick} on a timer, like the meter
636
+ * reporter. Off the request path, so the auth hot path never reads billing.
637
+ *
638
+ * Only environments where keys carry a billing snapshot are reconciled
639
+ * (sandbox today); production keys carry none.
640
+ */
641
+ export function createKeyBillingSyncer(
642
+ options: createKeyBillingSyncer.Options,
643
+ ): createKeyBillingSyncer.Syncer {
644
+ const { db, environments = ['sandbox'], kv } = options
645
+ return {
646
+ async tick() {
647
+ // Resolve per tick so Workers factories build a fresh Hyperdrive-pooled
648
+ // connection each run.
649
+ const database = Db.get(db)
650
+ let checked = 0
651
+ let updated = 0
652
+ for (const environment of environments) {
653
+ const rows = await listActive(database, environment)
654
+ for (const { active, orgId } of rows) {
655
+ checked += 1
656
+ // `setBillingActive` writes only drifted records, so a converged
657
+ // fleet costs list reads and no writes.
658
+ updated += await ApiKeys.setBillingActive(kv, { active, environment, orgId })
659
+ }
660
+ }
661
+ return { checked, updated }
662
+ },
663
+ }
664
+ }
665
+
666
+ export declare namespace createKeyBillingSyncer {
667
+ /** Options for {@link createKeyBillingSyncer}. */
668
+ type Options = {
669
+ /** Database holding billing sources, or a factory resolved per tick (Workers/Hyperdrive). */
670
+ db: Db.Source
671
+ /** Environments to reconcile. @default ['sandbox'] */
672
+ environments?: readonly StripeCustomers.Record['environment'][] | undefined
673
+ /** KV state store holding API-key records. */
674
+ kv: Store.State
675
+ }
676
+
677
+ /** A scheduled snapshot-reconcile job. */
678
+ type Syncer = {
679
+ /** Runs one reconcile pass, re-stamping drifted key records. */
680
+ tick(): Promise<{ checked: number; updated: number }>
681
+ }
682
+ }
@@ -1 +1,2 @@
1
1
  export { platform } from './App.js'
2
+ export * as Billing from './Billing.js'