tapimo 0.4.1 → 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 (243) 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/FundingRouteProviders.d.ts +94 -5
  55. package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
  56. package/dist/apps/data/FundingRouteProviders.js +196 -5
  57. package/dist/apps/data/FundingRouteProviders.js.map +1 -1
  58. package/dist/apps/data/routes/activities.d.ts +38 -38
  59. package/dist/apps/data/routes/addresses.d.ts +24 -24
  60. package/dist/apps/data/routes/blocks.d.ts +39 -39
  61. package/dist/apps/data/routes/exchanges.d.ts +453 -453
  62. package/dist/apps/data/routes/fee-amm.d.ts +101 -101
  63. package/dist/apps/data/routes/funding.d.ts +730 -138
  64. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  65. package/dist/apps/data/routes/funding.js +234 -0
  66. package/dist/apps/data/routes/funding.js.map +1 -1
  67. package/dist/apps/data/routes/indexer.d.ts +5 -5
  68. package/dist/apps/data/routes/transactions.d.ts +281 -281
  69. package/dist/apps/data/routes/transfers.d.ts +58 -58
  70. package/dist/apps/data/routes/verified-tokens.d.ts +81 -81
  71. package/dist/apps/management/App.d.ts +1156 -615
  72. package/dist/apps/management/App.d.ts.map +1 -1
  73. package/dist/apps/management/App.js +2 -2
  74. package/dist/apps/management/Billing.d.ts +3 -0
  75. package/dist/apps/management/Billing.d.ts.map +1 -1
  76. package/dist/apps/management/Billing.js +7 -2
  77. package/dist/apps/management/Billing.js.map +1 -1
  78. package/dist/apps/management/routes/api-keys.d.ts +106 -90
  79. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  80. package/dist/apps/management/routes/api-keys.js +41 -30
  81. package/dist/apps/management/routes/api-keys.js.map +1 -1
  82. package/dist/apps/management/routes/billing.d.ts +278 -146
  83. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  84. package/dist/apps/management/routes/billing.js +14 -14
  85. package/dist/apps/management/routes/billing.js.map +1 -1
  86. package/dist/apps/management/routes/invitations.d.ts +93 -20
  87. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  88. package/dist/apps/management/routes/invitations.js +12 -15
  89. package/dist/apps/management/routes/invitations.js.map +1 -1
  90. package/dist/apps/management/routes/me.d.ts +6 -6
  91. package/dist/apps/management/routes/members.d.ts +89 -18
  92. package/dist/apps/management/routes/members.d.ts.map +1 -1
  93. package/dist/apps/management/routes/members.js +10 -10
  94. package/dist/apps/management/routes/members.js.map +1 -1
  95. package/dist/apps/management/routes/orgs.d.ts +130 -70
  96. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  97. package/dist/apps/management/routes/orgs.js +19 -19
  98. package/dist/apps/management/routes/orgs.js.map +1 -1
  99. package/dist/apps/management/routes/projects.d.ts +137 -48
  100. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  101. package/dist/apps/management/routes/projects.js +12 -13
  102. package/dist/apps/management/routes/projects.js.map +1 -1
  103. package/dist/apps/management/routes/scopes.d.ts +4 -18
  104. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  105. package/dist/apps/management/routes/scopes.js +6 -7
  106. package/dist/apps/management/routes/scopes.js.map +1 -1
  107. package/dist/apps/management/routes/usage.d.ts +63 -19
  108. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  109. package/dist/apps/management/routes/usage.js +9 -10
  110. package/dist/apps/management/routes/usage.js.map +1 -1
  111. package/dist/apps/mcp.d.ts.map +1 -1
  112. package/dist/apps/mcp.js +35 -3
  113. package/dist/apps/mcp.js.map +1 -1
  114. package/dist/apps/relay/App.d.ts +26 -4
  115. package/dist/apps/relay/App.d.ts.map +1 -1
  116. package/dist/apps/relay/App.js +17 -14
  117. package/dist/apps/relay/App.js.map +1 -1
  118. package/dist/db/Db.d.ts +5 -3
  119. package/dist/db/Db.d.ts.map +1 -1
  120. package/dist/db/Db.js +52 -0
  121. package/dist/db/Db.js.map +1 -1
  122. package/dist/db/Schema.d.ts +13 -0
  123. package/dist/db/Schema.d.ts.map +1 -1
  124. package/dist/db/Schema.js +16 -1
  125. package/dist/db/Schema.js.map +1 -1
  126. package/dist/db/tables/adminAuditLogs.d.ts +27 -0
  127. package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
  128. package/dist/db/tables/adminAuditLogs.js +19 -0
  129. package/dist/db/tables/adminAuditLogs.js.map +1 -0
  130. package/dist/db/tables/invitations.d.ts +1 -1
  131. package/dist/db/tables/invitations.d.ts.map +1 -1
  132. package/dist/db/tables/memberships.d.ts +32 -4
  133. package/dist/db/tables/memberships.d.ts.map +1 -1
  134. package/dist/db/tables/memberships.js +31 -0
  135. package/dist/db/tables/memberships.js.map +1 -1
  136. package/dist/db/tables/organizations.d.ts +43 -1
  137. package/dist/db/tables/organizations.d.ts.map +1 -1
  138. package/dist/db/tables/organizations.js +103 -0
  139. package/dist/db/tables/organizations.js.map +1 -1
  140. package/dist/db/tables/projects.d.ts +25 -0
  141. package/dist/db/tables/projects.d.ts.map +1 -1
  142. package/dist/db/tables/projects.js +22 -0
  143. package/dist/db/tables/projects.js.map +1 -1
  144. package/dist/db/tables/webhookDeliveries.d.ts +26 -0
  145. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  146. package/dist/db/tables/webhookDeliveries.js +37 -0
  147. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  148. package/dist/internal/Auth.d.ts +13 -18
  149. package/dist/internal/Auth.d.ts.map +1 -1
  150. package/dist/internal/Auth.js +78 -58
  151. package/dist/internal/Auth.js.map +1 -1
  152. package/dist/internal/Cursor.d.ts +1 -1
  153. package/dist/internal/Cursor.d.ts.map +1 -1
  154. package/dist/internal/Cursor.js +4 -0
  155. package/dist/internal/Cursor.js.map +1 -1
  156. package/dist/internal/EdgeCache.d.ts.map +1 -1
  157. package/dist/internal/EdgeCache.js +2 -0
  158. package/dist/internal/EdgeCache.js.map +1 -1
  159. package/dist/internal/FundingRoutes.d.ts +170 -0
  160. package/dist/internal/FundingRoutes.d.ts.map +1 -1
  161. package/dist/internal/FundingRoutes.js +68 -6
  162. package/dist/internal/FundingRoutes.js.map +1 -1
  163. package/dist/internal/Log.d.ts +16 -1
  164. package/dist/internal/Log.d.ts.map +1 -1
  165. package/dist/internal/Log.js +5 -3
  166. package/dist/internal/Log.js.map +1 -1
  167. package/package.json +2 -2
  168. package/src/ApiKey.ts +2 -3
  169. package/src/ApiKeys.ts +95 -17
  170. package/src/App.test.ts +44 -2
  171. package/src/App.ts +29 -9
  172. package/src/Assets.ts +8 -0
  173. package/src/Auth.test-d.ts +36 -0
  174. package/src/Auth.ts +20 -1
  175. package/src/Client.test-d.ts +64 -17
  176. package/src/Scope.test.ts +34 -0
  177. package/src/Scope.ts +53 -17
  178. package/src/admin/App.test.ts +33 -3
  179. package/src/admin/App.ts +49 -14
  180. package/src/admin/apps/api-keys.ts +10 -3
  181. package/src/admin/apps/organizations.test.ts +455 -0
  182. package/src/admin/apps/organizations.ts +953 -0
  183. package/src/admin/apps/scopes.test.ts +10 -0
  184. package/src/admin/ui/plugin.ts +1 -1
  185. package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
  186. package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
  187. package/src/admin/ui/src/lib/api-keys.ts +36 -19
  188. package/src/admin/ui/src/lib/config.ts +2 -3
  189. package/src/admin/ui/src/lib/organizations.test.ts +89 -0
  190. package/src/admin/ui/src/lib/organizations.ts +262 -0
  191. package/src/admin/ui/src/lib/query-client.ts +3 -1
  192. package/src/admin/ui/src/lib/theme.ts +57 -0
  193. package/src/admin/ui/src/routeTree.gen.ts +47 -4
  194. package/src/admin/ui/src/routes/__root.tsx +18 -3
  195. package/src/admin/ui/src/routes/api-keys.tsx +52 -7
  196. package/src/admin/ui/src/routes/index.tsx +1 -1
  197. package/src/admin/ui/src/routes/organizations.tsx +158 -0
  198. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
  199. package/src/admin/ui/src/start.ts +6 -0
  200. package/src/admin/ui.generated.ts +63 -13
  201. package/src/analytics/Analytics.ts +25 -0
  202. package/src/analytics/tables/requestEvents.ts +33 -0
  203. package/src/apps/composition.test.ts +82 -2
  204. package/src/apps/data/FundingRouteProviders.test.ts +204 -0
  205. package/src/apps/data/FundingRouteProviders.ts +294 -9
  206. package/src/apps/data/routes/funding.test.ts +90 -23
  207. package/src/apps/data/routes/funding.ts +315 -0
  208. package/src/apps/management/App.ts +2 -2
  209. package/src/apps/management/Billing.ts +10 -2
  210. package/src/apps/management/routes/api-keys.test.ts +137 -1
  211. package/src/apps/management/routes/api-keys.ts +63 -38
  212. package/src/apps/management/routes/billing.ts +14 -14
  213. package/src/apps/management/routes/invitations.test.ts +102 -1
  214. package/src/apps/management/routes/invitations.ts +13 -15
  215. package/src/apps/management/routes/members.ts +10 -10
  216. package/src/apps/management/routes/orgs.test.ts +78 -16
  217. package/src/apps/management/routes/orgs.ts +21 -20
  218. package/src/apps/management/routes/projects.ts +12 -13
  219. package/src/apps/management/routes/scopes.test.ts +16 -0
  220. package/src/apps/management/routes/scopes.ts +6 -7
  221. package/src/apps/management/routes/usage.ts +9 -14
  222. package/src/apps/mcp.test.ts +25 -1
  223. package/src/apps/mcp.ts +43 -3
  224. package/src/apps/relay/App.test.ts +23 -18
  225. package/src/apps/relay/App.ts +35 -14
  226. package/src/db/Db.ts +59 -3
  227. package/src/db/Schema.ts +20 -1
  228. package/src/db/tables/adminAuditLogs.ts +47 -0
  229. package/src/db/tables/invitations.ts +1 -1
  230. package/src/db/tables/memberships.ts +59 -3
  231. package/src/db/tables/organizations.ts +148 -1
  232. package/src/db/tables/projects.ts +47 -0
  233. package/src/db/tables/webhookDeliveries.ts +83 -0
  234. package/src/internal/Auth.test.ts +174 -7
  235. package/src/internal/Auth.ts +90 -62
  236. package/src/internal/Cursor.test.ts +10 -0
  237. package/src/internal/Cursor.ts +5 -1
  238. package/src/internal/EdgeCache.test.ts +11 -0
  239. package/src/internal/EdgeCache.ts +2 -0
  240. package/src/internal/FundingRoutes.test.ts +26 -0
  241. package/src/internal/FundingRoutes.ts +187 -18
  242. package/src/internal/Log.test.ts +41 -3
  243. package/src/internal/Log.ts +23 -4
