tapimo 0.4.1 → 0.4.4

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 (263) 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 +1990 -1442
  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/apps/verified-tokens.d.ts +57 -57
  41. package/dist/admin/ui.generated.d.ts +1 -1
  42. package/dist/admin/ui.generated.d.ts.map +1 -1
  43. package/dist/admin/ui.generated.js +53 -13
  44. package/dist/admin/ui.generated.js.map +1 -1
  45. package/dist/analytics/Analytics.d.ts +10 -0
  46. package/dist/analytics/Analytics.d.ts.map +1 -1
  47. package/dist/analytics/Analytics.js +8 -0
  48. package/dist/analytics/Analytics.js.map +1 -1
  49. package/dist/analytics/tables/requestEvents.d.ts +9 -0
  50. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  51. package/dist/analytics/tables/requestEvents.js +14 -0
  52. package/dist/analytics/tables/requestEvents.js.map +1 -1
  53. package/dist/apps/data/App.d.ts +1304 -1304
  54. package/dist/apps/data/FundingRouteProviders.d.ts +4 -22
  55. package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
  56. package/dist/apps/data/FundingRouteProviders.js +10 -120
  57. package/dist/apps/data/FundingRouteProviders.js.map +1 -1
  58. package/dist/apps/data/routes/activities.d.ts +38 -38
  59. package/dist/apps/data/routes/addresses.d.ts +24 -24
  60. package/dist/apps/data/routes/blocks.d.ts +39 -39
  61. package/dist/apps/data/routes/fee-amm.d.ts +20 -20
  62. package/dist/apps/data/routes/funding.js +7 -7
  63. package/dist/apps/data/routes/funding.js.map +1 -1
  64. package/dist/apps/data/routes/indexer.d.ts +5 -5
  65. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  66. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  67. package/dist/apps/management/App.d.ts +808 -267
  68. package/dist/apps/management/App.d.ts.map +1 -1
  69. package/dist/apps/management/App.js +2 -2
  70. package/dist/apps/management/Billing.d.ts +3 -0
  71. package/dist/apps/management/Billing.d.ts.map +1 -1
  72. package/dist/apps/management/Billing.js +7 -2
  73. package/dist/apps/management/Billing.js.map +1 -1
  74. package/dist/apps/management/routes/api-keys.d.ts +116 -100
  75. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  76. package/dist/apps/management/routes/api-keys.js +41 -30
  77. package/dist/apps/management/routes/api-keys.js.map +1 -1
  78. package/dist/apps/management/routes/billing.d.ts +182 -50
  79. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  80. package/dist/apps/management/routes/billing.js +14 -14
  81. package/dist/apps/management/routes/billing.js.map +1 -1
  82. package/dist/apps/management/routes/invitations.d.ts +93 -20
  83. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  84. package/dist/apps/management/routes/invitations.js +12 -15
  85. package/dist/apps/management/routes/invitations.js.map +1 -1
  86. package/dist/apps/management/routes/members.d.ts +89 -18
  87. package/dist/apps/management/routes/members.d.ts.map +1 -1
  88. package/dist/apps/management/routes/members.js +10 -10
  89. package/dist/apps/management/routes/members.js.map +1 -1
  90. package/dist/apps/management/routes/orgs.d.ts +100 -40
  91. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  92. package/dist/apps/management/routes/orgs.js +19 -19
  93. package/dist/apps/management/routes/orgs.js.map +1 -1
  94. package/dist/apps/management/routes/projects.d.ts +122 -33
  95. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  96. package/dist/apps/management/routes/projects.js +12 -13
  97. package/dist/apps/management/routes/projects.js.map +1 -1
  98. package/dist/apps/management/routes/scopes.d.ts +4 -18
  99. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  100. package/dist/apps/management/routes/scopes.js +6 -7
  101. package/dist/apps/management/routes/scopes.js.map +1 -1
  102. package/dist/apps/management/routes/usage.d.ts +63 -19
  103. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  104. package/dist/apps/management/routes/usage.js +9 -10
  105. package/dist/apps/management/routes/usage.js.map +1 -1
  106. package/dist/apps/mcp.d.ts.map +1 -1
  107. package/dist/apps/mcp.js +38 -4
  108. package/dist/apps/mcp.js.map +1 -1
  109. package/dist/apps/relay/App.d.ts +26 -4
  110. package/dist/apps/relay/App.d.ts.map +1 -1
  111. package/dist/apps/relay/App.js +17 -14
  112. package/dist/apps/relay/App.js.map +1 -1
  113. package/dist/cloudflare.d.ts +2 -0
  114. package/dist/cloudflare.d.ts.map +1 -1
  115. package/dist/cloudflare.js +4 -0
  116. package/dist/cloudflare.js.map +1 -1
  117. package/dist/db/Db.d.ts +5 -3
  118. package/dist/db/Db.d.ts.map +1 -1
  119. package/dist/db/Db.js +52 -0
  120. package/dist/db/Db.js.map +1 -1
  121. package/dist/db/Schema.d.ts +13 -0
  122. package/dist/db/Schema.d.ts.map +1 -1
  123. package/dist/db/Schema.js +16 -1
  124. package/dist/db/Schema.js.map +1 -1
  125. package/dist/db/tables/adminAuditLogs.d.ts +27 -0
  126. package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
  127. package/dist/db/tables/adminAuditLogs.js +19 -0
  128. package/dist/db/tables/adminAuditLogs.js.map +1 -0
  129. package/dist/db/tables/invitations.d.ts +1 -1
  130. package/dist/db/tables/invitations.d.ts.map +1 -1
  131. package/dist/db/tables/memberships.d.ts +32 -4
  132. package/dist/db/tables/memberships.d.ts.map +1 -1
  133. package/dist/db/tables/memberships.js +31 -0
  134. package/dist/db/tables/memberships.js.map +1 -1
  135. package/dist/db/tables/organizations.d.ts +43 -1
  136. package/dist/db/tables/organizations.d.ts.map +1 -1
  137. package/dist/db/tables/organizations.js +103 -0
  138. package/dist/db/tables/organizations.js.map +1 -1
  139. package/dist/db/tables/projects.d.ts +25 -0
  140. package/dist/db/tables/projects.d.ts.map +1 -1
  141. package/dist/db/tables/projects.js +22 -0
  142. package/dist/db/tables/projects.js.map +1 -1
  143. package/dist/db/tables/webhookDeliveries.d.ts +26 -0
  144. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  145. package/dist/db/tables/webhookDeliveries.js +37 -0
  146. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  147. package/dist/internal/Auth.d.ts +46 -40
  148. package/dist/internal/Auth.d.ts.map +1 -1
  149. package/dist/internal/Auth.js +118 -69
  150. package/dist/internal/Auth.js.map +1 -1
  151. package/dist/internal/Cursor.d.ts +1 -1
  152. package/dist/internal/Cursor.d.ts.map +1 -1
  153. package/dist/internal/Cursor.js +4 -0
  154. package/dist/internal/Cursor.js.map +1 -1
  155. package/dist/internal/EdgeCache.d.ts.map +1 -1
  156. package/dist/internal/EdgeCache.js +2 -0
  157. package/dist/internal/EdgeCache.js.map +1 -1
  158. package/dist/internal/Errors.d.ts +6 -0
  159. package/dist/internal/Errors.d.ts.map +1 -0
  160. package/dist/internal/Errors.js +16 -0
  161. package/dist/internal/Errors.js.map +1 -0
  162. package/dist/internal/FundingRoutes.d.ts +15 -43
  163. package/dist/internal/FundingRoutes.d.ts.map +1 -1
  164. package/dist/internal/FundingRoutes.js +8 -17
  165. package/dist/internal/FundingRoutes.js.map +1 -1
  166. package/dist/internal/Log.d.ts +16 -1
  167. package/dist/internal/Log.d.ts.map +1 -1
  168. package/dist/internal/Log.js +5 -3
  169. package/dist/internal/Log.js.map +1 -1
  170. package/dist/internal/Store.d.ts +61 -16
  171. package/dist/internal/Store.d.ts.map +1 -1
  172. package/dist/internal/Store.js +72 -17
  173. package/dist/internal/Store.js.map +1 -1
  174. package/dist/internal/Viem.d.ts +1 -1
  175. package/dist/internal/index.d.ts +27 -0
  176. package/dist/internal/index.d.ts.map +1 -0
  177. package/dist/internal/index.js +27 -0
  178. package/dist/internal/index.js.map +1 -0
  179. package/package.json +8 -3
  180. package/src/ApiKey.ts +2 -3
  181. package/src/ApiKeys.ts +95 -17
  182. package/src/App.test.ts +104 -6
  183. package/src/App.ts +29 -9
  184. package/src/Assets.ts +8 -0
  185. package/src/Auth.test-d.ts +36 -0
  186. package/src/Auth.ts +20 -1
  187. package/src/Client.test-d.ts +17 -17
  188. package/src/Scope.test.ts +34 -0
  189. package/src/Scope.ts +53 -17
  190. package/src/admin/App.test.ts +33 -3
  191. package/src/admin/App.ts +49 -14
  192. package/src/admin/apps/api-keys.ts +10 -3
  193. package/src/admin/apps/organizations.test.ts +455 -0
  194. package/src/admin/apps/organizations.ts +953 -0
  195. package/src/admin/apps/scopes.test.ts +10 -0
  196. package/src/admin/ui/plugin.ts +1 -1
  197. package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
  198. package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
  199. package/src/admin/ui/src/lib/api-keys.ts +36 -19
  200. package/src/admin/ui/src/lib/config.ts +2 -3
  201. package/src/admin/ui/src/lib/organizations.test.ts +89 -0
  202. package/src/admin/ui/src/lib/organizations.ts +262 -0
  203. package/src/admin/ui/src/lib/query-client.ts +3 -1
  204. package/src/admin/ui/src/lib/theme.ts +57 -0
  205. package/src/admin/ui/src/routeTree.gen.ts +47 -4
  206. package/src/admin/ui/src/routes/__root.tsx +18 -3
  207. package/src/admin/ui/src/routes/api-keys.tsx +52 -7
  208. package/src/admin/ui/src/routes/index.tsx +1 -1
  209. package/src/admin/ui/src/routes/organizations.tsx +158 -0
  210. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
  211. package/src/admin/ui/src/start.ts +6 -0
  212. package/src/admin/ui.generated.ts +63 -13
  213. package/src/analytics/Analytics.ts +25 -0
  214. package/src/analytics/tables/requestEvents.ts +33 -0
  215. package/src/apps/composition.test.ts +82 -2
  216. package/src/apps/data/FundingRouteProviders.test.ts +37 -93
  217. package/src/apps/data/FundingRouteProviders.ts +14 -161
  218. package/src/apps/data/routes/funding.test.ts +7 -42
  219. package/src/apps/data/routes/funding.ts +8 -8
  220. package/src/apps/data/routes/indexer.test.ts +2 -2
  221. package/src/apps/data/routes/webhooks.test.ts +2 -2
  222. package/src/apps/management/App.ts +2 -2
  223. package/src/apps/management/Billing.ts +10 -2
  224. package/src/apps/management/routes/api-keys.test.ts +137 -1
  225. package/src/apps/management/routes/api-keys.ts +63 -38
  226. package/src/apps/management/routes/billing.ts +14 -14
  227. package/src/apps/management/routes/invitations.test.ts +102 -1
  228. package/src/apps/management/routes/invitations.ts +13 -15
  229. package/src/apps/management/routes/members.ts +10 -10
  230. package/src/apps/management/routes/orgs.test.ts +78 -16
  231. package/src/apps/management/routes/orgs.ts +21 -20
  232. package/src/apps/management/routes/projects.ts +12 -13
  233. package/src/apps/management/routes/scopes.test.ts +16 -0
  234. package/src/apps/management/routes/scopes.ts +6 -7
  235. package/src/apps/management/routes/usage.ts +9 -14
  236. package/src/apps/mcp.test.ts +72 -7
  237. package/src/apps/mcp.ts +46 -4
  238. package/src/apps/relay/App.test.ts +23 -18
  239. package/src/apps/relay/App.ts +35 -14
  240. package/src/cloudflare.ts +5 -0
  241. package/src/db/Db.ts +59 -3
  242. package/src/db/Schema.ts +20 -1
  243. package/src/db/tables/adminAuditLogs.ts +47 -0
  244. package/src/db/tables/invitations.ts +1 -1
  245. package/src/db/tables/memberships.ts +59 -3
  246. package/src/db/tables/organizations.ts +148 -1
  247. package/src/db/tables/projects.ts +47 -0
  248. package/src/db/tables/webhookDeliveries.ts +83 -0
  249. package/src/internal/Auth.test.ts +571 -23
  250. package/src/internal/Auth.ts +166 -94
  251. package/src/internal/Cursor.test.ts +10 -0
  252. package/src/internal/Cursor.ts +5 -1
  253. package/src/internal/EdgeCache.test.ts +11 -0
  254. package/src/internal/EdgeCache.ts +2 -0
  255. package/src/internal/FundingRoutes.test.ts +8 -8
  256. package/src/internal/FundingRoutes.ts +25 -29
  257. package/src/internal/Log.test.ts +41 -3
  258. package/src/internal/Log.ts +23 -4
  259. package/src/internal/Store.test-d.ts +15 -0
  260. package/src/internal/Store.test.ts +153 -2
  261. package/src/internal/Store.ts +157 -41
  262. package/src/internal/index.test.ts +34 -0
  263. package/src/internal/index.ts +26 -0
