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
package/src/App.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { compress as hono_compress } from 'hono/compress'
|
|
|
5
5
|
import { cors as hono_cors } from 'hono/cors'
|
|
6
6
|
import { HTTPException } from 'hono/http-exception'
|
|
7
7
|
import { requestId, type RequestIdVariables } from 'hono/request-id'
|
|
8
|
+
import { timeout as hono_timeout } from 'hono/timeout'
|
|
8
9
|
import { generateSpecs } from 'hono-openapi'
|
|
9
10
|
import type { Chain } from 'viem'
|
|
10
11
|
|
|
@@ -183,6 +184,10 @@ export declare namespace GetAsset {
|
|
|
183
184
|
/** Top-level OpenAPI `info.description`, rendered above the reference. */
|
|
184
185
|
const infoDescription = 'REST API for reading and interacting with Tempo'
|
|
185
186
|
|
|
187
|
+
// Comfortably above the slowest legitimate time-to-response observed in
|
|
188
|
+
// production (~40s tidx reads), far below the 17-100 minute stalls it bounds.
|
|
189
|
+
const requestTimeoutMs = 60_000
|
|
190
|
+
|
|
186
191
|
/** Creates the Tempo API Hono app. */
|
|
187
192
|
export function create<const path extends string = '/'>(options: create.Options<path>) {
|
|
188
193
|
const { path = '/' as path } = options
|
|
@@ -272,8 +277,8 @@ export function create<const path extends string = '/'>(options: create.Options<
|
|
|
272
277
|
base.use(
|
|
273
278
|
'*',
|
|
274
279
|
Log.middleware<Environment>({
|
|
275
|
-
emit: async (entry) => {
|
|
276
|
-
await log?.(entry)
|
|
280
|
+
emit: async (entry, cause) => {
|
|
281
|
+
await log?.(entry, cause)
|
|
277
282
|
recordRequest?.(entry)
|
|
278
283
|
// Return the analytics promise so `Log.middleware` extends the
|
|
279
284
|
// response with `waitUntil`; otherwise the async sink's `queue.send`
|
|
@@ -284,6 +289,30 @@ export function create<const path extends string = '/'>(options: create.Options<
|
|
|
284
289
|
}),
|
|
285
290
|
)
|
|
286
291
|
|
|
292
|
+
// Bounds time-to-response only; streaming bodies are unaffected. Without a
|
|
293
|
+
// deadline, a request stalled on an unbounded upstream wait pins its
|
|
294
|
+
// invocation (and isolate memory) until the client disconnects.
|
|
295
|
+
const deadline = hono_timeout(
|
|
296
|
+
requestTimeoutMs,
|
|
297
|
+
(c) =>
|
|
298
|
+
new HTTPException(504, {
|
|
299
|
+
res: Response.error(c, {
|
|
300
|
+
code: 'request_timeout',
|
|
301
|
+
message: 'Request timed out',
|
|
302
|
+
status: 504,
|
|
303
|
+
}),
|
|
304
|
+
}),
|
|
305
|
+
)
|
|
306
|
+
// Safe methods only: the expired handler is abandoned, not aborted, so a
|
|
307
|
+
// mutation could still commit after the 504. Paid requests are also exempt:
|
|
308
|
+
// MPP consumes payment before the handler, and the Payment-Receipt attaches
|
|
309
|
+
// only when the handler returns through the payment wrapper.
|
|
310
|
+
base.use('*', (c, next) => {
|
|
311
|
+
if (c.req.method !== 'GET' && c.req.method !== 'HEAD') return next()
|
|
312
|
+
if (Auth.hasPaymentCredential(c)) return next()
|
|
313
|
+
return deadline(c, next)
|
|
314
|
+
})
|
|
315
|
+
|
|
287
316
|
// Normalize the `db` option: the plain form is `{ source }` with no cached
|
|
288
317
|
// side; `cached` falls back to `source` so consumers never branch.
|
|
289
318
|
const db = 'source' in options.db ? options.db : { source: options.db }
|
|
@@ -331,7 +360,7 @@ export function create<const path extends string = '/'>(options: create.Options<
|
|
|
331
360
|
c.set('providers', providers)
|
|
332
361
|
c.set('rpc', options.rpc)
|
|
333
362
|
c.set('scopeCatalog', scopeCatalog)
|
|
334
|
-
c.set('store', Store.withRequest(store, c.req))
|
|
363
|
+
c.set('store', Store.withRequest(store, c.req, { waitUntil: getWaitUntil(c) }))
|
|
335
364
|
c.set('supportedChainIds', supportedChainIds)
|
|
336
365
|
c.set('zones', zones)
|
|
337
366
|
await next()
|
|
@@ -895,6 +924,16 @@ namespace ResponseBody {
|
|
|
895
924
|
}
|
|
896
925
|
}
|
|
897
926
|
|
|
927
|
+
function getWaitUntil(c: { executionCtx?: { waitUntil(promise: Promise<unknown>): void } }) {
|
|
928
|
+
// `c.executionCtx` throws in runtimes without one, so cache persistence falls back to an awaited write.
|
|
929
|
+
try {
|
|
930
|
+
const context = c.executionCtx
|
|
931
|
+
return context ? context.waitUntil.bind(context) : undefined
|
|
932
|
+
} catch {
|
|
933
|
+
return undefined
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
898
937
|
namespace NodeServer {
|
|
899
938
|
type GetRequestListener = (typeof import('@hono/node-server'))['getRequestListener']
|
|
900
939
|
export type Fetch = Parameters<GetRequestListener>[0]
|
package/src/Client.test-d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Mpp as PublicMpp } from 'tapimo/mpp'
|
|
|
3
3
|
import type * as z from 'zod/mini'
|
|
4
4
|
|
|
5
5
|
import * as Activities from './apps/data/routes/activities.js'
|
|
6
|
+
import * as ApiKeys from './apps/management/routes/api-keys.js'
|
|
6
7
|
import * as Balances from './apps/data/routes/balances.js'
|
|
7
8
|
import * as Billing from './apps/management/routes/billing.js'
|
|
8
9
|
import * as Blocks from './apps/data/routes/blocks.js'
|
|
@@ -82,6 +83,50 @@ test('returns typed client', async () => {
|
|
|
82
83
|
})
|
|
83
84
|
|
|
84
85
|
describe('narrows responses by status', () => {
|
|
86
|
+
test('POST /v1/orgs/:orgId/projects/:projectId/api-keys/:keyId/rotate', async () => {
|
|
87
|
+
const client = Client.create()
|
|
88
|
+
const res = await client.v1.orgs[':orgId{org_[A-Za-z0-9_-]+}'].projects[
|
|
89
|
+
':projectId{prj_[A-Za-z0-9_-]+}'
|
|
90
|
+
]['api-keys'][':keyId{key_[A-Za-z0-9_-]+}'].rotate.$post({
|
|
91
|
+
param: { keyId: 'key_test', orgId: 'org_test', projectId: 'prj_test' },
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 400 | 401 | 403 | 404 | 502>()
|
|
95
|
+
|
|
96
|
+
if (res.status === 200) {
|
|
97
|
+
const json = await res.json()
|
|
98
|
+
expectTypeOf(json).toEqualTypeOf<z.output<typeof ApiKeys.schema.rotateApiKey.Response>>()
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('POST /v1/orgs/:orgId/api-keys/:keyId/rotate', async () => {
|
|
103
|
+
const client = Client.create()
|
|
104
|
+
const res = await client.v1.orgs[':orgId{org_[A-Za-z0-9_-]+}']['api-keys'][
|
|
105
|
+
':keyId{key_[A-Za-z0-9_-]+}'
|
|
106
|
+
].rotate.$post({ param: { keyId: 'key_test', orgId: 'org_test' } })
|
|
107
|
+
|
|
108
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 400 | 401 | 403 | 404 | 502>()
|
|
109
|
+
|
|
110
|
+
if (res.status === 200) {
|
|
111
|
+
const json = await res.json()
|
|
112
|
+
expectTypeOf(json).toEqualTypeOf<z.output<typeof ApiKeys.schema.rotateApiKey.Response>>()
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('DELETE /v1/orgs/:orgId/api-keys/:keyId', async () => {
|
|
117
|
+
const client = Client.create()
|
|
118
|
+
const res = await client.v1.orgs[':orgId{org_[A-Za-z0-9_-]+}']['api-keys'][
|
|
119
|
+
':keyId{key_[A-Za-z0-9_-]+}'
|
|
120
|
+
].$delete({ param: { keyId: 'key_test', orgId: 'org_test' } })
|
|
121
|
+
|
|
122
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 400 | 401 | 403 | 404 | 429 | 502>()
|
|
123
|
+
|
|
124
|
+
if (res.status === 200) {
|
|
125
|
+
const json = await res.json()
|
|
126
|
+
expectTypeOf(json).toEqualTypeOf<z.output<typeof ApiKeys.schema.revokeApiKey.Response>>()
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
85
130
|
test('POST /v1/mpp/validate', async () => {
|
|
86
131
|
const client = Client.create()
|
|
87
132
|
const res = await client.v1.mpp.validate.$post({ json: mppInput })
|
|
@@ -2563,6 +2608,7 @@ describe('narrows responses by status', () => {
|
|
|
2563
2608
|
amount: '1000000',
|
|
2564
2609
|
destinationToken: '0x20c000000000000000000000b9537d11c60e8b50',
|
|
2565
2610
|
mode: 'exactSource',
|
|
2611
|
+
provider: 'stargate',
|
|
2566
2612
|
recipient: `0x${'11'.repeat(20)}`,
|
|
2567
2613
|
sender: `0x${'22'.repeat(20)}`,
|
|
2568
2614
|
sourceChain: 'eip155:8453',
|
package/src/Scope.test.ts
CHANGED
|
@@ -52,3 +52,23 @@ describe('extend', () => {
|
|
|
52
52
|
).toThrowErrorMatchingInlineSnapshot(`[Error: Duplicate scope \`data:read\`.]`)
|
|
53
53
|
})
|
|
54
54
|
})
|
|
55
|
+
|
|
56
|
+
describe('expandAliases', () => {
|
|
57
|
+
test('adds the current scope for a legacy Zone read scope', () => {
|
|
58
|
+
expect(
|
|
59
|
+
Scope.expandAliases(['data:read', 'zone:1424310001:read', 'zone:1424310001:write']),
|
|
60
|
+
).toEqual([
|
|
61
|
+
'data:read',
|
|
62
|
+
'zone:1424310001:read',
|
|
63
|
+
'zone:1424310001:write',
|
|
64
|
+
'zone:1424310003:read',
|
|
65
|
+
])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('does not duplicate an existing current scope', () => {
|
|
69
|
+
expect(Scope.expandAliases(['zone:1424310001:read', 'zone:1424310003:read'])).toEqual([
|
|
70
|
+
'zone:1424310001:read',
|
|
71
|
+
'zone:1424310003:read',
|
|
72
|
+
])
|
|
73
|
+
})
|
|
74
|
+
})
|
package/src/Scope.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import * as z from 'zod/mini'
|
|
8
8
|
|
|
9
|
+
import * as Schema from './internal/Schema.js'
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Wildcard scope. A key whose `scopes` contains this value satisfies every
|
|
11
13
|
* required scope, so it never needs re-provisioning when new scopes are added.
|
|
@@ -154,6 +156,20 @@ export function includes(catalog: Catalog, scope: string): scope is Id {
|
|
|
154
156
|
return schema.Zone.safeParse(scope).success || catalog.some((entry) => entry.scope === scope)
|
|
155
157
|
}
|
|
156
158
|
|
|
159
|
+
/** Adds current read scopes granted by legacy Zone read scopes. */
|
|
160
|
+
export function expandAliases(scopes: readonly string[]): readonly string[] {
|
|
161
|
+
const aliases = new Set<string>()
|
|
162
|
+
for (const scope of scopes) {
|
|
163
|
+
const match = /^zone:([1-9]\d*):read$/.exec(scope)
|
|
164
|
+
if (!match) continue
|
|
165
|
+
const chainId = Number(match[1])
|
|
166
|
+
const resolved = Schema.resolveChainId(chainId)
|
|
167
|
+
if (resolved !== chainId) aliases.add(`zone:${resolved}:read`)
|
|
168
|
+
}
|
|
169
|
+
for (const scope of scopes) aliases.delete(scope)
|
|
170
|
+
return aliases.size ? [...scopes, ...aliases] : scopes
|
|
171
|
+
}
|
|
172
|
+
|
|
157
173
|
/** Returns the self-issuable identifiers in a scope catalog. */
|
|
158
174
|
export function selfServeFrom(catalog: Catalog): readonly Id[] {
|
|
159
175
|
return catalog
|
|
@@ -299,6 +299,81 @@ describe('DELETE /api-keys/:id', () => {
|
|
|
299
299
|
})
|
|
300
300
|
})
|
|
301
301
|
|
|
302
|
+
describe('POST /api-keys/:id/rotate', () => {
|
|
303
|
+
test('mints a replacement with the source access and attribution', async () => {
|
|
304
|
+
const { client, store } = await setup()
|
|
305
|
+
const { record: source, token: sourceToken } = await core_ApiKeys.mint(store, {
|
|
306
|
+
allowedIps: ['203.0.113.0/24'],
|
|
307
|
+
billingActive: true,
|
|
308
|
+
createdBy: 'customer@example.com',
|
|
309
|
+
environment: 'sandbox',
|
|
310
|
+
expiresAt: '2100-01-01T00:00:00.000Z',
|
|
311
|
+
name: 'Privy Zones',
|
|
312
|
+
orgId: 'org_privy',
|
|
313
|
+
projectId: 'prj_privy',
|
|
314
|
+
rateLimits: { '*': { limit: 10_000, period: 'minute' } },
|
|
315
|
+
scopes: ['zone:421700001:read', 'zone:421700001:write'],
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
const response = await client['api-keys'][':id'].rotate.$post({
|
|
319
|
+
param: { id: source.id },
|
|
320
|
+
})
|
|
321
|
+
const body = await TestApp.json(response, ApiKeys.schema.rotateApiKey.Response)
|
|
322
|
+
|
|
323
|
+
expect(response.status).toBe(200)
|
|
324
|
+
expect(body).toMatchObject({
|
|
325
|
+
allowedIps: source.allowedIps,
|
|
326
|
+
billingActive: source.billingActive,
|
|
327
|
+
createdBy: TestAdmin.identity.email,
|
|
328
|
+
environment: source.environment,
|
|
329
|
+
expiresAt: source.expiresAt,
|
|
330
|
+
name: source.name,
|
|
331
|
+
orgId: source.orgId,
|
|
332
|
+
projectId: source.projectId,
|
|
333
|
+
rateLimits: source.rateLimits,
|
|
334
|
+
scopes: source.scopes,
|
|
335
|
+
})
|
|
336
|
+
expect(body.id).not.toBe(source.id)
|
|
337
|
+
expect(body.token).not.toBe(sourceToken)
|
|
338
|
+
expect(await core_ApiKeys.resolve(store, sourceToken)).not.toBeNull()
|
|
339
|
+
expect(await core_ApiKeys.resolve(store, body.token)).toMatchObject({
|
|
340
|
+
id: body.id,
|
|
341
|
+
scopes: source.scopes,
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
test('unknown id → 404', async () => {
|
|
346
|
+
const { client } = await setup()
|
|
347
|
+
const response = await client['api-keys'][':id'].rotate.$post({
|
|
348
|
+
param: { id: 'key_missing' },
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
expect(response.status).toMatchInlineSnapshot(`404`)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
test('keeps a rotated one-off key assignable to an organization', async () => {
|
|
355
|
+
const { client, db, store } = await setup()
|
|
356
|
+
const organization = await Organizations.create(db, { name: 'Tempo' })
|
|
357
|
+
const { record: source } = await core_ApiKeys.mint(store, {
|
|
358
|
+
name: 'One-off',
|
|
359
|
+
scopes: ['data:read'],
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
const rotateResponse = await client['api-keys'][':id'].rotate.$post({
|
|
363
|
+
param: { id: source.id },
|
|
364
|
+
})
|
|
365
|
+
const replacement = await TestApp.json(rotateResponse, ApiKeys.schema.rotateApiKey.Response)
|
|
366
|
+
expect(replacement.orgId).toBe(replacement.id)
|
|
367
|
+
|
|
368
|
+
const updateResponse = await client['api-keys'][':id'].$patch({
|
|
369
|
+
json: { orgId: organization.id },
|
|
370
|
+
param: { id: replacement.id },
|
|
371
|
+
})
|
|
372
|
+
expect(updateResponse.status).toBe(200)
|
|
373
|
+
expect((await core_ApiKeys.get(store, replacement.id))?.orgId).toBe(organization.id)
|
|
374
|
+
})
|
|
375
|
+
})
|
|
376
|
+
|
|
302
377
|
describe('PATCH /api-keys/:id', () => {
|
|
303
378
|
test('preserves the allowlist when omitted', async () => {
|
|
304
379
|
const { client, store } = TestAdmin.setup()
|
|
@@ -173,6 +173,24 @@ export namespace schema {
|
|
|
173
173
|
.object({ id: z.string().check(z.describe('Revoked key id.')) })
|
|
174
174
|
.check(z.describe('API-key revocation result.'))
|
|
175
175
|
}
|
|
176
|
+
|
|
177
|
+
/** Schemas for the rotateApiKey operation. */
|
|
178
|
+
export namespace rotateApiKey {
|
|
179
|
+
/** Path parameters for rotating a key. */
|
|
180
|
+
export const Params = z
|
|
181
|
+
.object({
|
|
182
|
+
id: z
|
|
183
|
+
.string()
|
|
184
|
+
.check(
|
|
185
|
+
z.describe('Stable id of the key to rotate.'),
|
|
186
|
+
z.meta({ examples: ['key_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
|
|
187
|
+
),
|
|
188
|
+
})
|
|
189
|
+
.check(z.describe('Path parameters for rotating an API key.'))
|
|
190
|
+
|
|
191
|
+
/** Response: the replacement record plus its one-time plaintext token. */
|
|
192
|
+
export const Response = createApiKey.Response
|
|
193
|
+
}
|
|
176
194
|
}
|
|
177
195
|
|
|
178
196
|
/**
|
|
@@ -183,6 +201,7 @@ export namespace schema {
|
|
|
183
201
|
* - `GET /` lists key records (metadata only), optionally filtered by `orgId`.
|
|
184
202
|
* - `DELETE /:id` revokes a key by id (`404` if absent).
|
|
185
203
|
* - `PATCH /:id` updates a key's allowlist, name, or project, or assigns its first organization.
|
|
204
|
+
* - `POST /:id/rotate` mints a replacement with the source key's access and attribution.
|
|
186
205
|
* - `POST /backfill` rebuilds the id and org index entries for every record.
|
|
187
206
|
*/
|
|
188
207
|
export function apiKeys() {
|
|
@@ -315,6 +334,51 @@ export function apiKeys() {
|
|
|
315
334
|
return c.json(Response.validated(schema.revokeApiKey.Response, { id }))
|
|
316
335
|
},
|
|
317
336
|
)
|
|
337
|
+
.post(
|
|
338
|
+
'/:id/rotate',
|
|
339
|
+
OpenApi.validate('param', schema.rotateApiKey.Params, {
|
|
340
|
+
code: 'param_invalid',
|
|
341
|
+
message: 'Invalid path parameters',
|
|
342
|
+
}),
|
|
343
|
+
OpenApi.describeRoute({
|
|
344
|
+
description: 'Create a replacement that preserves the source key access and attribution. The source key remains active.', // prettier-ignore
|
|
345
|
+
operationId: 'rotateAdminApiKey',
|
|
346
|
+
responses: OpenApi.responses({
|
|
347
|
+
errors: { 404: 'API key not found.' },
|
|
348
|
+
success: {
|
|
349
|
+
description: 'Replacement key (includes the one-time token).',
|
|
350
|
+
schema: schema.rotateApiKey.Response,
|
|
351
|
+
},
|
|
352
|
+
}),
|
|
353
|
+
summary: 'Rotate API key',
|
|
354
|
+
tags: ['API keys'],
|
|
355
|
+
}),
|
|
356
|
+
async (c) => {
|
|
357
|
+
const { id } = c.req.valid('param')
|
|
358
|
+
const store = c.get('kv').store
|
|
359
|
+
const source = await ApiKeys.get(store, id)
|
|
360
|
+
if (!source)
|
|
361
|
+
return Response.error(c, {
|
|
362
|
+
code: 'not_found',
|
|
363
|
+
message: 'API key not found.',
|
|
364
|
+
status: 404,
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
const { record, token } = await ApiKeys.mint(store, {
|
|
368
|
+
allowedIps: source.allowedIps,
|
|
369
|
+
createdBy: c.get('identity')!.email,
|
|
370
|
+
environment: source.environment,
|
|
371
|
+
scopes: source.scopes,
|
|
372
|
+
...(source.billingActive === undefined ? {} : { billingActive: source.billingActive }),
|
|
373
|
+
...(source.expiresAt === undefined ? {} : { expiresAt: source.expiresAt }),
|
|
374
|
+
...(source.name === undefined ? {} : { name: source.name }),
|
|
375
|
+
...(source.orgId === source.id ? {} : { orgId: source.orgId }),
|
|
376
|
+
...(source.projectId === undefined ? {} : { projectId: source.projectId }),
|
|
377
|
+
...(source.rateLimits === undefined ? {} : { rateLimits: source.rateLimits }),
|
|
378
|
+
})
|
|
379
|
+
return c.json(Response.validated(schema.rotateApiKey.Response, { ...record, token }), 200)
|
|
380
|
+
},
|
|
381
|
+
)
|
|
318
382
|
.patch(
|
|
319
383
|
'/:id',
|
|
320
384
|
OpenApi.validate('param', schema.updateApiKey.Params, {
|
|
@@ -204,7 +204,7 @@ describe('putVerifiedEarnVault', () => {
|
|
|
204
204
|
expect(await response.json()).toMatchObject({
|
|
205
205
|
error: {
|
|
206
206
|
code: 'body_invalid',
|
|
207
|
-
details: [{ message: 'Zone
|
|
207
|
+
details: [{ message: 'Zone 1424310003 is configured more than once.' }],
|
|
208
208
|
},
|
|
209
209
|
})
|
|
210
210
|
},
|
|
@@ -94,7 +94,7 @@ export namespace schema {
|
|
|
94
94
|
.check(z.describe('Custom vault slug, generated from the label when omitted.')),
|
|
95
95
|
vaultAddress: Schema.Address.check(
|
|
96
96
|
z.describe('Earn vault contract address.'),
|
|
97
|
-
z.meta({ examples: ['
|
|
97
|
+
z.meta({ examples: ['0x140a09b7c5826d34dfa38ea7bfde1c7e478c4408'] }),
|
|
98
98
|
),
|
|
99
99
|
zones: z
|
|
100
100
|
.optional(z.array(EarnVaults.schema.ZoneRoute))
|
|
@@ -93,6 +93,25 @@ export namespace update {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** Rotates an API key while preserving its access and attribution. */
|
|
97
|
+
export namespace rotate {
|
|
98
|
+
export async function fn(id: string) {
|
|
99
|
+
const response = await client['api-keys'][':id'].rotate.$post({ param: { id } })
|
|
100
|
+
if (!response.ok)
|
|
101
|
+
throw new Error(
|
|
102
|
+
await Response.errorMessage(response, { fallback: 'Failed to rotate the API key.' }),
|
|
103
|
+
)
|
|
104
|
+
return response.json()
|
|
105
|
+
}
|
|
106
|
+
export function useMutation() {
|
|
107
|
+
const queryClient = ReactQuery.useQueryClient()
|
|
108
|
+
return ReactQuery.useMutation({
|
|
109
|
+
mutationFn: fn,
|
|
110
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: get.baseKey }),
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
96
115
|
/** Revoke an API key by id. */
|
|
97
116
|
export namespace revoke {
|
|
98
117
|
export async function fn(id: string) {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Copy as CopyIcon,
|
|
9
9
|
Pencil,
|
|
10
10
|
Plus,
|
|
11
|
+
RefreshCw,
|
|
11
12
|
Search,
|
|
12
13
|
Trash2,
|
|
13
14
|
TriangleAlert,
|
|
@@ -207,13 +208,13 @@ function RouteComponent() {
|
|
|
207
208
|
)
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
function TokenBanner(props: { onDismiss: () => void; token: string }) {
|
|
211
|
+
function TokenBanner(props: { onDismiss: () => void; title?: string; token: string }) {
|
|
211
212
|
const { copy, notifying } = useCopy()
|
|
212
213
|
return (
|
|
213
214
|
<Card className="flex flex-col gap-3 p-4">
|
|
214
215
|
<div className="flex items-start justify-between gap-3">
|
|
215
216
|
<div className="flex flex-col gap-1">
|
|
216
|
-
<span className="text-label-14 text-foreground">Key provisioned</span>
|
|
217
|
+
<span className="text-label-14 text-foreground">{props.title ?? 'Key provisioned'}</span>
|
|
217
218
|
<span className="text-label-12 text-foreground-secondary">
|
|
218
219
|
Copy this token now, it won't be shown again.
|
|
219
220
|
</span>
|
|
@@ -732,6 +733,7 @@ function KeyRow(props: { currentUser?: string | undefined; record: ApiKeys.ApiKe
|
|
|
732
733
|
</dl>
|
|
733
734
|
<div className="flex justify-end gap-2 border-t border-border px-5 py-3">
|
|
734
735
|
<EditDialog record={record} />
|
|
736
|
+
<RotateDialog record={record} />
|
|
735
737
|
<RevokeDialog currentUser={currentUser} record={record} />
|
|
736
738
|
</div>
|
|
737
739
|
</Rows.Panel>
|
|
@@ -812,6 +814,91 @@ declare namespace EditDialog {
|
|
|
812
814
|
}
|
|
813
815
|
}
|
|
814
816
|
|
|
817
|
+
function RotateDialog(options: RotateDialog.Props) {
|
|
818
|
+
const { record } = options
|
|
819
|
+
const [open, setOpen] = useState(false)
|
|
820
|
+
const rotate = ApiKeys.rotate.useMutation()
|
|
821
|
+
|
|
822
|
+
return (
|
|
823
|
+
<Dialog.Root
|
|
824
|
+
onOpenChange={(value) => {
|
|
825
|
+
if (!value && rotate.isPending) return
|
|
826
|
+
setOpen(value)
|
|
827
|
+
if (!value) rotate.reset()
|
|
828
|
+
}}
|
|
829
|
+
open={open}
|
|
830
|
+
>
|
|
831
|
+
<Dialog.Trigger
|
|
832
|
+
render={
|
|
833
|
+
<Button size="small">
|
|
834
|
+
<RefreshCw className="size-4" />
|
|
835
|
+
Rotate key
|
|
836
|
+
</Button>
|
|
837
|
+
}
|
|
838
|
+
/>
|
|
839
|
+
<Dialog.Popup>
|
|
840
|
+
{rotate.data ? (
|
|
841
|
+
<>
|
|
842
|
+
<Dialog.Title>Key rotated</Dialog.Title>
|
|
843
|
+
<Dialog.Description>
|
|
844
|
+
Copy the replacement token now. The source key remains active until it is revoked.
|
|
845
|
+
</Dialog.Description>
|
|
846
|
+
<div className="mt-5">
|
|
847
|
+
<TokenBanner
|
|
848
|
+
onDismiss={() => setOpen(false)}
|
|
849
|
+
title="Replacement key provisioned"
|
|
850
|
+
token={rotate.data.token}
|
|
851
|
+
/>
|
|
852
|
+
</div>
|
|
853
|
+
</>
|
|
854
|
+
) : (
|
|
855
|
+
<>
|
|
856
|
+
<Dialog.Title>Rotate {record.name ?? 'API key'}?</Dialog.Title>
|
|
857
|
+
<Dialog.Description>
|
|
858
|
+
This creates a replacement with the same scopes, ownership, limits, and restrictions.
|
|
859
|
+
The source key remains active.
|
|
860
|
+
</Dialog.Description>
|
|
861
|
+
<dl className="mt-5 grid grid-cols-2 gap-4 border border-border bg-secondary p-4">
|
|
862
|
+
<Detail className="col-span-2" label="Key ID">
|
|
863
|
+
<code className="break-all">{record.id}</code>
|
|
864
|
+
</Detail>
|
|
865
|
+
<Detail label="Environment">{record.environment}</Detail>
|
|
866
|
+
<Detail label="Organization">{organization(record)}</Detail>
|
|
867
|
+
<Detail label="Project">{project(record)}</Detail>
|
|
868
|
+
<Detail label="Scopes">{record.scopes.length}</Detail>
|
|
869
|
+
</dl>
|
|
870
|
+
{rotate.error ? (
|
|
871
|
+
<p className="mt-3 text-copy-13 text-negative">{rotate.error.message}</p>
|
|
872
|
+
) : null}
|
|
873
|
+
<div className="mt-6 flex justify-end gap-2">
|
|
874
|
+
<Dialog.Close
|
|
875
|
+
render={
|
|
876
|
+
<Button disabled={rotate.isPending} type="button">
|
|
877
|
+
Cancel
|
|
878
|
+
</Button>
|
|
879
|
+
}
|
|
880
|
+
/>
|
|
881
|
+
<Button
|
|
882
|
+
loading={rotate.isPending}
|
|
883
|
+
onClick={() => rotate.mutate(record.id)}
|
|
884
|
+
variant="primary"
|
|
885
|
+
>
|
|
886
|
+
Rotate key
|
|
887
|
+
</Button>
|
|
888
|
+
</div>
|
|
889
|
+
</>
|
|
890
|
+
)}
|
|
891
|
+
</Dialog.Popup>
|
|
892
|
+
</Dialog.Root>
|
|
893
|
+
)
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
declare namespace RotateDialog {
|
|
897
|
+
type Props = {
|
|
898
|
+
record: ApiKeys.ApiKey
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
815
902
|
function RevokeDialog(props: { currentUser?: string | undefined; record: ApiKeys.ApiKey }) {
|
|
816
903
|
const { currentUser, record } = props
|
|
817
904
|
const [confirmation, setConfirmation] = useState('')
|