tapimo 0.12.0 → 0.13.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 (257) hide show
  1. package/dist/App.d.ts +5561 -5026
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +38 -3
  4. package/dist/App.js.map +1 -1
  5. package/dist/Scope.d.ts +2 -0
  6. package/dist/Scope.d.ts.map +1 -1
  7. package/dist/Scope.js +17 -0
  8. package/dist/Scope.js.map +1 -1
  9. package/dist/admin/App.d.ts +123 -74
  10. package/dist/admin/App.d.ts.map +1 -1
  11. package/dist/admin/apps/api-keys.d.ts +80 -0
  12. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  13. package/dist/admin/apps/api-keys.js +54 -0
  14. package/dist/admin/apps/api-keys.js.map +1 -1
  15. package/dist/admin/apps/earn-vaults.d.ts +15 -15
  16. package/dist/admin/apps/earn-vaults.js +1 -1
  17. package/dist/admin/apps/verified-tokens.d.ts +81 -81
  18. package/dist/admin/ui.generated.d.ts +1 -1
  19. package/dist/admin/ui.generated.d.ts.map +1 -1
  20. package/dist/admin/ui.generated.js +151 -151
  21. package/dist/admin/ui.generated.js.map +1 -1
  22. package/dist/apps/Zones.d.ts +16 -16
  23. package/dist/apps/Zones.js +4 -4
  24. package/dist/apps/data/App.js +18 -18
  25. package/dist/apps/data/App.js.map +1 -1
  26. package/dist/apps/data/ZoneSelection.d.ts +8 -8
  27. package/dist/apps/data/ZoneSelection.d.ts.map +1 -1
  28. package/dist/apps/data/ZoneSelection.js +1 -1
  29. package/dist/apps/data/ZoneSelection.js.map +1 -1
  30. package/dist/apps/data/routes/activities.d.ts +588 -584
  31. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  32. package/dist/apps/data/routes/activities.js +1561 -1239
  33. package/dist/apps/data/routes/activities.js.map +1 -1
  34. package/dist/apps/data/routes/balances.d.ts +26 -26
  35. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  36. package/dist/apps/data/routes/balances.js +43 -16
  37. package/dist/apps/data/routes/balances.js.map +1 -1
  38. package/dist/apps/data/routes/blocks.d.ts +44 -44
  39. package/dist/apps/data/routes/blocks.d.ts.map +1 -1
  40. package/dist/apps/data/routes/blocks.js +21 -11
  41. package/dist/apps/data/routes/blocks.js.map +1 -1
  42. package/dist/apps/data/routes/coingecko.d.ts +3 -3
  43. package/dist/apps/data/routes/earn.d.ts +18 -18
  44. package/dist/apps/data/routes/earn.js +13 -13
  45. package/dist/apps/data/routes/exchanges.d.ts +145 -145
  46. package/dist/apps/data/routes/fee-amm.d.ts +2 -2
  47. package/dist/apps/data/routes/receipts.d.ts +155 -55
  48. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  49. package/dist/apps/data/routes/receipts.js +28 -10
  50. package/dist/apps/data/routes/receipts.js.map +1 -1
  51. package/dist/apps/data/routes/tokenlist.d.ts +13 -13
  52. package/dist/apps/data/routes/tokens.d.ts +98 -98
  53. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  54. package/dist/apps/data/routes/tokens.js +51 -20
  55. package/dist/apps/data/routes/tokens.js.map +1 -1
  56. package/dist/apps/data/routes/transactions.d.ts +7 -2
  57. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  58. package/dist/apps/data/routes/transactions.js +37 -17
  59. package/dist/apps/data/routes/transactions.js.map +1 -1
  60. package/dist/apps/data/routes/transfers.d.ts +1 -1
  61. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  62. package/dist/apps/data/routes/transfers.js +18 -12
  63. package/dist/apps/data/routes/transfers.js.map +1 -1
  64. package/dist/apps/data/routes/valuation.d.ts +1 -1
  65. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  66. package/dist/apps/data/routes/valuation.js +12 -7
  67. package/dist/apps/data/routes/valuation.js.map +1 -1
  68. package/dist/apps/data/routes/verified-tokens.d.ts +3 -3
  69. package/dist/apps/data/routes/webhooks.d.ts +11 -11
  70. package/dist/apps/data/routes/zones.d.ts +14 -14
  71. package/dist/apps/data/routes/zones.js +1 -1
  72. package/dist/apps/funding/App.d.ts +11 -0
  73. package/dist/apps/funding/App.d.ts.map +1 -1
  74. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  75. package/dist/apps/funding/routes/deposit-addresses.js +16 -11
  76. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  77. package/dist/apps/funding/routes/transfers.d.ts +60 -48
  78. package/dist/apps/funding/routes/transfers.d.ts.map +1 -1
  79. package/dist/apps/funding/routes/transfers.js +27 -19
  80. package/dist/apps/funding/routes/transfers.js.map +1 -1
  81. package/dist/apps/management/App.d.ts +4487 -3963
  82. package/dist/apps/management/App.d.ts.map +1 -1
  83. package/dist/apps/management/routes/api-keys.d.ts +710 -159
  84. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  85. package/dist/apps/management/routes/api-keys.js +236 -4
  86. package/dist/apps/management/routes/api-keys.js.map +1 -1
  87. package/dist/apps/management/routes/faucet.d.ts +6 -6
  88. package/dist/apps/management/routes/invite-links.d.ts +42 -42
  89. package/dist/apps/management/routes/members.d.ts +30 -30
  90. package/dist/apps/management/routes/projects.d.ts +40 -40
  91. package/dist/apps/management/routes/usage.d.ts +24 -24
  92. package/dist/apps/management/routes/verified-token-requests.d.ts +1 -1
  93. package/dist/apps/management/routes/webhooks.d.ts +55 -55
  94. package/dist/db/Db.d.ts.map +1 -1
  95. package/dist/db/Db.js +12 -1
  96. package/dist/db/Db.js.map +1 -1
  97. package/dist/db/Schema.d.ts +3 -2
  98. package/dist/db/Schema.d.ts.map +1 -1
  99. package/dist/db/Schema.js +5 -2
  100. package/dist/db/Schema.js.map +1 -1
  101. package/dist/db/tables/fundingCatalog.d.ts +4 -2
  102. package/dist/db/tables/fundingCatalog.d.ts.map +1 -1
  103. package/dist/db/tables/fundingCatalog.js +5 -1
  104. package/dist/db/tables/fundingCatalog.js.map +1 -1
  105. package/dist/handlers/internal/utils.d.ts.map +1 -1
  106. package/dist/handlers/internal/utils.js +9 -0
  107. package/dist/handlers/internal/utils.js.map +1 -1
  108. package/dist/internal/Auth.d.ts +3 -1
  109. package/dist/internal/Auth.d.ts.map +1 -1
  110. package/dist/internal/Auth.js +6 -3
  111. package/dist/internal/Auth.js.map +1 -1
  112. package/dist/internal/EarnVaults.d.ts +3 -3
  113. package/dist/internal/EarnVaults.js +3 -3
  114. package/dist/internal/Fetch.d.ts +23 -0
  115. package/dist/internal/Fetch.d.ts.map +1 -0
  116. package/dist/internal/Fetch.js +26 -0
  117. package/dist/internal/Fetch.js.map +1 -0
  118. package/dist/internal/Log.d.ts +3 -1
  119. package/dist/internal/Log.d.ts.map +1 -1
  120. package/dist/internal/Log.js +16 -3
  121. package/dist/internal/Log.js.map +1 -1
  122. package/dist/internal/OpenApi.d.ts +5 -3
  123. package/dist/internal/OpenApi.d.ts.map +1 -1
  124. package/dist/internal/OpenApi.js +13 -3
  125. package/dist/internal/OpenApi.js.map +1 -1
  126. package/dist/internal/Response.d.ts +1 -1
  127. package/dist/internal/Response.d.ts.map +1 -1
  128. package/dist/internal/Schema.d.ts +6 -3
  129. package/dist/internal/Schema.d.ts.map +1 -1
  130. package/dist/internal/Schema.js +8 -2
  131. package/dist/internal/Schema.js.map +1 -1
  132. package/dist/internal/Store.d.ts +16 -3
  133. package/dist/internal/Store.d.ts.map +1 -1
  134. package/dist/internal/Store.js +82 -16
  135. package/dist/internal/Store.js.map +1 -1
  136. package/dist/internal/Tidx.d.ts.map +1 -1
  137. package/dist/internal/Tidx.js +7 -2
  138. package/dist/internal/Tidx.js.map +1 -1
  139. package/dist/internal/Timeout.d.ts +36 -0
  140. package/dist/internal/Timeout.d.ts.map +1 -0
  141. package/dist/internal/Timeout.js +37 -0
  142. package/dist/internal/Timeout.js.map +1 -0
  143. package/dist/internal/Timing.d.ts +9 -0
  144. package/dist/internal/Timing.d.ts.map +1 -1
  145. package/dist/internal/Timing.js +7 -0
  146. package/dist/internal/Timing.js.map +1 -1
  147. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  148. package/dist/internal/WebhookDestination.js +3 -0
  149. package/dist/internal/WebhookDestination.js.map +1 -1
  150. package/dist/internal/funding/Action.d.ts +9 -0
  151. package/dist/internal/funding/Action.d.ts.map +1 -1
  152. package/dist/internal/funding/Action.js +16 -8
  153. package/dist/internal/funding/Action.js.map +1 -1
  154. package/dist/internal/funding/Catalog.d.ts +9 -2
  155. package/dist/internal/funding/Catalog.d.ts.map +1 -1
  156. package/dist/internal/funding/Catalog.js +19 -8
  157. package/dist/internal/funding/Catalog.js.map +1 -1
  158. package/dist/internal/funding/Provider.d.ts +68 -13
  159. package/dist/internal/funding/Provider.d.ts.map +1 -1
  160. package/dist/internal/funding/Provider.js +29 -8
  161. package/dist/internal/funding/Provider.js.map +1 -1
  162. package/dist/internal/funding/Transfer.d.ts +13 -12
  163. package/dist/internal/funding/Transfer.d.ts.map +1 -1
  164. package/dist/internal/funding/Transfer.js +23 -21
  165. package/dist/internal/funding/Transfer.js.map +1 -1
  166. package/dist/internal/funding/providers/stargate.d.ts +28 -9
  167. package/dist/internal/funding/providers/stargate.d.ts.map +1 -1
  168. package/dist/internal/funding/providers/stargate.js +251 -117
  169. package/dist/internal/funding/providers/stargate.js.map +1 -1
  170. package/dist/internal/index.d.ts +1 -0
  171. package/dist/internal/index.d.ts.map +1 -1
  172. package/dist/internal/index.js +1 -0
  173. package/dist/internal/index.js.map +1 -1
  174. package/package.json +2 -2
  175. package/src/App.test.ts +100 -0
  176. package/src/App.ts +42 -3
  177. package/src/Client.test-d.ts +46 -0
  178. package/src/Scope.test.ts +20 -0
  179. package/src/Scope.ts +16 -0
  180. package/src/admin/apps/api-keys.test.ts +75 -0
  181. package/src/admin/apps/api-keys.ts +64 -0
  182. package/src/admin/apps/earn-vaults.test.ts +1 -1
  183. package/src/admin/apps/earn-vaults.ts +1 -1
  184. package/src/admin/ui/src/lib/api-keys.ts +19 -0
  185. package/src/admin/ui/src/routes/api-keys.tsx +89 -2
  186. package/src/admin/ui.generated.ts +189 -151
  187. package/src/apps/Zones.test.ts +4 -4
  188. package/src/apps/Zones.ts +4 -4
  189. package/src/apps/composition.test.ts +1 -1
  190. package/src/apps/data/App.test.ts +41 -0
  191. package/src/apps/data/App.ts +18 -18
  192. package/src/apps/data/ZoneSelection.test.ts +2 -2
  193. package/src/apps/data/ZoneSelection.ts +8 -8
  194. package/src/apps/data/routes/activities.test.ts +1526 -12
  195. package/src/apps/data/routes/activities.ts +1774 -1356
  196. package/src/apps/data/routes/balances.ts +133 -97
  197. package/src/apps/data/routes/blocks.test.ts +533 -0
  198. package/src/apps/data/routes/blocks.ts +35 -16
  199. package/src/apps/data/routes/earn.test.ts +4 -2
  200. package/src/apps/data/routes/earn.ts +13 -13
  201. package/src/apps/data/routes/earn.zone-access.test.ts +6 -6
  202. package/src/apps/data/routes/receipts.ts +47 -16
  203. package/src/apps/data/routes/tokens.test.ts +578 -0
  204. package/src/apps/data/routes/tokens.ts +283 -225
  205. package/src/apps/data/routes/transactions.ts +68 -29
  206. package/src/apps/data/routes/transfers.test.ts +421 -0
  207. package/src/apps/data/routes/transfers.ts +45 -27
  208. package/src/apps/data/routes/valuation.ts +94 -78
  209. package/src/apps/data/routes/zones.test.ts +2 -2
  210. package/src/apps/data/routes/zones.ts +1 -1
  211. package/src/apps/funding/routes/deposit-addresses.test.ts +25 -3
  212. package/src/apps/funding/routes/deposit-addresses.ts +17 -12
  213. package/src/apps/funding/routes/quotes.test.ts +5 -9
  214. package/src/apps/funding/routes/transfers.test.ts +395 -8
  215. package/src/apps/funding/routes/transfers.ts +76 -59
  216. package/src/apps/management/routes/api-keys.test.ts +220 -0
  217. package/src/apps/management/routes/api-keys.ts +280 -3
  218. package/src/db/Db.ts +13 -1
  219. package/src/db/Schema.ts +5 -2
  220. package/src/db/tables/earnVaults.test.ts +9 -9
  221. package/src/db/tables/fundingCatalog.test.ts +16 -2
  222. package/src/db/tables/fundingCatalog.ts +9 -3
  223. package/src/handlers/internal/utils.test.ts +53 -0
  224. package/src/handlers/internal/utils.ts +15 -0
  225. package/src/internal/Auth.ts +12 -4
  226. package/src/internal/EarnVaults.capabilities.test.ts +2 -2
  227. package/src/internal/EarnVaults.test.ts +1 -1
  228. package/src/internal/EarnVaults.ts +3 -3
  229. package/src/internal/Fetch.test.ts +46 -0
  230. package/src/internal/Fetch.ts +56 -0
  231. package/src/internal/Log.test.ts +21 -0
  232. package/src/internal/Log.ts +20 -5
  233. package/src/internal/OpenApi.ts +14 -4
  234. package/src/internal/Response.ts +1 -1
  235. package/src/internal/Schema.test.ts +2 -0
  236. package/src/internal/Schema.ts +12 -2
  237. package/src/internal/Store.test.ts +178 -0
  238. package/src/internal/Store.ts +104 -18
  239. package/src/internal/Tidx.test.ts +2 -1
  240. package/src/internal/Tidx.ts +7 -2
  241. package/src/internal/Timeout.test.ts +35 -0
  242. package/src/internal/Timeout.ts +73 -0
  243. package/src/internal/Timing.ts +16 -0
  244. package/src/internal/WebhookDestination.ts +3 -0
  245. package/src/internal/funding/Action.test.ts +41 -1
  246. package/src/internal/funding/Action.ts +69 -41
  247. package/src/internal/funding/Catalog.test.ts +34 -0
  248. package/src/internal/funding/Catalog.ts +28 -11
  249. package/src/internal/funding/Provider.test.ts +73 -0
  250. package/src/internal/funding/Provider.ts +82 -28
  251. package/src/internal/funding/Transfer.ts +133 -110
  252. package/src/internal/funding/providers/index.test.ts +4 -167
  253. package/src/internal/funding/providers/relay.test.ts +2 -2
  254. package/src/internal/funding/providers/stargate.test.ts +163 -0
  255. package/src/internal/funding/providers/stargate.ts +359 -134
  256. package/src/internal/index.test.ts +1 -0
  257. package/src/internal/index.ts +1 -0
