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.
Files changed (224) hide show
  1. package/dist/ApiKey.d.ts +2 -1
  2. package/dist/ApiKey.d.ts.map +1 -1
  3. package/dist/ApiKeys.d.ts +4 -2
  4. package/dist/ApiKeys.d.ts.map +1 -1
  5. package/dist/ApiKeys.js +26 -1
  6. package/dist/ApiKeys.js.map +1 -1
  7. package/dist/App.d.ts +3346 -2575
  8. package/dist/App.d.ts.map +1 -1
  9. package/dist/App.js +1 -0
  10. package/dist/App.js.map +1 -1
  11. package/dist/Webhooks.d.ts.map +1 -1
  12. package/dist/Webhooks.js +5 -5
  13. package/dist/Webhooks.js.map +1 -1
  14. package/dist/admin/App.d.ts +91 -84
  15. package/dist/admin/App.d.ts.map +1 -1
  16. package/dist/admin/apps/api-keys.d.ts +26 -17
  17. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  18. package/dist/admin/apps/api-keys.js +6 -0
  19. package/dist/admin/apps/api-keys.js.map +1 -1
  20. package/dist/admin/apps/organizations.d.ts +4 -2
  21. package/dist/admin/apps/organizations.d.ts.map +1 -1
  22. package/dist/admin/ui.generated.d.ts +1 -1
  23. package/dist/admin/ui.generated.js +69 -69
  24. package/dist/admin/ui.generated.js.map +1 -1
  25. package/dist/analytics/Analytics.d.ts.map +1 -1
  26. package/dist/analytics/Analytics.js +26 -0
  27. package/dist/analytics/Analytics.js.map +1 -1
  28. package/dist/apps/Zones.d.ts +4266 -82
  29. package/dist/apps/Zones.d.ts.map +1 -1
  30. package/dist/apps/Zones.js +27 -8
  31. package/dist/apps/Zones.js.map +1 -1
  32. package/dist/apps/data/App.d.ts +3467 -2696
  33. package/dist/apps/data/App.d.ts.map +1 -1
  34. package/dist/apps/data/routes/activities.d.ts +2999 -100
  35. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  36. package/dist/apps/data/routes/activities.js +340 -2
  37. package/dist/apps/data/routes/activities.js.map +1 -1
  38. package/dist/apps/data/routes/addresses.d.ts +850 -336
  39. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  40. package/dist/apps/data/routes/balances.d.ts +26 -26
  41. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  42. package/dist/apps/data/routes/balances.js +11 -3
  43. package/dist/apps/data/routes/balances.js.map +1 -1
  44. package/dist/apps/data/routes/blocks.d.ts +4 -4
  45. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  46. package/dist/apps/data/routes/earn.d.ts +75 -75
  47. package/dist/apps/data/routes/earn.js +2 -2
  48. package/dist/apps/data/routes/earn.js.map +1 -1
  49. package/dist/apps/data/routes/exchanges.d.ts +120 -120
  50. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  51. package/dist/apps/data/routes/indexer.d.ts +35 -34
  52. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  53. package/dist/apps/data/routes/indexer.js +3 -2
  54. package/dist/apps/data/routes/indexer.js.map +1 -1
  55. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  56. package/dist/apps/data/routes/rpc.js +5 -1
  57. package/dist/apps/data/routes/rpc.js.map +1 -1
  58. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  59. package/dist/apps/data/routes/transactions.d.ts +281 -281
  60. package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
  61. package/dist/apps/data/routes/webhooks.d.ts +418 -364
  62. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  63. package/dist/apps/data/routes/webhooks.js +161 -14
  64. package/dist/apps/data/routes/webhooks.js.map +1 -1
  65. package/dist/apps/data/routes/zones.d.ts +31 -31
  66. package/dist/apps/data/routes/zones.js +1 -1
  67. package/dist/apps/data/routes/zones.js.map +1 -1
  68. package/dist/apps/funding/App.d.ts +96 -96
  69. package/dist/apps/funding/routes/deposit-addresses.d.ts +52 -52
  70. package/dist/apps/funding/routes/deposit-addresses.js +1 -1
  71. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  72. package/dist/apps/funding/routes/providers.d.ts +13 -13
  73. package/dist/apps/funding/routes/transfers.d.ts +32 -32
  74. package/dist/apps/funding/routes/transfers.js +1 -1
  75. package/dist/apps/funding/routes/transfers.js.map +1 -1
  76. package/dist/apps/management/App.d.ts +418 -418
  77. package/dist/apps/management/routes/api-keys.d.ts +50 -50
  78. package/dist/apps/management/routes/billing.d.ts +72 -72
  79. package/dist/apps/management/routes/billing.js +1 -1
  80. package/dist/apps/management/routes/billing.js.map +1 -1
  81. package/dist/apps/management/routes/faucet.d.ts +37 -37
  82. package/dist/apps/management/routes/faucet.js +2 -2
  83. package/dist/apps/management/routes/faucet.js.map +1 -1
  84. package/dist/apps/management/routes/invite-links.d.ts +42 -42
  85. package/dist/apps/management/routes/me.d.ts +6 -6
  86. package/dist/apps/management/routes/members.d.ts +75 -75
  87. package/dist/apps/management/routes/projects.d.ts +40 -40
  88. package/dist/apps/management/routes/usage.d.ts +76 -76
  89. package/dist/apps/management/routes/webhooks.d.ts +49 -49
  90. package/dist/apps/mcp.d.ts.map +1 -1
  91. package/dist/apps/mcp.js +4 -1
  92. package/dist/apps/mcp.js.map +1 -1
  93. package/dist/db/Db.d.ts +2 -0
  94. package/dist/db/Db.d.ts.map +1 -1
  95. package/dist/db/Db.js +58 -0
  96. package/dist/db/Db.js.map +1 -1
  97. package/dist/db/Schema.d.ts +19 -0
  98. package/dist/db/Schema.d.ts.map +1 -1
  99. package/dist/db/Schema.js +24 -0
  100. package/dist/db/Schema.js.map +1 -1
  101. package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
  102. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
  103. package/dist/db/tables/webhookQueueEvents.js +160 -0
  104. package/dist/db/tables/webhookQueueEvents.js.map +1 -0
  105. package/dist/db/tables/webhookSubscriptions.d.ts +2 -0
  106. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  107. package/dist/db/tables/webhookSubscriptions.js +10 -0
  108. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  109. package/dist/internal/Auth.d.ts +3 -0
  110. package/dist/internal/Auth.d.ts.map +1 -1
  111. package/dist/internal/Auth.js +6 -4
  112. package/dist/internal/Auth.js.map +1 -1
  113. package/dist/internal/MetricSink.d.ts +27 -0
  114. package/dist/internal/MetricSink.d.ts.map +1 -1
  115. package/dist/internal/MetricSink.js +29 -2
  116. package/dist/internal/MetricSink.js.map +1 -1
  117. package/dist/internal/RateLimit.d.ts +4 -6
  118. package/dist/internal/RateLimit.d.ts.map +1 -1
  119. package/dist/internal/RateLimit.js +15 -14
  120. package/dist/internal/RateLimit.js.map +1 -1
  121. package/dist/internal/Store.d.ts +2 -1
  122. package/dist/internal/Store.d.ts.map +1 -1
  123. package/dist/internal/Store.js +57 -18
  124. package/dist/internal/Store.js.map +1 -1
  125. package/dist/internal/Value.d.ts +2 -0
  126. package/dist/internal/Value.d.ts.map +1 -1
  127. package/dist/internal/Value.js +25 -0
  128. package/dist/internal/Value.js.map +1 -1
  129. package/dist/internal/WebhookTransfer.d.ts +20 -0
  130. package/dist/internal/WebhookTransfer.d.ts.map +1 -1
  131. package/dist/internal/WebhookTransfer.js +7 -4
  132. package/dist/internal/WebhookTransfer.js.map +1 -1
  133. package/dist/internal/Webhooks.d.ts +139 -0
  134. package/dist/internal/Webhooks.d.ts.map +1 -1
  135. package/dist/internal/Webhooks.js +129 -7
  136. package/dist/internal/Webhooks.js.map +1 -1
  137. package/dist/internal/funding/providers/relay.d.ts +3 -4
  138. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  139. package/dist/internal/funding/providers/relay.js +59 -8
  140. package/dist/internal/funding/providers/relay.js.map +1 -1
  141. package/package.json +3 -3
  142. package/src/ApiKeys.test.ts +20 -0
  143. package/src/ApiKeys.ts +28 -1
  144. package/src/App.test.ts +10 -8
  145. package/src/App.ts +1 -0
  146. package/src/Client.test-d.ts +1 -0
  147. package/src/Webhooks.ts +19 -17
  148. package/src/admin/apps/api-keys.test.ts +9 -0
  149. package/src/admin/apps/api-keys.ts +15 -0
  150. package/src/admin/ui/src/lib/query-client.ts +3 -2
  151. package/src/admin/ui/src/routes/api-keys.tsx +31 -9
  152. package/src/admin/ui.generated.ts +69 -69
  153. package/src/analytics/Analytics.test.ts +8 -0
  154. package/src/analytics/Analytics.ts +26 -0
  155. package/src/apps/Zones.test.ts +20 -3
  156. package/src/apps/Zones.ts +28 -8
  157. package/src/apps/composition.test.ts +1 -0
  158. package/src/apps/data/App.test.ts +2 -2
  159. package/src/apps/data/routes/activities.bench.ts +41 -0
  160. package/src/apps/data/routes/activities.test.ts +103 -2
  161. package/src/apps/data/routes/activities.ts +401 -2
  162. package/src/apps/data/routes/balances.bench.ts +21 -0
  163. package/src/apps/data/routes/balances.ts +11 -3
  164. package/src/apps/data/routes/blocks.bench.ts +34 -0
  165. package/src/apps/data/routes/coingecko.bench.ts +53 -0
  166. package/src/apps/data/routes/earn.bench.ts +71 -0
  167. package/src/apps/data/routes/earn.ts +2 -2
  168. package/src/apps/data/routes/exchanges.bench.ts +85 -0
  169. package/src/apps/data/routes/fee-amm.bench.ts +13 -0
  170. package/src/apps/data/routes/indexer.bench.ts +8 -0
  171. package/src/apps/data/routes/indexer.test.ts +2 -1
  172. package/src/apps/data/routes/indexer.ts +3 -2
  173. package/src/apps/data/routes/receipts.bench.ts +39 -0
  174. package/src/apps/data/routes/rpc.bench.ts +14 -0
  175. package/src/apps/data/routes/rpc.test.ts +27 -0
  176. package/src/apps/data/routes/rpc.ts +5 -1
  177. package/src/apps/data/routes/tokenlist.bench.ts +3 -0
  178. package/src/apps/data/routes/tokens.bench.ts +76 -0
  179. package/src/apps/data/routes/transactions.bench.ts +40 -0
  180. package/src/apps/data/routes/transfers.bench.ts +28 -0
  181. package/src/apps/data/routes/valuation.bench.ts +14 -0
  182. package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
  183. package/src/apps/data/routes/webhooks.bench.ts +26 -0
  184. package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
  185. package/src/apps/data/routes/webhooks.test.ts +53 -6
  186. package/src/apps/data/routes/webhooks.ts +221 -14
  187. package/src/apps/data/routes/zones.bench.ts +32 -0
  188. package/src/apps/data/routes/zones.test.ts +1 -1
  189. package/src/apps/data/routes/zones.ts +1 -1
  190. package/src/apps/funding/routes/chains.test.ts +1 -1
  191. package/src/apps/funding/routes/deposit-addresses.test.ts +1 -1
  192. package/src/apps/funding/routes/deposit-addresses.ts +1 -1
  193. package/src/apps/funding/routes/providers.test.ts +1 -1
  194. package/src/apps/funding/routes/quotes.test.ts +1 -1
  195. package/src/apps/funding/routes/transfers.test.ts +3 -6
  196. package/src/apps/funding/routes/transfers.ts +1 -1
  197. package/src/apps/management/routes/billing.ts +1 -1
  198. package/src/apps/management/routes/faucet.ts +2 -2
  199. package/src/apps/management/routes/me.test.ts +31 -0
  200. package/src/apps/mcp.ts +4 -1
  201. package/src/db/Db.ts +81 -0
  202. package/src/db/Schema.ts +32 -0
  203. package/src/db/tables/webhookQueueEvents.ts +285 -0
  204. package/src/db/tables/webhookSubscriptions.ts +15 -0
  205. package/src/internal/Auth.test.ts +61 -50
  206. package/src/internal/Auth.ts +9 -4
  207. package/src/internal/EarnVaults.test.ts +2 -2
  208. package/src/internal/MetricSink.test.ts +58 -1
  209. package/src/internal/MetricSink.ts +60 -2
  210. package/src/internal/RateLimit.test.ts +23 -18
  211. package/src/internal/RateLimit.ts +23 -15
  212. package/src/internal/Store.test.ts +99 -13
  213. package/src/internal/Store.ts +52 -17
  214. package/src/internal/Tidx.test.ts +4 -4
  215. package/src/internal/Value.test.ts +25 -0
  216. package/src/internal/Value.ts +29 -0
  217. package/src/internal/WebhookTransfer.test.ts +89 -0
  218. package/src/internal/WebhookTransfer.ts +11 -7
  219. package/src/internal/Webhooks.test.ts +479 -0
  220. package/src/internal/Webhooks.ts +281 -7
  221. package/src/internal/funding/Provider.test.ts +1 -1
  222. package/src/internal/funding/providers/index.test.ts +73 -3
  223. package/src/internal/funding/providers/relay.test.ts +1 -5
  224. 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 = { perMinute: 2 }
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 = { perMinute: 1 }
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 = { perMinute: 1 }
302
+ const routeLimit = { limit: 1, period: 'minute' } satisfies RateLimit.Limit
303
303
  const app = TestApp.create({
304
304
  auth: {
305
- mpp: { rateLimit: { perMinute: 2 } },
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
- perMinute: 60,
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 = { perMinute: 1 }
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
- perMinute: 60,
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.perMinute; index++) await rateLimit.consume({ key, limit })
907
+ for (let index = 0; index < limit.limit; index++) await rateLimit.consume({ key, limit })
906
908
  }
package/src/App.ts CHANGED
@@ -309,6 +309,7 @@ export function create<const path extends string = '/'>(options: create.Options<
309
309
  const auth = Auth.install({
310
310
  auth: options.auth,
311
311
  db: db.source,
312
+ getClient,
312
313
  kv: options.kv,
313
314
  scopeCatalog,
314
315
  })
@@ -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
- metrics && innerBatch
79
- ? async (dispatchables: readonly AppWebhooks.poll.Dispatchable[]) => {
80
- const start = Date.now()
81
- const result = await innerBatch(dispatchables)
82
- const durationMs = Date.now() - start
83
- for (const dispatchable of dispatchables)
84
- metrics.record({
85
- chainId: dispatchable.envelope.chainId,
86
- durationMs,
87
- eventType: dispatchable.envelope.type,
88
- ok: result.ok,
89
- type: 'webhook:enqueue',
90
- })
91
- return result
92
- }
93
- : innerBatch
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
- maxAge: 1000 * 60 * 60 * 24,
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 perMinute = rateLimit ? Number(rateLimit) : undefined
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
- ...(perMinute === undefined ? {} : { rateLimits: { '*': { perMinute } } }),
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 [grid-template-rows:auto_auto_auto_auto_minmax(0,1fr)]"
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 min-h-0 flex-col gap-2 overflow-hidden">
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="min-h-0 flex-1 overflow-y-auto overscroll-contain border border-border">
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 perMinute = record.rateLimits?.['*']?.perMinute
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
- {perMinute === undefined ? 'Inherited' : `${perMinute} req/min`}
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?.['*']?.perMinute === undefined
820
+ {record.rateLimits?.['*'] === undefined
808
821
  ? 'Inherited'
809
- : `${record.rateLimits['*']!.perMinute} req/min`}
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