tapimo 0.6.0 → 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 (250) 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 +95 -87
  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 +5016 -4985
  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 +113 -113
  48. package/dist/apps/data/routes/fee-amm.d.ts +105 -91
  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 +5 -5
  57. package/dist/apps/data/routes/receipts.d.ts +6 -6
  58. package/dist/apps/data/routes/rpc.d.ts +31 -31
  59. package/dist/apps/data/routes/tokens.d.ts +2 -2
  60. package/dist/apps/data/routes/tokens.js +1 -1
  61. package/dist/apps/data/routes/transactions.d.ts +4 -4
  62. package/dist/apps/data/routes/transfers.d.ts +99 -88
  63. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  64. package/dist/apps/data/routes/transfers.js +226 -141
  65. package/dist/apps/data/routes/transfers.js.map +1 -1
  66. package/dist/apps/data/routes/valuation.d.ts +34 -34
  67. package/dist/apps/data/routes/valuation.js +3 -3
  68. package/dist/apps/data/routes/valuation.js.map +1 -1
  69. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  70. package/dist/apps/data/routes/webhooks.d.ts +366 -366
  71. package/dist/apps/index.d.ts +1 -0
  72. package/dist/apps/index.d.ts.map +1 -1
  73. package/dist/apps/index.js +1 -0
  74. package/dist/apps/index.js.map +1 -1
  75. package/dist/apps/management/App.d.ts +460 -523
  76. package/dist/apps/management/App.d.ts.map +1 -1
  77. package/dist/apps/management/routes/api-keys.d.ts +7 -23
  78. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  79. package/dist/apps/management/routes/api-keys.js +20 -8
  80. package/dist/apps/management/routes/api-keys.js.map +1 -1
  81. package/dist/apps/management/routes/billing.d.ts +106 -151
  82. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  83. package/dist/apps/management/routes/billing.js +28 -28
  84. package/dist/apps/management/routes/billing.js.map +1 -1
  85. package/dist/apps/management/routes/faucet.d.ts +40 -26
  86. package/dist/apps/management/routes/faucet.d.ts.map +1 -1
  87. package/dist/apps/management/routes/faucet.js +33 -18
  88. package/dist/apps/management/routes/faucet.js.map +1 -1
  89. package/dist/apps/management/routes/invitations.d.ts +131 -131
  90. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  91. package/dist/apps/management/routes/invitations.js +0 -3
  92. package/dist/apps/management/routes/invitations.js.map +1 -1
  93. package/dist/apps/management/routes/me.d.ts +6 -8
  94. package/dist/apps/management/routes/me.d.ts.map +1 -1
  95. package/dist/apps/management/routes/me.js +0 -4
  96. package/dist/apps/management/routes/me.js.map +1 -1
  97. package/dist/apps/management/routes/members.d.ts +45 -45
  98. package/dist/apps/management/routes/orgs.d.ts +94 -94
  99. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  100. package/dist/apps/management/routes/orgs.js +5 -0
  101. package/dist/apps/management/routes/orgs.js.map +1 -1
  102. package/dist/apps/management/routes/usage.d.ts +32 -32
  103. package/dist/apps/management/routes/verified-token-requests.d.ts +94 -94
  104. package/dist/apps/management/routes/webhooks.d.ts +1 -1
  105. package/dist/apps/mpp/App.d.ts +363 -0
  106. package/dist/apps/mpp/App.d.ts.map +1 -0
  107. package/dist/apps/mpp/App.js +72 -0
  108. package/dist/apps/mpp/App.js.map +1 -0
  109. package/dist/apps/mpp/Mpp.d.ts +178 -0
  110. package/dist/apps/mpp/Mpp.d.ts.map +1 -0
  111. package/dist/apps/mpp/Mpp.js +183 -0
  112. package/dist/apps/mpp/Mpp.js.map +1 -0
  113. package/dist/apps/mpp/index.d.ts +3 -0
  114. package/dist/apps/mpp/index.d.ts.map +1 -0
  115. package/dist/apps/mpp/index.js +3 -0
  116. package/dist/apps/mpp/index.js.map +1 -0
  117. package/dist/apps/relay/App.d.ts.map +1 -1
  118. package/dist/apps/relay/App.js +56 -9
  119. package/dist/apps/relay/App.js.map +1 -1
  120. package/dist/db/Db.d.ts +2 -0
  121. package/dist/db/Db.d.ts.map +1 -1
  122. package/dist/db/Db.js +27 -0
  123. package/dist/db/Db.js.map +1 -1
  124. package/dist/db/Schema.d.ts +12 -0
  125. package/dist/db/Schema.d.ts.map +1 -1
  126. package/dist/db/Schema.js +11 -0
  127. package/dist/db/Schema.js.map +1 -1
  128. package/dist/db/tables/enabledBillingSources.d.ts +73 -0
  129. package/dist/db/tables/enabledBillingSources.d.ts.map +1 -0
  130. package/dist/db/tables/enabledBillingSources.js +69 -0
  131. package/dist/db/tables/enabledBillingSources.js.map +1 -0
  132. package/dist/db/tables/invitations.d.ts +1 -12
  133. package/dist/db/tables/invitations.d.ts.map +1 -1
  134. package/dist/db/tables/invitations.js +3 -23
  135. package/dist/db/tables/invitations.js.map +1 -1
  136. package/dist/db/tables/organizations.d.ts +5 -2
  137. package/dist/db/tables/organizations.d.ts.map +1 -1
  138. package/dist/db/tables/organizations.js +13 -2
  139. package/dist/db/tables/organizations.js.map +1 -1
  140. package/dist/handlers/internal/utils.d.ts +1 -16
  141. package/dist/handlers/internal/utils.d.ts.map +1 -1
  142. package/dist/handlers/internal/utils.js +17 -6
  143. package/dist/handlers/internal/utils.js.map +1 -1
  144. package/dist/handlers/relay.d.ts.map +1 -1
  145. package/dist/handlers/relay.js +37 -5
  146. package/dist/handlers/relay.js.map +1 -1
  147. package/dist/internal/Auth.d.ts +0 -32
  148. package/dist/internal/Auth.d.ts.map +1 -1
  149. package/dist/internal/Auth.js +3 -51
  150. package/dist/internal/Auth.js.map +1 -1
  151. package/dist/internal/FundingQuotes.d.ts +78 -32
  152. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  153. package/dist/internal/FundingQuotes.js +45 -8
  154. package/dist/internal/FundingQuotes.js.map +1 -1
  155. package/dist/internal/Log.d.ts +18 -4
  156. package/dist/internal/Log.d.ts.map +1 -1
  157. package/dist/internal/Log.js +50 -3
  158. package/dist/internal/Log.js.map +1 -1
  159. package/dist/internal/MetricSink.d.ts.map +1 -1
  160. package/dist/internal/MetricSink.js +15 -0
  161. package/dist/internal/MetricSink.js.map +1 -1
  162. package/dist/internal/Response.d.ts +2 -2
  163. package/dist/internal/Response.d.ts.map +1 -1
  164. package/dist/internal/Response.js +12 -4
  165. package/dist/internal/Response.js.map +1 -1
  166. package/dist/internal/Viem.d.ts +1 -1
  167. package/package.json +7 -2
  168. package/src/App.test.ts +9 -4
  169. package/src/App.ts +14 -2
  170. package/src/Client.test-d.ts +109 -28
  171. package/src/Scope.ts +5 -0
  172. package/src/admin/App.test.ts +4 -0
  173. package/src/admin/apps/early-access.ts +6 -5
  174. package/src/admin/apps/organizations.test.ts +86 -2
  175. package/src/admin/apps/organizations.ts +155 -31
  176. package/src/admin/apps/scopes.test.ts +5 -0
  177. package/src/admin/ui/src/lib/organizations.test.ts +81 -1
  178. package/src/admin/ui/src/lib/organizations.ts +92 -0
  179. package/src/admin/ui/src/routes/api-keys.tsx +1 -1
  180. package/src/admin/ui/src/routes/early-access.tsx +8 -3
  181. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +103 -0
  182. package/src/admin/ui.generated.ts +95 -109
  183. package/src/analytics/Analytics.test.ts +83 -0
  184. package/src/analytics/Analytics.ts +20 -0
  185. package/src/analytics/tables/requestEvents.ts +60 -7
  186. package/src/apps/composition.test.ts +11 -2
  187. package/src/apps/data/App.test.ts +1 -1
  188. package/src/apps/data/FundingQuoteProviders.test.ts +0 -5
  189. package/src/apps/data/FundingQuoteProviders.ts +2 -29
  190. package/src/apps/data/FxOracle.test.ts +2 -2
  191. package/src/apps/data/FxOracle.ts +2 -2
  192. package/src/apps/data/routes/activities.test.ts +6 -2
  193. package/src/apps/data/routes/activities.ts +39 -1
  194. package/src/apps/data/routes/balances.test.ts +6 -2
  195. package/src/apps/data/routes/exchanges.test.ts +7 -3
  196. package/src/apps/data/routes/fee-amm.test.ts +47 -23
  197. package/src/apps/data/routes/fee-amm.ts +98 -119
  198. package/src/apps/data/routes/funding.test.ts +29 -8
  199. package/src/apps/data/routes/funding.ts +42 -48
  200. package/src/apps/data/routes/receipts.test.ts +6 -2
  201. package/src/apps/data/routes/tokens.ts +1 -1
  202. package/src/apps/data/routes/transfers.test.ts +203 -35
  203. package/src/apps/data/routes/transfers.ts +294 -172
  204. package/src/apps/data/routes/valuation.test.ts +18 -3
  205. package/src/apps/data/routes/valuation.ts +4 -4
  206. package/src/apps/index.ts +1 -0
  207. package/src/apps/management/routes/api-keys.test.ts +2 -75
  208. package/src/apps/management/routes/api-keys.ts +30 -7
  209. package/src/apps/management/routes/billing.test.ts +106 -85
  210. package/src/apps/management/routes/billing.ts +32 -21
  211. package/src/apps/management/routes/faucet.test.ts +11 -3
  212. package/src/apps/management/routes/faucet.ts +46 -30
  213. package/src/apps/management/routes/invitations.test.ts +0 -172
  214. package/src/apps/management/routes/invitations.ts +0 -3
  215. package/src/apps/management/routes/me.test.ts +3 -145
  216. package/src/apps/management/routes/me.ts +0 -6
  217. package/src/apps/management/routes/orgs.test.ts +45 -2
  218. package/src/apps/management/routes/orgs.ts +6 -0
  219. package/src/apps/management/routes/scopes.test.ts +1 -0
  220. package/src/apps/mpp/App.ts +86 -0
  221. package/src/apps/mpp/Mpp.test.ts +193 -0
  222. package/src/apps/mpp/Mpp.ts +353 -0
  223. package/src/apps/mpp/index.ts +2 -0
  224. package/src/apps/mpp/internal/Adapter.ts +11 -0
  225. package/src/apps/mpp/internal/Error.ts +9 -0
  226. package/src/apps/mpp/internal/Idempotency.ts +7 -0
  227. package/src/apps/mpp/internal/Screening.ts +10 -0
  228. package/src/apps/relay/App.test.ts +88 -6
  229. package/src/apps/relay/App.ts +61 -9
  230. package/src/db/Db.test.ts +2 -2
  231. package/src/db/Db.ts +30 -0
  232. package/src/db/Schema.ts +15 -0
  233. package/src/db/tables/enabledBillingSources.test.ts +303 -0
  234. package/src/db/tables/enabledBillingSources.ts +118 -0
  235. package/src/db/tables/invitations.ts +4 -26
  236. package/src/db/tables/organizations.test.ts +103 -0
  237. package/src/db/tables/organizations.ts +18 -2
  238. package/src/handlers/internal/utils.ts +22 -6
  239. package/src/handlers/relay.errors.test.ts +153 -0
  240. package/src/handlers/relay.ts +42 -7
  241. package/src/internal/Auth.test.ts +3 -3
  242. package/src/internal/Auth.ts +3 -63
  243. package/src/internal/FundingQuotes.test.ts +18 -5
  244. package/src/internal/FundingQuotes.ts +46 -11
  245. package/src/internal/Log.test.ts +91 -0
  246. package/src/internal/Log.ts +69 -5
  247. package/src/internal/MetricSink.test.ts +55 -0
  248. package/src/internal/MetricSink.ts +14 -0
  249. package/src/internal/Response.test.ts +28 -0
  250. package/src/internal/Response.ts +12 -4
