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