@@ -9,6 +9,7 @@ import { findTargetHandler } from 'hono/utils/handler'
9
9
  import { decodeJwt } from 'jose'
10
10
  import { 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,7 @@ 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 defaultMppRateLimit = { perMinute: 100 } satisfies RateLimit.Limit
33
35
 
34
36
  /** Auth context consumed by Tempo API handlers. */
35
37
  export type Context = {
@@ -47,7 +49,7 @@ export type Context = {
47
49
  }
48
50
  | undefined
49
51
  /** MPP payment configuration. */
50
- mpp?: Mpp | undefined
52
+ mpp?: MppHandler | undefined
51
53
  /**
52
54
  * Configured default anonymous public quota. Used as the throttle ceiling for
53
55
  * sandbox keys without active billing on routes that expose no public lane
@@ -89,22 +91,27 @@ export type Payment = {
89
91
  type: 'mpp'
90
92
  }
91
93
 
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
94
+ /** MPP route-policy types. Distinct from the SIWE {@link Session} capability. */
95
+ export declare namespace Mpp {
96
+ /** MPP Session request options used by route access policies. */
97
+ type Session = Method.RequestDefaults<ReturnType<typeof tempo.session>> & {
98
+ /** Optional human-readable payment description. */
99
+ description?: string | undefined
100
+ /** Optional challenge expiration timestamp or date. */
101
+ expires?: Date | string | undefined
102
+ /** Optional server-defined correlation data. */
103
+ meta?: Record<string, string> | undefined
104
+ /** Optional route/resource scope bound to the challenge. */
105
+ scope?: string | undefined
106
+ }
102
107
  }
103
108
 
104
109
  /** MPP payment handler consumed by auth policies. */
105
- type Mpp = {
106
- /** Creates a Hono middleware for a Tempo charge. */
107
- charge(options: Charge): MiddlewareHandler
110
+ type MppHandler = {
111
+ /** Paid-request quota per access principal. */
112
+ rateLimit?: RateLimit.Limit | undefined
113
+ /** Creates a Hono middleware for a Tempo Session payment. */
114
+ session(options: Mpp.Session): MiddlewareHandler
108
115
  }
109
116
 
110
117
  /**
@@ -127,11 +134,13 @@ export type PolicyOverride = {
127
134
  }
128
135
  | boolean
129
136
  | undefined
130
- /** MPP paid lane: object to override the charge, or boolean to enable/disable. */
137
+ /** MPP paid lane: object to override the Session request, or boolean to enable/disable. */
131
138
  mpp?:
132
139
  | {
133
- /** Tempo charge override for this route. */
134
- charge?: Charge | undefined
140
+ /** Paid-request quota override for this route (a protective per-route cap). */
141
+ rateLimit?: RateLimit.Limit | undefined
142
+ /** Tempo Session request override for this route. */
143
+ session?: Mpp.Session | undefined
135
144
  }
136
145
  | boolean
137
146
  | undefined
@@ -160,7 +169,7 @@ export type Principal =
160
169
  orgId: string
161
170
  /** Payment metadata for paid overflow requests. */
162
171
  payment?: Payment | undefined
163
- /** Owning project id, when the key carries project attribution. */
172
+ /** Attributed project id, when present. */
164
173
  projectId?: string | undefined
165
174
  /** Principal kind. */
166
175
  type: 'api_key'
@@ -205,9 +214,9 @@ export type Principal =
205
214
  export type Variables = {
206
215
  /** Auth context consumed by protected handlers. */
207
216
  auth: Context
208
- /** Caller's membership resolved by {@link ensureOrg}; absent for `super_admin`. */
217
+ /** Caller's membership resolved by {@link ensureOrg} or {@link ensureProject}; absent for API keys and `super_admin`. */
209
218
  membership?: Memberships.Record | undefined
210
- /** Organization resolved by {@link ensureOrg} for the current request. */
219
+ /** Organization resolved by {@link ensureOrg} or {@link ensureProject} for the current request. */
211
220
  org?: Organizations.Record | undefined
212
221
  /** Authenticated or anonymous caller. */
213
222
  principal?: Principal | undefined
@@ -221,13 +230,7 @@ export function getPrincipal<environment extends Environment>(c: hono_Context<en
221
230
  }
222
231
 
223
232
  /**
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}.
233
+ * Guards `:orgId` routes and resolves the organization. Sessions require membership, API keys require ownership, and the super admin bypasses ownership.
231
234
  */
232
235
  export function ensureOrg(options: ensureOrg.Options = {}): MiddlewareHandler {
233
236
  const { role: minimum = 'member' } = options
@@ -243,11 +246,17 @@ export function ensureOrg(options: ensureOrg.Options = {}): MiddlewareHandler {
243
246
  const orgId = c.req.param('orgId' as never) as string | undefined
244
247
  if (!orgId) return notFound()
245
248
  const principal = getPrincipal(c)
246
- if (principal?.type !== 'session' && principal?.type !== 'super_admin') return notFound()
249
+ if (
250
+ principal?.type !== 'api_key' &&
251
+ principal?.type !== 'session' &&
252
+ principal?.type !== 'super_admin'
253
+ )
254
+ return notFound()
255
+ if (principal.type === 'api_key' && principal.orgId !== orgId) return notFound()
247
256
  const db = Db.get(contextDb(c))
248
257
  const record = await Organizations.get(db, orgId)
249
258
  if (!record) return notFound()
250
- if (principal.type !== 'super_admin') {
259
+ if (principal.type === 'session') {
251
260
  const member = await Memberships.get(db, orgId, principal.id)
252
261
  if (!member) return notFound()
253
262
  if (Memberships.rank[member.role] < Memberships.rank[minimum])
@@ -267,16 +276,13 @@ export function ensureOrg(options: ensureOrg.Options = {}): MiddlewareHandler {
267
276
  export declare namespace ensureOrg {
268
277
  /** Options for the org guard. */
269
278
  type Options = {
270
- /** Minimum role required; defaults to `member` (any membership). */
279
+ /** Minimum session role; defaults to `member`. */
271
280
  role?: Memberships.Role | undefined
272
281
  }
273
282
  }
274
283
 
275
284
  /**
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}.
285
+ * Guards `:projectId` routes and resolves the project. Flat routes authorize against its organization; nested routes preserve the scope from {@link ensureOrg}.
280
286
  */
281
287
  export function ensureProject(): MiddlewareHandler {
282
288
  // Typed loosely (like {@link policy}) so route chains keep their inferred
@@ -290,10 +296,32 @@ export function ensureProject(): MiddlewareHandler {
290
296
  })
291
297
  const projectId = c.req.param('projectId' as never) as string | undefined
292
298
  if (!projectId) return notFound()
299
+ const principal = getPrincipal(c)
300
+ if (
301
+ principal?.type !== 'api_key' &&
302
+ principal?.type !== 'session' &&
303
+ principal?.type !== 'super_admin'
304
+ )
305
+ return notFound()
306
+ const db = Db.get(contextDb(c))
307
+ const record = await Projects.get(db, projectId)
308
+ if (!record) return notFound()
293
309
  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()
310
+ if (scoped) {
311
+ if (record.orgId !== scoped.id) return notFound()
312
+ c.set('project', record)
313
+ await next()
314
+ return undefined
315
+ }
316
+ if (principal.type === 'api_key' && principal.orgId !== record.orgId) return notFound()
317
+ const organization = await Organizations.get(db, record.orgId)
318
+ if (!organization) return notFound()
319
+ if (principal.type === 'session') {
320
+ const member = await Memberships.get(db, record.orgId, principal.id)
321
+ if (!member) return notFound()
322
+ c.set('membership', member)
323
+ }
324
+ c.set('org', organization)
297
325
  c.set('project', record)
298
326
  await next()
299
327
  return undefined
@@ -318,7 +346,7 @@ export function ensureOrgError(c: hono_Context) {
318
346
 
319
347
  /** Typed project-scope error response for Hono client inference. Never reached at runtime. */
320
348
  export function ensureProjectError(c: hono_Context) {
321
- // Project routes run the org scope first, so either not-found can surface.
349
+ // Nested project routes run the org scope first, so either not-found can surface.
322
350
  const code =
323
351
  c.req.header('tempo-narrow-scope-code') === 'organization_not_found'
324
352
  ? ('organization_not_found' as const)
@@ -330,19 +358,20 @@ export function ensureProjectError(c: hono_Context) {
330
358
  })
331
359
  }
332
360
 
333
- /** Reads the caller's membership resolved by {@link ensureOrg}; `undefined` for `super_admin`. */
361
+ /** Reads the caller's membership resolved by the organization or project guard; `undefined` for API keys and `super_admin`. */
334
362
  export function membership<environment extends Environment>(
335
363
  c: hono_Context<environment>,
336
364
  ): Memberships.Record | undefined {
337
365
  return c.get('membership')
338
366
  }
339
367
 
340
- /** Reads the organization resolved by {@link ensureOrg}; throws when the middleware did not run. */
368
+ /** Reads the organization resolved by the organization or project guard; throws when neither middleware ran. */
341
369
  export function org<environment extends Environment>(
342
370
  c: hono_Context<environment>,
343
371
  ): Organizations.Record {
344
372
  const record = c.get('org')
345
- if (!record) throw new Error('`orgScope` middleware must run before reading the organization')
373
+ if (!record)
374
+ throw new Error('`ensureOrg` or `ensureProject` must run before reading the organization')
346
375
  return record
347
376
  }
348
377
 
@@ -351,7 +380,7 @@ export function project<environment extends Environment>(
351
380
  c: hono_Context<environment>,
352
381
  ): Projects.Record {
353
382
  const record = c.get('project')
354
- if (!record) throw new Error('`projectScope` middleware must run before reading the project')
383
+ if (!record) throw new Error('`ensureProject` must run before reading the project')
355
384
  return record
356
385
  }
357
386
 
@@ -445,7 +474,11 @@ export function middleware<
445
474
  endpoint extends string = string,
446
475
  environment extends Environment = Environment,
447
476
  >(options: middleware.Options<endpoint>): MiddlewareHandler<environment> {
448
- const { charge = {}, ...mpp } = options.mpp === false ? {} : (options.mpp ?? {})
477
+ const {
478
+ rateLimit: mppRateLimit = defaultMppRateLimit,
479
+ session: session_ = {},
480
+ ...mpp
481
+ } = options.mpp === false ? {} : (options.mpp ?? {})
449
482
  const defaults = laneDefaults(options)
450
483
  // Default-deny: no lanes open until a route grants them via `policy`.
451
484
  // Otherwise the `super_admin` principal bypasses lane checks entirely.
@@ -460,7 +493,9 @@ export function middleware<
460
493
  const resolve = (() => {
461
494
  const uncached = (c: hono_Context, token: string) => {
462
495
  const kv = contextKv(c)
463
- return kv ? ApiKeys.resolve(kv.store, token) : Promise.resolve(null)
496
+ return kv
497
+ ? ApiKeys.resolve(kv.store, token, { scopeCatalog: options.scopeCatalog })
498
+ : Promise.resolve(null)
464
499
  }
465
500
  if (options.apiKey === false || options.apiKey?.cache === false) return uncached
466
501
  return cachedResolve(uncached, options.apiKey?.cache ?? {})
@@ -481,11 +516,23 @@ export function middleware<
481
516
  ...(options.mpp === false
482
517
  ? {}
483
518
  : {
484
- mpp: Mppx.create({
485
- ...mpp,
486
- methods: [tempo.charge({ amount: '0.001', ...charge })],
487
- secretKey: mpp.secretKey ?? crypto.randomUUID(),
488
- }),
519
+ mpp: {
520
+ rateLimit: mppRateLimit,
521
+ session: Mppx.create({
522
+ ...mpp,
523
+ // Defaults only; global `session` options may override, including
524
+ // enabling same-route HEAD bootstrap.
525
+ methods: [
526
+ tempo.session({
527
+ amount: '0.0001',
528
+ bootstrap: false,
529
+ currency: Addresses.pathUsd,
530
+ unitType: 'request',
531
+ ...session_,
532
+ }),
533
+ ],
534
+ }).session,
535
+ },
489
536
  }),
490
537
  rateLimit: RateLimit.memory(options.rateLimit),
491
538
  ...(options.session ? { session: options.session } : {}),
@@ -554,6 +601,8 @@ export declare namespace middleware {
554
601
  | undefined
555
602
  /** Rate-limit store options. */
556
603
  rateLimit?: RateLimit.memory.Options | undefined
604
+ /** Scope catalog accepted while resolving API-key records. */
605
+ scopeCatalog?: Scope.Catalog | undefined
557
606
  /** Session lane resolution — the session surface's capability; omit to leave the session lane closed. */
558
607
  session?: Session | undefined
559
608
  /**
@@ -576,8 +625,10 @@ export declare namespace middleware {
576
625
 
577
626
  /** MPP payment options used by auth middleware. */
578
627
  type MppOptions = Omit<NonNullable<Parameters<typeof Mppx.create>[0]>, 'methods'> & {
579
- /** Global Tempo charge options passed to the MPP method instantiator. */
580
- charge?: NonNullable<Parameters<typeof tempo.charge>[0]> | undefined
628
+ /** Paid-request quota per access principal. Defaults to 100 per minute. */
629
+ rateLimit?: RateLimit.Limit | undefined
630
+ /** Global Tempo Session options passed to the MPP method instantiator. */
631
+ session?: NonNullable<Parameters<typeof tempo.session>[0]> | undefined
581
632
  }
582
633
  }
583
634
 
@@ -678,7 +729,7 @@ function handlerError(description: string) {
678
729
  * disables both.
679
730
  */
680
731
  export function install(options: install.Options) {
681
- const { db, kv } = options
732
+ const { db, kv, scopeCatalog } = options
682
733
  const { session: options_session, ...options_middleware } =
683
734
  options.auth === false ? {} : (options.auth ?? {})
684
735
  if (options_session && !kv)
@@ -894,7 +945,11 @@ export function install(options: install.Options) {
894
945
  const middleware_ =
895
946
  options.auth === false
896
947
  ? undefined
897
- : middleware({ ...options_middleware, ...(app ? { session: app.session } : {}) })
948
+ : middleware({
949
+ ...options_middleware,
950
+ ...(app ? { session: app.session } : {}),
951
+ scopeCatalog,
952
+ })
898
953
 
899
954
  return { app, middleware: middleware_ }
900
955
  }
@@ -903,7 +958,7 @@ export declare namespace install {
903
958
  /** Options for building the built-in auth pieces. */
904
959
  type Options = {
905
960
  auth:
906
- | (Omit<middleware.Options, 'session'> & {
961
+ | (Omit<middleware.Options, 'scopeCatalog' | 'session'> & {
907
962
  /** 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
963
  session?: Session | undefined
909
964
  })
@@ -913,6 +968,8 @@ export declare namespace install {
913
968
  db: Db.Source
914
969
  /** KV state store, or undefined when key auth is closed. */
915
970
  kv: { store: Store.State } | undefined
971
+ /** Scope catalog accepted while resolving API-key records. */
972
+ scopeCatalog: Scope.Catalog
916
973
  }
917
974
 
918
975
  /** Session sign-in surface configuration: `true` for defaults, or a config object. */
@@ -1013,7 +1070,7 @@ function laneDefaults(options: middleware.Options): LaneDefaults {
1013
1070
  // catalog scopes via `Auth.policy`.
1014
1071
  scopes: apiKey?.scopes ?? [Scope.wildcard],
1015
1072
  },
1016
- mpp: { charge: {} },
1073
+ mpp: { session: {} },
1017
1074
  public: { rateLimit: public_?.rateLimit ?? { perMinute: 60 } },
1018
1075
  session: true,
1019
1076
  }
@@ -1042,8 +1099,8 @@ function resolvePolicy(
1042
1099
  // The OpenAPI summary/description rides along as the payment-challenge
1043
1100
  // description. It is metadata, not a policy override, so it only applies when
1044
1101
  // the paid lane is actually enabled and has no explicit description already.
1045
- if (mpp && description !== undefined && mpp.charge.description === undefined)
1046
- mpp = { charge: { ...mpp.charge, description } }
1102
+ if (mpp && description !== undefined && mpp.session.description === undefined)
1103
+ mpp = { ...mpp, session: { ...mpp.session, description } }
1047
1104
 
1048
1105
  // A lane-less policy stays enforced for super admin; only an explicit
1049
1106
  // `false` override opts a route out of auth entirely.
@@ -1079,7 +1136,10 @@ function resolveMppLane(
1079
1136
  if (override === false) return undefined
1080
1137
  if (override === true) return current ?? default_
1081
1138
  const base = current ?? default_
1082
- return { charge: { ...base.charge, ...override.charge } }
1139
+ return {
1140
+ rateLimit: override.rateLimit ?? base.rateLimit,
1141
+ session: { ...base.session, ...override.session },
1142
+ }
1083
1143
  }
1084
1144
 
1085
1145
  function resolvePublicLane(
@@ -1229,12 +1289,20 @@ export function require<environment extends Environment = Environment>(
1229
1289
  return createMiddleware<environment>(async (c, next) => {
1230
1290
  const auth = c.get('auth')
1231
1291
  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)
1292
+ const hasQueryApiKey = new URL(c.req.url).searchParams.has('key')
1293
+ // Auth owns `key`; strict route schemas must not validate it.
1294
+ stripApiKeyQuery(c)
1295
+ const proceed = async () => {
1296
+ await next()
1297
+ // Query credential responses must not enter shared caches.
1298
+ const cacheControl = c.res.headers.get('Cache-Control')?.toLowerCase()
1299
+ if (
1300
+ hasQueryApiKey &&
1301
+ !cacheControl?.includes('no-store') &&
1302
+ !cacheControl?.includes('private')
1303
+ )
1304
+ c.res.headers.set('Cache-Control', 'private')
1305
+ }
1238
1306
  if (credential === false)
1239
1307
  return jsonError(c, {
1240
1308
  code: 'api_key_malformed',
@@ -1248,7 +1316,7 @@ export function require<environment extends Environment = Environment>(
1248
1316
  // It must never reach KV resolution or scope/quota checks.
1249
1317
  if (auth.superAdmin && ApiKey.hash(credential.token) === auth.superAdmin.tokenHash) {
1250
1318
  c.set('principal', { actor: 'super_admin', id: 'super_admin', type: 'super_admin' })
1251
- return next()
1319
+ return proceed()
1252
1320
  }
1253
1321
 
1254
1322
  const apiKey = await auth.apiKey?.resolve(c, credential.token)
@@ -1260,7 +1328,7 @@ export function require<environment extends Environment = Environment>(
1260
1328
  const session = await resolveSession(c, auth)
1261
1329
  if (session) {
1262
1330
  c.set('principal', session)
1263
- return next()
1331
+ return proceed()
1264
1332
  }
1265
1333
  }
1266
1334
  return jsonError(c, {
@@ -1319,7 +1387,7 @@ export function require<environment extends Environment = Environment>(
1319
1387
  const throttled = apiKey.environment === 'sandbox' && apiKey.billingActive !== true
1320
1388
 
1321
1389
  if (hasPayment && !throttled)
1322
- return requirePayment(c, next, {
1390
+ return requirePayment(c, proceed, {
1323
1391
  auth,
1324
1392
  payment: { reason: 'api_key_over_quota', type: 'mpp' },
1325
1393
  policy,
@@ -1351,7 +1419,7 @@ export function require<environment extends Environment = Environment>(
1351
1419
  // A throttled sandbox key gets a plain 429, never an MPP challenge —
1352
1420
  // it must activate billing to lift the ceiling, not pay per request.
1353
1421
  if (throttled) return rateLimitExceeded(c, result)
1354
- return requirePaymentOrRateLimit(c, next, {
1422
+ return requirePaymentOrRateLimit(c, proceed, {
1355
1423
  auth,
1356
1424
  payment: { reason: 'api_key_over_quota', type: 'mpp' },
1357
1425
  policy,
@@ -1361,7 +1429,7 @@ export function require<environment extends Environment = Environment>(
1361
1429
  }
1362
1430
  }
1363
1431
 
1364
- return next()
1432
+ return proceed()
1365
1433
  }
1366
1434
 
1367
1435
  // Cookie sessions carry no Authorization credential; resolve the session
@@ -1371,14 +1439,14 @@ export function require<environment extends Environment = Environment>(
1371
1439
  const session = await resolveSession(c, auth)
1372
1440
  if (session) {
1373
1441
  c.set('principal', session)
1374
- return next()
1442
+ return proceed()
1375
1443
  }
1376
1444
  }
1377
1445
 
1378
1446
  const publicPrincipal = principalFromPublic(c)
1379
1447
  if (hasPayment) {
1380
1448
  c.set('principal', publicPrincipal)
1381
- return requirePayment(c, next, {
1449
+ return requirePayment(c, proceed, {
1382
1450
  auth,
1383
1451
  payment: { reason: 'public_over_quota', type: 'mpp' },
1384
1452
  policy,
@@ -1395,11 +1463,11 @@ export function require<environment extends Environment = Environment>(
1395
1463
  if (result) setRateLimitHeaders(c, result)
1396
1464
  if (!result || result.allowed) {
1397
1465
  c.set('principal', publicPrincipal)
1398
- return next()
1466
+ return proceed()
1399
1467
  }
1400
1468
 
1401
1469
  c.set('principal', publicPrincipal)
1402
- return requirePaymentOrRateLimit(c, next, {
1470
+ return requirePaymentOrRateLimit(c, proceed, {
1403
1471
  auth,
1404
1472
  payment: { reason: 'public_over_quota', type: 'mpp' },
1405
1473
  policy,
@@ -1410,7 +1478,7 @@ export function require<environment extends Environment = Environment>(
1410
1478
 
1411
1479
  if (isPaymentEnabled(auth, policy)) {
1412
1480
  c.set('principal', publicPrincipal)
1413
- return requirePayment(c, next, {
1481
+ return requirePayment(c, proceed, {
1414
1482
  auth,
1415
1483
  payment: { reason: 'public_over_quota', type: 'mpp' },
1416
1484
  policy,
@@ -1445,8 +1513,10 @@ export declare namespace require {
1445
1513
  /** MPP payment access policy. */
1446
1514
  mpp?:
1447
1515
  | {
1448
- /** Tempo charge policy. */
1449
- charge: Charge
1516
+ /** Paid-request quota override for this route (a protective per-route cap). */
1517
+ rateLimit?: RateLimit.Limit | undefined
1518
+ /** Tempo Session request policy. */
1519
+ session: Mpp.Session
1450
1520
  }
1451
1521
  | undefined
1452
1522
  /** Public access policy. */
@@ -1574,10 +1644,8 @@ function getApiKeyCredential(c: hono_Context): ApiKeyCredential | false | null {
1574
1644
  const header = c.req.header('tempo-api-key')
1575
1645
  if (header) return { token: header }
1576
1646
 
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.
1647
+ // Bearer supports clients unable to set the canonical header. Payment
1648
+ // credentials belong to MPP and must pass through untouched.
1581
1649
  const authorization = c.req.header('authorization')
1582
1650
  if (authorization && !Credential.extractPaymentScheme(authorization)) {
1583
1651
  const match = /^Bearer\s+(.+)$/i.exec(authorization)
@@ -1585,26 +1653,18 @@ function getApiKeyCredential(c: hono_Context): ApiKeyCredential | false | null {
1585
1653
  return { token: match[1] }
1586
1654
  }
1587
1655
 
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.
1656
+ // `x-api-key` remains a deprecated compatibility header.
1590
1657
  const legacyHeader = c.req.header('x-api-key')
1591
1658
  if (legacyHeader) return { token: legacyHeader }
1592
1659
 
1593
- const legacyQuery = c.req.query('key')
1594
- if (legacyQuery) return { token: legacyQuery }
1660
+ const apiKey = c.req.query('key')
1661
+ if (apiKey) return { token: apiKey }
1595
1662
 
1596
1663
  return null
1597
1664
  }
1598
1665
 
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) {
1666
+ /** Removes the reserved auth query parameter before route validation. */
1667
+ function stripApiKeyQuery(c: hono_Context) {
1608
1668
  const url = new URL(c.req.url)
1609
1669
  if (!url.searchParams.has('key')) return
1610
1670
  url.searchParams.delete('key')
@@ -1701,7 +1761,19 @@ async function requirePayment<environment extends Environment>(
1701
1761
  next: () => Promise<void>,
1702
1762
  options: PaymentAccess,
1703
1763
  ) {
1704
- const middleware = options.auth.mpp?.charge(options.policy.mpp?.charge ?? {})
1764
+ if (hasPaymentCredential(c)) {
1765
+ const result = await consumeRateLimit(options.auth, {
1766
+ key: `mpp:${options.principal.type}:${options.principal.id}`,
1767
+ limit: options.policy.mpp?.rateLimit ?? options.auth.mpp?.rateLimit ?? defaultMppRateLimit,
1768
+ })
1769
+ if (result) {
1770
+ setRateLimitHeaders(c, result)
1771
+ c.header('RateLimit-Scope', 'mpp')
1772
+ if (!result.allowed) return rateLimitExceeded(c, result)
1773
+ }
1774
+ }
1775
+
1776
+ const middleware = options.auth.mpp?.session(options.policy.mpp?.session ?? {})
1705
1777
  if (!middleware)
1706
1778
  return jsonError(c, {
1707
1779
  code: 'payment_required',
@@ -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