tapimo 0.4.4 → 0.5.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.
Files changed (162) hide show
  1. package/dist/App.d.ts +7403 -6127
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +4 -0
  4. package/dist/App.js.map +1 -1
  5. package/dist/Scope.d.ts +18 -1
  6. package/dist/Scope.d.ts.map +1 -1
  7. package/dist/Scope.js +24 -3
  8. package/dist/Scope.js.map +1 -1
  9. package/dist/admin/apps/verified-tokens.d.ts +115 -115
  10. package/dist/apps/data/App.d.ts +1838 -1534
  11. package/dist/apps/data/App.d.ts.map +1 -1
  12. package/dist/apps/data/App.js +135 -44
  13. package/dist/apps/data/App.js.map +1 -1
  14. package/dist/apps/data/{FundingRouteProviders.d.ts → FundingQuoteProviders.d.ts} +7 -7
  15. package/dist/apps/data/{FundingRouteProviders.d.ts.map → FundingQuoteProviders.d.ts.map} +1 -1
  16. package/dist/apps/data/{FundingRouteProviders.js → FundingQuoteProviders.js} +36 -91
  17. package/dist/apps/data/FundingQuoteProviders.js.map +1 -0
  18. package/dist/apps/data/index.d.ts +1 -1
  19. package/dist/apps/data/index.js +1 -1
  20. package/dist/apps/data/routes/activities.d.ts +242 -242
  21. package/dist/apps/data/routes/addresses.d.ts +227 -227
  22. package/dist/apps/data/routes/balances.d.ts +12 -12
  23. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  24. package/dist/apps/data/routes/balances.js +67 -149
  25. package/dist/apps/data/routes/balances.js.map +1 -1
  26. package/dist/apps/data/routes/blocks.d.ts +125 -126
  27. package/dist/apps/data/routes/blocks.d.ts.map +1 -1
  28. package/dist/apps/data/routes/blocks.js +3 -5
  29. package/dist/apps/data/routes/blocks.js.map +1 -1
  30. package/dist/apps/data/routes/coingecko.d.ts +55 -54
  31. package/dist/apps/data/routes/coingecko.d.ts.map +1 -1
  32. package/dist/apps/data/routes/coingecko.js +3 -8
  33. package/dist/apps/data/routes/coingecko.js.map +1 -1
  34. package/dist/apps/data/routes/funding.d.ts +207 -388
  35. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  36. package/dist/apps/data/routes/funding.js +143 -221
  37. package/dist/apps/data/routes/funding.js.map +1 -1
  38. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  39. package/dist/apps/data/routes/rpc.js +86 -16
  40. package/dist/apps/data/routes/rpc.js.map +1 -1
  41. package/dist/apps/data/routes/tokenlist.d.ts +33 -33
  42. package/dist/apps/data/routes/transactions.d.ts +281 -281
  43. package/dist/apps/data/routes/transfers.d.ts +58 -58
  44. package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
  45. package/dist/apps/data/routes/webhooks.d.ts +340 -0
  46. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  47. package/dist/apps/data/routes/webhooks.js +13 -1
  48. package/dist/apps/data/routes/webhooks.js.map +1 -1
  49. package/dist/apps/index.d.ts +1 -1
  50. package/dist/apps/index.js +1 -1
  51. package/dist/apps/management/App.d.ts +1090 -108
  52. package/dist/apps/management/App.d.ts.map +1 -1
  53. package/dist/apps/management/App.js +4 -0
  54. package/dist/apps/management/App.js.map +1 -1
  55. package/dist/apps/management/routes/api-keys.d.ts +118 -118
  56. package/dist/apps/management/routes/api-keys.js +2 -2
  57. package/dist/apps/management/routes/api-keys.js.map +1 -1
  58. package/dist/apps/management/routes/billing.d.ts +207 -207
  59. package/dist/apps/management/routes/invite-links.d.ts +1100 -0
  60. package/dist/apps/management/routes/invite-links.d.ts.map +1 -0
  61. package/dist/apps/management/routes/invite-links.js +411 -0
  62. package/dist/apps/management/routes/invite-links.js.map +1 -0
  63. package/dist/apps/management/routes/me.d.ts +17 -17
  64. package/dist/apps/management/routes/members.d.ts +71 -71
  65. package/dist/apps/management/routes/orgs.d.ts +12 -12
  66. package/dist/apps/management/routes/projects.d.ts +70 -70
  67. package/dist/apps/management/routes/scopes.d.ts +11 -11
  68. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  69. package/dist/apps/management/routes/scopes.js +2 -2
  70. package/dist/apps/management/routes/scopes.js.map +1 -1
  71. package/dist/apps/management/routes/usage.d.ts +75 -75
  72. package/dist/apps/relay/App.d.ts.map +1 -1
  73. package/dist/apps/relay/App.js +42 -4
  74. package/dist/apps/relay/App.js.map +1 -1
  75. package/dist/db/Db.d.ts +3 -0
  76. package/dist/db/Db.d.ts.map +1 -1
  77. package/dist/db/Db.js +57 -0
  78. package/dist/db/Db.js.map +1 -1
  79. package/dist/db/Schema.d.ts +31 -0
  80. package/dist/db/Schema.d.ts.map +1 -1
  81. package/dist/db/Schema.js +31 -0
  82. package/dist/db/Schema.js.map +1 -1
  83. package/dist/db/tables/inviteLinks.d.ts +42 -0
  84. package/dist/db/tables/inviteLinks.d.ts.map +1 -0
  85. package/dist/db/tables/inviteLinks.js +148 -0
  86. package/dist/db/tables/inviteLinks.js.map +1 -0
  87. package/dist/db/tables/organizations.d.ts.map +1 -1
  88. package/dist/db/tables/organizations.js +2 -0
  89. package/dist/db/tables/organizations.js.map +1 -1
  90. package/dist/handlers/relay.d.ts.map +1 -1
  91. package/dist/handlers/relay.js +13 -3
  92. package/dist/handlers/relay.js.map +1 -1
  93. package/dist/internal/Auth.d.ts +9 -1
  94. package/dist/internal/Auth.d.ts.map +1 -1
  95. package/dist/internal/Auth.js +130 -10
  96. package/dist/internal/Auth.js.map +1 -1
  97. package/dist/internal/EdgeCache.d.ts +3 -1
  98. package/dist/internal/EdgeCache.d.ts.map +1 -1
  99. package/dist/internal/EdgeCache.js +17 -2
  100. package/dist/internal/EdgeCache.js.map +1 -1
  101. package/dist/internal/{FundingRoutes.d.ts → FundingQuotes.d.ts} +185 -428
  102. package/dist/internal/FundingQuotes.d.ts.map +1 -0
  103. package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +93 -218
  104. package/dist/internal/FundingQuotes.js.map +1 -0
  105. package/dist/internal/Viem.d.ts +3 -1
  106. package/dist/internal/Viem.d.ts.map +1 -1
  107. package/dist/internal/Viem.js +11 -2
  108. package/dist/internal/Viem.js.map +1 -1
  109. package/dist/internal/index.d.ts +1 -1
  110. package/dist/internal/index.js +1 -1
  111. package/package.json +1 -1
  112. package/src/ApiKeys.test.ts +23 -0
  113. package/src/App.test-d.ts +2 -0
  114. package/src/App.ts +6 -0
  115. package/src/Client.test-d.ts +20 -11
  116. package/src/Scope.test.ts +1 -0
  117. package/src/Scope.ts +30 -4
  118. package/src/apps/data/App.test.ts +354 -0
  119. package/src/apps/data/App.ts +159 -52
  120. package/src/apps/data/FundingQuoteProviders.test.ts +151 -0
  121. package/src/apps/data/{FundingRouteProviders.ts → FundingQuoteProviders.ts} +40 -101
  122. package/src/apps/data/index.ts +1 -1
  123. package/src/apps/data/routes/balances.test.ts +64 -18
  124. package/src/apps/data/routes/balances.ts +76 -177
  125. package/src/apps/data/routes/blocks.ts +3 -5
  126. package/src/apps/data/routes/coingecko.ts +3 -8
  127. package/src/apps/data/routes/funding.test.ts +151 -206
  128. package/src/apps/data/routes/funding.ts +156 -291
  129. package/src/apps/data/routes/indexer.test.ts +97 -1
  130. package/src/apps/data/routes/rpc.test.ts +219 -0
  131. package/src/apps/data/routes/rpc.ts +92 -17
  132. package/src/apps/data/routes/webhooks.test.ts +21 -0
  133. package/src/apps/data/routes/webhooks.ts +13 -1
  134. package/src/apps/index.ts +1 -1
  135. package/src/apps/management/App.ts +4 -0
  136. package/src/apps/management/routes/api-keys.test.ts +47 -3
  137. package/src/apps/management/routes/api-keys.ts +2 -2
  138. package/src/apps/management/routes/invite-links.test.ts +385 -0
  139. package/src/apps/management/routes/invite-links.ts +570 -0
  140. package/src/apps/management/routes/scopes.test.ts +3 -2
  141. package/src/apps/management/routes/scopes.ts +4 -2
  142. package/src/apps/relay/App.test.ts +324 -4
  143. package/src/apps/relay/App.ts +40 -4
  144. package/src/db/Db.ts +65 -0
  145. package/src/db/Schema.ts +39 -0
  146. package/src/db/tables/inviteLinks.ts +187 -0
  147. package/src/db/tables/organizations.ts +2 -0
  148. package/src/handlers/relay.ts +16 -4
  149. package/src/internal/Auth.test.ts +132 -22
  150. package/src/internal/Auth.ts +171 -14
  151. package/src/internal/EdgeCache.test.ts +24 -3
  152. package/src/internal/EdgeCache.ts +18 -2
  153. package/src/internal/FundingQuotes.test.ts +174 -0
  154. package/src/internal/{FundingRoutes.ts → FundingQuotes.ts} +153 -312
  155. package/src/internal/Viem.ts +14 -5
  156. package/src/internal/index.test.ts +1 -1
  157. package/src/internal/index.ts +1 -1
  158. package/dist/apps/data/FundingRouteProviders.js.map +0 -1
  159. package/dist/internal/FundingRoutes.d.ts.map +0 -1
  160. package/dist/internal/FundingRoutes.js.map +0 -1
  161. package/src/apps/data/FundingRouteProviders.test.ts +0 -225
  162. package/src/internal/FundingRoutes.test.ts +0 -213
