tapimo 0.4.4 → 0.5.1

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 (229) hide show
  1. package/dist/App.d.ts +4701 -3416
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +24 -2
  4. package/dist/App.js.map +1 -1
  5. package/dist/Assets.js +3 -3
  6. package/dist/Assets.js.map +1 -1
  7. package/dist/Scope.d.ts +18 -1
  8. package/dist/Scope.d.ts.map +1 -1
  9. package/dist/Scope.js +23 -3
  10. package/dist/Scope.js.map +1 -1
  11. package/dist/Webhooks.d.ts +1 -1
  12. package/dist/Webhooks.d.ts.map +1 -1
  13. package/dist/Webhooks.js +5 -10
  14. package/dist/Webhooks.js.map +1 -1
  15. package/dist/admin/App.d.ts +6 -34
  16. package/dist/admin/App.d.ts.map +1 -1
  17. package/dist/admin/apps/api-keys.d.ts +3 -3
  18. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  19. package/dist/admin/apps/api-keys.js +4 -1
  20. package/dist/admin/apps/api-keys.js.map +1 -1
  21. package/dist/admin/apps/scopes.d.ts +6 -34
  22. package/dist/admin/apps/scopes.d.ts.map +1 -1
  23. package/dist/admin/apps/scopes.js +2 -2
  24. package/dist/admin/apps/scopes.js.map +1 -1
  25. package/dist/admin/apps/verified-tokens.d.ts +57 -57
  26. package/dist/admin/ui.generated.d.ts +1 -1
  27. package/dist/admin/ui.generated.js +33 -33
  28. package/dist/admin/ui.generated.js.map +1 -1
  29. package/dist/apps/data/App.d.ts +4574 -4277
  30. package/dist/apps/data/App.d.ts.map +1 -1
  31. package/dist/apps/data/App.js +155 -77
  32. package/dist/apps/data/App.js.map +1 -1
  33. package/dist/apps/data/FundingQuoteProviders.d.ts +109 -0
  34. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -0
  35. package/dist/apps/data/FundingQuoteProviders.js +464 -0
  36. package/dist/apps/data/FundingQuoteProviders.js.map +1 -0
  37. package/dist/apps/data/index.d.ts +1 -1
  38. package/dist/apps/data/index.js +1 -1
  39. package/dist/apps/data/routes/activities.d.ts +26 -26
  40. package/dist/apps/data/routes/addresses.d.ts +38 -38
  41. package/dist/apps/data/routes/balances.d.ts +45 -45
  42. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  43. package/dist/apps/data/routes/balances.js +67 -149
  44. package/dist/apps/data/routes/balances.js.map +1 -1
  45. package/dist/apps/data/routes/blocks.d.ts +2 -3
  46. package/dist/apps/data/routes/blocks.d.ts.map +1 -1
  47. package/dist/apps/data/routes/blocks.js +3 -5
  48. package/dist/apps/data/routes/blocks.js.map +1 -1
  49. package/dist/apps/data/routes/coingecko.d.ts +177 -176
  50. package/dist/apps/data/routes/coingecko.d.ts.map +1 -1
  51. package/dist/apps/data/routes/coingecko.js +3 -8
  52. package/dist/apps/data/routes/coingecko.js.map +1 -1
  53. package/dist/apps/data/routes/funding.d.ts +131 -228
  54. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  55. package/dist/apps/data/routes/funding.js +152 -219
  56. package/dist/apps/data/routes/funding.js.map +1 -1
  57. package/dist/apps/data/routes/indexer.d.ts +34 -34
  58. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  59. package/dist/apps/data/routes/indexer.js +9 -2
  60. package/dist/apps/data/routes/indexer.js.map +1 -1
  61. package/dist/apps/data/routes/receipts.d.ts +228 -228
  62. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  63. package/dist/apps/data/routes/rpc.js +86 -16
  64. package/dist/apps/data/routes/rpc.js.map +1 -1
  65. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  66. package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
  67. package/dist/apps/data/routes/webhooks.d.ts +340 -0
  68. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  69. package/dist/apps/data/routes/webhooks.js +13 -1
  70. package/dist/apps/data/routes/webhooks.js.map +1 -1
  71. package/dist/apps/index.d.ts +1 -1
  72. package/dist/apps/index.js +1 -1
  73. package/dist/apps/management/App.d.ts +1833 -851
  74. package/dist/apps/management/App.d.ts.map +1 -1
  75. package/dist/apps/management/App.js +4 -0
  76. package/dist/apps/management/App.js.map +1 -1
  77. package/dist/apps/management/routes/api-keys.d.ts +41 -41
  78. package/dist/apps/management/routes/api-keys.js +2 -2
  79. package/dist/apps/management/routes/api-keys.js.map +1 -1
  80. package/dist/apps/management/routes/billing.d.ts +48 -48
  81. package/dist/apps/management/routes/invitations.d.ts +132 -132
  82. package/dist/apps/management/routes/invite-links.d.ts +1100 -0
  83. package/dist/apps/management/routes/invite-links.d.ts.map +1 -0
  84. package/dist/apps/management/routes/invite-links.js +411 -0
  85. package/dist/apps/management/routes/invite-links.js.map +1 -0
  86. package/dist/apps/management/routes/members.d.ts +41 -41
  87. package/dist/apps/management/routes/orgs.d.ts +92 -92
  88. package/dist/apps/management/routes/projects.d.ts +9 -9
  89. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  90. package/dist/apps/management/routes/scopes.js +2 -2
  91. package/dist/apps/management/routes/scopes.js.map +1 -1
  92. package/dist/apps/management/routes/usage.d.ts +24 -24
  93. package/dist/apps/mcp.js +2 -2
  94. package/dist/apps/mcp.js.map +1 -1
  95. package/dist/apps/relay/App.d.ts +2 -2
  96. package/dist/apps/relay/App.d.ts.map +1 -1
  97. package/dist/apps/relay/App.js +44 -6
  98. package/dist/apps/relay/App.js.map +1 -1
  99. package/dist/db/Db.d.ts +3 -0
  100. package/dist/db/Db.d.ts.map +1 -1
  101. package/dist/db/Db.js +57 -0
  102. package/dist/db/Db.js.map +1 -1
  103. package/dist/db/Schema.d.ts +31 -0
  104. package/dist/db/Schema.d.ts.map +1 -1
  105. package/dist/db/Schema.js +31 -0
  106. package/dist/db/Schema.js.map +1 -1
  107. package/dist/db/tables/inviteLinks.d.ts +42 -0
  108. package/dist/db/tables/inviteLinks.d.ts.map +1 -0
  109. package/dist/db/tables/inviteLinks.js +148 -0
  110. package/dist/db/tables/inviteLinks.js.map +1 -0
  111. package/dist/db/tables/organizations.d.ts.map +1 -1
  112. package/dist/db/tables/organizations.js +2 -0
  113. package/dist/db/tables/organizations.js.map +1 -1
  114. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  115. package/dist/db/tables/sponsoredTransactions.js +6 -2
  116. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  117. package/dist/handlers/relay.d.ts.map +1 -1
  118. package/dist/handlers/relay.js +13 -3
  119. package/dist/handlers/relay.js.map +1 -1
  120. package/dist/internal/Auth.d.ts +9 -1
  121. package/dist/internal/Auth.d.ts.map +1 -1
  122. package/dist/internal/Auth.js +138 -10
  123. package/dist/internal/Auth.js.map +1 -1
  124. package/dist/internal/EdgeCache.d.ts +3 -1
  125. package/dist/internal/EdgeCache.d.ts.map +1 -1
  126. package/dist/internal/EdgeCache.js +17 -2
  127. package/dist/internal/EdgeCache.js.map +1 -1
  128. package/dist/internal/{FundingRoutes.d.ts → FundingQuotes.d.ts} +240 -369
  129. package/dist/internal/FundingQuotes.d.ts.map +1 -0
  130. package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +144 -183
  131. package/dist/internal/FundingQuotes.js.map +1 -0
  132. package/dist/internal/Log.d.ts +18 -12
  133. package/dist/internal/Log.d.ts.map +1 -1
  134. package/dist/internal/Log.js +2 -0
  135. package/dist/internal/Log.js.map +1 -1
  136. package/dist/internal/Tidx.d.ts +9 -3
  137. package/dist/internal/Tidx.d.ts.map +1 -1
  138. package/dist/internal/Tidx.js +13 -7
  139. package/dist/internal/Tidx.js.map +1 -1
  140. package/dist/internal/Viem.d.ts +17 -2
  141. package/dist/internal/Viem.d.ts.map +1 -1
  142. package/dist/internal/Viem.js +30 -3
  143. package/dist/internal/Viem.js.map +1 -1
  144. package/dist/internal/index.d.ts +1 -1
  145. package/dist/internal/index.js +1 -1
  146. package/package.json +2 -2
  147. package/src/ApiKeys.test.ts +25 -0
  148. package/src/App.test-d.ts +2 -0
  149. package/src/App.test.ts +13 -2
  150. package/src/App.ts +37 -5
  151. package/src/Assets.ts +3 -3
  152. package/src/Client.test-d.ts +21 -11
  153. package/src/Scope.test.ts +1 -0
  154. package/src/Scope.ts +32 -4
  155. package/src/Webhooks.ts +5 -10
  156. package/src/admin/apps/api-keys.test.ts +27 -0
  157. package/src/admin/apps/api-keys.ts +8 -1
  158. package/src/admin/apps/scopes.test.ts +7 -2
  159. package/src/admin/apps/scopes.ts +2 -2
  160. package/src/admin/ui/src/components/Dialog.tsx +8 -2
  161. package/src/admin/ui/src/lib/scopes.test.ts +19 -0
  162. package/src/admin/ui/src/lib/scopes.ts +12 -0
  163. package/src/admin/ui/src/routes/api-keys.tsx +128 -27
  164. package/src/admin/ui.generated.ts +33 -33
  165. package/src/apps/composition.test.ts +77 -0
  166. package/src/apps/data/App.test.ts +354 -0
  167. package/src/apps/data/App.ts +178 -91
  168. package/src/apps/data/FundingQuoteProviders.test.ts +577 -0
  169. package/src/apps/data/FundingQuoteProviders.ts +633 -0
  170. package/src/apps/data/index.ts +1 -1
  171. package/src/apps/data/routes/balances.test.ts +64 -18
  172. package/src/apps/data/routes/balances.ts +76 -177
  173. package/src/apps/data/routes/blocks.ts +3 -5
  174. package/src/apps/data/routes/coingecko.ts +3 -8
  175. package/src/apps/data/routes/funding.test.ts +304 -192
  176. package/src/apps/data/routes/funding.ts +166 -286
  177. package/src/apps/data/routes/indexer.test.ts +173 -15
  178. package/src/apps/data/routes/indexer.ts +8 -2
  179. package/src/apps/data/routes/rpc.test.ts +219 -0
  180. package/src/apps/data/routes/rpc.ts +92 -17
  181. package/src/apps/data/routes/webhooks.test.ts +21 -0
  182. package/src/apps/data/routes/webhooks.ts +13 -1
  183. package/src/apps/index.ts +1 -1
  184. package/src/apps/management/App.ts +4 -0
  185. package/src/apps/management/Billing.test.ts +2 -2
  186. package/src/apps/management/routes/api-keys.test.ts +47 -3
  187. package/src/apps/management/routes/api-keys.ts +2 -2
  188. package/src/apps/management/routes/invite-links.test.ts +385 -0
  189. package/src/apps/management/routes/invite-links.ts +570 -0
  190. package/src/apps/management/routes/scopes.test.ts +3 -2
  191. package/src/apps/management/routes/scopes.ts +4 -2
  192. package/src/apps/mcp.test.ts +18 -0
  193. package/src/apps/mcp.ts +2 -2
  194. package/src/apps/relay/App.test.ts +328 -7
  195. package/src/apps/relay/App.ts +42 -6
  196. package/src/apps/relay/Sponsorships.test.ts +2 -2
  197. package/src/db/Db.test.ts +5 -5
  198. package/src/db/Db.ts +65 -0
  199. package/src/db/Schema.ts +39 -0
  200. package/src/db/tables/billingSettings.test.ts +2 -2
  201. package/src/db/tables/inviteLinks.ts +187 -0
  202. package/src/db/tables/organizations.test.ts +2 -2
  203. package/src/db/tables/organizations.ts +2 -0
  204. package/src/db/tables/sponsoredTransactions.test.ts +19 -2
  205. package/src/db/tables/sponsoredTransactions.ts +21 -14
  206. package/src/db/tables/stripeCustomers.test.ts +2 -2
  207. package/src/handlers/relay.ts +16 -4
  208. package/src/internal/Auth.test.ts +132 -22
  209. package/src/internal/Auth.ts +181 -14
  210. package/src/internal/EdgeCache.test.ts +24 -3
  211. package/src/internal/EdgeCache.ts +18 -2
  212. package/src/internal/FundingQuotes.test.ts +268 -0
  213. package/src/internal/{FundingRoutes.ts → FundingQuotes.ts} +216 -277
  214. package/src/internal/Log.ts +21 -14
  215. package/src/internal/Tidx.test.ts +48 -0
  216. package/src/internal/Tidx.ts +21 -7
  217. package/src/internal/Viem.test.ts +46 -0
  218. package/src/internal/Viem.ts +48 -6
  219. package/src/internal/index.test.ts +1 -1
  220. package/src/internal/index.ts +1 -1
  221. package/dist/apps/data/FundingRouteProviders.d.ts +0 -36
  222. package/dist/apps/data/FundingRouteProviders.d.ts.map +0 -1
  223. package/dist/apps/data/FundingRouteProviders.js +0 -245
  224. package/dist/apps/data/FundingRouteProviders.js.map +0 -1
  225. package/dist/internal/FundingRoutes.d.ts.map +0 -1
  226. package/dist/internal/FundingRoutes.js.map +0 -1
  227. package/src/apps/data/FundingRouteProviders.test.ts +0 -225
  228. package/src/apps/data/FundingRouteProviders.ts +0 -312
  229. package/src/internal/FundingRoutes.test.ts +0 -213
