tapimo 0.2.0 → 0.3.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 (313) hide show
  1. package/README.md +31 -3
  2. package/dist/ApiKey.d.ts +1 -0
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +5 -2
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +28 -1
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +34 -1
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +7167 -4507
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +8 -0
  13. package/dist/App.js.map +1 -1
  14. package/dist/Scope.d.ts +11 -2
  15. package/dist/Scope.d.ts.map +1 -1
  16. package/dist/Scope.js +14 -3
  17. package/dist/Scope.js.map +1 -1
  18. package/dist/admin/App.d.ts +128 -118
  19. package/dist/admin/App.d.ts.map +1 -1
  20. package/dist/admin/apps/api-keys.d.ts +7 -0
  21. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  22. package/dist/admin/apps/scopes.d.ts +8 -0
  23. package/dist/admin/apps/scopes.d.ts.map +1 -1
  24. package/dist/admin/apps/scopes.js +3 -0
  25. package/dist/admin/apps/scopes.js.map +1 -1
  26. package/dist/admin/apps/verified-tokens.d.ts +57 -57
  27. package/dist/admin/ui.generated.d.ts +1 -1
  28. package/dist/admin/ui.generated.js +13 -13
  29. package/dist/admin/ui.generated.js.map +1 -1
  30. package/dist/analytics/Analytics.d.ts +151 -0
  31. package/dist/analytics/Analytics.d.ts.map +1 -0
  32. package/dist/analytics/Analytics.js +212 -0
  33. package/dist/analytics/Analytics.js.map +1 -0
  34. package/dist/analytics/tables/requestEvents.d.ts +215 -0
  35. package/dist/analytics/tables/requestEvents.d.ts.map +1 -0
  36. package/dist/analytics/tables/requestEvents.js +264 -0
  37. package/dist/analytics/tables/requestEvents.js.map +1 -0
  38. package/dist/apps/data/App.d.ts +1024 -624
  39. package/dist/apps/data/App.d.ts.map +1 -1
  40. package/dist/apps/data/App.js +36 -0
  41. package/dist/apps/data/App.js.map +1 -1
  42. package/dist/apps/data/routes/balances.d.ts +45 -45
  43. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  44. package/dist/apps/data/routes/exchanges.d.ts +455 -455
  45. package/dist/apps/data/routes/fee-amm.d.ts +101 -101
  46. package/dist/apps/data/routes/funding.d.ts +765 -0
  47. package/dist/apps/data/routes/funding.d.ts.map +1 -0
  48. package/dist/apps/data/routes/funding.js +383 -0
  49. package/dist/apps/data/routes/funding.js.map +1 -0
  50. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  51. package/dist/apps/data/routes/receipts.js +2 -8
  52. package/dist/apps/data/routes/receipts.js.map +1 -1
  53. package/dist/apps/data/routes/rpc.d.ts +29 -36
  54. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  55. package/dist/apps/data/routes/rpc.js +29 -1
  56. package/dist/apps/data/routes/rpc.js.map +1 -1
  57. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  58. package/dist/apps/data/routes/transfers.d.ts +56 -56
  59. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  60. package/dist/apps/data/routes/webhooks.d.ts +45 -45
  61. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  62. package/dist/apps/data/routes/webhooks.js +8 -8
  63. package/dist/apps/data/routes/webhooks.js.map +1 -1
  64. package/dist/apps/docs/App.d.ts +4 -2
  65. package/dist/apps/docs/App.d.ts.map +1 -1
  66. package/dist/apps/docs/App.js +27 -95
  67. package/dist/apps/docs/App.js.map +1 -1
  68. package/dist/apps/docs/reference.d.ts +21 -0
  69. package/dist/apps/docs/reference.d.ts.map +1 -0
  70. package/dist/apps/docs/reference.js +112 -0
  71. package/dist/apps/docs/reference.js.map +1 -0
  72. package/dist/apps/index.d.ts +1 -0
  73. package/dist/apps/index.d.ts.map +1 -1
  74. package/dist/apps/index.js +1 -0
  75. package/dist/apps/index.js.map +1 -1
  76. package/dist/apps/mcp.d.ts +76 -0
  77. package/dist/apps/mcp.d.ts.map +1 -0
  78. package/dist/apps/mcp.js +188 -0
  79. package/dist/apps/mcp.js.map +1 -0
  80. package/dist/apps/platform/App.d.ts +3005 -728
  81. package/dist/apps/platform/App.d.ts.map +1 -1
  82. package/dist/apps/platform/App.js +13 -3
  83. package/dist/apps/platform/App.js.map +1 -1
  84. package/dist/apps/platform/Billing.d.ts +279 -0
  85. package/dist/apps/platform/Billing.d.ts.map +1 -0
  86. package/dist/apps/platform/Billing.js +477 -0
  87. package/dist/apps/platform/Billing.js.map +1 -0
  88. package/dist/apps/platform/index.d.ts +1 -0
  89. package/dist/apps/platform/index.d.ts.map +1 -1
  90. package/dist/apps/platform/index.js +1 -0
  91. package/dist/apps/platform/index.js.map +1 -1
  92. package/dist/apps/platform/routes/api-keys.d.ts +341 -49
  93. package/dist/apps/platform/routes/api-keys.d.ts.map +1 -1
  94. package/dist/apps/platform/routes/api-keys.js +115 -13
  95. package/dist/apps/platform/routes/api-keys.js.map +1 -1
  96. package/dist/apps/platform/routes/billing.d.ts +1520 -0
  97. package/dist/apps/platform/routes/billing.d.ts.map +1 -0
  98. package/dist/apps/platform/routes/billing.js +795 -0
  99. package/dist/apps/platform/routes/billing.js.map +1 -0
  100. package/dist/apps/platform/routes/invitations.d.ts +201 -83
  101. package/dist/apps/platform/routes/invitations.d.ts.map +1 -1
  102. package/dist/apps/platform/routes/invitations.js +58 -0
  103. package/dist/apps/platform/routes/invitations.js.map +1 -1
  104. package/dist/apps/platform/routes/me.d.ts +17 -17
  105. package/dist/apps/platform/routes/orgs.d.ts +50 -36
  106. package/dist/apps/platform/routes/orgs.d.ts.map +1 -1
  107. package/dist/apps/platform/routes/orgs.js +14 -3
  108. package/dist/apps/platform/routes/orgs.js.map +1 -1
  109. package/dist/apps/platform/routes/projects.d.ts +97 -97
  110. package/dist/apps/platform/routes/scopes.d.ts +107 -0
  111. package/dist/apps/platform/routes/scopes.d.ts.map +1 -0
  112. package/dist/apps/platform/routes/scopes.js +66 -0
  113. package/dist/apps/platform/routes/scopes.js.map +1 -0
  114. package/dist/apps/platform/routes/usage.d.ts +588 -0
  115. package/dist/apps/platform/routes/usage.d.ts.map +1 -0
  116. package/dist/apps/platform/routes/usage.js +391 -0
  117. package/dist/apps/platform/routes/usage.js.map +1 -0
  118. package/dist/apps/relay/App.d.ts +4 -1
  119. package/dist/apps/relay/App.d.ts.map +1 -1
  120. package/dist/apps/relay/App.js +118 -10
  121. package/dist/apps/relay/App.js.map +1 -1
  122. package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
  123. package/dist/apps/relay/Sponsorships.js +2 -1
  124. package/dist/apps/relay/Sponsorships.js.map +1 -1
  125. package/dist/cli/Cli.d.ts +8 -1
  126. package/dist/cli/Cli.d.ts.map +1 -1
  127. package/dist/cli/Cli.js +11 -3
  128. package/dist/cli/Cli.js.map +1 -1
  129. package/dist/cli/commands/admin.d.ts +4 -0
  130. package/dist/cli/commands/admin.d.ts.map +1 -1
  131. package/dist/cli/commands/admin.js +80 -1
  132. package/dist/cli/commands/admin.js.map +1 -1
  133. package/dist/db/Db.d.ts +7 -0
  134. package/dist/db/Db.d.ts.map +1 -1
  135. package/dist/db/Db.js +126 -0
  136. package/dist/db/Db.js.map +1 -1
  137. package/dist/db/Schema.d.ts +79 -0
  138. package/dist/db/Schema.d.ts.map +1 -1
  139. package/dist/db/Schema.js +88 -2
  140. package/dist/db/Schema.js.map +1 -1
  141. package/dist/db/tables/billingSettings.d.ts +55 -0
  142. package/dist/db/tables/billingSettings.d.ts.map +1 -0
  143. package/dist/db/tables/billingSettings.js +70 -0
  144. package/dist/db/tables/billingSettings.js.map +1 -0
  145. package/dist/db/tables/invitations.js +2 -2
  146. package/dist/db/tables/invitations.js.map +1 -1
  147. package/dist/db/tables/organizations.d.ts +6 -2
  148. package/dist/db/tables/organizations.d.ts.map +1 -1
  149. package/dist/db/tables/organizations.js +10 -4
  150. package/dist/db/tables/organizations.js.map +1 -1
  151. package/dist/db/tables/projects.js +2 -2
  152. package/dist/db/tables/projects.js.map +1 -1
  153. package/dist/db/tables/requestUsage.d.ts +121 -0
  154. package/dist/db/tables/requestUsage.d.ts.map +1 -0
  155. package/dist/db/tables/requestUsage.js +150 -0
  156. package/dist/db/tables/requestUsage.js.map +1 -0
  157. package/dist/db/tables/sponsoredTransactions.d.ts +119 -0
  158. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  159. package/dist/db/tables/sponsoredTransactions.js +155 -2
  160. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  161. package/dist/db/tables/stripeCustomers.d.ts +81 -0
  162. package/dist/db/tables/stripeCustomers.d.ts.map +1 -0
  163. package/dist/db/tables/stripeCustomers.js +120 -0
  164. package/dist/db/tables/stripeCustomers.js.map +1 -0
  165. package/dist/db/tables/users.js +2 -2
  166. package/dist/db/tables/users.js.map +1 -1
  167. package/dist/handlers/internal/sponsorship.d.ts +5 -2
  168. package/dist/handlers/internal/sponsorship.d.ts.map +1 -1
  169. package/dist/handlers/internal/sponsorship.js +19 -5
  170. package/dist/handlers/internal/sponsorship.js.map +1 -1
  171. package/dist/handlers/internal/utils.d.ts +10 -3
  172. package/dist/handlers/internal/utils.d.ts.map +1 -1
  173. package/dist/handlers/internal/utils.js +14 -6
  174. package/dist/handlers/internal/utils.js.map +1 -1
  175. package/dist/index.d.ts +1 -0
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +1 -0
  178. package/dist/index.js.map +1 -1
  179. package/dist/internal/Auth.d.ts +18 -4
  180. package/dist/internal/Auth.d.ts.map +1 -1
  181. package/dist/internal/Auth.js +148 -76
  182. package/dist/internal/Auth.js.map +1 -1
  183. package/dist/internal/Cache.d.ts.map +1 -1
  184. package/dist/internal/Cache.js +4 -1
  185. package/dist/internal/Cache.js.map +1 -1
  186. package/dist/internal/EdgeCache.d.ts +10 -0
  187. package/dist/internal/EdgeCache.d.ts.map +1 -1
  188. package/dist/internal/EdgeCache.js +39 -8
  189. package/dist/internal/EdgeCache.js.map +1 -1
  190. package/dist/internal/Fees.d.ts +27 -0
  191. package/dist/internal/Fees.d.ts.map +1 -0
  192. package/dist/internal/Fees.js +48 -0
  193. package/dist/internal/Fees.js.map +1 -0
  194. package/dist/internal/FundingRoutes.d.ts +183 -0
  195. package/dist/internal/FundingRoutes.d.ts.map +1 -0
  196. package/dist/internal/FundingRoutes.js +490 -0
  197. package/dist/internal/FundingRoutes.js.map +1 -0
  198. package/dist/internal/Id.d.ts +15 -0
  199. package/dist/internal/Id.d.ts.map +1 -0
  200. package/dist/internal/Id.js +22 -0
  201. package/dist/internal/Id.js.map +1 -0
  202. package/dist/internal/Log.d.ts +2 -0
  203. package/dist/internal/Log.d.ts.map +1 -1
  204. package/dist/internal/Log.js +3 -0
  205. package/dist/internal/Log.js.map +1 -1
  206. package/dist/internal/OpenApi.d.ts +10 -1
  207. package/dist/internal/OpenApi.d.ts.map +1 -1
  208. package/dist/internal/OpenApi.js +8 -1
  209. package/dist/internal/OpenApi.js.map +1 -1
  210. package/dist/internal/RateLimit.d.ts +25 -0
  211. package/dist/internal/RateLimit.d.ts.map +1 -1
  212. package/dist/internal/RateLimit.js +51 -0
  213. package/dist/internal/RateLimit.js.map +1 -1
  214. package/dist/internal/Response.d.ts +1 -1
  215. package/dist/internal/Response.d.ts.map +1 -1
  216. package/dist/internal/Schema.d.ts +8 -0
  217. package/dist/internal/Schema.d.ts.map +1 -1
  218. package/dist/internal/Schema.js +7 -0
  219. package/dist/internal/Schema.js.map +1 -1
  220. package/dist/internal/Webhooks.d.ts.map +1 -1
  221. package/dist/internal/Webhooks.js +3 -3
  222. package/dist/internal/Webhooks.js.map +1 -1
  223. package/package.json +4 -3
  224. package/src/ApiKey.ts +9 -2
  225. package/src/ApiKeys.test.ts +51 -0
  226. package/src/ApiKeys.ts +50 -1
  227. package/src/App.test.ts +33 -86718
  228. package/src/App.ts +18 -0
  229. package/src/Client.test-d.ts +351 -0
  230. package/src/Scope.ts +16 -3
  231. package/src/admin/apps/scopes.test.ts +6 -0
  232. package/src/admin/apps/scopes.ts +3 -0
  233. package/src/admin/ui/src/components/Page.tsx +2 -3
  234. package/src/admin/ui/src/routes/__root.tsx +2 -2
  235. package/src/admin/ui/src/routes/api-keys.tsx +2 -2
  236. package/src/admin/ui.generated.ts +13 -13
  237. package/src/analytics/Analytics.test.ts +527 -0
  238. package/src/analytics/Analytics.ts +318 -0
  239. package/src/analytics/tables/requestEvents.ts +523 -0
  240. package/src/apps/data/App.test.ts +72 -0
  241. package/src/apps/data/App.ts +35 -0
  242. package/src/apps/data/routes/funding.test.ts +214 -0
  243. package/src/apps/data/routes/funding.ts +584 -0
  244. package/src/apps/data/routes/receipts.ts +2 -9
  245. package/src/apps/data/routes/rpc.test.ts +78 -2
  246. package/src/apps/data/routes/rpc.ts +31 -1
  247. package/src/apps/data/routes/webhooks.test.ts +1 -1
  248. package/src/apps/data/routes/webhooks.ts +11 -8
  249. package/src/apps/docs/App.ts +29 -100
  250. package/src/apps/docs/reference.ts +132 -0
  251. package/src/apps/index.ts +1 -0
  252. package/src/apps/mcp.test.ts +206 -0
  253. package/src/apps/mcp.ts +250 -0
  254. package/src/apps/platform/App.ts +43 -4
  255. package/src/apps/platform/Billing.test.ts +530 -0
  256. package/src/apps/platform/Billing.ts +682 -0
  257. package/src/apps/platform/index.ts +1 -0
  258. package/src/apps/platform/routes/api-keys.test.ts +172 -9
  259. package/src/apps/platform/routes/api-keys.ts +136 -20
  260. package/src/apps/platform/routes/billing.test.ts +1100 -0
  261. package/src/apps/platform/routes/billing.ts +966 -0
  262. package/src/apps/platform/routes/invitations.test.ts +55 -7
  263. package/src/apps/platform/routes/invitations.ts +68 -0
  264. package/src/apps/platform/routes/me.test.ts +230 -0
  265. package/src/apps/platform/routes/orgs.ts +26 -4
  266. package/src/apps/platform/routes/scopes.test.ts +29 -0
  267. package/src/apps/platform/routes/scopes.ts +85 -0
  268. package/src/apps/platform/routes/usage.test.ts +422 -0
  269. package/src/apps/platform/routes/usage.ts +527 -0
  270. package/src/apps/relay/App.test.ts +457 -8
  271. package/src/apps/relay/App.ts +120 -12
  272. package/src/apps/relay/Sponsorships.test.ts +1 -1
  273. package/src/apps/relay/Sponsorships.ts +2 -1
  274. package/src/cli/Cli.test.ts +25 -2
  275. package/src/cli/Cli.ts +17 -2
  276. package/src/cli/commands/admin.test.ts +83 -0
  277. package/src/cli/commands/admin.ts +89 -1
  278. package/src/db/Db.ts +137 -0
  279. package/src/db/Schema.ts +104 -2
  280. package/src/db/tables/billingSettings.test.ts +87 -0
  281. package/src/db/tables/billingSettings.ts +108 -0
  282. package/src/db/tables/invitations.ts +2 -2
  283. package/src/db/tables/organizations.test.ts +1 -1
  284. package/src/db/tables/organizations.ts +15 -5
  285. package/src/db/tables/projects.ts +2 -2
  286. package/src/db/tables/requestUsage.ts +231 -0
  287. package/src/db/tables/sponsoredTransactions.test.ts +108 -1
  288. package/src/db/tables/sponsoredTransactions.ts +230 -2
  289. package/src/db/tables/stripeCustomers.test.ts +109 -0
  290. package/src/db/tables/stripeCustomers.ts +162 -0
  291. package/src/db/tables/users.ts +2 -2
  292. package/src/handlers/internal/sponsorship.ts +34 -7
  293. package/src/handlers/internal/utils.ts +19 -7
  294. package/src/index.ts +1 -0
  295. package/src/internal/Auth.test.ts +175 -25
  296. package/src/internal/Auth.ts +175 -86
  297. package/src/internal/Cache.ts +5 -1
  298. package/src/internal/EdgeCache.test.ts +27 -4
  299. package/src/internal/EdgeCache.ts +47 -9
  300. package/src/internal/Fees.test.ts +32 -0
  301. package/src/internal/Fees.ts +51 -0
  302. package/src/internal/FundingRoutes.ts +583 -0
  303. package/src/internal/Id.test.ts +17 -0
  304. package/src/internal/Id.ts +24 -0
  305. package/src/internal/Log.test.ts +1 -0
  306. package/src/internal/Log.ts +5 -0
  307. package/src/internal/OpenApi.ts +12 -1
  308. package/src/internal/RateLimit.test.ts +84 -0
  309. package/src/internal/RateLimit.ts +70 -0
  310. package/src/internal/Response.ts +1 -1
  311. package/src/internal/Schema.ts +8 -0
  312. package/src/internal/Webhooks.test.ts +3 -3
  313. package/src/internal/Webhooks.ts +3 -3
