import { Secp256k1 } from 'ox' import { ZoneRpcAuthentication } from 'ox/tempo' import { Challenge, PaymentRequest } from 'mppx' import { RateLimit } from 'tapimo' import * as TestApp from '../../../test/App.js' import * as Tempo from '../../../test/Tempo.js' import * as Store from '../../internal/Store.js' import * as Viem from '../../internal/Viem.js' /** A supported non-mainnet chain id, exercising the sandbox testnet allowance. */ const testnet = Viem.chainId.testnet /** Sandbox reader: testnet-only across the chain-data routes. */ const sandbox = { environment: 'sandbox', id: 'key_sandbox', orgId: 'org_test', scopes: ['data:read'], token: 'secret_sandbox', } satisfies TestApp.kvStore.Key /** Production reader: unaffected by the sandbox chain guard. */ const production = { environment: 'production', id: 'key_prod', orgId: 'org_test', scopes: ['data:read'], token: 'secret_prod', } satisfies TestApp.kvStore.Key /** RequestInit carrying a bearer token for the given key. */ function as(key: { token: string }) { return { headers: { authorization: `Bearer ${key.token}` } } as const } /** A deployment whose data group defaults to mainnet when `chainId` is absent. */ function mainnetDefaultClient() { return TestApp.client({ auth: { keys: [sandbox, production] }, defaultChainId: Viem.chainId.mainnet, }) } type OpenApiDocument = { paths: Record> } const mpp = { secretKey: 'secret_test_key_0123456789abcdef', session: { chainId: Tempo.chain.id, currency: Tempo.currency, decimals: 6, getClient: () => Tempo.client, recipient: Tempo.accounts[2].address, }, } satisfies NonNullable, false>['mpp']> describe('MPP session management', () => { test('keeps generated POST routes private', async () => { const app = TestApp.create({ auth: false }) const indexer = await app.request('/v1/indexer/query', { method: 'POST' }) const token = await app.request(`/v1/tokens/${TestApp.token}`, { method: 'POST' }) const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument expect(indexer.status).toBe(404) expect(token.status).toBe(404) expect(spec.paths['/v1/indexer/query']).not.toHaveProperty('post') expect(spec.paths['/v1/tokens/{token}']).not.toHaveProperty('post') }) test('matches GET scopes for static, parameterized, and hidden resources', async () => { const rateLimitStore = Store.memory() const app = TestApp.create({ auth: { mpp }, rateLimit: { store: rateLimitStore } }) await exhaustPublicQuota(rateLimitStore) for (const resource of [ { path: `/v1/indexer/query?sql=select%201&chainId=${Tempo.chain.id}`, scope: 'GET /v1/indexer/query', }, { path: `/v1/tokens/${TestApp.token}?chainId=${Tempo.chain.id}`, scope: 'GET /v1/tokens/:token', }, { path: `/v1/verified-tokens?chainId=${Tempo.chain.id}`, scope: 'GET /v1/verified-tokens', }, ]) { const url = new URL(resource.path, 'http://tempo-api.test') const challenged = await app.fetch(new Request(url)) url.search = '' const management = await app.fetch(new Request(url, { method: 'POST' })) expect(challenged.status).toBe(402) expect(management.status).toBe(402) const expectedScope = PaymentRequest.serialize({ _mppx_scope: resource.scope, }) expect(Challenge.fromHeaders(challenged.headers).opaque).toBe(expectedScope) expect(Challenge.fromHeaders(management.headers).opaque).toBe(expectedScope) } }) test('preserves the mounted base path in session scope', async () => { const rateLimitStore = Store.memory() const app = TestApp.create({ auth: { mpp }, path: '/api', rateLimit: { store: rateLimitStore }, }) await exhaustPublicQuota(rateLimitStore) const resource = new URL( `/api/v1/tokens/${TestApp.token}?chainId=${Tempo.chain.id}`, 'http://tempo-api.test', ) const challenged = await app.fetch(new Request(resource)) resource.search = '' const management = await app.fetch(new Request(resource, { method: 'POST' })) expect(challenged.status).toBe(402) expect(management.status).toBe(402) const expectedScope = PaymentRequest.serialize({ _mppx_scope: 'GET /api/v1/tokens/:token', }) expect(Challenge.fromHeaders(challenged.headers).opaque).toBe(expectedScope) expect(Challenge.fromHeaders(management.headers).opaque).toBe(expectedScope) }) test('honors endpoint MPP overrides', async () => { const app = TestApp.create({ auth: { mpp, overrides: { 'GET /v1/scopes': { mpp: true }, 'GET /v1/tokens/:token': { mpp: false }, }, }, }) const enabled = await app.request('/v1/scopes', { method: 'POST' }) const disabled = await app.request(`/v1/tokens/${TestApp.token}`, { method: 'POST' }) expect(enabled.status).toBe(402) expect(disabled.status).toBe(404) expect(Challenge.fromHeaders(enabled.headers).opaque).toBe( PaymentRequest.serialize({ _mppx_scope: 'GET /v1/scopes' }), ) }) test('applies POST auth overrides to QUERY aliases', async () => { const app = TestApp.create({ auth: { overrides: { 'POST /v1/exchange/quotes': { mpp: false, public: false }, }, }, }) const post = await app.request('/v1/exchange/quotes', { method: 'POST' }) const query = await app.request('/v1/exchange/quotes', { method: 'QUERY' }) expect([post.status, query.status]).toEqual([401, 401]) }) test('prefers an explicit QUERY auth override', async () => { const app = TestApp.create({ auth: { overrides: { 'POST /v1/exchange/quotes': { mpp: false, public: false }, 'QUERY /v1/exchange/quotes': false, }, }, }) const post = await app.request('/v1/exchange/quotes', { method: 'POST' }) const query = await app.request('/v1/exchange/quotes', { method: 'QUERY' }) expect([post.status, query.status]).toEqual([401, 400]) }) }) describe('zone chain gate', () => { const legacyModerato = 1_424_310_001 const moderato = 1_424_310_003 /** A consumer-supplied zone chain id (the API never derives these). */ const zone = 421_700_001 /** A second zone, isolating per-zone grants. */ const zoneB = 4_217_000_002 const legacyModeratoReader = { id: 'key_legacy_moderato', orgId: 'org_test', scopes: ['data:read', 'indexer:query', `zone:${legacyModerato}:read`], token: 'secret_legacy_moderato', } satisfies TestApp.kvStore.Key function zoneOptions(chainId: number) { return TestApp.zone({ chainId, rpcUrl: `https://${chainId}.rpc.test`, }) } /** Zone reader: holds the per-zone scope for `zone` (only). */ const zoneReader = { id: 'key_zone', orgId: 'org_test', scopes: ['data:read', `zone:${zone}:read`], token: 'secret_zone', } satisfies TestApp.kvStore.Key /** Zone writer: broadcasts signed transactions and reads data for `zone`. */ const zoneWriter = { id: 'key_zone_writer', orgId: 'org_test', scopes: ['data:read', `zone:${zone}:write`], token: 'secret_zone_writer', } satisfies TestApp.kvStore.Key /** Full-access key: the wildcard covers every zone. */ const admin = { id: 'key_admin', orgId: 'org_test', scopes: ['*'], token: 'secret_admin', } satisfies TestApp.kvStore.Key /** Data reader without any zone grant. */ const outsider = { id: 'key_outsider', orgId: 'org_test', scopes: ['data:read'], token: 'secret_outsider', } satisfies TestApp.kvStore.Key /** A deployment serving two zones. */ function zoneApp() { return TestApp.create({ auth: { keys: [admin, outsider, zoneReader, zoneWriter] }, zones: [zoneOptions(zone), zoneOptions(zoneB)], }) } /** Signing key proving that a structurally valid caller token is insufficient for REST. */ const privateKey = Secp256k1.randomPrivateKey() /** Signs a caller Zone token. */ function zoneToken(chainId: number) { const issuedAt = Math.floor(Date.now() / 1000) const authentication = ZoneRpcAuthentication.from({ chainId, expiresAt: issuedAt + 300, issuedAt, zoneId: 1, }) return ZoneRpcAuthentication.serialize(authentication, { signature: Secp256k1.sign({ payload: ZoneRpcAuthentication.getSignPayload(authentication), privateKey, }), }) } test('zone chain ids are additive: unsupported ids list them as supported', async () => { const app = TestApp.create({ auth: false, zones: [zoneOptions(zone)] }) const response = await app.request('/v1/transfers?chainId=999999') const body = (await response.json()) as { error: { code: string; message: string } } expect(response.status).toBe(400) expect(body.error.code).toBe('chain_id_unsupported') expect(body.error.message).toContain(String(zone)) }) test('a default zone still applies authorization', async () => { const app = TestApp.create({ auth: false, defaultChainId: zone, rpc: { url: (chainId) => `https://${chainId}.rpc.test` }, zones: [zoneOptions(zone)], }) const rpc = await app.request('/rpc', { body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }), method: 'POST', }) const transfers = await app.request('/v1/transfers') const tokens = await app.request('/v1/tokens') const scopes = await app.request('/v1/scopes') expect(rpc.status).toBe(403) expect(transfers.status).toBe(403) expect(tokens.status).toBe(403) expect(scopes.status).toBe(200) }) test('a zone-scoped key reads Zone data', async () => { const response = await zoneApp().request(`/v1/tokenlist?chainId=${zone}`, as(zoneReader)) expect(response.status).toBe(200) }) test('the legacy Moderato id and read scope route to current upstreams', async () => { const fetch = vi .spyOn(globalThis, 'fetch') .mockResolvedValueOnce(Response.json({ id: 1, jsonrpc: '2.0', result: '0x54e89ac3' })) .mockResolvedValueOnce(Response.json({ columns: [], ok: true, row_count: 0, rows: [] })) try { const app = TestApp.create({ auth: { keys: [legacyModeratoReader] }, rpc: { url: (chainId) => `https://${chainId}.rpc.test` }, tidx: { baseUrl: (chainId) => `https://${chainId}.tidx.test` }, zones: true, }) const rpc = await app.request(`/rpc/${legacyModerato}`, { body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }), headers: as(legacyModeratoReader).headers, method: 'POST', }) const tidx = await app.request( `/v1/indexer/query?chainId=${legacyModerato}&sql=select%201`, as(legacyModeratoReader), ) const tidxUpstream = new URL((fetch.mock.calls[1]![0] as Request).url) expect([rpc.status, tidx.status]).toEqual([200, 200]) expect((fetch.mock.calls[0]![0] as Request).url).toBe(`https://${moderato}.rpc.test/`) expect(tidxUpstream.origin).toBe(`https://${moderato}.tidx.test`) expect(tidxUpstream.searchParams.get('chainId')).toBe(String(moderato)) } finally { fetch.mockRestore() } }) test('a Zone write scope reads Zone data', async () => { const response = await zoneApp().request(`/v1/tokenlist?chainId=${zone}`, as(zoneWriter)) expect(response.status).toBe(200) }) test('zone authorization applies to every data route', async () => { const app = TestApp.create({ auth: false, zones: [zoneOptions(zone)] }) for (const path of [ `/v1/tokens?chainId=${zone}`, `/v1/addresses/0x0000000000000000000000000000000000000001/activities?chainId=${zone}`, `/v1/transfers?chainId=${zone}&include=token.verified`, ]) { const response = await app.request(path) const body = (await response.json()) as { error: { code: string; message: string } } expect(response.status).toBe(403) expect(body.error.code).toBe('api_key_forbidden') expect(body.error.message).toContain('zone') } }) test('the wildcard scope covers every zone', async () => { const app = zoneApp() expect((await app.request(`/v1/tokenlist?chainId=${zone}`, as(admin))).status).toBe(200) expect((await app.request(`/v1/tokenlist?chainId=${zoneB}`, as(admin))).status).toBe(200) }) test('a key without the zone scope is refused, naming the scope', async () => { const app = zoneApp() const response = await app.request(`/v1/transfers?chainId=${zone}`, as(outsider)) const body = (await response.json()) as { error: { code: string; message: string } } expect(response.status).toBe(403) expect(body.error.code).toBe('api_key_forbidden') expect(body.error.message).toContain(`zone:${zone}:read`) expect(body.error.message).not.toContain('X-Authorization-Token') }) test('a zone scope does not grant another zone', async () => { const app = zoneApp() const response = await app.request(`/v1/transfers?chainId=${zoneB}`, as(zoneReader)) const body = (await response.json()) as { error: { code: string; message: string } } expect(response.status).toBe(403) expect(body.error.message).toContain(`zone:${zoneB}:read`) }) test('a caller-signed Zone token cannot authorize REST data', async () => { const response = await zoneApp().request(`/v1/tokenlist?chainId=${zone}`, { headers: { [ZoneRpcAuthentication.headerName]: zoneToken(zone) }, }) expect(response.status).toBe(403) }) test('an empty Zone token cannot authorize raw RPC', async () => { const response = await zoneApp().request(`/rpc/${zone}`, { body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }), headers: { [ZoneRpcAuthentication.headerName]: '' }, method: 'POST', }) expect(response.status).toBe(403) }) test('path chain selectors require Zone authorization', async () => { const app = TestApp.create({ auth: false, zones: [zoneOptions(zone)] }) const gecko = await app.request(`/gecko/${zone}/pairs`) const block = await app.request(`/chains/${zone}/blocks/by-timestamp?timestamp=1767225600`) expect(gecko.status).toBe(403) expect(block.status).toBe(403) }) test('applies Zone authorization under a base path', async () => { const app = TestApp.create({ auth: false, path: '/api', zones: [zoneOptions(zone)] }) expect((await app.request(`/api/v1/tokenlist?chainId=${zone}`)).status).toBe(403) expect((await app.request(`/api/gecko/${zone}/pairs`)).status).toBe(403) }) test('rejects conflicting path and query Zone selectors', async () => { const response = await zoneApp().request( `/gecko/${zoneB}/pairs?chainId=${zone}`, as(zoneReader), ) const body = (await response.json()) as { error: { code: string; message: string } } expect(response.status).toBe(400) expect(body.error.code).toBe('chain_id_invalid') expect(body.error.message).toBe('Conflicting chain ids') }) test('rejects conflicting query aliases', async () => { const response = await zoneApp().request( `/v1/tokenlist?chainId=${zone}&chain_id=${zoneB}`, as(admin), ) expect(response.status).toBe(400) }) test('does not apply the data gate to management routes', async () => { const response = await zoneApp().request(`/v1/scopes?chainId=${zone}`) expect(response.status).toBe(200) }) test('Zone responses never seed the anonymous edge cache', async () => { const app = zoneApp() const path = `/v1/tokenlist?chainId=${zone}` expect((await app.request(path, as(zoneReader))).status).toBe(200) expect((await app.request(path)).status).toBe(403) }) test('bypasses stale edge entries after a chain becomes a Zone', async () => { const store = Store.memory() const path = `/v1/tokenlist?chainId=${zone}` const publicApp = TestApp.create({ auth: false, cache: { store }, supportedChainIds: [zone], }) expect((await publicApp.request(path)).status).toBe(200) const privateApp = TestApp.create({ auth: false, cache: { store }, zones: [zoneOptions(zone)] }) // prettier-ignore expect((await privateApp.request(path)).status).toBe(403) }) test('serves anonymous token lists from the edge cache on public chains', async () => { const store = Store.memory() const app = TestApp.create({ auth: false, cache: { store }, zones: [zoneOptions(zone)] }) const path = `/v1/tokenlist?chainId=${Viem.chainId.testnet}` const first = await app.request(path) const second = await app.request(path) expect(first.status).toBe(200) expect(second.status).toBe(200) expect(second.headers.get('cache-control')).toContain('public') }) }) describe('sandbox chain guard', () => { test('rejects a sandbox key that omits `chainId` on a mainnet-default deployment', async () => { const response = await mainnetDefaultClient().v1.tokenlist.$get({ query: {} }, as(sandbox)) expect(response.status).toBe(403) expect(((await response.json()) as { error: { code: string } }).error.code).toBe( 'api_key_forbidden', ) }) test('rejects a sandbox key on an explicit mainnet `chainId`', async () => { const client = TestApp.client({ auth: { keys: [sandbox, production] } }) const response = await client.v1.tokenlist.$get( { query: { chainId: String(Viem.chainId.mainnet) } }, as(sandbox), ) expect(response.status).toBe(403) expect(((await response.json()) as { error: { code: string } }).error.code).toBe( 'api_key_forbidden', ) }) test('allows a sandbox key on a testnet `chainId`', async () => { const client = mainnetDefaultClient() const response = await client.v1.tokenlist.$get( { query: { chainId: String(testnet) } }, as(sandbox), ) expect(response.status).toBe(200) }) test('leaves production keys unaffected on a mainnet-default deployment', async () => { const response = await mainnetDefaultClient().v1.tokenlist.$get({ query: {} }, as(production)) expect(response.status).toBe(200) }) }) async function exhaustPublicQuota(store: Store.Store) { const limit = { limit: 60, period: 'minute' } satisfies RateLimit.Limit const rateLimit = RateLimit.memory({ store }) for (let index = 0; index < limit.limit; index++) await rateLimit.consume({ key: 'public:anonymous', limit }) }