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.
- package/dist/App.d.ts +5561 -5026
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +38 -3
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +2 -0
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +17 -0
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +123 -74
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +80 -0
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +54 -0
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/earn-vaults.d.ts +15 -15
- package/dist/admin/apps/earn-vaults.js +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +81 -81
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +151 -151
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/Zones.d.ts +16 -16
- package/dist/apps/Zones.js +4 -4
- package/dist/apps/data/App.js +18 -18
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/ZoneSelection.d.ts +8 -8
- package/dist/apps/data/ZoneSelection.d.ts.map +1 -1
- package/dist/apps/data/ZoneSelection.js +1 -1
- package/dist/apps/data/ZoneSelection.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +588 -584
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +1561 -1239
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +26 -26
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +43 -16
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +44 -44
- package/dist/apps/data/routes/blocks.d.ts.map +1 -1
- package/dist/apps/data/routes/blocks.js +21 -11
- package/dist/apps/data/routes/blocks.js.map +1 -1
- package/dist/apps/data/routes/coingecko.d.ts +3 -3
- package/dist/apps/data/routes/earn.d.ts +18 -18
- package/dist/apps/data/routes/earn.js +13 -13
- package/dist/apps/data/routes/exchanges.d.ts +145 -145
- package/dist/apps/data/routes/fee-amm.d.ts +2 -2
- package/dist/apps/data/routes/receipts.d.ts +155 -55
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +28 -10
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +13 -13
- package/dist/apps/data/routes/tokens.d.ts +98 -98
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/tokens.js +51 -20
- package/dist/apps/data/routes/tokens.js.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +7 -2
- package/dist/apps/data/routes/transactions.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.js +37 -17
- package/dist/apps/data/routes/transactions.js.map +1 -1
- package/dist/apps/data/routes/transfers.d.ts +1 -1
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +18 -12
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +1 -1
- package/dist/apps/data/routes/valuation.d.ts.map +1 -1
- package/dist/apps/data/routes/valuation.js +12 -7
- package/dist/apps/data/routes/valuation.js.map +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +3 -3
- package/dist/apps/data/routes/webhooks.d.ts +11 -11
- package/dist/apps/data/routes/zones.d.ts +14 -14
- package/dist/apps/data/routes/zones.js +1 -1
- package/dist/apps/funding/App.d.ts +11 -0
- package/dist/apps/funding/App.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js +16 -11
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/transfers.d.ts +60 -48
- package/dist/apps/funding/routes/transfers.d.ts.map +1 -1
- package/dist/apps/funding/routes/transfers.js +27 -19
- package/dist/apps/funding/routes/transfers.js.map +1 -1
- package/dist/apps/management/App.d.ts +4487 -3963
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +710 -159
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +236 -4
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +6 -6
- package/dist/apps/management/routes/invite-links.d.ts +42 -42
- package/dist/apps/management/routes/members.d.ts +30 -30
- package/dist/apps/management/routes/projects.d.ts +40 -40
- package/dist/apps/management/routes/usage.d.ts +24 -24
- package/dist/apps/management/routes/verified-token-requests.d.ts +1 -1
- package/dist/apps/management/routes/webhooks.d.ts +55 -55
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +12 -1
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +3 -2
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +5 -2
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/fundingCatalog.d.ts +4 -2
- package/dist/db/tables/fundingCatalog.d.ts.map +1 -1
- package/dist/db/tables/fundingCatalog.js +5 -1
- package/dist/db/tables/fundingCatalog.js.map +1 -1
- package/dist/handlers/internal/utils.d.ts.map +1 -1
- package/dist/handlers/internal/utils.js +9 -0
- package/dist/handlers/internal/utils.js.map +1 -1
- package/dist/internal/Auth.d.ts +3 -1
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +6 -3
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/EarnVaults.d.ts +3 -3
- package/dist/internal/EarnVaults.js +3 -3
- package/dist/internal/Fetch.d.ts +23 -0
- package/dist/internal/Fetch.d.ts.map +1 -0
- package/dist/internal/Fetch.js +26 -0
- package/dist/internal/Fetch.js.map +1 -0
- package/dist/internal/Log.d.ts +3 -1
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +16 -3
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +5 -3
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +13 -3
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/Response.d.ts +1 -1
- package/dist/internal/Response.d.ts.map +1 -1
- package/dist/internal/Schema.d.ts +6 -3
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +8 -2
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Store.d.ts +16 -3
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +82 -16
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +7 -2
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Timeout.d.ts +36 -0
- package/dist/internal/Timeout.d.ts.map +1 -0
- package/dist/internal/Timeout.js +37 -0
- package/dist/internal/Timeout.js.map +1 -0
- package/dist/internal/Timing.d.ts +9 -0
- package/dist/internal/Timing.d.ts.map +1 -1
- package/dist/internal/Timing.js +7 -0
- package/dist/internal/Timing.js.map +1 -1
- package/dist/internal/WebhookDestination.d.ts.map +1 -1
- package/dist/internal/WebhookDestination.js +3 -0
- package/dist/internal/WebhookDestination.js.map +1 -1
- package/dist/internal/funding/Action.d.ts +9 -0
- package/dist/internal/funding/Action.d.ts.map +1 -1
- package/dist/internal/funding/Action.js +16 -8
- package/dist/internal/funding/Action.js.map +1 -1
- package/dist/internal/funding/Catalog.d.ts +9 -2
- package/dist/internal/funding/Catalog.d.ts.map +1 -1
- package/dist/internal/funding/Catalog.js +19 -8
- package/dist/internal/funding/Catalog.js.map +1 -1
- package/dist/internal/funding/Provider.d.ts +68 -13
- package/dist/internal/funding/Provider.d.ts.map +1 -1
- package/dist/internal/funding/Provider.js +29 -8
- package/dist/internal/funding/Provider.js.map +1 -1
- package/dist/internal/funding/Transfer.d.ts +13 -12
- package/dist/internal/funding/Transfer.d.ts.map +1 -1
- package/dist/internal/funding/Transfer.js +23 -21
- package/dist/internal/funding/Transfer.js.map +1 -1
- package/dist/internal/funding/providers/stargate.d.ts +28 -9
- package/dist/internal/funding/providers/stargate.d.ts.map +1 -1
- package/dist/internal/funding/providers/stargate.js +251 -117
- package/dist/internal/funding/providers/stargate.js.map +1 -1
- package/dist/internal/index.d.ts +1 -0
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +1 -0
- package/dist/internal/index.js.map +1 -1
- package/package.json +2 -2
- package/src/App.test.ts +100 -0
- package/src/App.ts +42 -3
- package/src/Client.test-d.ts +46 -0
- package/src/Scope.test.ts +20 -0
- package/src/Scope.ts +16 -0
- package/src/admin/apps/api-keys.test.ts +75 -0
- package/src/admin/apps/api-keys.ts +64 -0
- package/src/admin/apps/earn-vaults.test.ts +1 -1
- package/src/admin/apps/earn-vaults.ts +1 -1
- package/src/admin/ui/src/lib/api-keys.ts +19 -0
- package/src/admin/ui/src/routes/api-keys.tsx +89 -2
- package/src/admin/ui.generated.ts +189 -151
- package/src/apps/Zones.test.ts +4 -4
- package/src/apps/Zones.ts +4 -4
- package/src/apps/composition.test.ts +1 -1
- package/src/apps/data/App.test.ts +41 -0
- package/src/apps/data/App.ts +18 -18
- package/src/apps/data/ZoneSelection.test.ts +2 -2
- package/src/apps/data/ZoneSelection.ts +8 -8
- package/src/apps/data/routes/activities.test.ts +1526 -12
- package/src/apps/data/routes/activities.ts +1774 -1356
- package/src/apps/data/routes/balances.ts +133 -97
- package/src/apps/data/routes/blocks.test.ts +533 -0
- package/src/apps/data/routes/blocks.ts +35 -16
- package/src/apps/data/routes/earn.test.ts +4 -2
- package/src/apps/data/routes/earn.ts +13 -13
- package/src/apps/data/routes/earn.zone-access.test.ts +6 -6
- package/src/apps/data/routes/receipts.ts +47 -16
- package/src/apps/data/routes/tokens.test.ts +578 -0
- package/src/apps/data/routes/tokens.ts +283 -225
- package/src/apps/data/routes/transactions.ts +68 -29
- package/src/apps/data/routes/transfers.test.ts +421 -0
- package/src/apps/data/routes/transfers.ts +45 -27
- package/src/apps/data/routes/valuation.ts +94 -78
- package/src/apps/data/routes/zones.test.ts +2 -2
- package/src/apps/data/routes/zones.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.test.ts +25 -3
- package/src/apps/funding/routes/deposit-addresses.ts +17 -12
- package/src/apps/funding/routes/quotes.test.ts +5 -9
- package/src/apps/funding/routes/transfers.test.ts +395 -8
- package/src/apps/funding/routes/transfers.ts +76 -59
- package/src/apps/management/routes/api-keys.test.ts +220 -0
- package/src/apps/management/routes/api-keys.ts +280 -3
- package/src/db/Db.ts +13 -1
- package/src/db/Schema.ts +5 -2
- package/src/db/tables/earnVaults.test.ts +9 -9
- package/src/db/tables/fundingCatalog.test.ts +16 -2
- package/src/db/tables/fundingCatalog.ts +9 -3
- package/src/handlers/internal/utils.test.ts +53 -0
- package/src/handlers/internal/utils.ts +15 -0
- package/src/internal/Auth.ts +12 -4
- package/src/internal/EarnVaults.capabilities.test.ts +2 -2
- package/src/internal/EarnVaults.test.ts +1 -1
- package/src/internal/EarnVaults.ts +3 -3
- package/src/internal/Fetch.test.ts +46 -0
- package/src/internal/Fetch.ts +56 -0
- package/src/internal/Log.test.ts +21 -0
- package/src/internal/Log.ts +20 -5
- package/src/internal/OpenApi.ts +14 -4
- package/src/internal/Response.ts +1 -1
- package/src/internal/Schema.test.ts +2 -0
- package/src/internal/Schema.ts +12 -2
- package/src/internal/Store.test.ts +178 -0
- package/src/internal/Store.ts +104 -18
- package/src/internal/Tidx.test.ts +2 -1
- package/src/internal/Tidx.ts +7 -2
- package/src/internal/Timeout.test.ts +35 -0
- package/src/internal/Timeout.ts +73 -0
- package/src/internal/Timing.ts +16 -0
- package/src/internal/WebhookDestination.ts +3 -0
- package/src/internal/funding/Action.test.ts +41 -1
- package/src/internal/funding/Action.ts +69 -41
- package/src/internal/funding/Catalog.test.ts +34 -0
- package/src/internal/funding/Catalog.ts +28 -11
- package/src/internal/funding/Provider.test.ts +73 -0
- package/src/internal/funding/Provider.ts +82 -28
- package/src/internal/funding/Transfer.ts +133 -110
- package/src/internal/funding/providers/index.test.ts +4 -167
- package/src/internal/funding/providers/relay.test.ts +2 -2
- package/src/internal/funding/providers/stargate.test.ts +163 -0
- package/src/internal/funding/providers/stargate.ts +359 -134
- package/src/internal/index.test.ts +1 -0
- 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
|
-
|
|
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
|
-
|
|
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:
|
|
17
|
-
deploymentBlock:
|
|
18
|
-
earnRouter: '
|
|
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":
|
|
49
|
-
"deploymentBlock":
|
|
50
|
-
"earnRouter": "
|
|
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":
|
|
97
|
-
"deploymentBlock":
|
|
98
|
-
"earnRouter": "
|
|
96
|
+
"chainId": 1424310003,
|
|
97
|
+
"deploymentBlock": 31730087,
|
|
98
|
+
"earnRouter": "0xebedc74ffeead186acc6cac05eb82fb9e093a7c7",
|
|
99
99
|
},
|
|
100
100
|
],
|
|
101
101
|
}
|