@@ -76,13 +76,13 @@ describe('invitations', () => {
76
76
  expect(sent).toHaveLength(1)
77
77
  expect(sent[0]!.to).toBe('dev@example.com')
78
78
  expect(sent[0]!.subject).toContain('Acme')
79
- expect(sent[0]!.text).toMatchInlineSnapshot(`
80
- "You've been invited to join Acme on the Tempo Developer Platform
81
-
82
- Sign in at https://console.tempo.xyz/?email=dev%40example.com to accept. This invitation expires in 7 days."
83
- `)
79
+ expect(sent[0]!.text).toBe(
80
+ `You've been invited to join Acme on the Tempo Developer Platform\n\nSign in at https://console.tempo.xyz/?email=dev%40example.com&invitation=${invitation.id} to accept. This invitation expires in 7 days.`,
81
+ )
84
82
  expect(sent[0]!.html).toContain('<strong>Acme</strong>')
85
- expect(sent[0]!.html).toContain('https://console.tempo.xyz/?email=dev%40example.com')
83
+ expect(sent[0]!.html).toContain(
84
+ `https://console.tempo.xyz/?email=dev%40example.com&invitation=${invitation.id}`,
85
+ )
86
86
 
87
87
  // Invisible without a verified email; visible once it matches.
88
88
  const invitee = await session(app)
@@ -260,6 +260,46 @@ describe('invitations', () => {
260
260
  expect(acceptExpired.status).toBe(404)
261
261
  })
262
262
 
263
+ test('declining settles the invitation', async () => {
264
+ const db = TestApp.database()
265
+ const app = createApp({ db })
266
+ const owner = await session(app)
267
+ const created = await app.request('/v1/orgs', json('POST', owner.cookie, { name: 'Acme' }))
268
+ const org = await TestApp.json(created, Orgs.schema.Organization)
269
+ const invited = await app.request(
270
+ `/v1/orgs/${org.id}/invitations`,
271
+ json('POST', owner.cookie, { email: 'dev@example.com', role: 'member' }),
272
+ )
273
+ const invitation = await TestApp.json(invited, InvitationsApp.schema.Invitation)
274
+
275
+ const invitee = await session(app)
276
+ await Users.setEmail(db, invitee.userId, 'dev@example.com')
277
+ const declined = await app.request(`/v1/invitations/${invitation.id}/decline`, {
278
+ headers: { cookie: invitee.cookie },
279
+ method: 'POST',
280
+ })
281
+ expect(declined.status).toBe(200)
282
+ expect(await TestApp.json(declined, InvitationsApp.schema.declineInvitation.Response)).toEqual({
283
+ id: invitation.id,
284
+ })
285
+
286
+ // The pending list drains and the invitation can no longer be accepted or re-declined.
287
+ const mine = await app.request('/v1/invitations', { headers: { cookie: invitee.cookie } })
288
+ expect(
289
+ (await TestApp.json(mine, InvitationsApp.schema.listMyInvitations.Response)).data,
290
+ ).toEqual([])
291
+ const accepted = await app.request(`/v1/invitations/${invitation.id}/accept`, {
292
+ headers: { cookie: invitee.cookie },
293
+ method: 'POST',
294
+ })
295
+ expect(accepted.status).toBe(404)
296
+ const again = await app.request(`/v1/invitations/${invitation.id}/decline`, {
297
+ headers: { cookie: invitee.cookie },
298
+ method: 'POST',
299
+ })
300
+ expect(again.status).toBe(404)
301
+ })
302
+
263
303
  test("hides invitations addressed to someone else's email", async () => {
264
304
  const db = TestApp.database()
265
305
  const app = createApp({ db })
@@ -279,6 +319,11 @@ describe('invitations', () => {
279
319
  method: 'POST',
280
320
  })
281
321
  expect(accepted.status).toBe(404)
322
+ const declined = await app.request(`/v1/invitations/${invitation.id}/decline`, {
323
+ headers: { cookie: other.cookie },
324
+ method: 'POST',
325
+ })
326
+ expect(declined.status).toBe(404)
282
327
  })
283
328
 
284
329
  test('a failing email sender never fails the invite', async () => {
@@ -356,6 +401,9 @@ describe('invitations', () => {
356
401
  json('POST', owner.cookie, { email: 'dev@example.com', role: 'member' }),
357
402
  )
358
403
  expect(invited.status).toBe(200)
359
- expect(sent[0]!.text).toContain('https://portal.selfhost.example/?email=dev%40example.com')
404
+ const invitation = await TestApp.json(invited, InvitationsApp.schema.Invitation)
405
+ expect(sent[0]!.text).toContain(
406
+ `https://portal.selfhost.example/?email=dev%40example.com&invitation=${invitation.id}`,
407
+ )
360
408
  })
361
409
  })
@@ -171,6 +171,21 @@ export namespace schema {
171
171
  )
172
172
  }