@@ -12,14 +12,30 @@ const origin = 'http://localhost'
12
12
  /** Super admin secret configured on the test app. */
13
13
  const secret = 'tempo:sk:b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1'
14
14
 
15
- /** Wildcard-scoped key: full data access, still not a super admin. */
15
+ /** Wildcard-scoped key: full API access, still not a super admin. */
16
16
  const wildcard = {
17
17
  id: 'key_wildcard',
18
- orgId: 'org_test',
18
+ orgId: 'org_a',
19
19
  scopes: ['*'],
20
20
  token: 'secret_wildcard_key',
21
21
  } satisfies TestApp.kvStore.Key
22
22
 
23
+ /** Key limited to management reads for Org A. */
24
+ const managementRead = {
25
+ id: 'key_management_read',
26
+ orgId: 'org_a',
27
+ scopes: ['management:read'],
28
+ token: 'secret_management_read_key',
29
+ } satisfies TestApp.kvStore.Key
30
+
31
+ /** Key limited to management writes for Org A. */
32
+ const managementWrite = {
33
+ id: 'key_management_write',
34
+ orgId: 'org_a',
35
+ scopes: ['management:write'],
36
+ token: 'secret_management_write_key',
37
+ } satisfies TestApp.kvStore.Key
38
+
23
39
  /** RequestInit presenting the given token. */
