tapimo 0.5.3 → 0.7.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 (376) hide show
  1. package/dist/App.d.ts +15830 -7220
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +9 -1
  4. package/dist/App.js.map +1 -1
  5. package/dist/Scope.d.ts +4 -0
  6. package/dist/Scope.d.ts.map +1 -1
  7. package/dist/Scope.js +5 -0
  8. package/dist/Scope.js.map +1 -1
  9. package/dist/admin/App.d.ts +1011 -412
  10. package/dist/admin/App.d.ts.map +1 -1
  11. package/dist/admin/App.js +4 -0
  12. package/dist/admin/App.js.map +1 -1
  13. package/dist/admin/apps/api-keys.d.ts +2 -1
  14. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  15. package/dist/admin/apps/early-access.d.ts +148 -0
  16. package/dist/admin/apps/early-access.d.ts.map +1 -0
  17. package/dist/admin/apps/early-access.js +133 -0
  18. package/dist/admin/apps/early-access.js.map +1 -0
  19. package/dist/admin/apps/organizations.d.ts +124 -1
  20. package/dist/admin/apps/organizations.d.ts.map +1 -1
  21. package/dist/admin/apps/organizations.js +97 -6
  22. package/dist/admin/apps/organizations.js.map +1 -1
  23. package/dist/admin/apps/verified-tokens.d.ts +526 -1
  24. package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
  25. package/dist/admin/apps/verified-tokens.js +214 -1
  26. package/dist/admin/apps/verified-tokens.js.map +1 -1
  27. package/dist/admin/ui.generated.d.ts +1 -1
  28. package/dist/admin/ui.generated.d.ts.map +1 -1
  29. package/dist/admin/ui.generated.js +95 -75
  30. package/dist/admin/ui.generated.js.map +1 -1
  31. package/dist/analytics/Analytics.d.ts.map +1 -1
  32. package/dist/analytics/Analytics.js +20 -0
  33. package/dist/analytics/Analytics.js.map +1 -1
  34. package/dist/analytics/tables/requestEvents.d.ts +18 -0
  35. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  36. package/dist/analytics/tables/requestEvents.js +36 -7
  37. package/dist/analytics/tables/requestEvents.js.map +1 -1
  38. package/dist/apps/data/App.d.ts +6260 -4924
  39. package/dist/apps/data/App.d.ts.map +1 -1
  40. package/dist/apps/data/App.js +11 -6
  41. package/dist/apps/data/App.js.map +1 -1
  42. package/dist/apps/data/FundingQuoteProviders.d.ts +34 -1
  43. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
  44. package/dist/apps/data/FundingQuoteProviders.js +194 -16
  45. package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
  46. package/dist/apps/data/FxOracle.d.ts +71 -0
  47. package/dist/apps/data/FxOracle.d.ts.map +1 -0
  48. package/dist/apps/data/FxOracle.js +97 -0
  49. package/dist/apps/data/FxOracle.js.map +1 -0
  50. package/dist/apps/data/index.d.ts +1 -0
  51. package/dist/apps/data/index.d.ts.map +1 -1
  52. package/dist/apps/data/index.js +1 -0
  53. package/dist/apps/data/index.js.map +1 -1
  54. package/dist/apps/data/routes/activities.d.ts +3599 -1382
  55. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  56. package/dist/apps/data/routes/activities.js +250 -88
  57. package/dist/apps/data/routes/activities.js.map +1 -1
  58. package/dist/apps/data/routes/addresses.d.ts +872 -251
  59. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  60. package/dist/apps/data/routes/addresses.js +11 -9
  61. package/dist/apps/data/routes/addresses.js.map +1 -1
  62. package/dist/apps/data/routes/balances.d.ts +77 -26
  63. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  64. package/dist/apps/data/routes/balances.js +88 -45
  65. package/dist/apps/data/routes/balances.js.map +1 -1
  66. package/dist/apps/data/routes/blocks.d.ts +39 -39
  67. package/dist/apps/data/routes/coingecko.d.ts +54 -54
  68. package/dist/apps/data/routes/exchanges.d.ts +856 -105
  69. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  70. package/dist/apps/data/routes/exchanges.js +518 -76
  71. package/dist/apps/data/routes/exchanges.js.map +1 -1
  72. package/dist/apps/data/routes/fee-amm.d.ts +81 -67
  73. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  74. package/dist/apps/data/routes/fee-amm.js +76 -91
  75. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  76. package/dist/apps/data/routes/funding.d.ts +80 -36
  77. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  78. package/dist/apps/data/routes/funding.js +65 -31
  79. package/dist/apps/data/routes/funding.js.map +1 -1
  80. package/dist/apps/data/routes/receipts.d.ts +392 -315
  81. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  82. package/dist/apps/data/routes/receipts.js +152 -77
  83. package/dist/apps/data/routes/receipts.js.map +1 -1
  84. package/dist/apps/data/routes/tokens.d.ts +167 -174
  85. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  86. package/dist/apps/data/routes/tokens.js +15 -13
  87. package/dist/apps/data/routes/tokens.js.map +1 -1
  88. package/dist/apps/data/routes/transactions.d.ts +277 -536
  89. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  90. package/dist/apps/data/routes/transactions.js +96 -77
  91. package/dist/apps/data/routes/transactions.js.map +1 -1
  92. package/dist/apps/data/routes/transfers.d.ts +157 -81
  93. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  94. package/dist/apps/data/routes/transfers.js +272 -130
  95. package/dist/apps/data/routes/transfers.js.map +1 -1
  96. package/dist/apps/data/routes/valuation.d.ts +307 -0
  97. package/dist/apps/data/routes/valuation.d.ts.map +1 -0
  98. package/dist/apps/data/routes/valuation.js +292 -0
  99. package/dist/apps/data/routes/valuation.js.map +1 -0
  100. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  101. package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
  102. package/dist/apps/data/routes/verified-tokens.js +7 -5
  103. package/dist/apps/data/routes/verified-tokens.js.map +1 -1
  104. package/dist/apps/data/routes/webhooks.d.ts +103 -123
  105. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  106. package/dist/apps/data/routes/webhooks.js +124 -25
  107. package/dist/apps/data/routes/webhooks.js.map +1 -1
  108. package/dist/apps/index.d.ts +2 -1
  109. package/dist/apps/index.d.ts.map +1 -1
  110. package/dist/apps/index.js +2 -1
  111. package/dist/apps/index.js.map +1 -1
  112. package/dist/apps/management/App.d.ts +8554 -1692
  113. package/dist/apps/management/App.d.ts.map +1 -1
  114. package/dist/apps/management/App.js +12 -1
  115. package/dist/apps/management/App.js.map +1 -1
  116. package/dist/apps/management/routes/api-keys.d.ts +7 -0
  117. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  118. package/dist/apps/management/routes/api-keys.js +17 -3
  119. package/dist/apps/management/routes/api-keys.js.map +1 -1
  120. package/dist/apps/management/routes/billing.d.ts +157 -162
  121. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  122. package/dist/apps/management/routes/billing.js +31 -23
  123. package/dist/apps/management/routes/billing.js.map +1 -1
  124. package/dist/apps/management/routes/faucet.d.ts +232 -0
  125. package/dist/apps/management/routes/faucet.d.ts.map +1 -0
  126. package/dist/apps/management/routes/faucet.js +154 -0
  127. package/dist/apps/management/routes/faucet.js.map +1 -0
  128. package/dist/apps/management/routes/invitations.d.ts +60 -60
  129. package/dist/apps/management/routes/invite-links.d.ts +49 -20
  130. package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
  131. package/dist/apps/management/routes/invite-links.js +26 -1
  132. package/dist/apps/management/routes/invite-links.js.map +1 -1
  133. package/dist/apps/management/routes/me.d.ts +6 -6
  134. package/dist/apps/management/routes/members.d.ts +6 -6
  135. package/dist/apps/management/routes/orgs.d.ts +42 -42
  136. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  137. package/dist/apps/management/routes/orgs.js +5 -0
  138. package/dist/apps/management/routes/orgs.js.map +1 -1
  139. package/dist/apps/management/routes/projects.d.ts +44 -44
  140. package/dist/apps/management/routes/usage.d.ts +3 -0
  141. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  142. package/dist/apps/management/routes/usage.js +4 -0
  143. package/dist/apps/management/routes/usage.js.map +1 -1
  144. package/dist/apps/management/routes/verified-token-requests.d.ts +697 -0
  145. package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -0
  146. package/dist/apps/management/routes/verified-token-requests.js +271 -0
  147. package/dist/apps/management/routes/verified-token-requests.js.map +1 -0
  148. package/dist/apps/management/routes/webhooks.d.ts +6977 -0
  149. package/dist/apps/management/routes/webhooks.d.ts.map +1 -0
  150. package/dist/apps/management/routes/webhooks.js +573 -0
  151. package/dist/apps/management/routes/webhooks.js.map +1 -0
  152. package/dist/apps/mpp/App.d.ts +363 -0
  153. package/dist/apps/mpp/App.d.ts.map +1 -0
  154. package/dist/apps/mpp/App.js +72 -0
  155. package/dist/apps/mpp/App.js.map +1 -0
  156. package/dist/apps/mpp/Mpp.d.ts +178 -0
  157. package/dist/apps/mpp/Mpp.d.ts.map +1 -0
  158. package/dist/apps/mpp/Mpp.js +183 -0
  159. package/dist/apps/mpp/Mpp.js.map +1 -0
  160. package/dist/apps/mpp/index.d.ts +3 -0
  161. package/dist/apps/mpp/index.d.ts.map +1 -0
  162. package/dist/apps/mpp/index.js +3 -0
  163. package/dist/apps/mpp/index.js.map +1 -0
  164. package/dist/apps/relay/App.d.ts.map +1 -1
  165. package/dist/apps/relay/App.js +56 -9
  166. package/dist/apps/relay/App.js.map +1 -1
  167. package/dist/db/Db.d.ts +6 -0
  168. package/dist/db/Db.d.ts.map +1 -1
  169. package/dist/db/Db.js +110 -0
  170. package/dist/db/Db.js.map +1 -1
  171. package/dist/db/Schema.d.ts +53 -0
  172. package/dist/db/Schema.d.ts.map +1 -1
  173. package/dist/db/Schema.js +56 -0
  174. package/dist/db/Schema.js.map +1 -1
  175. package/dist/db/tables/earlyAccess.d.ts +50 -0
  176. package/dist/db/tables/earlyAccess.d.ts.map +1 -0
  177. package/dist/db/tables/earlyAccess.js +63 -0
  178. package/dist/db/tables/earlyAccess.js.map +1 -0
  179. package/dist/db/tables/enabledBillingSources.d.ts +73 -0
  180. package/dist/db/tables/enabledBillingSources.d.ts.map +1 -0
  181. package/dist/db/tables/enabledBillingSources.js +69 -0
  182. package/dist/db/tables/enabledBillingSources.js.map +1 -0
  183. package/dist/db/tables/invitations.d.ts.map +1 -1
  184. package/dist/db/tables/invitations.js +3 -0
  185. package/dist/db/tables/invitations.js.map +1 -1
  186. package/dist/db/tables/inviteLinks.d.ts +10 -2
  187. package/dist/db/tables/inviteLinks.d.ts.map +1 -1
  188. package/dist/db/tables/inviteLinks.js +10 -1
  189. package/dist/db/tables/inviteLinks.js.map +1 -1
  190. package/dist/db/tables/organizations.d.ts +5 -2
  191. package/dist/db/tables/organizations.d.ts.map +1 -1
  192. package/dist/db/tables/organizations.js +13 -2
  193. package/dist/db/tables/organizations.js.map +1 -1
  194. package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
  195. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  196. package/dist/db/tables/sponsoredTransactions.js +2 -0
  197. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  198. package/dist/db/tables/verifiedTokenRequests.d.ts +94 -0
  199. package/dist/db/tables/verifiedTokenRequests.d.ts.map +1 -0
  200. package/dist/db/tables/verifiedTokenRequests.js +120 -0
  201. package/dist/db/tables/verifiedTokenRequests.js.map +1 -0
  202. package/dist/db/tables/webhookSubscriptions.d.ts +14 -0
  203. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  204. package/dist/db/tables/webhookSubscriptions.js +16 -0
  205. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  206. package/dist/handlers/internal/utils.d.ts +1 -16
  207. package/dist/handlers/internal/utils.d.ts.map +1 -1
  208. package/dist/handlers/internal/utils.js +17 -6
  209. package/dist/handlers/internal/utils.js.map +1 -1
  210. package/dist/handlers/relay.d.ts.map +1 -1
  211. package/dist/handlers/relay.js +37 -5
  212. package/dist/handlers/relay.js.map +1 -1
  213. package/dist/internal/Auth.d.ts +11 -2
  214. package/dist/internal/Auth.d.ts.map +1 -1
  215. package/dist/internal/Auth.js +14 -47
  216. package/dist/internal/Auth.js.map +1 -1
  217. package/dist/internal/FundingQuotes.d.ts +147 -47
  218. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  219. package/dist/internal/FundingQuotes.js +119 -33
  220. package/dist/internal/FundingQuotes.js.map +1 -1
  221. package/dist/internal/Log.d.ts +37 -5
  222. package/dist/internal/Log.d.ts.map +1 -1
  223. package/dist/internal/Log.js +52 -3
  224. package/dist/internal/Log.js.map +1 -1
  225. package/dist/internal/MetricSink.d.ts.map +1 -1
  226. package/dist/internal/MetricSink.js +24 -0
  227. package/dist/internal/MetricSink.js.map +1 -1
  228. package/dist/internal/OpenApi.d.ts +2 -2
  229. package/dist/internal/OpenApi.d.ts.map +1 -1
  230. package/dist/internal/OpenApi.js +3 -0
  231. package/dist/internal/OpenApi.js.map +1 -1
  232. package/dist/internal/Response.d.ts +2 -2
  233. package/dist/internal/Response.d.ts.map +1 -1
  234. package/dist/internal/Response.js +12 -4
  235. package/dist/internal/Response.js.map +1 -1
  236. package/dist/internal/Schema.d.ts +14 -5
  237. package/dist/internal/Schema.d.ts.map +1 -1
  238. package/dist/internal/Schema.js +23 -5
  239. package/dist/internal/Schema.js.map +1 -1
  240. package/dist/internal/Value.d.ts +18 -0
  241. package/dist/internal/Value.d.ts.map +1 -1
  242. package/dist/internal/Value.js +11 -1
  243. package/dist/internal/Value.js.map +1 -1
  244. package/dist/internal/VerifiedTokens.d.ts.map +1 -1
  245. package/dist/internal/VerifiedTokens.js +10 -1
  246. package/dist/internal/VerifiedTokens.js.map +1 -1
  247. package/dist/internal/Webhooks.d.ts +24 -3
  248. package/dist/internal/Webhooks.d.ts.map +1 -1
  249. package/dist/internal/Webhooks.js +34 -8
  250. package/dist/internal/Webhooks.js.map +1 -1
  251. package/package.json +7 -2
  252. package/src/App.test.ts +9 -4
  253. package/src/App.ts +23 -3
  254. package/src/Client.test-d.ts +733 -20
  255. package/src/Scope.ts +5 -0
  256. package/src/admin/App.test.ts +4 -0
  257. package/src/admin/App.ts +4 -0
  258. package/src/admin/apps/early-access.test.ts +125 -0
  259. package/src/admin/apps/early-access.ts +176 -0
  260. package/src/admin/apps/organizations.test.ts +86 -2
  261. package/src/admin/apps/organizations.ts +155 -31
  262. package/src/admin/apps/scopes.test.ts +5 -0
  263. package/src/admin/apps/verified-tokens.test.ts +201 -0
  264. package/src/admin/apps/verified-tokens.ts +532 -286
  265. package/src/admin/ui/plugin.ts +2 -1
  266. package/src/admin/ui/src/lib/early-access.ts +63 -0
  267. package/src/admin/ui/src/lib/organizations.test.ts +81 -1
  268. package/src/admin/ui/src/lib/organizations.ts +92 -0
  269. package/src/admin/ui/src/lib/verified-tokens.ts +79 -0
  270. package/src/admin/ui/src/routeTree.gen.ts +33 -2
  271. package/src/admin/ui/src/routes/__root.tsx +2 -1
  272. package/src/admin/ui/src/routes/api-keys.tsx +1 -1
  273. package/src/admin/ui/src/routes/early-access.tsx +162 -0
  274. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +103 -0
  275. package/src/admin/ui/src/routes/verified-tokens.tsx +230 -19
  276. package/src/admin/ui.generated.ts +95 -94
  277. package/src/analytics/Analytics.test.ts +83 -0
  278. package/src/analytics/Analytics.ts +20 -0
  279. package/src/analytics/tables/requestEvents.ts +60 -7
  280. package/src/apps/composition.test.ts +11 -2
  281. package/src/apps/data/App.test.ts +1 -1
  282. package/src/apps/data/App.ts +22 -14
  283. package/src/apps/data/FundingQuoteProviders.test.ts +297 -4
  284. package/src/apps/data/FundingQuoteProviders.ts +255 -29
  285. package/src/apps/data/FxOracle.test.ts +118 -0
  286. package/src/apps/data/FxOracle.ts +153 -0
  287. package/src/apps/data/index.ts +1 -0
  288. package/src/apps/data/routes/activities.test.ts +177 -3
  289. package/src/apps/data/routes/activities.ts +441 -132
  290. package/src/apps/data/routes/addresses.ts +19 -9
  291. package/src/apps/data/routes/balances.test.ts +148 -11
  292. package/src/apps/data/routes/balances.ts +140 -51
  293. package/src/apps/data/routes/exchanges.test.ts +544 -9
  294. package/src/apps/data/routes/exchanges.ts +690 -103
  295. package/src/apps/data/routes/fee-amm.test.ts +47 -23
  296. package/src/apps/data/routes/fee-amm.ts +98 -119
  297. package/src/apps/data/routes/funding.test.ts +113 -8
  298. package/src/apps/data/routes/funding.ts +77 -51
  299. package/src/apps/data/routes/receipts.test.ts +129 -23
  300. package/src/apps/data/routes/receipts.ts +205 -100
  301. package/src/apps/data/routes/tokens.test.ts +43 -0
  302. package/src/apps/data/routes/tokens.ts +28 -13
  303. package/src/apps/data/routes/transactions.test.ts +72 -19
  304. package/src/apps/data/routes/transactions.ts +162 -92
  305. package/src/apps/data/routes/transfers.test.ts +341 -31
  306. package/src/apps/data/routes/transfers.ts +358 -158
  307. package/src/apps/data/routes/valuation.test.ts +437 -0
  308. package/src/apps/data/routes/valuation.ts +423 -0
  309. package/src/apps/data/routes/verified-tokens.ts +14 -2
  310. package/src/apps/data/routes/webhooks.test.ts +56 -8
  311. package/src/apps/data/routes/webhooks.ts +144 -27
  312. package/src/apps/index.ts +2 -1
  313. package/src/apps/management/App.ts +13 -0
  314. package/src/apps/management/routes/api-keys.test.ts +14 -0
  315. package/src/apps/management/routes/api-keys.ts +27 -3
  316. package/src/apps/management/routes/billing.test.ts +152 -20
  317. package/src/apps/management/routes/billing.ts +34 -18
  318. package/src/apps/management/routes/faucet.test.ts +87 -0
  319. package/src/apps/management/routes/faucet.ts +201 -0
  320. package/src/apps/management/routes/invite-links.test.ts +117 -0
  321. package/src/apps/management/routes/invite-links.ts +43 -1
  322. package/src/apps/management/routes/me.test.ts +9 -99
  323. package/src/apps/management/routes/orgs.test.ts +45 -2
  324. package/src/apps/management/routes/orgs.ts +6 -0
  325. package/src/apps/management/routes/scopes.test.ts +1 -0
  326. package/src/apps/management/routes/usage.test.ts +6 -0
  327. package/src/apps/management/routes/usage.ts +7 -0
  328. package/src/apps/management/routes/verified-token-requests.test.ts +377 -0
  329. package/src/apps/management/routes/verified-token-requests.ts +397 -0
  330. package/src/apps/management/routes/webhooks.test.ts +426 -0
  331. package/src/apps/management/routes/webhooks.ts +672 -0
  332. package/src/apps/mpp/App.ts +86 -0
  333. package/src/apps/mpp/Mpp.test.ts +193 -0
  334. package/src/apps/mpp/Mpp.ts +353 -0
  335. package/src/apps/mpp/index.ts +2 -0
  336. package/src/apps/mpp/internal/Adapter.ts +11 -0
  337. package/src/apps/mpp/internal/Error.ts +9 -0
  338. package/src/apps/mpp/internal/Idempotency.ts +7 -0
  339. package/src/apps/mpp/internal/Screening.ts +10 -0
  340. package/src/apps/relay/App.test.ts +88 -6
  341. package/src/apps/relay/App.ts +61 -9
  342. package/src/db/Db.test.ts +19 -0
  343. package/src/db/Db.ts +125 -0
  344. package/src/db/Schema.ts +68 -0
  345. package/src/db/tables/earlyAccess.ts +86 -0
  346. package/src/db/tables/enabledBillingSources.test.ts +303 -0
  347. package/src/db/tables/enabledBillingSources.ts +118 -0
  348. package/src/db/tables/invitations.ts +3 -0
  349. package/src/db/tables/inviteLinks.ts +22 -3
  350. package/src/db/tables/organizations.test.ts +103 -0
  351. package/src/db/tables/organizations.ts +18 -2
  352. package/src/db/tables/sponsoredTransactions.ts +4 -0
  353. package/src/db/tables/verifiedTokenRequests.test.ts +200 -0
  354. package/src/db/tables/verifiedTokenRequests.ts +182 -0
  355. package/src/db/tables/webhookSubscriptions.ts +31 -0
  356. package/src/handlers/internal/utils.ts +22 -6
  357. package/src/handlers/relay.errors.test.ts +153 -0
  358. package/src/handlers/relay.ts +42 -7
  359. package/src/internal/Auth.test.ts +3 -3
  360. package/src/internal/Auth.ts +15 -47
  361. package/src/internal/FundingQuotes.test.ts +161 -6
  362. package/src/internal/FundingQuotes.ts +141 -39
  363. package/src/internal/Log.test-d.ts +21 -0
  364. package/src/internal/Log.test.ts +141 -0
  365. package/src/internal/Log.ts +93 -6
  366. package/src/internal/MetricSink.test.ts +215 -0
  367. package/src/internal/MetricSink.ts +24 -0
  368. package/src/internal/OpenApi.ts +3 -0
  369. package/src/internal/Response.test.ts +28 -0
  370. package/src/internal/Response.ts +12 -4
  371. package/src/internal/Schema.ts +50 -5
  372. package/src/internal/Value.test.ts +20 -0
  373. package/src/internal/Value.ts +24 -1
  374. package/src/internal/VerifiedTokens.ts +9 -1
  375. package/src/internal/Webhooks.test.ts +140 -0
  376. package/src/internal/Webhooks.ts +53 -9
