tapimo 0.4.1 → 0.4.4

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 (263) 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 +1990 -1442
  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/apps/verified-tokens.d.ts +57 -57
  41. package/dist/admin/ui.generated.d.ts +1 -1
  42. package/dist/admin/ui.generated.d.ts.map +1 -1
  43. package/dist/admin/ui.generated.js +53 -13
  44. package/dist/admin/ui.generated.js.map +1 -1
  45. package/dist/analytics/Analytics.d.ts +10 -0
  46. package/dist/analytics/Analytics.d.ts.map +1 -1
  47. package/dist/analytics/Analytics.js +8 -0
  48. package/dist/analytics/Analytics.js.map +1 -1
  49. package/dist/analytics/tables/requestEvents.d.ts +9 -0
  50. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  51. package/dist/analytics/tables/requestEvents.js +14 -0
  52. package/dist/analytics/tables/requestEvents.js.map +1 -1
  53. package/dist/apps/data/App.d.ts +1304 -1304
  54. package/dist/apps/data/FundingRouteProviders.d.ts +4 -22
  55. package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
  56. package/dist/apps/data/FundingRouteProviders.js +10 -120
  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/fee-amm.d.ts +20 -20
  62. package/dist/apps/data/routes/funding.js +7 -7
  63. package/dist/apps/data/routes/funding.js.map +1 -1
  64. package/dist/apps/data/routes/indexer.d.ts +5 -5
  65. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  66. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  67. package/dist/apps/management/App.d.ts +808 -267
  68. package/dist/apps/management/App.d.ts.map +1 -1
  69. package/dist/apps/management/App.js +2 -2
  70. package/dist/apps/management/Billing.d.ts +3 -0
  71. package/dist/apps/management/Billing.d.ts.map +1 -1
  72. package/dist/apps/management/Billing.js +7 -2
  73. package/dist/apps/management/Billing.js.map +1 -1
  74. package/dist/apps/management/routes/api-keys.d.ts +116 -100
  75. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  76. package/dist/apps/management/routes/api-keys.js +41 -30
  77. package/dist/apps/management/routes/api-keys.js.map +1 -1
  78. package/dist/apps/management/routes/billing.d.ts +182 -50
  79. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  80. package/dist/apps/management/routes/billing.js +14 -14
  81. package/dist/apps/management/routes/billing.js.map +1 -1
  82. package/dist/apps/management/routes/invitations.d.ts +93 -20
  83. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  84. package/dist/apps/management/routes/invitations.js +12 -15
  85. package/dist/apps/management/routes/invitations.js.map +1 -1
  86. package/dist/apps/management/routes/members.d.ts +89 -18
  87. package/dist/apps/management/routes/members.d.ts.map +1 -1
  88. package/dist/apps/management/routes/members.js +10 -10
  89. package/dist/apps/management/routes/members.js.map +1 -1
  90. package/dist/apps/management/routes/orgs.d.ts +100 -40
  91. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  92. package/dist/apps/management/routes/orgs.js +19 -19
  93. package/dist/apps/management/routes/orgs.js.map +1 -1
  94. package/dist/apps/management/routes/projects.d.ts +122 -33
  95. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  96. package/dist/apps/management/routes/projects.js +12 -13
  97. package/dist/apps/management/routes/projects.js.map +1 -1
  98. package/dist/apps/management/routes/scopes.d.ts +4 -18
  99. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  100. package/dist/apps/management/routes/scopes.js +6 -7
  101. package/dist/apps/management/routes/scopes.js.map +1 -1
  102. package/dist/apps/management/routes/usage.d.ts +63 -19
  103. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  104. package/dist/apps/management/routes/usage.js +9 -10
  105. package/dist/apps/management/routes/usage.js.map +1 -1
  106. package/dist/apps/mcp.d.ts.map +1 -1
  107. package/dist/apps/mcp.js +38 -4
  108. package/dist/apps/mcp.js.map +1 -1
  109. package/dist/apps/relay/App.d.ts +26 -4
  110. package/dist/apps/relay/App.d.ts.map +1 -1
  111. package/dist/apps/relay/App.js +17 -14
  112. package/dist/apps/relay/App.js.map +1 -1
  113. package/dist/cloudflare.d.ts +2 -0
  114. package/dist/cloudflare.d.ts.map +1 -1
  115. package/dist/cloudflare.js +4 -0
  116. package/dist/cloudflare.js.map +1 -1
  117. package/dist/db/Db.d.ts +5 -3
  118. package/dist/db/Db.d.ts.map +1 -1
  119. package/dist/db/Db.js +52 -0
  120. package/dist/db/Db.js.map +1 -1
  121. package/dist/db/Schema.d.ts +13 -0
  122. package/dist/db/Schema.d.ts.map +1 -1
  123. package/dist/db/Schema.js +16 -1
  124. package/dist/db/Schema.js.map +1 -1
  125. package/dist/db/tables/adminAuditLogs.d.ts +27 -0
  126. package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
  127. package/dist/db/tables/adminAuditLogs.js +19 -0
  128. package/dist/db/tables/adminAuditLogs.js.map +1 -0
  129. package/dist/db/tables/invitations.d.ts +1 -1
  130. package/dist/db/tables/invitations.d.ts.map +1 -1
  131. package/dist/db/tables/memberships.d.ts +32 -4
  132. package/dist/db/tables/memberships.d.ts.map +1 -1
  133. package/dist/db/tables/memberships.js +31 -0
  134. package/dist/db/tables/memberships.js.map +1 -1
  135. package/dist/db/tables/organizations.d.ts +43 -1
  136. package/dist/db/tables/organizations.d.ts.map +1 -1
  137. package/dist/db/tables/organizations.js +103 -0
  138. package/dist/db/tables/organizations.js.map +1 -1
  139. package/dist/db/tables/projects.d.ts +25 -0
  140. package/dist/db/tables/projects.d.ts.map +1 -1
  141. package/dist/db/tables/projects.js +22 -0
  142. package/dist/db/tables/projects.js.map +1 -1
  143. package/dist/db/tables/webhookDeliveries.d.ts +26 -0
  144. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  145. package/dist/db/tables/webhookDeliveries.js +37 -0
  146. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  147. package/dist/internal/Auth.d.ts +46 -40
  148. package/dist/internal/Auth.d.ts.map +1 -1
  149. package/dist/internal/Auth.js +118 -69
  150. package/dist/internal/Auth.js.map +1 -1
  151. package/dist/internal/Cursor.d.ts +1 -1
  152. package/dist/internal/Cursor.d.ts.map +1 -1
  153. package/dist/internal/Cursor.js +4 -0
  154. package/dist/internal/Cursor.js.map +1 -1
  155. package/dist/internal/EdgeCache.d.ts.map +1 -1
  156. package/dist/internal/EdgeCache.js +2 -0
  157. package/dist/internal/EdgeCache.js.map +1 -1
  158. package/dist/internal/Errors.d.ts +6 -0
  159. package/dist/internal/Errors.d.ts.map +1 -0
  160. package/dist/internal/Errors.js +16 -0
  161. package/dist/internal/Errors.js.map +1 -0
  162. package/dist/internal/FundingRoutes.d.ts +15 -43
  163. package/dist/internal/FundingRoutes.d.ts.map +1 -1
  164. package/dist/internal/FundingRoutes.js +8 -17
  165. package/dist/internal/FundingRoutes.js.map +1 -1
  166. package/dist/internal/Log.d.ts +16 -1
  167. package/dist/internal/Log.d.ts.map +1 -1
  168. package/dist/internal/Log.js +5 -3
  169. package/dist/internal/Log.js.map +1 -1
  170. package/dist/internal/Store.d.ts +61 -16
  171. package/dist/internal/Store.d.ts.map +1 -1
  172. package/dist/internal/Store.js +72 -17
  173. package/dist/internal/Store.js.map +1 -1
  174. package/dist/internal/Viem.d.ts +1 -1
  175. package/dist/internal/index.d.ts +27 -0
  176. package/dist/internal/index.d.ts.map +1 -0
  177. package/dist/internal/index.js +27 -0
  178. package/dist/internal/index.js.map +1 -0
  179. package/package.json +8 -3
  180. package/src/ApiKey.ts +2 -3
  181. package/src/ApiKeys.ts +95 -17
  182. package/src/App.test.ts +104 -6
  183. package/src/App.ts +29 -9
  184. package/src/Assets.ts +8 -0
  185. package/src/Auth.test-d.ts +36 -0
  186. package/src/Auth.ts +20 -1
  187. package/src/Client.test-d.ts +17 -17
  188. package/src/Scope.test.ts +34 -0
  189. package/src/Scope.ts +53 -17
  190. package/src/admin/App.test.ts +33 -3
  191. package/src/admin/App.ts +49 -14
  192. package/src/admin/apps/api-keys.ts +10 -3
  193. package/src/admin/apps/organizations.test.ts +455 -0
  194. package/src/admin/apps/organizations.ts +953 -0
  195. package/src/admin/apps/scopes.test.ts +10 -0
  196. package/src/admin/ui/plugin.ts +1 -1
  197. package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
  198. package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
  199. package/src/admin/ui/src/lib/api-keys.ts +36 -19
  200. package/src/admin/ui/src/lib/config.ts +2 -3
  201. package/src/admin/ui/src/lib/organizations.test.ts +89 -0
  202. package/src/admin/ui/src/lib/organizations.ts +262 -0
  203. package/src/admin/ui/src/lib/query-client.ts +3 -1
  204. package/src/admin/ui/src/lib/theme.ts +57 -0
  205. package/src/admin/ui/src/routeTree.gen.ts +47 -4
  206. package/src/admin/ui/src/routes/__root.tsx +18 -3
  207. package/src/admin/ui/src/routes/api-keys.tsx +52 -7
  208. package/src/admin/ui/src/routes/index.tsx +1 -1
  209. package/src/admin/ui/src/routes/organizations.tsx +158 -0
  210. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
  211. package/src/admin/ui/src/start.ts +6 -0
  212. package/src/admin/ui.generated.ts +63 -13
  213. package/src/analytics/Analytics.ts +25 -0
  214. package/src/analytics/tables/requestEvents.ts +33 -0
  215. package/src/apps/composition.test.ts +82 -2
  216. package/src/apps/data/FundingRouteProviders.test.ts +37 -93
  217. package/src/apps/data/FundingRouteProviders.ts +14 -161
  218. package/src/apps/data/routes/funding.test.ts +7 -42
  219. package/src/apps/data/routes/funding.ts +8 -8
  220. package/src/apps/data/routes/indexer.test.ts +2 -2
  221. package/src/apps/data/routes/webhooks.test.ts +2 -2
  222. package/src/apps/management/App.ts +2 -2
  223. package/src/apps/management/Billing.ts +10 -2
  224. package/src/apps/management/routes/api-keys.test.ts +137 -1
  225. package/src/apps/management/routes/api-keys.ts +63 -38
  226. package/src/apps/management/routes/billing.ts +14 -14
  227. package/src/apps/management/routes/invitations.test.ts +102 -1
  228. package/src/apps/management/routes/invitations.ts +13 -15
  229. package/src/apps/management/routes/members.ts +10 -10
  230. package/src/apps/management/routes/orgs.test.ts +78 -16
  231. package/src/apps/management/routes/orgs.ts +21 -20
  232. package/src/apps/management/routes/projects.ts +12 -13
  233. package/src/apps/management/routes/scopes.test.ts +16 -0
  234. package/src/apps/management/routes/scopes.ts +6 -7
  235. package/src/apps/management/routes/usage.ts +9 -14
  236. package/src/apps/mcp.test.ts +72 -7
  237. package/src/apps/mcp.ts +46 -4
  238. package/src/apps/relay/App.test.ts +23 -18
  239. package/src/apps/relay/App.ts +35 -14
  240. package/src/cloudflare.ts +5 -0
  241. package/src/db/Db.ts +59 -3
  242. package/src/db/Schema.ts +20 -1
  243. package/src/db/tables/adminAuditLogs.ts +47 -0
  244. package/src/db/tables/invitations.ts +1 -1
  245. package/src/db/tables/memberships.ts +59 -3
  246. package/src/db/tables/organizations.ts +148 -1
  247. package/src/db/tables/projects.ts +47 -0
  248. package/src/db/tables/webhookDeliveries.ts +83 -0
  249. package/src/internal/Auth.test.ts +571 -23
  250. package/src/internal/Auth.ts +166 -94
  251. package/src/internal/Cursor.test.ts +10 -0
  252. package/src/internal/Cursor.ts +5 -1
  253. package/src/internal/EdgeCache.test.ts +11 -0
  254. package/src/internal/EdgeCache.ts +2 -0
  255. package/src/internal/FundingRoutes.test.ts +8 -8
  256. package/src/internal/FundingRoutes.ts +25 -29
  257. package/src/internal/Log.test.ts +41 -3
  258. package/src/internal/Log.ts +23 -4
  259. package/src/internal/Store.test-d.ts +15 -0
  260. package/src/internal/Store.test.ts +153 -2
  261. package/src/internal/Store.ts +157 -41
  262. package/src/internal/index.test.ts +34 -0
  263. package/src/internal/index.ts +26 -0
