tapimo 0.5.0 → 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 +4879 -4870
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +20 -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.map +1 -1
- package/dist/Scope.js +1 -2
- 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 +116 -116
- 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 +3199 -3206
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +25 -38
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.d.ts +76 -3
- package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.js +276 -2
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +245 -245
- package/dist/apps/data/routes/addresses.d.ts +206 -206
- package/dist/apps/data/routes/balances.d.ts +46 -46
- package/dist/apps/data/routes/blocks.d.ts +123 -123
- package/dist/apps/data/routes/coingecko.d.ts +169 -169
- package/dist/apps/data/routes/funding.d.ts +230 -146
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +23 -12
- 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/tokenlist.d.ts +36 -36
- package/dist/apps/data/routes/transactions.d.ts +281 -281
- package/dist/apps/data/routes/transfers.d.ts +58 -58
- package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
- package/dist/apps/management/App.d.ts +1042 -1042
- package/dist/apps/management/routes/api-keys.d.ts +120 -120
- package/dist/apps/management/routes/billing.d.ts +211 -211
- package/dist/apps/management/routes/invitations.d.ts +132 -132
- package/dist/apps/management/routes/invite-links.d.ts +62 -62
- package/dist/apps/management/routes/me.d.ts +17 -17
- package/dist/apps/management/routes/members.d.ts +80 -80
- package/dist/apps/management/routes/orgs.d.ts +96 -96
- package/dist/apps/management/routes/projects.d.ts +61 -61
- package/dist/apps/management/routes/scopes.d.ts +11 -11
- package/dist/apps/management/routes/usage.d.ts +79 -79
- 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.js +2 -2
- 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/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +8 -0
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/FundingQuotes.d.ts +170 -56
- package/dist/internal/FundingQuotes.d.ts.map +1 -1
- package/dist/internal/FundingQuotes.js +91 -5
- package/dist/internal/FundingQuotes.js.map +1 -1
- 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 +14 -1
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +19 -1
- package/dist/internal/Viem.js.map +1 -1
- package/package.json +2 -2
- package/src/ApiKeys.test.ts +2 -0
- package/src/App.test.ts +13 -2
- package/src/App.ts +31 -5
- package/src/Assets.ts +3 -3
- package/src/Client.test-d.ts +3 -2
- package/src/Scope.ts +4 -2
- 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.ts +25 -45
- package/src/apps/data/FundingQuoteProviders.test.ts +431 -5
- package/src/apps/data/FundingQuoteProviders.ts +386 -4
- package/src/apps/data/routes/funding.test.ts +174 -7
- package/src/apps/data/routes/funding.ts +27 -12
- package/src/apps/data/routes/indexer.test.ts +82 -20
- package/src/apps/data/routes/indexer.ts +8 -2
- package/src/apps/management/Billing.test.ts +2 -2
- package/src/apps/mcp.test.ts +18 -0
- package/src/apps/mcp.ts +2 -2
- package/src/apps/relay/App.test.ts +6 -5
- package/src/apps/relay/App.ts +2 -2
- package/src/apps/relay/Sponsorships.test.ts +2 -2
- package/src/db/Db.test.ts +5 -5
- package/src/db/tables/billingSettings.test.ts +2 -2
- package/src/db/tables/organizations.test.ts +2 -2
- 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/internal/Auth.ts +10 -0
- package/src/internal/FundingQuotes.test.ts +97 -3
- package/src/internal/FundingQuotes.ts +104 -6
- 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 +34 -1
|
@@ -8,6 +8,7 @@ import * as FundingQuotes from '../../../internal/FundingQuotes.js'
|
|
|
8
8
|
import * as OpenApi from '../../../internal/OpenApi.js'
|
|
9
9
|
import * as Response from '../../../internal/Response.js'
|
|
10
10
|
import * as Schema from '../../../internal/Schema.js'
|
|
11
|
+
import * as FundingQuoteProviders from '../FundingQuoteProviders.js'
|
|
11
12
|
|
|
12
13
|
/** Zod schemas owned by funding quote resources. */
|
|
13
14
|
export namespace schema {
|
|
@@ -15,7 +16,7 @@ export namespace schema {
|
|
|
15
16
|
export const Chain = Schema.describe(
|
|
16
17
|
z.object({
|
|
17
18
|
addressFormat: z
|
|
18
|
-
.
|
|
19
|
+
.enum(['base58', 'base58check', 'evm'])
|
|
19
20
|
.check(
|
|
20
21
|
z.describe('Address family used by accounts and token identifiers on this chain.'),
|
|
21
22
|
z.meta({ examples: ['evm'] }),
|
|
@@ -24,12 +25,12 @@ export namespace schema {
|
|
|
24
25
|
.string()
|
|
25
26
|
.check(
|
|
26
27
|
z.describe(
|
|
27
|
-
'Stable
|
|
28
|
+
'Stable chain id. Uses CAIP-2 where standardized and a Tempo-defined compatible id otherwise.',
|
|
28
29
|
),
|
|
29
30
|
z.meta({ examples: ['eip155:8453'] }),
|
|
30
31
|
),
|
|
31
32
|
kind: z
|
|
32
|
-
.
|
|
33
|
+
.enum(['evm', 'solana', 'tron'])
|
|
33
34
|
.check(
|
|
34
35
|
z.describe('Chain execution family used for provider routing.'),
|
|
35
36
|
z.meta({ examples: ['evm'] }),
|
|
@@ -41,7 +42,7 @@ export namespace schema {
|
|
|
41
42
|
.optional(z.record(z.string(), z.string()))
|
|
42
43
|
.check(
|
|
43
44
|
z.describe('Provider-specific chain ids Tempo uses when requesting quotes.'),
|
|
44
|
-
z.meta({ examples: [{ relay: '8453' }] }),
|
|
45
|
+
z.meta({ examples: [{ across: '8453', relay: '8453', rhino: 'BASE' }] }),
|
|
45
46
|
),
|
|
46
47
|
}),
|
|
47
48
|
'A normalized chain reference for funding quotes.',
|
|
@@ -117,7 +118,7 @@ export namespace schema {
|
|
|
117
118
|
export const ProviderRef = Schema.describe(
|
|
118
119
|
z.object({
|
|
119
120
|
id: z
|
|
120
|
-
.
|
|
121
|
+
.enum(['across', 'relay', 'rhino'])
|
|
121
122
|
.check(z.describe('Stable provider id.'), z.meta({ examples: ['relay'] })),
|
|
122
123
|
kind: z
|
|
123
124
|
.string()
|
|
@@ -172,6 +173,12 @@ export namespace schema {
|
|
|
172
173
|
/** Provider-derived output, limits, and freshness data. */
|
|
173
174
|
export const QuoteDetails = Schema.describe(
|
|
174
175
|
z.object({
|
|
176
|
+
expiresAt: z
|
|
177
|
+
.optional(z.iso.datetime())
|
|
178
|
+
.check(
|
|
179
|
+
z.describe('Timestamp after which the provider quote is no longer valid.'),
|
|
180
|
+
z.meta({ examples: ['2026-01-01T00:01:00Z'] }),
|
|
181
|
+
),
|
|
175
182
|
inputAmount: Schema.DecimalString.check(
|
|
176
183
|
z.describe('Source amount sent to the provider in base units.'),
|
|
177
184
|
z.meta({ examples: ['1000000'] }),
|
|
@@ -230,7 +237,7 @@ export namespace schema {
|
|
|
230
237
|
export const Provider = Schema.describe(
|
|
231
238
|
z.object({
|
|
232
239
|
id: z
|
|
233
|
-
.
|
|
240
|
+
.enum(['across', 'relay', 'rhino'])
|
|
234
241
|
.check(z.describe('Stable provider id.'), z.meta({ examples: ['relay'] })),
|
|
235
242
|
kind: z
|
|
236
243
|
.string()
|
|
@@ -278,10 +285,10 @@ export namespace schema {
|
|
|
278
285
|
z.meta({ examples: ['USDC.e'] }),
|
|
279
286
|
),
|
|
280
287
|
provider: z
|
|
281
|
-
.optional(z.
|
|
288
|
+
.optional(z.enum(['across', 'relay', 'rhino']))
|
|
282
289
|
.check(
|
|
283
290
|
z.describe(
|
|
284
|
-
'Only request a quote from this provider.
|
|
291
|
+
'Only request a quote from this provider. Supported ids are `across`, `relay`, and `rhino`; omit it to query every configured provider.',
|
|
285
292
|
),
|
|
286
293
|
z.meta({ examples: ['relay'] }),
|
|
287
294
|
),
|
|
@@ -304,7 +311,9 @@ export namespace schema {
|
|
|
304
311
|
.string()
|
|
305
312
|
.check(
|
|
306
313
|
z.minLength(1),
|
|
307
|
-
z.describe(
|
|
314
|
+
z.describe(
|
|
315
|
+
'Source chain alias or stable id, such as `base`, `solana`, or `eip155:8453`.',
|
|
316
|
+
),
|
|
308
317
|
z.meta({ examples: ['base', 'eip155:8453'] }),
|
|
309
318
|
),
|
|
310
319
|
sourceToken: z
|
|
@@ -312,7 +321,7 @@ export namespace schema {
|
|
|
312
321
|
.check(
|
|
313
322
|
z.minLength(1),
|
|
314
323
|
z.describe(
|
|
315
|
-
'Source token symbol, contract address, or Tempo token key.
|
|
324
|
+
'Source token symbol, contract address, or Tempo token key. Lookup is scoped to `sourceChain`; non-EVM addresses are case-sensitive. An unknown token returns an empty list.',
|
|
316
325
|
),
|
|
317
326
|
z.meta({ examples: ['USDC', '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913'] }),
|
|
318
327
|
),
|
|
@@ -414,7 +423,8 @@ export function funding(options: funding.Options = {}) {
|
|
|
414
423
|
400: { codes: ['query_invalid'] },
|
|
415
424
|
502: {
|
|
416
425
|
codes: ['upstream_error'],
|
|
417
|
-
description:
|
|
426
|
+
description:
|
|
427
|
+
'No quote succeeded and at least one matching provider failed unexpectedly.',
|
|
418
428
|
},
|
|
419
429
|
},
|
|
420
430
|
success: {
|
|
@@ -456,12 +466,17 @@ export function funding(options: funding.Options = {}) {
|
|
|
456
466
|
} catch (cause) {
|
|
457
467
|
const message = requestErrorMessage(cause)
|
|
458
468
|
if (message) return Response.error(c, { code: 'query_invalid', message, status: 400 })
|
|
459
|
-
if (cause instanceof FundingQuotes.ProviderQuoteError)
|
|
469
|
+
if (cause instanceof FundingQuotes.ProviderQuoteError) {
|
|
470
|
+
const failures = cause.failures.map(FundingQuoteProviders.providerFailure)
|
|
471
|
+
const failure = failures[0]
|
|
472
|
+
if (failure) c.set('providerFailure', failure)
|
|
473
|
+
if (failures.length > 1) c.set('providerFailures', failures)
|
|
460
474
|
return Response.error(c, {
|
|
461
475
|
code: 'upstream_error',
|
|
462
476
|
message: 'Quote providers could not complete the request.',
|
|
463
477
|
status: 502,
|
|
464
478
|
})
|
|
479
|
+
}
|
|
465
480
|
throw cause
|
|
466
481
|
}
|
|
467
482
|
},
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Challenge } from 'mppx'
|
|
2
2
|
import { sessionManager } from 'mppx/client'
|
|
3
|
-
import {
|
|
3
|
+
import { ZoneRpcAuthentication } from 'ox/tempo'
|
|
4
4
|
import { Actions } from 'viem/tempo'
|
|
5
5
|
import * as TestApp from '../../../../test/App.js'
|
|
6
6
|
import * as Mppx from '../../../../test/Mppx.js'
|
|
7
7
|
import * as Runtime from '../../../../test/runtime.js'
|
|
8
8
|
import * as Tempo from '../../../../test/Tempo.js'
|
|
9
|
+
import * as Auth from '../../../internal/Auth.js'
|
|
9
10
|
import * as Tidx from '../../../internal/Tidx.js'
|
|
10
11
|
import * as Viem from '../../../internal/Viem.js'
|
|
11
12
|
import * as Indexer from './indexer.js'
|
|
@@ -83,13 +84,52 @@ describe('GET /indexer/query', () => {
|
|
|
83
84
|
})
|
|
84
85
|
const response = await client.v1.indexer.query.$get(
|
|
85
86
|
{ query: { chainId: String(zone), sql: 'select 1' } },
|
|
86
|
-
{
|
|
87
|
+
{
|
|
88
|
+
headers: {
|
|
89
|
+
authorization: `Bearer ${zoneReader.token}`,
|
|
90
|
+
[ZoneRpcAuthentication.headerName]: 'zone-rpc-secret',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
87
93
|
)
|
|
88
94
|
const upstream = fetch.mock.calls[0]![0] as Request
|
|
89
95
|
|
|
90
96
|
expect(response.status).toBe(200)
|
|
91
97
|
expect(new URL(upstream.url).origin).toBe('https://zone.tidx.test')
|
|
92
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')
|
|
93
133
|
} finally {
|
|
94
134
|
fetch.mockRestore()
|
|
95
135
|
}
|
|
@@ -156,6 +196,20 @@ describe('GET /indexer/query', () => {
|
|
|
156
196
|
expect(response.status).toMatchInlineSnapshot(`401`)
|
|
157
197
|
})
|
|
158
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
|
+
|
|
159
213
|
test('proxies without auth middleware', async () => {
|
|
160
214
|
const client = TestApp.client({ auth: false })
|
|
161
215
|
const response = await client.v1.indexer.query.$get({
|
|
@@ -167,10 +221,8 @@ describe('GET /indexer/query', () => {
|
|
|
167
221
|
expect(body.ok).toMatchInlineSnapshot(`true`)
|
|
168
222
|
})
|
|
169
223
|
|
|
170
|
-
test('
|
|
171
|
-
const rateLimitStore = Store.memory()
|
|
224
|
+
test('challenges anonymous callers for MPP payment', async () => {
|
|
172
225
|
const app = TestApp.create({
|
|
173
|
-
rateLimit: { store: rateLimitStore },
|
|
174
226
|
mpp: {
|
|
175
227
|
secretKey: 'secret_test_key_0123456789abcdef',
|
|
176
228
|
session: {
|
|
@@ -182,13 +234,9 @@ describe('GET /indexer/query', () => {
|
|
|
182
234
|
},
|
|
183
235
|
},
|
|
184
236
|
})
|
|
185
|
-
await exhaustQuota(RateLimit.memory({ store: rateLimitStore }), 'public:anonymous', {
|
|
186
|
-
perMinute: 60,
|
|
187
|
-
})
|
|
188
237
|
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// 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.
|
|
192
240
|
const challenge = await app.fetch(
|
|
193
241
|
new Request('http://tempo-api.test/v1/indexer/query?sql=select%201'),
|
|
194
242
|
)
|
|
@@ -209,10 +257,31 @@ describe('GET /indexer/query', () => {
|
|
|
209
257
|
expect(body.ok).toMatchInlineSnapshot(`true`)
|
|
210
258
|
})
|
|
211
259
|
|
|
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
|
+
|
|
212
283
|
test('accepts session management POSTs', { timeout: 120_000 }, async () => {
|
|
213
|
-
const rateLimitStore = Store.memory()
|
|
214
284
|
const app = TestApp.create({
|
|
215
|
-
rateLimit: { store: rateLimitStore },
|
|
216
285
|
mpp: {
|
|
217
286
|
secretKey: 'secret_test_key_0123456789abcdef',
|
|
218
287
|
session: {
|
|
@@ -225,9 +294,6 @@ describe('GET /indexer/query', () => {
|
|
|
225
294
|
},
|
|
226
295
|
},
|
|
227
296
|
})
|
|
228
|
-
await exhaustQuota(RateLimit.memory({ store: rateLimitStore }), 'public:anonymous', {
|
|
229
|
-
perMinute: 60,
|
|
230
|
-
})
|
|
231
297
|
await Actions.faucet.fundSync(Tempo.client, {
|
|
232
298
|
account: Tempo.accounts[2],
|
|
233
299
|
timeout: 60_000,
|
|
@@ -264,7 +330,3 @@ describe('GET /indexer/query', () => {
|
|
|
264
330
|
expect(state.deposit).toMatchInlineSnapshot(`0n`)
|
|
265
331
|
})
|
|
266
332
|
})
|
|
267
|
-
|
|
268
|
-
async function exhaustQuota(rateLimit: RateLimit.Store, key: string, limit: RateLimit.Limit) {
|
|
269
|
-
for (let index = 0; index < limit.perMinute; index++) await rateLimit.consume({ key, limit })
|
|
270
|
-
}
|
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid'
|
|
2
2
|
import type Stripe from 'stripe'
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import * as Runtime from '../../../test/runtime.js'
|
|
5
5
|
import * as ApiKeys from '../../ApiKeys.js'
|
|
6
6
|
import * as Db from '../../db/Db.js'
|
|
7
7
|
import * as RequestUsage from '../../db/tables/requestUsage.js'
|
|
@@ -11,7 +11,7 @@ import * as Store from '../../internal/Store.js'
|
|
|
11
11
|
import * as Viem from '../../internal/Viem.js'
|
|
12
12
|
import * as Billing from './Billing.js'
|
|
13
13
|
|
|
14
|
-
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
14
|
+
const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
|
|
15
15
|
|
|
16
16
|
/** Baseline billable mainnet sponsorship input; tests override per case. */
|
|
17
17
|
const input = {
|
package/src/apps/mcp.test.ts
CHANGED
|
@@ -269,3 +269,21 @@ test('documents POST /mcp and keeps it open under auth enforcement', async () =>
|
|
|
269
269
|
)
|
|
270
270
|
expect(rejected.status).toBe(401)
|
|
271
271
|
})
|
|
272
|
+
|
|
273
|
+
test('applies the MCP anonymous rate limit', async () => {
|
|
274
|
+
const app = TestApp.create()
|
|
275
|
+
app.route('/', mcp({ docs: false, fetch: (request) => app.fetch(request) }))
|
|
276
|
+
|
|
277
|
+
const response = await app.fetch(
|
|
278
|
+
new Request(new URL('/mcp', origin), {
|
|
279
|
+
body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'tools/list', params: {} }),
|
|
280
|
+
headers: {
|
|
281
|
+
accept: 'application/json, text/event-stream',
|
|
282
|
+
'content-type': 'application/json',
|
|
283
|
+
},
|
|
284
|
+
method: 'POST',
|
|
285
|
+
}),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
expect(response.headers.get('RateLimit-Limit')).toMatchInlineSnapshot(`"100"`)
|
|
289
|
+
})
|
package/src/apps/mcp.ts
CHANGED
|
@@ -125,10 +125,10 @@ export function mcp(options: mcp.Options) {
|
|
|
125
125
|
const app = new Hono<App.Environment>()
|
|
126
126
|
.post(
|
|
127
127
|
'/mcp',
|
|
128
|
-
Auth.policy({ apiKey: { scopes: [] }, public:
|
|
128
|
+
Auth.policy({ apiKey: { scopes: [] }, public: { rateLimit: { perMinute: 100 } } }),
|
|
129
129
|
OpenApi.describeRoute({
|
|
130
130
|
description:
|
|
131
|
-
"A stateless streamable-HTTP MCP server: send JSON-RPC 2.0 messages as `POST` with `Accept: application/json, text/event-stream`. Tools mirror the API's data domain, plus `docs_*` documentation search. Anonymous requests
|
|
131
|
+
"A stateless streamable-HTTP MCP server: send JSON-RPC 2.0 messages as `POST` with `Accept: application/json, text/event-stream`. Tools mirror the API's data domain, plus `docs_*` documentation search. Anonymous requests allow 100 requests per minute; send `Authorization: Bearer <api key>` for protected tools.",
|
|
132
132
|
operationId: 'mcpRequest',
|
|
133
133
|
// The JSON-RPC request envelope is protocol-owned (like `/rpc`), so the
|
|
134
134
|
// body documents an example rather than a schema.
|
|
@@ -450,7 +450,7 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
|
450
450
|
`)
|
|
451
451
|
})
|
|
452
452
|
|
|
453
|
-
test('behavior: production keys record billable rows', async () => {
|
|
453
|
+
test('behavior: non-mainnet production keys record billable rows', async () => {
|
|
454
454
|
const db = TestApp.database()
|
|
455
455
|
const app = TestApp.create({
|
|
456
456
|
auth: { keys },
|
|
@@ -1141,15 +1141,15 @@ describe.runIf(TestStripe.secretKey)(
|
|
|
1141
1141
|
() => {
|
|
1142
1142
|
// prettier-ignore
|
|
1143
1143
|
const webhookSecret = `whsec_${'c'.repeat(32)}`
|
|
1144
|
-
let node: Awaited<ReturnType<typeof Containers.
|
|
1144
|
+
let node: Awaited<ReturnType<typeof Containers.tempo>> | undefined
|
|
1145
1145
|
|
|
1146
1146
|
beforeAll(async () => {
|
|
1147
|
-
node = await Containers.
|
|
1147
|
+
node = await Containers.tempo({ chainId: Viem.chainId.mainnet })
|
|
1148
1148
|
}, 240_000)
|
|
1149
1149
|
|
|
1150
1150
|
afterAll(async () => {
|
|
1151
1151
|
await TestStripe.sweep()
|
|
1152
|
-
await node?.
|
|
1152
|
+
await node?.stop()
|
|
1153
1153
|
})
|
|
1154
1154
|
|
|
1155
1155
|
test(
|
|
@@ -1157,7 +1157,8 @@ describe.runIf(TestStripe.secretKey)(
|
|
|
1157
1157
|
{ retry: 0, timeout: 300_000 },
|
|
1158
1158
|
async () => {
|
|
1159
1159
|
// prettier-ignore
|
|
1160
|
-
const
|
|
1160
|
+
const endpoint = node!.endpoints.default
|
|
1161
|
+
const url = `http://${endpoint.host}:${endpoint.port}`
|
|
1161
1162
|
const stripe = TestStripe.client()
|
|
1162
1163
|
const db = TestApp.database()
|
|
1163
1164
|
// The key attributes to org_1; the row must exist for the usage read.
|
package/src/apps/relay/App.ts
CHANGED
|
@@ -20,8 +20,8 @@ import * as Scope from '../../Scope.js'
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* The relay route group exposes wallet features at `/rpc/relay` and fee
|
|
23
|
-
* sponsorship at `/rpc/sponsor`. Mount it after `data()`, which publishes
|
|
24
|
-
*
|
|
23
|
+
* sponsorship at `/rpc/sponsor`. Mount it after `data()`, which publishes the
|
|
24
|
+
* verified-token and zone context used by relay handlers.
|
|
25
25
|
*
|
|
26
26
|
* ```ts
|
|
27
27
|
* App.create(options).route('/', data()).route('/', relay())
|
|
@@ -2,12 +2,12 @@ import { nanoid } from 'nanoid'
|
|
|
2
2
|
import { TxEnvelopeTempo } from 'ox/tempo'
|
|
3
3
|
import type { Client } from 'viem'
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import * as Runtime from '../../../test/runtime.js'
|
|
6
6
|
import * as Db from '../../db/Db.js'
|
|
7
7
|
import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
|
|
8
8
|
import * as Sponsorships from './Sponsorships.js'
|
|
9
9
|
|
|
10
|
-
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
10
|
+
const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
|
|
11
11
|
|
|
12
12
|
/** Baseline insert input; tests override per case. */
|
|
13
13
|
const input = {
|
package/src/db/Db.test.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid'
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import * as Runtime from '../../test/runtime.js'
|
|
4
4
|
import * as Db from './Db.js'
|
|
5
5
|
import * as Organizations from './tables/organizations.js'
|
|
6
6
|
|
|
7
|
-
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
7
|
+
const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
|
|
8
8
|
|
|
9
9
|
describe('postgres', () => {
|
|
10
10
|
test('behavior: migrate is idempotent', async () => {
|
|
@@ -16,7 +16,7 @@ describe('postgres', () => {
|
|
|
16
16
|
})
|
|
17
17
|
|
|
18
18
|
test('behavior: creates a Postgres db without schema scoping', async () => {
|
|
19
|
-
const db = Db.postgres({ connectionString: postgresUrl })
|
|
19
|
+
const db = Db.postgres({ connectionString: Runtime.postgresUrl })
|
|
20
20
|
await db.close()
|
|
21
21
|
})
|
|
22
22
|
|
|
@@ -24,14 +24,14 @@ describe('postgres', () => {
|
|
|
24
24
|
const schema = `t_${nanoid()}`
|
|
25
25
|
// Pre-create the schema so concurrent migrate races only on the
|
|
26
26
|
// `migrations` ledger, not `CREATE SCHEMA`.
|
|
27
|
-
const seed = Db.postgres({ connectionString: postgresUrl, schema })
|
|
27
|
+
const seed = Db.postgres({ connectionString: Runtime.postgresUrl, schema })
|
|
28
28
|
await Organizations.get(seed, 'x').catch(() => {})
|
|
29
29
|
await seed.close()
|
|
30
30
|
|
|
31
31
|
// Several runners migrate the same fresh schema at once (mirrors parallel
|
|
32
32
|
// deploys). The session advisory lock serializes them; none throw.
|
|
33
33
|
const dbs = Array.from({ length: 5 }, () =>
|
|
34
|
-
Db.postgres({ connectionString: postgresUrl, schema }),
|
|
34
|
+
Db.postgres({ connectionString: Runtime.postgresUrl, schema }),
|
|
35
35
|
)
|
|
36
36
|
await Promise.all(dbs.map((db) => db.migrate()))
|
|
37
37
|
const created = await Organizations.create(dbs[0]!, { id: 'org_race', name: 'Race' })
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid'
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import * as Runtime from '../../../test/runtime.js'
|
|
4
4
|
import * as Db from '../Db.js'
|
|
5
5
|
import * as BillingSettings from './billingSettings.js'
|
|
6
6
|
|
|
7
|
-
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
7
|
+
const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
|
|
8
8
|
|
|
9
9
|
/** Redacts nondeterministic row fields (timestamps). */
|
|
10
10
|
function redact(record: BillingSettings.Record) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid'
|
|
2
2
|
|
|
3
|
+
import * as Runtime from '../../../test/runtime.js'
|
|
3
4
|
import * as Webhooks from '../../internal/Webhooks.js'
|
|
4
5
|
import * as Db from '../Db.js'
|
|
5
6
|
import * as Organizations from './organizations.js'
|
|
6
|
-
import { postgresUrl } from '../../../test/containers.js'
|
|
7
7
|
|
|
8
|
-
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
8
|
+
const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
|
|
9
9
|
|
|
10
10
|
/** Redacts nondeterministic row fields (generated nanoid ids, timestamps). */
|
|
11
11
|
function redact(record: Organizations.Record) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid'
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import * as Runtime from '../../../test/runtime.js'
|
|
4
4
|
import * as Db from '../Db.js'
|
|
5
5
|
import * as SponsoredTransactions from './sponsoredTransactions.js'
|
|
6
6
|
|
|
7
|
-
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
7
|
+
const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
|
|
8
8
|
|
|
9
9
|
/** Baseline insert input; tests override per case. */
|
|
10
10
|
const input = {
|
|
@@ -95,6 +95,23 @@ describe('upsert', () => {
|
|
|
95
95
|
await db.close()
|
|
96
96
|
})
|
|
97
97
|
|
|
98
|
+
test('behavior: rejects a sign payload already attributed to another org', async () => {
|
|
99
|
+
const db = create()
|
|
100
|
+
await db.migrate()
|
|
101
|
+
const first = await SponsoredTransactions.upsert(db, input)
|
|
102
|
+
|
|
103
|
+
await expect(
|
|
104
|
+
SponsoredTransactions.upsert(db, {
|
|
105
|
+
...input,
|
|
106
|
+
orgId: 'org_2',
|
|
107
|
+
transactionHash: `0x${'dd'.repeat(32)}`,
|
|
108
|
+
}),
|
|
109
|
+
).rejects.toThrow()
|
|
110
|
+
|
|
111
|
+
expect(await SponsoredTransactions.get(db, first.id)).toStrictEqual(first)
|
|
112
|
+
await db.close()
|
|
113
|
+
})
|
|
114
|
+
|
|
98
115
|
test('behavior: a raw submission reopens a failed fill intent', async () => {
|
|
99
116
|
const db = create()
|
|
100
117
|
await db.migrate()
|
|
@@ -47,20 +47,27 @@ export async function upsert(db: Db.Db, input: upsert.Input): Promise<Record> {
|
|
|
47
47
|
.insertInto('sponsored_transactions')
|
|
48
48
|
.values(record)
|
|
49
49
|
.onConflict((oc) =>
|
|
50
|
-
oc
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
oc
|
|
51
|
+
.column('signPayload')
|
|
52
|
+
.doUpdateSet((eb) => ({
|
|
53
|
+
// Same sign payload = same envelope = same cap; only fills older nulls.
|
|
54
|
+
currency: eb.fn.coalesce(eb.ref('sponsored_transactions.currency'), eb.ref('excluded.currency')), // prettier-ignore
|
|
55
|
+
feeMax: eb.fn.coalesce(
|
|
56
|
+
eb.ref('sponsored_transactions.feeMax'),
|
|
57
|
+
eb.ref('excluded.feeMax'),
|
|
58
|
+
),
|
|
59
|
+
// A raw submission fills the intent's hash; a duplicate intent never
|
|
60
|
+
// clears an already-known hash.
|
|
61
|
+
transactionHash: eb.fn.coalesce(
|
|
62
|
+
eb.ref('excluded.transactionHash'),
|
|
63
|
+
eb.ref('sponsored_transactions.transactionHash'),
|
|
64
|
+
),
|
|
65
|
+
createdAt: sql`CASE WHEN ${eb.ref('sponsored_transactions.status')} = 'failed' THEN ${eb.ref('excluded.createdAt')} ELSE ${eb.ref('sponsored_transactions.createdAt')} END`,
|
|
66
|
+
finalizedAt: sql`CASE WHEN ${eb.ref('sponsored_transactions.status')} = 'failed' THEN NULL ELSE ${eb.ref('sponsored_transactions.finalizedAt')} END`,
|
|
67
|
+
status: sql`CASE WHEN ${eb.ref('sponsored_transactions.status')} = 'failed' THEN 'pending' ELSE ${eb.ref('sponsored_transactions.status')} END`,
|
|
68
|
+
}))
|
|
69
|
+
// A shared envelope cannot transfer billing attribution across tenants.
|
|
70
|
+
.where('sponsored_transactions.orgId', '=', input.orgId),
|
|
64
71
|
)
|
|
65
72
|
.returningAll()
|
|
66
73
|
.executeTakeFirstOrThrow()
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid'
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import * as Runtime from '../../../test/runtime.js'
|
|
4
4
|
import * as Db from '../Db.js'
|
|
5
5
|
import * as StripeCustomers from './stripeCustomers.js'
|
|
6
6
|
|
|
7
|
-
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
7
|
+
const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
|
|
8
8
|
|
|
9
9
|
/** Redacts nondeterministic row fields (timestamps). */
|
|
10
10
|
function redact(record: StripeCustomers.Record) {
|
package/src/internal/Auth.ts
CHANGED
|
@@ -1406,6 +1406,16 @@ export function require<environment extends Environment = Environment>(
|
|
|
1406
1406
|
const hasQueryApiKey = new URL(c.req.url).searchParams.has('key')
|
|
1407
1407
|
// Auth owns `key`; strict route schemas must not validate it.
|
|
1408
1408
|
stripApiKeyQuery(c)
|
|
1409
|
+
// Reject unknown chains before MPP builds a chain-bound challenge. Route
|
|
1410
|
+
// groups publish their supported chains into the shared app context.
|
|
1411
|
+
const chainId = explicitChainId(c)
|
|
1412
|
+
const supportedChainIds = (c.var as Record<string, unknown>)['supportedChainIds']
|
|
1413
|
+
if (
|
|
1414
|
+
chainId !== undefined &&
|
|
1415
|
+
supportedChainIds instanceof Set &&
|
|
1416
|
+
!supportedChainIds.has(chainId)
|
|
1417
|
+
)
|
|
1418
|
+
return Response.unsupportedChainId(c, chainId, supportedChainIds)
|
|
1409
1419
|
const proceed = async () => {
|
|
1410
1420
|
await next()
|
|
1411
1421
|
// Query credential responses must not enter shared caches.
|