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
@@ -4,8 +4,6 @@ import { exportJWK, generateKeyPair } from 'jose'
4
4
  import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
5
5
 
6
6
  import * as TestApp from '../../../../test/App.js'
7
- import * as EarlyAccess from '../../../db/tables/earlyAccess.js'
8
- import * as Users from '../../../db/tables/users.js'
9
7
  import * as Me from './me.js'
10
8
 
11
9
  /** Origin pinned for SIWE domain binding; Hono test requests use this host. */
@@ -20,40 +18,6 @@ function createApp() {
20
18
  })
21
19
  }
22
20
 
23
- /** App with the early-access gate enabled; allowlist entries seed through `EarlyAccess.add`. */
24
- function createGatedApp(db = TestApp.database()) {
25
- return TestApp.create({
26
- auth: { keys: [TestApp.key] },
27
- db,
28
- session: { wallet: { origin } },
29
- tmp_earlyAccess: true,
30
- })
31
- }
32
-
33
- /** RequestInit for a JSON mutation carrying the session cookie. */
34
- function json(method: 'POST', cookie: string, body: unknown) {
35
- return {
36
- body: JSON.stringify(body),
37
- headers: { 'content-type': 'application/json', cookie },
38
- method,
39
- }
40
- }
41
-
42
- /** Signs in a fresh scripted account; returns its session cookie and user id. */
43
- async function session(app: TestApp.signIn.App) {
44
- const account = privateKeyToAccount(generatePrivateKey())
45
- const { cookie } = await TestApp.signIn(app, account)
46
- const me = await app.request('/v1/me', { headers: { cookie: cookie! } })
47
- const { id } = (await me.json()) as { id: string }
48
- return { cookie: cookie!, userId: id }
49
- }
50
-
51
- /** Reads the session's `tmp_earlyAccess` flag from `/v1/me`. TODO: remove after GA. */
52
- async function tmp_earlyAccess(app: TestApp.signIn.App, cookie: string) {
53
- const response = await app.request('/v1/me', { headers: { cookie } })
54
- return (await TestApp.json(response, Me.schema.User)).tmp_earlyAccess
55
- }
56
-
57
21
  describe('GET /me', () => {
58
22
  test('returns the signed-in user', async () => {
59
23
  const app = createApp()
@@ -219,13 +183,12 @@ describe('sign-in email requirement', () => {
219
183
  })
220
184
 
221
185
  describe('open sign-up', () => {
222
- test('allows new users on any domain with the gate on', async () => {
186
+ test('allows new users on any domain', async () => {
223
187
  const oidc = await createIssuer({ email: 'dev@example.com' })
224
188
  try {
225
189
  const app = TestApp.create({
226
190
  auth: { keys: [TestApp.key] },
227
191
  session: { wallet: { issuer: oidc.url, origin } },
228
- tmp_earlyAccess: true,
229
192
  })
230
193
  const account = privateKeyToAccount(generatePrivateKey())
231
194
  const { cookie, verifyResponse } = await TestApp.signIn(app, account, {
@@ -242,8 +205,8 @@ describe('open sign-up', () => {
242
205
  }
243
206
  })
244
207
 
245
- test('allows new users without an identity token with the gate on', async () => {
246
- const app = createGatedApp()
208
+ test('allows new users without an identity token', async () => {
209
+ const app = createApp()
247
210
  const account = privateKeyToAccount(generatePrivateKey())
248
211
  const { cookie, verifyResponse } = await TestApp.signIn(app, account)
249
212
 
@@ -252,111 +215,6 @@ describe('open sign-up', () => {
252
215
  })
253
216
  })
254
217
 
255
- describe('tmp_earlyAccess', () => {
256
- test('defaults to true while the gate is dormant', async () => {
257
- const app = createApp()
258
- const { cookie } = await session(app)
259
- expect(await tmp_earlyAccess(app, cookie)).toBe(true)
260
- })
261
-
262
- test('matches domain entries case-insensitively', async () => {
263
- const db = TestApp.database()
264
- const app = createGatedApp(db)
265
- await EarlyAccess.add(db, { createdBy: 'test', entry: 'Example.ORG' })
266
- const user = await session(app)
267
- await Users.setEmail(db, user.userId, 'Dev@EXAMPLE.org')
268
-
269
- expect(await tmp_earlyAccess(app, user.cookie)).toBe(true)
270
- })
271
-
272
- test('exact-email entries grant only that address', async () => {
273
- const db = TestApp.database()
274
- const app = createGatedApp(db)
275
- await EarlyAccess.add(db, { createdBy: 'test', entry: 'dev@example.org' })
276
- const granted = await session(app)
277
- await Users.setEmail(db, granted.userId, 'dev@example.org')
278
- const denied = await session(app)
279
- await Users.setEmail(db, denied.userId, 'other@example.org')
280
-
281
- expect(await tmp_earlyAccess(app, granted.cookie)).toBe(true)
282
- expect(await tmp_earlyAccess(app, denied.cookie)).toBe(false)
283
- })
284
-
285
- test('stays false without a matching entry', async () => {
286
- const db = TestApp.database()
287
- const app = createGatedApp(db)
288
- const user = await session(app)
289
- await Users.setEmail(db, user.userId, 'dev@example.net')
290
-
291
- expect(await tmp_earlyAccess(app, user.cookie)).toBe(false)
292
- })
293
-
294
- test('stays false without a verified email', async () => {
295
- const app = createGatedApp()
296
- const { cookie } = await session(app)
297
-
298
- expect(await tmp_earlyAccess(app, cookie)).toBe(false)
299
- })
300
-
301
- test('flips false once the entry is removed', async () => {
302
- const db = TestApp.database()
303
- const app = createGatedApp(db)
304
- await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
305
- const user = await session(app)
306
- await Users.setEmail(db, user.userId, 'dev@example.org')
307
- expect(await tmp_earlyAccess(app, user.cookie)).toBe(true)
308
-
309
- await EarlyAccess.remove(db, 'example.org')
310
- expect(await tmp_earlyAccess(app, user.cookie)).toBe(false)
311
- })
312
-
313
- test('invitations from early-access inviters grant access until revoked', async () => {
314
- const db = TestApp.database()
315
- const app = createGatedApp(db)
316
- await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
317
- const inviter = await session(app)
318
- await Users.setEmail(db, inviter.userId, 'owner@example.org')
319
- const created = await app.request('/v1/orgs', json('POST', inviter.cookie, { name: 'Acme' }))
320
- const org = (await created.json()) as { id: string }
321
- const invited = await app.request(
322
- `/v1/orgs/${org.id}/invitations`,
323
- json('POST', inviter.cookie, { email: 'invitee@example.net', role: 'member' }),
324
- )
325
- expect(invited.status).toBe(200)
326
- const invitation = (await invited.json()) as { id: string }
327
-
328
- const invitee = await session(app)
329
- await Users.setEmail(db, invitee.userId, 'invitee@example.net')
330
- expect(await tmp_earlyAccess(app, invitee.cookie)).toBe(true)
331
-
332
- // Revoking the invitation withdraws the grant.
333
- const revoked = await app.request(`/v1/orgs/${org.id}/invitations/${invitation.id}`, {
334
- headers: { cookie: inviter.cookie },
335
- method: 'DELETE',
336
- })
337
- expect(revoked.status).toBe(200)
338
- expect(await tmp_earlyAccess(app, invitee.cookie)).toBe(false)
339
- })
340
-
341
- test('invitations from non-early-access inviters do not grant access', async () => {
342
- const db = TestApp.database()
343
- const app = createGatedApp(db)
344
- const inviter = await session(app)
345
- await Users.setEmail(db, inviter.userId, 'owner@example.net')
346
- const created = await app.request('/v1/orgs', json('POST', inviter.cookie, { name: 'Acme' }))
347
- const org = (await created.json()) as { id: string }
348
- const invited = await app.request(
349
- `/v1/orgs/${org.id}/invitations`,
350
- json('POST', inviter.cookie, { email: 'friend@example.net', role: 'member' }),
351
- )
352
- expect(invited.status).toBe(200)
353
-
354
- const invitee = await session(app)
355
- await Users.setEmail(db, invitee.userId, 'friend@example.net')
356
- expect(await tmp_earlyAccess(app, invitee.cookie)).toBe(false)
357
- })
358
- })
359
-
360
218
  /** Local OIDC issuer standing in for the wallet: the SDK's own provider (discovery, JWKS, token mint). */
361
219
  async function createIssuer(options: { email: string }) {
362
220
  // Extractable: the provider takes key material as JWK strings.
@@ -35,11 +35,6 @@ export namespace schema {
35
35
  z.describe('Opaque user id (`usr_…`).'),
36
36
  z.meta({ examples: ['usr_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
37
37
  ),
38
- // TODO: remove after GA.
39
- tmp_earlyAccess: z.boolean().check(
40
- z.describe('Whether the user has early access; gated features require it while the platform is invite-only.'), // prettier-ignore
41
- z.meta({ examples: [true] }),
42
- ),
43
38
  updatedAt: z
44
39
  .string()
45
40
  .check(
@@ -99,7 +94,6 @@ export function me() {
99
94
  createdAt: user.createdAt,
100
95
  ...(user.email ? { email: user.email } : {}),
101
96
  id: user.id,
102
- tmp_earlyAccess: await Auth.tmp_earlyAccess(c),
103
97
  updatedAt: user.updatedAt,
104
98
  }),
105
99
  200,
@@ -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",
@@ -0,0 +1,86 @@
1
+ import { Hono } from 'hono'
2
+ import type * as z from 'zod/mini'
3
+
4
+ import type * as App from '../../App.js'
5
+ import * as Auth from '../../internal/Auth.js'
6
+ import * as OpenApi from '../../internal/OpenApi.js'
7
+ import * as Metadata from '../metadata.js'
8
+ import * as Mpp from './Mpp.js'
9
+
10
+ /** Mounts MPP credential verification and broadcast routes. */
11
+ export function mpp() {
12
+ const app = new Hono<App.Environment>()
13
+ .post(
14
+ '/v1/mpp/verify',
15
+ Auth.policy({ apiKey: { scopes: ['mpp:write'] }, mpp: false, public: false }),
16
+ OpenApi.validate('json', Mpp.schema.RelayInput, bodyValidation),
17
+ OpenApi.describeRoute({
18
+ operationId: 'verifyMppCredential',
19
+ responses: responses({
20
+ description: 'The MPP credential validation result.',
21
+ schema: Mpp.schema.VerifyResponse,
22
+ }),
23
+ summary: 'Verify MPP credential',
24
+ tags: ['MPP Relay'],
25
+ }),
26
+ (c) => {
27
+ if (Auth.narrowAccess) return Auth.accessError(c)
28
+ if (OpenApi.narrowValidation) return OpenApi.validationError(c, bodyValidation)
29
+ return Mpp.unavailable<Mpp.VerifyResponse>(c)
30
+ },
31
+ )
32
+ .post(
33
+ '/v1/mpp/broadcast',
34
+ Auth.policy({ apiKey: { scopes: ['mpp:write'] }, mpp: false, public: false }),
35
+ OpenApi.validate('json', Mpp.schema.RelayInput, bodyValidation),
36
+ OpenApi.describeRoute({
37
+ operationId: 'broadcastMppCredential',
38
+ responses: responses({
39
+ description: 'The MPP credential broadcast result.',
40
+ schema: Mpp.schema.BroadcastResponse,
41
+ }),
42
+ summary: 'Broadcast MPP credential',
43
+ tags: ['MPP Relay'],
44
+ }),
45
+ (c) => {
46
+ if (Auth.narrowAccess) return Auth.accessError(c)
47
+ if (OpenApi.narrowValidation) return OpenApi.validationError(c, bodyValidation)
48
+ return Mpp.unavailable<Mpp.BroadcastResponse>(c)
49
+ },
50
+ )
51
+ return Metadata.attach(app, openapi)
52
+ }
53
+
54
+ const bodyValidation = {
55
+ code: 'body_invalid',
56
+ message: 'Check the MPP credential and challenge, then try again.',
57
+ } as const
58
+
59
+ function responses<schema extends z.ZodMiniType>(options: { description: string; schema: schema }) {
60
+ return {
61
+ ...OpenApi.responses({
62
+ errors: { 403: 'The API key does not grant MPP relay access.' },
63
+ success: options,
64
+ }),
65
+ 503: {
66
+ content: { 'application/json': { schema: OpenApi.resolver(Mpp.schema.Unavailable) } },
67
+ description: 'MPP relay settlement is temporarily unavailable.',
68
+ headers: {
69
+ 'Retry-After': {
70
+ description: 'How many seconds to wait before retrying the relay.',
71
+ schema: { type: 'integer' as const },
72
+ },
73
+ },
74
+ },
75
+ }
76
+ }
77
+
78
+ const openapi = {
79
+ tags: [
80
+ {
81
+ name: 'MPP Relay',
82
+ description: 'Validate and broadcast MPP payment credentials through Tempo.',
83
+ },
84
+ ],
85
+ 'x-tagGroups': [{ name: 'Payments', tags: ['MPP Relay'] }],
86
+ } satisfies App.create.Metadata
@@ -0,0 +1,193 @@
1
+ import * as TestApp from '../../../test/App.js'
2
+ import * as Mpp from './Mpp.js'
3
+
4
+ const key = {
5
+ id: 'key_mpp',
6
+ orgId: 'org_test',
7
+ scopes: ['mpp:write'],
8
+ token: 'secret_mpp',
9
+ } satisfies TestApp.kvStore.Key
10
+
11
+ const input = {
12
+ challenge: {
13
+ description: 'Tempo API payment',
14
+ id: 'ch_01j3j1k2l3m4n5p6q7r8s9t0u',
15
+ intent: 'charge',
16
+ method: 'tempo',
17
+ opaque: 'eyJhbGciOiJIUzI1NiJ9',
18
+ realm: 'merchant.example',
19
+ request: { amount: '1', currency: 'USD' },
20
+ },
21
+ payload: { signature: `0x${'aa'.repeat(65)}`, type: 'transaction' },
22
+ } satisfies Mpp.RelayInput
23
+
24
+ const errorCodes = [
25
+ 'unsupported',
26
+ 'policy_denied',
27
+ 'invalid_payment',
28
+ 'insufficient_funds',
29
+ 'expired',
30
+ 'already_used',
31
+ 'screen_rejected',
32
+ 'simulation_failed',
33
+ 'broadcast_failed',
34
+ 'temporarily_unavailable',
35
+ 'unknown',
36
+ ] as const
37
+
38
+ const errorDescriptions = [
39
+ 'Method, intent, network, or asset is unsupported.',
40
+ 'Payment violates relay or account policy.',
41
+ 'Payload, signature, parties, or payment requirements are invalid.',
42
+ 'Sender lacks funds.',
43
+ 'Challenge or payment authorization expired.',
44
+ 'Credential or transaction already used.',
45
+ 'Originator or recipient failed screening.',
46
+ 'Transaction simulation failed before broadcast.',
47
+ 'Transaction broadcast or confirmation failed.',
48
+ 'Retry may succeed.',
49
+ 'Unexpected relay failure.',
50
+ ] as const
51
+
52
+ describe('MPP relay', () => {
53
+ test.each(['/v1/mpp/verify', '/v1/mpp/broadcast'])('%s requires the MPP scope', async (path) => {
54
+ const app = TestApp.create({ auth: { keys: [key] } })
55
+ const response = await app.fetch(
56
+ new Request(`http://tempo-api.test${path}`, {
57
+ body: JSON.stringify(input),
58
+ headers: { 'content-type': 'application/json', authorization: 'Bearer secret_mpp' },
59
+ method: 'POST',
60
+ }),
61
+ )
62
+
63
+ expect(response.status).toBe(503)
64
+ expect(response.headers.get('retry-after')).toBe('60')
65
+ expect(await response.json()).toMatchInlineSnapshot(`
66
+ {
67
+ "error": {
68
+ "code": "temporarily_unavailable",
69
+ "message": "MPP relay is not enabled yet.",
70
+ },
71
+ "success": false,
72
+ }
73
+ `)
74
+ })
75
+
76
+ test('rejects unscoped API keys', async () => {
77
+ const app = TestApp.create({ auth: { keys: [key, TestApp.key] } })
78
+ const response = await app.fetch(
79
+ new Request('http://tempo-api.test/v1/mpp/verify', {
80
+ body: JSON.stringify(input),
81
+ headers: {
82
+ 'content-type': 'application/json',
83
+ authorization: `Bearer ${TestApp.key.token}`,
84
+ },
85
+ method: 'POST',
86
+ }),
87
+ )
88
+
89
+ expect(response.status).toBe(403)
90
+ expect(await response.json()).toMatchObject({ error: { code: 'api_key_forbidden' } })
91
+ })
92
+
93
+ test('requires API-key authentication', async () => {
94
+ const app = TestApp.create()
95
+ const response = await app.fetch(
96
+ new Request('http://tempo-api.test/v1/mpp/verify', {
97
+ body: JSON.stringify(input),
98
+ headers: { 'content-type': 'application/json' },
99
+ method: 'POST',
100
+ }),
101
+ )
102
+
103
+ expect(response.status).toBe(401)
104
+ expect(await response.json()).toMatchObject({ error: { code: 'api_key_missing' } })
105
+ })
106
+
107
+ test.each(['/v1/mpp/verify', '/v1/mpp/broadcast'])(
108
+ '%s rejects malformed credential input before the disabled handler',
109
+ async (path) => {
110
+ const app = TestApp.create({ auth: { keys: [key] } })
111
+ const response = await app.fetch(
112
+ new Request(`http://tempo-api.test${path}`, {
113
+ body: JSON.stringify({}),
114
+ headers: { 'content-type': 'application/json', authorization: 'Bearer secret_mpp' },
115
+ method: 'POST',
116
+ }),
117
+ )
118
+
119
+ expect(response.status).toBe(400)
120
+ expect(await response.json()).toMatchObject({ error: { code: 'body_invalid' } })
121
+ },
122
+ )
123
+
124
+ test.each(['/v1/mpp/verify', '/v1/mpp/broadcast'])(
125
+ '%s requires a credential payload',
126
+ async (path) => {
127
+ const app = TestApp.create({ auth: { keys: [key] } })
128
+ const response = await app.fetch(
129
+ new Request(`http://tempo-api.test${path}`, {
130
+ body: JSON.stringify({ challenge: input.challenge }),
131
+ headers: { 'content-type': 'application/json', authorization: 'Bearer secret_mpp' },
132
+ method: 'POST',
133
+ }),
134
+ )
135
+
136
+ expect(response.status).toBe(400)
137
+ expect(await response.json()).toMatchObject({ error: { code: 'body_invalid' } })
138
+ },
139
+ )
140
+
141
+ test('publishes the MPP endpoints', async () => {
142
+ const app = TestApp.create()
143
+ const spec = (await (await app.request('/openapi.json')).json()) as {
144
+ paths: Record<string, { post?: { responses?: Record<string, unknown> } }>
145
+ }
146
+
147
+ for (const path of ['/v1/mpp/verify', '/v1/mpp/broadcast']) {
148
+ expect(spec.paths[path]?.post?.responses?.['200']).toBeDefined()
149
+ expect(spec.paths[path]?.post?.responses?.['503']).toBeDefined()
150
+ }
151
+
152
+ const response = JSON.stringify(spec.paths['/v1/mpp/verify']?.post?.responses?.['200'])
153
+ for (const description of errorDescriptions) expect(response).toContain(description)
154
+ })
155
+ })
156
+
157
+ describe('MPP relay schemas', () => {
158
+ test('parses public relay contracts', () => {
159
+ expect(Mpp.schema.RelayInput.parse(input)).toEqual(input)
160
+ expect(Mpp.schema.VerifyResponse.parse({ success: true })).toEqual({ success: true })
161
+ expect(
162
+ Mpp.schema.BroadcastResponse.parse({
163
+ receipt: {
164
+ method: 'tempo',
165
+ reference: `0x${'22'.repeat(32)}`,
166
+ timestamp: '2026-07-20T18:30:00.000Z',
167
+ },
168
+ success: true,
169
+ }),
170
+ ).toMatchInlineSnapshot(`
171
+ {
172
+ "receipt": {
173
+ "method": "tempo",
174
+ "reference": "0x2222222222222222222222222222222222222222222222222222222222222222",
175
+ "timestamp": "2026-07-20T18:30:00.000Z",
176
+ },
177
+ "success": true,
178
+ }
179
+ `)
180
+ })
181
+
182
+ test.each(errorCodes)('accepts the %s error code', (code) => {
183
+ expect(Mpp.schema.RelayError.parse({ code })).toEqual({ code })
184
+ })
185
+
186
+ test('publishes the relay error code type', () => {
187
+ expectTypeOf<Mpp.RelayErrorCode>().toEqualTypeOf<(typeof errorCodes)[number]>()
188
+ })
189
+
190
+ test('rejects an unknown error code', () => {
191
+ expect(Mpp.schema.RelayError.safeParse({ code: 'unexpected' }).success).toBe(false)
192
+ })
193
+ })