@@ -88,6 +88,7 @@ test('publishes generator-ready OpenAPI contracts', async () => {
88
88
  const collection = spec.paths['/v1/orgs/{orgId}/projects/{projectId}/api-keys']
89
89
  const detail = spec.paths['/v1/orgs/{orgId}/projects/{projectId}/api-keys/{keyId}']
90
90
  const organization = spec.paths['/v1/orgs/{orgId}/api-keys']
91
+ const rotation = spec.paths['/v1/orgs/{orgId}/projects/{projectId}/api-keys/{keyId}/rotate']
91
92
 
92
93
  expect({
93
94
  components: [
@@ -96,6 +97,7 @@ test('publishes generator-ready OpenAPI contracts', async () => {
96
97
  'CreateApiKeyRequest',
97
98
  'CreateApiKeyResponse',
98
99
  'RevokeApiKeyResponse',
100
+ 'RotateApiKeyResponse',
99
101
  'UpdateApiKeyRequest',
100
102
  ].filter((name) => spec.components.schemas[name]),
101
103
  create: {
@@ -135,6 +137,15 @@ test('publishes generator-ready OpenAPI contracts', async () => {
135
137
  operationId: detail.delete.operationId,
136
138
  response: detail.delete.responses[200].content['application/json'].schema,
137
139
  },
140
+ rotate: {
141
+ errors: {
142
+ 400: rotation.post.responses[400].content['application/json'].schema,
143
+ 403: rotation.post.responses[403].content['application/json'].schema,
144
+ 404: rotation.post.responses[404].content['application/json'].schema,
145
+ },
146
+ operationId: rotation.post.operationId,
147
+ response: rotation.post.responses[200].content['application/json'].schema,
148
+ },
138
149
  timestamps: {
139
150
  createdAt: spec.components.schemas.ApiKey.properties.createdAt.format,
140
151
  expiresAt: spec.components.schemas.ApiKey.properties.expiresAt.format,
@@ -158,6 +169,7 @@ test('publishes generator-ready OpenAPI contracts', async () => {
158
169
  "CreateApiKeyRequest",
159
170
  "CreateApiKeyResponse",
160
171
  "RevokeApiKeyResponse",
172
+ "RotateApiKeyResponse",
161
173
  "UpdateApiKeyRequest",
162
174
  ],
163
175
  "create": {
@@ -231,6 +243,23 @@ test('publishes generator-ready OpenAPI contracts', async () => {
231
243
  "$ref": "#/components/schemas/RevokeApiKeyResponse",
232
244
  },
233
245
  },
246
+ "rotate": {
247
+ "errors": {
248
+ "400": {
249
+ "$ref": "#/components/schemas/ApiKeyMalformedOrParamInvalidError",
250
+ },
251
+ "403": {
252
+ "$ref": "#/components/schemas/ApiKeyForbiddenOrApiKeyIpForbiddenOrForbiddenOrScopeNotIssuableError",
253
+ },
254
+ "404": {
255
+ "$ref": "#/components/schemas/ApiKeyNotFoundOrApiKeysNotEnabledOrOrganizationNotFoundOrProjectNotFoundError",
256
+ },
257
+ },
258
+ "operationId": "rotateApiKey",
259
+ "response": {
260
+ "$ref": "#/components/schemas/RotateApiKeyResponse",
261
+ },
262
+ },
234
263
  "timestamps": {
235
264
  "createdAt": "date-time",
236
265
  "expiresAt": "date-time",
@@ -287,6 +316,54 @@ describe('keys', () => {
287
316
 
288
317
  const write = await app.request(base, json('POST', owner, { scopes: ['management:write'] }))
289
318
  expect(write.status).toBe(200)
319
+
320
+ const created = await TestApp.json(write, Keys.schema.createApiKey.Response)
321
+ const rotateDenied = await app.request(`${base}/${created.id}/rotate`, {
322
+ headers: { cookie: admin },
323
+ method: 'POST',
324
+ })
325
+ expect(rotateDenied.status).toBe(403)
326
+ expect(await rotateDenied.json()).toMatchObject({ error: { code: 'scope_not_issuable' } })
327
+
328
+ const rotateAllowed = await app.request(`${base}/${created.id}/rotate`, {
329
+ headers: { cookie: owner },
330
+ method: 'POST',
331
+ })
332
+ expect(rotateAllowed.status).toBe(200)
333
+ })
334
+
335
+ test('requires an owner to rotate wildcard keys and allows the super admin', async () => {
336
+ const db = TestApp.database()
337
+ const store = TestApp.kvStore({ keys: [] })
338
+ const app = createApp({ db, kv: { store } })
339
+ const owner = await session(app)
340
+ const { org, project } = await fixture(app, owner)
341
+ const admin = await session(app)
342
+ const user = await TestApp.json(
343
+ await app.request('/v1/me', { headers: { cookie: admin } }),
344
+ Me.schema.User,
345
+ )
346
+ await Memberships.create(db, { orgId: org.id, role: 'admin', userId: user.id })
347
+ const base = `/v1/orgs/${org.id}/projects/${project.id}/api-keys`
348
+ const { record: source } = await ApiKeys.mint(store, {
349
+ environment: 'production',
350
+ orgId: org.id,
351
+ projectId: project.id,
352
+ scopes: [Scope.wildcard],
353
+ })
354
+
355
+ const denied = await app.request(`${base}/${source.id}/rotate`, {
356
+ headers: { cookie: admin },
357
+ method: 'POST',
358
+ })
359
+ expect(denied.status).toBe(403)
360
+ expect(await denied.json()).toMatchObject({ error: { code: 'scope_not_issuable' } })
361
+
362
+ const allowed = await app.request(`${base}/${source.id}/rotate`, {
363
+ headers: { 'tempo-api-key': secret },
364
+ method: 'POST',
365
+ })
366
+ expect(allowed.status).toBe(200)
290
367
  })
291
368
 
292
369
  test('zone scopes are never self-serve', async () => {
@@ -304,6 +381,149 @@ describe('keys', () => {
304
381
  }
305
382
  })
306
383
 
384
+ test('rotates a super-admin-issued zone key without delegating scope issuance', async () => {
385
+ const store = TestApp.kvStore({ keys: [] })
386
+ const app = createApp({ kv: { store } })
387
+ const owner = await session(app)
388
+ const { org, project } = await fixture(app, owner)
389
+ const base = `/v1/orgs/${org.id}/projects/${project.id}/api-keys`
390
+ const source = await TestApp.json(
391
+ await app.request(
392
+ base,
393
+ superAdminJson('POST', {
394
+ allowedIps: ['203.0.113.0/24'],
395
+ environment: 'production',
396
+ name: 'Privy',
397
+ scopes: ['zone:421700001:read', 'zone:421700001:write'],
398
+ }),
399
+ ),
400
+ Keys.schema.createApiKey.Response,
401
+ )
402
+
403
+ const response = await app.request(`${base}/${source.id}/rotate`, {
404
+ headers: { cookie: owner },
405
+ method: 'POST',
406
+ })
407
+ expect(response.status).toBe(200)
408
+ const replacement = await TestApp.json(response, Keys.schema.rotateApiKey.Response)
409
+ expect(replacement).toMatchObject({
410
+ allowedIps: ['203.0.113.0/24'],
411
+ environment: 'production',
412
+ name: 'Privy',
413
+ orgId: org.id,
414
+ projectId: project.id,
415
+ scopes: ['zone:421700001:read', 'zone:421700001:write'],
416
+ })
417
+ expect(replacement.createdBy).not.toBe('super_admin')
418
+ expect(replacement.id).not.toBe(source.id)
419
+ expect(replacement.token).not.toBe(source.token)
420
+ expect(await ApiKeys.resolve(store, source.token)).not.toBeNull()
421
+ expect(await ApiKeys.resolve(store, replacement.token)).not.toBeNull()
422
+ })
423
+
424
+ test('rotates an organization-level super-admin-issued zone key', async () => {
425
+ const store = TestApp.kvStore({ keys: [] })
426
+ const app = createApp({ kv: { store } })
427
+ const owner = await session(app)
428
+ const { org, project } = await fixture(app, owner)
429
+ const { record: projectSource } = await ApiKeys.mint(store, {
430
+ orgId: org.id,
431
+ projectId: project.id,
432
+ scopes: ['data:read'],
433
+ })
434
+ const projectResponse = await app.request(
435
+ `/v1/orgs/${org.id}/api-keys/${projectSource.id}/rotate`,
436
+ { headers: { cookie: owner }, method: 'POST' },
437
+ )
438
+ expect(projectResponse.status).toBe(404)
439
+ const projectRevoke = await app.request(`/v1/orgs/${org.id}/api-keys/${projectSource.id}`, {
440
+ headers: { cookie: owner },
441
+ method: 'DELETE',
442
+ })
443
+ expect(projectRevoke.status).toBe(404)
444
+
445
+ const { record: source } = await ApiKeys.mint(store, {
446
+ createdBy: 'super_admin',
447
+ environment: 'production',
448
+ name: 'Privy',
449
+ orgId: org.id,
450
+ scopes: ['zone:421700001:read', 'zone:421700001:write'],
451
+ })
452
+
453
+ const response = await app.request(`/v1/orgs/${org.id}/api-keys/${source.id}/rotate`, {
454
+ headers: { cookie: owner },
455
+ method: 'POST',
456
+ })
457
+ expect(response.status).toBe(200)
458
+ const replacement = await TestApp.json(response, Keys.schema.rotateApiKey.Response)
459
+ expect(replacement).toMatchObject({
460
+ environment: 'production',
461
+ name: 'Privy',
462
+ orgId: org.id,
463
+ scopes: ['zone:421700001:read', 'zone:421700001:write'],
464
+ })
465
+ expect(replacement.projectId).toBeUndefined()
466
+ expect(replacement.createdBy).not.toBe('super_admin')
467
+ expect(replacement.id).not.toBe(source.id)
468
+
469
+ const revoked = await app.request(`/v1/orgs/${org.id}/api-keys/${replacement.id}`, {
470
+ headers: { cookie: owner },
471
+ method: 'DELETE',
472
+ })
473
+ expect(revoked.status).toBe(200)
474
+ expect(await TestApp.json(revoked, Keys.schema.revokeApiKey.Response)).toEqual({
475
+ id: replacement.id,
476
+ })
477
+ expect(await ApiKeys.resolve(store, replacement.token)).toBeNull()
478
+ })
479
+
480
+ test('refreshes sandbox billing state during rotation', async () => {
481
+ const db = TestApp.database()
482
+ const store = TestApp.kvStore({ keys: [] })
483
+ const app = createApp({ db, kv: { store } })
484
+ const owner = await session(app)
485
+ const { org, project } = await fixture(app, owner)
486
+ const { record: source } = await ApiKeys.mint(store, {
487
+ billingActive: true,
488
+ environment: 'sandbox',
489
+ orgId: org.id,
490
+ projectId: project.id,
491
+ scopes: ['data:read'],
492
+ })
493
+
494
+ const response = await app.request(
495
+ `/v1/orgs/${org.id}/projects/${project.id}/api-keys/${source.id}/rotate`,
496
+ { headers: { cookie: owner }, method: 'POST' },
497
+ )
498
+ expect(response.status).toBe(200)
499
+ const replacement = await TestApp.json(response, Keys.schema.rotateApiKey.Response)
500
+ expect((await ApiKeys.get(store, replacement.id))?.billingActive).toBe(false)
501
+ })
502
+
503
+ test('does not rotate a key from another project', async () => {
504
+ const app = createApp()
505
+ const owner = await session(app)
506
+ const { org, project } = await fixture(app, owner)
507
+ const other = await TestApp.json(
508
+ await app.request(`/v1/orgs/${org.id}/projects`, json('POST', owner, { name: 'Other' })),
509
+ Projects.schema.Project,
510
+ )
511
+ const source = await TestApp.json(
512
+ await app.request(
513
+ `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
514
+ superAdminJson('POST', { scopes: ['zone:421700001:read'] }),
515
+ ),
516
+ Keys.schema.createApiKey.Response,
517
+ )
518
+
519
+ const response = await app.request(
520
+ `/v1/orgs/${org.id}/projects/${other.id}/api-keys/${source.id}/rotate`,
521
+ { headers: { cookie: owner }, method: 'POST' },
522
+ )
523
+ expect(response.status).toBe(404)
524
+ expect(await response.json()).toMatchObject({ error: { code: 'api_key_not_found' } })
525
+ })
526
+
307
527
  test('restricts project management keys without delegating issuance', async () => {
308
528
  const app = createApp()
309
529
  const cookie = await session(app)
@@ -12,6 +12,7 @@ import * as Billing from '../Billing.js'
12
12
  import * as OpenApi from '../../../internal/OpenApi.js'
13
13
  import * as Response from '../../../internal/Response.js'
14
14
  import * as Schema from '../../../internal/Schema.js'
15
+ import * as Store from '../../../internal/Store.js'
15
16
  import * as Scope from '../../../Scope.js'
16
17
  import * as ManagementError from '../Error.js'
17
18
 
@@ -147,6 +148,24 @@ export namespace schema {
147
148
  })
148
149
  .check(z.describe('Path parameters for one API key.'))
149
150
 
151
+ /** Path parameters addressing one organization-level API key. */
152
+ export const OrgKeyParams = z
153
+ .object({
154
+ keyId: z
155
+ .string()
156
+ .check(
157
+ z.describe('The key id (`key_…`).'),
158
+ z.meta({ examples: ['key_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
159
+ ),
160
+ orgId: z
161
+ .string()
162
+ .check(
163
+ z.describe('The organization id (`org_…`).'),
164
+ z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
165
+ ),
166
+ })
167
+ .check(z.describe('Path parameters for one organization-level API key.'))
168
+
150
169
  /** Request body minting a key. The wildcard scope is not issuable here. */
151
170
  export const CreateApiKeyRequest = OpenApi.component(
152
171
  z
@@ -251,6 +270,25 @@ export namespace schema {
251
270
  )
252
271
  }
253
272
 
273
+ /** Schemas for the rotateApiKey operation. */
274
+ export namespace rotateApiKey {
275
+ /** The replacement key plus its one-time token. */
276
+ export const Response = OpenApi.component(
277
+ Schema.describe(
278
+ z.extend(Key, {
279
+ token: z
280
+ .string()
281
+ .check(
282
+ z.describe('The plaintext token — shown once, unrecoverable afterward.'),
283
+ z.meta({ examples: ['tempo_sandbox:sk:…'] }),
284
+ ),
285
+ }),
286
+ 'The replacement key, including its one-time token.',
287
+ ),
288
+ 'RotateApiKeyResponse',
289
+ )
290
+ }
291
+
254
292
  /** Schemas for the updateApiKey operation. */
255
293
  export namespace updateApiKey {
256
294
  /** Request body replacing or clearing a key's IP allowlist. */
@@ -518,6 +556,124 @@ export function apiKeys(options: apiKeys.Options = {}) {
518
556
  }
519
557
  },
520
558
  )
559
+ .post(
560
+ '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/api-keys/:keyId{key_[A-Za-z0-9_-]+}/rotate',
561
+ Auth.policy({ session: true }),
562
+ Auth.ensureOrg({ role: 'admin' }),
563
+ OpenApi.validate('param', schema.OrgKeyParams, {
564
+ code: 'param_invalid',
565
+ message: 'Check the path parameters and try again.',
566
+ }),
567
+ OpenApi.describeRoute({
568
+ description:
569
+ 'Create a replacement organization-level API key with the same access as an existing key. The existing key remains active until revoked.',
570
+ hide: hidden,
571
+ operationId: 'rotateOrgApiKey',
572
+ responses: OpenApi.responses({
573
+ errors: {
574
+ 400: { codes: ['param_invalid'], description: 'Malformed API key or invalid path.' },
575
+ 403: {
576
+ codes: ['scope_not_issuable'],
577
+ description: 'The source key contains scopes the caller cannot rotate.',
578
+ },
579
+ 404: {
580
+ codes: ['api_key_not_found', 'api_keys_not_enabled', 'organization_not_found'],
581
+ description: 'No accessible API key or API key surface was found.',
582
+ },
583
+ },
584
+ success: {
585
+ description: 'The replacement key, including its one-time token.',
586
+ schema: schema.rotateApiKey.Response,
587
+ },
588
+ }),
589
+ summary: 'Rotate organization API key',
590
+ tags: ['API Keys'],
591
+ }),
592
+ async (c) => {
593
+ if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
594
+ if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c)
595
+ if (OpenApi.narrowValidation)
596
+ return OpenApi.validationError(c, {
597
+ code: 'param_invalid',
598
+ message: 'Check the path parameters and try again.',
599
+ })
600
+
601
+ const kv = c.get('kv')
602
+ if (!kv) return notEnabled(c)
603
+ const createdBy = mintedBy(c)
604
+ if (!createdBy) return ManagementError.sessionRequired(c)
605
+ const { keyId } = c.req.valid('param')
606
+ try {
607
+ const source = await ApiKeys.get(kv.store, keyId, {
608
+ scopeCatalog: c.get('scopeCatalog'),
609
+ })
610
+ if (!source || source.orgId !== Auth.org(c).id || source.projectId !== undefined)
611
+ return keyNotFound(c)
612
+ if (!canRotate(c, source.scopes))
613
+ return Response.error(c, {
614
+ code: 'scope_not_issuable',
615
+ message: 'The source key contains scopes the caller cannot rotate.',
616
+ status: 403,
617
+ })
618
+ return rotate(c, kv.store, source, createdBy)
619
+ } catch (cause) {
620
+ return Response.upstream(c, cause)
621
+ }
622
+ },
623
+ )
624
+ .delete(
625
+ '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/api-keys/:keyId{key_[A-Za-z0-9_-]+}',
626
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
627
+ Auth.ensureOrg({ role: 'admin' }),
628
+ OpenApi.validate('param', schema.OrgKeyParams, {
629
+ code: 'param_invalid',
630
+ message: 'Check the path parameters and try again.',
631
+ }),
632
+ OpenApi.describeRoute({
633
+ description:
634
+ 'Revoke an organization-level API key. Requests presenting its token stop resolving.',
635
+ hide: hidden,
636
+ operationId: 'revokeOrgApiKey',
637
+ responses: OpenApi.responses({
638
+ errors: {
639
+ 400: { codes: ['param_invalid'], description: 'Malformed API key or invalid path.' },
640
+ 404: {
641
+ codes: ['api_key_not_found', 'api_keys_not_enabled', 'organization_not_found'],
642
+ description: 'No accessible API key or API key surface was found.',
643
+ },
644
+ },
645
+ success: {
646
+ description: 'Confirmation that the key was revoked.',
647
+ schema: schema.revokeApiKey.Response,
648
+ },
649
+ }),
650
+ summary: 'Revoke organization API key',
651
+ tags: ['API Keys'],
652
+ }),
653
+ async (c) => {
654
+ if (Auth.narrowAccess) return Auth.accessError(c)
655
+ if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c)
656
+ if (OpenApi.narrowValidation)
657
+ return OpenApi.validationError(c, {
658
+ code: 'param_invalid',
659
+ message: 'Check the path parameters and try again.',
660
+ })
661
+
662
+ const kv = c.get('kv')
663
+ if (!kv) return notEnabled(c)
664
+ const { keyId } = c.req.valid('param')
665
+ try {
666
+ const record = await ApiKeys.get(kv.store, keyId, {
667
+ scopeCatalog: c.get('scopeCatalog'),
668
+ })
669
+ if (!record || record.orgId !== Auth.org(c).id || record.projectId !== undefined)
670
+ return keyNotFound(c)
671
+ return revoke(c, kv.store, keyId)
672
+ } catch (cause) {
673
+ return Response.upstream(c, cause)
674
+ }
675
+ },
676
+ )
521
677
  .patch(
522
678
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys/:keyId{key_[A-Za-z0-9_-]+}',
523
679
  Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
@@ -594,6 +750,78 @@ export function apiKeys(options: apiKeys.Options = {}) {
594
750
  }
595
751
  },
596
752
  )
753
+ .post(
754
+ '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys/:keyId{key_[A-Za-z0-9_-]+}/rotate',
755
+ Auth.policy({ session: true }),
756
+ Auth.ensureOrg({ role: 'admin' }),
757
+ Auth.ensureProject(),
758
+ OpenApi.validate('param', schema.KeyParams, {
759
+ code: 'param_invalid',
760
+ message: 'Check the path parameters and try again.',
761
+ }),
762
+ OpenApi.describeRoute({
763
+ description:
764
+ 'Create a replacement API key with the same access as an existing key. The existing key remains active until revoked.',
765
+ hide: hidden,
766
+ operationId: 'rotateApiKey',
767
+ responses: OpenApi.responses({
768
+ errors: {
769
+ 400: { codes: ['param_invalid'], description: 'Malformed API key or invalid path.' },
770
+ 403: {
771
+ codes: ['scope_not_issuable'],
772
+ description: 'The source key contains scopes the caller cannot rotate.',
773
+ },
774
+ 404: {
775
+ codes: [
776
+ 'api_key_not_found',
777
+ 'api_keys_not_enabled',
778
+ 'organization_not_found',
779
+ 'project_not_found',
780
+ ],
781
+ description: 'No accessible API key or API key surface was found.',
782
+ },
783
+ },
784
+ success: {
785
+ description: 'The replacement key, including its one-time token.',
786
+ schema: schema.rotateApiKey.Response,
787
+ },
788
+ }),
789
+ summary: 'Rotate API key',
790
+ tags: ['API Keys'],
791
+ }),
792
+ async (c) => {
793
+ if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
794
+ if (Auth.narrowScope) return Auth.ensureProjectError(c)
795
+ if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c)
796
+ if (OpenApi.narrowValidation)
797
+ return OpenApi.validationError(c, {
798
+ code: 'param_invalid',
799
+ message: 'Check the path parameters and try again.',
800
+ })
801
+
802
+ const kv = c.get('kv')
803
+ if (!kv) return notEnabled(c)
804
+ const createdBy = mintedBy(c)
805
+ if (!createdBy) return ManagementError.sessionRequired(c)
806
+ const { keyId } = c.req.valid('param')
807
+ try {
808
+ const source = await ApiKeys.get(kv.store, keyId, {
809
+ scopeCatalog: c.get('scopeCatalog'),
810
+ })
811
+ if (!source || source.orgId !== Auth.org(c).id || source.projectId !== Auth.project(c).id)
812
+ return keyNotFound(c)
813
+ if (!canRotate(c, source.scopes))
814
+ return Response.error(c, {
815
+ code: 'scope_not_issuable',
816
+ message: 'The source key contains scopes the caller cannot rotate.',
817
+ status: 403,
818
+ })
819
+ return rotate(c, kv.store, source, createdBy)
820
+ } catch (cause) {
821
+ return Response.upstream(c, cause)
822
+ }
823
+ },
824
+ )
597
825
  .delete(
598
826
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys/:keyId{key_[A-Za-z0-9_-]+}',
599
827
  Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
@@ -649,9 +877,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
649
877
  })
650
878
  if (!record || record.orgId !== Auth.org(c).id || record.projectId !== Auth.project(c).id)
651
879
  return keyNotFound(c)
652
- const revoked = await ApiKeys.revoke(kv.store, keyId)
653
- if (!revoked) return keyNotFound(c)
654
- return c.json(Response.validated(schema.revokeApiKey.Response, { id: keyId }), 200)
880
+ return revoke(c, kv.store, keyId)
655
881
  } catch (cause) {
656
882
  return Response.upstream(c, cause)
657
883
  }
@@ -722,6 +948,57 @@ function canIssue(c: Context<App.Environment>, scopes: readonly Scope.Id[]) {
722
948
  return false
723
949
  }
724
950
 
951
+ /** Checks caller-specific restrictions that still apply when copying existing grants. */
952
+ function canRotate(c: Context<App.Environment>, scopes: readonly Scope.Id[]) {
953
+ const principal = Auth.getPrincipal(c)
954
+ if (principal?.type === 'super_admin') return true
955
+ if (principal?.type !== 'session') return false
956
+ return (
957
+ (!scopes.includes('management:write') && !scopes.includes(Scope.wildcard)) ||
958
+ Auth.membership(c)?.role === 'owner'
959
+ )
960
+ }
961
+
962
+ /** Mints a replacement that preserves the source key's access and attribution. */
963
+ async function rotate(
964
+ c: Context<App.Environment>,
965
+ store: Store.State,
966
+ source: ApiKeys.Record,
967
+ createdBy: string,
968
+ ) {
969
+ const billingActive =
970
+ source.environment === 'sandbox'
971
+ ? await Billing.active(Db.get(c.get('db')), source.orgId, 'sandbox')
972
+ : undefined
973
+ const { record, token } = await ApiKeys.mint(
974
+ store,
975
+ {
976
+ allowedIps: source.allowedIps,
977
+ createdBy,
978
+ environment: source.environment,
979
+ orgId: source.orgId,
980
+ ...(source.projectId === undefined ? {} : { projectId: source.projectId }),
981
+ scopes: source.scopes,
982
+ ...(billingActive === undefined ? {} : { billingActive }),
983
+ ...(source.expiresAt === undefined ? {} : { expiresAt: source.expiresAt }),
984
+ ...(source.name === undefined ? {} : { name: source.name }),
985
+ ...(source.rateLimits === undefined ? {} : { rateLimits: source.rateLimits }),
986
+ },
987
+ { scopeCatalog: c.get('scopeCatalog') },
988
+ )
989
+ return c.json(
990
+ Response.validated(schema.rotateApiKey.Response, { ...serializeKey(record), token }),
991
+ 200,
992
+ )
993
+ }
994
+
995
+ /** Revokes an API key after its route has verified ownership. */
996
+ async function revoke(c: Context<App.Environment>, store: Store.State, keyId: string) {
997
+ const revoked = await ApiKeys.revoke(store, keyId)
998
+ if (!revoked) return keyNotFound(c)
999
+ return c.json(Response.validated(schema.revokeApiKey.Response, { id: keyId }), 200)
1000
+ }
1001
+
725
1002
  function keyNotFound(c: Context<App.Environment>) {
726
1003
  return Response.error(c, {
727
1004
  code: 'api_key_not_found',
package/src/db/Db.ts CHANGED
@@ -1483,6 +1483,11 @@ export const migrations: readonly Migration[] = [
1483
1483
  sql`ALTER TABLE earn_vaults ADD COLUMN IF NOT EXISTS zones jsonb NOT NULL DEFAULT '[]'::jsonb`,
1484
1484
  ),
1485
1485
  },
1486
+ {
1487
+ name: '0137_funding_routes_configuration',
1488
+ up: (db) =>
1489
+ raw(db, sql`ALTER TABLE funding_routes ADD COLUMN IF NOT EXISTS configuration jsonb`),
1490
+ },
1486
1491
  ]
1487
1492
 
1488
1493
  /**
@@ -1620,7 +1625,14 @@ declare namespace fromDialect {
1620
1625
  */
1621
1626
  export function postgres(options: postgres.Options): Db {
1622
1627
  const { connectionString, schema } = options
1623
- const pool = new pg.Pool(schema ? { connectionString, max: 1 } : { connectionString })
1628
+ // Bounded connect and pool-checkout wait: the pg default (0) waits forever,
1629
+ // so an unresponsive origin parks callers and their invocation memory
1630
+ // invisibly instead of failing into the retry paths.
1631
+ const pool = new pg.Pool({
1632
+ connectionString,
1633
+ connectionTimeoutMillis: 30_000,
1634
+ ...(schema ? { max: 1 } : {}),
1635
+ })
1624
1636
  return fromDialect(new PostgresDialect({ pool }), { schema })
1625
1637
  }
1626
1638
 
package/src/db/Schema.ts CHANGED
@@ -453,11 +453,14 @@ export type FundingChainToken = z.output<typeof FundingChainToken>
453
453
 
454
454
  /** One source-to-destination route supported by a funding provider. */
455
455
  export const FundingRoute = z.object({
456
- destinationChainId: z.string().check(z.describe('Destination funding chain identifier.')),
457
- destinationTokenId: z.string().check(z.describe('Destination funding token identifier.')),
458
456
  capabilities: z
459
457
  .nullable(z.custom<funding_Catalog.Capabilities>())
460
458
  .check(z.describe('Executable capability (`jsonb`), or null for indicative-only routes.')),
459
+ configuration: z
460
+ .nullable(z.custom<funding_Catalog.Configuration>())
461
+ .check(z.describe('Provider-specific route configuration (`jsonb`), or null.')),
462
+ destinationChainId: z.string().check(z.describe('Destination funding chain identifier.')),
463
+ destinationTokenId: z.string().check(z.describe('Destination funding token identifier.')),
461
464
  providerId: z.string().check(z.describe('Funding provider identifier.')),
462
465
  sourceChainId: z.string().check(z.describe('Source funding chain identifier.')),
463
466
  sourceTokenId: z.string().check(z.describe('Source funding token identifier.')),
@@ -13,9 +13,9 @@ const input = {
13
13
  vaultAddress: '0x2f05f4d3625B2eFFb383eFbEDAFd2D8F04371636',
14
14
  zones: [
15
15
  {
16
- chainId: 1_424_310_001,
17
- deploymentBlock: 31_039_238,
18
- earnRouter: '0x9144086A79dea966d8bf1B52125eBED8919fC45c',
16
+ chainId: 1_424_310_003,
17
+ deploymentBlock: 31_730_087,
18
+ earnRouter: '0xeBEdC74FFeEAD186ACC6caC05Eb82fB9e093a7c7',
19
19
  },
20
20
  ],
21
21
  } as const satisfies EarnVaults.upsert.Options
@@ -45,9 +45,9 @@ describe('upsert', () => {
45
45
  "vaultAddress": "0x2f05f4d3625b2effb383efbedafd2d8f04371636",
46
46
  "zones": [
47
47
  {
48
- "chainId": 1424310001,
49
- "deploymentBlock": 31039238,
50
- "earnRouter": "0x9144086a79dea966d8bf1b52125ebed8919fc45c",
48
+ "chainId": 1424310003,
49
+ "deploymentBlock": 31730087,
50
+ "earnRouter": "0xebedc74ffeead186acc6cac05eb82fb9e093a7c7",
51
51
  },
52
52
  ],
53
53
  }
@@ -93,9 +93,9 @@ describe('upsert', () => {
93
93
  "updatedAt": "2026-01-02T00:00:00.000Z",
94
94
  "zones": [
95
95
  {
96
- "chainId": 1424310001,
97
- "deploymentBlock": 31039238,
98
- "earnRouter": "0x9144086a79dea966d8bf1b52125ebed8919fc45c",
96
+ "chainId": 1424310003,
97
+ "deploymentBlock": 31730087,
98
+ "earnRouter": "0xebedc74ffeead186acc6cac05eb82fb9e093a7c7",
99
99
  },
100
100
  ],
101
101
  }