tapimo 0.6.0 → 0.7.1

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 (288) hide show
  1. package/dist/App.d.ts +8641 -8263
  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 +112 -11
  10. package/dist/admin/App.d.ts.map +1 -1
  11. package/dist/admin/apps/api-keys.d.ts +2 -1
  12. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  13. package/dist/admin/apps/early-access.d.ts +2 -2
  14. package/dist/admin/apps/early-access.d.ts.map +1 -1
  15. package/dist/admin/apps/early-access.js +5 -5
  16. package/dist/admin/apps/early-access.js.map +1 -1
  17. package/dist/admin/apps/organizations.d.ts +124 -1
  18. package/dist/admin/apps/organizations.d.ts.map +1 -1
  19. package/dist/admin/apps/organizations.js +97 -6
  20. package/dist/admin/apps/organizations.js.map +1 -1
  21. package/dist/admin/ui.generated.d.ts +1 -1
  22. package/dist/admin/ui.generated.d.ts.map +1 -1
  23. package/dist/admin/ui.generated.js +47 -39
  24. package/dist/admin/ui.generated.js.map +1 -1
  25. package/dist/analytics/Analytics.d.ts.map +1 -1
  26. package/dist/analytics/Analytics.js +20 -0
  27. package/dist/analytics/Analytics.js.map +1 -1
  28. package/dist/analytics/tables/requestEvents.d.ts +18 -0
  29. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  30. package/dist/analytics/tables/requestEvents.js +36 -7
  31. package/dist/analytics/tables/requestEvents.js.map +1 -1
  32. package/dist/apps/data/App.d.ts +8050 -8019
  33. package/dist/apps/data/App.d.ts.map +1 -1
  34. package/dist/apps/data/FundingQuoteProviders.js +2 -15
  35. package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
  36. package/dist/apps/data/FxOracle.d.ts +1 -1
  37. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  38. package/dist/apps/data/FxOracle.js +1 -1
  39. package/dist/apps/data/FxOracle.js.map +1 -1
  40. package/dist/apps/data/routes/activities.d.ts +27 -27
  41. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  42. package/dist/apps/data/routes/activities.js +21 -1
  43. package/dist/apps/data/routes/activities.js.map +1 -1
  44. package/dist/apps/data/routes/balances.d.ts +58 -58
  45. package/dist/apps/data/routes/blocks.d.ts +39 -39
  46. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  47. package/dist/apps/data/routes/exchanges.d.ts +669 -669
  48. package/dist/apps/data/routes/fee-amm.d.ts +157 -143
  49. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  50. package/dist/apps/data/routes/fee-amm.js +76 -91
  51. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  52. package/dist/apps/data/routes/funding.d.ts +72 -34
  53. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  54. package/dist/apps/data/routes/funding.js +34 -26
  55. package/dist/apps/data/routes/funding.js.map +1 -1
  56. package/dist/apps/data/routes/indexer.d.ts +30 -30
  57. package/dist/apps/data/routes/receipts.d.ts +8 -8
  58. package/dist/apps/data/routes/rpc.d.ts +31 -31
  59. package/dist/apps/data/routes/tokenlist.d.ts.map +1 -1
  60. package/dist/apps/data/routes/tokenlist.js +12 -3
  61. package/dist/apps/data/routes/tokenlist.js.map +1 -1
  62. package/dist/apps/data/routes/tokens.d.ts +4 -4
  63. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  64. package/dist/apps/data/routes/tokens.js +5 -1
  65. package/dist/apps/data/routes/tokens.js.map +1 -1
  66. package/dist/apps/data/routes/transactions.d.ts +6 -6
  67. package/dist/apps/data/routes/transfers.d.ts +101 -90
  68. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  69. package/dist/apps/data/routes/transfers.js +226 -141
  70. package/dist/apps/data/routes/transfers.js.map +1 -1
  71. package/dist/apps/data/routes/valuation.d.ts +2 -2
  72. package/dist/apps/data/routes/valuation.js +3 -3
  73. package/dist/apps/data/routes/valuation.js.map +1 -1
  74. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  75. package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
  76. package/dist/apps/data/routes/verified-tokens.js +34 -9
  77. package/dist/apps/data/routes/verified-tokens.js.map +1 -1
  78. package/dist/apps/data/routes/webhooks.d.ts +2 -2
  79. package/dist/apps/data/routes/webhooks.js +14 -6
  80. package/dist/apps/data/routes/webhooks.js.map +1 -1
  81. package/dist/apps/index.d.ts +1 -0
  82. package/dist/apps/index.d.ts.map +1 -1
  83. package/dist/apps/index.js +1 -0
  84. package/dist/apps/index.js.map +1 -1
  85. package/dist/apps/management/App.d.ts +460 -523
  86. package/dist/apps/management/App.d.ts.map +1 -1
  87. package/dist/apps/management/routes/api-keys.d.ts +7 -23
  88. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  89. package/dist/apps/management/routes/api-keys.js +20 -8
  90. package/dist/apps/management/routes/api-keys.js.map +1 -1
  91. package/dist/apps/management/routes/billing.d.ts +106 -151
  92. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  93. package/dist/apps/management/routes/billing.js +28 -28
  94. package/dist/apps/management/routes/billing.js.map +1 -1
  95. package/dist/apps/management/routes/faucet.d.ts +48 -34
  96. package/dist/apps/management/routes/faucet.d.ts.map +1 -1
  97. package/dist/apps/management/routes/faucet.js +33 -18
  98. package/dist/apps/management/routes/faucet.js.map +1 -1
  99. package/dist/apps/management/routes/invitations.d.ts +131 -131
  100. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  101. package/dist/apps/management/routes/invitations.js +0 -3
  102. package/dist/apps/management/routes/invitations.js.map +1 -1
  103. package/dist/apps/management/routes/me.d.ts +6 -8
  104. package/dist/apps/management/routes/me.d.ts.map +1 -1
  105. package/dist/apps/management/routes/me.js +0 -4
  106. package/dist/apps/management/routes/me.js.map +1 -1
  107. package/dist/apps/management/routes/members.d.ts +73 -73
  108. package/dist/apps/management/routes/orgs.d.ts +94 -94
  109. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  110. package/dist/apps/management/routes/orgs.js +5 -0
  111. package/dist/apps/management/routes/orgs.js.map +1 -1
  112. package/dist/apps/management/routes/usage.d.ts +72 -72
  113. package/dist/apps/management/routes/verified-token-requests.d.ts +94 -94
  114. package/dist/apps/management/routes/webhooks.d.ts +1 -1
  115. package/dist/apps/mpp/App.d.ts +363 -0
  116. package/dist/apps/mpp/App.d.ts.map +1 -0
  117. package/dist/apps/mpp/App.js +72 -0
  118. package/dist/apps/mpp/App.js.map +1 -0
  119. package/dist/apps/mpp/Mpp.d.ts +178 -0
  120. package/dist/apps/mpp/Mpp.d.ts.map +1 -0
  121. package/dist/apps/mpp/Mpp.js +183 -0
  122. package/dist/apps/mpp/Mpp.js.map +1 -0
  123. package/dist/apps/mpp/index.d.ts +3 -0
  124. package/dist/apps/mpp/index.d.ts.map +1 -0
  125. package/dist/apps/mpp/index.js +3 -0
  126. package/dist/apps/mpp/index.js.map +1 -0
  127. package/dist/apps/relay/App.d.ts.map +1 -1
  128. package/dist/apps/relay/App.js +57 -10
  129. package/dist/apps/relay/App.js.map +1 -1
  130. package/dist/apps/relay/Sponsorships.js +1 -1
  131. package/dist/apps/relay/Sponsorships.js.map +1 -1
  132. package/dist/db/Db.d.ts +2 -0
  133. package/dist/db/Db.d.ts.map +1 -1
  134. package/dist/db/Db.js +27 -0
  135. package/dist/db/Db.js.map +1 -1
  136. package/dist/db/Schema.d.ts +12 -0
  137. package/dist/db/Schema.d.ts.map +1 -1
  138. package/dist/db/Schema.js +11 -0
  139. package/dist/db/Schema.js.map +1 -1
  140. package/dist/db/tables/enabledBillingSources.d.ts +73 -0
  141. package/dist/db/tables/enabledBillingSources.d.ts.map +1 -0
  142. package/dist/db/tables/enabledBillingSources.js +69 -0
  143. package/dist/db/tables/enabledBillingSources.js.map +1 -0
  144. package/dist/db/tables/invitations.d.ts +1 -12
  145. package/dist/db/tables/invitations.d.ts.map +1 -1
  146. package/dist/db/tables/invitations.js +3 -23
  147. package/dist/db/tables/invitations.js.map +1 -1
  148. package/dist/db/tables/organizations.d.ts +5 -2
  149. package/dist/db/tables/organizations.d.ts.map +1 -1
  150. package/dist/db/tables/organizations.js +32 -2
  151. package/dist/db/tables/organizations.js.map +1 -1
  152. package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
  153. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  154. package/dist/db/tables/sponsoredTransactions.js +13 -3
  155. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  156. package/dist/handlers/internal/utils.d.ts +1 -16
  157. package/dist/handlers/internal/utils.d.ts.map +1 -1
  158. package/dist/handlers/internal/utils.js +17 -6
  159. package/dist/handlers/internal/utils.js.map +1 -1
  160. package/dist/handlers/relay.d.ts.map +1 -1
  161. package/dist/handlers/relay.js +37 -5
  162. package/dist/handlers/relay.js.map +1 -1
  163. package/dist/internal/Auth.d.ts +0 -32
  164. package/dist/internal/Auth.d.ts.map +1 -1
  165. package/dist/internal/Auth.js +3 -51
  166. package/dist/internal/Auth.js.map +1 -1
  167. package/dist/internal/EdgeCache.d.ts +1 -1
  168. package/dist/internal/EdgeCache.d.ts.map +1 -1
  169. package/dist/internal/EdgeCache.js +3 -2
  170. package/dist/internal/EdgeCache.js.map +1 -1
  171. package/dist/internal/FundingQuotes.d.ts +78 -32
  172. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  173. package/dist/internal/FundingQuotes.js +45 -8
  174. package/dist/internal/FundingQuotes.js.map +1 -1
  175. package/dist/internal/Log.d.ts +18 -4
  176. package/dist/internal/Log.d.ts.map +1 -1
  177. package/dist/internal/Log.js +50 -3
  178. package/dist/internal/Log.js.map +1 -1
  179. package/dist/internal/MetricSink.d.ts.map +1 -1
  180. package/dist/internal/MetricSink.js +15 -0
  181. package/dist/internal/MetricSink.js.map +1 -1
  182. package/dist/internal/Response.d.ts +2 -2
  183. package/dist/internal/Response.d.ts.map +1 -1
  184. package/dist/internal/Response.js +12 -4
  185. package/dist/internal/Response.js.map +1 -1
  186. package/dist/internal/Schema.d.ts +2 -2
  187. package/dist/internal/Schema.d.ts.map +1 -1
  188. package/dist/internal/Schema.js +4 -2
  189. package/dist/internal/Schema.js.map +1 -1
  190. package/dist/internal/Viem.d.ts +1 -1
  191. package/package.json +7 -2
  192. package/src/App.test.ts +9 -4
  193. package/src/App.ts +14 -2
  194. package/src/Client.test-d.ts +109 -28
  195. package/src/Scope.ts +5 -0
  196. package/src/admin/App.test.ts +4 -0
  197. package/src/admin/apps/early-access.ts +6 -5
  198. package/src/admin/apps/organizations.test.ts +86 -2
  199. package/src/admin/apps/organizations.ts +155 -31
  200. package/src/admin/apps/scopes.test.ts +5 -0
  201. package/src/admin/ui/src/lib/organizations.test.ts +81 -1
  202. package/src/admin/ui/src/lib/organizations.ts +92 -0
  203. package/src/admin/ui/src/routes/api-keys.tsx +1 -1
  204. package/src/admin/ui/src/routes/early-access.tsx +8 -3
  205. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +103 -0
  206. package/src/admin/ui.generated.ts +49 -39
  207. package/src/analytics/Analytics.test.ts +83 -0
  208. package/src/analytics/Analytics.ts +20 -0
  209. package/src/analytics/tables/requestEvents.ts +60 -7
  210. package/src/apps/composition.test.ts +11 -2
  211. package/src/apps/data/App.test.ts +14 -2
  212. package/src/apps/data/FundingQuoteProviders.test.ts +0 -5
  213. package/src/apps/data/FundingQuoteProviders.ts +2 -29
  214. package/src/apps/data/FxOracle.test.ts +2 -2
  215. package/src/apps/data/FxOracle.ts +2 -2
  216. package/src/apps/data/routes/activities.test.ts +6 -2
  217. package/src/apps/data/routes/activities.ts +39 -1
  218. package/src/apps/data/routes/balances.test.ts +6 -2
  219. package/src/apps/data/routes/exchanges.test.ts +7 -3
  220. package/src/apps/data/routes/fee-amm.test.ts +47 -23
  221. package/src/apps/data/routes/fee-amm.ts +98 -119
  222. package/src/apps/data/routes/funding.test.ts +29 -8
  223. package/src/apps/data/routes/funding.ts +42 -48
  224. package/src/apps/data/routes/receipts.test.ts +6 -2
  225. package/src/apps/data/routes/tokenlist.test.ts +15 -0
  226. package/src/apps/data/routes/tokenlist.ts +18 -5
  227. package/src/apps/data/routes/tokens.ts +5 -1
  228. package/src/apps/data/routes/transfers.test.ts +203 -35
  229. package/src/apps/data/routes/transfers.ts +294 -172
  230. package/src/apps/data/routes/valuation.test.ts +18 -3
  231. package/src/apps/data/routes/valuation.ts +4 -4
  232. package/src/apps/data/routes/verified-tokens.test.ts +51 -0
  233. package/src/apps/data/routes/verified-tokens.ts +52 -15
  234. package/src/apps/data/routes/webhooks.test.ts +56 -0
  235. package/src/apps/data/routes/webhooks.ts +17 -8
  236. package/src/apps/index.ts +1 -0
  237. package/src/apps/management/routes/api-keys.test.ts +2 -75
  238. package/src/apps/management/routes/api-keys.ts +30 -7
  239. package/src/apps/management/routes/billing.test.ts +106 -85
  240. package/src/apps/management/routes/billing.ts +32 -21
  241. package/src/apps/management/routes/faucet.test.ts +11 -3
  242. package/src/apps/management/routes/faucet.ts +46 -30
  243. package/src/apps/management/routes/invitations.test.ts +0 -172
  244. package/src/apps/management/routes/invitations.ts +0 -3
  245. package/src/apps/management/routes/me.test.ts +3 -145
  246. package/src/apps/management/routes/me.ts +0 -6
  247. package/src/apps/management/routes/orgs.test.ts +45 -2
  248. package/src/apps/management/routes/orgs.ts +6 -0
  249. package/src/apps/management/routes/scopes.test.ts +1 -0
  250. package/src/apps/mpp/App.ts +86 -0
  251. package/src/apps/mpp/Mpp.test.ts +193 -0
  252. package/src/apps/mpp/Mpp.ts +353 -0
  253. package/src/apps/mpp/index.ts +2 -0
  254. package/src/apps/mpp/internal/Adapter.ts +11 -0
  255. package/src/apps/mpp/internal/Error.ts +9 -0
  256. package/src/apps/mpp/internal/Idempotency.ts +7 -0
  257. package/src/apps/mpp/internal/Screening.ts +10 -0
  258. package/src/apps/relay/App.test.ts +90 -8
  259. package/src/apps/relay/App.ts +62 -10
  260. package/src/apps/relay/Sponsorships.test.ts +2 -0
  261. package/src/apps/relay/Sponsorships.ts +1 -1
  262. package/src/db/Db.test.ts +2 -2
  263. package/src/db/Db.ts +30 -0
  264. package/src/db/Schema.ts +15 -0
  265. package/src/db/tables/enabledBillingSources.test.ts +303 -0
  266. package/src/db/tables/enabledBillingSources.ts +118 -0
  267. package/src/db/tables/invitations.ts +4 -26
  268. package/src/db/tables/organizations.test.ts +191 -0
  269. package/src/db/tables/organizations.ts +44 -3
  270. package/src/db/tables/sponsoredTransactions.test.ts +41 -2
  271. package/src/db/tables/sponsoredTransactions.ts +15 -3
  272. package/src/handlers/internal/utils.ts +22 -6
  273. package/src/handlers/relay.errors.test.ts +153 -0
  274. package/src/handlers/relay.ts +42 -7
  275. package/src/internal/Auth.test.ts +3 -3
  276. package/src/internal/Auth.ts +3 -63
  277. package/src/internal/EdgeCache.test.ts +18 -5
  278. package/src/internal/EdgeCache.ts +4 -2
  279. package/src/internal/FundingQuotes.test.ts +18 -5
  280. package/src/internal/FundingQuotes.ts +46 -11
  281. package/src/internal/Log.test.ts +91 -0
  282. package/src/internal/Log.ts +69 -5
  283. package/src/internal/MetricSink.test.ts +55 -0
  284. package/src/internal/MetricSink.ts +14 -0
  285. package/src/internal/Response.test.ts +28 -0
  286. package/src/internal/Response.ts +12 -4
  287. package/src/internal/Schema.test.ts +13 -0
  288. package/src/internal/Schema.ts +14 -7
