tapimo 0.4.3 → 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 (192) hide show
  1. package/dist/App.d.ts +7222 -6316
  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 +116 -116
  10. package/dist/apps/data/App.d.ts +2541 -2607
  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/FundingQuoteProviders.d.ts +36 -0
  15. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -0
  16. package/dist/apps/data/FundingQuoteProviders.js +190 -0
  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/exchanges.d.ts +455 -455
  35. package/dist/apps/data/routes/fee-amm.d.ts +92 -92
  36. package/dist/apps/data/routes/funding.d.ts +91 -864
  37. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  38. package/dist/apps/data/routes/funding.js +138 -450
  39. package/dist/apps/data/routes/funding.js.map +1 -1
  40. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  41. package/dist/apps/data/routes/rpc.js +86 -16
  42. package/dist/apps/data/routes/rpc.js.map +1 -1
  43. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  44. package/dist/apps/data/routes/webhooks.d.ts +340 -0
  45. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  46. package/dist/apps/data/routes/webhooks.js +13 -1
  47. package/dist/apps/data/routes/webhooks.js.map +1 -1
  48. package/dist/apps/index.d.ts +1 -1
  49. package/dist/apps/index.js +1 -1
  50. package/dist/apps/management/App.d.ts +1843 -861
  51. package/dist/apps/management/App.d.ts.map +1 -1
  52. package/dist/apps/management/App.js +4 -0
  53. package/dist/apps/management/App.js.map +1 -1
  54. package/dist/apps/management/routes/api-keys.d.ts +121 -121
  55. package/dist/apps/management/routes/api-keys.js +2 -2
  56. package/dist/apps/management/routes/api-keys.js.map +1 -1
  57. package/dist/apps/management/routes/billing.d.ts +2 -2
  58. package/dist/apps/management/routes/invite-links.d.ts +1100 -0
  59. package/dist/apps/management/routes/invite-links.d.ts.map +1 -0
  60. package/dist/apps/management/routes/invite-links.js +411 -0
  61. package/dist/apps/management/routes/invite-links.js.map +1 -0
  62. package/dist/apps/management/routes/me.d.ts +13 -13
  63. package/dist/apps/management/routes/members.d.ts +77 -77
  64. package/dist/apps/management/routes/orgs.d.ts +96 -96
  65. package/dist/apps/management/routes/projects.d.ts +62 -62
  66. package/dist/apps/management/routes/scopes.d.ts +11 -11
  67. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  68. package/dist/apps/management/routes/scopes.js +2 -2
  69. package/dist/apps/management/routes/scopes.js.map +1 -1
  70. package/dist/apps/management/routes/usage.d.ts +79 -79
  71. package/dist/apps/mcp.d.ts.map +1 -1
  72. package/dist/apps/mcp.js +3 -1
  73. package/dist/apps/mcp.js.map +1 -1
  74. package/dist/apps/relay/App.d.ts.map +1 -1
  75. package/dist/apps/relay/App.js +42 -4
  76. package/dist/apps/relay/App.js.map +1 -1
  77. package/dist/cloudflare.d.ts +2 -0
  78. package/dist/cloudflare.d.ts.map +1 -1
  79. package/dist/cloudflare.js +4 -0
  80. package/dist/cloudflare.js.map +1 -1
  81. package/dist/db/Db.d.ts +3 -0
  82. package/dist/db/Db.d.ts.map +1 -1
  83. package/dist/db/Db.js +57 -0
  84. package/dist/db/Db.js.map +1 -1
  85. package/dist/db/Schema.d.ts +31 -0
  86. package/dist/db/Schema.d.ts.map +1 -1
  87. package/dist/db/Schema.js +31 -0
  88. package/dist/db/Schema.js.map +1 -1
  89. package/dist/db/tables/inviteLinks.d.ts +42 -0
  90. package/dist/db/tables/inviteLinks.d.ts.map +1 -0
  91. package/dist/db/tables/inviteLinks.js +148 -0
  92. package/dist/db/tables/inviteLinks.js.map +1 -0
  93. package/dist/db/tables/organizations.d.ts.map +1 -1
  94. package/dist/db/tables/organizations.js +2 -0
  95. package/dist/db/tables/organizations.js.map +1 -1
  96. package/dist/handlers/relay.d.ts.map +1 -1
  97. package/dist/handlers/relay.js +13 -3
  98. package/dist/handlers/relay.js.map +1 -1
  99. package/dist/internal/Auth.d.ts +42 -23
  100. package/dist/internal/Auth.d.ts.map +1 -1
  101. package/dist/internal/Auth.js +169 -20
  102. package/dist/internal/Auth.js.map +1 -1
  103. package/dist/internal/EdgeCache.d.ts +3 -1
  104. package/dist/internal/EdgeCache.d.ts.map +1 -1
  105. package/dist/internal/EdgeCache.js +17 -2
  106. package/dist/internal/EdgeCache.js.map +1 -1
  107. package/dist/internal/Errors.d.ts +6 -0
  108. package/dist/internal/Errors.d.ts.map +1 -0
  109. package/dist/internal/Errors.js +16 -0
  110. package/dist/internal/Errors.js.map +1 -0
  111. package/dist/internal/FundingQuotes.d.ts +644 -0
  112. package/dist/internal/FundingQuotes.d.ts.map +1 -0
  113. package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +93 -289
  114. package/dist/internal/FundingQuotes.js.map +1 -0
  115. package/dist/internal/Store.d.ts +61 -16
  116. package/dist/internal/Store.d.ts.map +1 -1
  117. package/dist/internal/Store.js +72 -17
  118. package/dist/internal/Store.js.map +1 -1
  119. package/dist/internal/Viem.d.ts +2 -0
  120. package/dist/internal/Viem.d.ts.map +1 -1
  121. package/dist/internal/Viem.js +11 -2
  122. package/dist/internal/Viem.js.map +1 -1
  123. package/dist/internal/index.d.ts +27 -0
  124. package/dist/internal/index.d.ts.map +1 -0
  125. package/dist/internal/index.js +27 -0
  126. package/dist/internal/index.js.map +1 -0
  127. package/package.json +8 -3
  128. package/src/ApiKeys.test.ts +23 -0
  129. package/src/App.test-d.ts +2 -0
  130. package/src/App.test.ts +60 -4
  131. package/src/App.ts +6 -0
  132. package/src/Client.test-d.ts +16 -54
  133. package/src/Scope.test.ts +1 -0
  134. package/src/Scope.ts +30 -4
  135. package/src/apps/data/App.test.ts +354 -0
  136. package/src/apps/data/App.ts +159 -52
  137. package/src/apps/data/FundingQuoteProviders.test.ts +151 -0
  138. package/src/apps/data/FundingQuoteProviders.ts +251 -0
  139. package/src/apps/data/index.ts +1 -1
  140. package/src/apps/data/routes/balances.test.ts +64 -18
  141. package/src/apps/data/routes/balances.ts +76 -177
  142. package/src/apps/data/routes/blocks.ts +3 -5
  143. package/src/apps/data/routes/coingecko.ts +3 -8
  144. package/src/apps/data/routes/funding.test.ts +149 -306
  145. package/src/apps/data/routes/funding.ts +154 -604
  146. package/src/apps/data/routes/indexer.test.ts +99 -3
  147. package/src/apps/data/routes/rpc.test.ts +219 -0
  148. package/src/apps/data/routes/rpc.ts +92 -17
  149. package/src/apps/data/routes/webhooks.test.ts +23 -2
  150. package/src/apps/data/routes/webhooks.ts +13 -1
  151. package/src/apps/index.ts +1 -1
  152. package/src/apps/management/App.ts +4 -0
  153. package/src/apps/management/routes/api-keys.test.ts +47 -3
  154. package/src/apps/management/routes/api-keys.ts +2 -2
  155. package/src/apps/management/routes/invite-links.test.ts +385 -0
  156. package/src/apps/management/routes/invite-links.ts +570 -0
  157. package/src/apps/management/routes/scopes.test.ts +3 -2
  158. package/src/apps/management/routes/scopes.ts +4 -2
  159. package/src/apps/mcp.test.ts +55 -14
  160. package/src/apps/mcp.ts +3 -1
  161. package/src/apps/relay/App.test.ts +324 -4
  162. package/src/apps/relay/App.ts +40 -4
  163. package/src/cloudflare.ts +5 -0
  164. package/src/db/Db.ts +65 -0
  165. package/src/db/Schema.ts +39 -0
  166. package/src/db/tables/inviteLinks.ts +187 -0
  167. package/src/db/tables/organizations.ts +2 -0
  168. package/src/handlers/relay.ts +16 -4
  169. package/src/internal/Auth.test.ts +528 -37
  170. package/src/internal/Auth.ts +246 -45
  171. package/src/internal/EdgeCache.test.ts +24 -3
  172. package/src/internal/EdgeCache.ts +18 -2
  173. package/src/internal/FundingQuotes.test.ts +174 -0
  174. package/src/internal/FundingQuotes.ts +735 -0
  175. package/src/internal/Log.test.ts +4 -4
  176. package/src/internal/Store.test-d.ts +15 -0
  177. package/src/internal/Store.test.ts +153 -2
  178. package/src/internal/Store.ts +157 -41
  179. package/src/internal/Viem.ts +14 -5
  180. package/src/internal/index.test.ts +34 -0
  181. package/src/internal/index.ts +26 -0
  182. package/dist/apps/data/FundingRouteProviders.d.ts +0 -143
  183. package/dist/apps/data/FundingRouteProviders.d.ts.map +0 -1
  184. package/dist/apps/data/FundingRouteProviders.js +0 -546
  185. package/dist/apps/data/FundingRouteProviders.js.map +0 -1
  186. package/dist/internal/FundingRoutes.d.ts +0 -1085
  187. package/dist/internal/FundingRoutes.d.ts.map +0 -1
  188. package/dist/internal/FundingRoutes.js.map +0 -1
  189. package/src/apps/data/FundingRouteProviders.test.ts +0 -485
  190. package/src/apps/data/FundingRouteProviders.ts +0 -744
  191. package/src/internal/FundingRoutes.test.ts +0 -239
  192. package/src/internal/FundingRoutes.ts +0 -1067
