tapimo 0.4.0 → 0.4.3

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 (255) hide show
  1. package/README.md +1 -1
  2. package/dist/ApiKey.d.ts +1 -9
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +2 -3
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +53 -27
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +32 -17
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +2451 -1533
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +23 -9
  13. package/dist/App.js.map +1 -1
  14. package/dist/Assets.d.ts +10 -0
  15. package/dist/Assets.d.ts.map +1 -1
  16. package/dist/Assets.js +4 -0
  17. package/dist/Assets.js.map +1 -1
  18. package/dist/Auth.d.ts +2 -1
  19. package/dist/Auth.d.ts.map +1 -1
  20. package/dist/Auth.js +2 -1
  21. package/dist/Auth.js.map +1 -1
  22. package/dist/Scope.d.ts +40 -15
  23. package/dist/Scope.d.ts.map +1 -1
  24. package/dist/Scope.js +44 -13
  25. package/dist/Scope.js.map +1 -1
  26. package/dist/admin/App.d.ts +560 -9
  27. package/dist/admin/App.d.ts.map +1 -1
  28. package/dist/admin/App.js +38 -6
  29. package/dist/admin/App.js.map +1 -1
  30. package/dist/admin/apps/api-keys.d.ts +10 -24
  31. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  32. package/dist/admin/apps/api-keys.js +10 -3
  33. package/dist/admin/apps/api-keys.js.map +1 -1
  34. package/dist/admin/apps/organizations.d.ts +833 -0
  35. package/dist/admin/apps/organizations.d.ts.map +1 -0
  36. package/dist/admin/apps/organizations.js +733 -0
  37. package/dist/admin/apps/organizations.js.map +1 -0
  38. package/dist/admin/apps/scopes.d.ts +8 -0
  39. package/dist/admin/apps/scopes.d.ts.map +1 -1
  40. package/dist/admin/ui.generated.d.ts +1 -1
  41. package/dist/admin/ui.generated.d.ts.map +1 -1
  42. package/dist/admin/ui.generated.js +53 -13
  43. package/dist/admin/ui.generated.js.map +1 -1
  44. package/dist/analytics/Analytics.d.ts +10 -0
  45. package/dist/analytics/Analytics.d.ts.map +1 -1
  46. package/dist/analytics/Analytics.js +8 -0
  47. package/dist/analytics/Analytics.js.map +1 -1
  48. package/dist/analytics/tables/requestEvents.d.ts +9 -0
  49. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  50. package/dist/analytics/tables/requestEvents.js +14 -0
  51. package/dist/analytics/tables/requestEvents.js.map +1 -1
  52. package/dist/apps/data/App.d.ts +1354 -984
  53. package/dist/apps/data/App.d.ts.map +1 -1
  54. package/dist/apps/data/App.js +20 -3
  55. package/dist/apps/data/App.js.map +1 -1
  56. package/dist/apps/data/FundingRouteProviders.d.ts +94 -5
  57. package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
  58. package/dist/apps/data/FundingRouteProviders.js +196 -5
  59. package/dist/apps/data/FundingRouteProviders.js.map +1 -1
  60. package/dist/apps/data/routes/activities.d.ts +38 -38
  61. package/dist/apps/data/routes/addresses.d.ts +24 -24
  62. package/dist/apps/data/routes/balances.d.ts +45 -45
  63. package/dist/apps/data/routes/blocks.d.ts +39 -39
  64. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  65. package/dist/apps/data/routes/exchanges.d.ts +453 -453
  66. package/dist/apps/data/routes/fee-amm.d.ts +101 -101
  67. package/dist/apps/data/routes/funding.d.ts +730 -138
  68. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  69. package/dist/apps/data/routes/funding.js +234 -0
  70. package/dist/apps/data/routes/funding.js.map +1 -1
  71. package/dist/apps/data/routes/indexer.d.ts +5 -5
  72. package/dist/apps/data/routes/rpc.d.ts +31 -31
  73. package/dist/apps/data/routes/verified-tokens.d.ts +81 -81
  74. package/dist/apps/management/App.d.ts +1156 -615
  75. package/dist/apps/management/App.d.ts.map +1 -1
  76. package/dist/apps/management/App.js +2 -2
  77. package/dist/apps/management/Billing.d.ts +3 -0
  78. package/dist/apps/management/Billing.d.ts.map +1 -1
  79. package/dist/apps/management/Billing.js +7 -2
  80. package/dist/apps/management/Billing.js.map +1 -1
  81. package/dist/apps/management/routes/api-keys.d.ts +106 -90
  82. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  83. package/dist/apps/management/routes/api-keys.js +41 -30
  84. package/dist/apps/management/routes/api-keys.js.map +1 -1
  85. package/dist/apps/management/routes/billing.d.ts +278 -146
  86. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  87. package/dist/apps/management/routes/billing.js +14 -14
  88. package/dist/apps/management/routes/billing.js.map +1 -1
  89. package/dist/apps/management/routes/invitations.d.ts +93 -20
  90. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  91. package/dist/apps/management/routes/invitations.js +12 -15
  92. package/dist/apps/management/routes/invitations.js.map +1 -1
  93. package/dist/apps/management/routes/me.d.ts +13 -13
  94. package/dist/apps/management/routes/members.d.ts +89 -18
  95. package/dist/apps/management/routes/members.d.ts.map +1 -1
  96. package/dist/apps/management/routes/members.js +10 -10
  97. package/dist/apps/management/routes/members.js.map +1 -1
  98. package/dist/apps/management/routes/orgs.d.ts +130 -70
  99. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  100. package/dist/apps/management/routes/orgs.js +19 -19
  101. package/dist/apps/management/routes/orgs.js.map +1 -1
  102. package/dist/apps/management/routes/projects.d.ts +137 -48
  103. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  104. package/dist/apps/management/routes/projects.js +12 -13
  105. package/dist/apps/management/routes/projects.js.map +1 -1
  106. package/dist/apps/management/routes/scopes.d.ts +14 -28
  107. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  108. package/dist/apps/management/routes/scopes.js +6 -7
  109. package/dist/apps/management/routes/scopes.js.map +1 -1
  110. package/dist/apps/management/routes/usage.d.ts +63 -19
  111. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  112. package/dist/apps/management/routes/usage.js +9 -10
  113. package/dist/apps/management/routes/usage.js.map +1 -1
  114. package/dist/apps/mcp.d.ts.map +1 -1
  115. package/dist/apps/mcp.js +35 -3
  116. package/dist/apps/mcp.js.map +1 -1
  117. package/dist/apps/relay/App.d.ts +26 -4
  118. package/dist/apps/relay/App.d.ts.map +1 -1
  119. package/dist/apps/relay/App.js +17 -14
  120. package/dist/apps/relay/App.js.map +1 -1
  121. package/dist/db/Db.d.ts +5 -3
  122. package/dist/db/Db.d.ts.map +1 -1
  123. package/dist/db/Db.js +52 -0
  124. package/dist/db/Db.js.map +1 -1
  125. package/dist/db/Schema.d.ts +13 -0
  126. package/dist/db/Schema.d.ts.map +1 -1
  127. package/dist/db/Schema.js +16 -1
  128. package/dist/db/Schema.js.map +1 -1
  129. package/dist/db/tables/adminAuditLogs.d.ts +27 -0
  130. package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
  131. package/dist/db/tables/adminAuditLogs.js +19 -0
  132. package/dist/db/tables/adminAuditLogs.js.map +1 -0
  133. package/dist/db/tables/invitations.d.ts +1 -1
  134. package/dist/db/tables/invitations.d.ts.map +1 -1
  135. package/dist/db/tables/memberships.d.ts +32 -4
  136. package/dist/db/tables/memberships.d.ts.map +1 -1
  137. package/dist/db/tables/memberships.js +31 -0
  138. package/dist/db/tables/memberships.js.map +1 -1
  139. package/dist/db/tables/organizations.d.ts +43 -1
  140. package/dist/db/tables/organizations.d.ts.map +1 -1
  141. package/dist/db/tables/organizations.js +103 -0
  142. package/dist/db/tables/organizations.js.map +1 -1
  143. package/dist/db/tables/projects.d.ts +25 -0
  144. package/dist/db/tables/projects.d.ts.map +1 -1
  145. package/dist/db/tables/projects.js +22 -0
  146. package/dist/db/tables/projects.js.map +1 -1
  147. package/dist/db/tables/webhookDeliveries.d.ts +26 -0
  148. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  149. package/dist/db/tables/webhookDeliveries.js +37 -0
  150. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  151. package/dist/handlers/relay.d.ts +13 -10
  152. package/dist/handlers/relay.d.ts.map +1 -1
  153. package/dist/handlers/relay.js +46 -16
  154. package/dist/handlers/relay.js.map +1 -1
  155. package/dist/internal/Auth.d.ts +13 -18
  156. package/dist/internal/Auth.d.ts.map +1 -1
  157. package/dist/internal/Auth.js +78 -58
  158. package/dist/internal/Auth.js.map +1 -1
  159. package/dist/internal/Cursor.d.ts +1 -1
  160. package/dist/internal/Cursor.d.ts.map +1 -1
  161. package/dist/internal/Cursor.js +4 -0
  162. package/dist/internal/Cursor.js.map +1 -1
  163. package/dist/internal/EdgeCache.d.ts.map +1 -1
  164. package/dist/internal/EdgeCache.js +2 -0
  165. package/dist/internal/EdgeCache.js.map +1 -1
  166. package/dist/internal/FundingRoutes.d.ts +170 -0
  167. package/dist/internal/FundingRoutes.d.ts.map +1 -1
  168. package/dist/internal/FundingRoutes.js +68 -6
  169. package/dist/internal/FundingRoutes.js.map +1 -1
  170. package/dist/internal/Log.d.ts +16 -1
  171. package/dist/internal/Log.d.ts.map +1 -1
  172. package/dist/internal/Log.js +5 -3
  173. package/dist/internal/Log.js.map +1 -1
  174. package/package.json +2 -2
  175. package/src/ApiKey.ts +2 -3
  176. package/src/ApiKeys.ts +95 -17
  177. package/src/App.test.ts +44 -2
  178. package/src/App.ts +29 -9
  179. package/src/Assets.ts +8 -0
  180. package/src/Auth.test-d.ts +36 -0
  181. package/src/Auth.ts +20 -1
  182. package/src/Client.test-d.ts +64 -17
  183. package/src/Scope.test.ts +34 -0
  184. package/src/Scope.ts +53 -17
  185. package/src/admin/App.test.ts +33 -3
  186. package/src/admin/App.ts +49 -14
  187. package/src/admin/apps/api-keys.ts +10 -3
  188. package/src/admin/apps/organizations.test.ts +455 -0
  189. package/src/admin/apps/organizations.ts +953 -0
  190. package/src/admin/apps/scopes.test.ts +10 -0
  191. package/src/admin/ui/plugin.ts +1 -1
  192. package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
  193. package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
  194. package/src/admin/ui/src/lib/api-keys.ts +36 -19
  195. package/src/admin/ui/src/lib/config.ts +2 -3
  196. package/src/admin/ui/src/lib/organizations.test.ts +89 -0
  197. package/src/admin/ui/src/lib/organizations.ts +262 -0
  198. package/src/admin/ui/src/lib/query-client.ts +3 -1
  199. package/src/admin/ui/src/lib/theme.ts +57 -0
  200. package/src/admin/ui/src/routeTree.gen.ts +47 -4
  201. package/src/admin/ui/src/routes/__root.tsx +18 -3
  202. package/src/admin/ui/src/routes/api-keys.tsx +52 -7
  203. package/src/admin/ui/src/routes/index.tsx +1 -1
  204. package/src/admin/ui/src/routes/organizations.tsx +158 -0
  205. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
  206. package/src/admin/ui/src/start.ts +6 -0
  207. package/src/admin/ui.generated.ts +63 -13
  208. package/src/analytics/Analytics.ts +25 -0
  209. package/src/analytics/tables/requestEvents.ts +33 -0
  210. package/src/apps/composition.test.ts +82 -2
  211. package/src/apps/data/App.ts +24 -3
  212. package/src/apps/data/FundingRouteProviders.test.ts +204 -0
  213. package/src/apps/data/FundingRouteProviders.ts +294 -9
  214. package/src/apps/data/routes/funding.test.ts +90 -23
  215. package/src/apps/data/routes/funding.ts +315 -0
  216. package/src/apps/data/routes/rpc.test.ts +33 -0
  217. package/src/apps/management/App.ts +2 -2
  218. package/src/apps/management/Billing.ts +10 -2
  219. package/src/apps/management/routes/api-keys.test.ts +137 -1
  220. package/src/apps/management/routes/api-keys.ts +63 -38
  221. package/src/apps/management/routes/billing.ts +14 -14
  222. package/src/apps/management/routes/invitations.test.ts +102 -1
  223. package/src/apps/management/routes/invitations.ts +13 -15
  224. package/src/apps/management/routes/members.ts +10 -10
  225. package/src/apps/management/routes/orgs.test.ts +78 -16
  226. package/src/apps/management/routes/orgs.ts +21 -20
  227. package/src/apps/management/routes/projects.ts +12 -13
  228. package/src/apps/management/routes/scopes.test.ts +16 -0
  229. package/src/apps/management/routes/scopes.ts +6 -7
  230. package/src/apps/management/routes/usage.ts +9 -14
  231. package/src/apps/mcp.test.ts +25 -1
  232. package/src/apps/mcp.ts +43 -3
  233. package/src/apps/relay/App.test.ts +23 -18
  234. package/src/apps/relay/App.ts +35 -14
  235. package/src/db/Db.ts +59 -3
  236. package/src/db/Schema.ts +20 -1
  237. package/src/db/tables/adminAuditLogs.ts +47 -0
  238. package/src/db/tables/invitations.ts +1 -1
  239. package/src/db/tables/memberships.ts +59 -3
  240. package/src/db/tables/organizations.ts +148 -1
  241. package/src/db/tables/projects.ts +47 -0
  242. package/src/db/tables/webhookDeliveries.ts +83 -0
  243. package/src/handlers/relay.test-d.ts +8 -0
  244. package/src/handlers/relay.test.ts +2 -2
  245. package/src/handlers/relay.ts +62 -22
  246. package/src/internal/Auth.test.ts +174 -7
  247. package/src/internal/Auth.ts +90 -62
  248. package/src/internal/Cursor.test.ts +10 -0
  249. package/src/internal/Cursor.ts +5 -1
  250. package/src/internal/EdgeCache.test.ts +11 -0
  251. package/src/internal/EdgeCache.ts +2 -0
  252. package/src/internal/FundingRoutes.test.ts +26 -0
  253. package/src/internal/FundingRoutes.ts +187 -18
  254. package/src/internal/Log.test.ts +41 -3
  255. package/src/internal/Log.ts +23 -4