@@ -51,7 +51,11 @@ const seed: readonly SeedToken[] = JSON.parse(
51
51
  // Deterministic EUR-based rate set so conversion math is exact in assertions.
52
52
  const fixed = FxOracle.from({
53
53
  name: 'fixed',
54
- rates: async () => ({ asOf: '2026-01-01', base: 'EUR', rates: { AUD: '1.6', USD: '1.0' } }),
54
+ rates: async () => ({
55
+ asOf: '2026-01-01T00:00:00.000Z',
56
+ base: 'EUR',
57
+ rates: { AUD: '1.6', USD: '1.0' },
58
+ }),
55
59
  })
56
60
 
57
61
  async function holderWithBalance(client: ReturnType<typeof app>) {
@@ -93,6 +97,17 @@ async function chainSum(holder: `0x${string}`, currency: string) {
93
97
  }
94
98
 
95
99
  describe('GET /addresses/:address/valuation', () => {
100
+ test('requires a full pricing timestamp', () => {
101
+ const pricing = { asOf: '2026-01-01', basis: 'nominal', source: 'fixed' }
102
+ expect(Valuation.schema.Pricing.safeParse(pricing).success).toBe(false)
103
+ expect(
104
+ Valuation.schema.Pricing.safeParse({
105
+ ...pricing,
106
+ asOf: '2026-01-01T00:00:00.000Z',
107
+ }).success,
108
+ ).toBe(true)
109
+ })
110
+
96
111
  test('normalizes the denomination', () => {
97
112
  expect(
98
113
  Valuation.schema.getAddressValuation.Query.parse({ denomination: 'aud' }).denomination,
@@ -176,7 +191,7 @@ describe('GET /addresses/:address/valuation', () => {
176
191
  expect(aud.amount).toBe(core_Value.format((sum * 16n) / 10n, 6))
177
192
  expect(aud.pricing).toMatchInlineSnapshot(`
178
193
  {
179
- "asOf": "2026-01-01",
194
+ "asOf": "2026-01-01T00:00:00.000Z",
180
195
  "basis": "nominal",
181
196
  "source": "fixed",
182
197
  }
@@ -201,7 +216,7 @@ describe('GET /addresses/:address/valuation', () => {
201
216
  expect(Number(body.amount)).toBeGreaterThan(0)
202
217
  expect(body.pricing?.basis).toBe('nominal')
203
218
  expect(body.pricing?.source).toBe('ecb')
204
- expect(body.pricing?.asOf).toMatch(/^\d{4}-\d{2}-\d{2}$/)
219
+ expect(body.pricing?.asOf).toBe('2026-01-01T00:00:00.000Z')
205
220
  })
206
221
 
207
222
  test.skipIf(runtime.mode !== 'localnet')(
@@ -44,11 +44,11 @@ export namespace schema {
44
44
  /** Rate provenance for a converted valuation. */
45
45
  export const Pricing = z
46
46
  .object({
47
- asOf: z
48
- .string()
47
+ asOf: z.iso
48
+ .datetime()
49
49
  .check(
50
- z.describe('Publication date of the rate set used for conversion.'),
51
- z.meta({ examples: ['2026-07-20'] }),
50
+ z.describe('Publication date of the rate set as an ISO 8601 timestamp.'),
51
+ z.meta({ examples: ['2026-07-20T00:00:00.000Z'] }),
52
52
  ),
53
53
  basis: z
54
54
  .literal('nominal')
package/src/apps/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { data, FundingQuoteProviders, FxOracle } from './data/index.js'
2
2
  export { management } from './management/index.js'
3
3
  export { mcp } from './mcp.js'
4
+ export { mpp } from './mpp/index.js'
4
5
  export { relay } from './relay/index.js'
@@ -5,9 +5,7 @@ import * as Auth from '../../../internal/Auth.js'
5
5
  import * as OpenApi from '../../../internal/OpenApi.js'
6
6
  import * as Scope from '../../../Scope.js'
7
7
  import * as TestApp from '../../../../test/App.js'
8
- import * as EarlyAccess from '../../../db/tables/earlyAccess.js'
9
8
  import * as Memberships from '../../../db/tables/memberships.js'
10
- import * as Users from '../../../db/tables/users.js'
11
9
  import * as Keys from './api-keys.js'
12
10
  import * as Me from './me.js'
13
11
  import * as Orgs from './orgs.js'
@@ -45,7 +43,6 @@ function createApp(
45
43
  db?: ReturnType<typeof TestApp.database>
46
44
  kv?: { store: State }
47
45
  scopes?: readonly Scope.Entry[]
48
- tmp_earlyAccess?: boolean
49
46
  } = {},
50
47
  ) {
51
48
  return TestApp.create({
@@ -54,7 +51,6 @@ function createApp(
54
51
  ...(options.kv ? { kv: options.kv } : {}),
55
52
  ...(options.scopes ? { scopes: options.scopes } : {}),
56
53
  session: { wallet: { origin } },
57
- ...(options.tmp_earlyAccess === undefined ? {} : { tmp_earlyAccess: options.tmp_earlyAccess }),
58
54
  })
59
55
  }
60
56
 
@@ -677,66 +673,9 @@ describe('keys', () => {
677
673
  })
678
674
 
679
675
  describe('early access', () => {
680
- test('rejects mints from non-early-access sessions', async () => {
681
- const app = createApp({ tmp_earlyAccess: true })
682
- const cookie = await session(app)
683
- const { org, project } = await fixture(app, cookie)
684
-
685
- const denied = await app.request(
686
- `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
687
- json('POST', cookie, { scopes: ['data:read'] }),
688
- )
689
- expect(denied.status).toBe(403)
690
- const { requestId, ...body } = (await denied.json()) as { requestId?: string }
691
- expect(requestId).toBeDefined()
692
- expect(body).toMatchInlineSnapshot(`
693
- {
694
- "error": {
695
- "code": "early_access_required",
696
- "message": "Requires early access. Ask a teammate with access to invite you by email, or contact Tempo.",
697
- },
698
- }
699
- `)
700
- })
701
-
702
- test('the gate precedes membership resolution', async () => {
703
- // The gate depends only on the caller, so it leaks nothing about the
704
- // project; early-access strangers still get the membership 404.
705
- const db = TestApp.database()
706
- const app = createApp({ db, tmp_earlyAccess: true })
707
- await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
708
- const owner = await session(app)
709
- const { org, project } = await fixture(app, owner)
710
-
711
- const other = await session(app)
712
- const gated = await app.request(
713
- `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
714
- json('POST', other, { scopes: ['data:read'] }),
715
- )
716
- expect(gated.status).toBe(403)
717
- expect(((await gated.json()) as { error: { code: string } }).error.code).toBe('early_access_required') // prettier-ignore
718
-
719
- const granted = await session(app)
720
- const me = await app.request('/v1/me', { headers: { cookie: granted } })
721
- const { id: userId } = (await me.json()) as { id: string }
722
- await Users.setEmail(db, userId, 'stranger@example.org')
723
- const denied = await app.request(
724
- `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
725
- json('POST', granted, { scopes: ['data:read'] }),
726
- )
727
- expect(denied.status).toBe(404)
728
- })
729
-
730
- test('lets early-access sessions mint', async () => {
731
- const db = TestApp.database()
732
- const app = createApp({ db, tmp_earlyAccess: true })
733
- await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
676
+ test('does not require an early-access grant', async () => {
677
+ const app = createApp()
734
678
  const cookie = await session(app)
735
- const user = await TestApp.json(
736
- await app.request('/v1/me', { headers: { cookie } }),
737
- Me.schema.User,
738
- )
739
- await Users.setEmail(db, user.id, 'dev@example.org')
740
679
  const { org, project } = await fixture(app, cookie)
741
680
 
742
681
  const minted = await app.request(
@@ -745,16 +684,4 @@ describe('early access', () => {
745
684
  )
746
685
  expect(minted.status).toBe(200)
747
686
  })
748
-
749
- test('lets the super admin mint', async () => {
750
- const app = createApp({ tmp_earlyAccess: true })
751
- const cookie = await session(app)
752
- const { org, project } = await fixture(app, cookie)
753
-
754
- const minted = await app.request(
755
- `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
756
- superAdminJson('POST', { scopes: ['data:read'] }),
757
- )
758
- expect(minted.status).toBe(200)
759
- })
760
687
  })
@@ -1,6 +1,8 @@
1
1
  import { type Context, Hono } from 'hono'
2
2
  import * as z from 'zod/mini'
3
3
 
4
+ import * as Analytics from '../../../analytics/Analytics.js'
5
+ import * as RequestEvents from '../../../analytics/tables/requestEvents.js'
4
6
  import * as ApiKeys from '../../../ApiKeys.js'
5
7
  import type * as App from '../../../App.js'
6
8
  import * as Db from '../../../db/Db.js'
@@ -43,6 +45,12 @@ export namespace schema {
43
45
  z.describe('Opaque key id (`key_…`).'),
44
46
  z.meta({ examples: ['key_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
45
47
  ),
48
+ lastUsedAt: z
49
+ .optional(z.string())
50
+ .check(
51
+ z.describe('When the key last authenticated a request (ISO 8601).'),
52
+ z.meta({ examples: ['2026-07-21T12:34:56.000Z'] }),
53
+ ),
46
54
  name: z
47
55
  .optional(z.string())
48
56
  .check(z.describe('Human-readable key name.'), z.meta({ examples: ['CI'] })),
@@ -225,7 +233,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
225
233
  return new Hono<App.Environment>()
226
234
  .post(
227
235
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys',
228
- Auth.policy({ session: true, tmp_earlyAccess: true }),
236
+ Auth.policy({ session: true }),
229
237
  Auth.ensureOrg({ role: 'admin' }),
230
238
  Auth.ensureProject(),
231
239
  OpenApi.validate('param', schema.Params, {
@@ -243,8 +251,8 @@ export function apiKeys(options: apiKeys.Options = {}) {
243
251
  responses: OpenApi.responses({
244
252
  errors: {
245
253
  403: {
246
- codes: ['scope_not_issuable', 'early_access_required'],
247
- description: 'The requested scopes are not issuable, or the caller lacks early access.', // prettier-ignore
254
+ codes: ['scope_not_issuable'],
255
+ description: 'The requested scopes are not issuable.',
248
256
  },
249
257
  404: 'No accessible project was found for this id.',
250
258
  },
@@ -259,7 +267,6 @@ export function apiKeys(options: apiKeys.Options = {}) {
259
267
  async (c) => {
260
268
  if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
261
269
  if (Auth.narrowScope) return Auth.ensureProjectError(c)
262
- if (Auth.tmp_narrowEarlyAccess) return Auth.tmp_ensureEarlyAccessError(c)
263
270
  if (OpenApi.narrowValidation)
264
271
  return OpenApi.validationError(c, {
265
272
  code: 'param_invalid',
@@ -375,7 +382,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
375
382
  })
376
383
  return c.json(
377
384
  Response.validated(schema.listApiKeys.Response, {
378
- data: records.map(serializeKey),
385
+ data: await serializeKeys(c, records),
379
386
  }),
380
387
  200,
381
388
  )
@@ -436,7 +443,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
436
443
  })
437
444
  return c.json(
438
445
  Response.validated(schema.listOrgApiKeys.Response, {
439
- data: records.map(serializeKey),
446
+ data: await serializeKeys(c, records),
440
447
  }),
441
448
  200,
442
449
  )
@@ -506,14 +513,30 @@ export declare namespace apiKeys {
506
513
  }
507
514
  }
508
515
 
516
+ /** Enriches key records with their latest authenticated request timestamp. */
517
+ async function serializeKeys(c: Context<App.Environment>, records: readonly ApiKeys.Record[]) {
518
+ const source = c.get('analytics')
519
+ if (!source || records.length === 0) return records.map((record) => serializeKey(record))
520
+ const lastUsedAt = new Map(
521
+ (
522
+ await RequestEvents.readLastUsedAt(
523
+ Analytics.get(source),
524
+ records.map(({ id }) => id),
525
+ )
526
+ ).map((usage) => [usage.apiKeyId, usage.lastUsedAt]),
527
+ )
528
+ return records.map((record) => serializeKey(record, lastUsedAt.get(record.id)))
529
+ }
530
+
509
531
  /** Projects a persisted record onto the management key shape (no rate limits, no token). */
510
- function serializeKey(record: ApiKeys.Record) {
532
+ function serializeKey(record: ApiKeys.Record, lastUsedAt?: string) {
511
533
  return {
512
534
  createdAt: record.createdAt,
513
535
  ...(record.createdBy === undefined ? {} : { createdBy: record.createdBy }),
514
536
  environment: record.environment,
515
537
  ...(record.expiresAt === undefined ? {} : { expiresAt: record.expiresAt }),
516
538
  id: record.id,
539
+ ...(lastUsedAt === undefined ? {} : { lastUsedAt }),
517
540
  ...(record.name === undefined ? {} : { name: record.name }),
518
541
  orgId: record.orgId,
519
542
  ...(record.projectId === undefined ? {} : { projectId: record.projectId }),
@@ -5,11 +5,10 @@ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
5
5
  import * as ApiKeys from '../../../ApiKeys.js'
6
6
  import * as TestApp from '../../../../test/App.js'
7
7
  import * as TestStripe from '../../../../test/Stripe.js'
8
- import * as EarlyAccess from '../../../db/tables/earlyAccess.js'
8
+ import * as EnabledBillingSources from '../../../db/tables/enabledBillingSources.js'
9
9
  import * as Memberships from '../../../db/tables/memberships.js'
10
10
  import * as SponsoredTransactions from '../../../db/tables/sponsoredTransactions.js'
11
11
  import * as StripeCustomers from '../../../db/tables/stripeCustomers.js'
12
- import * as Users from '../../../db/tables/users.js'
13
12
  import * as Viem from '../../../internal/Viem.js'
14
13
  import * as core_Billing from '../Billing.js'
15
14
  import type * as App from '../../../App.js'
@@ -66,7 +65,6 @@ function createApp(
66
65
  billing?: App.Billing | undefined
67
66
  db?: TestApp.create.Options['db']
68
67
  kv?: { store: ReturnType<typeof TestApp.kvStore> }
69
- tmp_earlyAccess?: boolean
70
68
  } = {},
71
69
  ) {
72
70
  // prettier-ignore
@@ -76,7 +74,6 @@ function createApp(
76
74
  db: options.db ?? TestApp.database(),
77
75
  ...(options.kv ? { kv: options.kv } : {}),
78
76
  session: { wallet: { origin } },
79
- ...(options.tmp_earlyAccess === undefined ? {} : { tmp_earlyAccess: options.tmp_earlyAccess }),
80
77
  })
81
78
  }
82
79
 
@@ -108,6 +105,20 @@ async function seeded(app: ReturnType<typeof createApp>) {
108
105
  return { cookie, orgId: id }
109
106
  }
110
107
 
108
+ /** Seeds an owned organization with Stripe setup enabled. */
109
+ async function seededWithStripe(
110
+ app: ReturnType<typeof createApp>,
111
+ db: ReturnType<typeof TestApp.database>,
112
+ ) {
113
+ const result = await seeded(app)
114
+ await EnabledBillingSources.add(db, {
115
+ createdBy: 'test',
116
+ orgId: result.orgId,
117
+ source: 'stripe',
118
+ })
119
+ return result
120
+ }
121
+
111
122
  describe('GET /orgs/:orgId/billing', () => {
112
123
  test('behavior: synthesizes none for orgs without a billing source', async () => {
113
124
  const app = createApp()
@@ -116,6 +127,7 @@ describe('GET /orgs/:orgId/billing', () => {
116
127
  expect(response.status).toBe(200)
117
128
  expect(await response.json()).toMatchInlineSnapshot(`
118
129
  {
130
+ "enabledSources": [],
119
131
  "spend": {
120
132
  "amount": "0",
121
133
  "currency": "usd",
@@ -139,6 +151,28 @@ describe('GET /orgs/:orgId/billing', () => {
139
151
  expect(body.updatedAt).toBeDefined()
140
152
  })
141
153
 
154
+ test('behavior: lists organization-wide sources in canonical order', async () => {
155
+ const db = TestApp.database()
156
+ const app = createApp({ billing: billingWithSandbox(), db })
157
+ const { cookie, orgId } = await seeded(app)
158
+ await EnabledBillingSources.add(db, { createdBy: 'test', orgId, source: 'tempo' })
159
+ await EnabledBillingSources.add(db, { createdBy: 'test', orgId, source: 'stripe' })
160
+
161
+ const production = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } })
162
+ const sandbox = await app.request(`/v1/orgs/${orgId}/billing?environment=sandbox`, {
163
+ headers: { cookie },
164
+ })
165
+
166
+ expect(await production.json()).toMatchObject({
167
+ enabledSources: ['stripe', 'tempo'],
168
+ status: 'none',
169
+ })
170
+ expect(await sandbox.json()).toMatchObject({
171
+ enabledSources: ['stripe', 'tempo'],
172
+ status: 'none',
173
+ })
174
+ })
175
+
142
176
  test('behavior: the super admin reads any org', async () => {
143
177
  const db = TestApp.database()
144
178
  const app = createApp({ db })
@@ -165,7 +199,7 @@ describe('GET /orgs/:orgId/billing', () => {
165
199
  expect(response.status).toBe(401)
166
200
  })
167
201
 
168
- test('behavior: 501 when billing is unconfigured', async () => {
202
+ test('behavior: reads database-backed state when Stripe is unconfigured', async () => {
169
203
  const app = TestApp.create({
170
204
  auth: { superAdmin: { secret } },
171
205
  db: TestApp.database(),
@@ -173,9 +207,18 @@ describe('GET /orgs/:orgId/billing', () => {
173
207
  })
174
208
  const { cookie, orgId } = await seeded(app)
175
209
  const response = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } })
176
- expect(response.status).toBe(501)
177
- const body = (await response.json()) as { error: { code: string } }
178
- expect(body.error.code).toMatchInlineSnapshot(`"billing_unconfigured"`)
210
+ expect(response.status).toBe(200)
211
+ expect(await response.json()).toMatchInlineSnapshot(`
212
+ {
213
+ "enabledSources": [],
214
+ "spend": {
215
+ "amount": "0",
216
+ "currency": "usd",
217
+ "period": "month",
218
+ },
219
+ "status": "none",
220
+ }
221
+ `)
179
222
  })
180
223
  })
181
224
 
@@ -202,6 +245,7 @@ describe('PATCH /orgs/:orgId/billing', () => {
202
245
  expect(response.status).toBe(200)
203
246
  expect(await response.json()).toMatchInlineSnapshot(`
204
247
  {
248
+ "enabledSources": [],
205
249
  "spend": {
206
250
  "amount": "0",
207
251
  "currency": "usd",
@@ -294,7 +338,7 @@ describe('PATCH /orgs/:orgId/billing', () => {
294
338
  expect(response.status).toBe(404)
295
339
  })
296
340
 
297
- test('behavior: 501 when billing is unconfigured', async () => {
341
+ test('behavior: updates database-backed state when Stripe is unconfigured', async () => {
298
342
  const app = TestApp.create({
299
343
  auth: { superAdmin: { secret } },
300
344
  db: TestApp.database(),
@@ -303,9 +347,12 @@ describe('PATCH /orgs/:orgId/billing', () => {
303
347
  const { cookie, orgId } = await seeded(app)
304
348
  const response = await app.request(
305
349
  `/v1/orgs/${orgId}/billing`,
306
- patch(cookie, { spendLimit: null }),
350
+ patch(cookie, { spendLimit: { amount: '250' } }),
307
351
  )
308
- expect(response.status).toBe(501)
352
+ expect(response.status).toBe(200)
353
+ expect(
354
+ ((await response.json()) as { spendLimit?: { amount: string } }).spendLimit?.amount,
355
+ ).toBe('250')
309
356
  })
310
357
  })
311
358
 
@@ -355,8 +402,9 @@ describe('POST /orgs/:orgId/billing/stripe/checkout', () => {
355
402
  })
356
403
 
357
404
  test('behavior: an unreachable Stripe maps to the upstream error envelope', async () => {
358
- const app = createApp()
359
- const { cookie, orgId } = await seeded(app)
405
+ const db = TestApp.database()
406
+ const app = createApp({ db })
407
+ const { cookie, orgId } = await seededWithStripe(app, db)
360
408
  // First Stripe call the route makes is the customer create; the connection
361
409
  // is refused for real (no transport fake).
362
410
  const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
@@ -366,9 +414,13 @@ describe('POST /orgs/:orgId/billing/stripe/checkout', () => {
366
414
  })
367
415
  })
368
416
 
369
- describe('early access', () => {
370
- test('behavior: checkout requires early access', async () => {
371
- const app = createApp({ tmp_earlyAccess: true })
417
+ describe('billing source access', () => {
418
+ test('behavior: checkout requires Stripe to be enabled', async () => {
419
+ const app = TestApp.create({
420
+ auth: { superAdmin: { secret } },
421
+ db: TestApp.database(),
422
+ session: { wallet: { origin } },
423
+ })
372
424
  const { cookie, orgId } = await seeded(app)
373
425
  const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
374
426
  expect(response.status).toBe(403)
@@ -377,75 +429,29 @@ describe('early access', () => {
377
429
  expect(body).toMatchInlineSnapshot(`
378
430
  {
379
431
  "error": {
380
- "code": "early_access_required",
381
- "message": "Requires early access. Ask a teammate with access to invite you by email, or contact Tempo.",
432
+ "code": "billing_source_disabled",
433
+ "message": "Stripe billing is not enabled for this organization",
382
434
  },
383
435
  }
384
436
  `)
385
437
  })
386
438
 
387
- test('behavior: the gate precedes membership resolution', async () => {
388
- // The gate depends only on the caller, so it leaks nothing about the org;
389
- // early-access strangers still get the membership 404.
439
+ test('behavior: the source gate replaces temporary billing early access', async () => {
390
440
  const db = TestApp.database()
391
- const app = createApp({ db, tmp_earlyAccess: true })
392
- await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
393
- const { orgId } = await seeded(app)
394
-
395
- const other = await session(app)
396
- const gated = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(other))
397
- expect(gated.status).toBe(403)
398
- expect(((await gated.json()) as { error: { code: string } }).error.code).toBe('early_access_required') // prettier-ignore
399
-
400
- const granted = await session(app)
401
- const me = await app.request('/v1/me', { headers: { cookie: granted } })
402
- const { id: userId } = (await me.json()) as { id: string }
403
- await Users.setEmail(db, userId, 'stranger@example.org')
404
- expect(
405
- (await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(granted))).status,
406
- ).toBe(404)
407
- })
408
-
409
- test('behavior: the gate precedes the owner-role check', async () => {
410
- const db = TestApp.database()
411
- const app = createApp({ db, tmp_earlyAccess: true })
412
- await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
413
- const { orgId } = await seeded(app)
414
- const member = await session(app)
415
- const me = await app.request('/v1/me', { headers: { cookie: member } })
416
- const { id: userId } = (await me.json()) as { id: string }
417
- await Memberships.create(db, { orgId, role: 'member', userId })
418
-
419
- const gated = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(member))
420
- expect(gated.status).toBe(403)
421
- expect(((await gated.json()) as { error: { code: string } }).error.code).toBe('early_access_required') // prettier-ignore
422
-
423
- // Early-access members still fail the owner-role check.
424
- await Users.setEmail(db, userId, 'member@example.org')
425
- const write = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(member))
426
- expect(write.status).toBe(403)
427
- expect(((await write.json()) as { error: { code: string } }).error.code).toBe('forbidden')
428
- })
429
-
430
- test('behavior: early-access owners pass the gate', async () => {
431
- const db = TestApp.database()
432
- const app = createApp({ db, tmp_earlyAccess: true })
433
- await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
434
- const { cookie, orgId } = await seeded(app)
435
- const me = await app.request('/v1/me', { headers: { cookie } })
436
- const { id: userId } = (await me.json()) as { id: string }
437
- await Users.setEmail(db, userId, 'dev@example.org')
441
+ const app = createApp({ db })
442
+ const { cookie, orgId } = await seededWithStripe(app, db)
438
443
 
439
- // The unreachable Stripe upstream (502) proves the request passed the gate.
444
+ // The unreachable Stripe upstream proves source access, not user early access, is the gate.
440
445
  const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
441
446
  expect(response.status).toBe(502)
442
447
  expect(((await response.json()) as { error: { code: string } }).error.code).toBe('upstream_error') // prettier-ignore
443
448
  })
444
449
 
445
- test('behavior: management keys pass the gate', async () => {
450
+ test('behavior: an enabled management key reaches Stripe', async () => {
451
+ const db = TestApp.database()
446
452
  const store = TestApp.kvStore({ keys: [] })
447
- const app = createApp({ tmp_earlyAccess: true, kv: { store } })
448
- const { orgId } = await seeded(app)
453
+ const app = createApp({ db, kv: { store } })
454
+ const { orgId } = await seededWithStripe(app, db)
449
455
  const { token } = await ApiKeys.mint(store, { orgId, scopes: ['management:write'] })
450
456
 
451
457
  const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, {
@@ -455,9 +461,10 @@ describe('early access', () => {
455
461
  expect(response.status).toBe(502)
456
462
  })
457
463
 
458
- test('behavior: the super admin passes the gate', async () => {
459
- const app = createApp({ tmp_earlyAccess: true })
460
- const { orgId } = await seeded(app)
464
+ test('behavior: an enabled super admin reaches Stripe', async () => {
465
+ const db = TestApp.database()
466
+ const app = createApp({ db })
467
+ const { orgId } = await seededWithStripe(app, db)
461
468
 
462
469
  const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, {
463
470
  ...as(secret),
@@ -551,6 +558,7 @@ describe('DELETE /orgs/:orgId', () => {
551
558
  const db = TestApp.database()
552
559
  const app = createApp({ db })
553
560
  const { cookie, orgId } = await seeded(app)
561
+ await EnabledBillingSources.add(db, { createdBy: 'test', orgId, source: 'stripe' })
554
562
  await StripeCustomers.create(db, { orgId, stripeCustomerId: 'cus_cascade' })
555
563
 
556
564
  const response = await app.request(`/v1/orgs/${orgId}`, {
@@ -558,19 +566,30 @@ describe('DELETE /orgs/:orgId', () => {
558
566
  method: 'DELETE',
559
567
  })
560
568
  expect(response.status).toBe(200)
569
+ expect(await EnabledBillingSources.listByOrg(db, orgId)).toEqual([])
561
570
  expect(await StripeCustomers.get(db, orgId)).toBeUndefined()
562
571
  })
563
572
  })
564
573
 
565
574
  describe('environment scoping', () => {
566
- test('behavior: sandbox is 501 when no sandbox source is configured', async () => {
575
+ test('behavior: sandbox billing state remains readable without a Stripe source', async () => {
567
576
  const app = createApp()
568
577
  const { cookie, orgId } = await seeded(app)
569
578
  const response = await app.request(`/v1/orgs/${orgId}/billing?environment=sandbox`, {
570
579
  headers: { cookie },
571
580
  })
572
- expect(response.status).toBe(501)
573
- expect(((await response.json()) as { error: { code: string } }).error.code).toBe('billing_unconfigured') // prettier-ignore
581
+ expect(response.status).toBe(200)
582
+ expect(await response.json()).toMatchInlineSnapshot(`
583
+ {
584
+ "enabledSources": [],
585
+ "spend": {
586
+ "amount": "0",
587
+ "currency": "usd",
588
+ "period": "month",
589
+ },
590
+ "status": "none",
591
+ }
592
+ `)
574
593
  })
575
594
 
576
595
  test('behavior: rejects an unknown environment', async () => {
@@ -756,7 +775,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
756
775
  test('checkout provisions distinct customers per environment', { timeout: 30_000 }, async () => {
757
776
  const db = TestApp.database()
758
777
  const app = createApp({ billing: sandboxBillingBoth(), db })
759
- const { cookie, orgId } = await seeded(app)
778
+ const { cookie, orgId } = await seededWithStripe(app, db)
760
779
 
761
780
  const prod = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
762
781
  const sbx = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout?environment=sandbox`, post(cookie)) // prettier-ignore
@@ -783,7 +802,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
783
802
  test('checkout provisions a real customer and setup session', { timeout: 30_000 }, async () => {
784
803
  const db = TestApp.database()
785
804
  const app = createApp({ billing: sandboxBilling(), db })
786
- const { cookie, orgId } = await seeded(app)
805
+ const { cookie, orgId } = await seededWithStripe(app, db)
787
806
 
788
807
  const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
789
808
  // Track for teardown before asserting: a failed assertion must not leak
@@ -831,7 +850,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
831
850
  // prettier-ignore
832
851
  const db = TestApp.database()
833
852
  const app = createApp({ billing: sandboxBilling(), db })
834
- const { cookie, orgId } = await seeded(app)
853
+ const { cookie, orgId } = await seededWithStripe(app, db)
835
854
  const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
836
855
  const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId
837
856
  if (customerId) TestStripe.track(customerId)
@@ -844,8 +863,9 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
844
863
  invoice_settings: { default_payment_method: attached.paymentMethod },
845
864
  })
846
865
  await StripeCustomers.setStatus(db, orgId, 'active')
866
+ await EnabledBillingSources.remove(db, { orgId, source: 'stripe' })
847
867
 
848
- // The list reads live Stripe state: card details plus the default marker.
868
+ // Disabling setup does not block management of the existing Stripe source.
849
869
  const list = await app.request(`/v1/orgs/${orgId}/billing/payment-methods`, {
850
870
  headers: { cookie },
851
871
  })
@@ -900,7 +920,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
900
920
  // prettier-ignore
901
921
  const db = TestApp.database()
902
922
  const app = createApp({ billing: sandboxBilling(), db })
903
- const { cookie, orgId } = await seeded(app)
923
+ const { cookie, orgId } = await seededWithStripe(app, db)
904
924
  const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
905
925
  const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId
906
926
  if (customerId) TestStripe.track(customerId)
@@ -930,11 +950,12 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
930
950
 
931
951
  const db = TestApp.database()
932
952
  const app = createApp({ billing: sandboxBilling(), db })
933
- const { cookie, orgId } = await seeded(app)
953
+ const { cookie, orgId } = await seededWithStripe(app, db)
934
954
  const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
935
955
  const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId
936
956
  if (customerId) TestStripe.track(customerId)
937
957
  expect(checkout.status).toBe(200)
958
+ await EnabledBillingSources.remove(db, { orgId, source: 'stripe' })
938
959
 
939
960
  const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/manage`, post(cookie))
940
961
  expect(response.status).toBe(200)
@@ -963,7 +984,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (live delivery)', () => {
963
984
  })
964
985
 
965
986
  test('attach → active, detach → none, delete → canceled', { timeout: 300_000 }, async () => {
966
- const { cookie, orgId } = await seeded(app)
987
+ const { cookie, orgId } = await seededWithStripe(app, db)
967
988
  const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
968
989
  const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId
969
990
  if (customerId) TestStripe.track(customerId)