24
40
  function as(token: string) {
25
41
  return { headers: { 'tempo-api-key': token } } as const
@@ -36,7 +52,10 @@ function json(method: 'PATCH' | 'POST', cookie: string, body: unknown) {
36
52
 
37
53
  function createApp(db = TestApp.database()) {
38
54
  return TestApp.create({
39
- auth: { keys: [TestApp.key, wildcard], superAdmin: { secret } },
55
+ auth: {
56
+ keys: [TestApp.key, managementRead, managementWrite, wildcard],
57
+ superAdmin: { secret },
58
+ },
40
59
  db,
41
60
  session: { wallet: { origin } },
42
61
  })
@@ -81,6 +100,19 @@ describe('GET /orgs', () => {
81
100
  `)
82
101
  })
83
102
 
103
+ test('accepts a management API key in the query parameter', async () => {
104
+ const app = await seeded()
105
+ const response = await app.request(`/v1/orgs?key=${encodeURIComponent(managementRead.token)}`)
106
+ const body = await TestApp.json(response, Orgs.schema.listOrganizations.Response)
107
+
108
+ expect(response.status).toMatchInlineSnapshot(`200`)
109
+ expect(body.data.map(({ id }) => id)).toMatchInlineSnapshot(`
110
+ [
111
+ "org_a",
112
+ ]
113
+ `)
114
+ })
115
+
84
116
  test('lists only owned organizations for a session', async () => {
85
117
  const app = await seeded()
86
118
  const cookie = await session(app)
@@ -112,24 +144,26 @@ describe('GET /orgs', () => {
112
144
  `)
113
145
  })
114
146
 
115
- test('rejects API keys without the wildcard scope', async () => {
147
+ test('rejects API keys without the management read scope', async () => {
116
148
  const response = await (await seeded()).request('/v1/orgs', as(TestApp.key.token))
117
149
  expect(response.status).toBe(403)
118
150
  })
119
151
 
120
- test('rejects wildcard API keys', async () => {
152
+ test('lists only the owning organization for scoped and wildcard keys', async () => {
153
+ const app = await seeded()
154
+ const scoped = await app.request('/v1/orgs', as(managementRead.token))
155
+ expect(scoped.status).toBe(200)
156
+ expect(
157
+ (await TestApp.json(scoped, Orgs.schema.listOrganizations.Response)).data.map(({ id }) => id),
158
+ ).toEqual(['org_a'])
159
+
121
160
  const response = await (await seeded()).request('/v1/orgs', as(wildcard.token))
122
- expect(response.status).toBe(403)
123
- const { requestId, ...body } = (await response.json()) as { requestId?: string }
124
- expect(requestId).toBeDefined()
125
- expect(body).toMatchInlineSnapshot(`
126
- {
127
- "error": {
128
- "code": "forbidden",
129
- "message": "API key not permitted for this route",
130
- },
131
- }
132
- `)
161
+ expect(response.status).toBe(200)
162
+ expect(
163
+ (await TestApp.json(response, Orgs.schema.listOrganizations.Response)).data.map(
164
+ ({ id }) => id,
165
+ ),
166
+ ).toEqual(['org_a'])
133
167
  })
134
168
 
135
169
  test('rejects the secret when super admin is unconfigured', async () => {
@@ -200,6 +234,13 @@ describe('POST /orgs', () => {
200
234
  })
201
235
 
202
236
  describe('GET /orgs/:orgId', () => {
237
+ test('binds API keys to their owning organization', async () => {
238
+ const app = await seeded()
239
+ expect((await app.request('/v1/orgs/org_a', as(managementRead.token))).status).toBe(200)
240
+ expect((await app.request('/v1/orgs/org_b', as(managementRead.token))).status).toBe(404)
241
+ expect((await app.request('/v1/orgs/org_a', as(managementWrite.token))).status).toBe(403)
242
+ })
243
+
203
244
  test('hides foreign organizations behind 404', async () => {
204
245
  const app = createApp()
205
246
  const owner = await session(app)
@@ -219,6 +260,27 @@ describe('GET /orgs/:orgId', () => {
219
260
  })
220
261
 
221
262
  describe('PATCH /orgs/:orgId', () => {
263
+ test('requires the management write scope', async () => {
264
+ const app = await seeded()
265
+ const write = await app.request(
266
+ `/v1/orgs/org_a?key=${encodeURIComponent(managementWrite.token)}`,
267
+ {
268
+ body: JSON.stringify({ name: 'Managed' }),
269
+ headers: { 'content-type': 'application/json' },
270
+ method: 'PATCH',
271
+ },
272
+ )
273
+ expect(write.status).toBe(200)
274
+ expect((await TestApp.json(write, Orgs.schema.Organization)).name).toBe('Managed')
275
+
276
+ const readOnly = await app.request('/v1/orgs/org_a', {
277
+ body: JSON.stringify({ name: 'Denied' }),
278
+ headers: { 'content-type': 'application/json', 'tempo-api-key': managementRead.token },
279
+ method: 'PATCH',
280
+ })
281
+ expect(readOnly.status).toBe(403)
282
+ })
283
+
222
284
  test('renames an owned organization', async () => {
223
285
  const app = createApp()
224
286
  const cookie = await session(app)
@@ -46,7 +46,7 @@ export namespace schema {
46
46
  role: z
47
47
  .optional(z.enum(Memberships.roles))
48
48
  .check(
49
- z.describe("The caller's role in this organization; absent for the super admin."),
49
+ z.describe("The caller's role; absent for API keys and the super admin."),
50
50
  z.meta({ examples: ['owner'] }),
51
51
  ),
52
52
  updatedAt: z
@@ -104,17 +104,13 @@ export namespace schema {
104
104
  }
105
105
 
106
106
  /**
107
- * Mounts the `/orgs` resource: session-owned tenancy (each organization is
108
- * owned by the user that created it). The super admin bypasses ownership;
109
- * inaccessible or absent ids render `404` so foreign org ids are
110
- * indistinguishable from missing ones. Projects nest under `/orgs/:orgId` in
111
- * the projects app.
107
+ * Mounts organizations. Sessions access memberships, API keys access their owning organization, and the super admin bypasses ownership.
112
108
  */
113
109
  export function orgs() {
114
110
  return new Hono<App.Environment>()
115
111
  .get(
116
112
  '/v1/orgs',
117
- Auth.policy({ session: true }),
113
+ Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
118
114
  OpenApi.describeRoute({
119
115
  operationId: 'listOrganizations',
120
116
  responses: OpenApi.responses({
@@ -127,18 +123,21 @@ export function orgs() {
127
123
  tags: ['Organizations'],
128
124
  }),
129
125
  async (c) => {
130
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
131
-
132
- const principal = Auth.getPrincipal(c)
133
- if (principal?.type !== 'session' && principal?.type !== 'super_admin')
134
- return sessionRequired(c)
126
+ if (Auth.narrowAccess) return Auth.accessError(c)
135
127
 
136
128
  const db = Db.get(c.get('db'))
137
129
  try {
130
+ const principal = Auth.getPrincipal(c)
131
+ if (!principal || principal.type === 'public')
132
+ throw new Error('organization auth policy admitted an unsupported principal')
138
133
  const records =
139
134
  principal.type === 'super_admin'
140
135
  ? await Organizations.list(db)
141
- : await Organizations.listByMember(db, principal.id)
136
+ : principal.type === 'api_key'
137
+ ? [await Organizations.get(db, principal.orgId)].filter(
138
+ (record): record is Organizations.Record => record !== undefined,
139
+ )
140
+ : await Organizations.listByMember(db, principal.id)
142
141
  return c.json(
143
142
  Response.validated(schema.listOrganizations.Response, {
144
143
  data: records.map(serializeOrganization),
@@ -203,7 +202,7 @@ export function orgs() {
203
202
  )
204
203
  .get(
205
204
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}',
206
- Auth.policy({ session: true }),
205
+ Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
207
206
  Auth.ensureOrg(),
208
207
  OpenApi.validate('param', schema.Params, {
209
208
  code: 'param_invalid',
@@ -219,7 +218,7 @@ export function orgs() {
219
218
  tags: ['Organizations'],
220
219
  }),
221
220
  async (c) => {
222
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
221
+ if (Auth.narrowAccess) return Auth.accessError(c)
223
222
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
224
223
  if (OpenApi.narrowValidation)
225
224
  return OpenApi.validationError(c, {
@@ -238,7 +237,7 @@ export function orgs() {
238
237
  )
239
238
  .patch(
240
239
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}',
241
- Auth.policy({ session: true }),
240
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
242
241
  Auth.ensureOrg({ role: 'owner' }),
243
242
  OpenApi.validate('param', schema.Params, {
244
243
  code: 'param_invalid',
@@ -258,7 +257,7 @@ export function orgs() {
258
257
  tags: ['Organizations'],
259
258
  }),
260
259
  async (c) => {
261
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
260
+ if (Auth.narrowAccess) return Auth.accessError(c)
262
261
  if (OpenApi.narrowValidation)
263
262
  return OpenApi.validationError(c, {
264
263
  code: 'param_invalid',
@@ -289,7 +288,7 @@ export function orgs() {
289
288
  )
290
289
  .delete(
291
290
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}',
292
- Auth.policy({ session: true }),
291
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
293
292
  Auth.ensureOrg({ role: 'owner' }),
294
293
  OpenApi.validate('param', schema.Params, {
295
294
  code: 'param_invalid',
@@ -309,7 +308,7 @@ export function orgs() {
309
308
  tags: ['Organizations'],
310
309
  }),
311
310
  async (c) => {
312
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
311
+ if (Auth.narrowAccess) return Auth.accessError(c)
313
312
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
314
313
  if (OpenApi.narrowValidation)
315
314
  return OpenApi.validationError(c, {
@@ -321,7 +320,9 @@ export function orgs() {
321
320
  try {
322
321
  const kv = c.get('kv')
323
322
  if (kv)
324
- for (const record of await ApiKeys.listByOrg(kv.store, Auth.org(c).id))
323
+ for (const record of await ApiKeys.listByOrg(kv.store, Auth.org(c).id, {
324
+ scopeCatalog: c.get('scopeCatalog'),
325
+ }))
325
326
  await ApiKeys.revoke(kv.store, record.id)
326
327
  const deleted = await Organizations.deleteOrganization(db, Auth.org(c).id)
327
328
  if (!deleted) return organizationNotFound(c)
@@ -125,15 +125,13 @@ export namespace schema {
125
125
  }
126
126
 
127
127
  /**
128
- * Mounts the `/orgs/:orgId/projects` resource: projects nested under their
129
- * owning organization. Access follows the org scope (owning session or super
130
- * admin); inaccessible or absent ids render `404`.
128
+ * Mounts projects under their owning organization. Access follows the scoped session, API key, or super admin; inaccessible projects return `404`.
131
129
  */
132
130
  export function projects() {
133
131
  return new Hono<App.Environment>()
134
132
  .get(
135
133
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects',
136
- Auth.policy({ session: true }),
134
+ Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
137
135
  Auth.ensureOrg(),
138
136
  OpenApi.validate('param', schema.Params, {
139
137
  code: 'param_invalid',
@@ -152,7 +150,7 @@ export function projects() {
152
150
  tags: ['Projects'],
153
151
  }),
154
152
  async (c) => {
155
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
153
+ if (Auth.narrowAccess) return Auth.accessError(c)
156
154
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
157
155
  if (OpenApi.narrowValidation)
158
156
  return OpenApi.validationError(c, {
@@ -176,7 +174,7 @@ export function projects() {
176
174
  )
177
175
  .post(
178
176
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects',
179
- Auth.policy({ session: true }),
177
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
180
178
  Auth.ensureOrg({ role: 'admin' }),
181
179
  OpenApi.validate('param', schema.Params, {
182
180
  code: 'param_invalid',
@@ -196,7 +194,7 @@ export function projects() {
196
194
  tags: ['Projects'],
197
195
  }),
198
196
  async (c) => {
199
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
197
+ if (Auth.narrowAccess) return Auth.accessError(c)
200
198
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
201
199
  if (OpenApi.narrowValidation)
202
200
  return OpenApi.validationError(c, {
@@ -221,7 +219,7 @@ export function projects() {
221
219
  )
222
220
  .get(
223
221
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}',
224
- Auth.policy({ session: true }),
222
+ Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
225
223
  Auth.ensureOrg(),
226
224
  Auth.ensureProject(),
227
225
  OpenApi.validate('param', schema.ProjectParams, {
@@ -238,7 +236,7 @@ export function projects() {
238
236
  tags: ['Projects'],
239
237
  }),
240
238
  async (c) => {
241
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
239
+ if (Auth.narrowAccess) return Auth.accessError(c)
242
240
  if (Auth.narrowScope) return Auth.ensureProjectError(c)
243
241
  if (OpenApi.narrowValidation)
244
242
  return OpenApi.validationError(c, {
@@ -251,7 +249,7 @@ export function projects() {
251
249
  )
252
250
  .patch(
253
251
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}',
254
- Auth.policy({ session: true }),
252
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
255
253
  Auth.ensureOrg({ role: 'admin' }),
256
254
  Auth.ensureProject(),
257
255
  OpenApi.validate('param', schema.ProjectParams, {
@@ -272,7 +270,7 @@ export function projects() {
272
270
  tags: ['Projects'],
273
271
  }),
274
272
  async (c) => {
275
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
273
+ if (Auth.narrowAccess) return Auth.accessError(c)
276
274
  if (OpenApi.narrowValidation)
277
275
  return OpenApi.validationError(c, {
278
276
  code: 'param_invalid',
@@ -298,7 +296,7 @@ export function projects() {
298
296
  )
299
297
  .delete(
300
298
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}',
301
- Auth.policy({ session: true }),
299
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
302
300
  Auth.ensureOrg({ role: 'admin' }),
303
301
  Auth.ensureProject(),
304
302
  OpenApi.validate('param', schema.ProjectParams, {
@@ -318,7 +316,7 @@ export function projects() {
318
316
  tags: ['Projects'],
319
317
  }),
320
318
  async (c) => {
321
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
319
+ if (Auth.narrowAccess) return Auth.accessError(c)
322
320
  if (Auth.narrowScope) return Auth.ensureProjectError(c)
323
321
  if (OpenApi.narrowValidation)
324
322
  return OpenApi.validationError(c, {
@@ -332,6 +330,7 @@ export function projects() {
332
330
  if (kv)
333
331
  for (const record of await ApiKeys.listByOrg(kv.store, Auth.org(c).id, {
334
332
  projectId: Auth.project(c).id,
333
+ scopeCatalog: c.get('scopeCatalog'),
335
334
  }))
336
335
  await ApiKeys.revoke(kv.store, record.id)
337
336
  const deleted = await Projects.deleteProject(db, Auth.project(c).id)
@@ -19,6 +19,8 @@ describe('scopes', () => {
19
19
  [
20
20
  "data:read",
21
21
  "indexer:query",
22
+ "management:read",
23
+ "management:write",
22
24
  "rpc-relay:read",
23
25
  "rpc-relay:sponsor",
24
26
  "webhooks:read",
@@ -26,4 +28,18 @@ describe('scopes', () => {
26
28
  ]
27
29
  `)
28
30
  })
31
+
32
+ test('includes host scopes configured on the app', async () => {
33
+ const custom = {
34
+ description: 'Read project accounts.',
35
+ scope: 'accounts:read',
36
+ selfServe: true,
37
+ } as const satisfies Scope.Entry
38
+ const app = TestApp.create({ scopes: [custom] })
39
+ const response = await app.request('/v1/scopes')
40
+ const body = await TestApp.json(response, Scopes.schema.getScopes.Response)
41
+
42
+ expect(response.status).toBe(200)
43
+ expect(body.data).toEqual([...Scope.catalog, custom])
44
+ })
29
45
  })
@@ -6,7 +6,6 @@ import * as Auth from '../../../internal/Auth.js'
6
6
  import * as Cache from '../../../internal/Cache.js'
7
7
  import * as OpenApi from '../../../internal/OpenApi.js'
8
8
  import * as Response from '../../../internal/Response.js'
9
- import * as Scope from '../../../Scope.js'
10
9
 
11
10
  /** Zod schemas owned by the scope-catalog resource. */
12
11
  export namespace schema {
@@ -22,7 +21,7 @@ export namespace schema {
22
21
  z.meta({ examples: ['Read webhook subscriptions.'] }),
23
22
  ),
24
23
  scope: z
25
- .enum(Scope.catalog.map((entry) => entry.scope))
24
+ .string()
26
25
  .check(
27
26
  z.describe('Scope identifier used in API-key grants and route policies.'),
28
27
  z.meta({ examples: ['data:read'] }),
@@ -30,7 +29,7 @@ export namespace schema {
30
29
  selfServe: z
31
30
  .boolean()
32
31
  .check(
33
- z.describe('Whether a session may self-mint this scope (else super-admin-only).'),
32
+ z.describe('Whether an eligible session may self-mint this scope.'),
34
33
  z.meta({ examples: [true] }),
35
34
  ),
36
35
  })
@@ -47,13 +46,13 @@ export namespace schema {
47
46
 
48
47
  /**
49
48
  * Mounts the `/scopes` resource: the issuable API-key scope vocabulary, served
50
- * from {@link Scope.catalog}. Public catalog data: anonymous callers are served
49
+ * from the configured catalog. Public catalog data: anonymous callers are served
51
50
  * within the public quota; the session lane lets signed-in consoles skip it.
52
51
  */
53
52
  export function scopes() {
54
53
  return new Hono<App.Environment>().get(
55
54
  '/v1/scopes',
56
- Auth.policy({ public: true, session: true }),
55
+ Auth.policy({ apiKey: { scopes: [] }, public: true, session: true }),
57
56
  OpenApi.describeRoute({
58
57
  description: 'See which scopes can be granted to API keys.',
59
58
  operationId: 'getScopes',
@@ -65,7 +64,7 @@ export function scopes() {
65
64
  }),
66
65
  Cache.response<App.Environment>({
67
66
  cacheControl: Cache.policies.stable,
68
- name: 'tempo-api:scopes:v1',
67
+ name: 'tempo-api:scopes:v2',
69
68
  key: (c) => {
70
69
  const url = new URL(c.req.url)
71
70
  url.search = ''
@@ -76,7 +75,7 @@ export function scopes() {
76
75
  if (Auth.narrowAccess) return Auth.accessError(c)
77
76
  return c.json(
78
77
  Response.validated(schema.getScopes.Response, {
79
- data: Scope.catalog.map((entry) => ({ ...entry })),
78
+ data: c.get('scopeCatalog').map((entry) => ({ ...entry })),
80
79
  }),
81
80
  200,
82
81
  )
@@ -285,16 +285,12 @@ export namespace schema {
285
285
  }
286
286
  }
287
287
 
288
- /**
289
- * Mounts the usage surface: project request usage (read from a host-supplied
290
- * analytics capability) and an organization's sponsorship volume and fees
291
- * over time. Session lane; reads for members.
292
- */
288
+ /** Mounts organization request and sponsorship usage. Reads require membership or a scoped API key. */
293
289
  export function usage() {
294
290
  return new Hono<Environment>()
295
291
  .get(
296
292
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/usage/requests',
297
- Auth.policy({ session: true }),
293
+ Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
298
294
  Auth.ensureOrg(),
299
295
  OpenApi.validate('param', schema.getRequestUsage.Params, {
300
296
  code: 'param_invalid',
@@ -327,7 +323,7 @@ export function usage() {
327
323
  tags: ['Usage'],
328
324
  }),
329
325
  async (c) => {
330
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
326
+ if (Auth.narrowAccess) return Auth.accessError(c)
331
327
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
332
328
  if (OpenApi.narrowValidation)
333
329
  return OpenApi.validationError(c, {
@@ -398,7 +394,7 @@ export function usage() {
398
394
  )
399
395
  .get(
400
396
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/usage/sponsorships',
401
- Auth.policy({ session: true }),
397
+ Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
402
398
  Auth.ensureOrg(),
403
399
  OpenApi.validate('param', schema.getSponsorshipUsage.Params, {
404
400
  code: 'param_invalid',
@@ -423,7 +419,7 @@ export function usage() {
423
419
  tags: ['Usage'],
424
420
  }),
425
421
  async (c) => {
426
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
422
+ if (Auth.narrowAccess) return Auth.accessError(c)
427
423
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
428
424
  if (OpenApi.narrowValidation)
429
425
  return OpenApi.validationError(c, {
@@ -498,11 +494,10 @@ async function legacyKeyIds(
498
494
  ): Promise<readonly string[]> {
499
495
  const kv = c.get('kv')
500
496
  if (!kv) return []
501
- const records = await ApiKeys.listByOrg(
502
- kv.store,
503
- Auth.org(c).id,
504
- options.projectId === undefined ? {} : { projectId: options.projectId },
505
- )
497
+ const records = await ApiKeys.listByOrg(kv.store, Auth.org(c).id, {
498
+ ...(options.projectId === undefined ? {} : { projectId: options.projectId }),
499
+ scopeCatalog: c.get('scopeCatalog'),
500
+ })
506
501
  return records
507
502
  .filter(
508
503
  (record) => options.environment === undefined || record.environment === options.environment,
@@ -121,6 +121,30 @@ test('scopes tools to the data domain', async () => {
121
121
  expect(tools.some((name) => name.startsWith('v1_me'))).toBe(false)
122
122
  })
123
123
 
124
+ test('keeps tool input schemas compact', async () => {
125
+ const { host } = createHost()
126
+ await initialize(host)
127
+
128
+ const body = await request(host, { id: 2, jsonrpc: '2.0', method: 'tools/list', params: {} })
129
+ const tools = body.result.tools as {
130
+ name: string
131
+ inputSchema: { properties?: Record<string, { description?: string }> }
132
+ }[]
133
+ const tool = tools.find((entry) => entry.name === 'v1_tokens_get')
134
+ const properties = tool?.inputSchema.properties ?? {}
135
+
136
+ // Credential inputs stay off the schema; auth rides the forwarded header.
137
+ expect(properties['authorization']).toBeUndefined()
138
+ expect(properties['tempo-api-key']).toBeUndefined()
139
+ // Shared parameters carry the terse tool descriptions.
140
+ expect(properties['limit']?.description).toBe('Items per page.')
141
+ expect(properties['cursor']?.description).toBe(
142
+ 'Keyset cursor from a previous response `nextCursor`; omit for the first page.',
143
+ )
144
+ // `compact` strips examples from generated schemas.
145
+ expect(JSON.stringify(tool?.inputSchema)).not.toContain('"examples"')
146
+ })
147
+
124
148
  test('executes generated tools against the composed app', async () => {
125
149
  // Verified tokens are database-backed, so seed the default chain.
126
150
  const db = TestApp.database()
@@ -176,7 +200,7 @@ test('documents POST /mcp and keeps it open under auth enforcement', async () =>
176
200
  expect(operation?.operationId).toBe('mcpRequest')
177
201
  expect(operation?.tags).toEqual(['MCP'])
178
202
  // Anonymous and any-valid-key access; no required scopes.
179
- expect(operation?.security).toEqual([{ apiKey: [] }, { bearerAuth: [] }, {}])
203
+ expect(operation?.security).toEqual([{ apiKey: [] }, { queryApiKey: [] }, { bearerAuth: [] }, {}])
180
204
 
181
205
  // Surfaces under `Data API` in the reference, but the tool filter excludes the `MCP` tag.
182
206
  const groups = Object.fromEntries(spec['x-tagGroups'].map((group) => [group.name, group.tags]))
package/src/apps/mcp.ts CHANGED
@@ -15,7 +15,7 @@ const excludedTags = ['CoinGecko', 'MCP']
15
15
 
16
16
  /** Usage guidance surfaced to MCP clients on initialize. */
17
17
  const instructions =
18
- "Tools mirror the Tempo API's data domain. Authenticated tools accept a Tempo API key: send it as an `Authorization: Bearer` header on the HTTP request, or pass the `authorization` tool input per call. List endpoints paginate via `cursor`/`nextCursor`. `docs_*` tools search the Tempo documentation."
18
+ "Tools mirror the Tempo API's data domain. Authenticated tools accept a Tempo API key sent as an `Authorization: Bearer` header on the HTTP request. List endpoints paginate via `cursor`/`nextCursor`. `docs_*` tools search the Tempo documentation."
19
19
 
20
20
  /** Zod schemas owned by the MCP endpoint's OpenAPI description. */
21
21
  export namespace schema {
@@ -97,7 +97,15 @@ export function mcp(options: mcp.Options) {
97
97
  },
98
98
  mcp: { instructions },
99
99
  openapi: scope(document),
100
- openapiConfig: { forwardHeaders: ['authorization'], mode: 'namespace' },
100
+ // `compact` + `security: false` keep tool schemas lean: no examples,
101
+ // oversized regexes, or per-tool credential inputs (auth rides the
102
+ // forwarded `Authorization` header).
103
+ openapiConfig: {
104
+ compact: true,
105
+ forwardHeaders: ['authorization'],
106
+ mode: 'namespace',
107
+ security: false,
108
+ },
101
109
  version,
102
110
  })
103
111
  if (docs !== false)
@@ -217,11 +225,43 @@ function scope(document: Document): Openapi.OpenAPISpec {
217
225
  return tags_operation?.some((tag) => tags.has(tag)) ?? false
218
226
  })
219
227
  if (entries.some(([method]) => methods.includes(method)))
220
- paths[path] = Object.fromEntries(entries)
228
+ paths[path] = Object.fromEntries(
229
+ entries.map(([method, operation]) =>
230
+ methods.includes(method) ? [method, condense(operation)] : [method, operation],
231
+ ),
232
+ )
221
233
  }
222
234
  return { ...document, paths }
223
235
  }
224
236
 
237
+ /** Terse tool descriptions for parameters repeated across most operations; the public document keeps the full prose. */
238
+ const parameterDescriptions: Record<string, string> = {
239
+ chainId: 'Chain: `mainnet` (default), `testnet`, or a numeric chain id.',
240
+ cursor: 'Keyset cursor from a previous response `nextCursor`; omit for the first page.',
241
+ include: 'Comma-separated related resources to include.',
242
+ limit: 'Items per page.',
243
+ order: 'Sort order: `asc` or `desc` (default).',
244
+ page: '1-indexed page number; mutually exclusive with `cursor`.',
245
+ }
246
+
247
+ /** Swaps shared query-parameter prose for the terse tool descriptions. */
248
+ function condense(operation: unknown) {
249
+ const { parameters } = operation as {
250
+ parameters?: readonly { in?: string; name?: string }[] | undefined
251
+ }
252
+ if (!parameters) return operation
253
+ return {
254
+ ...(operation as object),
255
+ parameters: parameters.map((parameter) => {
256
+ const description =
257
+ parameter.in === 'query' && parameter.name
258
+ ? parameterDescriptions[parameter.name]
259
+ : undefined
260
+ return description ? { ...parameter, description } : parameter
261
+ }),
262
+ }
263
+ }
264
+
225
265
  export declare namespace mcp {
226
266
  /** Options for the MCP route group. */
227
267
  type Options = {