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
@@ -53,11 +53,11 @@ export namespace schema {
53
53
  z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
54
54
  ),
55
55
  projectId: z.optional(z.string()).check(
56
- z.describe('Owning project id (`prj_…`).'), // prettier-ignore
56
+ z.describe('Attributed project id (`prj_…`).'), // prettier-ignore
57
57
  z.meta({ examples: ['prj_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
58
58
  ),
59
59
  scopes: z
60
- .array(z.enum([...Scope.catalog.map((entry) => entry.scope), Scope.wildcard]))
60
+ .array(z.string().check(z.minLength(1)))
61
61
  .check(
62
62
  z.describe(
63
63
  "Granted scopes. '*' (grant-all) appears only on keys minted by the super admin.",
@@ -138,7 +138,15 @@ export namespace schema {
138
138
  .optional(z.string().check(z.minLength(1), z.maxLength(100)))
139
139
  .check(z.describe('Human-readable key name.'), z.meta({ examples: ['CI'] })),
140
140
  scopes: z
141
- ._default(z.array(z.enum(Scope.catalog.map((entry) => entry.scope))), [])
141
+ ._default(
142
+ z.array(
143
+ z.string().check(
144
+ z.minLength(1),
145
+ z.refine((scope) => scope !== Scope.wildcard),
146
+ ),
147
+ ),
148
+ [],
149
+ )
142
150
  .check(z.describe('Scopes to grant.'), z.meta({ examples: [['data:read']] })),
143
151
  })
144
152
  .check(z.describe('Fields for minting an API key.'))
@@ -210,11 +218,7 @@ export namespace schema {
210
218
  }
211
219
 
212
220
  /**
213
- * Mounts the `/orgs/:orgId/projects/:projectId/api-keys` resource
214
- * (project-scoped mint/list/revoke) plus the org-wide read
215
- * `/orgs/:orgId/api-keys`, over the KV state store. Access follows the
216
- * org/project scope (owning session or super admin); the token appears once,
217
- * at mint.
221
+ * Mounts project-attributed API key mint, list, and revoke routes plus the org-wide list. Access follows the caller's organization ownership.
218
222
  */
219
223
  export function apiKeys(options: apiKeys.Options = {}) {
220
224
  const hidden = !options.enabled
@@ -233,7 +237,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
233
237
  message: 'Check the request body and try again.',
234
238
  }),
235
239
  OpenApi.describeRoute({
236
- description: 'Mint a project-scoped API key. The token appears once and is never shown again.', // prettier-ignore
240
+ description: 'Mint a project-attributed API key. The token appears once and is never shown again.', // prettier-ignore
237
241
  hide: hidden,
238
242
  operationId: 'createApiKey',
239
243
  responses: OpenApi.responses({
@@ -273,20 +277,19 @@ export function apiKeys(options: apiKeys.Options = {}) {
273
277
 
274
278
  const body = c.req.valid('json')
275
279
 
276
- // Sessions mint only self-issuable scopes; the super admin grants the
277
- // full catalog. Self-serve covers the whole catalog now that billing
278
- // ships, so this is a safety net: a future non-self-serve scope would
279
- // be refused here without further changes.
280
- if (createdBy !== 'super_admin') {
281
- const denied = body.scopes.filter((scope) => !Scope.selfServe.includes(scope))
282
- if (denied.length > 0)
283
- return Response.error(c, {
284
- code: 'scope_not_issuable',
285
- message: 'The requested scopes are not issuable.',
286
- status: 403,
287
- })
288
- }
280
+ if (!body.scopes.every((scope) => Scope.includes(c.get('scopeCatalog'), scope)))
281
+ return Response.error(c, {
282
+ code: 'body_invalid',
283
+ message: 'Check the request body and try again.',
284
+ status: 400,
285
+ })
289
286
 
287
+ if (!canIssue(c, body.scopes))
288
+ return Response.error(c, {
289
+ code: 'scope_not_issuable',
290
+ message: 'The requested scopes are not issuable.',
291
+ status: 403,
292
+ })
290
293
  try {
291
294
  const orgId = Auth.org(c).id
292
295
  // Snapshot billing onto the record so the auth path throttles a
@@ -296,15 +299,19 @@ export function apiKeys(options: apiKeys.Options = {}) {
296
299
  body.environment === 'sandbox'
297
300
  ? await Billing.active(Db.get(c.get('db')), orgId, 'sandbox')
298
301
  : undefined
299
- const { record, token } = await ApiKeys.mint(kv.store, {
300
- createdBy,
301
- environment: body.environment,
302
- ...(billingActive === undefined ? {} : { billingActive }),
303
- ...(body.name === undefined ? {} : { name: body.name }),
304
- orgId,
305
- projectId: Auth.project(c).id,
306
- scopes: body.scopes,
307
- })
302
+ const { record, token } = await ApiKeys.mint(
303
+ kv.store,
304
+ {
305
+ createdBy,
306
+ environment: body.environment,
307
+ ...(billingActive === undefined ? {} : { billingActive }),
308
+ ...(body.name === undefined ? {} : { name: body.name }),
309
+ orgId,
310
+ projectId: Auth.project(c).id,
311
+ scopes: body.scopes,
312
+ },
313
+ { scopeCatalog: c.get('scopeCatalog') },
314
+ )
308
315
  return c.json(
309
316
  Response.validated(schema.createApiKey.Response, { ...serializeKey(record), token }),
310
317
  200,
@@ -316,7 +323,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
316
323
  )
317
324
  .get(
318
325
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys',
319
- Auth.policy({ session: true }),
326
+ Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
320
327
  Auth.ensureOrg(),
321
328
  Auth.ensureProject(),
322
329
  OpenApi.validate('param', schema.Params, {
@@ -344,7 +351,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
344
351
  tags: ['API Keys'],
345
352
  }),
346
353
  async (c) => {
347
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
354
+ if (Auth.narrowAccess) return Auth.accessError(c)
348
355
  if (Auth.narrowScope) return Auth.ensureProjectError(c)
349
356
  if (OpenApi.narrowValidation)
350
357
  return OpenApi.validationError(c, {
@@ -363,6 +370,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
363
370
  const records = await ApiKeys.listByOrg(kv.store, Auth.org(c).id, {
364
371
  environment: c.req.valid('query').environment,
365
372
  projectId: Auth.project(c).id,
373
+ scopeCatalog: c.get('scopeCatalog'),
366
374
  })
367
375
  return c.json(
368
376
  Response.validated(schema.listApiKeys.Response, {
@@ -377,7 +385,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
377
385
  )
378
386
  .get(
379
387
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/api-keys',
380
- Auth.policy({ session: true }),
388
+ Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
381
389
  Auth.ensureOrg(),
382
390
  OpenApi.validate('param', schema.OrgParams, {
383
391
  code: 'param_invalid',
@@ -405,7 +413,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
405
413
  tags: ['API Keys'],
406
414
  }),
407
415
  async (c) => {
408
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
416
+ if (Auth.narrowAccess) return Auth.accessError(c)
409
417
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
410
418
  if (OpenApi.narrowValidation)
411
419
  return OpenApi.validationError(c, {
@@ -423,6 +431,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
423
431
  try {
424
432
  const records = await ApiKeys.listByOrg(kv.store, Auth.org(c).id, {
425
433
  environment: c.req.valid('query').environment,
434
+ scopeCatalog: c.get('scopeCatalog'),
426
435
  })
427
436
  return c.json(
428
437
  Response.validated(schema.listOrgApiKeys.Response, {
@@ -437,7 +446,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
437
446
  )
438
447
  .delete(
439
448
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys/:keyId{key_[A-Za-z0-9_-]+}',
440
- Auth.policy({ session: true }),
449
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
441
450
  Auth.ensureOrg({ role: 'admin' }),
442
451
  Auth.ensureProject(),
443
452
  OpenApi.validate('param', schema.KeyParams, {
@@ -459,7 +468,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
459
468
  tags: ['API Keys'],
460
469
  }),
461
470
  async (c) => {
462
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
471
+ if (Auth.narrowAccess) return Auth.accessError(c)
463
472
  if (Auth.narrowScope) return Auth.ensureProjectError(c)
464
473
  if (OpenApi.narrowValidation)
465
474
  return OpenApi.validationError(c, {
@@ -473,7 +482,9 @@ export function apiKeys(options: apiKeys.Options = {}) {
473
482
  try {
474
483
  // Bind the key to the scoped project before revoking: a key id from
475
484
  // another org or project must be indistinguishable from a missing one.
476
- const record = await ApiKeys.get(kv.store, keyId)
485
+ const record = await ApiKeys.get(kv.store, keyId, {
486
+ scopeCatalog: c.get('scopeCatalog'),
487
+ })
477
488
  if (!record || record.orgId !== Auth.org(c).id || record.projectId !== Auth.project(c).id)
478
489
  return keyNotFound(c)
479
490
  const revoked = await ApiKeys.revoke(kv.store, keyId)
@@ -518,6 +529,20 @@ function mintedBy(c: Context<App.Environment>) {
518
529
  return undefined
519
530
  }
520
531
 
532
+ /** Checks whether the caller may grant every requested scope. */
533
+ function canIssue(c: Context<App.Environment>, scopes: readonly Scope.Id[]) {
534
+ const principal = Auth.getPrincipal(c)
535
+ if (principal?.type === 'super_admin') return true
536
+ if (principal?.type === 'session') {
537
+ const selfServe = Scope.selfServeFrom(c.get('scopeCatalog'))
538
+ return (
539
+ scopes.every((scope) => selfServe.includes(scope)) &&
540
+ (!scopes.includes('management:write') || Auth.membership(c)?.role === 'owner')
541
+ )
542
+ }
543
+ return false
544
+ }
545
+
521
546
  function keyNotFound(c: Context<App.Environment>) {
522
547
  return Response.error(c, {
523
548
  code: 'api_key_not_found',
@@ -234,14 +234,13 @@ export namespace schema {
234
234
  }
235
235
 
236
236
  /**
237
- * Mounts the billing surface: an organization's billing status. Orgs without
238
- * a Stripe billing source read as `none`. Session lane; reads for members.
237
+ * Mounts billing routes. Reads require membership or scoped API key access; organizations without Stripe state return `none`.
239
238
  */
240
239
  export function billing() {
241
240
  return new Hono<Environment>()
242
241
  .get(
243
242
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing',
244
- Auth.policy({ session: true }),
243
+ Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
245
244
  Auth.ensureOrg(),
246
245
  OpenApi.validate('param', schema.Params, {
247
246
  code: 'param_invalid',
@@ -264,7 +263,7 @@ export function billing() {
264
263
  tags: ['Billing'],
265
264
  }),
266
265
  async (c) => {
267
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
266
+ if (Auth.narrowAccess) return Auth.accessError(c)
268
267
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
269
268
  if (OpenApi.narrowValidation)
270
269
  return OpenApi.validationError(c, {
@@ -294,7 +293,7 @@ export function billing() {
294
293
  )
295
294
  .patch(
296
295
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing',
297
- Auth.policy({ session: true }),
296
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
298
297
  Auth.ensureOrg({ role: 'owner' }),
299
298
  OpenApi.validate('param', schema.Params, {
300
299
  code: 'param_invalid',
@@ -325,7 +324,7 @@ export function billing() {
325
324
  tags: ['Billing'],
326
325
  }),
327
326
  async (c) => {
328
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
327
+ if (Auth.narrowAccess) return Auth.accessError(c)
329
328
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
330
329
  if (OpenApi.narrowValidation)
331
330
  return OpenApi.validationError(c, {
@@ -381,7 +380,7 @@ export function billing() {
381
380
  )
382
381
  .post(
383
382
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/stripe/checkout',
384
- Auth.policy({ session: true }),
383
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
385
384
  Auth.ensureOrg({ role: 'owner' }),
386
385
  OpenApi.validate('param', schema.Params, {
387
386
  code: 'param_invalid',
@@ -408,7 +407,7 @@ export function billing() {
408
407
  tags: ['Billing'],
409
408
  }),
410
409
  async (c) => {
411
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
410
+ if (Auth.narrowAccess) return Auth.accessError(c)
412
411
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
413
412
  if (OpenApi.narrowValidation)
414
413
  return OpenApi.validationError(c, {
@@ -466,7 +465,7 @@ export function billing() {
466
465
  )
467
466
  .post(
468
467
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/stripe/manage',
469
- Auth.policy({ session: true }),
468
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
470
469
  Auth.ensureOrg({ role: 'owner' }),
471
470
  OpenApi.validate('param', schema.Params, {
472
471
  code: 'param_invalid',
@@ -493,7 +492,7 @@ export function billing() {
493
492
  tags: ['Billing'],
494
493
  }),
495
494
  async (c) => {
496
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
495
+ if (Auth.narrowAccess) return Auth.accessError(c)
497
496
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
498
497
  if (OpenApi.narrowValidation)
499
498
  return OpenApi.validationError(c, {
@@ -529,7 +528,7 @@ export function billing() {
529
528
  )
530
529
  .get(
531
530
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/payment-methods',
532
- Auth.policy({ session: true }),
531
+ Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
533
532
  Auth.ensureOrg(),
534
533
  OpenApi.validate('param', schema.Params, {
535
534
  code: 'param_invalid',
@@ -555,7 +554,7 @@ export function billing() {
555
554
  tags: ['Billing'],
556
555
  }),
557
556
  async (c) => {
558
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
557
+ if (Auth.narrowAccess) return Auth.accessError(c)
559
558
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
560
559
  if (OpenApi.narrowValidation)
561
560
  return OpenApi.validationError(c, {
@@ -615,7 +614,7 @@ export function billing() {
615
614
  )
616
615
  .delete(
617
616
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/payment-methods/:methodId{pm_[A-Za-z0-9_-]+}',
618
- Auth.policy({ session: true }),
617
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
619
618
  Auth.ensureOrg({ role: 'owner' }),
620
619
  OpenApi.validate('param', schema.MethodParams, {
621
620
  code: 'param_invalid',
@@ -642,7 +641,7 @@ export function billing() {
642
641
  tags: ['Billing'],
643
642
  }),
644
643
  async (c) => {
645
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
644
+ if (Auth.narrowAccess) return Auth.accessError(c)
646
645
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
647
646
  if (OpenApi.narrowValidation)
648
647
  return OpenApi.validationError(c, {
@@ -797,6 +796,7 @@ async function syncKeyBilling(
797
796
  active: options.status === 'active',
798
797
  environment: 'sandbox',
799
798
  orgId: options.orgId,
799
+ scopeCatalog: c.get('scopeCatalog'),
800
800
  })
801
801
  }
802
802
 
@@ -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, {