@@ -0,0 +1,303 @@
1
+ import { nanoid } from 'nanoid'
2
+
3
+ import * as Runtime from '../../../test/runtime.js'
4
+ import * as Db from '../Db.js'
5
+ import * as EarlyAccess from './earlyAccess.js'
6
+ import * as EnabledBillingSources from './enabledBillingSources.js'
7
+ import * as Invitations from './invitations.js'
8
+ import * as Organizations from './organizations.js'
9
+ import * as StripeCustomers from './stripeCustomers.js'
10
+ import * as Users from './users.js'
11
+
12
+ const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
13
+
14
+ async function createOwned(db: Db.Db, options: createOwned.Options) {
15
+ const user = await Users.upsertByAddress(db, { address: options.address })
16
+ if (options.email) await Users.setEmail(db, user.id, options.email)
17
+ const organization = await Organizations.createOwned(db, { name: options.name, userId: user.id })
18
+ return { organization, user }
19
+ }
20
+
21
+ declare namespace createOwned {
22
+ /** Inputs for an owned-organization fixture. */
23
+ type Options = {
24
+ /** User address. */
25
+ address: string
26
+ /** Optional user email. */
27
+ email?: string | undefined
28
+ /** Organization name. */
29
+ name: string
30
+ }
31
+ }
32
+
33
+ /** Redacts nondeterministic row fields. */
34
+ function redact(record: EnabledBillingSources.Record) {
35
+ return { ...record, createdAt: '<timestamp>' }
36
+ }
37
+
38
+ describe('add', () => {
39
+ test('behavior: enables each source once per organization', async () => {
40
+ const db = create()
41
+ await db.migrate()
42
+ const stripe = await EnabledBillingSources.add(db, {
43
+ createdBy: 'admin@tempo.xyz',
44
+ orgId: 'org_1',
45
+ source: 'stripe',
46
+ })
47
+ expect(stripe && redact(stripe)).toMatchInlineSnapshot(`
48
+ {
49
+ "createdAt": "<timestamp>",
50
+ "createdBy": "admin@tempo.xyz",
51
+ "orgId": "org_1",
52
+ "source": "stripe",
53
+ }
54
+ `)
55
+ expect(
56
+ await EnabledBillingSources.add(db, {
57
+ createdBy: 'other@tempo.xyz',
58
+ orgId: 'org_1',
59
+ source: 'stripe',
60
+ }),
61
+ ).toBeUndefined()
62
+ expect(
63
+ await EnabledBillingSources.add(db, {
64
+ createdBy: 'admin@tempo.xyz',
65
+ orgId: 'org_1',
66
+ source: 'tempo',
67
+ }),
68
+ ).toBeDefined()
69
+ await db.close()
70
+ })
71
+ })
72
+
73
+ describe('listByOrg', () => {
74
+ test('behavior: lists only the organization in canonical source order', async () => {
75
+ const db = create()
76
+ await db.migrate()
77
+ await EnabledBillingSources.add(db, {
78
+ createdBy: 'admin@tempo.xyz',
79
+ orgId: 'org_1',
80
+ source: 'tempo',
81
+ })
82
+ await EnabledBillingSources.add(db, {
83
+ createdBy: 'admin@tempo.xyz',
84
+ orgId: 'org_2',
85
+ source: 'stripe',
86
+ })
87
+ await EnabledBillingSources.add(db, {
88
+ createdBy: 'admin@tempo.xyz',
89
+ orgId: 'org_1',
90
+ source: 'stripe',
91
+ })
92
+ expect((await EnabledBillingSources.listByOrg(db, 'org_1')).map(redact)).toMatchInlineSnapshot(`
93
+ [
94
+ {
95
+ "createdAt": "<timestamp>",
96
+ "createdBy": "admin@tempo.xyz",
97
+ "orgId": "org_1",
98
+ "source": "stripe",
99
+ },
100
+ {
101
+ "createdAt": "<timestamp>",
102
+ "createdBy": "admin@tempo.xyz",
103
+ "orgId": "org_1",
104
+ "source": "tempo",
105
+ },
106
+ ]
107
+ `)
108
+ await db.close()
109
+ })
110
+
111
+ test('behavior: new organizations start with no enabled sources', async () => {
112
+ const db = create()
113
+ await db.migrate()
114
+ await Organizations.create(db, { id: 'org_new', name: 'New' })
115
+ expect(await EnabledBillingSources.listByOrg(db, 'org_new')).toEqual([])
116
+ await db.close()
117
+ })
118
+ })
119
+
120
+ describe('remove', () => {
121
+ test('behavior: removes one source and reports absence', async () => {
122
+ const db = create()
123
+ await db.migrate()
124
+ await EnabledBillingSources.add(db, {
125
+ createdBy: 'admin@tempo.xyz',
126
+ orgId: 'org_1',
127
+ source: 'stripe',
128
+ })
129
+ await EnabledBillingSources.add(db, {
130
+ createdBy: 'admin@tempo.xyz',
131
+ orgId: 'org_1',
132
+ source: 'tempo',
133
+ })
134
+ expect(
135
+ redact(
136
+ (await EnabledBillingSources.remove(db, {
137
+ orgId: 'org_1',
138
+ source: 'stripe',
139
+ }))!,
140
+ ),
141
+ ).toMatchInlineSnapshot(`
142
+ {
143
+ "createdAt": "<timestamp>",
144
+ "createdBy": "admin@tempo.xyz",
145
+ "orgId": "org_1",
146
+ "source": "stripe",
147
+ }
148
+ `)
149
+ expect(
150
+ await EnabledBillingSources.remove(db, { orgId: 'org_1', source: 'stripe' }),
151
+ ).toBeUndefined()
152
+ expect(await EnabledBillingSources.isEnabled(db, { orgId: 'org_1', source: 'tempo' })).toBe(
153
+ true,
154
+ )
155
+ await db.close()
156
+ })
157
+ })
158
+
159
+ describe('isEnabled', () => {
160
+ test('behavior: checks one organization and source', async () => {
161
+ const db = create()
162
+ await db.migrate()
163
+ await EnabledBillingSources.add(db, {
164
+ createdBy: 'admin@tempo.xyz',
165
+ orgId: 'org_1',
166
+ source: 'stripe',
167
+ })
168
+ expect(await EnabledBillingSources.isEnabled(db, { orgId: 'org_1', source: 'stripe' })).toBe(
169
+ true,
170
+ )
171
+ expect(await EnabledBillingSources.isEnabled(db, { orgId: 'org_1', source: 'tempo' })).toBe(
172
+ false,
173
+ )
174
+ expect(await EnabledBillingSources.isEnabled(db, { orgId: 'org_2', source: 'stripe' })).toBe(
175
+ false,
176
+ )
177
+ await db.close()
178
+ })
179
+ })
180
+
181
+ describe('migration', () => {
182
+ test('behavior: catch-up backfills one Stripe source per distinct customer organization', async () => {
183
+ const db = create()
184
+ await db.migrate()
185
+ await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_live_1' })
186
+ await StripeCustomers.create(db, {
187
+ environment: 'sandbox',
188
+ orgId: 'org_1',
189
+ stripeCustomerId: 'cus_test_1',
190
+ })
191
+ await StripeCustomers.create(db, { orgId: 'org_2', stripeCustomerId: 'cus_live_2' })
192
+ await db.kysely
193
+ .deleteFrom('migrations')
194
+ .where('name', '=', '0072_enabled_billing_sources_stripe_catch_up')
195
+ .execute()
196
+ await db.migrate()
197
+ await db.kysely
198
+ .deleteFrom('migrations')
199
+ .where('name', '=', '0072_enabled_billing_sources_stripe_catch_up')
200
+ .execute()
201
+ await db.migrate()
202
+
203
+ expect(
204
+ (await EnabledBillingSources.listByOrg(db, 'org_1')).map((record) => ({
205
+ createdBy: record.createdBy,
206
+ source: record.source,
207
+ })),
208
+ ).toMatchInlineSnapshot(`
209
+ [
210
+ {
211
+ "createdBy": "migration",
212
+ "source": "stripe",
213
+ },
214
+ ]
215
+ `)
216
+ expect((await EnabledBillingSources.listByOrg(db, 'org_2')).map((record) => record.source))
217
+ .toMatchInlineSnapshot(`
218
+ [
219
+ "stripe",
220
+ ]
221
+ `)
222
+ await db.close()
223
+ })
224
+
225
+ test('behavior: backfills Stripe only for organizations owned by allowlisted users', async () => {
226
+ const db = create()
227
+ await db.migrate()
228
+ await EarlyAccess.add(db, { createdBy: 'admin@tempo.xyz', entry: 'exact@example.net' })
229
+ await EarlyAccess.add(db, { createdBy: 'admin@tempo.xyz', entry: 'example.org' })
230
+ const exact = await createOwned(db, {
231
+ address: `0x${'11'.repeat(20)}`,
232
+ email: ' Exact@Example.NET ',
233
+ name: 'Exact',
234
+ })
235
+ const domain = await createOwned(db, {
236
+ address: `0x${'22'.repeat(20)}`,
237
+ email: 'Owner@Example.ORG',
238
+ name: 'Domain',
239
+ })
240
+ const invited = await createOwned(db, {
241
+ address: `0x${'33'.repeat(20)}`,
242
+ email: 'Invited@Example.NET',
243
+ name: 'Invited',
244
+ })
245
+ const legacyInvitation = await Invitations.create(db, {
246
+ email: 'invited@example.net',
247
+ expiresAt: '2020-01-01T00:00:00.000Z',
248
+ invitedBy: exact.user.id,
249
+ orgId: exact.organization.id,
250
+ role: 'member',
251
+ })
252
+ await db.kysely
253
+ .updateTable('invitations')
254
+ .set({ grantsEarlyAccess: true })
255
+ .where('id', '=', legacyInvitation.id)
256
+ .execute()
257
+ const plain = await createOwned(db, {
258
+ address: `0x${'44'.repeat(20)}`,
259
+ email: 'plain@example.net',
260
+ name: 'Plain',
261
+ })
262
+ const emailLess = await createOwned(db, {
263
+ address: `0x${'55'.repeat(20)}`,
264
+ name: 'Email-less',
265
+ })
266
+
267
+ for (let attempt = 0; attempt < 2; attempt++) {
268
+ await db.kysely
269
+ .deleteFrom('migrations')
270
+ .where('name', '=', '0073_enabled_billing_sources_early_access_backfill')
271
+ .execute()
272
+ await db.migrate()
273
+ }
274
+
275
+ const result = await Promise.all(
276
+ [exact, domain, invited, plain, emailLess].map(async ({ organization }) =>
277
+ (await EnabledBillingSources.listByOrg(db, organization.id)).map(
278
+ ({ createdBy, source }) => ({ createdBy, source }),
279
+ ),
280
+ ),
281
+ )
282
+ expect(result).toMatchInlineSnapshot(`
283
+ [
284
+ [
285
+ {
286
+ "createdBy": "migration",
287
+ "source": "stripe",
288
+ },
289
+ ],
290
+ [
291
+ {
292
+ "createdBy": "migration",
293
+ "source": "stripe",
294
+ },
295
+ ],
296
+ [],
297
+ [],
298
+ [],
299
+ ]
300
+ `)
301
+ await db.close()
302
+ })
303
+ })
@@ -0,0 +1,118 @@
1
+ import type { Selectable } from 'kysely'
2
+
3
+ import type * as Db from '../Db.js'
4
+ import type * as db_Schema from '../Schema.js'
5
+
6
+ /** Columns of the `enabled_billing_sources` table, derived from `Schema.EnabledBillingSource`. */
7
+ export type Table = db_Schema.EnabledBillingSource
8
+
9
+ /** A stored enabled billing-source row. */
10
+ export type Record = Selectable<Table>
11
+
12
+ /** An enabled billing-source type. */
13
+ export type Source = Record['source']
14
+
15
+ /** Billing-source types in canonical display order. */
16
+ export const sources = ['stripe', 'tempo'] as const satisfies readonly Source[]
17
+
18
+ /**
19
+ * Enables a billing-source type for an organization.
20
+ *
21
+ * @param db - The database.
22
+ * @param input - The source to enable.
23
+ * @returns The stored row, or `undefined` when already enabled.
24
+ */
25
+ export function add(db: Db.Db, input: add.Input): Promise<Record | undefined> {
26
+ return db.kysely
27
+ .insertInto('enabled_billing_sources')
28
+ .values({
29
+ createdAt: new Date().toISOString(),
30
+ createdBy: input.createdBy,
31
+ orgId: input.orgId,
32
+ source: input.source,
33
+ })
34
+ .onConflict((oc) => oc.columns(['orgId', 'source']).doNothing())
35
+ .returningAll()
36
+ .executeTakeFirst()
37
+ }
38
+
39
+ export declare namespace add {
40
+ /** Fields accepted when enabling a billing source. */
41
+ type Input = {
42
+ /** Identity enabling the source. */
43
+ createdBy: string
44
+ /** Organization id (`org_…`) the source is enabled for. */
45
+ orgId: string
46
+ /** Billing-source type to enable. */
47
+ source: Source
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Lists an organization's enabled billing sources in canonical order.
53
+ *
54
+ * @param db - The database.
55
+ * @param orgId - The organization id (`org_…`).
56
+ * @returns The enabled sources.
57
+ */
58
+ export function listByOrg(db: Db.Db, orgId: string): Promise<Record[]> {
59
+ return db.kysely
60
+ .selectFrom('enabled_billing_sources')
61
+ .selectAll()
62
+ .where('orgId', '=', orgId)
63
+ .orderBy('source', 'asc')
64
+ .execute()
65
+ }
66
+
67
+ /**
68
+ * Removes one enabled billing source from an organization.
69
+ *
70
+ * @param db - The database.
71
+ * @param input - The source to remove.
72
+ * @returns The removed row, or `undefined` when absent.
73
+ */
74
+ export function remove(db: Db.Db, input: remove.Input): Promise<Record | undefined> {
75
+ return db.kysely
76
+ .deleteFrom('enabled_billing_sources')
77
+ .where('orgId', '=', input.orgId)
78
+ .where('source', '=', input.source)
79
+ .returningAll()
80
+ .executeTakeFirst()
81
+ }
82
+
83
+ export declare namespace remove {
84
+ /** Fields identifying one enabled billing source. */
85
+ type Input = {
86
+ /** Organization id (`org_…`) the source is enabled for. */
87
+ orgId: string
88
+ /** Billing-source type to remove. */
89
+ source: Source
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Checks whether a billing-source type is enabled for an organization.
95
+ *
96
+ * @param db - The database.
97
+ * @param input - The source to check.
98
+ * @returns Whether the source is enabled.
99
+ */
100
+ export async function isEnabled(db: Db.Db, input: isEnabled.Input): Promise<boolean> {
101
+ const record = await db.kysely
102
+ .selectFrom('enabled_billing_sources')
103
+ .select('source')
104
+ .where('orgId', '=', input.orgId)
105
+ .where('source', '=', input.source)
106
+ .executeTakeFirst()
107
+ return record !== undefined
108
+ }
109
+
110
+ export declare namespace isEnabled {
111
+ /** Fields identifying one enabled billing source. */
112
+ type Input = {
113
+ /** Organization id (`org_…`) the source is enabled for. */
114
+ orgId: string
115
+ /** Billing-source type to check. */
116
+ source: Source
117
+ }
118
+ }
@@ -1,4 +1,4 @@
1
- import { type Selectable, sql } from 'kysely'
1
+ import type { Selectable } from 'kysely'
2
2
 
3
3
  import * as Id from '../../internal/Id.js'
4
4
  import type * as Db from '../Db.js'
@@ -31,7 +31,8 @@ export function create(db: Db.Db, input: create.Input): Promise<Record> {
31
31
  createdAt: now,
32
32
  email: input.email.toLowerCase(),
33
33
  expiresAt: input.expiresAt,
34
- grantsEarlyAccess: input.grantsEarlyAccess,
34
+ // Keep the legacy column inert until it can be removed safely.
35
+ grantsEarlyAccess: false,
35
36
  id: Id.generate('inv'),
36
37
  invitedBy: input.invitedBy,
37
38
  orgId: input.orgId,
@@ -46,8 +47,7 @@ export function create(db: Db.Db, input: create.Input): Promise<Record> {
46
47
  .doUpdateSet({
47
48
  createdAt: now,
48
49
  expiresAt: input.expiresAt,
49
- // Re-invites upgrade the grant, never withdraw it (raw SQL stays snake_case).
50
- grantsEarlyAccess: sql<boolean>`invitations.grants_early_access OR excluded.grants_early_access`,
50
+ grantsEarlyAccess: false,
51
51
  invitedBy: input.invitedBy,
52
52
  role: input.role,
53
53
  }),
@@ -63,8 +63,6 @@ export declare namespace create {
63
63
  email: string
64
64
  /** When the invitation expires (ISO 8601). */
65
65
  expiresAt: string
66
- /** Whether accepting grants early access; stamped from the inviter's own access. */
67
- grantsEarlyAccess: boolean
68
66
  /** User or API key id creating the invitation. */
69
67
  invitedBy: string
70
68
  /** Organization id (`org_…`) the invitation joins. */
@@ -119,26 +117,6 @@ export function listByEmail(db: Db.Db, email: string): Promise<(Record & { orgNa
119
117
  .execute()
120
118
  }
121
119
 
122
- /**
123
- * Whether any unrevoked invitation grants `email` early access. Acceptance and
124
- * expiry are deliberately ignored: a trusted invite grants access until revoked.
125
- *
126
- * @param db - The database.
127
- * @param email - The invitee email (matched lowercase).
128
- * @returns Whether a grant exists.
129
- */
130
- export async function hasEarlyAccessGrant(db: Db.Db, email: string): Promise<boolean> {
131
- const row = await db.kysely
132
- .selectFrom('invitations')
133
- .select('id')
134
- .where('email', '=', email.toLowerCase())
135
- .where('grantsEarlyAccess', '=', true)
136
- .where('revokedAt', 'is', null)
137
- .limit(1)
138
- .executeTakeFirst()
139
- return row !== undefined
140
- }
141
-
142
120
  /**
143
121
  * Revokes a pending invitation.
144
122
  *
@@ -2,7 +2,10 @@ import { nanoid } from 'nanoid'
2
2
 
3
3
  import * as Runtime from '../../../test/runtime.js'
4
4
  import * as Webhooks from '../../internal/Webhooks.js'
5
+ import * as Viem from '../../internal/Viem.js'
5
6
  import * as Db from '../Db.js'
7
+ import * as EnabledBillingSources from './enabledBillingSources.js'
8
+ import * as Memberships from './memberships.js'
6
9
  import * as Organizations from './organizations.js'
7
10
  import * as SponsoredTransactions from './sponsoredTransactions.js'
8
11
  import * as StripeCustomers from './stripeCustomers.js'
@@ -60,6 +63,66 @@ describe('create', () => {
60
63
  })
61
64
  })
62
65
 
66
+ describe('createOwned', () => {
67
+ test('behavior: creates the organization, owner, and enabled sources atomically', async () => {
68
+ const db = create()
69
+ await db.migrate()
70
+ const record = await Organizations.createOwned(db, {
71
+ enabledBillingSources: ['stripe'],
72
+ name: 'Acme, Inc.',
73
+ userId: 'usr_owner',
74
+ })
75
+
76
+ expect(
77
+ (await Memberships.listByOrg(db, record.id)).map(({ role, userId }) => ({ role, userId })),
78
+ ).toMatchInlineSnapshot(`
79
+ [
80
+ {
81
+ "role": "owner",
82
+ "userId": "usr_owner",
83
+ },
84
+ ]
85
+ `)
86
+ expect(
87
+ (await EnabledBillingSources.listByOrg(db, record.id)).map(({ createdBy, source }) => ({
88
+ createdBy,
89
+ source,
90
+ })),
91
+ ).toMatchInlineSnapshot(`
92
+ [
93
+ {
94
+ "createdBy": "usr_owner",
95
+ "source": "stripe",
96
+ },
97
+ ]
98
+ `)
99
+ await db.close()
100
+ })
101
+
102
+ test('behavior: rolls back all rows when initial source insertion fails', async () => {
103
+ const db = create()
104
+ await db.migrate()
105
+
106
+ await expect(
107
+ Organizations.createOwned(db, {
108
+ enabledBillingSources: ['stripe', 'stripe'],
109
+ name: 'Acme, Inc.',
110
+ userId: 'usr_owner',
111
+ }),
112
+ ).rejects.toThrow()
113
+ expect(await Organizations.listByUser(db, 'usr_owner')).toEqual([])
114
+ expect(await db.kysely.selectFrom('enabled_billing_sources').selectAll().execute()).toEqual([])
115
+ expect(
116
+ await db.kysely
117
+ .selectFrom('memberships')
118
+ .selectAll()
119
+ .where('userId', '=', 'usr_owner')
120
+ .execute(),
121
+ ).toEqual([])
122
+ await db.close()
123
+ })
124
+ })
125
+
63
126
  describe('get', () => {
64
127
  test('behavior: returns undefined when absent', async () => {
65
128
  const db = create()
@@ -137,6 +200,11 @@ describe('deleteOrganization', () => {
137
200
  orgId: 'org_1',
138
201
  stripeCustomerId: 'cus_org_1',
139
202
  })
203
+ await EnabledBillingSources.add(db, {
204
+ createdBy: 'admin@tempo.xyz',
205
+ orgId: 'org_1',
206
+ source: 'stripe',
207
+ })
140
208
  await SponsoredTransactions.upsert(db, {
141
209
  apiKeyId: 'key_1',
142
210
  billable: true,
@@ -149,14 +217,137 @@ describe('deleteOrganization', () => {
149
217
  transactionHash: `0x${'aa'.repeat(32)}`,
150
218
  })
151
219
 
220
+ expect(await Organizations.hasUnreportedSponsorships(db, 'org_1')).toBe(true)
152
221
  await expect(Organizations.deleteOrganization(db, 'org_1')).rejects.toThrow(
153
222
  Organizations.UnreportedSponsorshipsError,
154
223
  )
155
224
  expect(await Organizations.get(db, 'org_1')).toBeDefined()
225
+ expect(await EnabledBillingSources.isEnabled(db, { orgId: 'org_1', source: 'stripe' })).toBe(
226
+ true,
227
+ )
156
228
  expect(await StripeCustomers.get(db, 'org_1')).toBeDefined()
157
229
  await db.close()
158
230
  })
159
231
 
232
+ test('behavior: deletes an org with only failed or non-mainnet sponsorships', async () => {
233
+ const db = create()
234
+ await db.migrate()
235
+ await Organizations.create(db, { id: 'org_1', name: 'One' })
236
+ const failed = await SponsoredTransactions.upsert(db, {
237
+ apiKeyId: 'key_1',
238
+ billable: true,
239
+ chainId: Viem.chainId.mainnet,
240
+ environment: 'production',
241
+ orgId: 'org_1',
242
+ projectId: 'prj_1',
243
+ signPayload: `0x${'bb'.repeat(32)}`,
244
+ transaction: `0x76${'cc'.repeat(16)}`,
245
+ transactionHash: `0x${'aa'.repeat(32)}`,
246
+ })
247
+ await SponsoredTransactions.fail(db, failed.id, '2026-01-01T00:00:00.000Z')
248
+ const testnet = await SponsoredTransactions.upsert(db, {
249
+ apiKeyId: 'key_1',
250
+ billable: true,
251
+ chainId: Viem.chainId.testnet,
252
+ environment: 'production',
253
+ orgId: 'org_1',
254
+ projectId: 'prj_1',
255
+ signPayload: `0x${'dd'.repeat(32)}`,
256
+ transaction: `0x76${'ee'.repeat(16)}`,
257
+ transactionHash: `0x${'ff'.repeat(32)}`,
258
+ })
259
+ await SponsoredTransactions.finalize(db, testnet.id, {
260
+ feeAmount: '100',
261
+ finalizedAt: '2026-01-01T00:00:00.000Z',
262
+ })
263
+
264
+ expect(await Organizations.hasUnreportedSponsorships(db, 'org_1')).toBe(false)
265
+ expect(await Organizations.deleteOrganization(db, 'org_1')).toBe(true)
266
+ await db.close()
267
+ })
268
+
269
+ test('behavior: preserves an org with a recoverable failed fill intent', async () => {
270
+ const db = create()
271
+ await db.migrate()
272
+ await Organizations.create(db, { id: 'org_1', name: 'One' })
273
+ const failed = await SponsoredTransactions.upsert(db, {
274
+ apiKeyId: 'key_1',
275
+ billable: true,
276
+ chainId: Viem.chainId.mainnet,
277
+ environment: 'production',
278
+ orgId: 'org_1',
279
+ projectId: 'prj_1',
280
+ signPayload: `0x${'bb'.repeat(32)}`,
281
+ transaction: `0x76${'cc'.repeat(16)}`,
282
+ })
283
+ await SponsoredTransactions.fail(db, failed.id, new Date().toISOString())
284
+
285
+ expect(await Organizations.hasUnreportedSponsorships(db, 'org_1')).toBe(true)
286
+ await expect(Organizations.deleteOrganization(db, 'org_1')).rejects.toThrow(
287
+ Organizations.UnreportedSponsorshipsError,
288
+ )
289
+ expect(await Organizations.get(db, 'org_1')).toBeDefined()
290
+ await db.close()
291
+ })
292
+
293
+ test('behavior: deletes an org with only expired failed fill intents', async () => {
294
+ const db = create()
295
+ await db.migrate()
296
+ await Organizations.create(db, { id: 'org_1', name: 'One' })
297
+ const failed = await SponsoredTransactions.upsert(db, {
298
+ apiKeyId: 'key_1',
299
+ billable: true,
300
+ chainId: Viem.chainId.mainnet,
301
+ environment: 'production',
302
+ orgId: 'org_1',
303
+ projectId: 'prj_1',
304
+ signPayload: `0x${'bb'.repeat(32)}`,
305
+ transaction: `0x76${'cc'.repeat(16)}`,
306
+ })
307
+ await SponsoredTransactions.fail(
308
+ db,
309
+ failed.id,
310
+ new Date(Date.now() - SponsoredTransactions.failedIntentRecoveryTtlMs - 1).toISOString(),
311
+ )
312
+
313
+ expect(await Organizations.hasUnreportedSponsorships(db, 'org_1')).toBe(false)
314
+ expect(await Organizations.deleteOrganization(db, 'org_1')).toBe(true)
315
+ await db.close()
316
+ })
317
+
318
+ test('behavior: deletes enabled billing sources', async () => {
319
+ const db = create()
320
+ await db.migrate()
321
+ await Organizations.create(db, { id: 'org_1', name: 'One' })
322
+ await Organizations.create(db, { id: 'org_2', name: 'Two' })
323
+ await EnabledBillingSources.add(db, {
324
+ createdBy: 'admin@tempo.xyz',
325
+ orgId: 'org_1',
326
+ source: 'stripe',
327
+ })
328
+ await EnabledBillingSources.add(db, {
329
+ createdBy: 'admin@tempo.xyz',
330
+ orgId: 'org_1',
331
+ source: 'tempo',
332
+ })
333
+ await EnabledBillingSources.add(db, {
334
+ createdBy: 'admin@tempo.xyz',
335
+ orgId: 'org_2',
336
+ source: 'stripe',
337
+ })
338
+
339
+ expect(await Organizations.deleteOrganization(db, 'org_1')).toBe(true)
340
+
341
+ expect(await EnabledBillingSources.listByOrg(db, 'org_1')).toEqual([])
342
+ expect((await EnabledBillingSources.listByOrg(db, 'org_2')).map((record) => record.source))
343
+ .toMatchInlineSnapshot(`
344
+ [
345
+ "stripe",
346
+ ]
347
+ `)
348
+ await db.close()
349
+ })
350
+
160
351
  test('behavior: deletes api-key-owned webhook subscriptions', async () => {
161
352
  const db = create()
162
353
  await db.migrate()