@@ -0,0 +1,201 @@
1
+ import { Hono, type Context } from 'hono'
2
+ import * as z from 'zod/mini'
3
+
4
+ import * as Auth from '../../../internal/Auth.js'
5
+ import * as OpenApi from '../../../internal/OpenApi.js'
6
+ import * as RateLimit from '../../../internal/RateLimit.js'
7
+ import * as Response from '../../../internal/Response.js'
8
+ import * as Schema from '../../../internal/Schema.js'
9
+ import * as Value from '../../../internal/Value.js'
10
+ import * as Viem from '../../../internal/Viem.js'
11
+ import type { Environment } from '../App.js'
12
+
13
+ const addressRateLimit = { perMinute: 1 } satisfies RateLimit.Limit
14
+ const userRateLimit = { perMinute: 3 } satisfies RateLimit.Limit
15
+
16
+ /** OpenAPI schemas owned by the sandbox faucet. */
17
+ export namespace schema {
18
+ const Token = Schema.describe(
19
+ z.object({
20
+ address: Schema.TokenAddress.check(z.describe('The test token contract that minted funds.')),
21
+ currency: z
22
+ .string()
23
+ .check(z.describe('The token’s currency label.'), z.meta({ examples: ['USD'] })),
24
+ decimals: z
25
+ .number()
26
+ .check(
27
+ z.int(),
28
+ z.nonnegative(),
29
+ z.describe('The decimal places used by the token.'),
30
+ z.meta({ examples: [6] }),
31
+ ),
32
+ name: z
33
+ .string()
34
+ .check(z.describe('The token’s human-readable name.'), z.meta({ examples: ['Test USD'] })),
35
+ symbol: z.string().check(z.describe('The token’s symbol.'), z.meta({ examples: ['TUSD'] })),
36
+ }),
37
+ 'A test token minted by the faucet.',
38
+ )
39
+
40
+ const FundedToken = Schema.describe(
41
+ z.object({
42
+ amount: Schema.TokenAmount.check(z.describe('Amount minted by the faucet.')),
43
+ token: Token,
44
+ transactionHash: Schema.Hash.check(z.describe('The transaction that minted this token.')),
45
+ }),
46
+ 'One token amount minted by the faucet.',
47
+ )
48
+
49
+ /** Faucet request body. */
50
+ export const FundBody = Schema.describe(
51
+ z.object({ address: Schema.Address }),
52
+ 'A Tempo testnet account to fund.',
53
+ )
54
+
55
+ /** Faucet response. */
56
+ export const FundResponse = Schema.describe(
57
+ z.object({
58
+ tokens: z
59
+ .array(FundedToken)
60
+ .check(z.describe('The configured test tokens minted to the account.')),
61
+ transactionHashes: z
62
+ .array(Schema.Hash)
63
+ .check(z.describe('Transactions that minted the configured test tokens.')),
64
+ }),
65
+ 'Test tokens minted while funding the account.',
66
+ )
67
+
68
+ /** Faucet path parameters. */
69
+ export const Params = Schema.describe(
70
+ z.object({
71
+ orgId: z
72
+ .string()
73
+ .check(
74
+ z.describe('The organization id (`org_…`).'),
75
+ z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
76
+ ),
77
+ }),
78
+ 'Path parameters for the organization faucet.',
79
+ )
80
+ }
81
+
82
+ const bodyValidation = {
83
+ code: 'body_invalid',
84
+ message: 'Enter a valid Tempo address.',
85
+ } as const
86
+ const paramValidation = {
87
+ code: 'param_invalid',
88
+ message: 'Check the path parameters and try again.',
89
+ } as const
90
+
91
+ /** Mounts the session-authenticated Tempo testnet faucet. */
92
+ export function faucet() {
93
+ return new Hono<Environment>().post(
94
+ '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/faucet',
95
+ Auth.policy({ session: true }),
96
+ Auth.ensureOrg(),
97
+ OpenApi.validate('param', schema.Params, paramValidation),
98
+ OpenApi.validate('json', schema.FundBody, bodyValidation),
99
+ OpenApi.describeRoute({
100
+ description:
101
+ 'Funds an account with the test tokens configured on Tempo testnet. This operation is available to signed-in organization members and is rate limited.',
102
+ operationId: 'fundTestnetAccount',
103
+ responses: OpenApi.responses({
104
+ errors: {
105
+ 400: {
106
+ codes: ['api_key_malformed', 'body_invalid', 'param_invalid'],
107
+ description: 'The path or account address is invalid.',
108
+ },
109
+ 404: 'No accessible organization was found.',
110
+ },
111
+ success: {
112
+ description: 'The account was funded.',
113
+ schema: schema.FundResponse,
114
+ },
115
+ }),
116
+ summary: 'Fund testnet account',
117
+ tags: ['Faucet'],
118
+ }),
119
+ async (c) => {
120
+ if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
121
+ if (Auth.narrowScope) return Auth.ensureOrgError(c)
122
+ if (OpenApi.narrowValidation) return OpenApi.validationError(c, paramValidation)
123
+ if (OpenApi.narrowValidation) return OpenApi.validationError(c, bodyValidation)
124
+
125
+ const { address } = c.req.valid('json')
126
+ const limited = await consumeRateLimit(c, address)
127
+ if (limited) return rateLimitError(c, limited)
128
+
129
+ try {
130
+ const client = c.get('getClient')(Viem.chainId.testnet)
131
+ const receipts = await client.faucet.fundSync({ account: address, timeout: 30_000 })
132
+ const tokens = await Promise.all(
133
+ receipts.map(async (receipt) => {
134
+ const event = client.token.mint.extractEvent(receipt.logs)
135
+ const metadata = await client.token.getMetadata({ token: event.address })
136
+ return {
137
+ amount: Value.tokenAmount({
138
+ baseUnits: event.args.amount,
139
+ currency: metadata.currency,
140
+ decimals: metadata.decimals,
141
+ }),
142
+ token: {
143
+ address: event.address,
144
+ currency: metadata.currency,
145
+ decimals: metadata.decimals,
146
+ name: metadata.name,
147
+ symbol: metadata.symbol,
148
+ },
149
+ transactionHash: receipt.transactionHash,
150
+ }
151
+ }),
152
+ )
153
+ const transactionHashes = tokens.map((entry) => entry.transactionHash)
154
+ return c.json(Response.validated(schema.FundResponse, { tokens, transactionHashes }), 200)
155
+ } catch (cause) {
156
+ return Response.upstream(c, cause)
157
+ }
158
+ },
159
+ )
160
+ }
161
+
162
+ async function consumeRateLimit(c: Context<Environment>, address: string) {
163
+ const principal = Auth.getPrincipal(c)
164
+ const store = c.get('auth').rateLimit
165
+ if (!principal || !store) return undefined
166
+
167
+ const user = await store.consume({
168
+ key: `faucet:user:${principal.id}`,
169
+ limit: userRateLimit,
170
+ })
171
+ if (!user.allowed) return { result: user, scope: 'faucet-user' }
172
+
173
+ const account = await store.consume({
174
+ key: `faucet:address:${address}`,
175
+ limit: addressRateLimit,
176
+ })
177
+ if (!account.allowed) return { result: account, scope: 'faucet-address' }
178
+ return undefined
179
+ }
180
+
181
+ function rateLimitError(
182
+ c: Context<Environment>,
183
+ limited: {
184
+ /** Denied quota result. */
185
+ result: RateLimit.Result
186
+ /** Quota dimension that denied the request. */
187
+ scope: string
188
+ },
189
+ ) {
190
+ const { result, scope } = limited
191
+ c.header('RateLimit-Limit', String(result.limit))
192
+ c.header('RateLimit-Remaining', String(result.remaining))
193
+ c.header('RateLimit-Reset', String(result.reset))
194
+ c.header('RateLimit-Scope', scope)
195
+ c.header('Retry-After', String(Math.max(result.reset - Math.ceil(Date.now() / 1_000), 1)))
196
+ return Response.error(c, {
197
+ code: 'rate_limit_exceeded',
198
+ message: 'This account was funded recently. Try again after the cooldown.',
199
+ status: 429,
200
+ })
201
+ }
@@ -82,6 +82,7 @@ describe('inviteLinks', () => {
82
82
 
83
83
  const link = await createLink(context, org.id, admin.cookie, { role: 'member' })
84
84
  expect(link).toMatchObject({
85
+ allowedEmailDomains: null,
85
86
  enabled: true,
86
87
  lastUsedAt: null,
87
88
  maxUses: null,
@@ -143,6 +144,122 @@ describe('inviteLinks', () => {
143
144
  expect((await listed.json()).data).toEqual([])
144
145
  })
145
146
 
147
+ test('normalizes and validates allowed email domains', async () => {
148
+ const context = createApp()
149
+ const { client } = context
150
+ const { org, owner } = await setup(context)
151
+ const orgLinks = client.v1.orgs[':orgId{org_[A-Za-z0-9_-]+}']['invite-links']
152
+
153
+ const link = await createLink(context, org.id, owner.cookie, {
154
+ allowedEmailDomains: [' Example.COM ', 'example.com', 'TEMPO.XYZ'],
155
+ name: 'Restricted',
156
+ })
157
+ expect(link.allowedEmailDomains).toEqual(['example.com', 'tempo.xyz'])
158
+
159
+ const listed = await orgLinks.$get({ param: { orgId: org.id } }, as(owner.cookie))
160
+ expect(listed.status).toBe(200)
161
+ if (listed.status !== 200) throw new Error(`Failed to list invite links: ${listed.status}`)
162
+ expect((await listed.json()).data[0]?.allowedEmailDomains).toEqual(['example.com', 'tempo.xyz'])
163
+
164
+ for (const allowedEmailDomains of [
165
+ [],
166
+ ['user@example.com'],
167
+ ['https://example.com'],
168
+ ['*.example.com'],
169
+ ]) {
170
+ const response = await orgLinks.$post(
171
+ {
172
+ json: { allowedEmailDomains },
173
+ param: { orgId: org.id },
174
+ },
175
+ as(owner.cookie),
176
+ )
177
+ expect(response.status).toBe(400)
178
+ if (response.status !== 400)
179
+ throw new Error(`Expected invalid invite link body, received ${response.status}`)
180
+ expect((await response.json()).error.code).toBe('body_invalid')
181
+ }
182
+ })
183
+
184
+ test('restricts new memberships without consuming rejected attempts', async () => {
185
+ const db = TestApp.database()
186
+ const context = createApp(db)
187
+ const { client } = context
188
+ const { org, owner } = await setup(context)
189
+ const orgLinks = client.v1.orgs[':orgId{org_[A-Za-z0-9_-]+}']['invite-links']
190
+ const publicLinks = client.v1['invite-links']
191
+ const link = await createLink(context, org.id, owner.cookie, {
192
+ allowedEmailDomains: [' Example.COM '],
193
+ expiresAt: null,
194
+ maxUses: 1,
195
+ name: 'Restricted',
196
+ })
197
+
198
+ const member = await session(context)
199
+ await Users.setEmail(db, member.userId, 'member@sub.example.com')
200
+ await Memberships.create(db, { orgId: org.id, role: 'member', userId: member.userId })
201
+ const memberRetry = await publicLinks.accept.$post(
202
+ { json: { token: link.token } },
203
+ as(member.cookie),
204
+ )
205
+ expect(memberRetry.status).toBe(200)
206
+
207
+ const subdomain = await session(context)
208
+ await Users.setEmail(db, subdomain.userId, 'dev@sub.example.com')
209
+ const rejected = await publicLinks.accept.$post(
210
+ { json: { token: link.token } },
211
+ as(subdomain.cookie),
212
+ )
213
+ expect(rejected.status).toBe(403)
214
+ if (rejected.status !== 403)
215
+ throw new Error(`Expected forbidden invite link acceptance, received ${rejected.status}`)
216
+ expect((await rejected.json()).error.code).toBe('invite_link_email_forbidden')
217
+ expect(await Memberships.get(db, org.id, subdomain.userId)).toBeUndefined()
218
+
219
+ const audits = await orgLinks[':inviteLinkId{iln_[A-Za-z0-9_-]+}'].redemptions.$get(
220
+ { param: { inviteLinkId: link.id, orgId: org.id } },
221
+ as(owner.cookie),
222
+ )
223
+ expect(audits.status).toBe(200)
224
+ if (audits.status !== 200) throw new Error(`Failed to list redemptions: ${audits.status}`)
225
+ expect((await audits.json()).data).toEqual([])
226
+
227
+ const beforeJoin = await orgLinks.$get({ param: { orgId: org.id } }, as(owner.cookie))
228
+ expect(beforeJoin.status).toBe(200)
229
+ if (beforeJoin.status !== 200)
230
+ throw new Error(`Failed to list invite links: ${beforeJoin.status}`)
231
+ expect((await beforeJoin.json()).data[0]).toMatchObject({ status: 'active', useCount: 0 })
232
+
233
+ const exact = await session(context)
234
+ await Users.setEmail(db, exact.userId, 'Dev@Example.com')
235
+ const accepted = await publicLinks.accept.$post(
236
+ { json: { token: link.token } },
237
+ as(exact.cookie),
238
+ )
239
+ expect(accepted.status).toBe(200)
240
+
241
+ const exhaustedRetry = await publicLinks.accept.$post(
242
+ { json: { token: link.token } },
243
+ as(member.cookie),
244
+ )
245
+ expect(exhaustedRetry.status).toBe(200)
246
+
247
+ const afterJoin = await orgLinks.$get({ param: { orgId: org.id } }, as(owner.cookie))
248
+ expect(afterJoin.status).toBe(200)
249
+ if (afterJoin.status !== 200)
250
+ throw new Error(`Failed to list invite links: ${afterJoin.status}`)
251
+ expect((await afterJoin.json()).data[0]).toMatchObject({ status: 'exhausted', useCount: 1 })
252
+
253
+ const auditsAfterJoin = await orgLinks[':inviteLinkId{iln_[A-Za-z0-9_-]+}'].redemptions.$get(
254
+ { param: { inviteLinkId: link.id, orgId: org.id } },
255
+ as(owner.cookie),
256
+ )
257
+ expect(auditsAfterJoin.status).toBe(200)
258
+ if (auditsAfterJoin.status !== 200)
259
+ throw new Error(`Failed to list redemptions: ${auditsAfterJoin.status}`)
260
+ expect((await auditsAfterJoin.json()).data).toHaveLength(1)
261
+ })
262
+
146
263
  test('resolve reveals only a summary and hides every unavailable reason', async () => {
147
264
  const db = TestApp.database()
148
265
  const context = createApp(db)
@@ -10,8 +10,34 @@ import * as Response from '../../../internal/Response.js'
10
10
  import * as Schema from '../../../internal/Schema.js'
11
11
  import type { Environment } from '../App.js'
12
12
 
13
+ // A bare domain (`example.org`), normalized before validation.
14
+ const domainPattern = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/
15
+
13
16
  /** OpenAPI schemas owned by reusable invite links. */
14
17
  export namespace schema {
18
+ const allowedEmailDomains = z
19
+ .nullable(
20
+ z.readonly(
21
+ z
22
+ .array(
23
+ z
24
+ .string()
25
+ .check(
26
+ z.trim(),
27
+ z.toLowerCase(),
28
+ z.regex(domainPattern, 'Must be a bare domain (example.org).'),
29
+ ),
30
+ )
31
+ .check(
32
+ z.minLength(1),
33
+ z.overwrite((domains) => [...new Set(domains)]),
34
+ ),
35
+ ),
36
+ )
37
+ .check(
38
+ z.describe('Email domains allowed to redeem, or null for unrestricted.'),
39
+ z.meta({ examples: [['example.org'], null] }),
40
+ )
15
41
  const nullableExpiry = z
16
42
  .nullable(z.iso.datetime({ offset: true }))
17
43
  .check(
@@ -31,6 +57,7 @@ export namespace schema {
31
57
  /** Management representation, including the recopyable bearer id. */
32
58
  export const InviteLink = Schema.describe(
33
59
  z.object({
60
+ allowedEmailDomains,
34
61
  createdAt: z
35
62
  .string()
36
63
  .check(
@@ -146,6 +173,7 @@ export namespace schema {
146
173
  /** Create body. */
147
174
  export const CreateBody = z
148
175
  .object({
176
+ allowedEmailDomains: z.optional(allowedEmailDomains),
149
177
  expiresAt: z.optional(nullableExpiry),
150
178
  maxUses: z.optional(nullableUses),
151
179
  name: z.optional(
@@ -318,6 +346,7 @@ export function inviteLinks() {
318
346
  const body = c.req.valid('json')
319
347
  try {
320
348
  const record = await InviteLinks.create(Db.get(c.get('db')), {
349
+ allowedEmailDomains: body.allowedEmailDomains ?? null,
321
350
  createdBy: Auth.membership(c)?.userId ?? 'super_admin',
322
351
  expiresAt:
323
352
  body.expiresAt === undefined
@@ -498,7 +527,13 @@ export function inviteLinks() {
498
527
  OpenApi.describeRoute({
499
528
  operationId: 'acceptInviteLink',
500
529
  responses: OpenApi.responses({
501
- errors: { 403: 'A verified session email is required.', ...errors },
530
+ errors: {
531
+ 403: {
532
+ codes: ['forbidden', 'invite_link_email_forbidden'],
533
+ description: 'A verified session email from an allowed domain is required.',
534
+ },
535
+ ...errors,
536
+ },
502
537
  success: { description: 'Membership.', schema: schema.AcceptResponse },
503
538
  }),
504
539
  summary: 'Accept invite link',
@@ -531,6 +566,12 @@ export function inviteLinks() {
531
566
  200,
532
567
  )
533
568
  } catch (cause) {
569
+ if (cause instanceof InviteLinks.EmailDomainForbiddenError)
570
+ return Response.error(c, {
571
+ code: 'invite_link_email_forbidden',
572
+ message: 'Sign in with an email address on an allowed domain',
573
+ status: 403,
574
+ })
534
575
  return Response.upstream(c, cause)
535
576
  }
536
577
  },
@@ -539,6 +580,7 @@ export function inviteLinks() {
539
580
 
540
581
  function serialize(record: InviteLinks.Record) {
541
582
  return {
583
+ allowedEmailDomains: record.allowedEmailDomains,
542
584
  createdAt: record.createdAt,
543
585
  createdBy: record.createdBy,
544
586
  enabled: record.enabled,
@@ -182,13 +182,13 @@ describe('sign-in email requirement', () => {
182
182
  })
183
183
  })
184
184
 
185
- describe('sign-up email domains', () => {
186
- test('allows new users on an allowlisted domain', async () => {
187
- const oidc = await createIssuer({ email: 'dev@TEMPO.xyz' })
185
+ describe('open sign-up', () => {
186
+ test('allows new users on any domain', async () => {
187
+ const oidc = await createIssuer({ email: 'dev@example.com' })
188
188
  try {
189
189
  const app = TestApp.create({
190
190
  auth: { keys: [TestApp.key] },
191
- session: { emailAllowlist: ['Tempo.XYZ'], wallet: { issuer: oidc.url, origin } },
191
+ session: { wallet: { issuer: oidc.url, origin } },
192
192
  })
193
193
  const account = privateKeyToAccount(generatePrivateKey())
194
194
  const { cookie, verifyResponse } = await TestApp.signIn(app, account, {
@@ -199,109 +199,19 @@ describe('sign-up email domains', () => {
199
199
  const response = await app.request('/v1/me', { headers: { cookie: cookie! } })
200
200
  expect(response.status).toBe(200)
201
201
  const body = await TestApp.json(response, Me.schema.User)
202
- expect(body.email).toBe('dev@TEMPO.xyz')
203
- } finally {
204
- await oidc.close()
205
- }
206
- })
207
-
208
- test('rejects new users on other domains', async () => {
209
- const oidc = await createIssuer({ email: 'dev@example.com' })
210
- try {
211
- const app = TestApp.create({
212
- auth: { keys: [TestApp.key] },
213
- session: { emailAllowlist: ['tempo.xyz'], wallet: { issuer: oidc.url, origin } },
214
- })
215
- const account = privateKeyToAccount(generatePrivateKey())
216
- const { cookie, verifyResponse } = await TestApp.signIn(app, account, {
217
- idToken: ({ nonce }) => oidc.mint({ address: account.address, nonce }),
218
- })
219
-
220
- expect(verifyResponse.status).toBe(403)
221
- expect(cookie).toBeUndefined()
222
- expect(await verifyResponse.json()).toMatchInlineSnapshot(`
223
- {
224
- "error": "sign-ups are restricted to approved email domains or invited emails",
225
- }
226
- `)
202
+ expect(body.email).toBe('dev@example.com')
227
203
  } finally {
228
204
  await oidc.close()
229
205
  }
230
206
  })
231
207
 
232
- test('rejects new users without an identity token', async () => {
233
- const app = TestApp.create({
234
- auth: { keys: [TestApp.key] },
235
- session: { emailAllowlist: ['tempo.xyz'], wallet: { origin } },
236
- })
208
+ test('allows new users without an identity token', async () => {
209
+ const app = createApp()
237
210
  const account = privateKeyToAccount(generatePrivateKey())
238
211
  const { cookie, verifyResponse } = await TestApp.signIn(app, account)
239
212
 
240
- expect(verifyResponse.status).toBe(403)
241
- expect(cookie).toBeUndefined()
242
- })
243
-
244
- test('allows invited emails regardless of domain', async () => {
245
- const oidc = await createIssuer({ email: 'Invited@Example.com' })
246
- try {
247
- const db = TestApp.database()
248
-
249
- // Owner signs up while sign-up is open and issues the invitation.
250
- const open = TestApp.create({
251
- auth: { keys: [TestApp.key] },
252
- db,
253
- session: { wallet: { origin } },
254
- })
255
- const owner = privateKeyToAccount(generatePrivateKey())
256
- const { cookie } = await TestApp.signIn(open, owner)
257
- const created = await open.request('/v1/orgs', {
258
- body: JSON.stringify({ name: 'Acme' }),
259
- headers: { 'content-type': 'application/json', cookie: cookie! },
260
- method: 'POST',
261
- })
262
- const org = (await created.json()) as { id: string }
263
- const invited = await open.request(`/v1/orgs/${org.id}/invitations`, {
264
- body: JSON.stringify({ email: 'invited@example.com', role: 'member' }),
265
- headers: { 'content-type': 'application/json', cookie: cookie! },
266
- method: 'POST',
267
- })
268
- expect(invited.status).toBe(200)
269
-
270
- const restricted = TestApp.create({
271
- auth: { keys: [TestApp.key] },
272
- db,
273
- session: { emailAllowlist: ['tempo.xyz'], wallet: { issuer: oidc.url, origin } },
274
- })
275
- const account = privateKeyToAccount(generatePrivateKey())
276
- const { verifyResponse } = await TestApp.signIn(restricted, account, {
277
- idToken: ({ nonce }) => oidc.mint({ address: account.address, nonce }),
278
- })
279
- expect(verifyResponse.status).toBe(200)
280
- } finally {
281
- await oidc.close()
282
- }
283
- })
284
-
285
- test('allows existing users regardless of domain', async () => {
286
- const db = TestApp.database()
287
- const account = privateKeyToAccount(generatePrivateKey())
288
-
289
- // Row created before the restriction (open sign-up deployment).
290
- const open = TestApp.create({
291
- auth: { keys: [TestApp.key] },
292
- db,
293
- session: { wallet: { origin } },
294
- })
295
- const first = await TestApp.signIn(open, account)
296
- expect(first.verifyResponse.status).toBe(200)
297
-
298
- const restricted = TestApp.create({
299
- auth: { keys: [TestApp.key] },
300
- db,
301
- session: { emailAllowlist: ['tempo.xyz'], wallet: { origin } },
302
- })
303
- const second = await TestApp.signIn(restricted, account)
304
- expect(second.verifyResponse.status).toBe(200)
213
+ expect(verifyResponse.status).toBe(200)
214
+ expect(cookie).toBeDefined()
305
215
  })
306
216
  })
307
217
 
@@ -1,8 +1,11 @@
1
1
  import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
2
2
 
3
3
  import * as TestApp from '../../../../test/App.js'
4
+ import * as EarlyAccess from '../../../db/tables/earlyAccess.js'
5
+ import * as EnabledBillingSources from '../../../db/tables/enabledBillingSources.js'
4
6
  import * as Organizations from '../../../db/tables/organizations.js'
5
7
  import * as Projects from '../../../db/tables/projects.js'
8
+ import * as Users from '../../../db/tables/users.js'
6
9
  import type * as Log from '../../../internal/Log.js'
7
10
  import * as Orgs from './orgs.js'
8
11
 
@@ -196,7 +199,8 @@ describe('GET /orgs', () => {
196
199
 
197
200
  describe('POST /orgs', () => {
198
201
  test('creates a session-owned organization', async () => {
199
- const app = createApp()
202
+ const db = TestApp.database()
203
+ const app = createApp(db)
200
204
  const cookie = await session(app)
201
205
 
202
206
  const response = await app.request('/v1/orgs', json('POST', cookie, { name: 'Acme' }))
@@ -204,10 +208,48 @@ describe('POST /orgs', () => {
204
208
  const body = await TestApp.json(response, Orgs.schema.Organization)
205
209
  expect(body.id.startsWith('org_')).toBe(true)
206
210
  expect(body.name).toBe('Acme')
211
+ expect(await EnabledBillingSources.listByOrg(db, body.id)).toEqual([])
212
+ })
213
+
214
+ test.each([
215
+ { email: 'Dev@Example.NET', entry: 'dev@example.net', kind: 'email' },
216
+ { email: 'Dev@Example.ORG', entry: 'example.org', kind: 'domain' },
217
+ ])('enables Stripe for a session matching an early-access $kind', async ({ email, entry }) => {
218
+ const db = TestApp.database()
219
+ const app = createApp(db)
220
+ await EarlyAccess.add(db, { createdBy: 'test', entry })
221
+ const account = privateKeyToAccount(generatePrivateKey())
222
+ const { cookie } = await TestApp.signIn(app, account)
223
+ const user = await Users.getByAddress(db, account.address)
224
+ if (!user) throw new Error('sign-in did not create a user')
225
+ await Users.setEmail(db, user.id, email)
226
+
227
+ const response = await app.request('/v1/orgs', json('POST', cookie!, { name: 'Acme' }))
228
+ expect(response.status).toBe(200)
229
+ const org = await TestApp.json(response, Orgs.schema.Organization)
230
+ const sources = await EnabledBillingSources.listByOrg(db, org.id)
231
+ expect(sources.map(({ createdBy, source }) => ({ createdBy, source }))).toEqual([
232
+ { createdBy: user.id, source: 'stripe' },
233
+ ])
234
+ })
235
+
236
+ test('does not enable Stripe without an early-access grant', async () => {
237
+ const db = TestApp.database()
238
+ const app = createApp(db)
239
+ const account = privateKeyToAccount(generatePrivateKey())
240
+ const { cookie } = await TestApp.signIn(app, account)
241
+ const user = await Users.getByAddress(db, account.address)
242
+ if (!user) throw new Error('sign-in did not create a user')
243
+ await Users.setEmail(db, user.id, 'dev@example.net')
244
+
245
+ const response = await app.request('/v1/orgs', json('POST', cookie!, { name: 'Acme' }))
246
+ const org = await TestApp.json(response, Orgs.schema.Organization)
247
+ expect(await EnabledBillingSources.listByOrg(db, org.id)).toEqual([])
207
248
  })
208
249
 
209
250
  test('creates a memberless org as the super admin', async () => {
210
- const app = createApp()
251
+ const db = TestApp.database()
252
+ const app = createApp(db)
211
253
  const response = await app.request('/v1/orgs', {
212
254
  body: JSON.stringify({ name: 'Ops' }),
213
255
  headers: { 'content-type': 'application/json', 'tempo-api-key': secret },
@@ -216,6 +258,7 @@ describe('POST /orgs', () => {
216
258
  expect(response.status).toBe(200)
217
259
  const org = await TestApp.json(response, Orgs.schema.Organization)
218
260
  expect(org.name).toBe('Ops')
261
+ expect(await EnabledBillingSources.listByOrg(db, org.id)).toEqual([])
219
262
 
220
263
  // Memberless: invisible to sessions until an owner is seated.
221
264
  const cookie = await session(app)
@@ -5,6 +5,7 @@ import * as ApiKeys from '../../../ApiKeys.js'
5
5
  import type * as App from '../../../App.js'
6
6
  import * as Auth from '../../../internal/Auth.js'
7
7
  import * as Db from '../../../db/Db.js'
8
+ import * as EarlyAccess from '../../../db/tables/earlyAccess.js'
8
9
  import * as Memberships from '../../../db/tables/memberships.js'
9
10
  import * as OpenApi from '../../../internal/OpenApi.js'
10
11
  import * as Organizations from '../../../db/tables/organizations.js'
@@ -189,6 +190,11 @@ export function orgs() {
189
190
  ? await Organizations.create(db, { createdBy: 'super_admin', name: body.name })
190
191
  : {
191
192
  ...(await Organizations.createOwned(db, {
193
+ enabledBillingSources:
194
+ principal.email &&
195
+ (await EarlyAccess.matches(db, principal.email.trim().toLowerCase()))
196
+ ? ['stripe']
197
+ : [],
192
198
  name: body.name,
193
199
  userId: principal.id,
194
200
  })),
@@ -21,6 +21,7 @@ describe('scopes', () => {
21
21
  "indexer:query",
22
22
  "management:read",
23
23
  "management:write",
24
+ "mpp:write",
24
25
  "rpc-relay:read",
25
26
  "rpc-relay:sponsor",
26
27
  "webhooks:read",