@@ -7,7 +7,7 @@ import { matchedRoutes } from 'hono/route'
7
7
  import type { MergePath, RouterRoute, Schema as core_Schema } from 'hono/types'
8
8
  import { findTargetHandler } from 'hono/utils/handler'
9
9
  import { decodeJwt } from 'jose'
10
- import { Credential, type Method } from 'mppx'
10
+ import { Challenge, Credential, type Method } from 'mppx'
11
11
  import { Mppx, tempo } from 'mppx/hono'
12
12
  import { Addresses } from 'viem/tempo'
13
13
  import * as z from 'zod/mini'
@@ -31,6 +31,7 @@ import * as Users from '../db/tables/users.js'
31
31
  import * as Viem from './Viem.js'
32
32
 
33
33
  const policySymbol = Symbol('tempo-api.auth.policy')
34
+ const mppManagementSymbol = Symbol('tempo-api.auth.mpp-management')
34
35
  const defaultMppRateLimit = { perMinute: 100 } satisfies RateLimit.Limit
35
36
 
36
37
  /** Auth context consumed by Tempo API handlers. */
@@ -112,6 +113,8 @@ type MppHandler = {
112
113
  rateLimit?: RateLimit.Limit | undefined
113
114
  /** Creates a Hono middleware for a Tempo Session payment. */
114
115
  session(options: Mpp.Session): MiddlewareHandler
116
+ /** Resolves the chain from a challenge issued by this server. */
117
+ sessionChainId?: ((c: hono_Context) => number | undefined) | undefined
115
118
  }