@@ -160,7 +160,7 @@ export type Principal =
160
160
  orgId: string
161
161
  /** Payment metadata for paid overflow requests. */
162
162
  payment?: Payment | undefined
163
- /** Owning project id, when the key carries project attribution. */
163
+ /** Attributed project id, when present. */
164
164
  projectId?: string | undefined
165
165
  /** Principal kind. */
166
166
  type: 'api_key'
@@ -205,9 +205,9 @@ export type Principal =
205
205
  export type Variables = {
206
206
  /** Auth context consumed by protected handlers. */
207
207
  auth: Context
208
- /** Caller's membership resolved by {@link ensureOrg}; absent for `super_admin`. */
208
+ /** Caller's membership resolved by {@link ensureOrg} or {@link ensureProject}; absent for API keys and `super_admin`. */
209
209
  membership?: Memberships.Record | undefined
210
- /** Organization resolved by {@link ensureOrg} for the current request. */
210
+ /** Organization resolved by {@link ensureOrg} or {@link ensureProject} for the current request. */
211
211
  org?: Organizations.Record | undefined
212
212
  /** Authenticated or anonymous caller. */
213
213
  principal?: Principal | undefined
@@ -221,13 +221,7 @@ export function getPrincipal<environment extends Environment>(c: hono_Context<en
221
221
  }
222
222
 
223
223
  /**
224
- * Guards org-scoped routes (`:orgId`). Sessions reach only organizations they
225
- * are members of; the super admin reaches any. Missing or foreign
226
- * organizations return `404`, so foreign org ids are indistinguishable from
227
- * absent ones; a member below the required role gets `403` (the resource is
228
- * visible to them, the action is not). The resolved organization and
229
- * membership land on context — read them with {@link org} and
230
- * {@link membership}.
224
+ * Guards `:orgId` routes and resolves the organization. Sessions require membership, API keys require ownership, and the super admin bypasses ownership.
231
225
  */
232
226
  export function ensureOrg(options: ensureOrg.Options = {}): MiddlewareHandler {
233
227
  const { role: minimum = 'member' } = options
@@ -243,11 +237,17 @@ export function ensureOrg(options: ensureOrg.Options = {}): MiddlewareHandler {
243
237
  const orgId = c.req.param('orgId' as never) as string | undefined
244
238
  if (!orgId) return notFound()
245
239
  const principal = getPrincipal(c)
246
- if (principal?.type !== 'session' && principal?.type !== 'super_admin') return notFound()
240
+ if (
241
+ principal?.type !== 'api_key' &&
242
+ principal?.type !== 'session' &&
243
+ principal?.type !== 'super_admin'
244
+ )
245
+ return notFound()
246
+ if (principal.type === 'api_key' && principal.orgId !== orgId) return notFound()
247
247
  const db = Db.get(contextDb(c))
248
248
  const record = await Organizations.get(db, orgId)
249
249
  if (!record) return notFound()
250
- if (principal.type !== 'super_admin') {
250
+ if (principal.type === 'session') {
251
251
  const member = await Memberships.get(db, orgId, principal.id)
252
252
  if (!member) return notFound()
253
253
  if (Memberships.rank[member.role] < Memberships.rank[minimum])
@@ -267,16 +267,13 @@ export function ensureOrg(options: ensureOrg.Options = {}): MiddlewareHandler {
267
267
  export declare namespace ensureOrg {
268
268
  /** Options for the org guard. */
269
269
  type Options = {
270
- /** Minimum role required; defaults to `member` (any membership). */
270
+ /** Minimum session role; defaults to `member`. */
271
271
  role?: Memberships.Role | undefined
272
272
  }
273
273
  }
274
274
 
275
275
  /**
276
- * Guards project-scoped routes (`:projectId`) nested under {@link ensureOrg},
277
- * which must run first. Projects resolve only within the scoped organization;
278
- * missing or cross-org projects return `404`. The resolved project lands on
279
- * context — read it with {@link project}.
276
+ * Guards `:projectId` routes and resolves the project. Flat routes authorize against its organization; nested routes preserve the scope from {@link ensureOrg}.
280
277
  */
281
278
  export function ensureProject(): MiddlewareHandler {
282
279
  // Typed loosely (like {@link policy}) so route chains keep their inferred
@@ -290,10 +287,32 @@ export function ensureProject(): MiddlewareHandler {
290
287
  })
291
288
  const projectId = c.req.param('projectId' as never) as string | undefined
292
289
  if (!projectId) return notFound()
290
+ const principal = getPrincipal(c)
291
+ if (
292
+ principal?.type !== 'api_key' &&
293
+ principal?.type !== 'session' &&
294
+ principal?.type !== 'super_admin'
295
+ )
296
+ return notFound()
297
+ const db = Db.get(contextDb(c))
298
+ const record = await Projects.get(db, projectId)
299
+ if (!record) return notFound()
293
300
  const scoped = c.get('org')
294
- if (!scoped) return notFound()
295
- const record = await Projects.get(Db.get(contextDb(c)), projectId)
296
- if (!record || record.orgId !== scoped.id) return notFound()
301
+ if (scoped) {
302
+ if (record.orgId !== scoped.id) return notFound()
303
+ c.set('project', record)
304
+ await next()
305
+ return undefined
306
+ }
307
+ if (principal.type === 'api_key' && principal.orgId !== record.orgId) return notFound()
308
+ const organization = await Organizations.get(db, record.orgId)
309
+ if (!organization) return notFound()
310
+ if (principal.type === 'session') {
311
+ const member = await Memberships.get(db, record.orgId, principal.id)
312
+ if (!member) return notFound()
313
+ c.set('membership', member)
314
+ }
315
+ c.set('org', organization)
297
316
  c.set('project', record)
298
317
  await next()
299
318
  return undefined
@@ -318,7 +337,7 @@ export function ensureOrgError(c: hono_Context) {
318
337
 
319
338
  /** Typed project-scope error response for Hono client inference. Never reached at runtime. */
320
339
  export function ensureProjectError(c: hono_Context) {
321
- // Project routes run the org scope first, so either not-found can surface.
340
+ // Nested project routes run the org scope first, so either not-found can surface.
322
341
  const code =
323
342
  c.req.header('tempo-narrow-scope-code') === 'organization_not_found'
324
343
  ? ('organization_not_found' as const)
@@ -330,19 +349,20 @@ export function ensureProjectError(c: hono_Context) {
330
349
  })
331
350
  }
332
351
 
333
- /** Reads the caller's membership resolved by {@link ensureOrg}; `undefined` for `super_admin`. */
352
+ /** Reads the caller's membership resolved by the organization or project guard; `undefined` for API keys and `super_admin`. */
334
353
  export function membership<environment extends Environment>(
335
354
  c: hono_Context<environment>,
336
355
  ): Memberships.Record | undefined {
337
356
  return c.get('membership')
338
357
  }
339
358
 
340
- /** Reads the organization resolved by {@link ensureOrg}; throws when the middleware did not run. */
359
+ /** Reads the organization resolved by the organization or project guard; throws when neither middleware ran. */
341
360
  export function org<environment extends Environment>(
342
361
  c: hono_Context<environment>,
343
362
  ): Organizations.Record {
344
363
  const record = c.get('org')
345
- if (!record) throw new Error('`orgScope` middleware must run before reading the organization')
364
+ if (!record)
365
+ throw new Error('`ensureOrg` or `ensureProject` must run before reading the organization')
346
366
  return record
347
367
  }
348
368
 
@@ -351,7 +371,7 @@ export function project<environment extends Environment>(
351
371
  c: hono_Context<environment>,
352
372
  ): Projects.Record {
353
373
  const record = c.get('project')
354
- if (!record) throw new Error('`projectScope` middleware must run before reading the project')
374
+ if (!record) throw new Error('`ensureProject` must run before reading the project')
355
375
  return record
356
376
  }
357
377
 
@@ -460,7 +480,9 @@ export function middleware<
460
480
  const resolve = (() => {
461
481
  const uncached = (c: hono_Context, token: string) => {
462
482
  const kv = contextKv(c)
463
- return kv ? ApiKeys.resolve(kv.store, token) : Promise.resolve(null)
483
+ return kv
484
+ ? ApiKeys.resolve(kv.store, token, { scopeCatalog: options.scopeCatalog })
485
+ : Promise.resolve(null)
464
486
  }
465
487
  if (options.apiKey === false || options.apiKey?.cache === false) return uncached
466
488
  return cachedResolve(uncached, options.apiKey?.cache ?? {})
@@ -554,6 +576,8 @@ export declare namespace middleware {
554
576
  | undefined
555
577
  /** Rate-limit store options. */
556
578
  rateLimit?: RateLimit.memory.Options | undefined
579
+ /** Scope catalog accepted while resolving API-key records. */
580
+ scopeCatalog?: Scope.Catalog | undefined
557
581
  /** Session lane resolution — the session surface's capability; omit to leave the session lane closed. */
558
582
  session?: Session | undefined
559
583
  /**
@@ -678,7 +702,7 @@ function handlerError(description: string) {
678
702
  * disables both.
679
703
  */
680
704
  export function install(options: install.Options) {
681
- const { db, kv } = options
705
+ const { db, kv, scopeCatalog } = options
682
706
  const { session: options_session, ...options_middleware } =
683
707
  options.auth === false ? {} : (options.auth ?? {})
684
708
  if (options_session && !kv)
@@ -894,7 +918,11 @@ export function install(options: install.Options) {
894
918
  const middleware_ =
895
919
  options.auth === false
896
920
  ? undefined
897
- : middleware({ ...options_middleware, ...(app ? { session: app.session } : {}) })
921
+ : middleware({
922
+ ...options_middleware,
923
+ ...(app ? { session: app.session } : {}),
924
+ scopeCatalog,
925
+ })
898
926
 
899
927
  return { app, middleware: middleware_ }
900
928
  }
@@ -903,7 +931,7 @@ export declare namespace install {
903
931
  /** Options for building the built-in auth pieces. */
904
932
  type Options = {
905
933
  auth:
906
- | (Omit<middleware.Options, 'session'> & {
934
+ | (Omit<middleware.Options, 'scopeCatalog' | 'session'> & {
907
935
  /** Session sign-in surface: mounts `/v1/auth/siwe` (challenge/verify/logout) and enables the session lane. Pass true for defaults or a config object. Requires `kv`. */
908
936
  session?: Session | undefined
909
937
  })
@@ -913,6 +941,8 @@ export declare namespace install {
913
941
  db: Db.Source
914
942
  /** KV state store, or undefined when key auth is closed. */
915
943
  kv: { store: Store.State } | undefined
944
+ /** Scope catalog accepted while resolving API-key records. */
945
+ scopeCatalog: Scope.Catalog
916
946
  }
917
947
 
918
948
  /** Session sign-in surface configuration: `true` for defaults, or a config object. */
@@ -1229,12 +1259,20 @@ export function require<environment extends Environment = Environment>(
1229
1259
  return createMiddleware<environment>(async (c, next) => {
1230
1260
  const auth = c.get('auth')
1231
1261
  const credential = getApiKeyCredential(c)
1232
- // The deprecated `key` query param has now been consumed for auth. Strip it
1233
- // from the request URL so it never reaches a route's query validator —
1234
- // endpoint query schemas are `z.strictObject`, so an unexpected `key`
1235
- // param would otherwise fail validation with `query_invalid`, breaking the
1236
- // legacy `?key=...` lane on every strict-query endpoint.
1237
- stripLegacyAuthQuery(c)
1262
+ const hasQueryApiKey = new URL(c.req.url).searchParams.has('key')
1263
+ // Auth owns `key`; strict route schemas must not validate it.
1264
+ stripApiKeyQuery(c)
1265
+ const proceed = async () => {
1266
+ await next()
1267
+ // Query credential responses must not enter shared caches.
1268
+ const cacheControl = c.res.headers.get('Cache-Control')?.toLowerCase()
1269
+ if (
1270
+ hasQueryApiKey &&
1271
+ !cacheControl?.includes('no-store') &&
1272
+ !cacheControl?.includes('private')
1273
+ )
1274
+ c.res.headers.set('Cache-Control', 'private')
1275
+ }
1238
1276
  if (credential === false)
1239
1277
  return jsonError(c, {
1240
1278
  code: 'api_key_malformed',
@@ -1248,7 +1286,7 @@ export function require<environment extends Environment = Environment>(
1248
1286
  // It must never reach KV resolution or scope/quota checks.
1249
1287
  if (auth.superAdmin && ApiKey.hash(credential.token) === auth.superAdmin.tokenHash) {
1250
1288
  c.set('principal', { actor: 'super_admin', id: 'super_admin', type: 'super_admin' })
1251
- return next()
1289
+ return proceed()
1252
1290
  }
1253
1291
 
1254
1292
  const apiKey = await auth.apiKey?.resolve(c, credential.token)
@@ -1260,7 +1298,7 @@ export function require<environment extends Environment = Environment>(
1260
1298
  const session = await resolveSession(c, auth)
1261
1299
  if (session) {
1262
1300
  c.set('principal', session)
1263
- return next()
1301
+ return proceed()
1264
1302
  }
1265
1303
  }
1266
1304
  return jsonError(c, {
@@ -1319,7 +1357,7 @@ export function require<environment extends Environment = Environment>(
1319
1357
  const throttled = apiKey.environment === 'sandbox' && apiKey.billingActive !== true
1320
1358
 
1321
1359
  if (hasPayment && !throttled)
1322
- return requirePayment(c, next, {
1360
+ return requirePayment(c, proceed, {
1323
1361
  auth,
1324
1362
  payment: { reason: 'api_key_over_quota', type: 'mpp' },
1325
1363
  policy,
@@ -1351,7 +1389,7 @@ export function require<environment extends Environment = Environment>(
1351
1389
  // A throttled sandbox key gets a plain 429, never an MPP challenge —
1352
1390
  // it must activate billing to lift the ceiling, not pay per request.
1353
1391
  if (throttled) return rateLimitExceeded(c, result)
1354
- return requirePaymentOrRateLimit(c, next, {
1392
+ return requirePaymentOrRateLimit(c, proceed, {
1355
1393
  auth,
1356
1394
  payment: { reason: 'api_key_over_quota', type: 'mpp' },
1357
1395
  policy,
@@ -1361,7 +1399,7 @@ export function require<environment extends Environment = Environment>(
1361
1399
  }
1362
1400
  }
1363
1401
 
1364
- return next()
1402
+ return proceed()
1365
1403
  }
1366
1404
 
1367
1405
  // Cookie sessions carry no Authorization credential; resolve the session
@@ -1371,14 +1409,14 @@ export function require<environment extends Environment = Environment>(
1371
1409
  const session = await resolveSession(c, auth)
1372
1410
  if (session) {
1373
1411
  c.set('principal', session)
1374
- return next()
1412
+ return proceed()
1375
1413
  }
1376
1414
  }
1377
1415
 
1378
1416
  const publicPrincipal = principalFromPublic(c)
1379
1417
  if (hasPayment) {
1380
1418
  c.set('principal', publicPrincipal)
1381
- return requirePayment(c, next, {
1419
+ return requirePayment(c, proceed, {
1382
1420
  auth,
1383
1421
  payment: { reason: 'public_over_quota', type: 'mpp' },
1384
1422
  policy,
@@ -1395,11 +1433,11 @@ export function require<environment extends Environment = Environment>(
1395
1433
  if (result) setRateLimitHeaders(c, result)
1396
1434
  if (!result || result.allowed) {
1397
1435
  c.set('principal', publicPrincipal)
1398
- return next()
1436
+ return proceed()
1399
1437
  }
1400
1438
 
1401
1439
  c.set('principal', publicPrincipal)
1402
- return requirePaymentOrRateLimit(c, next, {
1440
+ return requirePaymentOrRateLimit(c, proceed, {
1403
1441
  auth,
1404
1442
  payment: { reason: 'public_over_quota', type: 'mpp' },
1405
1443
  policy,
@@ -1410,7 +1448,7 @@ export function require<environment extends Environment = Environment>(
1410
1448
 
1411
1449
  if (isPaymentEnabled(auth, policy)) {
1412
1450
  c.set('principal', publicPrincipal)
1413
- return requirePayment(c, next, {
1451
+ return requirePayment(c, proceed, {
1414
1452
  auth,
1415
1453
  payment: { reason: 'public_over_quota', type: 'mpp' },
1416
1454
  policy,
@@ -1574,10 +1612,8 @@ function getApiKeyCredential(c: hono_Context): ApiKeyCredential | false | null {
1574
1612
  const header = c.req.header('tempo-api-key')
1575
1613
  if (header) return { token: header }
1576
1614
 
1577
- // `Authorization: Bearer` compatibility fallback. Must not consume or reject
1578
- // `Authorization: Payment` (MPP), which is a distinct credential — a request
1579
- // carrying only a payment scheme falls through to the deprecated lanes below
1580
- // (which find nothing) and resolves to no API-key credential.
1615
+ // Bearer supports clients unable to set the canonical header. Payment
1616
+ // credentials belong to MPP and must pass through untouched.
1581
1617
  const authorization = c.req.header('authorization')
1582
1618
  if (authorization && !Credential.extractPaymentScheme(authorization)) {
1583
1619
  const match = /^Bearer\s+(.+)$/i.exec(authorization)
@@ -1585,26 +1621,18 @@ function getApiKeyCredential(c: hono_Context): ApiKeyCredential | false | null {
1585
1621
  return { token: match[1] }
1586
1622
  }
1587
1623
 
1588
- // Deprecated fallbacks carried over from the v1 API so existing integrations
1589
- // keep authenticating: the `x-api-key` header and the `key` query parameter.
1624
+ // `x-api-key` remains a deprecated compatibility header.
1590
1625
  const legacyHeader = c.req.header('x-api-key')
1591
1626
  if (legacyHeader) return { token: legacyHeader }
1592
1627
 
1593
- const legacyQuery = c.req.query('key')
1594
- if (legacyQuery) return { token: legacyQuery }
1628
+ const apiKey = c.req.query('key')
1629
+ if (apiKey) return { token: apiKey }
1595
1630
 
1596
1631
  return null
1597
1632
  }
1598
1633
 
1599
- /**
1600
- * Remove the deprecated `key` auth query param from the request URL once it has
1601
- * been read for authentication. Route query schemas are strict
1602
- * (`z.strictObject`), so leaving `key` in the query string would make the
1603
- * downstream validator reject the request as `query_invalid`. Stripping it
1604
- * keeps the legacy `?key=...` auth lane working on strict-query endpoints
1605
- * (e.g. `GET /v1/tokens`).
1606
- */
1607
- function stripLegacyAuthQuery(c: hono_Context) {
1634
+ /** Removes the reserved auth query parameter before route validation. */
1635
+ function stripApiKeyQuery(c: hono_Context) {
1608
1636
  const url = new URL(c.req.url)
1609
1637
  if (!url.searchParams.has('key')) return
1610
1638
  url.searchParams.delete('key')
@@ -18,6 +18,14 @@ describe('Cursor', () => {
18
18
  ])
19
19
  })
20
20
 
21
+ test('round-trips a timestamp/id tuple', () => {
22
+ const token = Cursor.encode(['2026-01-01T00:00:00.000Z', 'org_abc123'])
23
+ expect(Cursor.decode(token, ['timestamp', 'id'])).toEqual([
24
+ '2026-01-01T00:00:00.000Z',
25
+ 'org_abc123',
26
+ ])
27
+ })
28
+
21
29
  test('returns undefined for malformed, mis-typed, or wrong-arity tokens', () => {
22
30
  expect(Cursor.decode('not-base64!!', ['int', 'int'])).toBeUndefined()
23
31
  expect(Cursor.decode(btoa('{"oops":true}'), ['int', 'int'])).toBeUndefined()
@@ -27,6 +35,7 @@ describe('Cursor', () => {
27
35
  expect(Cursor.decode(btoa('[-1,2]'), ['int', 'int'])).toBeUndefined()
28
36
  expect(Cursor.decode(btoa('["x",2]'), ['uint', 'int'])).toBeUndefined()
29
37
  expect(Cursor.decode(btoa('["0xnotanaddress",2]'), ['address', 'int'])).toBeUndefined()
38
+ expect(Cursor.decode(btoa('["org_abc\' OR 1=1"]'), ['id'])).toBeUndefined()
30
39
  })
31
40
 
32
41
  test('renders injection-safe SQL literals per field kind', () => {
@@ -35,6 +44,7 @@ describe('Cursor', () => {
35
44
  expect(Cursor.literal('0xAbC0000000000000000000000000000000000000', 'address')).toBe(
36
45
  "'0xabc0000000000000000000000000000000000000'",
37
46
  )
47
+ expect(Cursor.literal('org_abc123', 'id')).toBe("'org_abc123'")
38
48
  expect(Cursor.literal('2024-01-01T00:00:00Z', 'timestamp')).toBe("'2024-01-01T00:00:00Z'")
39
49
  })
40
50
 
@@ -16,7 +16,7 @@
16
16
  export type Cursor = (string | number)[]
17
17
 
18
18
  /** The kind of a cursor field, determining its validation and SQL literal form. */
19
- export type Field = 'int' | 'uint' | 'address' | 'timestamp'
19
+ export type Field = 'address' | 'id' | 'int' | 'timestamp' | 'uint'
20
20
 
21
21
  /** Encodes a positional tuple as an opaque, URL-safe token. */
22
22
  export function encode(values: Cursor): string {
@@ -76,6 +76,8 @@ export function literal(value: string | number, field: Field): string {
76
76
  return String(value)
77
77
  case 'address':
78
78
  return `'${String(value).toLowerCase()}'`
79
+ case 'id':
80
+ return `'${String(value)}'`
79
81
  case 'timestamp':
80
82
  return `'${String(value)}'`
81
83
  }
@@ -135,6 +137,8 @@ function isValid(value: unknown, field: Field): boolean {
135
137
  return typeof value === 'string' && /^\d+$/.test(value)
136
138
  case 'address':
137
139
  return typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value)
140
+ case 'id':
141
+ return typeof value === 'string' && /^[A-Za-z0-9_-]+$/.test(value)
138
142
  case 'timestamp':
139
143
  return typeof value === 'string' && !Number.isNaN(Date.parse(value))
140
144
  }
@@ -51,6 +51,17 @@ describe('middleware', () => {
51
51
  expect(store.size).toMatchInlineSnapshot(`1`)
52
52
  })
53
53
 
54
+ test('bypasses the cache for query API key credentials', async () => {
55
+ const { app, store, downstreamCalls } = createApp()
56
+
57
+ await app.request('/cached?key=opaque-secret')
58
+ await app.request('/cached?key=opaque-secret')
59
+
60
+ expect(downstreamCalls()).toMatchInlineSnapshot(`2`)
61
+ expect(store.gets).toMatchInlineSnapshot(`0`)
62
+ expect(store.size).toMatchInlineSnapshot(`0`)
63
+ })
64
+
54
65
  test('refreshes the entry when the request sends `Cache-Control: no-cache`', async () => {
55
66
  const { app, store, downstreamCalls } = createApp()
56
67
 
@@ -80,6 +80,8 @@ export function middleware<environment extends Env>(
80
80
  store !== undefined &&
81
81
  c.req.method === 'GET' &&
82
82
  !c.req.raw.headers.has('range') &&
83
+ // Query credentials must reach auth and never enter cache keys.
84
+ !new URL(c.req.url).searchParams.has('key') &&
83
85
  isCacheableRoute(c)
84
86
  const key = eligible ? cacheKey(c.req.url) : undefined
85
87
 
@@ -9,6 +9,15 @@ const baseUsdcRequest = {
9
9
  sourceToken: 'USDC',
10
10
  } as const
11
11
 
12
+ const baseAcrossQuoteRequest = {
13
+ depositor: '0x1111111111111111111111111111111111111111',
14
+ provider: 'across',
15
+ sourceAmount: '1000000',
16
+ sourceAmountUnits: 'baseUnits',
17
+ sourceChain: 'base',
18
+ sourceToken: 'USDC',
19
+ } as const
20
+
12
21
  describe('listRoutes', () => {
13
22
  test('enriches static candidates with adapter quote probes', async () => {
14
23
  let seen: FundingRoutes.QuoteProbeInput | undefined
@@ -200,6 +209,23 @@ describe('listRoutes', () => {
200
209
  })
201
210
  })
202
211
 
212
+ describe('quoteRoute', () => {
213
+ test('rejects requests without a configured quote capability', async () => {
214
+ await expect(FundingRoutes.quoteRoute(baseAcrossQuoteRequest)).rejects.toBeInstanceOf(
215
+ FundingRoutes.ProviderNotConfiguredError,
216
+ )
217
+ })
218
+
219
+ test('rejects requests outside curated inventory', async () => {
220
+ await expect(
221
+ FundingRoutes.quoteRoute({
222
+ ...baseAcrossQuoteRequest,
223
+ sourceToken: 'USDT',
224
+ }),
225
+ ).rejects.toBeInstanceOf(FundingRoutes.FundingRouteNotFoundError)
226
+ })
227
+ })
228
+
203
229
  describe('parseInclude', () => {
204
230
  test('parses comma-separated include values', () => {
205
231
  expect(FundingRoutes.parseInclude('quotes,quotes')).toEqual(['quotes'])