tapimo 0.4.0 → 0.4.3

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 (255) hide show
  1. package/README.md +1 -1
  2. package/dist/ApiKey.d.ts +1 -9
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +2 -3
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +53 -27
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +32 -17
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +2451 -1533
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +23 -9
  13. package/dist/App.js.map +1 -1
  14. package/dist/Assets.d.ts +10 -0
  15. package/dist/Assets.d.ts.map +1 -1
  16. package/dist/Assets.js +4 -0
  17. package/dist/Assets.js.map +1 -1
  18. package/dist/Auth.d.ts +2 -1
  19. package/dist/Auth.d.ts.map +1 -1
  20. package/dist/Auth.js +2 -1
  21. package/dist/Auth.js.map +1 -1
  22. package/dist/Scope.d.ts +40 -15
  23. package/dist/Scope.d.ts.map +1 -1
  24. package/dist/Scope.js +44 -13
  25. package/dist/Scope.js.map +1 -1
  26. package/dist/admin/App.d.ts +560 -9
  27. package/dist/admin/App.d.ts.map +1 -1
  28. package/dist/admin/App.js +38 -6
  29. package/dist/admin/App.js.map +1 -1
  30. package/dist/admin/apps/api-keys.d.ts +10 -24
  31. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  32. package/dist/admin/apps/api-keys.js +10 -3
  33. package/dist/admin/apps/api-keys.js.map +1 -1
  34. package/dist/admin/apps/organizations.d.ts +833 -0
  35. package/dist/admin/apps/organizations.d.ts.map +1 -0
  36. package/dist/admin/apps/organizations.js +733 -0
  37. package/dist/admin/apps/organizations.js.map +1 -0
  38. package/dist/admin/apps/scopes.d.ts +8 -0
  39. package/dist/admin/apps/scopes.d.ts.map +1 -1
  40. package/dist/admin/ui.generated.d.ts +1 -1
  41. package/dist/admin/ui.generated.d.ts.map +1 -1
  42. package/dist/admin/ui.generated.js +53 -13
  43. package/dist/admin/ui.generated.js.map +1 -1
  44. package/dist/analytics/Analytics.d.ts +10 -0
  45. package/dist/analytics/Analytics.d.ts.map +1 -1
  46. package/dist/analytics/Analytics.js +8 -0
  47. package/dist/analytics/Analytics.js.map +1 -1
  48. package/dist/analytics/tables/requestEvents.d.ts +9 -0
  49. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  50. package/dist/analytics/tables/requestEvents.js +14 -0
  51. package/dist/analytics/tables/requestEvents.js.map +1 -1
  52. package/dist/apps/data/App.d.ts +1354 -984
  53. package/dist/apps/data/App.d.ts.map +1 -1
  54. package/dist/apps/data/App.js +20 -3
  55. package/dist/apps/data/App.js.map +1 -1
  56. package/dist/apps/data/FundingRouteProviders.d.ts +94 -5
  57. package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
  58. package/dist/apps/data/FundingRouteProviders.js +196 -5
  59. package/dist/apps/data/FundingRouteProviders.js.map +1 -1
  60. package/dist/apps/data/routes/activities.d.ts +38 -38
  61. package/dist/apps/data/routes/addresses.d.ts +24 -24
  62. package/dist/apps/data/routes/balances.d.ts +45 -45
  63. package/dist/apps/data/routes/blocks.d.ts +39 -39
  64. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  65. package/dist/apps/data/routes/exchanges.d.ts +453 -453
  66. package/dist/apps/data/routes/fee-amm.d.ts +101 -101
  67. package/dist/apps/data/routes/funding.d.ts +730 -138
  68. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  69. package/dist/apps/data/routes/funding.js +234 -0
  70. package/dist/apps/data/routes/funding.js.map +1 -1
  71. package/dist/apps/data/routes/indexer.d.ts +5 -5
  72. package/dist/apps/data/routes/rpc.d.ts +31 -31
  73. package/dist/apps/data/routes/verified-tokens.d.ts +81 -81
  74. package/dist/apps/management/App.d.ts +1156 -615
  75. package/dist/apps/management/App.d.ts.map +1 -1
  76. package/dist/apps/management/App.js +2 -2
  77. package/dist/apps/management/Billing.d.ts +3 -0
  78. package/dist/apps/management/Billing.d.ts.map +1 -1
  79. package/dist/apps/management/Billing.js +7 -2
  80. package/dist/apps/management/Billing.js.map +1 -1
  81. package/dist/apps/management/routes/api-keys.d.ts +106 -90
  82. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  83. package/dist/apps/management/routes/api-keys.js +41 -30
  84. package/dist/apps/management/routes/api-keys.js.map +1 -1
  85. package/dist/apps/management/routes/billing.d.ts +278 -146
  86. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  87. package/dist/apps/management/routes/billing.js +14 -14
  88. package/dist/apps/management/routes/billing.js.map +1 -1
  89. package/dist/apps/management/routes/invitations.d.ts +93 -20
  90. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  91. package/dist/apps/management/routes/invitations.js +12 -15
  92. package/dist/apps/management/routes/invitations.js.map +1 -1
  93. package/dist/apps/management/routes/me.d.ts +13 -13
  94. package/dist/apps/management/routes/members.d.ts +89 -18
  95. package/dist/apps/management/routes/members.d.ts.map +1 -1
  96. package/dist/apps/management/routes/members.js +10 -10
  97. package/dist/apps/management/routes/members.js.map +1 -1
  98. package/dist/apps/management/routes/orgs.d.ts +130 -70
  99. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  100. package/dist/apps/management/routes/orgs.js +19 -19
  101. package/dist/apps/management/routes/orgs.js.map +1 -1
  102. package/dist/apps/management/routes/projects.d.ts +137 -48
  103. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  104. package/dist/apps/management/routes/projects.js +12 -13
  105. package/dist/apps/management/routes/projects.js.map +1 -1
  106. package/dist/apps/management/routes/scopes.d.ts +14 -28
  107. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  108. package/dist/apps/management/routes/scopes.js +6 -7
  109. package/dist/apps/management/routes/scopes.js.map +1 -1
  110. package/dist/apps/management/routes/usage.d.ts +63 -19
  111. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  112. package/dist/apps/management/routes/usage.js +9 -10
  113. package/dist/apps/management/routes/usage.js.map +1 -1
  114. package/dist/apps/mcp.d.ts.map +1 -1
  115. package/dist/apps/mcp.js +35 -3
  116. package/dist/apps/mcp.js.map +1 -1
  117. package/dist/apps/relay/App.d.ts +26 -4
  118. package/dist/apps/relay/App.d.ts.map +1 -1
  119. package/dist/apps/relay/App.js +17 -14
  120. package/dist/apps/relay/App.js.map +1 -1
  121. package/dist/db/Db.d.ts +5 -3
  122. package/dist/db/Db.d.ts.map +1 -1
  123. package/dist/db/Db.js +52 -0
  124. package/dist/db/Db.js.map +1 -1
  125. package/dist/db/Schema.d.ts +13 -0
  126. package/dist/db/Schema.d.ts.map +1 -1
  127. package/dist/db/Schema.js +16 -1
  128. package/dist/db/Schema.js.map +1 -1
  129. package/dist/db/tables/adminAuditLogs.d.ts +27 -0
  130. package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
  131. package/dist/db/tables/adminAuditLogs.js +19 -0
  132. package/dist/db/tables/adminAuditLogs.js.map +1 -0
  133. package/dist/db/tables/invitations.d.ts +1 -1
  134. package/dist/db/tables/invitations.d.ts.map +1 -1
  135. package/dist/db/tables/memberships.d.ts +32 -4
  136. package/dist/db/tables/memberships.d.ts.map +1 -1
  137. package/dist/db/tables/memberships.js +31 -0
  138. package/dist/db/tables/memberships.js.map +1 -1
  139. package/dist/db/tables/organizations.d.ts +43 -1
  140. package/dist/db/tables/organizations.d.ts.map +1 -1
  141. package/dist/db/tables/organizations.js +103 -0
  142. package/dist/db/tables/organizations.js.map +1 -1
  143. package/dist/db/tables/projects.d.ts +25 -0
  144. package/dist/db/tables/projects.d.ts.map +1 -1
  145. package/dist/db/tables/projects.js +22 -0
  146. package/dist/db/tables/projects.js.map +1 -1
  147. package/dist/db/tables/webhookDeliveries.d.ts +26 -0
  148. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  149. package/dist/db/tables/webhookDeliveries.js +37 -0
  150. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  151. package/dist/handlers/relay.d.ts +13 -10
  152. package/dist/handlers/relay.d.ts.map +1 -1
  153. package/dist/handlers/relay.js +46 -16
  154. package/dist/handlers/relay.js.map +1 -1
  155. package/dist/internal/Auth.d.ts +13 -18
  156. package/dist/internal/Auth.d.ts.map +1 -1
  157. package/dist/internal/Auth.js +78 -58
  158. package/dist/internal/Auth.js.map +1 -1
  159. package/dist/internal/Cursor.d.ts +1 -1
  160. package/dist/internal/Cursor.d.ts.map +1 -1
  161. package/dist/internal/Cursor.js +4 -0
  162. package/dist/internal/Cursor.js.map +1 -1
  163. package/dist/internal/EdgeCache.d.ts.map +1 -1
  164. package/dist/internal/EdgeCache.js +2 -0
  165. package/dist/internal/EdgeCache.js.map +1 -1
  166. package/dist/internal/FundingRoutes.d.ts +170 -0
  167. package/dist/internal/FundingRoutes.d.ts.map +1 -1
  168. package/dist/internal/FundingRoutes.js +68 -6
  169. package/dist/internal/FundingRoutes.js.map +1 -1
  170. package/dist/internal/Log.d.ts +16 -1
  171. package/dist/internal/Log.d.ts.map +1 -1
  172. package/dist/internal/Log.js +5 -3
  173. package/dist/internal/Log.js.map +1 -1
  174. package/package.json +2 -2
  175. package/src/ApiKey.ts +2 -3
  176. package/src/ApiKeys.ts +95 -17
  177. package/src/App.test.ts +44 -2
  178. package/src/App.ts +29 -9
  179. package/src/Assets.ts +8 -0
  180. package/src/Auth.test-d.ts +36 -0
  181. package/src/Auth.ts +20 -1
  182. package/src/Client.test-d.ts +64 -17
  183. package/src/Scope.test.ts +34 -0
  184. package/src/Scope.ts +53 -17
  185. package/src/admin/App.test.ts +33 -3
  186. package/src/admin/App.ts +49 -14
  187. package/src/admin/apps/api-keys.ts +10 -3
  188. package/src/admin/apps/organizations.test.ts +455 -0
  189. package/src/admin/apps/organizations.ts +953 -0
  190. package/src/admin/apps/scopes.test.ts +10 -0
  191. package/src/admin/ui/plugin.ts +1 -1
  192. package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
  193. package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
  194. package/src/admin/ui/src/lib/api-keys.ts +36 -19
  195. package/src/admin/ui/src/lib/config.ts +2 -3
  196. package/src/admin/ui/src/lib/organizations.test.ts +89 -0
  197. package/src/admin/ui/src/lib/organizations.ts +262 -0
  198. package/src/admin/ui/src/lib/query-client.ts +3 -1
  199. package/src/admin/ui/src/lib/theme.ts +57 -0
  200. package/src/admin/ui/src/routeTree.gen.ts +47 -4
  201. package/src/admin/ui/src/routes/__root.tsx +18 -3
  202. package/src/admin/ui/src/routes/api-keys.tsx +52 -7
  203. package/src/admin/ui/src/routes/index.tsx +1 -1
  204. package/src/admin/ui/src/routes/organizations.tsx +158 -0
  205. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
  206. package/src/admin/ui/src/start.ts +6 -0
  207. package/src/admin/ui.generated.ts +63 -13
  208. package/src/analytics/Analytics.ts +25 -0
  209. package/src/analytics/tables/requestEvents.ts +33 -0
  210. package/src/apps/composition.test.ts +82 -2
  211. package/src/apps/data/App.ts +24 -3
  212. package/src/apps/data/FundingRouteProviders.test.ts +204 -0
  213. package/src/apps/data/FundingRouteProviders.ts +294 -9
  214. package/src/apps/data/routes/funding.test.ts +90 -23
  215. package/src/apps/data/routes/funding.ts +315 -0
  216. package/src/apps/data/routes/rpc.test.ts +33 -0
  217. package/src/apps/management/App.ts +2 -2
  218. package/src/apps/management/Billing.ts +10 -2
  219. package/src/apps/management/routes/api-keys.test.ts +137 -1
  220. package/src/apps/management/routes/api-keys.ts +63 -38
  221. package/src/apps/management/routes/billing.ts +14 -14
  222. package/src/apps/management/routes/invitations.test.ts +102 -1
  223. package/src/apps/management/routes/invitations.ts +13 -15
  224. package/src/apps/management/routes/members.ts +10 -10
  225. package/src/apps/management/routes/orgs.test.ts +78 -16
  226. package/src/apps/management/routes/orgs.ts +21 -20
  227. package/src/apps/management/routes/projects.ts +12 -13
  228. package/src/apps/management/routes/scopes.test.ts +16 -0
  229. package/src/apps/management/routes/scopes.ts +6 -7
  230. package/src/apps/management/routes/usage.ts +9 -14
  231. package/src/apps/mcp.test.ts +25 -1
  232. package/src/apps/mcp.ts +43 -3
  233. package/src/apps/relay/App.test.ts +23 -18
  234. package/src/apps/relay/App.ts +35 -14
  235. package/src/db/Db.ts +59 -3
  236. package/src/db/Schema.ts +20 -1
  237. package/src/db/tables/adminAuditLogs.ts +47 -0
  238. package/src/db/tables/invitations.ts +1 -1
  239. package/src/db/tables/memberships.ts +59 -3
  240. package/src/db/tables/organizations.ts +148 -1
  241. package/src/db/tables/projects.ts +47 -0
  242. package/src/db/tables/webhookDeliveries.ts +83 -0
  243. package/src/handlers/relay.test-d.ts +8 -0
  244. package/src/handlers/relay.test.ts +2 -2
  245. package/src/handlers/relay.ts +62 -22
  246. package/src/internal/Auth.test.ts +174 -7
  247. package/src/internal/Auth.ts +90 -62
  248. package/src/internal/Cursor.test.ts +10 -0
  249. package/src/internal/Cursor.ts +5 -1
  250. package/src/internal/EdgeCache.test.ts +11 -0
  251. package/src/internal/EdgeCache.ts +2 -0
  252. package/src/internal/FundingRoutes.test.ts +26 -0
  253. package/src/internal/FundingRoutes.ts +187 -18
  254. package/src/internal/Log.test.ts +41 -3
  255. package/src/internal/Log.ts +23 -4
