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.
- package/dist/App.d.ts +4701 -3416
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +24 -2
- package/dist/App.js.map +1 -1
- package/dist/Assets.js +3 -3
- package/dist/Assets.js.map +1 -1
- package/dist/Scope.d.ts +18 -1
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +23 -3
- package/dist/Scope.js.map +1 -1
- package/dist/Webhooks.d.ts +1 -1
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +5 -10
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +6 -34
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +3 -3
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +4 -1
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/scopes.d.ts +6 -34
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/scopes.js +2 -2
- package/dist/admin/apps/scopes.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +57 -57
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +33 -33
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/data/App.d.ts +4574 -4277
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +155 -77
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.d.ts +109 -0
- package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -0
- package/dist/apps/data/FundingQuoteProviders.js +464 -0
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -0
- package/dist/apps/data/index.d.ts +1 -1
- package/dist/apps/data/index.js +1 -1
- package/dist/apps/data/routes/activities.d.ts +26 -26
- package/dist/apps/data/routes/addresses.d.ts +38 -38
- package/dist/apps/data/routes/balances.d.ts +45 -45
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +67 -149
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +2 -3
- package/dist/apps/data/routes/blocks.d.ts.map +1 -1
- package/dist/apps/data/routes/blocks.js +3 -5
- package/dist/apps/data/routes/blocks.js.map +1 -1
- package/dist/apps/data/routes/coingecko.d.ts +177 -176
- package/dist/apps/data/routes/coingecko.d.ts.map +1 -1
- package/dist/apps/data/routes/coingecko.js +3 -8
- package/dist/apps/data/routes/coingecko.js.map +1 -1
- package/dist/apps/data/routes/funding.d.ts +131 -228
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +152 -219
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +34 -34
- package/dist/apps/data/routes/indexer.d.ts.map +1 -1
- package/dist/apps/data/routes/indexer.js +9 -2
- package/dist/apps/data/routes/indexer.js.map +1 -1
- package/dist/apps/data/routes/receipts.d.ts +228 -228
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +86 -16
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +12 -12
- package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
- package/dist/apps/data/routes/webhooks.d.ts +340 -0
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +13 -1
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/index.js +1 -1
- package/dist/apps/management/App.d.ts +1833 -851
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/App.js +4 -0
- package/dist/apps/management/App.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +41 -41
- package/dist/apps/management/routes/api-keys.js +2 -2
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +48 -48
- package/dist/apps/management/routes/invitations.d.ts +132 -132
- package/dist/apps/management/routes/invite-links.d.ts +1100 -0
- package/dist/apps/management/routes/invite-links.d.ts.map +1 -0
- package/dist/apps/management/routes/invite-links.js +411 -0
- package/dist/apps/management/routes/invite-links.js.map +1 -0
- package/dist/apps/management/routes/members.d.ts +41 -41
- package/dist/apps/management/routes/orgs.d.ts +92 -92
- package/dist/apps/management/routes/projects.d.ts +9 -9
- package/dist/apps/management/routes/scopes.d.ts.map +1 -1
- package/dist/apps/management/routes/scopes.js +2 -2
- package/dist/apps/management/routes/scopes.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +24 -24
- package/dist/apps/mcp.js +2 -2
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/relay/App.d.ts +2 -2
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +44 -6
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/db/Db.d.ts +3 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +57 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +31 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +31 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/inviteLinks.d.ts +42 -0
- package/dist/db/tables/inviteLinks.d.ts.map +1 -0
- package/dist/db/tables/inviteLinks.js +148 -0
- package/dist/db/tables/inviteLinks.js.map +1 -0
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +2 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +6 -2
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +13 -3
- package/dist/handlers/relay.js.map +1 -1
- package/dist/internal/Auth.d.ts +9 -1
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +138 -10
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +3 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +17 -2
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/{FundingRoutes.d.ts → FundingQuotes.d.ts} +240 -369
- package/dist/internal/FundingQuotes.d.ts.map +1 -0
- package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +144 -183
- package/dist/internal/FundingQuotes.js.map +1 -0
- package/dist/internal/Log.d.ts +18 -12
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +2 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/Tidx.d.ts +9 -3
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +13 -7
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Viem.d.ts +17 -2
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +30 -3
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/index.d.ts +1 -1
- package/dist/internal/index.js +1 -1
- package/package.json +2 -2
- package/src/ApiKeys.test.ts +25 -0
- package/src/App.test-d.ts +2 -0
- package/src/App.test.ts +13 -2
- package/src/App.ts +37 -5
- package/src/Assets.ts +3 -3
- package/src/Client.test-d.ts +21 -11
- package/src/Scope.test.ts +1 -0
- package/src/Scope.ts +32 -4
- package/src/Webhooks.ts +5 -10
- package/src/admin/apps/api-keys.test.ts +27 -0
- package/src/admin/apps/api-keys.ts +8 -1
- package/src/admin/apps/scopes.test.ts +7 -2
- package/src/admin/apps/scopes.ts +2 -2
- package/src/admin/ui/src/components/Dialog.tsx +8 -2
- package/src/admin/ui/src/lib/scopes.test.ts +19 -0
- package/src/admin/ui/src/lib/scopes.ts +12 -0
- package/src/admin/ui/src/routes/api-keys.tsx +128 -27
- package/src/admin/ui.generated.ts +33 -33
- package/src/apps/composition.test.ts +77 -0
- package/src/apps/data/App.test.ts +354 -0
- package/src/apps/data/App.ts +178 -91
- package/src/apps/data/FundingQuoteProviders.test.ts +577 -0
- package/src/apps/data/FundingQuoteProviders.ts +633 -0
- package/src/apps/data/index.ts +1 -1
- package/src/apps/data/routes/balances.test.ts +64 -18
- package/src/apps/data/routes/balances.ts +76 -177
- package/src/apps/data/routes/blocks.ts +3 -5
- package/src/apps/data/routes/coingecko.ts +3 -8
- package/src/apps/data/routes/funding.test.ts +304 -192
- package/src/apps/data/routes/funding.ts +166 -286
- package/src/apps/data/routes/indexer.test.ts +173 -15
- package/src/apps/data/routes/indexer.ts +8 -2
- package/src/apps/data/routes/rpc.test.ts +219 -0
- package/src/apps/data/routes/rpc.ts +92 -17
- package/src/apps/data/routes/webhooks.test.ts +21 -0
- package/src/apps/data/routes/webhooks.ts +13 -1
- package/src/apps/index.ts +1 -1
- package/src/apps/management/App.ts +4 -0
- package/src/apps/management/Billing.test.ts +2 -2
- package/src/apps/management/routes/api-keys.test.ts +47 -3
- package/src/apps/management/routes/api-keys.ts +2 -2
- package/src/apps/management/routes/invite-links.test.ts +385 -0
- package/src/apps/management/routes/invite-links.ts +570 -0
- package/src/apps/management/routes/scopes.test.ts +3 -2
- package/src/apps/management/routes/scopes.ts +4 -2
- package/src/apps/mcp.test.ts +18 -0
- package/src/apps/mcp.ts +2 -2
- package/src/apps/relay/App.test.ts +328 -7
- package/src/apps/relay/App.ts +42 -6
- package/src/apps/relay/Sponsorships.test.ts +2 -2
- package/src/db/Db.test.ts +5 -5
- package/src/db/Db.ts +65 -0
- package/src/db/Schema.ts +39 -0
- package/src/db/tables/billingSettings.test.ts +2 -2
- package/src/db/tables/inviteLinks.ts +187 -0
- package/src/db/tables/organizations.test.ts +2 -2
- package/src/db/tables/organizations.ts +2 -0
- package/src/db/tables/sponsoredTransactions.test.ts +19 -2
- package/src/db/tables/sponsoredTransactions.ts +21 -14
- package/src/db/tables/stripeCustomers.test.ts +2 -2
- package/src/handlers/relay.ts +16 -4
- package/src/internal/Auth.test.ts +132 -22
- package/src/internal/Auth.ts +181 -14
- package/src/internal/EdgeCache.test.ts +24 -3
- package/src/internal/EdgeCache.ts +18 -2
- package/src/internal/FundingQuotes.test.ts +268 -0
- package/src/internal/{FundingRoutes.ts → FundingQuotes.ts} +216 -277
- package/src/internal/Log.ts +21 -14
- package/src/internal/Tidx.test.ts +48 -0
- package/src/internal/Tidx.ts +21 -7
- package/src/internal/Viem.test.ts +46 -0
- package/src/internal/Viem.ts +48 -6
- package/src/internal/index.test.ts +1 -1
- package/src/internal/index.ts +1 -1
- package/dist/apps/data/FundingRouteProviders.d.ts +0 -36
- package/dist/apps/data/FundingRouteProviders.d.ts.map +0 -1
- package/dist/apps/data/FundingRouteProviders.js +0 -245
- package/dist/apps/data/FundingRouteProviders.js.map +0 -1
- package/dist/internal/FundingRoutes.d.ts.map +0 -1
- package/dist/internal/FundingRoutes.js.map +0 -1
- package/src/apps/data/FundingRouteProviders.test.ts +0 -225
- package/src/apps/data/FundingRouteProviders.ts +0 -312
- package/src/internal/FundingRoutes.test.ts +0 -213
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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('
|
|
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
|
-
//
|
|
149
|
-
//
|
|
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
|
-
|
|
173
|
-
|
|
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
|
|
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, {
|
|
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,
|