173
173
 
174
+ /** Schemas for the declineInvitation operation. */
175
+ export namespace declineInvitation {
176
+ /** Confirmation that the invitation was declined. */
177
+ export const Response = z
178
+ .object({
179
+ id: z
180
+ .string()
181
+ .check(
182
+ z.describe('ID of the invitation that was declined.'),
183
+ z.meta({ examples: ['inv_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
184
+ ),
185
+ })
186
+ .check(z.describe('Confirmation that the invitation was declined.'))
187
+ }
188
+
174
189
  /** Schemas for the revokeInvitation operation. */
175
190
  export namespace revokeInvitation {
176
191
  /** Confirmation that the invitation was revoked. */
@@ -450,6 +465,57 @@ export function invitations() {
450
465
  }
451
466
  },
452
467
  )
468
+ .post(
469
+ '/v1/invitations/:invitationId{inv_[A-Za-z0-9_-]+}/decline',
470
+ Auth.policy({ session: true }),
471
+ OpenApi.validate('param', schema.AcceptParams, {
472
+ code: 'param_invalid',
473
+ message: 'Check the path parameters and try again.',
474
+ }),
475
+ OpenApi.describeRoute({
476
+ description: "Decline an invitation addressed to the session's verified email. Declining settles the invitation; it can no longer be accepted.", // prettier-ignore
477
+ operationId: 'declineInvitation',
478
+ responses: OpenApi.responses({
479
+ errors: { 404: 'No accessible pending invitation was found for this id.' },
480
+ success: {
481
+ description: 'Confirmation that the invitation was declined.',
482
+ schema: schema.declineInvitation.Response,
483
+ },
484
+ }),
485
+ summary: 'Decline invitation',
486
+ tags: ['Invitations'],
487
+ }),
488
+ async (c) => {
489
+ if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
490
+ if (OpenApi.narrowValidation)
491
+ return OpenApi.validationError(c, {
492
+ code: 'param_invalid',
493
+ message: 'Check the path parameters and try again.',
494
+ })
495
+
496
+ const principal = Auth.getPrincipal(c)
497
+ if (principal?.type !== 'session') return sessionRequired(c)
498
+
499
+ const { invitationId } = c.req.valid('param')
500
+ const db = Db.get(c.get('db'))
501
+ try {
502
+ // An invitation is visible only to the verified holder of its email;
503
+ // everyone else sees 404.
504
+ const record = await Invitations.get(db, invitationId)
505
+ if (!record || record.email !== principal.email?.toLowerCase())
506
+ return invitationNotFound(c)
507
+ // Declining reuses the revoke settle; no surface distinguishes the two.
508
+ const declined = await Invitations.revoke(db, invitationId)
509
+ if (!declined) return invitationNotFound(c)
510
+ return c.json(
511
+ Response.validated(schema.declineInvitation.Response, { id: invitationId }),
512
+ 200,
513
+ )
514
+ } catch (cause) {
515
+ return Response.upstream(c, cause)
516
+ }
517
+ },
518
+ )
453
519
  }
454
520
 
455
521
  /** Dispatches the invitation email through the app's sender, best-effort. */
@@ -463,6 +529,8 @@ function dispatchEmail(
463
529
  const expiry = days === 1 ? '1 day' : `${days} days`
464
530
  const link = new URL(sender.consoleUrl ?? 'https://console.tempo.xyz')
465
531
  link.searchParams.set('email', record.email)
532
+ // The console's index route steers `?invitation=` to the accept screen.
533
+ link.searchParams.set('invitation', record.id)
466
534
  const url = link.href
467
535
  const dispatch = Promise.resolve(
468
536
  sender.send({
@@ -1,3 +1,6 @@
1
+ import { serve } from '@hono/node-server'
2
+ import * as Accounts from 'accounts/server'
3
+ import { exportJWK, generateKeyPair } from 'jose'
1
4
  import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
2
5
 
3
6
  import * as TestApp from '../../../../test/App.js'
@@ -77,6 +80,24 @@ describe('GET /me', () => {
77
80
  expect(response.status).toBe(401)
78
81
  })
79
82
 
83
+ test('logout revokes bearer-token sessions', async () => {
84
+ const app = createApp()
85
+ const account = privateKeyToAccount(generatePrivateKey())
86
+ const { verifyResponse } = await TestApp.signIn(app, account, { returnToken: true })
87
+ const { token } = (await verifyResponse.json()) as { token: string }
88
+
89
+ const logout = await app.request('/v1/auth/siwe/logout', {
90
+ headers: { authorization: `Bearer ${token}` },
91
+ method: 'POST',
92
+ })
93
+ expect(logout.status).toBe(204)
94
+
95
+ const response = await app.request('/v1/me', {
96
+ headers: { authorization: `Bearer ${token}` },
97
+ })
98
+ expect(response.status).toBe(401)
99
+ })
100
+
80
101
  test('rejects anonymous requests', async () => {
81
102
  const app = createApp()
82
103
  const response = await app.request('/v1/me')
@@ -119,3 +140,212 @@ describe('GET /me', () => {
119
140
  `)
120
141
  })
121
142
  })
143
+
144
+ describe('sign-in email requirement', () => {
145
+ test('rejects sign-ins without an identity token', async () => {
146
+ const app = TestApp.create({
147
+ auth: { keys: [TestApp.key] },
148
+ session: { wallet: { origin, requireEmail: true } },
149
+ })
150
+ const account = privateKeyToAccount(generatePrivateKey())
151
+ const { cookie, verifyResponse } = await TestApp.signIn(app, account)
152
+
153
+ expect(verifyResponse.status).toBe(400)
154
+ expect(cookie).toBeUndefined()
155
+ expect(await verifyResponse.json()).toMatchInlineSnapshot(`
156
+ {
157
+ "error": "identity token required",
158
+ }
159
+ `)
160
+ })
161
+
162
+ test('folds the token-verified email onto the session', async () => {
163
+ const oidc = await createIssuer({ email: 'dev@example.com' })
164
+ try {
165
+ const app = TestApp.create({
166
+ auth: { keys: [TestApp.key] },
167
+ session: { wallet: { issuer: oidc.url, origin, requireEmail: true } },
168
+ })
169
+ const account = privateKeyToAccount(generatePrivateKey())
170
+ const { cookie, verifyResponse } = await TestApp.signIn(app, account, {
171
+ idToken: ({ nonce }) => oidc.mint({ address: account.address, nonce }),
172
+ })
173
+
174
+ expect(verifyResponse.status).toBe(200)
175
+ const response = await app.request('/v1/me', { headers: { cookie: cookie! } })
176
+ expect(response.status).toBe(200)
177
+ const body = await TestApp.json(response, Me.schema.User)
178
+ expect(body.email).toBe('dev@example.com')
179
+ } finally {
180
+ await oidc.close()
181
+ }
182
+ })
183
+ })
184
+
185
+ describe('sign-up email domains', () => {
186
+ test('allows new users on an allowlisted domain', async () => {
187
+ const oidc = await createIssuer({ email: 'dev@TEMPO.xyz' })
188
+ try {
189
+ const app = TestApp.create({
190
+ auth: { keys: [TestApp.key] },
191
+ session: { emailAllowlist: ['Tempo.XYZ'], wallet: { issuer: oidc.url, origin } },
192
+ })
193
+ const account = privateKeyToAccount(generatePrivateKey())
194
+ const { cookie, verifyResponse } = await TestApp.signIn(app, account, {
195
+ idToken: ({ nonce }) => oidc.mint({ address: account.address, nonce }),
196
+ })
197
+
198
+ expect(verifyResponse.status).toBe(200)
199
+ const response = await app.request('/v1/me', { headers: { cookie: cookie! } })
200
+ expect(response.status).toBe(200)
201
+ const body = await TestApp.json(response, Me.schema.User)
202
+ expect(body.email).toBe('dev@TEMPO.xyz')
203
+ } finally {
204
+ await oidc.close()
205
+ }
206
+ })
207
+
208
+ test('rejects new users on other domains', async () => {
209
+ const oidc = await createIssuer({ email: 'dev@example.com' })
210
+ try {
211
+ const app = TestApp.create({
212
+ auth: { keys: [TestApp.key] },
213
+ session: { emailAllowlist: ['tempo.xyz'], wallet: { issuer: oidc.url, origin } },
214
+ })
215
+ const account = privateKeyToAccount(generatePrivateKey())
216
+ const { cookie, verifyResponse } = await TestApp.signIn(app, account, {
217
+ idToken: ({ nonce }) => oidc.mint({ address: account.address, nonce }),
218
+ })
219
+
220
+ expect(verifyResponse.status).toBe(403)
221
+ expect(cookie).toBeUndefined()
222
+ expect(await verifyResponse.json()).toMatchInlineSnapshot(`
223
+ {
224
+ "error": "sign-ups are restricted to approved email domains or invited emails",
225
+ }
226
+ `)
227
+ } finally {
228
+ await oidc.close()
229
+ }
230
+ })
231
+
232
+ test('rejects new users without an identity token', async () => {
233
+ const app = TestApp.create({
234
+ auth: { keys: [TestApp.key] },
235
+ session: { emailAllowlist: ['tempo.xyz'], wallet: { origin } },
236
+ })
237
+ const account = privateKeyToAccount(generatePrivateKey())
238
+ const { cookie, verifyResponse } = await TestApp.signIn(app, account)
239
+
240
+ expect(verifyResponse.status).toBe(403)
241
+ expect(cookie).toBeUndefined()
242
+ })
243
+
244
+ test('allows invited emails regardless of domain', async () => {
245
+ const oidc = await createIssuer({ email: 'Invited@Example.com' })
246
+ try {
247
+ const db = TestApp.database()
248
+
249
+ // Owner signs up while sign-up is open and issues the invitation.
250
+ const open = TestApp.create({
251
+ auth: { keys: [TestApp.key] },
252
+ db,
253
+ session: { wallet: { origin } },
254
+ })
255
+ const owner = privateKeyToAccount(generatePrivateKey())
256
+ const { cookie } = await TestApp.signIn(open, owner)
257
+ const created = await open.request('/v1/orgs', {
258
+ body: JSON.stringify({ name: 'Acme' }),
259
+ headers: { 'content-type': 'application/json', cookie: cookie! },
260
+ method: 'POST',
261
+ })
262
+ const org = (await created.json()) as { id: string }
263
+ const invited = await open.request(`/v1/orgs/${org.id}/invitations`, {
264
+ body: JSON.stringify({ email: 'invited@example.com', role: 'member' }),
265
+ headers: { 'content-type': 'application/json', cookie: cookie! },
266
+ method: 'POST',
267
+ })
268
+ expect(invited.status).toBe(200)
269
+
270
+ const restricted = TestApp.create({
271
+ auth: { keys: [TestApp.key] },
272
+ db,
273
+ session: { emailAllowlist: ['tempo.xyz'], wallet: { issuer: oidc.url, origin } },
274
+ })
275
+ const account = privateKeyToAccount(generatePrivateKey())
276
+ const { verifyResponse } = await TestApp.signIn(restricted, account, {
277
+ idToken: ({ nonce }) => oidc.mint({ address: account.address, nonce }),
278
+ })
279
+ expect(verifyResponse.status).toBe(200)
280
+ } finally {
281
+ await oidc.close()
282
+ }
283
+ })
284
+
285
+ test('allows existing users regardless of domain', async () => {
286
+ const db = TestApp.database()
287
+ const account = privateKeyToAccount(generatePrivateKey())
288
+
289
+ // Row created before the restriction (open sign-up deployment).
290
+ const open = TestApp.create({
291
+ auth: { keys: [TestApp.key] },
292
+ db,
293
+ session: { wallet: { origin } },
294
+ })
295
+ const first = await TestApp.signIn(open, account)
296
+ expect(first.verifyResponse.status).toBe(200)
297
+
298
+ const restricted = TestApp.create({
299
+ auth: { keys: [TestApp.key] },
300
+ db,
301
+ session: { emailAllowlist: ['tempo.xyz'], wallet: { origin } },
302
+ })
303
+ const second = await TestApp.signIn(restricted, account)
304
+ expect(second.verifyResponse.status).toBe(200)
305
+ })
306
+ })
307
+
308
+ /** Local OIDC issuer standing in for the wallet: the SDK's own provider (discovery, JWKS, token mint). */
309
+ async function createIssuer(options: { email: string }) {
310
+ // Extractable: the provider takes key material as JWK strings.
311
+ const { privateKey, publicKey } = await generateKeyPair('EdDSA', { extractable: true })
312
+ // The provider pins its `issuer` URL at construction, but the ephemeral
313
+ // port is only known after bind — listen through a closure, assign after.
314
+ let provider!: ReturnType<typeof Accounts.Handler.oidcProvider>
315
+ type Listener = { close: () => Promise<void>; url: string }
316
+ const { close, url } = await new Promise<Listener>((resolve) => {
317
+ const server = serve({ fetch: (request) => provider.fetch(request), port: 0 }, (info) =>
318
+ resolve({
319
+ close: () => new Promise((done) => server.close(() => done())),
320
+ url: `http://127.0.0.1:${info.port}`,
321
+ }),
322
+ )
323
+ })
324
+ provider = Accounts.Handler.oidcProvider({
325
+ // Test-only: no `authenticate`, so the request body's `subject` is trusted.
326
+ getClaims: () => ({ email: options.email, email_verified: true }),
327
+ issuer: url,
328
+ publicKey: JSON.stringify(await exportJWK(publicKey)),
329
+ signingKey: JSON.stringify(await exportJWK(privateKey)),
330
+ })
331
+ return {
332
+ close,
333
+ /** Mints a wallet-style identity token bound to the SIWE nonce and signer. */
334
+ async mint(mintOptions: { address: string; nonce: string }) {
335
+ const response = await provider.fetch(
336
+ new Request(`${url}/token`, {
337
+ body: JSON.stringify({
338
+ audience: origin,
339
+ nonce: mintOptions.nonce,
340
+ subject: mintOptions.address,
341
+ }),
342
+ headers: { 'content-type': 'application/json' },
343
+ method: 'POST',
344
+ }),
345
+ )
346
+ const { idToken } = (await response.json()) as { idToken: string }
347
+ return idToken
348
+ },
349
+ url,
350
+ }
351
+ }
@@ -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 Memberships from '../../../db/tables/memberships.js'
8
9
  import * as OpenApi from '../../../internal/OpenApi.js'
9
10
  import * as Organizations from '../../../db/tables/organizations.js'
10
11
  import * as Response from '../../../internal/Response.js'
@@ -42,6 +43,12 @@ export namespace schema {
42
43
  z.describe('Human-readable organization name.'),
43
44
  z.meta({ examples: ['Acme, Inc.'] }),
44
45
  ),
46
+ role: z
47
+ .optional(z.enum(Memberships.roles))
48
+ .check(
49
+ z.describe("The caller's role in this organization; absent for the super admin."),
50
+ z.meta({ examples: ['owner'] }),
51
+ ),
45
52
  updatedAt: z
46
53
  .string()
47
54
  .check(
@@ -181,7 +188,13 @@ export function orgs() {
181
188
  const record =
182
189
  principal.type === 'super_admin'
183
190
  ? await Organizations.create(db, { createdBy: 'super_admin', name: body.name })
184
- : await Organizations.createOwned(db, { name: body.name, userId: principal.id })
191
+ : {
192
+ ...(await Organizations.createOwned(db, {
193
+ name: body.name,
194
+ userId: principal.id,
195
+ })),
196
+ role: 'owner' as const,
197
+ }
185
198
  return c.json(Response.validated(schema.Organization, serializeOrganization(record)), 200)
186
199
  } catch (cause) {
187
200
  return Response.upstream(c, cause)
@@ -215,7 +228,10 @@ export function orgs() {
215
228
  })
216
229
 
217
230
  return c.json(
218
- Response.validated(schema.Organization, serializeOrganization(Auth.org(c))),
231
+ Response.validated(
232
+ schema.Organization,
233
+ serializeOrganization({ ...Auth.org(c), role: Auth.membership(c)?.role }),
234
+ ),
219
235
  200,
220
236
  )
221
237
  },
@@ -260,7 +276,10 @@ export function orgs() {
260
276
  const updated = await Organizations.update(db, Auth.org(c).id, { name: body.name })
261
277
  if (!updated) return organizationNotFound(c)
262
278
  return c.json(
263
- Response.validated(schema.Organization, serializeOrganization(updated)),
279
+ Response.validated(
280
+ schema.Organization,
281
+ serializeOrganization({ ...updated, role: Auth.membership(c)?.role }),
282
+ ),
264
283
  200,
265
284
  )
266
285
  } catch (cause) {
@@ -317,11 +336,14 @@ export function orgs() {
317
336
  )
318
337
  }
319
338
 
320
- function serializeOrganization(record: Organizations.Record) {
339
+ function serializeOrganization(
340
+ record: Organizations.Record & { role?: Memberships.Role | undefined },
341
+ ) {
321
342
  return {
322
343
  createdAt: record.createdAt,
323
344
  id: record.id,
324
345
  name: record.name,
346
+ ...(record.role ? { role: record.role } : {}),
325
347
  updatedAt: record.updatedAt,
326
348
  }
327
349
  }
@@ -0,0 +1,29 @@
1
+ import * as Scope from '../../../Scope.js'
2
+ import * as TestApp from '../../../../test/App.js'
3
+ import * as Scopes from './scopes.js'
4
+
5
+ describe('scopes', () => {
6
+ test('lists the scope catalog without authentication (public catalog)', async () => {
7
+ const app = TestApp.create()
8
+ const response = await app.request('/v1/scopes')
9
+ const body = await TestApp.json(response, Scopes.schema.getScopes.Response)
10
+
11
+ expect(response.status).toMatchInlineSnapshot(`200`)
12
+ // Static public data must be cacheable (`stable`); without it every
13
+ // request pays the full auth/rate-limit floor.
14
+ expect(response.headers.get('cache-control')).toMatchInlineSnapshot(
15
+ `"private, max-age=300, stale-while-revalidate=3600"`,
16
+ )
17
+ expect(body.data).toEqual(Scope.catalog.map((entry) => ({ ...entry })))
18
+ expect(body.data.map((entry) => entry.scope)).toMatchInlineSnapshot(`
19
+ [
20
+ "data:read",
21
+ "indexer:query",
22
+ "rpc-relay:read",
23
+ "rpc-relay:sponsor",
24
+ "webhooks:read",
25
+ "webhooks:write",
26
+ ]
27
+ `)
28
+ })
29
+ })
@@ -0,0 +1,85 @@
1
+ import { Hono } from 'hono'
2
+ import * 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 Cache from '../../../internal/Cache.js'
7
+ import * as OpenApi from '../../../internal/OpenApi.js'
8
+ import * as Response from '../../../internal/Response.js'
9
+ import * as Scope from '../../../Scope.js'
10
+
11
+ /** Zod schemas owned by the scope-catalog resource. */
12
+ export namespace schema {
13
+ /** Schemas for the getScopes operation. */
14
+ export namespace getScopes {
15
+ /** A single issuable API-key scope. */
16
+ export const Entry = z
17
+ .object({
18
+ description: z
19
+ .string()
20
+ .check(
21
+ z.describe('Human-readable explanation of what the scope grants.'),
22
+ z.meta({ examples: ['Read webhook subscriptions.'] }),
23
+ ),
24
+ scope: z
25
+ .enum(Scope.catalog.map((entry) => entry.scope))
26
+ .check(
27
+ z.describe('Scope identifier used in API-key grants and route policies.'),
28
+ z.meta({ examples: ['data:read'] }),
29
+ ),
30
+ selfServe: z
31
+ .boolean()
32
+ .check(
33
+ z.describe('Whether a session may self-mint this scope (else super-admin-only).'),
34
+ z.meta({ examples: [true] }),
35
+ ),
36
+ })
37
+ .check(z.describe('A single issuable API-key scope.'))
38
+
39
+ /** Response body for the scope catalog. */
40
+ export const Response = z
41
+ .object({
42
+ data: z.array(Entry).check(z.describe('Issuable API-key scopes.')),
43
+ })
44
+ .check(z.describe('Response body for the scope catalog.'))
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Mounts the `/scopes` resource: the issuable API-key scope vocabulary, served
50
+ * from {@link Scope.catalog}. Public catalog data: anonymous callers are served
51
+ * within the public quota; the session lane lets signed-in consoles skip it.
52
+ */
53
+ export function scopes() {
54
+ return new Hono<App.Environment>().get(
55
+ '/v1/scopes',
56
+ Auth.policy({ public: true, session: true }),
57
+ OpenApi.describeRoute({
58
+ description: 'See which scopes can be granted to API keys.',
59
+ operationId: 'getScopes',
60
+ responses: OpenApi.responses({
61
+ success: { description: 'Issuable scope catalog.', schema: schema.getScopes.Response },
62
+ }),
63
+ summary: 'List scopes',
64
+ tags: ['API Keys'],
65
+ }),
66
+ Cache.response<App.Environment>({
67
+ cacheControl: Cache.policies.stable,
68
+ name: 'tempo-api:scopes:v1',
69
+ key: (c) => {
70
+ const url = new URL(c.req.url)
71
+ url.search = ''
72
+ return url.toString()
73
+ },
74
+ }),
75
+ (c) => {
76
+ if (Auth.narrowAccess) return Auth.accessError(c)
77
+ return c.json(
78
+ Response.validated(schema.getScopes.Response, {
79
+ data: Scope.catalog.map((entry) => ({ ...entry })),
80
+ }),
81
+ 200,
82
+ )
83
+ },
84
+ )
85
+ }