@@ -0,0 +1,455 @@
1
+ import type * as Analytics from '../../analytics/Analytics.js'
2
+ import * as ApiKeys from '../../ApiKeys.js'
3
+ import * as BillingSettings from '../../db/tables/billingSettings.js'
4
+ import * as Memberships from '../../db/tables/memberships.js'
5
+ import * as Organizations from '../../db/tables/organizations.js'
6
+ import * as Projects from '../../db/tables/projects.js'
7
+ import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
8
+ import * as StripeCustomers from '../../db/tables/stripeCustomers.js'
9
+ import * as Users from '../../db/tables/users.js'
10
+ import * as WebhookDeliveries from '../../db/tables/webhookDeliveries.js'
11
+ import * as WebhookSubscriptions from '../../db/tables/webhookSubscriptions.js'
12
+ import * as Viem from '../../internal/Viem.js'
13
+
14
+ import * as TestAdmin from '../../../test/Admin.js'
15
+ import * as TestApp from '../../../test/App.js'
16
+ import * as AdminOrganizations from './organizations.js'
17
+
18
+ describe('GET /organizations', () => {
19
+ test('lists summaries newest first with member and project counts', async () => {
20
+ const { client, db } = TestAdmin.setup()
21
+ const user = await Users.upsertByAddress(db, {
22
+ address: `0x${'11'.repeat(20)}`,
23
+ })
24
+ await Users.setEmail(db, user.id, 'owner@acme.example')
25
+ const organization = await Organizations.createOwned(db, { name: 'Acme', userId: user.id })
26
+ await Projects.create(db, { name: 'Checkout', orgId: organization.id })
27
+ await Projects.create(db, { name: 'Treasury', orgId: organization.id })
28
+
29
+ const response = await client.organizations.$get({ query: {} })
30
+ const body = await TestApp.json(response, AdminOrganizations.schema.listOrganizations.Response)
31
+
32
+ expect(response.status).toBe(200)
33
+ expect(body.nextCursor).toBeNull()
34
+ expect(body.data).toHaveLength(1)
35
+ expect(body.data[0]).toMatchObject({
36
+ id: organization.id,
37
+ memberCount: 1,
38
+ name: 'Acme',
39
+ projectCount: 2,
40
+ userId: user.id,
41
+ })
42
+ })
43
+
44
+ test('searches organization and member identity fields case-insensitively', async () => {
45
+ const { client, db } = TestAdmin.setup()
46
+ const user = await Users.upsertByAddress(db, {
47
+ address: `0x${'22'.repeat(20)}`,
48
+ })
49
+ await Users.setEmail(db, user.id, 'Owner@Example.com')
50
+ const organization = await Organizations.createOwned(db, {
51
+ name: 'Merchant Operations',
52
+ userId: user.id,
53
+ })
54
+ await Organizations.create(db, { id: 'org_unrelated', name: 'Unrelated' })
55
+
56
+ for (const query of ['merchant', organization.id, user.id, 'owner@']) {
57
+ const response = await client.organizations.$get({ query: { query } })
58
+ const body = await TestApp.json(
59
+ response,
60
+ AdminOrganizations.schema.listOrganizations.Response,
61
+ )
62
+ expect(body.data.map(({ id }) => id)).toEqual([organization.id])
63
+ }
64
+ })
65
+
66
+ test('treats SQL wildcard characters as literal search text', async () => {
67
+ const { client, db } = TestAdmin.setup()
68
+ const organization = await Organizations.create(db, { name: 'A_B Merchant' })
69
+ await Organizations.create(db, { name: 'AcB Merchant' })
70
+
71
+ const response = await client.organizations.$get({ query: { query: 'a_b' } })
72
+ const body = await TestApp.json(response, AdminOrganizations.schema.listOrganizations.Response)
73
+
74
+ expect(body.data.map(({ id }) => id)).toEqual([organization.id])
75
+ })
76
+
77
+ test('orders distinct creation times newest first', async () => {
78
+ const { client, db } = TestAdmin.setup()
79
+ vi.useFakeTimers()
80
+ try {
81
+ vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'))
82
+ const older = await Organizations.create(db, { name: 'Older' })
83
+ vi.setSystemTime(new Date('2026-01-02T00:00:00.000Z'))
84
+ const newer = await Organizations.create(db, { name: 'Newer' })
85
+
86
+ const response = await client.organizations.$get({ query: {} })
87
+ const body = await TestApp.json(
88
+ response,
89
+ AdminOrganizations.schema.listOrganizations.Response,
90
+ )
91
+ expect(body.data.map(({ id }) => id)).toEqual([newer.id, older.id])
92
+ } finally {
93
+ vi.useRealTimers()
94
+ }
95
+ })
96
+
97
+ test('rejects broad queries shorter than three characters', async () => {
98
+ const { client } = TestAdmin.setup()
99
+ const response = await client.organizations.$get({ query: { query: 'a' } })
100
+ expect(response.status).toBe(400)
101
+ })
102
+
103
+ test('walks a stable cursor without duplicate organizations', async () => {
104
+ const { client, db } = TestAdmin.setup()
105
+ for (const id of ['org_f', 'org_e', 'org_d', 'org_c', 'org_b', 'org_a'])
106
+ await Organizations.create(db, { id, name: id })
107
+
108
+ const firstResponse = await client.organizations.$get({ query: { limit: '5' } })
109
+ const first = await TestApp.json(
110
+ firstResponse,
111
+ AdminOrganizations.schema.listOrganizations.Response,
112
+ )
113
+ expect(first.data).toHaveLength(5)
114
+ expect(first.nextCursor).not.toBeNull()
115
+
116
+ const secondResponse = await client.organizations.$get({
117
+ query: { cursor: first.nextCursor!, limit: '5' },
118
+ })
119
+ const second = await TestApp.json(
120
+ secondResponse,
121
+ AdminOrganizations.schema.listOrganizations.Response,
122
+ )
123
+ expect(second.data).toHaveLength(1)
124
+ expect(second.nextCursor).toBeNull()
125
+ expect(new Set([...first.data, ...second.data].map(({ id }) => id)).size).toBe(6)
126
+ })
127
+ })
128
+
129
+ describe('GET /organizations/:orgId', () => {
130
+ test('returns counts and billing state split by environment', async () => {
131
+ const { client, db } = TestAdmin.setup()
132
+ const user = await Users.upsertByAddress(db, {
133
+ address: `0x${'33'.repeat(20)}`,
134
+ })
135
+ await Users.setEmail(db, user.id, 'owner@tempo.xyz')
136
+ const organization = await Organizations.createOwned(db, { name: 'Tempo', userId: user.id })
137
+ await Projects.create(db, { name: 'API', orgId: organization.id })
138
+ await BillingSettings.upsert(db, {
139
+ environment: 'production',
140
+ orgId: organization.id,
141
+ spendLimit: '250',
142
+ txFeeLimit: '0.50',
143
+ })
144
+ await BillingSettings.upsert(db, {
145
+ environment: 'sandbox',
146
+ orgId: organization.id,
147
+ spendLimit: '1000',
148
+ })
149
+ await StripeCustomers.create(db, {
150
+ environment: 'production',
151
+ orgId: organization.id,
152
+ stripeCustomerId: 'cus_admin_lookup',
153
+ })
154
+ await StripeCustomers.setStatus(db, organization.id, 'active', 'production')
155
+
156
+ const response = await client.organizations[':orgId'].$get({
157
+ param: { orgId: organization.id },
158
+ })
159
+ const body = await TestApp.json(response, AdminOrganizations.schema.getOrganization.Response)
160
+
161
+ expect(response.status).toBe(200)
162
+ expect(body.memberCount).toBe(1)
163
+ expect(body.projectCount).toBe(1)
164
+ expect(body.billing.production).toMatchObject({
165
+ settings: { spendLimit: '250', txFeeLimit: '0.50' },
166
+ source: { status: 'active', stripeCustomerId: 'cus_admin_lookup' },
167
+ })
168
+ expect(body.billing.sandbox).toMatchObject({
169
+ settings: { spendLimit: '1000', txFeeLimit: null },
170
+ source: null,
171
+ })
172
+ })
173
+
174
+ test('returns empty counts and billing for a memberless organization', async () => {
175
+ const { client, db } = TestAdmin.setup()
176
+ const organization = await Organizations.create(db, { name: 'Support-created' })
177
+
178
+ const response = await client.organizations[':orgId'].$get({
179
+ param: { orgId: organization.id },
180
+ })
181
+ const body = await TestApp.json(response, AdminOrganizations.schema.getOrganization.Response)
182
+
183
+ expect(body).toMatchObject({
184
+ billing: {
185
+ production: { settings: null, source: null },
186
+ sandbox: { settings: null, source: null },
187
+ },
188
+ memberCount: 0,
189
+ projectCount: 0,
190
+ })
191
+ })
192
+
193
+ test('returns 404 for an unknown organization', async () => {
194
+ const { client } = TestAdmin.setup()
195
+ const response = await client.organizations[':orgId'].$get({
196
+ param: { orgId: 'org_missing' },
197
+ })
198
+ const body = (await response.json()) as { error: { code: string } }
199
+
200
+ expect(response.status).toBe(404)
201
+ expect(body.error.code).toBe('not_found')
202
+ })
203
+ })
204
+
205
+ describe('GET /organizations/:orgId/members', () => {
206
+ test('returns a bounded cursor page without duplicates', async () => {
207
+ const { client, db } = TestAdmin.setup()
208
+ const organization = await Organizations.create(db, { name: 'Members' })
209
+ for (let index = 0; index < 6; index++) {
210
+ const user = await Users.upsertByAddress(db, {
211
+ address: `0x${String(index + 1).padStart(40, '0')}`,
212
+ })
213
+ await Users.setEmail(db, user.id, `member-${index}@example.com`)
214
+ await Memberships.create(db, { orgId: organization.id, role: 'member', userId: user.id })
215
+ }
216
+
217
+ const firstResponse = await client.organizations[':orgId'].members.$get({
218
+ param: { orgId: organization.id },
219
+ query: { limit: '5' },
220
+ })
221
+ const first = await TestApp.json(
222
+ firstResponse,
223
+ AdminOrganizations.schema.listOrganizationMembers.Response,
224
+ )
225
+ const secondResponse = await client.organizations[':orgId'].members.$get({
226
+ param: { orgId: organization.id },
227
+ query: { cursor: first.nextCursor!, limit: '5' },
228
+ })
229
+ const second = await TestApp.json(
230
+ secondResponse,
231
+ AdminOrganizations.schema.listOrganizationMembers.Response,
232
+ )
233
+
234
+ expect(first.data).toHaveLength(5)
235
+ expect(second.data).toHaveLength(1)
236
+ expect(new Set([...first.data, ...second.data].map(({ userId }) => userId)).size).toBe(6)
237
+ })
238
+ })
239
+
240
+ describe('GET /organizations/:orgId/projects', () => {
241
+ test('returns a bounded cursor page without duplicates', async () => {
242
+ const { client, db } = TestAdmin.setup()
243
+ const organization = await Organizations.create(db, { name: 'Projects' })
244
+ for (let index = 0; index < 6; index++)
245
+ await Projects.create(db, { name: `Project ${index}`, orgId: organization.id })
246
+
247
+ const firstResponse = await client.organizations[':orgId'].projects.$get({
248
+ param: { orgId: organization.id },
249
+ query: { limit: '5' },
250
+ })
251
+ const first = await TestApp.json(
252
+ firstResponse,
253
+ AdminOrganizations.schema.listOrganizationProjects.Response,
254
+ )
255
+ const secondResponse = await client.organizations[':orgId'].projects.$get({
256
+ param: { orgId: organization.id },
257
+ query: { cursor: first.nextCursor!, limit: '5' },
258
+ })
259
+ const second = await TestApp.json(
260
+ secondResponse,
261
+ AdminOrganizations.schema.listOrganizationProjects.Response,
262
+ )
263
+
264
+ expect(first.data).toHaveLength(5)
265
+ expect(second.data).toHaveLength(1)
266
+ expect(new Set([...first.data, ...second.data].map(({ id }) => id)).size).toBe(6)
267
+ })
268
+ })
269
+
270
+ describe('GET /organizations/:orgId/api-keys', () => {
271
+ test('returns active key environment, scopes, and creator without tokens', async () => {
272
+ const { client, db, store } = TestAdmin.setup()
273
+ const organization = await Organizations.create(db, { name: 'Keys' })
274
+ await ApiKeys.mint(store, {
275
+ createdBy: 'ops@tempo.xyz',
276
+ environment: 'sandbox',
277
+ orgId: organization.id,
278
+ scopes: ['data:read'],
279
+ })
280
+
281
+ const response = await client.organizations[':orgId']['api-keys'].$get({
282
+ param: { orgId: organization.id },
283
+ })
284
+ const body = await TestApp.json(
285
+ response,
286
+ AdminOrganizations.schema.listOrganizationApiKeys.Response,
287
+ )
288
+
289
+ expect(body.data[0]).toMatchObject({
290
+ createdBy: 'ops@tempo.xyz',
291
+ environment: 'sandbox',
292
+ scopes: ['data:read'],
293
+ })
294
+ expect(body.data[0]).not.toHaveProperty('status')
295
+ expect(body.data[0]).not.toHaveProperty('token')
296
+ })
297
+ })
298
+
299
+ describe('GET /organizations/:orgId/usage', () => {
300
+ test('returns recent totals and error breakdowns from analytics', async () => {
301
+ const { client, db } = TestAdmin.setup({ analytics: usageAnalytics })
302
+ const organization = await Organizations.create(db, { name: 'Usage' })
303
+ const response = await client.organizations[':orgId'].usage.$get({
304
+ param: { orgId: organization.id },
305
+ query: { period: '7d' },
306
+ })
307
+ const body = await TestApp.json(
308
+ response,
309
+ AdminOrganizations.schema.getOrganizationUsage.Response,
310
+ )
311
+
312
+ expect(body).toMatchObject({
313
+ byError: [{ code: 'upstream_error', requests: 2 }],
314
+ byRoute: [{ averageDurationMs: 12.5, errors: 2, requests: 20, route: '/v1/tokens' }],
315
+ byStatus: [{ requests: 2, status: 500 }],
316
+ period: '7d',
317
+ totals: { averageDurationMs: 10.5, errors: 2, requests: 25 },
318
+ })
319
+ })
320
+ })
321
+
322
+ describe('GET /organizations/:orgId/sponsorship', () => {
323
+ test('returns current-period committed spend split by environment', async () => {
324
+ const { client, db } = TestAdmin.setup()
325
+ const organization = await Organizations.create(db, { name: 'Sponsorship' })
326
+ await SponsoredTransactions.upsert(db, {
327
+ apiKeyId: 'key_1',
328
+ billable: true,
329
+ chainId: Viem.chainId.mainnet,
330
+ environment: 'production',
331
+ feeMax: '1250000',
332
+ orgId: organization.id,
333
+ projectId: 'prj_1',
334
+ signPayload: `0x${'11'.repeat(32)}`,
335
+ transaction: `0x${'22'.repeat(32)}`,
336
+ })
337
+
338
+ const response = await client.organizations[':orgId'].sponsorship.$get({
339
+ param: { orgId: organization.id },
340
+ })
341
+ const body = await TestApp.json(
342
+ response,
343
+ AdminOrganizations.schema.getOrganizationSponsorship.Response,
344
+ )
345
+
346
+ expect(body.production).toMatchObject({
347
+ committedSpend: '1.25',
348
+ currency: 'usd',
349
+ period: 'month',
350
+ })
351
+ expect(body.sandbox).toMatchObject({
352
+ committedSpend: '0',
353
+ currency: 'usd',
354
+ period: 'month',
355
+ })
356
+ })
357
+ })
358
+
359
+ describe('GET /organizations/:orgId/webhooks', () => {
360
+ test('redacts destinations and returns recent delivery failures', async () => {
361
+ const { client, db } = TestAdmin.setup()
362
+ const organization = await Organizations.create(db, { name: 'Webhooks' })
363
+ const createdAt = new Date().toISOString()
364
+ await WebhookSubscriptions.insert(db, {
365
+ chainId: 4217,
366
+ createdAt,
367
+ destination: { type: 'url', url: 'https://hooks.example.com/private/path' },
368
+ eventType: 'token:transfer',
369
+ failureCount: 1,
370
+ filters: {},
371
+ id: 'wh_admin_visibility',
372
+ owner: { orgId: organization.id, type: 'api_key' },
373
+ secret: 'secret',
374
+ status: 'active',
375
+ updatedAt: createdAt,
376
+ })
377
+ for (let index = 0; index < 26; index++)
378
+ await WebhookDeliveries.insert(
379
+ db,
380
+ {
381
+ attempt: 1,
382
+ createdAt,
383
+ envelope: {
384
+ chainId: 4217,
385
+ createdAt,
386
+ data: {},
387
+ id: `evt_admin_visibility_${index}`,
388
+ subscriptionId: 'wh_admin_visibility',
389
+ type: 'token:transfer',
390
+ },
391
+ error: 'HTTP 500',
392
+ eventId: `evt_admin_visibility_${index}`,
393
+ id: `whd_admin_visibility_${index}`,
394
+ requestUrl: 'https://hooks.example.com/private/path',
395
+ responseStatus: 500,
396
+ status: 'failed',
397
+ subscriptionId: 'wh_admin_visibility',
398
+ },
399
+ new Date(Date.now() + 60_000).toISOString(),
400
+ )
401
+
402
+ const oldCreatedAt = new Date(Date.now() - 8 * 24 * 60 * 60 * 1_000).toISOString()
403
+ await WebhookDeliveries.insert(
404
+ db,
405
+ {
406
+ attempt: 1,
407
+ createdAt: oldCreatedAt,
408
+ envelope: {
409
+ chainId: 4217,
410
+ createdAt: oldCreatedAt,
411
+ data: {},
412
+ id: 'evt_admin_visibility_old',
413
+ subscriptionId: 'wh_admin_visibility',
414
+ type: 'token:transfer',
415
+ },
416
+ error: 'HTTP 500',
417
+ eventId: 'evt_admin_visibility_old',
418
+ id: 'whd_admin_visibility_old',
419
+ requestUrl: 'https://hooks.example.com/private/path',
420
+ responseStatus: 500,
421
+ status: 'failed',
422
+ subscriptionId: 'wh_admin_visibility',
423
+ },
424
+ new Date(Date.now() + 60_000).toISOString(),
425
+ )
426
+
427
+ const response = await client.organizations[':orgId'].webhooks.$get({
428
+ param: { orgId: organization.id },
429
+ })
430
+ const body = await TestApp.json(
431
+ response,
432
+ AdminOrganizations.schema.getOrganizationWebhooks.Response,
433
+ )
434
+
435
+ expect(body.subscriptions[0]?.destination).toBe('https://hooks.example.com/…')
436
+ expect(body.failureCount).toBe(26)
437
+ expect(body.failures).toHaveLength(25)
438
+ expect(body.failures[0]).toMatchObject({ error: 'HTTP 500', responseStatus: 500 })
439
+ expect(body.failuresTruncated).toBe(true)
440
+ })
441
+ })
442
+
443
+ const usageAnalytics: Analytics.Analytics = {
444
+ async insert() {},
445
+ async migrate() {},
446
+ async query(sql) {
447
+ if (sql.includes('GROUP BY error_code'))
448
+ return [{ code: 'upstream_error', requests: 2 }] as never[]
449
+ if (sql.includes('GROUP BY route'))
450
+ return [{ averageDurationMs: 12.5, errors: 2, requests: 20, route: '/v1/tokens' }] as never[]
451
+ if (sql.includes('GROUP BY status')) return [{ requests: 2, status: 500 }] as never[]
452
+ if (sql.includes('GROUP BY key_id') || sql.includes('GROUP BY toStartOf')) return []
453
+ return [{ averageDurationMs: 10.5, errors: 2, requests: 25 }] as never[]
454
+ },
455
+ }