116
119
 
117
120
  /**
@@ -282,7 +285,7 @@ export declare namespace ensureOrg {
282
285
  }
283
286
 
284
287
  /**
285
- * Guards `:projectId` routes and resolves the project. Flat routes authorize against its organization; nested routes preserve the scope from {@link ensureOrg}.
288
+ * Resolves `:projectId`, preserving nested organization scope. Sessions require membership; API keys require organization ownership and matching project attribution when present.
286
289
  */
287
290
  export function ensureProject(): MiddlewareHandler {
288
291
  // Typed loosely (like {@link policy}) so route chains keep their inferred
@@ -306,6 +309,12 @@ export function ensureProject(): MiddlewareHandler {
306
309
  const db = Db.get(contextDb(c))
307
310
  const record = await Projects.get(db, projectId)
308
311
  if (!record) return notFound()
312
+ if (
313
+ principal.type === 'api_key' &&
314
+ principal.projectId !== undefined &&
315
+ principal.projectId !== record.id
316
+ )
317
+ return notFound()
309
318
  const scoped = c.get('org')
310
319
  if (scoped) {
311
320
  if (record.orgId !== scoped.id) return notFound()
@@ -479,6 +488,7 @@ export function middleware<
479
488
  session: session_ = {},
480
489
  ...mpp
481
490
  } = options.mpp === false ? {} : (options.mpp ?? {})
491
+ const secretKey = resolveMppSecretKey(mpp.secretKey)
482
492
  const defaults = laneDefaults(options)
483
493
  // Default-deny: no lanes open until a route grants them via `policy`.
484
494
  // Otherwise the `super_admin` principal bypasses lane checks entirely.
@@ -520,6 +530,7 @@ export function middleware<
520
530
  rateLimit: mppRateLimit,
521
531
  session: Mppx.create({
522
532
  ...mpp,
533
+ ...(secretKey ? { secretKey } : {}),
523
534
  // Defaults only; global `session` options may override, including
524
535
  // enabling same-route HEAD bootstrap.
525
536
  methods: [
@@ -532,6 +543,7 @@ export function middleware<
532
543
  }),
533
544
  ],
534
545
  }).session,
