tapimo 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (313) hide show
  1. package/README.md +31 -3
  2. package/dist/ApiKey.d.ts +1 -0
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +5 -2
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +28 -1
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +34 -1
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +7167 -4507
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +8 -0
  13. package/dist/App.js.map +1 -1
  14. package/dist/Scope.d.ts +11 -2
  15. package/dist/Scope.d.ts.map +1 -1
  16. package/dist/Scope.js +14 -3
  17. package/dist/Scope.js.map +1 -1
  18. package/dist/admin/App.d.ts +128 -118
  19. package/dist/admin/App.d.ts.map +1 -1
  20. package/dist/admin/apps/api-keys.d.ts +7 -0
  21. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  22. package/dist/admin/apps/scopes.d.ts +8 -0
  23. package/dist/admin/apps/scopes.d.ts.map +1 -1
  24. package/dist/admin/apps/scopes.js +3 -0
  25. package/dist/admin/apps/scopes.js.map +1 -1
  26. package/dist/admin/apps/verified-tokens.d.ts +57 -57
  27. package/dist/admin/ui.generated.d.ts +1 -1
  28. package/dist/admin/ui.generated.js +13 -13
  29. package/dist/admin/ui.generated.js.map +1 -1
  30. package/dist/analytics/Analytics.d.ts +151 -0
  31. package/dist/analytics/Analytics.d.ts.map +1 -0
  32. package/dist/analytics/Analytics.js +212 -0
  33. package/dist/analytics/Analytics.js.map +1 -0
  34. package/dist/analytics/tables/requestEvents.d.ts +215 -0
  35. package/dist/analytics/tables/requestEvents.d.ts.map +1 -0
  36. package/dist/analytics/tables/requestEvents.js +264 -0
  37. package/dist/analytics/tables/requestEvents.js.map +1 -0
  38. package/dist/apps/data/App.d.ts +1024 -624
  39. package/dist/apps/data/App.d.ts.map +1 -1
  40. package/dist/apps/data/App.js +36 -0
  41. package/dist/apps/data/App.js.map +1 -1
  42. package/dist/apps/data/routes/balances.d.ts +45 -45
  43. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  44. package/dist/apps/data/routes/exchanges.d.ts +455 -455
  45. package/dist/apps/data/routes/fee-amm.d.ts +101 -101
  46. package/dist/apps/data/routes/funding.d.ts +765 -0
  47. package/dist/apps/data/routes/funding.d.ts.map +1 -0
  48. package/dist/apps/data/routes/funding.js +383 -0
  49. package/dist/apps/data/routes/funding.js.map +1 -0
  50. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  51. package/dist/apps/data/routes/receipts.js +2 -8
  52. package/dist/apps/data/routes/receipts.js.map +1 -1
  53. package/dist/apps/data/routes/rpc.d.ts +29 -36
  54. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  55. package/dist/apps/data/routes/rpc.js +29 -1
  56. package/dist/apps/data/routes/rpc.js.map +1 -1
  57. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  58. package/dist/apps/data/routes/transfers.d.ts +56 -56
  59. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  60. package/dist/apps/data/routes/webhooks.d.ts +45 -45
  61. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  62. package/dist/apps/data/routes/webhooks.js +8 -8
  63. package/dist/apps/data/routes/webhooks.js.map +1 -1
  64. package/dist/apps/docs/App.d.ts +4 -2
  65. package/dist/apps/docs/App.d.ts.map +1 -1
  66. package/dist/apps/docs/App.js +27 -95
  67. package/dist/apps/docs/App.js.map +1 -1
  68. package/dist/apps/docs/reference.d.ts +21 -0
  69. package/dist/apps/docs/reference.d.ts.map +1 -0
  70. package/dist/apps/docs/reference.js +112 -0
  71. package/dist/apps/docs/reference.js.map +1 -0
  72. package/dist/apps/index.d.ts +1 -0
  73. package/dist/apps/index.d.ts.map +1 -1
  74. package/dist/apps/index.js +1 -0
  75. package/dist/apps/index.js.map +1 -1
  76. package/dist/apps/mcp.d.ts +76 -0
  77. package/dist/apps/mcp.d.ts.map +1 -0
  78. package/dist/apps/mcp.js +188 -0
  79. package/dist/apps/mcp.js.map +1 -0
  80. package/dist/apps/platform/App.d.ts +3005 -728
  81. package/dist/apps/platform/App.d.ts.map +1 -1
  82. package/dist/apps/platform/App.js +13 -3
  83. package/dist/apps/platform/App.js.map +1 -1
  84. package/dist/apps/platform/Billing.d.ts +279 -0
  85. package/dist/apps/platform/Billing.d.ts.map +1 -0
  86. package/dist/apps/platform/Billing.js +477 -0
  87. package/dist/apps/platform/Billing.js.map +1 -0
  88. package/dist/apps/platform/index.d.ts +1 -0
  89. package/dist/apps/platform/index.d.ts.map +1 -1
  90. package/dist/apps/platform/index.js +1 -0
  91. package/dist/apps/platform/index.js.map +1 -1
  92. package/dist/apps/platform/routes/api-keys.d.ts +341 -49
  93. package/dist/apps/platform/routes/api-keys.d.ts.map +1 -1
  94. package/dist/apps/platform/routes/api-keys.js +115 -13
  95. package/dist/apps/platform/routes/api-keys.js.map +1 -1
  96. package/dist/apps/platform/routes/billing.d.ts +1520 -0
  97. package/dist/apps/platform/routes/billing.d.ts.map +1 -0
  98. package/dist/apps/platform/routes/billing.js +795 -0
  99. package/dist/apps/platform/routes/billing.js.map +1 -0
  100. package/dist/apps/platform/routes/invitations.d.ts +201 -83
  101. package/dist/apps/platform/routes/invitations.d.ts.map +1 -1
  102. package/dist/apps/platform/routes/invitations.js +58 -0
  103. package/dist/apps/platform/routes/invitations.js.map +1 -1
  104. package/dist/apps/platform/routes/me.d.ts +17 -17
  105. package/dist/apps/platform/routes/orgs.d.ts +50 -36
  106. package/dist/apps/platform/routes/orgs.d.ts.map +1 -1
  107. package/dist/apps/platform/routes/orgs.js +14 -3
  108. package/dist/apps/platform/routes/orgs.js.map +1 -1
  109. package/dist/apps/platform/routes/projects.d.ts +97 -97
  110. package/dist/apps/platform/routes/scopes.d.ts +107 -0
  111. package/dist/apps/platform/routes/scopes.d.ts.map +1 -0
  112. package/dist/apps/platform/routes/scopes.js +66 -0
  113. package/dist/apps/platform/routes/scopes.js.map +1 -0
  114. package/dist/apps/platform/routes/usage.d.ts +588 -0
  115. package/dist/apps/platform/routes/usage.d.ts.map +1 -0
  116. package/dist/apps/platform/routes/usage.js +391 -0
  117. package/dist/apps/platform/routes/usage.js.map +1 -0
  118. package/dist/apps/relay/App.d.ts +4 -1
  119. package/dist/apps/relay/App.d.ts.map +1 -1
  120. package/dist/apps/relay/App.js +118 -10
  121. package/dist/apps/relay/App.js.map +1 -1
  122. package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
  123. package/dist/apps/relay/Sponsorships.js +2 -1
  124. package/dist/apps/relay/Sponsorships.js.map +1 -1
  125. package/dist/cli/Cli.d.ts +8 -1
  126. package/dist/cli/Cli.d.ts.map +1 -1
  127. package/dist/cli/Cli.js +11 -3
  128. package/dist/cli/Cli.js.map +1 -1
  129. package/dist/cli/commands/admin.d.ts +4 -0
  130. package/dist/cli/commands/admin.d.ts.map +1 -1
  131. package/dist/cli/commands/admin.js +80 -1
  132. package/dist/cli/commands/admin.js.map +1 -1
  133. package/dist/db/Db.d.ts +7 -0
  134. package/dist/db/Db.d.ts.map +1 -1
  135. package/dist/db/Db.js +126 -0
  136. package/dist/db/Db.js.map +1 -1
  137. package/dist/db/Schema.d.ts +79 -0
  138. package/dist/db/Schema.d.ts.map +1 -1
  139. package/dist/db/Schema.js +88 -2
  140. package/dist/db/Schema.js.map +1 -1
  141. package/dist/db/tables/billingSettings.d.ts +55 -0
  142. package/dist/db/tables/billingSettings.d.ts.map +1 -0
  143. package/dist/db/tables/billingSettings.js +70 -0
  144. package/dist/db/tables/billingSettings.js.map +1 -0
  145. package/dist/db/tables/invitations.js +2 -2
  146. package/dist/db/tables/invitations.js.map +1 -1
  147. package/dist/db/tables/organizations.d.ts +6 -2
  148. package/dist/db/tables/organizations.d.ts.map +1 -1
  149. package/dist/db/tables/organizations.js +10 -4
  150. package/dist/db/tables/organizations.js.map +1 -1
  151. package/dist/db/tables/projects.js +2 -2
  152. package/dist/db/tables/projects.js.map +1 -1
  153. package/dist/db/tables/requestUsage.d.ts +121 -0
  154. package/dist/db/tables/requestUsage.d.ts.map +1 -0
  155. package/dist/db/tables/requestUsage.js +150 -0
  156. package/dist/db/tables/requestUsage.js.map +1 -0
  157. package/dist/db/tables/sponsoredTransactions.d.ts +119 -0
  158. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  159. package/dist/db/tables/sponsoredTransactions.js +155 -2
  160. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  161. package/dist/db/tables/stripeCustomers.d.ts +81 -0
  162. package/dist/db/tables/stripeCustomers.d.ts.map +1 -0
  163. package/dist/db/tables/stripeCustomers.js +120 -0
  164. package/dist/db/tables/stripeCustomers.js.map +1 -0
  165. package/dist/db/tables/users.js +2 -2
  166. package/dist/db/tables/users.js.map +1 -1
  167. package/dist/handlers/internal/sponsorship.d.ts +5 -2
  168. package/dist/handlers/internal/sponsorship.d.ts.map +1 -1
  169. package/dist/handlers/internal/sponsorship.js +19 -5
  170. package/dist/handlers/internal/sponsorship.js.map +1 -1
  171. package/dist/handlers/internal/utils.d.ts +10 -3
  172. package/dist/handlers/internal/utils.d.ts.map +1 -1
  173. package/dist/handlers/internal/utils.js +14 -6
  174. package/dist/handlers/internal/utils.js.map +1 -1
  175. package/dist/index.d.ts +1 -0
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +1 -0
  178. package/dist/index.js.map +1 -1
  179. package/dist/internal/Auth.d.ts +18 -4
  180. package/dist/internal/Auth.d.ts.map +1 -1
  181. package/dist/internal/Auth.js +148 -76
  182. package/dist/internal/Auth.js.map +1 -1
  183. package/dist/internal/Cache.d.ts.map +1 -1
  184. package/dist/internal/Cache.js +4 -1
  185. package/dist/internal/Cache.js.map +1 -1
  186. package/dist/internal/EdgeCache.d.ts +10 -0
  187. package/dist/internal/EdgeCache.d.ts.map +1 -1
  188. package/dist/internal/EdgeCache.js +39 -8
  189. package/dist/internal/EdgeCache.js.map +1 -1
  190. package/dist/internal/Fees.d.ts +27 -0
  191. package/dist/internal/Fees.d.ts.map +1 -0
  192. package/dist/internal/Fees.js +48 -0
  193. package/dist/internal/Fees.js.map +1 -0
  194. package/dist/internal/FundingRoutes.d.ts +183 -0
  195. package/dist/internal/FundingRoutes.d.ts.map +1 -0
  196. package/dist/internal/FundingRoutes.js +490 -0
  197. package/dist/internal/FundingRoutes.js.map +1 -0
  198. package/dist/internal/Id.d.ts +15 -0
  199. package/dist/internal/Id.d.ts.map +1 -0
  200. package/dist/internal/Id.js +22 -0
  201. package/dist/internal/Id.js.map +1 -0
  202. package/dist/internal/Log.d.ts +2 -0
  203. package/dist/internal/Log.d.ts.map +1 -1
  204. package/dist/internal/Log.js +3 -0
  205. package/dist/internal/Log.js.map +1 -1
  206. package/dist/internal/OpenApi.d.ts +10 -1
  207. package/dist/internal/OpenApi.d.ts.map +1 -1
  208. package/dist/internal/OpenApi.js +8 -1
  209. package/dist/internal/OpenApi.js.map +1 -1
  210. package/dist/internal/RateLimit.d.ts +25 -0
  211. package/dist/internal/RateLimit.d.ts.map +1 -1
  212. package/dist/internal/RateLimit.js +51 -0
  213. package/dist/internal/RateLimit.js.map +1 -1
  214. package/dist/internal/Response.d.ts +1 -1
  215. package/dist/internal/Response.d.ts.map +1 -1
  216. package/dist/internal/Schema.d.ts +8 -0
  217. package/dist/internal/Schema.d.ts.map +1 -1
  218. package/dist/internal/Schema.js +7 -0
  219. package/dist/internal/Schema.js.map +1 -1
  220. package/dist/internal/Webhooks.d.ts.map +1 -1
  221. package/dist/internal/Webhooks.js +3 -3
  222. package/dist/internal/Webhooks.js.map +1 -1
  223. package/package.json +4 -3
  224. package/src/ApiKey.ts +9 -2
  225. package/src/ApiKeys.test.ts +51 -0
  226. package/src/ApiKeys.ts +50 -1
  227. package/src/App.test.ts +33 -86718
  228. package/src/App.ts +18 -0
  229. package/src/Client.test-d.ts +351 -0
  230. package/src/Scope.ts +16 -3
  231. package/src/admin/apps/scopes.test.ts +6 -0
  232. package/src/admin/apps/scopes.ts +3 -0
  233. package/src/admin/ui/src/components/Page.tsx +2 -3
  234. package/src/admin/ui/src/routes/__root.tsx +2 -2
  235. package/src/admin/ui/src/routes/api-keys.tsx +2 -2
  236. package/src/admin/ui.generated.ts +13 -13
  237. package/src/analytics/Analytics.test.ts +527 -0
  238. package/src/analytics/Analytics.ts +318 -0
  239. package/src/analytics/tables/requestEvents.ts +523 -0
  240. package/src/apps/data/App.test.ts +72 -0
  241. package/src/apps/data/App.ts +35 -0
  242. package/src/apps/data/routes/funding.test.ts +214 -0
  243. package/src/apps/data/routes/funding.ts +584 -0
  244. package/src/apps/data/routes/receipts.ts +2 -9
  245. package/src/apps/data/routes/rpc.test.ts +78 -2
  246. package/src/apps/data/routes/rpc.ts +31 -1
  247. package/src/apps/data/routes/webhooks.test.ts +1 -1
  248. package/src/apps/data/routes/webhooks.ts +11 -8
  249. package/src/apps/docs/App.ts +29 -100
  250. package/src/apps/docs/reference.ts +132 -0
  251. package/src/apps/index.ts +1 -0
  252. package/src/apps/mcp.test.ts +206 -0
  253. package/src/apps/mcp.ts +250 -0
  254. package/src/apps/platform/App.ts +43 -4
  255. package/src/apps/platform/Billing.test.ts +530 -0
  256. package/src/apps/platform/Billing.ts +682 -0
  257. package/src/apps/platform/index.ts +1 -0
  258. package/src/apps/platform/routes/api-keys.test.ts +172 -9
  259. package/src/apps/platform/routes/api-keys.ts +136 -20
  260. package/src/apps/platform/routes/billing.test.ts +1100 -0
  261. package/src/apps/platform/routes/billing.ts +966 -0
  262. package/src/apps/platform/routes/invitations.test.ts +55 -7
  263. package/src/apps/platform/routes/invitations.ts +68 -0
  264. package/src/apps/platform/routes/me.test.ts +230 -0
  265. package/src/apps/platform/routes/orgs.ts +26 -4
  266. package/src/apps/platform/routes/scopes.test.ts +29 -0
  267. package/src/apps/platform/routes/scopes.ts +85 -0
  268. package/src/apps/platform/routes/usage.test.ts +422 -0
  269. package/src/apps/platform/routes/usage.ts +527 -0
  270. package/src/apps/relay/App.test.ts +457 -8
  271. package/src/apps/relay/App.ts +120 -12
  272. package/src/apps/relay/Sponsorships.test.ts +1 -1
  273. package/src/apps/relay/Sponsorships.ts +2 -1
  274. package/src/cli/Cli.test.ts +25 -2
  275. package/src/cli/Cli.ts +17 -2
  276. package/src/cli/commands/admin.test.ts +83 -0
  277. package/src/cli/commands/admin.ts +89 -1
  278. package/src/db/Db.ts +137 -0
  279. package/src/db/Schema.ts +104 -2
  280. package/src/db/tables/billingSettings.test.ts +87 -0
  281. package/src/db/tables/billingSettings.ts +108 -0
  282. package/src/db/tables/invitations.ts +2 -2
  283. package/src/db/tables/organizations.test.ts +1 -1
  284. package/src/db/tables/organizations.ts +15 -5
  285. package/src/db/tables/projects.ts +2 -2
  286. package/src/db/tables/requestUsage.ts +231 -0
  287. package/src/db/tables/sponsoredTransactions.test.ts +108 -1
  288. package/src/db/tables/sponsoredTransactions.ts +230 -2
  289. package/src/db/tables/stripeCustomers.test.ts +109 -0
  290. package/src/db/tables/stripeCustomers.ts +162 -0
  291. package/src/db/tables/users.ts +2 -2
  292. package/src/handlers/internal/sponsorship.ts +34 -7
  293. package/src/handlers/internal/utils.ts +19 -7
  294. package/src/index.ts +1 -0
  295. package/src/internal/Auth.test.ts +175 -25
  296. package/src/internal/Auth.ts +175 -86
  297. package/src/internal/Cache.ts +5 -1
  298. package/src/internal/EdgeCache.test.ts +27 -4
  299. package/src/internal/EdgeCache.ts +47 -9
  300. package/src/internal/Fees.test.ts +32 -0
  301. package/src/internal/Fees.ts +51 -0
  302. package/src/internal/FundingRoutes.ts +583 -0
  303. package/src/internal/Id.test.ts +17 -0
  304. package/src/internal/Id.ts +24 -0
  305. package/src/internal/Log.test.ts +1 -0
  306. package/src/internal/Log.ts +5 -0
  307. package/src/internal/OpenApi.ts +12 -1
  308. package/src/internal/RateLimit.test.ts +84 -0
  309. package/src/internal/RateLimit.ts +70 -0
  310. package/src/internal/Response.ts +1 -1
  311. package/src/internal/Schema.ts +8 -0
  312. package/src/internal/Webhooks.test.ts +3 -3
  313. package/src/internal/Webhooks.ts +3 -3
