tapimo 0.10.0 → 0.10.1
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/ApiKey.d.ts +2 -1
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKeys.d.ts +4 -2
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +26 -1
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +3346 -2575
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +1 -0
- package/dist/App.js.map +1 -1
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +5 -5
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +91 -84
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +26 -17
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +6 -0
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +4 -2
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +69 -69
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +26 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/apps/Zones.d.ts +4266 -82
- package/dist/apps/Zones.d.ts.map +1 -1
- package/dist/apps/Zones.js +27 -8
- package/dist/apps/Zones.js.map +1 -1
- package/dist/apps/data/App.d.ts +3467 -2696
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +2999 -100
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +340 -2
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +850 -336
- package/dist/apps/data/routes/addresses.d.ts.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 +11 -3
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +4 -4
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/earn.d.ts +75 -75
- package/dist/apps/data/routes/earn.js +2 -2
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +120 -120
- package/dist/apps/data/routes/fee-amm.d.ts +24 -24
- package/dist/apps/data/routes/indexer.d.ts +35 -34
- package/dist/apps/data/routes/indexer.d.ts.map +1 -1
- package/dist/apps/data/routes/indexer.js +3 -2
- package/dist/apps/data/routes/indexer.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +5 -1
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +12 -12
- package/dist/apps/data/routes/transactions.d.ts +281 -281
- package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
- package/dist/apps/data/routes/webhooks.d.ts +418 -364
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +161 -14
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/data/routes/zones.d.ts +31 -31
- package/dist/apps/data/routes/zones.js +1 -1
- package/dist/apps/data/routes/zones.js.map +1 -1
- package/dist/apps/funding/App.d.ts +96 -96
- package/dist/apps/funding/routes/deposit-addresses.d.ts +52 -52
- package/dist/apps/funding/routes/deposit-addresses.js +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/providers.d.ts +13 -13
- package/dist/apps/funding/routes/transfers.d.ts +32 -32
- package/dist/apps/funding/routes/transfers.js +1 -1
- package/dist/apps/funding/routes/transfers.js.map +1 -1
- package/dist/apps/management/App.d.ts +418 -418
- package/dist/apps/management/routes/api-keys.d.ts +50 -50
- package/dist/apps/management/routes/billing.d.ts +72 -72
- package/dist/apps/management/routes/billing.js +1 -1
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +37 -37
- package/dist/apps/management/routes/faucet.js +2 -2
- package/dist/apps/management/routes/faucet.js.map +1 -1
- package/dist/apps/management/routes/invite-links.d.ts +42 -42
- package/dist/apps/management/routes/me.d.ts +6 -6
- package/dist/apps/management/routes/members.d.ts +75 -75
- package/dist/apps/management/routes/projects.d.ts +40 -40
- package/dist/apps/management/routes/usage.d.ts +76 -76
- package/dist/apps/management/routes/webhooks.d.ts +49 -49
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +4 -1
- package/dist/apps/mcp.js.map +1 -1
- package/dist/db/Db.d.ts +2 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +58 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +19 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +24 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
- package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
- package/dist/db/tables/webhookQueueEvents.js +160 -0
- package/dist/db/tables/webhookQueueEvents.js.map +1 -0
- package/dist/db/tables/webhookSubscriptions.d.ts +2 -0
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +10 -0
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/Auth.d.ts +3 -0
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +6 -4
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/MetricSink.d.ts +27 -0
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +29 -2
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/RateLimit.d.ts +4 -6
- package/dist/internal/RateLimit.d.ts.map +1 -1
- package/dist/internal/RateLimit.js +15 -14
- package/dist/internal/RateLimit.js.map +1 -1
- package/dist/internal/Store.d.ts +2 -1
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +57 -18
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Value.d.ts +2 -0
- package/dist/internal/Value.d.ts.map +1 -1
- package/dist/internal/Value.js +25 -0
- package/dist/internal/Value.js.map +1 -1
- package/dist/internal/WebhookTransfer.d.ts +20 -0
- package/dist/internal/WebhookTransfer.d.ts.map +1 -1
- package/dist/internal/WebhookTransfer.js +7 -4
- package/dist/internal/WebhookTransfer.js.map +1 -1
- package/dist/internal/Webhooks.d.ts +139 -0
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +129 -7
- package/dist/internal/Webhooks.js.map +1 -1
- package/dist/internal/funding/providers/relay.d.ts +3 -4
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +59 -8
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/package.json +3 -3
- package/src/ApiKeys.test.ts +20 -0
- package/src/ApiKeys.ts +28 -1
- package/src/App.test.ts +10 -8
- package/src/App.ts +1 -0
- package/src/Client.test-d.ts +1 -0
- package/src/Webhooks.ts +19 -17
- package/src/admin/apps/api-keys.test.ts +9 -0
- package/src/admin/apps/api-keys.ts +15 -0
- package/src/admin/ui/src/lib/query-client.ts +3 -2
- package/src/admin/ui/src/routes/api-keys.tsx +31 -9
- package/src/admin/ui.generated.ts +69 -69
- package/src/analytics/Analytics.test.ts +8 -0
- package/src/analytics/Analytics.ts +26 -0
- package/src/apps/Zones.test.ts +20 -3
- package/src/apps/Zones.ts +28 -8
- package/src/apps/composition.test.ts +1 -0
- package/src/apps/data/App.test.ts +2 -2
- package/src/apps/data/routes/activities.bench.ts +41 -0
- package/src/apps/data/routes/activities.test.ts +103 -2
- package/src/apps/data/routes/activities.ts +401 -2
- package/src/apps/data/routes/balances.bench.ts +21 -0
- package/src/apps/data/routes/balances.ts +11 -3
- package/src/apps/data/routes/blocks.bench.ts +34 -0
- package/src/apps/data/routes/coingecko.bench.ts +53 -0
- package/src/apps/data/routes/earn.bench.ts +71 -0
- package/src/apps/data/routes/earn.ts +2 -2
- package/src/apps/data/routes/exchanges.bench.ts +85 -0
- package/src/apps/data/routes/fee-amm.bench.ts +13 -0
- package/src/apps/data/routes/indexer.bench.ts +8 -0
- package/src/apps/data/routes/indexer.test.ts +2 -1
- package/src/apps/data/routes/indexer.ts +3 -2
- package/src/apps/data/routes/receipts.bench.ts +39 -0
- package/src/apps/data/routes/rpc.bench.ts +14 -0
- package/src/apps/data/routes/rpc.test.ts +27 -0
- package/src/apps/data/routes/rpc.ts +5 -1
- package/src/apps/data/routes/tokenlist.bench.ts +3 -0
- package/src/apps/data/routes/tokens.bench.ts +76 -0
- package/src/apps/data/routes/transactions.bench.ts +40 -0
- package/src/apps/data/routes/transfers.bench.ts +28 -0
- package/src/apps/data/routes/valuation.bench.ts +14 -0
- package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
- package/src/apps/data/routes/webhooks.bench.ts +26 -0
- package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
- package/src/apps/data/routes/webhooks.test.ts +53 -6
- package/src/apps/data/routes/webhooks.ts +221 -14
- package/src/apps/data/routes/zones.bench.ts +32 -0
- package/src/apps/data/routes/zones.test.ts +1 -1
- package/src/apps/data/routes/zones.ts +1 -1
- package/src/apps/funding/routes/chains.test.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.test.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.ts +1 -1
- package/src/apps/funding/routes/providers.test.ts +1 -1
- package/src/apps/funding/routes/quotes.test.ts +1 -1
- package/src/apps/funding/routes/transfers.test.ts +3 -6
- package/src/apps/funding/routes/transfers.ts +1 -1
- package/src/apps/management/routes/billing.ts +1 -1
- package/src/apps/management/routes/faucet.ts +2 -2
- package/src/apps/management/routes/me.test.ts +31 -0
- package/src/apps/mcp.ts +4 -1
- package/src/db/Db.ts +81 -0
- package/src/db/Schema.ts +32 -0
- package/src/db/tables/webhookQueueEvents.ts +285 -0
- package/src/db/tables/webhookSubscriptions.ts +15 -0
- package/src/internal/Auth.test.ts +61 -50
- package/src/internal/Auth.ts +9 -4
- package/src/internal/EarnVaults.test.ts +2 -2
- package/src/internal/MetricSink.test.ts +58 -1
- package/src/internal/MetricSink.ts +60 -2
- package/src/internal/RateLimit.test.ts +23 -18
- package/src/internal/RateLimit.ts +23 -15
- package/src/internal/Store.test.ts +99 -13
- package/src/internal/Store.ts +52 -17
- package/src/internal/Tidx.test.ts +4 -4
- package/src/internal/Value.test.ts +25 -0
- package/src/internal/Value.ts +29 -0
- package/src/internal/WebhookTransfer.test.ts +89 -0
- package/src/internal/WebhookTransfer.ts +11 -7
- package/src/internal/Webhooks.test.ts +479 -0
- package/src/internal/Webhooks.ts +281 -7
- package/src/internal/funding/Provider.test.ts +1 -1
- package/src/internal/funding/providers/index.test.ts +73 -3
- package/src/internal/funding/providers/relay.test.ts +1 -5
- package/src/internal/funding/providers/relay.ts +56 -12
package/src/ApiKeys.ts
CHANGED
|
@@ -491,7 +491,7 @@ async function read(
|
|
|
491
491
|
}
|
|
492
492
|
})()
|
|
493
493
|
if (value === undefined) return null
|
|
494
|
-
const parsed = schema.Record.safeParse(value)
|
|
494
|
+
const parsed = schema.Record.safeParse(StoredRecord.normalizeRateLimits(value))
|
|
495
495
|
if (!parsed.success) return null
|
|
496
496
|
const record = parsed.data
|
|
497
497
|
if (!validScopes(record.scopes, scopeCatalog)) return null
|
|
@@ -499,6 +499,33 @@ async function read(
|
|
|
499
499
|
return record
|
|
500
500
|
}
|
|
501
501
|
|
|
502
|
+
namespace StoredRecord {
|
|
503
|
+
type UnknownRecord = { [key: string]: unknown }
|
|
504
|
+
|
|
505
|
+
/** Normalizes persisted pre-period quotas without accepting legacy fields at the write boundary. */
|
|
506
|
+
export function normalizeRateLimits(value: unknown): unknown {
|
|
507
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return value
|
|
508
|
+
const record = value as UnknownRecord
|
|
509
|
+
const rateLimits = record['rateLimits']
|
|
510
|
+
if (!rateLimits || typeof rateLimits !== 'object' || Array.isArray(rateLimits)) return value
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
...record,
|
|
514
|
+
rateLimits: Object.fromEntries(
|
|
515
|
+
Object.entries(rateLimits).map(([scope, rateLimit]) => {
|
|
516
|
+
if (!rateLimit || typeof rateLimit !== 'object' || Array.isArray(rateLimit))
|
|
517
|
+
return [scope, rateLimit]
|
|
518
|
+
const perMinute = (rateLimit as UnknownRecord)['perMinute']
|
|
519
|
+
return [
|
|
520
|
+
scope,
|
|
521
|
+
typeof perMinute === 'number' ? { limit: perMinute, period: 'minute' } : rateLimit,
|
|
522
|
+
]
|
|
523
|
+
}),
|
|
524
|
+
),
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
502
529
|
/** Returns whether every granted scope belongs to the configured catalog. */
|
|
503
530
|
function validScopes(scopes: readonly string[], scopeCatalog: Scope.Catalog): boolean {
|
|
504
531
|
return scopes.every((scope) => scope === Scope.wildcard || Scope.includes(scopeCatalog, scope))
|
package/src/App.test.ts
CHANGED
|
@@ -246,7 +246,7 @@ describe('create', () => {
|
|
|
246
246
|
|
|
247
247
|
test('applies the configured global MPP rate limit', async () => {
|
|
248
248
|
const store = Store.memory()
|
|
249
|
-
const limit = {
|
|
249
|
+
const limit = { limit: 2, period: 'minute' } satisfies RateLimit.Limit
|
|
250
250
|
const app = TestApp.create({
|
|
251
251
|
auth: { mpp: { rateLimit: limit } },
|
|
252
252
|
rateLimit: { store },
|
|
@@ -267,7 +267,7 @@ describe('create', () => {
|
|
|
267
267
|
|
|
268
268
|
test('rejects an unsupported MPP payment chain without throwing', async () => {
|
|
269
269
|
const store = Store.memory()
|
|
270
|
-
const limit = {
|
|
270
|
+
const limit = { limit: 1, period: 'minute' } satisfies RateLimit.Limit
|
|
271
271
|
const app = TestApp.create({
|
|
272
272
|
auth: {
|
|
273
273
|
mpp: {
|
|
@@ -299,10 +299,10 @@ describe('create', () => {
|
|
|
299
299
|
|
|
300
300
|
test('endpoint MPP rate limit overrides the global limit', async () => {
|
|
301
301
|
const store = Store.memory()
|
|
302
|
-
const routeLimit = {
|
|
302
|
+
const routeLimit = { limit: 1, period: 'minute' } satisfies RateLimit.Limit
|
|
303
303
|
const app = TestApp.create({
|
|
304
304
|
auth: {
|
|
305
|
-
mpp: { rateLimit: {
|
|
305
|
+
mpp: { rateLimit: { limit: 2, period: 'minute' } },
|
|
306
306
|
overrides: {
|
|
307
307
|
'GET /v1/tokens/:token': { mpp: { rateLimit: routeLimit } },
|
|
308
308
|
},
|
|
@@ -687,7 +687,8 @@ describe('create', () => {
|
|
|
687
687
|
const rateLimitStore = Store.memory()
|
|
688
688
|
const app = TestApp.create({ rateLimit: { store: rateLimitStore } })
|
|
689
689
|
await exhaustQuota(RateLimit.memory({ store: rateLimitStore }), 'public:anonymous', {
|
|
690
|
-
|
|
690
|
+
limit: 60,
|
|
691
|
+
period: 'minute',
|
|
691
692
|
})
|
|
692
693
|
|
|
693
694
|
const response = await app.request(`/v1/tokens/${TestApp.token}`)
|
|
@@ -699,7 +700,7 @@ describe('create', () => {
|
|
|
699
700
|
})
|
|
700
701
|
|
|
701
702
|
test('returns payment challenge when API-key quota is exhausted', async () => {
|
|
702
|
-
const limit = {
|
|
703
|
+
const limit = { limit: 1, period: 'minute' } satisfies RateLimit.Limit
|
|
703
704
|
const key = { ...TestApp.key, rateLimits: { 'data:read': limit } }
|
|
704
705
|
const rateLimitStore = Store.memory()
|
|
705
706
|
const app = TestApp.create({
|
|
@@ -811,7 +812,8 @@ describe('create', () => {
|
|
|
811
812
|
expect(authorized.status).toMatchInlineSnapshot(`400`)
|
|
812
813
|
|
|
813
814
|
await exhaustQuota(RateLimit.memory({ now, store: rateLimitStore }), 'public:anonymous', {
|
|
814
|
-
|
|
815
|
+
limit: 60,
|
|
816
|
+
period: 'minute',
|
|
815
817
|
})
|
|
816
818
|
const overQuota = await app.request('/v1/tokens/not-an-address')
|
|
817
819
|
expect(overQuota.status).toMatchInlineSnapshot(`402`)
|
|
@@ -902,5 +904,5 @@ describe('chain id validation', () => {
|
|
|
902
904
|
})
|
|
903
905
|
|
|
904
906
|
async function exhaustQuota(rateLimit: RateLimit.Store, key: string, limit: RateLimit.Limit) {
|
|
905
|
-
for (let index = 0; index < limit.
|
|
907
|
+
for (let index = 0; index < limit.limit; index++) await rateLimit.consume({ key, limit })
|
|
906
908
|
}
|
package/src/App.ts
CHANGED
package/src/Client.test-d.ts
CHANGED
|
@@ -3102,6 +3102,7 @@ describe('narrows responses by status', () => {
|
|
|
3102
3102
|
expectTypeOf(json.row_count).toEqualTypeOf<
|
|
3103
3103
|
z.output<typeof Indexer.schema.Response>['row_count']
|
|
3104
3104
|
>()
|
|
3105
|
+
expectTypeOf(json.engine).toEqualTypeOf<'postgres' | 'clickhouse' | 'tiered' | undefined>()
|
|
3105
3106
|
void json
|
|
3106
3107
|
return
|
|
3107
3108
|
}
|
package/src/Webhooks.ts
CHANGED
|
@@ -74,23 +74,25 @@ export function createPoller(options: createPoller.Options): createPoller.Poller
|
|
|
74
74
|
return result
|
|
75
75
|
}
|
|
76
76
|
: inner
|
|
77
|
-
const dispatchBatch =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
77
|
+
const dispatchBatch = innerBatch
|
|
78
|
+
? async (
|
|
79
|
+
dispatchables: readonly AppWebhooks.poll.Dispatchable[],
|
|
80
|
+
context: AppWebhooks.poll.DispatchContext,
|
|
81
|
+
) => {
|
|
82
|
+
const start = Date.now()
|
|
83
|
+
const result = await innerBatch(dispatchables, context)
|
|
84
|
+
const durationMs = Date.now() - start
|
|
85
|
+
for (const dispatchable of dispatchables)
|
|
86
|
+
metrics?.record({
|
|
87
|
+
chainId: dispatchable.envelope.chainId,
|
|
88
|
+
durationMs,
|
|
89
|
+
eventType: dispatchable.envelope.type,
|
|
90
|
+
ok: result.ok,
|
|
91
|
+
type: 'webhook:enqueue',
|
|
92
|
+
})
|
|
93
|
+
return result
|
|
94
|
+
}
|
|
95
|
+
: undefined
|
|
94
96
|
|
|
95
97
|
// Resolve per tick so Workers factories build a fresh Hyperdrive-pooled
|
|
96
98
|
// connection each run.
|
|
@@ -105,6 +105,15 @@ describe('POST /api-keys', () => {
|
|
|
105
105
|
expect((await core_ApiKeys.list(store))[0]?.expiresAt).toBe(expiresAt)
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
+
test('rejects an expiry without the minimum storage lead time', async () => {
|
|
109
|
+
const { client } = await setup()
|
|
110
|
+
const response = await client['api-keys'].$post({
|
|
111
|
+
json: { ...validBody, expiresAt: new Date(Date.now() + 60_000).toISOString() },
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
expect(response.status).toBe(400)
|
|
115
|
+
})
|
|
116
|
+
|
|
108
117
|
test('rejects an unknown scope with 400', async () => {
|
|
109
118
|
const { client } = await setup()
|
|
110
119
|
const response = await client['api-keys'].$post({
|
|
@@ -15,6 +15,9 @@ import type * as App from '../App.js'
|
|
|
15
15
|
|
|
16
16
|
/** Zod schemas owned by the admin API-keys handler. */
|
|
17
17
|
export namespace schema {
|
|
18
|
+
// Leave time for validation and database lookups before KV writes its minimum 60-second TTL.
|
|
19
|
+
const minimumExpiryLeadMs = 2 * 60_000
|
|
20
|
+
|
|
18
21
|
const Name = z
|
|
19
22
|
.pipe(
|
|
20
23
|
z.string(),
|
|
@@ -27,6 +30,17 @@ export namespace schema {
|
|
|
27
30
|
z.meta({ examples: ['Explorer testnet'] }),
|
|
28
31
|
)
|
|
29
32
|
|
|
33
|
+
const ExpiresAt = z.optional(z.iso.datetime()).check(
|
|
34
|
+
z.refine(
|
|
35
|
+
(value) => value === undefined || Date.parse(value) >= Date.now() + minimumExpiryLeadMs,
|
|
36
|
+
{
|
|
37
|
+
error: 'Expiry must be at least two minutes in the future.',
|
|
38
|
+
},
|
|
39
|
+
),
|
|
40
|
+
z.describe('ISO 8601 expiry timestamp. Omit for a non-expiring key.'),
|
|
41
|
+
z.meta({ examples: ['2100-01-01T00:00:00.000Z'] }),
|
|
42
|
+
)
|
|
43
|
+
|
|
30
44
|
const ApiKey = Schema.describe(
|
|
31
45
|
z.extend(ApiKeys.schema.Record, {
|
|
32
46
|
createdByLabel: z
|
|
@@ -59,6 +73,7 @@ export namespace schema {
|
|
|
59
73
|
*/
|
|
60
74
|
export const Body = Schema.describe(
|
|
61
75
|
z.extend(z.omit(ApiKeys.schema.MintInput, { createdBy: true, name: true }), {
|
|
76
|
+
expiresAt: ExpiresAt,
|
|
62
77
|
name: Name,
|
|
63
78
|
scopes: z
|
|
64
79
|
.readonly(
|
|
@@ -17,12 +17,13 @@ const persister = createAsyncStoragePersister({
|
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
export const persistOptions: Omit<PersistQueryClientOptions, 'queryClient'> = {
|
|
20
|
-
|
|
21
|
-
persister,
|
|
20
|
+
buster: 'rate-limit-periods',
|
|
22
21
|
dehydrateOptions: {
|
|
23
22
|
shouldDehydrateQuery: (query) =>
|
|
24
23
|
query.queryKey[0] !== 'config' &&
|
|
25
24
|
query.queryKey[0] !== 'organizations' &&
|
|
26
25
|
defaultShouldDehydrateQuery(query),
|
|
27
26
|
},
|
|
27
|
+
maxAge: 1000 * 60 * 60 * 24,
|
|
28
|
+
persister,
|
|
28
29
|
}
|
|
@@ -59,9 +59,11 @@ function RouteComponent() {
|
|
|
59
59
|
alert('Enter a name that identifies what uses this key.')
|
|
60
60
|
return
|
|
61
61
|
}
|
|
62
|
+
const expiresAt_value = field('expiresAt')
|
|
63
|
+
const expiresAt = expiresAt_value ? new Date(expiresAt_value).toISOString() : undefined
|
|
62
64
|
const orgId = filters.orgId ?? field('orgId')
|
|
63
65
|
const rateLimit = field('rateLimit')
|
|
64
|
-
const
|
|
66
|
+
const limit = rateLimit ? Number(rateLimit) : undefined
|
|
65
67
|
const environment = field('environment') === 'sandbox' ? 'sandbox' : 'production'
|
|
66
68
|
const pendingZoneScopes = (['read', 'write'] as const).map((access) => {
|
|
67
69
|
const chainId = field(`zoneChainId_${access}`)
|
|
@@ -87,10 +89,13 @@ function RouteComponent() {
|
|
|
87
89
|
provision.mutate(
|
|
88
90
|
{
|
|
89
91
|
environment,
|
|
92
|
+
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
90
93
|
name,
|
|
91
94
|
...(orgId ? { orgId } : {}),
|
|
92
95
|
...(filters.projectId ? { projectId: filters.projectId } : {}),
|
|
93
|
-
...(
|
|
96
|
+
...(limit === undefined
|
|
97
|
+
? {}
|
|
98
|
+
: { rateLimits: { '*': { limit, period: 'minute' as const } } }),
|
|
94
99
|
scopes: scopes as ApiKeys.provision.Input['scopes'],
|
|
95
100
|
},
|
|
96
101
|
{ onError: (error) => alert(error.message), onSuccess: () => setOpen(false) },
|
|
@@ -273,7 +278,7 @@ function ProvisionForm(props: {
|
|
|
273
278
|
</div>
|
|
274
279
|
) : null}
|
|
275
280
|
<Fields
|
|
276
|
-
className="min-h-0 flex-1 gap-6
|
|
281
|
+
className="min-h-0 flex-1 gap-6 overflow-y-auto overscroll-contain"
|
|
277
282
|
variant="default"
|
|
278
283
|
>
|
|
279
284
|
<OrganizationAutocomplete orgId={props.orgId} />
|
|
@@ -289,6 +294,12 @@ function ProvisionForm(props: {
|
|
|
289
294
|
<option value="sandbox">Sandbox</option>
|
|
290
295
|
</select>
|
|
291
296
|
</div>
|
|
297
|
+
<Input
|
|
298
|
+
label="Expires at (optional)"
|
|
299
|
+
min={toLocalDateTimeCeil(new Date(Date.now() + 2 * 60_000))}
|
|
300
|
+
name="expiresAt"
|
|
301
|
+
type="datetime-local"
|
|
302
|
+
/>
|
|
292
303
|
<Input
|
|
293
304
|
defaultValue="10000"
|
|
294
305
|
label="Rate limit"
|
|
@@ -298,14 +309,14 @@ function ProvisionForm(props: {
|
|
|
298
309
|
suffix="req/min"
|
|
299
310
|
type="number"
|
|
300
311
|
/>
|
|
301
|
-
<div className="flex
|
|
312
|
+
<div className="flex flex-col gap-2">
|
|
302
313
|
<span className="label-13 text-foreground-secondary">Scopes</span>
|
|
303
314
|
{scopes.isPending ? (
|
|
304
315
|
<span className="text-label-13 text-foreground-secondary">Loading scopes…</span>
|
|
305
316
|
) : scopes.error ? (
|
|
306
317
|
<span className="text-label-13 text-negative">{scopes.error.message}</span>
|
|
307
318
|
) : (
|
|
308
|
-
<div className="
|
|
319
|
+
<div className="border border-border">
|
|
309
320
|
<Rows variant="surface">
|
|
310
321
|
{(scopes.data?.data ?? []).map((entry) => {
|
|
311
322
|
const zoneAccess = Scopes.accessFromTemplate(entry.scope)
|
|
@@ -618,7 +629,7 @@ function KeyList(props: {
|
|
|
618
629
|
function KeyRow(props: { currentUser?: string | undefined; record: ApiKeys.ApiKey }) {
|
|
619
630
|
const { currentUser, record } = props
|
|
620
631
|
const [expanded, setExpanded] = useState(false)
|
|
621
|
-
const
|
|
632
|
+
const rateLimit = record.rateLimits?.['*']
|
|
622
633
|
|
|
623
634
|
return (
|
|
624
635
|
<>
|
|
@@ -659,7 +670,9 @@ function KeyRow(props: { currentUser?: string | undefined; record: ApiKeys.ApiKe
|
|
|
659
670
|
<Detail label="Key ID">{record.id}</Detail>
|
|
660
671
|
<Detail label="Environment">{record.environment ?? 'production'}</Detail>
|
|
661
672
|
<Detail label="Rate limit">
|
|
662
|
-
{
|
|
673
|
+
{rateLimit === undefined
|
|
674
|
+
? 'Inherited'
|
|
675
|
+
: `${rateLimit.limit} req/${rateLimit.period === 'minute' ? 'min' : 'sec'}`}
|
|
663
676
|
</Detail>
|
|
664
677
|
<Detail label="Token">···{record.tokenLast4}</Detail>
|
|
665
678
|
<Detail label="Organization">{organization(record)}</Detail>
|
|
@@ -804,9 +817,11 @@ function RevokeDialog(props: { currentUser?: string | undefined; record: ApiKeys
|
|
|
804
817
|
<Detail label="Project">{project(record)}</Detail>
|
|
805
818
|
<Detail label="Provisioned by">{creator ?? 'Unknown'}</Detail>
|
|
806
819
|
<Detail label="Rate limit">
|
|
807
|
-
{record.rateLimits?.['*']
|
|
820
|
+
{record.rateLimits?.['*'] === undefined
|
|
808
821
|
? 'Inherited'
|
|
809
|
-
: `${record.rateLimits['*']!.
|
|
822
|
+
: `${record.rateLimits['*']!.limit} req/${
|
|
823
|
+
record.rateLimits['*']!.period === 'minute' ? 'min' : 'sec'
|
|
824
|
+
}`}
|
|
810
825
|
</Detail>
|
|
811
826
|
</dl>
|
|
812
827
|
{differentCreator ? (
|
|
@@ -858,6 +873,13 @@ function Detail(props: { children: ReactNode; className?: string; label: ReactNo
|
|
|
858
873
|
)
|
|
859
874
|
}
|
|
860
875
|
|
|
876
|
+
/** Rounds an instant up for a minute-precision `datetime-local` control. */
|
|
877
|
+
function toLocalDateTimeCeil(date: Date) {
|
|
878
|
+
const minute = 60_000
|
|
879
|
+
const time = Math.ceil(date.getTime() / minute) * minute
|
|
880
|
+
return new Date(time - date.getTimezoneOffset() * minute).toISOString().slice(0, 16)
|
|
881
|
+
}
|
|
882
|
+
|
|
861
883
|
function matches(record: ApiKeys.ApiKey, query?: string) {
|
|
862
884
|
const normalized = query?.trim().toLowerCase()
|
|
863
885
|
if (!normalized) return true
|