tapimo 0.4.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/README.md +1 -1
  2. package/dist/ApiKey.d.ts +1 -9
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +2 -3
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +53 -27
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +32 -17
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +2451 -1533
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +23 -9
  13. package/dist/App.js.map +1 -1
  14. package/dist/Assets.d.ts +10 -0
  15. package/dist/Assets.d.ts.map +1 -1
  16. package/dist/Assets.js +4 -0
  17. package/dist/Assets.js.map +1 -1
  18. package/dist/Auth.d.ts +2 -1
  19. package/dist/Auth.d.ts.map +1 -1
  20. package/dist/Auth.js +2 -1
  21. package/dist/Auth.js.map +1 -1
  22. package/dist/Scope.d.ts +40 -15
  23. package/dist/Scope.d.ts.map +1 -1
  24. package/dist/Scope.js +44 -13
  25. package/dist/Scope.js.map +1 -1
  26. package/dist/admin/App.d.ts +560 -9
  27. package/dist/admin/App.d.ts.map +1 -1
  28. package/dist/admin/App.js +38 -6
  29. package/dist/admin/App.js.map +1 -1
  30. package/dist/admin/apps/api-keys.d.ts +10 -24
  31. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  32. package/dist/admin/apps/api-keys.js +10 -3
  33. package/dist/admin/apps/api-keys.js.map +1 -1
  34. package/dist/admin/apps/organizations.d.ts +833 -0
  35. package/dist/admin/apps/organizations.d.ts.map +1 -0
  36. package/dist/admin/apps/organizations.js +733 -0
  37. package/dist/admin/apps/organizations.js.map +1 -0
  38. package/dist/admin/apps/scopes.d.ts +8 -0
  39. package/dist/admin/apps/scopes.d.ts.map +1 -1
  40. package/dist/admin/ui.generated.d.ts +1 -1
  41. package/dist/admin/ui.generated.d.ts.map +1 -1
  42. package/dist/admin/ui.generated.js +53 -13
  43. package/dist/admin/ui.generated.js.map +1 -1
  44. package/dist/analytics/Analytics.d.ts +10 -0
  45. package/dist/analytics/Analytics.d.ts.map +1 -1
  46. package/dist/analytics/Analytics.js +8 -0
  47. package/dist/analytics/Analytics.js.map +1 -1
  48. package/dist/analytics/tables/requestEvents.d.ts +9 -0
  49. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  50. package/dist/analytics/tables/requestEvents.js +14 -0
  51. package/dist/analytics/tables/requestEvents.js.map +1 -1
  52. package/dist/apps/data/App.d.ts +1354 -984
  53. package/dist/apps/data/App.d.ts.map +1 -1
  54. package/dist/apps/data/App.js +20 -3
  55. package/dist/apps/data/App.js.map +1 -1
  56. package/dist/apps/data/FundingRouteProviders.d.ts +94 -5
  57. package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
  58. package/dist/apps/data/FundingRouteProviders.js +196 -5
  59. package/dist/apps/data/FundingRouteProviders.js.map +1 -1
  60. package/dist/apps/data/routes/activities.d.ts +38 -38
  61. package/dist/apps/data/routes/addresses.d.ts +24 -24
  62. package/dist/apps/data/routes/balances.d.ts +45 -45
  63. package/dist/apps/data/routes/blocks.d.ts +39 -39
  64. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  65. package/dist/apps/data/routes/exchanges.d.ts +453 -453
  66. package/dist/apps/data/routes/fee-amm.d.ts +101 -101
  67. package/dist/apps/data/routes/funding.d.ts +730 -138
  68. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  69. package/dist/apps/data/routes/funding.js +234 -0
  70. package/dist/apps/data/routes/funding.js.map +1 -1
  71. package/dist/apps/data/routes/indexer.d.ts +5 -5
  72. package/dist/apps/data/routes/rpc.d.ts +31 -31
  73. package/dist/apps/data/routes/verified-tokens.d.ts +81 -81
  74. package/dist/apps/management/App.d.ts +1156 -615
  75. package/dist/apps/management/App.d.ts.map +1 -1
  76. package/dist/apps/management/App.js +2 -2
  77. package/dist/apps/management/Billing.d.ts +3 -0
  78. package/dist/apps/management/Billing.d.ts.map +1 -1
  79. package/dist/apps/management/Billing.js +7 -2
  80. package/dist/apps/management/Billing.js.map +1 -1
  81. package/dist/apps/management/routes/api-keys.d.ts +106 -90
  82. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  83. package/dist/apps/management/routes/api-keys.js +41 -30
  84. package/dist/apps/management/routes/api-keys.js.map +1 -1
  85. package/dist/apps/management/routes/billing.d.ts +278 -146
  86. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  87. package/dist/apps/management/routes/billing.js +14 -14
  88. package/dist/apps/management/routes/billing.js.map +1 -1
  89. package/dist/apps/management/routes/invitations.d.ts +93 -20
  90. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  91. package/dist/apps/management/routes/invitations.js +12 -15
  92. package/dist/apps/management/routes/invitations.js.map +1 -1
  93. package/dist/apps/management/routes/me.d.ts +13 -13
  94. package/dist/apps/management/routes/members.d.ts +89 -18
  95. package/dist/apps/management/routes/members.d.ts.map +1 -1
  96. package/dist/apps/management/routes/members.js +10 -10
  97. package/dist/apps/management/routes/members.js.map +1 -1
  98. package/dist/apps/management/routes/orgs.d.ts +130 -70
  99. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  100. package/dist/apps/management/routes/orgs.js +19 -19
  101. package/dist/apps/management/routes/orgs.js.map +1 -1
  102. package/dist/apps/management/routes/projects.d.ts +137 -48
  103. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  104. package/dist/apps/management/routes/projects.js +12 -13
  105. package/dist/apps/management/routes/projects.js.map +1 -1
  106. package/dist/apps/management/routes/scopes.d.ts +14 -28
  107. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  108. package/dist/apps/management/routes/scopes.js +6 -7
  109. package/dist/apps/management/routes/scopes.js.map +1 -1
  110. package/dist/apps/management/routes/usage.d.ts +63 -19
  111. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  112. package/dist/apps/management/routes/usage.js +9 -10
  113. package/dist/apps/management/routes/usage.js.map +1 -1
  114. package/dist/apps/mcp.d.ts.map +1 -1
  115. package/dist/apps/mcp.js +35 -3
  116. package/dist/apps/mcp.js.map +1 -1
  117. package/dist/apps/relay/App.d.ts +26 -4
  118. package/dist/apps/relay/App.d.ts.map +1 -1
  119. package/dist/apps/relay/App.js +17 -14
  120. package/dist/apps/relay/App.js.map +1 -1
  121. package/dist/db/Db.d.ts +5 -3
  122. package/dist/db/Db.d.ts.map +1 -1
  123. package/dist/db/Db.js +52 -0
  124. package/dist/db/Db.js.map +1 -1
  125. package/dist/db/Schema.d.ts +13 -0
  126. package/dist/db/Schema.d.ts.map +1 -1
  127. package/dist/db/Schema.js +16 -1
  128. package/dist/db/Schema.js.map +1 -1
  129. package/dist/db/tables/adminAuditLogs.d.ts +27 -0
  130. package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
  131. package/dist/db/tables/adminAuditLogs.js +19 -0
  132. package/dist/db/tables/adminAuditLogs.js.map +1 -0
  133. package/dist/db/tables/invitations.d.ts +1 -1
  134. package/dist/db/tables/invitations.d.ts.map +1 -1
  135. package/dist/db/tables/memberships.d.ts +32 -4
  136. package/dist/db/tables/memberships.d.ts.map +1 -1
  137. package/dist/db/tables/memberships.js +31 -0
  138. package/dist/db/tables/memberships.js.map +1 -1
  139. package/dist/db/tables/organizations.d.ts +43 -1
  140. package/dist/db/tables/organizations.d.ts.map +1 -1
  141. package/dist/db/tables/organizations.js +103 -0
  142. package/dist/db/tables/organizations.js.map +1 -1
  143. package/dist/db/tables/projects.d.ts +25 -0
  144. package/dist/db/tables/projects.d.ts.map +1 -1
  145. package/dist/db/tables/projects.js +22 -0
  146. package/dist/db/tables/projects.js.map +1 -1
  147. package/dist/db/tables/webhookDeliveries.d.ts +26 -0
  148. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  149. package/dist/db/tables/webhookDeliveries.js +37 -0
  150. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  151. package/dist/handlers/relay.d.ts +13 -10
  152. package/dist/handlers/relay.d.ts.map +1 -1
  153. package/dist/handlers/relay.js +46 -16
  154. package/dist/handlers/relay.js.map +1 -1
  155. package/dist/internal/Auth.d.ts +13 -18
  156. package/dist/internal/Auth.d.ts.map +1 -1
  157. package/dist/internal/Auth.js +78 -58
  158. package/dist/internal/Auth.js.map +1 -1
  159. package/dist/internal/Cursor.d.ts +1 -1
  160. package/dist/internal/Cursor.d.ts.map +1 -1
  161. package/dist/internal/Cursor.js +4 -0
  162. package/dist/internal/Cursor.js.map +1 -1
  163. package/dist/internal/EdgeCache.d.ts.map +1 -1
  164. package/dist/internal/EdgeCache.js +2 -0
  165. package/dist/internal/EdgeCache.js.map +1 -1
  166. package/dist/internal/FundingRoutes.d.ts +170 -0
  167. package/dist/internal/FundingRoutes.d.ts.map +1 -1
  168. package/dist/internal/FundingRoutes.js +68 -6
  169. package/dist/internal/FundingRoutes.js.map +1 -1
  170. package/dist/internal/Log.d.ts +16 -1
  171. package/dist/internal/Log.d.ts.map +1 -1
  172. package/dist/internal/Log.js +5 -3
  173. package/dist/internal/Log.js.map +1 -1
  174. package/package.json +2 -2
  175. package/src/ApiKey.ts +2 -3
  176. package/src/ApiKeys.ts +95 -17
  177. package/src/App.test.ts +44 -2
  178. package/src/App.ts +29 -9
  179. package/src/Assets.ts +8 -0
  180. package/src/Auth.test-d.ts +36 -0
  181. package/src/Auth.ts +20 -1
  182. package/src/Client.test-d.ts +64 -17
  183. package/src/Scope.test.ts +34 -0
  184. package/src/Scope.ts +53 -17
  185. package/src/admin/App.test.ts +33 -3
  186. package/src/admin/App.ts +49 -14
  187. package/src/admin/apps/api-keys.ts +10 -3
  188. package/src/admin/apps/organizations.test.ts +455 -0
  189. package/src/admin/apps/organizations.ts +953 -0
  190. package/src/admin/apps/scopes.test.ts +10 -0
  191. package/src/admin/ui/plugin.ts +1 -1
  192. package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
  193. package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
  194. package/src/admin/ui/src/lib/api-keys.ts +36 -19
  195. package/src/admin/ui/src/lib/config.ts +2 -3
  196. package/src/admin/ui/src/lib/organizations.test.ts +89 -0
  197. package/src/admin/ui/src/lib/organizations.ts +262 -0
  198. package/src/admin/ui/src/lib/query-client.ts +3 -1
  199. package/src/admin/ui/src/lib/theme.ts +57 -0
  200. package/src/admin/ui/src/routeTree.gen.ts +47 -4
  201. package/src/admin/ui/src/routes/__root.tsx +18 -3
  202. package/src/admin/ui/src/routes/api-keys.tsx +52 -7
  203. package/src/admin/ui/src/routes/index.tsx +1 -1
  204. package/src/admin/ui/src/routes/organizations.tsx +158 -0
  205. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
  206. package/src/admin/ui/src/start.ts +6 -0
  207. package/src/admin/ui.generated.ts +63 -13
  208. package/src/analytics/Analytics.ts +25 -0
  209. package/src/analytics/tables/requestEvents.ts +33 -0
  210. package/src/apps/composition.test.ts +82 -2
  211. package/src/apps/data/App.ts +24 -3
  212. package/src/apps/data/FundingRouteProviders.test.ts +204 -0
  213. package/src/apps/data/FundingRouteProviders.ts +294 -9
  214. package/src/apps/data/routes/funding.test.ts +90 -23
  215. package/src/apps/data/routes/funding.ts +315 -0
  216. package/src/apps/data/routes/rpc.test.ts +33 -0
  217. package/src/apps/management/App.ts +2 -2
  218. package/src/apps/management/Billing.ts +10 -2
  219. package/src/apps/management/routes/api-keys.test.ts +137 -1
  220. package/src/apps/management/routes/api-keys.ts +63 -38
  221. package/src/apps/management/routes/billing.ts +14 -14
  222. package/src/apps/management/routes/invitations.test.ts +102 -1
  223. package/src/apps/management/routes/invitations.ts +13 -15
  224. package/src/apps/management/routes/members.ts +10 -10
  225. package/src/apps/management/routes/orgs.test.ts +78 -16
  226. package/src/apps/management/routes/orgs.ts +21 -20
  227. package/src/apps/management/routes/projects.ts +12 -13
  228. package/src/apps/management/routes/scopes.test.ts +16 -0
  229. package/src/apps/management/routes/scopes.ts +6 -7
  230. package/src/apps/management/routes/usage.ts +9 -14
  231. package/src/apps/mcp.test.ts +25 -1
  232. package/src/apps/mcp.ts +43 -3
  233. package/src/apps/relay/App.test.ts +23 -18
  234. package/src/apps/relay/App.ts +35 -14
  235. package/src/db/Db.ts +59 -3
  236. package/src/db/Schema.ts +20 -1
  237. package/src/db/tables/adminAuditLogs.ts +47 -0
  238. package/src/db/tables/invitations.ts +1 -1
  239. package/src/db/tables/memberships.ts +59 -3
  240. package/src/db/tables/organizations.ts +148 -1
  241. package/src/db/tables/projects.ts +47 -0
  242. package/src/db/tables/webhookDeliveries.ts +83 -0
  243. package/src/handlers/relay.test-d.ts +8 -0
  244. package/src/handlers/relay.test.ts +2 -2
  245. package/src/handlers/relay.ts +62 -22
  246. package/src/internal/Auth.test.ts +174 -7
  247. package/src/internal/Auth.ts +90 -62
  248. package/src/internal/Cursor.test.ts +10 -0
  249. package/src/internal/Cursor.ts +5 -1
  250. package/src/internal/EdgeCache.test.ts +11 -0
  251. package/src/internal/EdgeCache.ts +2 -0
  252. package/src/internal/FundingRoutes.test.ts +26 -0
  253. package/src/internal/FundingRoutes.ts +187 -18
  254. package/src/internal/Log.test.ts +41 -3
  255. package/src/internal/Log.ts +23 -4