@@ -1,5 +1,6 @@
1
1
  import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
2
2
 
3
+ import * as ApiKeys from '../../../ApiKeys.js'
3
4
  import * as TestApp from '../../../../test/App.js'
4
5
  import * as Keys from './api-keys.js'
5
6
  import * as Orgs from './orgs.js'
@@ -95,8 +96,9 @@ describe('keys', () => {
95
96
  })
96
97
  expect(data.status).toBe(200)
97
98
 
98
- // Listing exposes metadata only — never the token.
99
- const listed = await app.request(base, { headers: { cookie } })
99
+ // Listing exposes metadata only — never the token. The key is sandbox, so
100
+ // the list is scoped to that environment (production is the default).
101
+ const listed = await app.request(`${base}?environment=sandbox`, { headers: { cookie } })
100
102
  expect(listed.status).toBe(200)
101
103
  const list = await TestApp.json(listed, Keys.schema.listApiKeys.Response)
102
104
  expect(list.data.map(({ id }) => id)).toEqual([created.id])
@@ -136,28 +138,87 @@ describe('keys', () => {
136
138
  expect(response.status).toBe(400)
137
139
  })
138
140
 
139
- test('rejects session mints while no scope is self-issuable', async () => {
141
+ test('mints zero-scope keys for sessions', async () => {
140
142
  const app = createApp()
141
143
  const cookie = await session(app)
142
144
  const { org, project } = await fixture(app, cookie)
143
145
 
144
- const response = await app.request(
146
+ const minted = await app.request(
145
147
  `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
146
- json('POST', cookie, { scopes: ['data:read'] }),
148
+ json('POST', cookie, { name: 'CI' }),
147
149
  )
148
- expect(response.status).toBe(403)
149
- const { requestId, ...body } = (await response.json()) as { requestId?: string }
150
+ expect(minted.status).toBe(200)
151
+ const created = await TestApp.json(minted, Keys.schema.createApiKey.Response)
152
+ expect(created.scopes).toEqual([])
153
+ expect(created.createdBy).toMatch(/^usr_/)
154
+
155
+ // The key authenticates but grants nothing: scoped routes stay forbidden.
156
+ const data = await app.request('/v1/tokens', {
157
+ headers: { 'cache-control': 'no-store', 'tempo-api-key': created.token },
158
+ })
159
+ expect(data.status).toBe(403)
160
+ const { requestId, ...body } = (await data.json()) as { requestId?: string }
150
161
  expect(requestId).toBeDefined()
151
162
  expect(body).toMatchInlineSnapshot(`
152
163
  {
153
164
  "error": {
154
- "code": "scope_not_issuable",
155
- "message": "The requested scopes are not issuable.",
165
+ "code": "api_key_forbidden",
166
+ "message": "API key missing required scope",
156
167
  },
157
168
  }
158
169
  `)
159
170
  })
160
171
 
172
+ test('lets sessions mint catalog scopes on production keys', async () => {
173
+ const app = createApp()
174
+ const cookie = await session(app)
175
+ const { org, project } = await fixture(app, cookie)
176
+
177
+ const created = await TestApp.json(
178
+ await app.request(
179
+ `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
180
+ json('POST', cookie, { scopes: ['data:read'] }),
181
+ ),
182
+ Keys.schema.createApiKey.Response,
183
+ )
184
+ expect(created.environment).toBe('production')
185
+ expect(created.scopes).toEqual(['data:read'])
186
+ // Session-minted, not granted through the super admin.
187
+ expect(created.createdBy).not.toBe('super_admin')
188
+ })
189
+
190
+ test('rejects session mints of non-self-serve scopes (webhooks)', async () => {
191
+ const app = createApp()
192
+ const cookie = await session(app)
193
+ const { org, project } = await fixture(app, cookie)
194
+
195
+ const response = await app.request(
196
+ `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
197
+ json('POST', cookie, { scopes: ['webhooks:write'] }),
198
+ )
199
+ expect(response.status).toBe(403)
200
+ const body = (await response.json()) as { error: { code: string } }
201
+ expect(body.error.code).toBe('scope_not_issuable')
202
+ })
203
+
204
+ test('lets sessions mint catalog scopes on sandbox keys', async () => {
205
+ const app = createApp()
206
+ const cookie = await session(app)
207
+ const { org, project } = await fixture(app, cookie)
208
+
209
+ const created = await TestApp.json(
210
+ await app.request(
211
+ `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
212
+ json('POST', cookie, { environment: 'sandbox', scopes: ['data:read'] }),
213
+ ),
214
+ Keys.schema.createApiKey.Response,
215
+ )
216
+ expect(created.environment).toBe('sandbox')
217
+ expect(created.scopes).toEqual(['data:read'])
218
+ // Session-minted, not granted through the super admin.
219
+ expect(created.createdBy).not.toBe('super_admin')
220
+ })
221
+
161
222
  test('binds keys to their project on every verb', async () => {
162
223
  const app = createApp()
163
224
  const owner = await session(app)
@@ -206,6 +267,108 @@ describe('keys', () => {
206
267
  expect(list.data.map(({ id }) => id)).toEqual([minted.id])
207
268
  })
208
269
 
270
+ test('lists keys org-wide across projects', async () => {
271
+ const store = TestApp.kvStore({ keys: [] })
272
+ const app = createApp({ kv: { store } })
273
+ const owner = await session(app)
274
+ const { org, project } = await fixture(app, owner)
275
+ const other = await TestApp.json(
276
+ await app.request(`/v1/orgs/${org.id}/projects`, json('POST', owner, { name: 'Payments' })),
277
+ Projects.schema.Project,
278
+ )
279
+
280
+ const first = await TestApp.json(
281
+ await app.request(
282
+ `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
283
+ superAdminJson('POST', { name: 'Checkout CI', scopes: ['data:read'] }),
284
+ ),
285
+ Keys.schema.createApiKey.Response,
286
+ )
287
+ const second = await TestApp.json(
288
+ await app.request(
289
+ `/v1/orgs/${org.id}/projects/${other.id}/api-keys`,
290
+ superAdminJson('POST', { name: 'Payments CI', scopes: ['data:read'] }),
291
+ ),
292
+ Keys.schema.createApiKey.Response,
293
+ )
294
+ // Org-attributed key without a project (admin-surface mints land like this).
295
+ const orgWide = await ApiKeys.mint(store, { orgId: org.id, scopes: ['data:read'] })
296
+
297
+ const listed = await app.request(`/v1/orgs/${org.id}/api-keys`, {
298
+ headers: { cookie: owner },
299
+ })
300
+ const list = await TestApp.json(listed, Keys.schema.listOrgApiKeys.Response)
301
+ expect(list.data.map(({ id }) => id).sort()).toEqual(
302
+ [first.id, second.id, orgWide.record.id].sort(),
303
+ )
304
+ expect(list.data.find(({ id }) => id === first.id)?.projectId).toBe(project.id)
305
+ expect(list.data.find(({ id }) => id === second.id)?.projectId).toBe(other.id)
306
+ expect(list.data.find(({ id }) => id === orgWide.record.id)?.projectId).toBeUndefined()
307
+
308
+ // Non-members see the org as absent.
309
+ const stranger = await session(app)
310
+ const foreign = await app.request(`/v1/orgs/${org.id}/api-keys`, {
311
+ headers: { cookie: stranger },
312
+ })
313
+ expect(foreign.status).toBe(404)
314
+ })
315
+
316
+ test('filters keys by environment (defaulting to production)', async () => {
317
+ const app = createApp()
318
+ const cookie = await session(app)
319
+ const { org, project } = await fixture(app, cookie)
320
+ const projectBase = `/v1/orgs/${org.id}/projects/${project.id}/api-keys`
321
+ const orgBase = `/v1/orgs/${org.id}/api-keys`
322
+
323
+ const production = await TestApp.json(
324
+ await app.request(
325
+ projectBase,
326
+ superAdminJson('POST', { name: 'Prod', scopes: ['data:read'] }),
327
+ ),
328
+ Keys.schema.createApiKey.Response,
329
+ )
330
+ const sandbox = await TestApp.json(
331
+ await app.request(
332
+ projectBase,
333
+ superAdminJson('POST', { environment: 'sandbox', name: 'Sandbox', scopes: ['data:read'] }),
334
+ ),
335
+ Keys.schema.createApiKey.Response,
336
+ )
337
+
338
+ const ids = async (path: string) =>
339
+ (
340
+ await TestApp.json(
341
+ await app.request(path, { headers: { cookie } }),
342
+ Keys.schema.listApiKeys.Response,
343
+ )
344
+ ).data
345
+ .map(({ id }) => id)
346
+ .sort()
347
+
348
+ // Each filter returns only its own environment.
349
+ expect(await ids(`${projectBase}?environment=production`)).toEqual([production.id])
350
+ expect(await ids(`${projectBase}?environment=sandbox`)).toEqual([sandbox.id])
351
+ // Absent param defaults to production.
352
+ expect(await ids(projectBase)).toEqual([production.id])
353
+
354
+ // Same behavior on the org-wide list route.
355
+ const orgIds = async (path: string) =>
356
+ (
357
+ await TestApp.json(
358
+ await app.request(path, { headers: { cookie } }),
359
+ Keys.schema.listOrgApiKeys.Response,
360
+ )
361
+ ).data
362
+ .map(({ id }) => id)
363
+ .sort()
364
+ expect(await orgIds(`${orgBase}?environment=sandbox`)).toEqual([sandbox.id])
365
+ expect(await orgIds(orgBase)).toEqual([production.id])
366
+
367
+ // An unknown environment value is rejected.
368
+ const invalid = await app.request(`${projectBase}?environment=staging`, { headers: { cookie } })
369
+ expect(invalid.status).toBe(400)
370
+ })
371
+
209
372
  test('revokes keys when their project or organization is deleted', async () => {
210
373
  const app = createApp()
211
374
  const cookie = await session(app)
@@ -3,7 +3,9 @@ import * as z from 'zod/mini'
3
3
 
4
4
  import * as ApiKeys from '../../../ApiKeys.js'
5
5
  import type * as App from '../../../App.js'
6
+ import * as Db from '../../../db/Db.js'
6
7
  import * as Auth from '../../../internal/Auth.js'
8
+ import * as Billing from '../Billing.js'
7
9
  import * as OpenApi from '../../../internal/OpenApi.js'
8
10
  import * as Response from '../../../internal/Response.js'
9
11
  import * as Schema from '../../../internal/Schema.js'
@@ -50,12 +52,10 @@ export namespace schema {
50
52
  z.describe('Owning organization id (`org_…`).'),
51
53
  z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
52
54
  ),
53
- projectId: z
54
- .string()
55
- .check(
56
- z.describe('Owning project id (`prj_…`).'),
57
- z.meta({ examples: ['prj_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
58
- ),
55
+ projectId: z.optional(z.string()).check(
56
+ z.describe('Owning project id (`prj_…`).'), // prettier-ignore
57
+ z.meta({ examples: ['prj_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
58
+ ),
59
59
  scopes: z
60
60
  .array(z.enum([...Scope.catalog.map((entry) => entry.scope), Scope.wildcard]))
61
61
  .check(
@@ -92,6 +92,18 @@ export namespace schema {
92
92
  })
93
93
  .check(z.describe("Path parameters for a project's API keys."))
94
94
 
95
+ /** Path parameters addressing an organization's key collection. */
96
+ export const OrgParams = z
97
+ .object({
98
+ orgId: z
99
+ .string()
100
+ .check(
101
+ z.describe('The organization id (`org_…`).'),
102
+ z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
103
+ ),
104
+ })
105
+ .check(z.describe("Path parameters for an organization's API keys."))
106
+
95
107
  /** Path parameters addressing one API key. */
96
108
  export const KeyParams = z
97
109
  .object({
@@ -126,12 +138,8 @@ export namespace schema {
126
138
  .optional(z.string().check(z.minLength(1), z.maxLength(100)))
127
139
  .check(z.describe('Human-readable key name.'), z.meta({ examples: ['CI'] })),
128
140
  scopes: z
129
- .array(z.enum(Scope.catalog.map((entry) => entry.scope)))
130
- .check(
131
- z.minLength(1),
132
- z.describe('Scopes to grant.'),
133
- z.meta({ examples: [['data:read']] }),
134
- ),
141
+ ._default(z.array(z.enum(Scope.catalog.map((entry) => entry.scope))), [])
142
+ .check(z.describe('Scopes to grant.'), z.meta({ examples: [['data:read']] })),
135
143
  })
136
144
  .check(z.describe('Fields for minting an API key.'))
137
145
 
@@ -153,6 +161,11 @@ export namespace schema {
153
161
 
154
162
  /** Schemas for the listApiKeys operation. */
155
163
  export namespace listApiKeys {
164
+ /** Query parameters filtering a project's keys. */
165
+ export const Query = z
166
+ .strictObject({ environment: Schema.Environment })
167
+ .check(z.describe("Query parameters for a project's API keys."))
168
+
156
169
  /** Non-paginated list of a project's keys. */
157
170
  export const Response = Schema.describe(
158
171
  z.object({
@@ -162,6 +175,24 @@ export namespace schema {
162
175
  )
163
176
  }
164
177
 
178
+ /** Schemas for the listOrgApiKeys operation. */
179
+ export namespace listOrgApiKeys {
180
+ /** Query parameters filtering the organization's keys. */
181
+ export const Query = z
182
+ .strictObject({ environment: Schema.Environment })
183
+ .check(z.describe("Query parameters for an organization's API keys."))
184
+
185
+ /** Non-paginated list of every key in the organization. */
186
+ export const Response = Schema.describe(
187
+ z.object({
188
+ data: z
189
+ .array(Key)
190
+ .check(z.describe("The organization's keys across all projects, newest first.")),
191
+ }),
192
+ "A non-paginated list of an organization's API keys (metadata only).",
193
+ )
194
+ }
195
+
165
196
  /** Schemas for the revokeApiKey operation. */
166
197
  export namespace revokeApiKey {
167
198
  /** Confirmation that the key was revoked. */
@@ -179,9 +210,11 @@ export namespace schema {
179
210
  }
180
211
 
181
212
  /**
182
- * Mounts the `/orgs/:orgId/projects/:projectId/api-keys` resource:
183
- * project-scoped API keys over the KV state store. Access follows the project
184
- * scope (owning session or super admin); the token appears once, at mint.
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.
185
218
  */
186
219
  export function apiKeys(options: apiKeys.Options = {}) {
187
220
  const hidden = !options.enabled
@@ -241,7 +274,9 @@ export function apiKeys(options: apiKeys.Options = {}) {
241
274
  const body = c.req.valid('json')
242
275
 
243
276
  // Sessions mint only self-issuable scopes; the super admin grants the
244
- // full catalog.
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.
245
280
  if (createdBy !== 'super_admin') {
246
281
  const denied = body.scopes.filter((scope) => !Scope.selfServe.includes(scope))
247
282
  if (denied.length > 0)
@@ -253,11 +288,20 @@ export function apiKeys(options: apiKeys.Options = {}) {
253
288
  }
254
289
 
255
290
  try {
291
+ const orgId = Auth.org(c).id
292
+ // Snapshot billing onto the record so the auth path throttles a
293
+ // sandbox key to the public quota without a per-request billing read.
294
+ // Only sandbox is gated; production keys carry no snapshot.
295
+ const billingActive =
296
+ body.environment === 'sandbox'
297
+ ? await Billing.active(Db.get(c.get('db')), orgId, 'sandbox')
298
+ : undefined
256
299
  const { record, token } = await ApiKeys.mint(kv.store, {
257
300
  createdBy,
258
301
  environment: body.environment,
302
+ ...(billingActive === undefined ? {} : { billingActive }),
259
303
  ...(body.name === undefined ? {} : { name: body.name }),
260
- orgId: Auth.org(c).id,
304
+ orgId,
261
305
  projectId: Auth.project(c).id,
262
306
  scopes: body.scopes,
263
307
  })
@@ -279,11 +323,18 @@ export function apiKeys(options: apiKeys.Options = {}) {
279
323
  code: 'param_invalid',
280
324
  message: 'Check the path parameters and try again.',
281
325
  }),
326
+ OpenApi.validate('query', schema.listApiKeys.Query, {
327
+ code: 'query_invalid',
328
+ message: 'Check the query parameters and try again.',
329
+ }),
282
330
  OpenApi.describeRoute({
283
331
  hide: hidden,
284
332
  operationId: 'listApiKeys',
285
333
  responses: OpenApi.responses({
286
- errors: { 404: 'No accessible project was found for this id.' },
334
+ errors: {
335
+ 400: { codes: ['query_invalid'], description: 'Invalid query parameters.' },
336
+ 404: 'No accessible project was found for this id.',
337
+ },
287
338
  success: {
288
339
  description: "The project's keys, newest first (metadata only).",
289
340
  schema: schema.listApiKeys.Response,
@@ -300,11 +351,17 @@ export function apiKeys(options: apiKeys.Options = {}) {
300
351
  code: 'param_invalid',
301
352
  message: 'Check the path parameters and try again.',
302
353
  })
354
+ if (OpenApi.narrowValidation)
355
+ return OpenApi.validationError(c, {
356
+ code: 'query_invalid',
357
+ message: 'Check the query parameters and try again.',
358
+ })
303
359
 
304
360
  const kv = c.get('kv')
305
361
  if (!kv) return notEnabled(c)
306
362
  try {
307
363
  const records = await ApiKeys.listByOrg(kv.store, Auth.org(c).id, {
364
+ environment: c.req.valid('query').environment,
308
365
  projectId: Auth.project(c).id,
309
366
  })
310
367
  return c.json(
@@ -318,6 +375,66 @@ export function apiKeys(options: apiKeys.Options = {}) {
318
375
  }
319
376
  },
320
377
  )
378
+ .get(
379
+ '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/api-keys',
380
+ Auth.policy({ session: true }),
381
+ Auth.ensureOrg(),
382
+ OpenApi.validate('param', schema.OrgParams, {
383
+ code: 'param_invalid',
384
+ message: 'Check the path parameters and try again.',
385
+ }),
386
+ OpenApi.validate('query', schema.listOrgApiKeys.Query, {
387
+ code: 'query_invalid',
388
+ message: 'Check the query parameters and try again.',
389
+ }),
390
+ OpenApi.describeRoute({
391
+ description: 'List every API key in the organization, across all of its projects.',
392
+ hide: hidden,
393
+ operationId: 'listOrgApiKeys',
394
+ responses: OpenApi.responses({
395
+ errors: {
396
+ 400: { codes: ['query_invalid'], description: 'Invalid query parameters.' },
397
+ 404: 'No accessible organization was found for this id.',
398
+ },
399
+ success: {
400
+ description: "The organization's keys across all projects, newest first (metadata only).", // prettier-ignore
401
+ schema: schema.listOrgApiKeys.Response,
402
+ },
403
+ }),
404
+ summary: 'List organization API keys',
405
+ tags: ['API Keys'],
406
+ }),
407
+ async (c) => {
408
+ if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
409
+ if (Auth.narrowScope) return Auth.ensureOrgError(c)
410
+ if (OpenApi.narrowValidation)
411
+ return OpenApi.validationError(c, {
412
+ code: 'param_invalid',
413
+ message: 'Check the path parameters and try again.',
414
+ })
415
+ if (OpenApi.narrowValidation)
416
+ return OpenApi.validationError(c, {
417
+ code: 'query_invalid',
418
+ message: 'Check the query parameters and try again.',
419
+ })
420
+
421
+ const kv = c.get('kv')
422
+ if (!kv) return notEnabled(c)
423
+ try {
424
+ const records = await ApiKeys.listByOrg(kv.store, Auth.org(c).id, {
425
+ environment: c.req.valid('query').environment,
426
+ })
427
+ return c.json(
428
+ Response.validated(schema.listOrgApiKeys.Response, {
429
+ data: records.map(serializeKey),
430
+ }),
431
+ 200,
432
+ )
433
+ } catch (cause) {
434
+ return Response.upstream(c, cause)
435
+ }
436
+ },
437
+ )
321
438
  .delete(
322
439
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys/:keyId{key_[A-Za-z0-9_-]+}',
323
440
  Auth.policy({ session: true }),
@@ -387,8 +504,7 @@ function serializeKey(record: ApiKeys.Record) {
387
504
  id: record.id,
388
505
  ...(record.name === undefined ? {} : { name: record.name }),
389
506
  orgId: record.orgId,
390
- // The project filter guarantees attribution; the fallback never renders.
391
- projectId: record.projectId ?? '',
507
+ ...(record.projectId === undefined ? {} : { projectId: record.projectId }),
392
508
  scopes: [...record.scopes],
393
509
  tokenLast4: record.tokenLast4,
394
510
  }