tapimo 0.2.0 → 0.4.0

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