546
+ sessionChainId: (c) => paymentSessionChainId(c, secretKey),
535
547
  },
536
548
  }),
537
549
  rateLimit: RateLimit.memory(options.rateLimit),
@@ -545,17 +557,19 @@ export function middleware<
545
557
  const route = openApiRoute(c)
546
558
  if (!route) return next()
547
559
 
548
- const { key, description } = route
560
+ const { key, description, management, scope } = route
549
561
  const overrides = options.overrides as
550
562
  | Record<string, PolicyOverride | false | undefined>
551
563
  | undefined
552
- const endpointPolicy = resolvePolicy(
553
- base,
554
- defaults,
555
- [...policies(c), overrides?.[key]],
556
- description,
564
+ const configuredOverride = overrides?.[key]
565
+ const routeOverride =
566
+ management && configuredOverride ? { mpp: configuredOverride.mpp } : configuredOverride
567
+ const endpointPolicy = bindMppScope(
568
+ resolvePolicy(base, defaults, [...policies(c), routeOverride], description),
569
+ scope,
557
570
  )
558
571
  if (!endpointPolicy) return next()
572
+ if (management && (!auth.mpp || !endpointPolicy.mpp)) return next()
559
573
 
560
574
  c.set('auth', auth)
561
575
  return require<environment>(endpointPolicy)(c, next)
@@ -1002,6 +1016,55 @@ export function policy(options: PolicyOverride): MiddlewareHandler {
1002
1016
  return middleware
1003
1017
  }
1004
1018
 
1019
+ /** Adds management POSTs for OpenAPI-registered MPP GET resources. */
1020
+ export function installMppManagementRoutes(
1021
+ app: Hono<any, any>,
1022
+ options: MppManagementOptions = {},
1023
+ ) {
1024
+ const routes = [...app.routes]
1025
+ const postPaths = new Set(
1026
+ routes.filter((route) => route.method === 'POST').map((route) => route.path),
1027
+ )
1028
+ const targets = new Map<string, MppManagementTarget>()
1029
+
1030
+ for (const route of routes) {
1031
+ if (route.method !== 'GET') continue
1032
+ const target = targets.get(route.path) ?? { path: route.path }
1033
+ const override = policyFromRoute(route)
1034
+ if (override?.mpp !== undefined)
1035
+ target.mpp = resolveMppLane(target.mpp, override.mpp, { session: {} })
1036
+
1037
+ const spec = openApiSpec(route) as MppManagementSpec | undefined
1038
+ if (spec) {
1039
+ target.registered = true
1040
+ const description = typeof spec.summary === 'string' ? spec.summary : spec.description
1041
+ if (typeof description === 'string') target.description = description
1042
+ }
1043
+ targets.set(route.path, target)
1044
+ }
1045
+
1046
+ for (const target of targets.values()) {
1047
+ const key = `GET ${stripBasePath(normalizeRoutePath(target.path), options.basePath)}`
1048
+ const override = options.overrides?.[key]
1049
+ if (override === false) continue
1050
+ const mpp =
1051
+ override?.mpp === undefined
1052
+ ? target.mpp
1053
+ : resolveMppLane(target.mpp, override.mpp, { session: {} })
1054
+ if (!target.registered || !mpp) continue
1055
+ if (postPaths.has(target.path))
1056
+ throw new Error(`MPP management POST conflicts with an existing route: ${target.path}`)
1057
+
1058
+ const management = policy({ mpp }) as MiddlewareHandler & MppManagementHandler
1059
+ management[mppManagementSymbol] = { description: target.description }
1060
+ app.post(
1061
+ stripBasePath(target.path, options.basePath),
1062
+ management,
1063
+ OpenApi.describeRoute({ hide: true }),
1064
+ )
1065
+ }
1066
+ }
1067
+
1005
1068
  export declare namespace endpoint {
1006
1069
  /**
1007
1070
  * HTTP endpoint key inferred from a Hono app schema. Distributes over the
@@ -1048,12 +1111,35 @@ type OpenApiHandler = {
1048
1111
  type OpenApiRoute = {
1049
1112
  key: string
1050
1113
  description: string | undefined
1114
+ management: boolean
1115
+ scope: string
1051
1116
  }
1052
1117
 
1053
1118
  type PolicyHandler = {
1054
1119
  [policySymbol]?: PolicyOverride | undefined
1055
1120
  }
1056
1121
 
1122
+ type MppManagementHandler = {
1123
+ [mppManagementSymbol]?: { description?: string | undefined } | undefined
1124
+ }
1125
+
1126
+ type MppManagementOptions = {
1127
+ basePath?: string | undefined
1128
+ overrides?: Record<string, PolicyOverride | false | undefined> | undefined
1129
+ }
1130
+
1131
+ type MppManagementSpec = {
1132
+ description?: unknown
1133
+ summary?: unknown
1134
+ }
1135
+
1136
+ type MppManagementTarget = {
1137
+ description?: string | undefined
1138
+ mpp?: NonNullable<require.Policy['mpp']> | undefined
1139
+ path: string
1140
+ registered?: boolean | undefined
1141
+ }
1142
+
1057
1143
  function laneDefaults(options: middleware.Options): LaneDefaults {
1058
1144
  const apiKey = options.apiKey === false ? undefined : options.apiKey
1059
1145
  const public_ = options.public === false ? undefined : options.public
@@ -1112,6 +1198,17 @@ function resolvePolicy(
1112
1198
  }
1113
1199
  }
1114
1200
 
1201
+ function bindMppScope(policy: require.Policy | undefined, scope: string) {
1202
+ if (!policy?.mpp || policy.mpp.session.scope !== undefined) return policy
1203
+ return {
1204
+ ...policy,
1205
+ mpp: {
1206
+ ...policy.mpp,
1207
+ session: { ...policy.mpp.session, scope },
1208
+ },
1209
+ }
1210
+ }
1211
+
1115
1212
  function resolveApiKeyLane(
1116
1213
  current: require.Policy['apiKey'],
1117
1214
  override: PolicyOverride['apiKey'],
@@ -1165,9 +1262,19 @@ function resolveSessionLane(
1165
1262
  }
1166
1263
 
1167
1264
  function openApiRoute(c: hono_Context): OpenApiRoute | undefined {
1168
- const route = matchedRoutes(c)
1169
- .slice(c.req.routeIndex + 1)
1170
- .find((route) => !!openApiSpec(route))
1265
+ const routes = matchedRoutes(c).slice(c.req.routeIndex + 1)
1266
+ const managementRoute = routes.find((route) => !!mppManagementFromRoute(route))
1267
+ if (managementRoute) {
1268
+ const management = mppManagementFromRoute(managementRoute)!
1269
+ return {
1270
+ key: endpointKey(c, managementRoute, 'GET'),
1271
+ description: management.description,
1272
+ management: true,
1273
+ scope: `GET ${managementRoute.path}`,
1274
+ }
1275
+ }
1276
+
1277
+ const route = routes.find((route) => !!openApiSpec(route))
1171
1278
  if (!route) return undefined
1172
1279
 
1173
1280
  const spec = openApiSpec(route)
@@ -1177,6 +1284,8 @@ function openApiRoute(c: hono_Context): OpenApiRoute | undefined {
1177
1284
  return {
1178
1285
  key: endpointKey(c, route),
1179
1286
  description: typeof description === 'string' ? description : undefined,
1287
+ management: false,
1288
+ scope: `${route.method.toUpperCase()} ${route.path}`,
1180
1289
  }
1181
1290
  }
1182
1291
 
@@ -1199,6 +1308,11 @@ function policyFromRoute(route: RouterRoute) {
1199
1308
  return handler[policySymbol]
1200
1309
  }
1201
1310
 
1311
+ function mppManagementFromRoute(route: RouterRoute) {
1312
+ const handler = findTargetHandler(route.handler) as MppManagementHandler
1313
+ return handler[mppManagementSymbol]
1314
+ }
1315
+
1202
1316
  /** Resolved access lanes for one documented operation. */
1203
1317
  export type OperationAccess = {
1204
1318
  /** Whether API-key access is allowed. */
@@ -1253,8 +1367,8 @@ export function describeAccess(app: Hono<any, any>): Record<string, OperationAcc
1253
1367
  return access
1254
1368
  }
1255
1369
 
1256
- function endpointKey(c: hono_Context, route: RouterRoute) {
1257
- return `${route.method.toUpperCase()} ${stripBasePath(normalizeRoutePath(route.path), basePath(c))}`
1370
+ function endpointKey(c: hono_Context, route: RouterRoute, method = route.method) {
1371
+ return `${method.toUpperCase()} ${stripBasePath(normalizeRoutePath(route.path), basePath(c))}`
1258
1372
  }
1259
1373
 
1260
1374
  function normalizeRoutePath(path: string) {
@@ -1695,6 +1809,40 @@ function hasPaymentCredential(c: hono_Context) {
1695
1809
  return !!Credential.extractPaymentScheme(authorization)
1696
1810
  }
1697
1811
 
1812
+ function paymentSessionChainId(c: hono_Context, secretKey: string | undefined): number | undefined {
1813
+ if (!secretKey) return undefined
1814
+ try {
1815
+ const credential = Credential.fromRequest(c.req.raw)
1816
+ if (!Challenge.verify(credential.challenge, { secretKey })) return undefined
1817
+ if (credential.challenge.method !== 'tempo' || credential.challenge.intent !== 'session')
1818
+ return undefined
1819
+ const details = credential.challenge.request['methodDetails']
1820
+ if (!details || typeof details !== 'object') return undefined
1821
+ const chainId = (details as Record<string, unknown>)['chainId']
1822
+ return typeof chainId === 'number' ? chainId : undefined
1823
+ } catch {
1824
+ return undefined
1825
+ }
1826
+ }
1827
+
1828
+ function resolveMppSecretKey(explicit: string | undefined): string | undefined {
1829
+ if (explicit) return explicit
1830
+ try {
1831
+ const value = typeof process === 'undefined' ? undefined : process.env['MPP_SECRET_KEY']
1832
+ if (value) return value
1833
+ } catch {}
1834
+ try {
1835
+ const deno = (
1836
+ globalThis as typeof globalThis & {
1837
+ Deno?: { env?: { get?: (name: string) => string | undefined } | undefined } | undefined
1838
+ }
1839
+ ).Deno
1840
+ return deno?.env?.get?.('MPP_SECRET_KEY') || undefined
1841
+ } catch {
1842
+ return undefined
1843
+ }
1844
+ }
1845
+
1698
1846
  function isPaymentEnabled(auth: Context, policy: require.Policy) {
1699
1847
  return !!auth.mpp && !!policy.mpp
1700
1848
  }
@@ -1773,7 +1921,16 @@ async function requirePayment<environment extends Environment>(
1773
1921
  }
1774
1922
  }
1775
1923
 
1776
- const middleware = options.auth.mpp?.session(options.policy.mpp?.session ?? {})
1924
+ const session = options.policy.mpp?.session ?? {}
1925
+ const chainId =
1926
+ explicitChainId(c) ??
1927
+ (c.req.method === 'POST' && session.scope?.startsWith('GET ')
1928
+ ? options.auth.mpp?.sessionChainId?.(c)
1929
+ : undefined)
1930
+ const middleware = options.auth.mpp?.session({
1931
+ ...session,
1932
+ ...(chainId === undefined ? {} : { chainId }),
1933
+ })
1777
1934
  if (!middleware)
1778
1935
  return jsonError(c, {
1779
1936
  code: 'payment_required',
@@ -1,4 +1,4 @@
1
- import { Hono } from 'hono'
1
+ import { type Context, Hono } from 'hono'
2
2
  import { createMiddleware } from 'hono/factory'
3
3
  import * as EdgeCache from './EdgeCache.js'
4
4
  import type * as Store from './Store.js'
@@ -189,6 +189,19 @@ describe('middleware', () => {
189
189
 
190
190
  expect(downstreamCalls()).toMatchInlineSnapshot(`2`)
191
191
  })
192
+
193
+ test('rechecks route eligibility before every cache lookup', async () => {
194
+ let eligible = true
195
+ const { app, downstreamCalls, store } = createApp({ eligibility: () => eligible })
196
+
197
+ await app.request('/cached')
198
+ eligible = false
199
+ await app.request('/cached')
200
+
201
+ expect(downstreamCalls()).toBe(2)
202
+ expect(store.gets).toBe(1)
203
+ expect(store.size).toBe(1)
204
+ })
192
205
  })
193
206
 
194
207
  type Environment = { Variables: EdgeCache.Variables }
@@ -221,7 +234,12 @@ function memoryStore() {
221
234
  } satisfies Store.Store & { readonly gets: number; readonly size: number }
222
235
  }
223
236
 
224
- function createApp(options: { store?: Store.Store | undefined } = {}) {
237
+ function createApp(
238
+ options: {
239
+ eligibility?: ((c: Context<Environment>) => boolean) | undefined
240
+ store?: Store.Store | undefined
241
+ } = {},
242
+ ) {
225
243
  const store = memoryStore()
226
244
  const edgeStore = 'store' in options ? options.store : store
227
245
  let downstream = 0
@@ -234,9 +252,12 @@ function createApp(options: { store?: Store.Store | undefined } = {}) {
234
252
  })
235
253
  // Routes publishing a policy mount a marked middleware, as `Cache.response`
236
254
  // routes do; unmarked routes must skip the lookup entirely.
237
- const cacheable = EdgeCache.markCacheable(
255
+ const cacheable_marked = EdgeCache.markCacheable(
238
256
  createMiddleware<Environment>(async (_c, next) => next()),
239
257
  )
258
+ const cacheable = options.eligibility
259
+ ? EdgeCache.setEligibility(cacheable_marked, options.eligibility)
260
+ : cacheable_marked
240
261
  app.get('/cached', cacheable, (c) => {
241
262
  c.set('edgeCache', { maxAge: 30, staleWhileRevalidate: 300 })
242
263
  c.header('Age', '123')
@@ -135,6 +135,7 @@ export function middleware<environment extends Env>(
135
135
  // Marks handlers whose route publishes an edge-cache policy; the middleware
136
136
  // pays the store lookup only when a matched handler carries it.
137
137
  const cacheableSymbol = Symbol('tempoApi.edgeCacheable')
138
+ const eligibility = new WeakMap<object, (c: Context) => boolean>()
138
139
 
139
140
  /**
140
141
  * Marks a route handler/middleware as publishing an edge-cache policy, so
@@ -147,9 +148,24 @@ export function markCacheable<const handler extends (...args: never[]) => unknow
147
148
  return Object.assign(handler, { [cacheableSymbol]: true })
148
149
  }
149
150
 
150
- // True when any handler matched for this request is marked cacheable.
151
+ /** Adds a request eligibility check to a cacheable route handler. */
152
+ export function setEligibility<const handler extends (...args: never[]) => unknown>(
153
+ handler: handler,
154
+ predicate: (c: Context) => boolean,
155
+ ): handler {
156
+ if (cacheableSymbol in handler) eligibility.set(handler, predicate)
157
+ return handler
158
+ }
159
+
160
+ // True when a marked handler matches and every marked handler permits caching.
151
161
  function isCacheableRoute(c: Context): boolean {
152
- return matchedRoutes(c).some((route) => cacheableSymbol in route.handler)
162
+ let cacheable = false
163
+ for (const route of matchedRoutes(c)) {
164
+ if (!(cacheableSymbol in route.handler)) continue
165
+ cacheable = true
166
+ if (eligibility.get(route.handler)?.(c) === false) return false
167
+ }
168
+ return cacheable
153
169
  }
154
170
 
155
171
  export declare namespace middleware {
@@ -0,0 +1,174 @@
1
+ import * as FundingQuotes from './FundingQuotes.js'
2
+
3
+ const baseUsdcRequest = {
4
+ provider: 'relay',
5
+ sourceAmount: '1000000',
6
+ sourceAmountUnits: 'baseUnits',
7
+ sourceChain: 'base',
8
+ sourceToken: 'USDC',
9
+ } as const
10
+
11
+ describe('listQuotes', () => {
12
+ test('returns normalized successful provider quotes', async () => {
13
+ let seen: FundingQuotes.QuoteProbeInput | undefined
14
+ const quotes = await FundingQuotes.listQuotes({
15
+ ...baseUsdcRequest,
16
+ adapters: {
17
+ relay: {
18
+ async probeQuote(input, signal) {
19
+ seen = input
20
+ expect(signal.aborted).toBe(false)
21
+ return {
22
+ minimumOutputAmount: '980000',
23
+ outputAmount: '990000',
24
+ quality: {
25
+ estimatedSeconds: 4,
26
+ liquiditySource: 'providerQuote',
27
+ sourceDetail: 'providerEndpoint',
28
+ tier: 'liquid',
29
+ },
30
+ sampledAt: '2026-01-01T00:00:00Z',
31
+ status: 'available',
32
+ }
33
+ },
34
+ },
35
+ },
36
+ })
37
+
38
+ expect(seen?.candidate.id).toBe('base-usdc-tempo-usdce-relay')
39
+ expect(quotes).toHaveLength(1)
40
+ expect(quotes[0]).toMatchObject({
41
+ id: 'base-usdc-tempo-usdce-relay',
42
+ quality: {
43
+ liquiditySource: 'providerQuote',
44
+ tier: 'liquid',
45
+ },
46
+ quote: {
47
+ inputAmount: '1000000',
48
+ minimumOutputAmount: '980000',
49
+ outputAmount: '990000',
50
+ outputAmountFormatted: '0.99',
51
+ },
52
+ })
53
+ })
54
+
55
+ test('returns no static candidates when no adapter is configured', async () => {
56
+ await expect(FundingQuotes.listQuotes(baseUsdcRequest)).resolves.toEqual([])
57
+ })
58
+
59
+ test('omits provider-declared unavailable quotes', async () => {
60
+ const quotes = FundingQuotes.listQuotes({
61
+ ...baseUsdcRequest,
62
+ adapters: {
63
+ relay: {
64
+ async probeQuote() {
65
+ return { sampledAt: '2026-01-01T00:00:00Z', status: 'unavailable' }
66
+ },
67
+ },
68
+ },
69
+ })
70
+
71
+ await expect(quotes).resolves.toEqual([])
72
+ })
73
+
74
+ test('rejects provider failures when no quote succeeds', async () => {
75
+ const quotes = FundingQuotes.listQuotes({
76
+ ...baseUsdcRequest,
77
+ adapters: {
78
+ relay: {
79
+ async probeQuote() {
80
+ throw new Error('upstream unavailable')
81
+ },
82
+ },
83
+ },
84
+ })
85
+
86
+ await expect(quotes).rejects.toThrowErrorMatchingInlineSnapshot(
87
+ `[FundingQuotes.ProviderQuoteError]`,
88
+ )
89
+ })
90
+
91
+ test('rejects successful provider responses without output amounts', async () => {
92
+ const quotes = FundingQuotes.listQuotes({
93
+ ...baseUsdcRequest,
94
+ adapters: {
95
+ relay: {
96
+ async probeQuote() {
97
+ return { sampledAt: '2026-01-01T00:00:00Z', status: 'available' }
98
+ },
99
+ },
100
+ },
101
+ })
102
+
103
+ await expect(quotes).rejects.toThrowErrorMatchingInlineSnapshot(
104
+ `[FundingQuotes.ProviderQuoteError]`,
105
+ )
106
+ })
107
+
108
+ test('aborts providers that exceed the quote deadline', async () => {
109
+ let aborted = false
110
+ const quotes = FundingQuotes.listQuotes({
111
+ ...baseUsdcRequest,
112
+ adapterTimeoutMs: 1,
113
+ adapters: {
114
+ relay: {
115
+ probeQuote(_input, signal) {
116
+ signal.addEventListener('abort', () => {
117
+ aborted = true
118
+ })
119
+ return new Promise<FundingQuotes.QuoteProbeResult>(() => {})
120
+ },
121
+ },
122
+ },
123
+ })
124
+
125
+ await expect(quotes).rejects.toThrowErrorMatchingInlineSnapshot(
126
+ `[FundingQuotes.ProviderQuoteError]`,
127
+ )
128
+ expect(aborted).toBe(true)
129
+ })
130
+
131
+ test('passes cache policy through to providers', async () => {
132
+ let seen: FundingQuotes.QuoteProbeInput | undefined
133
+ await FundingQuotes.listQuotes({
134
+ ...baseUsdcRequest,
135
+ adapters: {
136
+ relay: {
137
+ async probeQuote(input) {
138
+ seen = input
139
+ return { sampledAt: '2026-01-01T00:00:00Z', status: 'unavailable' }
140
+ },
141
+ },
142
+ },
143
+ cache: { bypass: true },
144
+ })
145
+
146
+ expect(seen?.cache.bypass).toBe(true)
147
+ })
148
+ })
149
+
150
+ describe('listChains', () => {
151
+ test('returns only chain and token inputs with quote inventory', () => {
152
+ expect(FundingQuotes.listChains().map((chain) => chain.id)).toEqual([
153
+ 'eip155:1',
154
+ 'eip155:8453',
155
+ 'eip155:56',
156
+ ])
157
+ })
158
+ })
159
+
160
+ describe('listProviders', () => {
161
+ test('reports deployment configuration', () => {
162
+ expect(FundingQuotes.listProviders()).toMatchInlineSnapshot(`
163
+ [
164
+ {
165
+ "id": "relay",
166
+ "kind": "solverNetwork",
167
+ "name": "Relay",
168
+ "requiresApiKey": false,
169
+ "status": "unconfigured",
170
+ },
171
+ ]
172
+ `)
173
+ })
174
+ })