@@ -81,6 +81,53 @@ export function listByOrg(db: Db.Db, orgId: string): Promise<Record[]> {
81
81
  .execute()
82
82
  }
83
83
 
84
+ /**
85
+ * Lists a bounded page of an organization's projects, newest first.
86
+ *
87
+ * @param db - The database.
88
+ * @param orgId - The organization id.
89
+ * @param options - Keyset and page-size options.
90
+ * @returns At most `limit + 1` rows for next-cursor derivation.
91
+ */
92
+ export function listByOrgPage(
93
+ db: Db.Db,
94
+ orgId: string,
95
+ options: listByOrgPage.Options,
96
+ ): Promise<Record[]> {
97
+ const cursor = options.cursor
98
+ let query = db.kysely.selectFrom('projects').selectAll().where('orgId', '=', orgId)
99
+ if (cursor)
100
+ query = query.where((eb) =>
101
+ eb.or([
102
+ eb('createdAt', '<', cursor.createdAt),
103
+ eb.and([eb('createdAt', '=', cursor.createdAt), eb('id', '<', cursor.id)]),
104
+ ]),
105
+ )
106
+ return query
107
+ .orderBy('createdAt', 'desc')
108
+ .orderBy('id', 'desc')
109
+ .limit(options.limit + 1)
110
+ .execute()
111
+ }
112
+
113
+ export declare namespace listByOrgPage {
114
+ /** Cursor fields for the last project on the previous page. */
115
+ type Cursor = {
116
+ /** Project creation time. */
117
+ createdAt: string
118
+ /** Project id, used as a deterministic tie-breaker. */
119
+ id: string
120
+ }
121
+
122
+ /** Options for {@link listByOrgPage}. */
123
+ type Options = {
124
+ /** Last project returned by the previous page. */
125
+ cursor?: Cursor | undefined
126
+ /** Requested page size. */
127
+ limit: number
128
+ }
129
+ }
130
+
84
131
  /**
85
132
  * Renames a project, bumping `updatedAt`.
86
133
  *
@@ -10,6 +10,12 @@ export type Table = db_Schema.WebhookDelivery
10
10
  /** A stored webhook-delivery row. */