@@ -1,12 +1,18 @@
1
- import { RateLimit, Store } from 'tapimo'
1
+ import { Challenge } from 'mppx'
2
+ import { sessionManager } from 'mppx/client'
3
+ import { ZoneRpcAuthentication } from 'ox/tempo'
4
+ import { Actions } from 'viem/tempo'
2
5
  import * as TestApp from '../../../../test/App.js'
3
6
  import * as Mppx from '../../../../test/Mppx.js'
4
7
  import * as Runtime from '../../../../test/runtime.js'
5
8
  import * as Tempo from '../../../../test/Tempo.js'
9
+ import * as Auth from '../../../internal/Auth.js'
6
10
  import * as Tidx from '../../../internal/Tidx.js'
11
+ import * as Viem from '../../../internal/Viem.js'
7
12
  import * as Indexer from './indexer.js'
8
13
 
9
- const chainId = String(Runtime.get().chainId)
14
+ const runtime = Runtime.get()
15
+ const chainId = String(runtime.chainId)
10
16
 
11
17
  describe('GET /indexer/query', () => {
12
18
  test('proxies a query to the upstream indexer', async () => {
@@ -54,6 +60,81 @@ describe('GET /indexer/query', () => {
54
60
  expect(body.rows[0]?.[0]).toBe(value)
55
61
  })
56
62
 
63
+ test('routes Zone queries to the Zone TIDX with its basic auth', async () => {
64
+ const zone = runtime.zone.chainId
65
+ const zoneReader = {
66
+ id: 'key_zone_indexer',
67
+ orgId: 'org_test',
68
+ scopes: ['data:read', 'indexer:query', `zone:${zone}:read`],
69
+ token: 'secret_zone_indexer',
70
+ } satisfies TestApp.kvStore.Key
71
+ const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
72
+ new Response(JSON.stringify({ columns: [], ok: true, row_count: 0, rows: [] }), {
73
+ headers: { 'content-type': 'application/json' },
74
+ }),
75
+ )
76
+ try {
77
+ const client = TestApp.client({
78
+ auth: { keys: [zoneReader] },
79
+ tidx: ({ chainId }) =>
80
+ chainId === zone
81
+ ? { baseUrl: 'https://zone.tidx.test', basicAuth: 'zone-secret' }
82
+ : { baseUrl: 'https://public.tidx.test', basicAuth: 'public-secret' },
83
+ zones: [{ chainId: zone, forwardRpcUrl: runtime.zone.forwardRpcUrl }],
84
+ })
85
+ const response = await client.v1.indexer.query.$get(
86
+ { query: { chainId: String(zone), sql: 'select 1' } },
87
+ {
88
+ headers: {
89
+ authorization: `Bearer ${zoneReader.token}`,
90
+ [ZoneRpcAuthentication.headerName]: 'zone-rpc-secret',
91
+ },
92
+ },
93
+ )
94
+ const upstream = fetch.mock.calls[0]![0] as Request
95
+
96
+ expect(response.status).toBe(200)
97
+ expect(new URL(upstream.url).origin).toBe('https://zone.tidx.test')
98
+ expect(upstream.headers.get('authorization')).toBe('Basic em9uZS1zZWNyZXQ=')
99
+ expect(upstream.headers.get(ZoneRpcAuthentication.headerName)).toBeNull()
100
+ } finally {
101
+ fetch.mockRestore()
102
+ }
103
+ })
104
+
105
+ test('routes Zone queries to the Zone TIDX with its bearer auth', async () => {
106
+ const zone = runtime.zone.chainId
107
+ const zoneReader = {
108
+ id: 'key_zone_bearer_indexer',
109
+ orgId: 'org_test',
110
+ scopes: ['data:read', 'indexer:query', `zone:${zone}:read`],
111
+ token: 'secret_zone_bearer_indexer',
112
+ } satisfies TestApp.kvStore.Key
113
+ const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
114
+ new Response(JSON.stringify({ columns: [], ok: true, row_count: 0, rows: [] }), {
115
+ headers: { 'content-type': 'application/json' },
116
+ }),
117
+ )
118
+ try {
119
+ const client = TestApp.client({
120
+ auth: { keys: [zoneReader] },
121
+ tidx: { baseUrl: 'https://zone.tidx.test', bearerAuth: 'zone-secret' },
122
+ zones: [{ chainId: zone, forwardRpcUrl: runtime.zone.forwardRpcUrl }],
123
+ })
124
+ const response = await client.v1.indexer.query.$get(
125
+ { query: { chainId: String(zone), sql: 'select 1' } },
126
+ { headers: { authorization: `Bearer ${zoneReader.token}` } },
127
+ )
128
+ const upstream = fetch.mock.calls[0]![0] as Request
129
+
130
+ expect(response.status).toBe(200)
131
+ expect(new URL(upstream.url).origin).toBe('https://zone.tidx.test')
132
+ expect(upstream.headers.get('authorization')).toBe('Bearer zone-secret')
133
+ } finally {
134
+ fetch.mockRestore()
135
+ }
136
+ })
137
+
57
138
  test('bounds and caches buffered queries', async () => {
58
139
  // Stub the upstream so the injected execution bounds are observable on the
59
140
  // outgoing request; the auth path is in-memory and never touches fetch.
@@ -115,6 +196,20 @@ describe('GET /indexer/query', () => {
115
196
  expect(response.status).toMatchInlineSnapshot(`401`)
116
197
  })
117
198
 
199
+ test('does not expose anonymous access', () => {
200
+ expect(Auth.describeAccess(Indexer.indexer())['indexerQuery']).toMatchInlineSnapshot(`
201
+ {
202
+ "apiKey": true,
203
+ "mpp": true,
204
+ "public": false,
205
+ "scopes": [
206
+ "indexer:query",
207
+ ],
208
+ "session": false,
209
+ }
210
+ `)
211
+ })
212
+
118
213
  test('proxies without auth middleware', async () => {
119
214
  const client = TestApp.client({ auth: false })
120
215
  const response = await client.v1.indexer.query.$get({
@@ -126,10 +221,8 @@ describe('GET /indexer/query', () => {
126
221
  expect(body.ok).toMatchInlineSnapshot(`true`)
127
222
  })
128
223
 
129
- test('completes the MPP payment lifecycle when public quota is exhausted', async () => {
130
- const rateLimitStore = Store.memory()
224
+ test('challenges anonymous callers for MPP payment', async () => {
131
225
  const app = TestApp.create({
132
- rateLimit: { store: rateLimitStore },
133
226
  mpp: {
134
227
  secretKey: 'secret_test_key_0123456789abcdef',
135
228
  session: {
@@ -141,13 +234,9 @@ describe('GET /indexer/query', () => {
141
234
  },
142
235
  },
143
236
  })
144
- await exhaustQuota(RateLimit.memory({ store: rateLimitStore }), 'public:anonymous', {
145
- perMinute: 60,
146
- })
147
237
 
148
- // An unpaid over-quota request is challenged for payment. MPP challenges
149
- // and receipts ride on real HTTP headers, so we drive the lifecycle
150
- // through `app.fetch` rather than the typed route client.
238
+ // MPP challenges and receipts ride on real HTTP headers, so drive the
239
+ // lifecycle through `app.fetch` rather than the typed route client.
151
240
  const challenge = await app.fetch(
152
241
  new Request('http://tempo-api.test/v1/indexer/query?sql=select%201'),
153
242
  )
@@ -167,8 +256,77 @@ describe('GET /indexer/query', () => {
167
256
  expect(paid.headers.has('payment-receipt')).toMatchInlineSnapshot(`true`)
168
257
  expect(body.ok).toMatchInlineSnapshot(`true`)
169
258
  })
170
- })
171
259
 
172
- async function exhaustQuota(rateLimit: RateLimit.Store, key: string, limit: RateLimit.Limit) {
173
- for (let index = 0; index < limit.perMinute; index++) await rateLimit.consume({ key, limit })
174
- }
260
+ test('rejects an unsupported chain before issuing an MPP challenge', async () => {
261
+ const app = TestApp.create({
262
+ mpp: {
263
+ secretKey: 'secret_test_key_0123456789abcdef',
264
+ session: {
265
+ chainId: Tempo.chain.id,
266
+ currency: Tempo.currency,
267
+ decimals: 6,
268
+ getClient: () => Tempo.client,
269
+ recipient: Tempo.accounts[2].address,
270
+ },
271
+ },
272
+ })
273
+ const response = await app.fetch(
274
+ new Request('http://tempo-api.test/v1/indexer/query?sql=select%201&chainId=31318'),
275
+ )
276
+ const body = (await response.json()) as { error: { code: string } }
277
+
278
+ expect(response.status).toBe(400)
279
+ expect(response.headers.has('www-authenticate')).toBe(false)
280
+ expect(body.error.code).toBe('chain_id_unsupported')
281
+ })
282
+
283
+ test('accepts session management POSTs', { timeout: 120_000 }, async () => {
284
+ const app = TestApp.create({
285
+ mpp: {
286
+ secretKey: 'secret_test_key_0123456789abcdef',
287
+ session: {
288
+ account: Tempo.accounts[2],
289
+ chainId: Viem.chainId.mainnet,
290
+ currency: Tempo.currency,
291
+ decimals: 6,
292
+ getClient: () => Tempo.client,
293
+ suggestedDeposit: '0.01',
294
+ },
295
+ },
296
+ })
297
+ await Actions.faucet.fundSync(Tempo.client, {
298
+ account: Tempo.accounts[2],
299
+ timeout: 60_000,
300
+ })
301
+
302
+ const url = `http://tempo-api.test/v1/indexer/query?sql=select%201&chainId=${chainId}`
303
+ const challenged = await app.fetch(new Request(url))
304
+ expect(challenged.status).toMatchInlineSnapshot(`402`)
305
+ expect(Challenge.fromHeaders(challenged.headers).request).toMatchObject({
306
+ amount: '100',
307
+ suggestedDeposit: '10000',
308
+ })
309
+
310
+ const manager = sessionManager({
311
+ account: Tempo.accounts[1],
312
+ client: Tempo.client,
313
+ fetch: async (input, init) => app.fetch(new Request(input, init)),
314
+ })
315
+ const paid = await manager.fetch(url)
316
+ expect(paid.status).toMatchInlineSnapshot(`200`)
317
+
318
+ const toppedUp = await manager.topUp('0.01')
319
+ if (!toppedUp) throw new Error('expected top-up receipt')
320
+ expect(toppedUp.status).toMatchInlineSnapshot(`"success"`)
321
+
322
+ const closed = await manager.close()
323
+ if (!closed) throw new Error('expected close receipt')
324
+ expect(closed.status).toMatchInlineSnapshot(`"success"`)
325
+ expect(closed.channelId).toBe(toppedUp.channelId)
326
+
327
+ const state = await Actions.channel.getStates(Tempo.client, {
328
+ channel: closed.channelId,
329
+ })
330
+ expect(state.deposit).toMatchInlineSnapshot(`0n`)
331
+ })
332
+ })
@@ -1,4 +1,5 @@
1
1
  import { Hono, type Context, type TypedResponse } from 'hono'
2
+ import { ZoneRpcAuthentication } from 'ox/tempo'
2
3
  import * as z from 'zod/mini'
3
4
 
4
5
  import type * as App from '../../../App.js'
@@ -25,6 +26,7 @@ const strippedHeaders = new Set([
25
26
  'transfer-encoding',
26
27
  'upgrade',
27
28
  'tempo-api-key',
29
+ ZoneRpcAuthentication.headerName.toLowerCase(),
28
30
  ])
29
31
 
30
32
  /** Never reached; carries typed passthrough response variants into Hono client inference. */
@@ -99,7 +101,7 @@ export function indexer() {
99
101
 
100
102
  return new Hono<App.Environment>().get(
101
103
  '/v1/indexer/query',
102
- Auth.policy({ apiKey: { scopes: ['indexer:query'] }, mpp: true, public: true }),
104
+ Auth.policy({ apiKey: { scopes: ['indexer:query'] }, mpp: true }),
103
105
  OpenApi.describeRoute({
104
106
  description:
105
107
  'Runs a read-only SQL-style query against Tempo’s indexed chain data, with optional live streaming as new blocks arrive.',
@@ -256,7 +258,10 @@ async function passthrough(
256
258
  // Resolve the upstream for this caller. A resolver `tidx` can route by
257
259
  // principal (e.g. anonymous `type: 'public'` callers to a public, no-auth
258
260
  // indexer); a static config ignores the principal.
259
- const { baseUrl, basicAuth } = Tidx.resolve(tidx, { chainId, principal: Auth.getPrincipal(c) })
261
+ const { baseUrl, basicAuth, bearerAuth } = Tidx.resolve(tidx, {
262
+ chainId,
263
+ principal: Auth.getPrincipal(c),
264
+ })
260
265
  // The chain id is syntactically valid but this deployment has no indexer
261
266
  // upstream configured for it (no `tidx` config and no built-in default).
262
267
  if (!baseUrl)
@@ -290,6 +295,7 @@ async function passthrough(
290
295
  for (const [name, value] of c.req.raw.headers)
291
296
  if (!strippedHeaders.has(name.toLowerCase())) headers.set(name, value)
292
297
  if (basicAuth) headers.set('authorization', `Basic ${btoa(basicAuth)}`)
298
+ else if (bearerAuth) headers.set('authorization', `Bearer ${bearerAuth}`)
293
299
 
294
300
  const method = c.req.method
295
301
 
@@ -1,8 +1,43 @@
1
+ import { Hex, Secp256k1 } from 'ox'
2
+ import { ZoneRpcAuthentication } from 'ox/tempo'
3
+
1
4
  import type * as Viem from '../../../internal/Viem.js'
5
+ import * as RequestListener from '../../../handlers/internal/requestListener.js'
2
6
  import * as TestApp from '../../../../test/App.js'
7
+ import * as Relay from '../../../../test/Relay.js'
3
8
  import * as Runtime from '../../../../test/runtime.js'
4
9
 
5
10
  const runtime = Runtime.get()
11
+ const zone = runtime.zone.chainId
12
+ const zoneOptions = {
13
+ chainId: zone,
14
+ forwardRpcUrl: runtime.zone.forwardRpcUrl,
15
+ }
16
+ const zoneReader = {
17
+ id: 'key_zone_reader',
18
+ orgId: 'org_test',
19
+ scopes: ['data:read', `zone:${zone}:read`],
20
+ token: 'secret_zone_reader',
21
+ } satisfies TestApp.kvStore.Key
22
+
23
+ // Prool's default Zone dev key.
24
+ const zonePrivateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
25
+
26
+ function zoneToken() {
27
+ const issuedAt = Math.floor(Date.now() / 1_000)
28
+ const authentication = ZoneRpcAuthentication.from({
29
+ chainId: zone,
30
+ expiresAt: issuedAt + 300,
31
+ issuedAt,
32
+ zoneId: 1,
33
+ })
34
+ return ZoneRpcAuthentication.serialize(authentication, {
35
+ signature: Secp256k1.sign({
36
+ payload: ZoneRpcAuthentication.getSignPayload(authentication),
37
+ privateKey: zonePrivateKey,
38
+ }),
39
+ })
40
+ }
6
41
 
7
42
  describe('POST /rpc', () => {
8
43
  test('routes by principal: a resolver sees the request principal and routes accordingly', async () => {
@@ -65,6 +100,7 @@ describe('POST /rpc', () => {
65
100
  'content-type': 'application/json',
66
101
  cookie: 'session=secret',
67
102
  'tempo-api-key': 'caller-key',
103
+ 'x-authorization-token': 'zone-secret',
68
104
  'x-client-trace': 'trace_test',
69
105
  },
70
106
  method: 'POST',
@@ -90,6 +126,7 @@ describe('POST /rpc', () => {
90
126
  expect(call.headers['authorization']).toMatchInlineSnapshot(`"Basic cnBjLXNlY3JldA=="`)
91
127
  expect(call.headers['cookie']).toMatchInlineSnapshot(`null`)
92
128
  expect(call.headers['tempo-api-key']).toMatchInlineSnapshot(`null`)
129
+ expect(call.headers['x-authorization-token']).toBeNull()
93
130
  expect(call.headers['x-client-trace']).toMatchInlineSnapshot(`"trace_test"`)
94
131
  } finally {
95
132
  fetch.restore()
@@ -267,6 +304,187 @@ describe('POST /rpc', () => {
267
304
  }
268
305
  })
269
306
 
307
+ test('proxies zone chain ids and forwards the caller zone auth token', async () => {
308
+ const fetch = mockRpc()
309
+
310
+ try {
311
+ const app = TestApp.create({
312
+ auth: false,
313
+ zones: [{ chainId: zone, forwardRpcUrl: `https://${zone}.rpc.test` }],
314
+ })
315
+
316
+ const response = await app.fetch(
317
+ new Request(`http://tempo-api.test/rpc/${zone}`, {
318
+ body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_getBalance' }),
319
+ headers: {
320
+ authorization: 'Bearer caller-secret',
321
+ 'x-authorization-token': 'zone-token',
322
+ },
323
+ method: 'POST',
324
+ }),
325
+ )
326
+ const call = fetch.calls[0]!
327
+
328
+ expect(response.status).toMatchInlineSnapshot(`200`)
329
+ expect(call.url).toBe(`https://${zone}.rpc.test/`)
330
+ // Zone RPC auth: the caller's signed token passes through; the API's own
331
+ // credentials do not leak upstream.
332
+ expect(call.headers['x-authorization-token']).toMatchInlineSnapshot(`"zone-token"`)
333
+ expect(call.headers['authorization']).toBeNull()
334
+ expect(call.headers['tempo-api-key']).toMatchInlineSnapshot(`null`)
335
+ } finally {
336
+ fetch.restore()
337
+ }
338
+ })
339
+
340
+ test('refuses a zone rpc call without a zone scope or auth token', async () => {
341
+ const fetch = mockRpc()
342
+
343
+ try {
344
+ const app = TestApp.create({
345
+ auth: false,
346
+ zones: [{ chainId: zone, forwardRpcUrl: `https://${zone}.rpc.test` }],
347
+ })
348
+
349
+ const response = await app.fetch(
350
+ new Request(`http://tempo-api.test/rpc/${zone}`, {
351
+ body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_getBalance' }),
352
+ method: 'POST',
353
+ }),
354
+ )
355
+ const body = (await response.json()) as { error: { code: string; message: string } }
356
+
357
+ expect(response.status).toBe(403)
358
+ expect(body.error.code).toBe('api_key_forbidden')
359
+ expect(body.error.message).toContain(`zone:${zone}:read`)
360
+ // Refused before any upstream round-trip.
361
+ expect(fetch.calls).toHaveLength(0)
362
+ } finally {
363
+ fetch.restore()
364
+ }
365
+ })
366
+
367
+ test('routes Zone-scoped reads to the internal RPC', async () => {
368
+ const app = TestApp.create({ auth: { keys: [zoneReader] }, zones: [zoneOptions] })
369
+ const response = await app.fetch(
370
+ new Request(`http://tempo-api.test/rpc/${zone}`, {
371
+ body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }),
372
+ headers: { 'content-type': 'application/json', 'tempo-api-key': zoneReader.token },
373
+ method: 'POST',
374
+ }),
375
+ )
376
+
377
+ expect(response.status).toBe(200)
378
+ expect(await response.json()).toMatchObject({ result: Hex.fromNumber(zone) })
379
+ })
380
+
381
+ test('allows Zone-scoped eth_fillTransaction calls', async () => {
382
+ const app = TestApp.create({ auth: { keys: [zoneReader] }, zones: [zoneOptions] })
383
+ const response = await app.fetch(
384
+ new Request(`http://tempo-api.test/rpc/${zone}`, {
385
+ body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_fillTransaction' }),
386
+ headers: { 'content-type': 'application/json', 'tempo-api-key': zoneReader.token },
387
+ method: 'POST',
388
+ }),
389
+ )
390
+
391
+ expect(response.status).toBe(200)
392
+ })
393
+
394
+ test('forwards RPC basic auth to the internal Zone RPC', async () => {
395
+ const authorization: (string | null)[] = []
396
+ const server = await Relay.createServer(
397
+ RequestListener.fromFetchHandler(async (request) => {
398
+ authorization.push(request.headers.get('authorization'))
399
+ return Response.json({ id: 1, jsonrpc: '2.0', result: Hex.fromNumber(zone) })
400
+ }),
401
+ )
402
+ try {
403
+ const app = TestApp.create({
404
+ auth: { keys: [zoneReader] },
405
+ rpc: ({ chainId }) =>
406
+ chainId === zone ? { basicAuth: 'rpc-secret', url: server.url } : {},
407
+ zones: [zoneOptions],
408
+ })
409
+ const response = await app.fetch(
410
+ new Request(`http://tempo-api.test/rpc/${zone}`, {
411
+ body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }),
412
+ headers: { 'content-type': 'application/json', 'tempo-api-key': zoneReader.token },
413
+ method: 'POST',
414
+ }),
415
+ )
416
+
417
+ expect(response.status).toBe(200)
418
+ expect(authorization).toEqual(['Basic cnBjLXNlY3JldA=='])
419
+ } finally {
420
+ await server.closeAsync()
421
+ }
422
+ })
423
+
424
+ test('rejects Zone-scoped writes before the internal RPC', async () => {
425
+ const app = TestApp.create({ auth: { keys: [zoneReader] }, zones: [zoneOptions] })
426
+ const response = await app.fetch(
427
+ new Request(`http://tempo-api.test/rpc/${zone}`, {
428
+ body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_sendRawTransaction' }),
429
+ headers: { 'content-type': 'application/json', 'tempo-api-key': zoneReader.token },
430
+ method: 'POST',
431
+ }),
432
+ )
433
+
434
+ expect(response.status).toBe(403)
435
+ expect(await response.json()).toMatchObject({ error: { code: 'api_key_forbidden' } })
436
+ })
437
+
438
+ test('rejects a Zone-scoped batch containing a write', async () => {
439
+ const app = TestApp.create({ auth: { keys: [zoneReader] }, zones: [zoneOptions] })
440
+ const response = await app.fetch(
441
+ new Request(`http://tempo-api.test/rpc/${zone}`, {
442
+ body: JSON.stringify([
443
+ { id: 1, jsonrpc: '2.0', method: 'eth_chainId' },
444
+ { id: 2, jsonrpc: '2.0', method: 'eth_sendRawTransaction' },
445
+ ]),
446
+ headers: { 'content-type': 'application/json', 'tempo-api-key': zoneReader.token },
447
+ method: 'POST',
448
+ }),
449
+ )
450
+
451
+ expect(response.status).toBe(403)
452
+ expect(await response.json()).toMatchObject({ error: { code: 'api_key_forbidden' } })
453
+ })
454
+
455
+ test('routes caller Zone tokens to the private RPC', async () => {
456
+ const app = TestApp.create({ auth: false, zones: [zoneOptions] })
457
+ const response = await app.fetch(
458
+ new Request(`http://tempo-api.test/rpc/${zone}`, {
459
+ body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }),
460
+ headers: {
461
+ 'content-type': 'application/json',
462
+ [ZoneRpcAuthentication.headerName]: zoneToken(),
463
+ },
464
+ method: 'POST',
465
+ }),
466
+ )
467
+
468
+ expect(response.status).toBe(200)
469
+ expect(await response.json()).toMatchObject({ result: Hex.fromNumber(zone) })
470
+ })
471
+
472
+ test('rejects invalid Zone tokens at the private RPC', async () => {
473
+ const app = TestApp.create({ auth: false, zones: [zoneOptions] })
474
+ const response = await app.fetch(
475
+ new Request(`http://tempo-api.test/rpc/${zone}`, {
476
+ body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }),
477
+ headers: {
478
+ 'content-type': 'application/json',
479
+ [ZoneRpcAuthentication.headerName]: 'invalid',
480
+ },
481
+ method: 'POST',
482
+ }),
483
+ )
484
+
485
+ expect(response.status).toBe(403)
486
+ })
487
+
270
488
  test('rejects invalid chain ids before proxying upstream', async () => {
271
489
  const fetch = mockRpc()
272
490
 
@@ -520,6 +738,7 @@ function mockRpc() {
520
738
  authorization: request.headers.get('authorization'),
521
739
  cookie: request.headers.get('cookie'),
522
740
  'tempo-api-key': request.headers.get('tempo-api-key'),
741
+ 'x-authorization-token': request.headers.get('x-authorization-token'),
523
742
  'x-client-trace': request.headers.get('x-client-trace'),
524
743
  },
525
744
  method: request.method,