@@ -6,6 +6,7 @@ import * as Invitations from '../../../db/tables/invitations.js'
6
6
  import * as InvitationsApp from './invitations.js'
7
7
  import * as Memberships from '../../../db/tables/memberships.js'
8
8
  import * as Orgs from './orgs.js'
9
+ import * as Organizations from '../../../db/tables/organizations.js'
9
10
  import * as Users from '../../../db/tables/users.js'
10
11
 
11
12
  /** Origin pinned for SIWE domain binding; Hono test requests use this host. */
@@ -36,11 +37,26 @@ function sender() {
36
37
  /** Super admin secret configured on the test app. */
37
38
  const secret = 'tempo:sk:d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3'
38
39
 
39
- function createApp(options: { db?: ReturnType<typeof TestApp.database>; email?: App.Email } = {}) {
40
+ /** Organization management credential used by API-key route tests. */
41
+ const management = {
42
+ id: 'key_management',
43
+ orgId: 'org_management',
44
+ scopes: ['management:read', 'management:write'],
45
+ token: 'secret_management_key',
46
+ } satisfies TestApp.kvStore.Key
47
+
48
+ function createApp(
49
+ options: {
50
+ db?: ReturnType<typeof TestApp.database>
51
+ email?: App.Email
52
+ kv?: { store: ReturnType<typeof TestApp.kvStore> }
53
+ } = {},
54
+ ) {
40
55
  return TestApp.create({
41
56
  auth: { superAdmin: { secret } },
42
57
  db: options.db ?? TestApp.database(),
43
58
  ...(options.email ? { email: options.email } : {}),
59
+ ...(options.kv ? { kv: options.kv } : {}),
44
60
  session: { wallet: { origin } },
45
61
  })
46
62
  }
@@ -55,6 +71,47 @@ async function session(app: ReturnType<typeof createApp>) {
55
71
  }
56
72
 
57
73
  describe('invitations', () => {
74
+ test('lets management keys manage organization invitations', async () => {
75
+ const db = TestApp.database()
76
+ await Organizations.create(db, { id: management.orgId, name: 'Acme' })
77
+ const app = createApp({
78
+ db,
79
+ kv: { store: TestApp.kvStore({ keys: [management] }) },
80
+ })
81
+ const path = `/v1/orgs/${management.orgId}/invitations`
82
+ const headers = {
83
+ 'content-type': 'application/json',
84
+ 'tempo-api-key': management.token,
85
+ }
86
+
87
+ const created = await app.request(path, {
88
+ body: JSON.stringify({ email: 'dev@example.com', role: 'owner' }),
89
+ headers,
90
+ method: 'POST',
91
+ })
92
+ expect(created.status).toBe(200)
93
+ const invitation = await TestApp.json(created, InvitationsApp.schema.Invitation)
94
+ expect(invitation.invitedBy).toBe(management.id)
95
+
96
+ const listed = await app.request(path, { headers })
97
+ expect(listed.status).toBe(200)
98
+ expect(
99
+ (await TestApp.json(listed, InvitationsApp.schema.listInvitations.Response)).data.map(
100
+ ({ id }) => id,
101
+ ),
102
+ ).toEqual([invitation.id])
103
+
104
+ expect((await app.request('/v1/invitations', { headers })).status).toBe(403)
105
+ expect(
106
+ (
107
+ await app.request(`${path}/${invitation.id}`, {
108
+ headers,
109
+ method: 'DELETE',
110
+ })
111
+ ).status,
112
+ ).toBe(200)
113
+ })
114
+
58
115
  test('invites, surfaces, and accepts into membership', async () => {
59
116
  const db = TestApp.database()
60
117
  const { email, sent } = sender()
@@ -123,6 +180,50 @@ describe('invitations', () => {
123
180
  expect(again.status).toBe(404)
124
181
  })
125
182
 
183
+ test('accepting an invitation preserves membership in another organization', async () => {
184
+ const db = TestApp.database()
185
+ const app = createApp({ db })
186
+ const invitee = await session(app)
187
+ const createdA = await app.request('/v1/orgs', json('POST', invitee.cookie, { name: 'Org A' }))
188
+ const orgA = await TestApp.json(createdA, Orgs.schema.Organization)
189
+ await Users.setEmail(db, invitee.userId, 'dev@example.com')
190
+
191
+ const owner = await session(app)
192
+ const createdB = await app.request('/v1/orgs', json('POST', owner.cookie, { name: 'Org B' }))
193
+ const orgB = await TestApp.json(createdB, Orgs.schema.Organization)
194
+ const invited = await app.request(
195
+ `/v1/orgs/${orgB.id}/invitations`,
196
+ json('POST', owner.cookie, { email: 'dev@example.com', role: 'member' }),
197
+ )
198
+ const invitation = await TestApp.json(invited, InvitationsApp.schema.Invitation)
199
+
200
+ const accepted = await app.request(`/v1/invitations/${invitation.id}/accept`, {
201
+ headers: { cookie: invitee.cookie },
202
+ method: 'POST',
203
+ })
204
+ expect(accepted.status).toBe(200)
205
+
206
+ const listed = await app.request('/v1/orgs', { headers: { cookie: invitee.cookie } })
207
+ const organizations = await TestApp.json(listed, Orgs.schema.listOrganizations.Response)
208
+ expect(
209
+ organizations.data
210
+ .map(({ id, role }) => ({ id, role }))
211
+ .sort((a, b) => a.id.localeCompare(b.id)),
212
+ ).toEqual(
213
+ [
214
+ { id: orgA.id, role: 'owner' },
215
+ { id: orgB.id, role: 'member' },
216
+ ].sort((a, b) => a.id.localeCompare(b.id)),
217
+ )
218
+
219
+ const [readA, readB] = await Promise.all([
220
+ app.request(`/v1/orgs/${orgA.id}`, { headers: { cookie: invitee.cookie } }),
221
+ app.request(`/v1/orgs/${orgB.id}`, { headers: { cookie: invitee.cookie } }),
222
+ ])
223
+ expect(readA.status).toBe(200)
224
+ expect(readB.status).toBe(200)
225
+ })
226
+
126
227
  test('accepting elevates an existing member but never demotes', async () => {
127
228
  const db = TestApp.database()
128
229
  const app = createApp({ db })
@@ -48,7 +48,7 @@ export namespace schema {
48
48
  invitedBy: z
49
49
  .string()
50
50
  .check(
51
- z.describe('User id (`usr_…`) that created the invitation.'),
51
+ z.describe('User or API key id that created the invitation.'),
52
52
  z.meta({ examples: ['usr_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
53
53
  ),
54
54
  orgId: z
@@ -204,16 +204,14 @@ export namespace schema {
204
204
 
205
205
  /**
206
206
  * Mounts the invitations surface: org-scoped management
207
- * (`/orgs/:orgId/invitations`, admins and owners) plus the invitee's side
208
- * (`/invitations`, matched against the session's verified email). Invitation
209
- * emails dispatch through the sender the management group publishes on context
210
- * (`c.get('email')`), best-effort.
207
+ * (`/orgs/:orgId/invitations`) plus session-only invitee actions. Invitation
208
+ * emails dispatch through the management group's sender, best-effort.
211
209
  */
212
210
  export function invitations() {
213
211
  return new Hono<Environment>()
214
212
  .post(
215
213
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/invitations',
216
- Auth.policy({ session: true }),
214
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
217
215
  Auth.ensureOrg({ role: 'admin' }),
218
216
  OpenApi.validate('param', schema.Params, {
219
217
  code: 'param_invalid',
@@ -234,7 +232,7 @@ export function invitations() {
234
232
  tags: ['Invitations'],
235
233
  }),
236
234
  async (c) => {
237
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
235
+ if (Auth.narrowAccess) return Auth.accessError(c)
238
236
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
239
237
  if (OpenApi.narrowValidation)
240
238
  return OpenApi.validationError(c, {
@@ -248,9 +246,7 @@ export function invitations() {
248
246
  })
249
247
 
250
248
  const body = c.req.valid('json')
251
- // Owners invite anyone; everyone else grants only roles strictly below
252
- // their own (an admin invites members, never peers). The super admin
253
- // has no membership and may grant any role.
249
+ // Owners, write-scoped keys, and the super admin grant any role; admins grant only members.
254
250
  const caller = Auth.membership(c)
255
251
  if (
256
252
  caller &&
@@ -262,7 +258,9 @@ export function invitations() {
262
258
  message: 'Cannot invite at or above your own role',
263
259
  status: 403,
264
260
  })
265
- const invitedBy = caller?.userId ?? 'super_admin'
261
+ const principal = Auth.getPrincipal(c)
262
+ const invitedBy =
263
+ principal?.type === 'api_key' ? principal.id : (caller?.userId ?? 'super_admin')
266
264
 
267
265
  const db = Db.get(c.get('db'))
268
266
  try {
@@ -282,7 +280,7 @@ export function invitations() {
282
280
  )
283
281
  .get(
284
282
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/invitations',
285
- Auth.policy({ session: true }),
283
+ Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
286
284
  Auth.ensureOrg({ role: 'admin' }),
287
285
  OpenApi.validate('param', schema.Params, {
288
286
  code: 'param_invalid',
@@ -301,7 +299,7 @@ export function invitations() {
301
299
  tags: ['Invitations'],
302
300
  }),
303
301
  async (c) => {
304
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
302
+ if (Auth.narrowAccess) return Auth.accessError(c)
305
303
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
306
304
  if (OpenApi.narrowValidation)
307
305
  return OpenApi.validationError(c, {
@@ -325,7 +323,7 @@ export function invitations() {
325
323
  )
326
324
  .delete(
327
325
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/invitations/:invitationId{inv_[A-Za-z0-9_-]+}',
328
- Auth.policy({ session: true }),
326
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
329
327
  Auth.ensureOrg({ role: 'admin' }),
330
328
  OpenApi.validate('param', schema.InvitationParams, {
331
329
  code: 'param_invalid',
@@ -344,7 +342,7 @@ export function invitations() {
344
342
  tags: ['Invitations'],
345
343
  }),
346
344
  async (c) => {
347
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
345
+ if (Auth.narrowAccess) return Auth.accessError(c)
348
346
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
349
347
  if (OpenApi.narrowValidation)
350
348
  return OpenApi.validationError(c, {
@@ -113,14 +113,14 @@ export namespace schema {
113
113
 
114
114
  /**
115
115
  * Mounts the `/orgs/:orgId/members` resource: the organization's team.
116
- * Members read; owners change roles and remove members (any member may remove
117
- * themself). The last owner can never be demoted or removed.
116
+ * Members and read-scoped keys list. Owners and write-scoped keys manage;
117
+ * members may remove themselves. The last owner remains protected.
118
118
  */
119
119
  export function members() {
120
120
  return new Hono<App.Environment>()
121
121
  .get(
122
122
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/members',
123
- Auth.policy({ session: true }),
123
+ Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
124
124
  Auth.ensureOrg(),
125
125
  OpenApi.validate('param', schema.Params, {
126
126
  code: 'param_invalid',
@@ -139,7 +139,7 @@ export function members() {
139
139
  tags: ['Members'],
140
140
  }),
141
141
  async (c) => {
142
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
142
+ if (Auth.narrowAccess) return Auth.accessError(c)
143
143
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
144
144
  if (OpenApi.narrowValidation)
145
145
  return OpenApi.validationError(c, {
@@ -163,7 +163,7 @@ export function members() {
163
163
  )
164
164
  .patch(
165
165
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/members/:userId{usr_[A-Za-z0-9_-]+}',
166
- Auth.policy({ session: true }),
166
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
167
167
  Auth.ensureOrg({ role: 'owner' }),
168
168
  OpenApi.validate('param', schema.MemberParams, {
169
169
  code: 'param_invalid',
@@ -190,7 +190,7 @@ export function members() {
190
190
  tags: ['Members'],
191
191
  }),
192
192
  async (c) => {
193
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
193
+ if (Auth.narrowAccess) return Auth.accessError(c)
194
194
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
195
195
  if (OpenApi.narrowValidation)
196
196
  return OpenApi.validationError(c, {
@@ -226,14 +226,14 @@ export function members() {
226
226
  )
227
227
  .delete(
228
228
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/members/:userId{usr_[A-Za-z0-9_-]+}',
229
- Auth.policy({ session: true }),
229
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
230
230
  Auth.ensureOrg(),
231
231
  OpenApi.validate('param', schema.MemberParams, {
232
232
  code: 'param_invalid',
233
233
  message: 'Check the path parameters and try again.',
234
234
  }),
235
235
  OpenApi.describeRoute({
236
- description: 'Remove a member. Owners remove anyone; any member removes themself. The last owner cannot be removed.', // prettier-ignore
236
+ description: 'Remove a member. Owners and management-write keys remove anyone; members remove themselves. The last owner stays.', // prettier-ignore
237
237
  operationId: 'removeMember',
238
238
  responses: OpenApi.responses({
239
239
  errors: {
@@ -252,7 +252,7 @@ export function members() {
252
252
  tags: ['Members'],
253
253
  }),
254
254
  async (c) => {
255
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
255
+ if (Auth.narrowAccess) return Auth.accessError(c)
256
256
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
257
257
  if (OpenApi.narrowValidation)
258
258
  return OpenApi.validationError(c, {
@@ -261,7 +261,7 @@ export function members() {
261
261
  })
262
262
 
263
263
  const { userId } = c.req.valid('param')
264
- // Owners remove anyone; everyone else only themself (leaving the org).
264
+ // Owners and write-scoped keys remove anyone; other members only themselves.
265
265
  const caller = Auth.membership(c)
266
266
  if (caller && caller.role !== 'owner' && caller.userId !== userId)
267
267
  return Response.error(c, {
@@ -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)