@@ -7,8 +7,9 @@ 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
+ import { Addresses } from 'viem/tempo'
12
13
  import * as z from 'zod/mini'
13
14
 
14
15
  import type * as App from '../App.js'
@@ -30,6 +31,8 @@ import * as Users from '../db/tables/users.js'
30
31
  import * as Viem from './Viem.js'
31
32
 
32
33
  const policySymbol = Symbol('tempo-api.auth.policy')
34
+ const mppManagementSymbol = Symbol('tempo-api.auth.mpp-management')
35
+ const defaultMppRateLimit = { perMinute: 100 } satisfies RateLimit.Limit
33
36
 
34
37
  /** Auth context consumed by Tempo API handlers. */
35
38
  export type Context = {
@@ -47,7 +50,7 @@ export type Context = {
47
50
  }
48
51
  | undefined
49
52
  /** MPP payment configuration. */
50
- mpp?: Mpp | undefined
53
+ mpp?: MppHandler | undefined
51
54
  /**
52
55
  * Configured default anonymous public quota. Used as the throttle ceiling for
53
56
  * sandbox keys without active billing on routes that expose no public lane
@@ -89,22 +92,29 @@ export type Payment = {
89
92
  type: 'mpp'
90
93
  }
91
94
 
92
- /** MPP charge options used by route access policies. */
93
- export type Charge = Method.RequestDefaults<ReturnType<typeof tempo.charge>> & {
94
- /** Optional human-readable payment description. */
95
- description?: string | undefined
96
- /** Optional challenge expiration timestamp or date. */
97
- expires?: Date | string | undefined
98
- /** Optional server-defined correlation data. */
99
- meta?: Record<string, string> | undefined
100
- /** Optional route/resource scope bound to the challenge. */
101
- scope?: string | undefined
95
+ /** MPP route-policy types. Distinct from the SIWE {@link Session} capability. */
96
+ export declare namespace Mpp {
97
+ /** MPP Session request options used by route access policies. */
98
+ type Session = Method.RequestDefaults<ReturnType<typeof tempo.session>> & {
99
+ /** Optional human-readable payment description. */
100
+ description?: string | undefined
101
+ /** Optional challenge expiration timestamp or date. */
102
+ expires?: Date | string | undefined
103
+ /** Optional server-defined correlation data. */
104
+ meta?: Record<string, string> | undefined
105
+ /** Optional route/resource scope bound to the challenge. */
106
+ scope?: string | undefined
107
+ }
102
108
  }
103
109
 
104
110
  /** MPP payment handler consumed by auth policies. */
105
- type Mpp = {
106
- /** Creates a Hono middleware for a Tempo charge. */
107
- charge(options: Charge): MiddlewareHandler
111
+ type MppHandler = {
112
+ /** Paid-request quota per access principal. */
113
+ rateLimit?: RateLimit.Limit | undefined
114
+ /** Creates a Hono middleware for a Tempo Session payment. */
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
108
118
  }
109
119
 
110
120
  /**
@@ -127,11 +137,13 @@ export type PolicyOverride = {
127
137
  }
128
138
  | boolean
129
139
  | undefined
130
- /** MPP paid lane: object to override the charge, or boolean to enable/disable. */
140
+ /** MPP paid lane: object to override the Session request, or boolean to enable/disable. */
131
141
  mpp?:
132
142
  | {
133
- /** Tempo charge override for this route. */
134
- charge?: Charge | undefined
143
+ /** Paid-request quota override for this route (a protective per-route cap). */
144
+ rateLimit?: RateLimit.Limit | undefined
145
+ /** Tempo Session request override for this route. */
146
+ session?: Mpp.Session | undefined
135
147
  }
136
148
  | boolean
137
149
  | undefined
@@ -273,7 +285,7 @@ export declare namespace ensureOrg {
273
285
  }
274
286
 
275
287
  /**
276
- * 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.
277
289
  */
278
290
  export function ensureProject(): MiddlewareHandler {
279
291
  // Typed loosely (like {@link policy}) so route chains keep their inferred
@@ -297,6 +309,12 @@ export function ensureProject(): MiddlewareHandler {
297
309
  const db = Db.get(contextDb(c))
298
310
  const record = await Projects.get(db, projectId)
299
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()
300
318
  const scoped = c.get('org')
301
319
  if (scoped) {
302
320
  if (record.orgId !== scoped.id) return notFound()
@@ -465,7 +483,12 @@ export function middleware<
465
483
  endpoint extends string = string,
466
484
  environment extends Environment = Environment,
467
485
  >(options: middleware.Options<endpoint>): MiddlewareHandler<environment> {
468
- const { charge = {}, ...mpp } = options.mpp === false ? {} : (options.mpp ?? {})
486
+ const {
487
+ rateLimit: mppRateLimit = defaultMppRateLimit,
488
+ session: session_ = {},
489
+ ...mpp
490
+ } = options.mpp === false ? {} : (options.mpp ?? {})
491
+ const secretKey = resolveMppSecretKey(mpp.secretKey)
469
492
  const defaults = laneDefaults(options)
470
493
  // Default-deny: no lanes open until a route grants them via `policy`.
471
494
  // Otherwise the `super_admin` principal bypasses lane checks entirely.
@@ -503,11 +526,25 @@ export function middleware<
503
526
  ...(options.mpp === false
504
527
  ? {}
505
528
  : {
506
- mpp: Mppx.create({
507
- ...mpp,
508
- methods: [tempo.charge({ amount: '0.001', ...charge })],
509
- secretKey: mpp.secretKey ?? crypto.randomUUID(),
510
- }),
529
+ mpp: {
530
+ rateLimit: mppRateLimit,
531
+ session: Mppx.create({
532
+ ...mpp,
533
+ ...(secretKey ? { secretKey } : {}),
534
+ // Defaults only; global `session` options may override, including
535
+ // enabling same-route HEAD bootstrap.
536
+ methods: [
537
+ tempo.session({
538
+ amount: '0.0001',
539
+ bootstrap: false,
540
+ currency: Addresses.pathUsd,
541
+ unitType: 'request',
542
+ ...session_,
543
+ }),
544
+ ],
545
+ }).session,
546
+ sessionChainId: (c) => paymentSessionChainId(c, secretKey),
547
+ },
511
548
  }),
512
549
  rateLimit: RateLimit.memory(options.rateLimit),
513
550
  ...(options.session ? { session: options.session } : {}),
@@ -520,17 +557,19 @@ export function middleware<
520
557
  const route = openApiRoute(c)
521
558
  if (!route) return next()
522
559
 
523
- const { key, description } = route
560
+ const { key, description, management, scope } = route
524
561
  const overrides = options.overrides as
525
562
  | Record<string, PolicyOverride | false | undefined>
526
563
  | undefined
527
- const endpointPolicy = resolvePolicy(
528
- base,
529
- defaults,
530
- [...policies(c), overrides?.[key]],
531
- 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,
532
570
  )
533
571
  if (!endpointPolicy) return next()
572
+ if (management && (!auth.mpp || !endpointPolicy.mpp)) return next()
534
573
 
535
574
  c.set('auth', auth)
536
575
  return require<environment>(endpointPolicy)(c, next)
@@ -600,8 +639,10 @@ export declare namespace middleware {
600
639
 
601
640
  /** MPP payment options used by auth middleware. */
602
641
  type MppOptions = Omit<NonNullable<Parameters<typeof Mppx.create>[0]>, 'methods'> & {
603
- /** Global Tempo charge options passed to the MPP method instantiator. */
604
- charge?: NonNullable<Parameters<typeof tempo.charge>[0]> | undefined
642
+ /** Paid-request quota per access principal. Defaults to 100 per minute. */
643
+ rateLimit?: RateLimit.Limit | undefined
644
+ /** Global Tempo Session options passed to the MPP method instantiator. */
645
+ session?: NonNullable<Parameters<typeof tempo.session>[0]> | undefined
605
646
  }
606
647
  }
607
648
 
@@ -975,6 +1016,55 @@ export function policy(options: PolicyOverride): MiddlewareHandler {
975
1016
  return middleware
976
1017
  }
977
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
+
978
1068
  export declare namespace endpoint {
979
1069
  /**
980
1070
  * HTTP endpoint key inferred from a Hono app schema. Distributes over the
@@ -1021,12 +1111,35 @@ type OpenApiHandler = {
1021
1111
  type OpenApiRoute = {
1022
1112
  key: string
1023
1113
  description: string | undefined
1114
+ management: boolean
1115
+ scope: string
1024
1116
  }
1025
1117
 
1026
1118
  type PolicyHandler = {
1027
1119
  [policySymbol]?: PolicyOverride | undefined
1028
1120
  }
1029
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
+
1030
1143
  function laneDefaults(options: middleware.Options): LaneDefaults {
1031
1144
  const apiKey = options.apiKey === false ? undefined : options.apiKey
1032
1145
  const public_ = options.public === false ? undefined : options.public
@@ -1043,7 +1156,7 @@ function laneDefaults(options: middleware.Options): LaneDefaults {
1043
1156
  // catalog scopes via `Auth.policy`.
1044
1157
  scopes: apiKey?.scopes ?? [Scope.wildcard],
1045
1158
  },
1046
- mpp: { charge: {} },
1159
+ mpp: { session: {} },
1047
1160
  public: { rateLimit: public_?.rateLimit ?? { perMinute: 60 } },
1048
1161
  session: true,
1049
1162
  }
@@ -1072,8 +1185,8 @@ function resolvePolicy(
1072
1185
  // The OpenAPI summary/description rides along as the payment-challenge
1073
1186
  // description. It is metadata, not a policy override, so it only applies when
1074
1187
  // the paid lane is actually enabled and has no explicit description already.
1075
- if (mpp && description !== undefined && mpp.charge.description === undefined)
1076
- mpp = { charge: { ...mpp.charge, description } }
1188
+ if (mpp && description !== undefined && mpp.session.description === undefined)
1189
+ mpp = { ...mpp, session: { ...mpp.session, description } }
1077
1190
 
1078
1191
  // A lane-less policy stays enforced for super admin; only an explicit
1079
1192
  // `false` override opts a route out of auth entirely.
@@ -1085,6 +1198,17 @@ function resolvePolicy(
1085
1198
  }
1086
1199
  }
1087
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
+
1088
1212
  function resolveApiKeyLane(
1089
1213
  current: require.Policy['apiKey'],
1090
1214
  override: PolicyOverride['apiKey'],
@@ -1109,7 +1233,10 @@ function resolveMppLane(
1109
1233
  if (override === false) return undefined
1110
1234
  if (override === true) return current ?? default_
1111
1235
  const base = current ?? default_
1112
- return { charge: { ...base.charge, ...override.charge } }
1236
+ return {
1237
+ rateLimit: override.rateLimit ?? base.rateLimit,
1238
+ session: { ...base.session, ...override.session },
1239
+ }
1113
1240
  }
1114
1241
 
1115
1242
  function resolvePublicLane(
@@ -1135,9 +1262,19 @@ function resolveSessionLane(
1135
1262
  }
1136
1263
 
1137
1264
  function openApiRoute(c: hono_Context): OpenApiRoute | undefined {
1138
- const route = matchedRoutes(c)
1139
- .slice(c.req.routeIndex + 1)
1140
- .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))
1141
1278
  if (!route) return undefined
1142
1279
 
1143
1280
  const spec = openApiSpec(route)
@@ -1147,6 +1284,8 @@ function openApiRoute(c: hono_Context): OpenApiRoute | undefined {
1147
1284
  return {
1148
1285
  key: endpointKey(c, route),
1149
1286
  description: typeof description === 'string' ? description : undefined,
1287
+ management: false,
1288
+ scope: `${route.method.toUpperCase()} ${route.path}`,
1150
1289
  }
1151
1290
  }
1152
1291
 
@@ -1169,6 +1308,11 @@ function policyFromRoute(route: RouterRoute) {
1169
1308
  return handler[policySymbol]
1170
1309
  }
1171
1310
 
1311
+ function mppManagementFromRoute(route: RouterRoute) {
1312
+ const handler = findTargetHandler(route.handler) as MppManagementHandler
1313
+ return handler[mppManagementSymbol]
1314
+ }
1315
+
1172
1316
  /** Resolved access lanes for one documented operation. */
1173
1317
  export type OperationAccess = {
1174
1318
  /** Whether API-key access is allowed. */
@@ -1223,8 +1367,8 @@ export function describeAccess(app: Hono<any, any>): Record<string, OperationAcc
1223
1367
  return access
1224
1368
  }
1225
1369
 
1226
- function endpointKey(c: hono_Context, route: RouterRoute) {
1227
- 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))}`
1228
1372
  }
1229
1373
 
1230
1374
  function normalizeRoutePath(path: string) {
@@ -1483,8 +1627,10 @@ export declare namespace require {
1483
1627
  /** MPP payment access policy. */
1484
1628
  mpp?:
1485
1629
  | {
1486
- /** Tempo charge policy. */
1487
- charge: Charge
1630
+ /** Paid-request quota override for this route (a protective per-route cap). */
1631
+ rateLimit?: RateLimit.Limit | undefined
1632
+ /** Tempo Session request policy. */
1633
+ session: Mpp.Session
1488
1634
  }
1489
1635
  | undefined
1490
1636
  /** Public access policy. */
@@ -1663,6 +1809,40 @@ function hasPaymentCredential(c: hono_Context) {
1663
1809
  return !!Credential.extractPaymentScheme(authorization)
1664
1810
  }
1665
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
+
1666
1846
  function isPaymentEnabled(auth: Context, policy: require.Policy) {
1667
1847
  return !!auth.mpp && !!policy.mpp
1668
1848
  }
@@ -1729,7 +1909,28 @@ async function requirePayment<environment extends Environment>(
1729
1909
  next: () => Promise<void>,
1730
1910
  options: PaymentAccess,
1731
1911
  ) {
1732
- const middleware = options.auth.mpp?.charge(options.policy.mpp?.charge ?? {})
1912
+ if (hasPaymentCredential(c)) {
1913
+ const result = await consumeRateLimit(options.auth, {
1914
+ key: `mpp:${options.principal.type}:${options.principal.id}`,
1915
+ limit: options.policy.mpp?.rateLimit ?? options.auth.mpp?.rateLimit ?? defaultMppRateLimit,
1916
+ })
1917
+ if (result) {
1918
+ setRateLimitHeaders(c, result)
1919
+ c.header('RateLimit-Scope', 'mpp')
1920
+ if (!result.allowed) return rateLimitExceeded(c, result)
1921
+ }
1922
+ }
1923
+
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
+ })
1733
1934
  if (!middleware)
1734
1935
  return jsonError(c, {
1735
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 {