11
11
  export type Record = Selectable<Table>
12
12
 
13
+ /** A failed delivery joined to its organization-owned subscription. */
14
+ export type OrganizationFailure = Pick<
15
+ Record,
16
+ 'attempt' | 'createdAt' | 'error' | 'id' | 'responseStatus' | 'subscriptionId'
17
+ >
18
+
13
19
  /** Maps a stored row to the domain delivery (null columns → absent fields). */
14
20
  export function toDelivery(row: Record): Webhooks.Delivery {
15
21
  return {
@@ -85,6 +91,83 @@ export async function list(
85
91
  return (await query.execute()).map(toDelivery)
86
92
  }
87
93
 
94
+ /** Lists recent failed webhook deliveries across an organization. */
95
+ export function listFailuresByOrg(
96
+ db: Db.Db,
97
+ orgId: string,
98
+ options: listFailuresByOrg.Options,
99
+ ): Promise<OrganizationFailure[]> {
100
+ return db.kysely
101
+ .selectFrom('webhook_deliveries')
102
+ .innerJoin(
103
+ 'webhook_subscriptions',
104
+ 'webhook_subscriptions.id',
105
+ 'webhook_deliveries.subscriptionId',
106
+ )
107
+ .select([
108
+ 'webhook_deliveries.attempt',
109
+ 'webhook_deliveries.createdAt',
110
+ 'webhook_deliveries.error',
111
+ 'webhook_deliveries.id',
112
+ 'webhook_deliveries.responseStatus',
113
+ 'webhook_deliveries.subscriptionId',
114
+ ])
115
+ .where('webhook_subscriptions.ownerId', '=', orgId)
116
+ .where('webhook_subscriptions.ownerType', '=', 'api_key')
117
+ .where('webhook_deliveries.createdAt', '>=', options.from)
118
+ .where('webhook_deliveries.expiresAt', '>', options.now)
119
+ .where('webhook_deliveries.status', '=', 'failed')
120
+ .orderBy('webhook_deliveries.createdAt', 'desc')
121
+ .orderBy('webhook_deliveries.id', 'desc')
122
+ .limit(options.limit)
123
+ .execute()
124
+ }
125
+
126
+ export declare namespace listFailuresByOrg {
127
+ /** Bounds for the organization failure scan. */
128
+ type Options = {
129
+ /** Window start (ISO 8601), inclusive. */
130
+ from: string
131
+ /** Maximum failures to return. */
132
+ limit: number
133
+ /** ISO timestamp retention is evaluated against. */
134
+ now: string
135
+ }
136
+ }
137
+
138
+ /** Counts failed webhook deliveries across an organization in a time window. */
139
+ export async function countFailuresByOrg(
140
+ db: Db.Db,
141
+ orgId: string,
142
+ options: countFailuresByOrg.Options,
143
+ ): Promise<number> {
144
+ const row = await db.kysely
145
+ .selectFrom('webhook_deliveries')
146
+ .innerJoin(
147
+ 'webhook_subscriptions',
148
+ 'webhook_subscriptions.id',
149
+ 'webhook_deliveries.subscriptionId',
150
+ )
151
+ .select((eb) => eb.fn.countAll().as('count'))
152
+ .where('webhook_subscriptions.ownerId', '=', orgId)
153
+ .where('webhook_subscriptions.ownerType', '=', 'api_key')
154
+ .where('webhook_deliveries.createdAt', '>=', options.from)
155
+ .where('webhook_deliveries.expiresAt', '>', options.now)
156
+ .where('webhook_deliveries.status', '=', 'failed')
157
+ .executeTakeFirstOrThrow()
158
+ return Number(row.count)
159
+ }
160
+
161
+ export declare namespace countFailuresByOrg {
162
+ /** Bounds for the organization failure count. */
163
+ type Options = {
164
+ /** Window start (ISO 8601), inclusive. */
165
+ from: string
166
+ /** ISO timestamp retention is evaluated against. */
167
+ now: string
168
+ }
169
+ }
170
+
88
171
  export declare namespace list {
89
172
  /** Options for {@link list}. */
90
173
  type Options = {
@@ -0,0 +1,8 @@
1
+ import { Handler } from 'tapimo'
2
+
3
+ describe('Handler.relay', () => {
4
+ test('accepts Tempo API defaults', () => {
5
+ expectTypeOf(Handler.relay()).toEqualTypeOf<Handler.Handler>()
6
+ expectTypeOf(Handler.relay({ apiKey: 'secret_api_key' })).toEqualTypeOf<Handler.Handler>()
7
+ })
8
+ })
@@ -16,8 +16,8 @@ const recipient = Relay.accounts[7]!
16
16
 
17
17
  /**
18
18
  * Tokens the relay handler probes for fee-token resolution. The default
19
- * `resolveTokens` fetches `tokenlist.tempo.xyz`, which doesn't know about
20
- * the localnet chain — so tests inject this list explicitly.
19
+ * `resolveTokens` uses the Tempo API for mainnet and testnet, so localnet
20
+ * tests inject this list explicitly.
21
21
  */
22
22
  const localnetTokens = [
23
23
  {
@@ -16,8 +16,10 @@ import {
16
16
  import type { LocalAccount } from 'viem/accounts'
17
17
  import { simulateCalls } from 'viem/actions'
18
18
  import { Abis, Actions, Addresses, Capabilities, VirtualAddress } from 'viem/tempo'
19
+ import { tempoMainnet, tempoTestnet } from 'viem/tempo/chains'
19
20
 
20
21
  import * as ExecutionError from './internal/executionError.js'
22
+ import * as TempoApi from '../Client.js'
21
23
  import type * as RpcSchema from '../internal/RpcSchema.js'
22
24
  import { type Handler, from } from '../Handler.js'
23
25
  import * as Store from '../internal/Store.js'
@@ -25,6 +27,8 @@ import * as Multisig from './internal/multisig.js'
25
27
  import * as Sponsorship from './internal/sponsorship.js'
26
28
  import * as Utils from './internal/utils.js'
27
29
 
30
+ const defaultChains = [tempoMainnet, tempoTestnet] as const
31
+
28
32
  /**
29
33
  * Instantiates a relay handler that proxies `eth_fillTransaction`
30
34
  * with wallet-aware enrichment (fee token resolution, simulation,
@@ -32,9 +36,9 @@ import * as Utils from './internal/utils.js'
32
36
  *
33
37
  * @example
34
38
  * ```ts
35
- * import { Handler } from 'tempo-api/server'
39
+ * import { Handler } from 'tapimo'
36
40
  *
37
- * const handler = Handler.relay({ getClient })
41
+ * const handler = Handler.relay({ apiKey: process.env.TEMPO_API_KEY })
38
42
  *
39
43
  * // Plug handler into your server framework of choice:
40
44
  * createServer(handler.listener) // Node.js
@@ -51,10 +55,9 @@ import * as Utils from './internal/utils.js'
51
55
  *
52
56
  * ```ts
53
57
  * import { privateKeyToAccount } from 'viem/accounts'
54
- * import { Handler } from 'tempo-api/server'
58
+ * import { Handler } from 'tapimo'
55
59
  *
56
60
  * const handler = Handler.relay({
57
- * getClient,
58
61
  * feePayer: {
59
62
  * account: privateKeyToAccount('0x...'),
60
63
  * },
@@ -64,26 +67,26 @@ import * as Utils from './internal/utils.js'
64
67
  * @param options - Options.
65
68
  * @returns Request handler.
66
69
  */
67
- export function relay(options: relay.Options): Handler {
70
+ export function relay(options: relay.Options = {}): Handler {
68
71
  const {
69
- getClient,
72
+ apiKey,
73
+ getClient: getClient_,
70
74
  internal_allowUnsafeUrls = false,
71
75
  onRequest,
72
76
  path = '/',
73
- resolveTokens,
77
+ resolveTokens: resolveTokens_,
74
78
  ...rest
75
79
  } = options
80
+ const getClient = getClient_ ?? defaultGetClient({ apiKey })
81
+ const resolveTokens = resolveTokens_ ?? defaultResolveTokens({ apiKey })
76
82
  const feePayerOptions = options.feePayer
77
83
  // Adapt the state store into the multisig operation store once, up front.
78
84
  const multisigOptions = options.multisig
79
85
  ? { ...options.multisig, store: Multisig.fromStore(options.multisig.store) }
80
86
  : undefined
81
87
 
82
- // Resolves the chain's fee-token candidates through the caller-provided
83
- // resolver. An unconfigured resolver (or a chain it has no list for) yields
84
- // an empty candidate set.
85
88
  const getTokens = async (chainId: number | undefined): Promise<readonly Address[]> => {
86
- if (!resolveTokens || chainId === undefined) return []
89
+ if (chainId === undefined) return []
87
90
  return await resolveTokens(chainId)
88
91
  }
89
92
 
@@ -94,7 +97,7 @@ export function relay(options: relay.Options): Handler {
94
97
 
95
98
  const features = {
96
99
  autoSwap: options.autoSwap ?? options.features === 'all',
97
- feeTokenResolution: options.features === 'all' || Boolean(resolveTokens),
100
+ feeTokenResolution: options.features === 'all' || Boolean(resolveTokens_),
98
101
  simulate: options.features === 'all',
99
102
  }
100
103
 
@@ -175,6 +178,7 @@ export function relay(options: relay.Options): Handler {
175
178
  let filled: Awaited<ReturnType<typeof fill>>
176
179
  let sponsored = false
177
180
  let feeToken = sponsoredFeeToken
181
+ const tokens = getTokens(chainId)
178
182
 
179
183
  // Lazily resolve a swap source token when autoSwap needs one.
180
184
  const resolveFeeTokenForSwap = from
@@ -183,7 +187,7 @@ export function relay(options: relay.Options): Handler {
183
187
  account: from,
184
188
  feeToken: undefined,
185
189
  store,
186
- tokens: (await getTokens(chainId)).filter(
190
+ tokens: (await tokens).filter(
187
191
  (t) => t.toLowerCase() !== insufficientToken.toLowerCase(),
188
192
  ),
189
193
  })
@@ -194,7 +198,7 @@ export function relay(options: relay.Options): Handler {
194
198
  // the transaction is calling — typically the token being
195
199
  // transferred — so a user transferring USDC.e can pay gas in
196
200
  // USDC.e even when the configured list defaults to pathUSD.
197
- const configuredTokens = await getTokens(chainId)
201
+ const configuredTokens = await tokens
198
202
  const unsponsoredTokens = [
199
203
  ...configuredTokens,
200
204
  ...callTargetTokens(baseTx).filter(
@@ -221,7 +225,7 @@ export function relay(options: relay.Options): Handler {
221
225
  // `validate` only governs the wallet's own fee payer).
222
226
  // Default to the chain's first token so the broadcast envelope
223
227
  // always carries a feeToken the chain can charge.
224
- if (!feeToken) feeToken = (await getTokens(chainId))[0]
228
+ if (!feeToken) feeToken = configuredTokens[0]
225
229
  const transaction = {
226
230
  ...baseTx,
227
231
  feePayer: true,
@@ -334,8 +338,7 @@ export function relay(options: relay.Options): Handler {
334
338
  const transaction_filled = filled.transaction
335
339
  const swap = 'swap' in filled ? filled.swap : undefined
336
340
  if (!feeToken)
337
- feeToken =
338
- (transaction_filled.feeToken as Address | undefined) ?? (await getTokens(chainId))[0]
341
+ feeToken = (transaction_filled.feeToken as Address | undefined) ?? configuredTokens[0]
339
342
 
340
343
  // Parallelize: simulate, fee payer signing, and autoSwap metadata.
341
344
  const alreadySigned =
@@ -648,8 +651,46 @@ export function relay(options: relay.Options): Handler {
648
651
  return router
649
652
  }
650
653
 
654
+ function defaultGetClient(options: { apiKey?: string | undefined }) {
655
+ const clients = new Map<number, Client>()
656
+ for (const chain of defaultChains) {
657
+ const transport = options.apiKey
658
+ ? http(new URL(`/rpc/${chain.id}`, TempoApi.defaultUrl).toString(), {
659
+ fetchOptions: { headers: { 'tempo-api-key': options.apiKey } },
660
+ })
661
+ : http()
662
+ clients.set(
663
+ chain.id,
664
+ createClient({ batch: { multicall: { deployless: true } }, chain, transport }),
665
+ )
666
+ }
667
+
668
+ return (chainId: number = tempoMainnet.id): Client => {
669
+ const client = clients.get(chainId)
670
+ if (!client) throw new Error(`Chain ${chainId} not configured`)
671
+ return client
672
+ }
673
+ }
674
+
675
+ function defaultResolveTokens(options: { apiKey?: string | undefined }) {
676
+ const client = TempoApi.create({ apiKey: options.apiKey })
677
+
678
+ return async (chainId: number): Promise<readonly Address[]> => {
679
+ if (!defaultChains.some((chain) => chain.id === chainId)) return []
680
+ const response = await client.v1.tokenlist.$get({ query: { chainId: String(chainId) } })
681
+ if (response.status !== 200) return []
682
+ const { tokens } = await response.json()
683
+ return tokens.map((token) => token.address)
684
+ }
685
+ }
686
+
651
687
  export namespace relay {
652
688
  export type Options = from.Options & {
689
+ /**
690
+ * Tempo API key used by the default RPC clients and token resolver. When
691
+ * omitted, RPC clients use each chain's default transport.
692
+ */
693
+ apiKey?: string | undefined
653
694
  /**
654
695
  * Auto-swap options.
655
696
  */
@@ -663,9 +704,9 @@ export namespace relay {
663
704
  /**
664
705
  * Resolves a viem client for a chain id. The handler resolves the client
665
706
  * from the `chainId` in the incoming transaction (falling back to the
666
- * client's default chain). Called with no argument for the default client.
707
+ * client's default chain). Defaults to Tempo mainnet and testnet clients.
667
708
  */
668
- getClient: (chainId?: number) => Client
709
+ getClient?: ((chainId?: number) => Client) | undefined
669
710
  /**
670
711
  * Fee payer / sponsor configuration. When provided, the relay will
671
712
  * sign `feePayerSignature` on the filled transaction.
@@ -721,9 +762,8 @@ export namespace relay {
721
762
  /** Path to use for the handler. @default "/" */
722
763
  path?: string | undefined
723
764
  /**
724
- * Resolves the known fee-token candidates for a chain. The relay checks
725
- * the caller's balance for each candidate and picks the highest during
726
- * fee-token resolution. Omit to disable fee-token resolution.
765
+ * Resolves the known fee-token candidates for a chain. Defaults to the
766
+ * Tempo API verified token list for mainnet and testnet.
727
767
  */
728
768
  resolveTokens?:
729
769
  | ((chainId: number) => readonly Address[] | Promise<readonly Address[]>)
@@ -1,8 +1,13 @@
1
- import { Hono } from 'hono'
1
+ import { Hono, type Context } from 'hono'
2
2
  import { requestId } from 'hono/request-id'
3
3
  import { Mppx, tempo } from 'mppx/hono'
4
4
  import { RateLimit } from 'tapimo'
5
5
  import * as ApiKey from '../ApiKey.js'
6
+ import type * as Db from '../db/Db.js'
7
+ import * as Memberships from '../db/tables/memberships.js'
8
+ import * as Organizations from '../db/tables/organizations.js'
9
+ import * as Projects from '../db/tables/projects.js'
10
+ import * as Users from '../db/tables/users.js'
6
11
  import * as Auth from './Auth.js'
7
12
  import * as Log from './Log.js'
8
13
  import * as Scope from '../Scope.js'
@@ -75,12 +80,13 @@ describe('require', () => {
75
80
  `)
76
81
  })
77
82
 
78
- test('accepts API key from deprecated key query param', async () => {
83
+ test('accepts API key from key query param', async () => {
79
84
  const app = createApp()
80
85
  const response = await app.request(`/protected?key=${token}`)
81
86
  const body = await response.json()
82
87
 
83
88
  expect(response.status).toMatchInlineSnapshot(`200`)
89
+ expect(response.headers.get('cache-control')).toMatchInlineSnapshot(`"private"`)
84
90
  expect(body).toMatchInlineSnapshot(`
85
91
  {
86
92
  "id": "key_test",
@@ -90,11 +96,30 @@ describe('require', () => {
90
96
  `)
91
97
  })
92
98
 
93
- test('strips the deprecated key query param before route validation', async () => {
94
- // Regression: route query schemas are strict (`z.strictObject`), so the
95
- // `key` auth param must be removed from the URL once consumed — otherwise
96
- // the downstream validator rejects it as `query_invalid` and the legacy
97
- // `?key=...` lane breaks on every strict-query endpoint.
99
+ test('keeps query-authenticated responses private after downstream caching', async () => {
100
+ const auth = {
101
+ apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
102
+ rateLimit: RateLimit.memory(),
103
+ }
104
+ const app = new Hono<Auth.Environment>()
105
+ app.use('*', async (c, next) => {
106
+ c.set('auth', auth)
107
+ await next()
108
+ })
109
+ app.use('/cached', Auth.require({ apiKey: { scopes: ['data:read'] } }))
110
+ app.get('/cached', (c) => {
111
+ c.header('Cache-Control', 'public, max-age=86400')
112
+ return c.json({ ok: true })
113
+ })
114
+
115
+ const response = await app.request(`/cached?key=${token}`)
116
+
117
+ expect(response.status).toMatchInlineSnapshot(`200`)
118
+ expect(response.headers.get('cache-control')).toMatchInlineSnapshot(`"private"`)
119
+ })
120
+
121
+ test('strips the key query param before route validation', async () => {
122
+ // Auth owns `key`, so strict route schemas must not see it.
98
123
  const auth = {
99
124
  apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
100
125
  rateLimit: RateLimit.memory(),
@@ -1298,6 +1323,87 @@ describe('require', () => {
1298
1323
  })
1299
1324
  })
1300
1325
 
1326
+ describe('ensureProject', () => {
1327
+ test('authorizes a flat project route against the API key organization', async () => {
1328
+ const db = TestApp.database()
1329
+ const organization = await Organizations.create(db, { id: key.orgId, name: 'Test org' })
1330
+ const project = await Projects.create(db, { name: 'Test project', orgId: organization.id })
1331
+ const response = await createProjectGuardApp(db, apiKeyPrincipal(organization.id)).request(
1332
+ `/v1/projects/${project.id}/resource`,
1333
+ )
1334
+
1335
+ expect(response.status).toMatchInlineSnapshot(`200`)
1336
+ expect(await response.json()).toEqual({
1337
+ membership: null,
1338
+ orgId: organization.id,
1339
+ projectId: project.id,
1340
+ })
1341
+ })
1342
+
1343
+ test('hides a flat project from an API key in another organization', async () => {
1344
+ const db = TestApp.database()
1345
+ const organization = await Organizations.create(db, { id: key.orgId, name: 'Test org' })
1346
+ const project = await Projects.create(db, { name: 'Test project', orgId: organization.id })
1347
+ const response = await createProjectGuardApp(db, apiKeyPrincipal('org_foreign')).request(
1348
+ `/v1/projects/${project.id}/resource`,
1349
+ )
1350
+
1351
+ expect(response.status).toMatchInlineSnapshot(`404`)
1352
+ expect(await response.json()).toMatchInlineSnapshot(`
1353
+ {
1354
+ "error": {
1355
+ "code": "project_not_found",
1356
+ "message": "Project not found",
1357
+ },
1358
+ }
1359
+ `)
1360
+ })
1361
+
1362
+ test('resolves session membership for a flat project route', async () => {
1363
+ const db = TestApp.database()
1364
+ const user = await Users.upsertByAddress(db, {
1365
+ address: '0x0000000000000000000000000000000000000001',
1366
+ })
1367
+ const organization = await Organizations.create(db, { name: 'Test org' })
1368
+ await Memberships.create(db, { orgId: organization.id, role: 'member', userId: user.id })
1369
+ const project = await Projects.create(db, { name: 'Test project', orgId: organization.id })
1370
+ const response = await createProjectGuardApp(db, sessionPrincipal(user.id)).request(
1371
+ `/v1/projects/${project.id}/resource`,
1372
+ )
1373
+ const body = (await response.json()) as Record<string, unknown>
1374
+
1375
+ expect(response.status).toMatchInlineSnapshot(`200`)
1376
+ expect(body).toEqual({
1377
+ membership: 'member',
1378
+ orgId: organization.id,
1379
+ projectId: project.id,
1380
+ })
1381
+ })
1382
+
1383
+ test('preserves nested organization role authorization', async () => {
1384
+ const db = TestApp.database()
1385
+ const user = await Users.upsertByAddress(db, {
1386
+ address: '0x0000000000000000000000000000000000000001',
1387
+ })
1388
+ const organization = await Organizations.create(db, { name: 'Test org' })
1389
+ await Memberships.create(db, { orgId: organization.id, role: 'member', userId: user.id })
1390
+ const project = await Projects.create(db, { name: 'Test project', orgId: organization.id })
1391
+ const response = await createNestedProjectGuardApp(db, sessionPrincipal(user.id)).request(
1392
+ `/v1/orgs/${organization.id}/projects/${project.id}/resource`,
1393
+ )
1394
+
1395
+ expect(response.status).toMatchInlineSnapshot(`403`)
1396
+ expect(await response.json()).toMatchInlineSnapshot(`
1397
+ {
1398
+ "error": {
1399
+ "code": "forbidden",
1400
+ "message": "Requires the admin role",
1401
+ },
1402
+ }
1403
+ `)
1404
+ })
1405
+ })
1406
+
1301
1407
  function createScopedApp(options: {
1302
1408
  auth?: Auth.Context | undefined
1303
1409
  routes: readonly { path: string; policy: Auth.require.Policy }[]
@@ -1322,6 +1428,67 @@ function createScopedApp(options: {
1322
1428
  return app
1323
1429
  }
1324
1430
 
1431
+ type GuardEnvironment = {
1432
+ Variables: Auth.Variables & { db: Db.Source }
1433
+ }
1434
+
1435
+ function createProjectGuardApp(db: Db.Source, principal: Auth.Principal) {
1436
+ const app = new Hono<GuardEnvironment>()
1437
+ app.use('*', async (c, next) => {
1438
+ c.set('db', db)
1439
+ c.set('principal', principal)
1440
+ await next()
1441
+ })
1442
+ app.get('/v1/projects/:projectId/resource', Auth.ensureProject(), projectGuardHandler)
1443
+ return app
1444
+ }
1445
+
1446
+ function createNestedProjectGuardApp(db: Db.Source, principal: Auth.Principal) {
1447
+ const app = new Hono<GuardEnvironment>()
1448
+ app.use('*', async (c, next) => {
1449
+ c.set('db', db)
1450
+ c.set('principal', principal)
1451
+ await next()
1452
+ })
1453
+ app.get(
1454
+ '/v1/orgs/:orgId/projects/:projectId/resource',
1455
+ Auth.ensureOrg({ role: 'admin' }),
1456
+ Auth.ensureProject(),
1457
+ projectGuardHandler,
1458
+ )
1459
+ return app
1460
+ }
1461
+
1462
+ function projectGuardHandler(c: Context<GuardEnvironment>) {
1463
+ return c.json({
1464
+ membership: Auth.membership(c)?.role ?? null,
1465
+ orgId: Auth.org(c).id,
1466
+ projectId: Auth.project(c).id,
1467
+ })
1468
+ }
1469
+
1470
+ function apiKeyPrincipal(orgId: string): Auth.Principal {
1471
+ const apiKey = { ...key, orgId }
1472
+ return {
1473
+ apiKey,
1474
+ environment: apiKey.environment,
1475
+ id: apiKey.id,
1476
+ orgId,
1477
+ type: 'api_key',
1478
+ }
1479
+ }
1480
+
1481
+ function sessionPrincipal(id: string): Auth.Principal {
1482
+ return {
1483
+ id,
1484
+ identity: {
1485
+ provider: 'wallet',
1486
+ subject: '0x0000000000000000000000000000000000000001',
1487
+ },
1488
+ type: 'session',
1489
+ }
1490
+ }
1491
+
1325
1492
  type Options = {
1326
1493
  auth?: Auth.Context | undefined
1327
1494
  logger?: Log.Emit | undefined