import { ZoneRpcAuthentication } from 'ox/tempo' import type * as Log from '../../../internal/Log.js' import type * as Viem from '../../../internal/Viem.js' import * as RequestListener from '../../../handlers/internal/requestListener.js' import * as TestApp from '../../../../test/App.js' import * as Relay from '../../../../test/Relay.js' const chainId = 421_700_001 describe('POST /rpc Zone authorization', () => { test('rejects unavailable public endpoints before contacting the internal node', async () => { const calls: string[] = [] const server = await Relay.createServer( RequestListener.fromFetchHandler(async (request) => { calls.push(await request.text()) return Response.json({ id: 1, jsonrpc: '2.0', result: null }) }), ) try { const configurations: readonly Viem.getClient.Rpc[] = [ {}, { publicZoneUrl: {} }, { publicZoneUrl: { [chainId + 1]: 'https://public.zone.test' } }, { publicZoneUrl: '' }, { publicZoneUrl: 'not a URL' }, { publicZoneUrl: 'file:///rpc' }, { publicZoneUrl: 'https://user:secret@public.zone.test' }, { publicZoneUrl: server.url }, { publicZoneUrl: `${server.url}/?public=true#rpc` }, { publicZoneUrl: `${server.url}/public` }, () => ({}), ] const rpc = { auth: 'internal:secret', url: server.url, zoneHeaders: { 'CF-Access-Client-Id': 'service-id', 'CF-Access-Client-Secret': 'service-secret', }, } for (const configuration of configurations) for (const scopes of [undefined, ['data:read'], ['*']]) { const entries: Log.Entry[] = [] const key = { ...TestApp.key, scopes: scopes ?? [] } const app = TestApp.create({ auth: scopes ? { keys: [key] } : {}, logger: (entry) => void entries.push(entry), rpc: typeof configuration === 'function' ? (context) => ({ ...rpc, ...configuration(context) }) : { ...rpc, ...configuration }, zones: [TestApp.zone({ chainId, rpcUrl: server.url })], }) const response = await app.fetch( new Request(`http://tempo-api.test/rpc/${chainId}`, { body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }), headers: { ...(scopes ? { 'tempo-api-key': key.token } : {}), [ZoneRpcAuthentication.headerName]: 'unvalidated-token', }, method: 'POST', }), ) expect(response.status).toBe(502) expect(await response.json()).toMatchObject({ error: { code: 'upstream_error' } }) expect(entries[0]?.provider).toMatchInlineSnapshot(` { "chainId": 421700001, "failure": "unknown", "id": "rpc", "operation": "request", } `) } expect(calls).toMatchInlineSnapshot(`[]`) } finally { await server.closeAsync() } }) test('rejects oversized Zone bodies before proxying, regardless of declared length', async () => { const calls: string[] = [] const server = await Relay.createServer( RequestListener.fromFetchHandler(async (request) => { calls.push(await request.text()) return Response.json({ id: 1, jsonrpc: '2.0', result: null }) }), ) try { const app = TestApp.create({ rpc: { publicZoneUrl: server.url, url: 'http://internal.zone.test' }, zones: [TestApp.zone({ chainId, rpcUrl: 'http://internal.zone.test' })], }) for (const length of [undefined, '1', '71680']) { let chunks = 0 let cancelled = false const request = new Request(`http://tempo-api.test/rpc/${chainId}`, { body: new ReadableStream({ cancel() { cancelled = true }, pull(controller) { const value = chunks === 0 ? JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }) : '' controller.enqueue(new TextEncoder().encode(value.padEnd(1024, ' '))) if (++chunks === 70) controller.close() }, }), duplex: 'half', headers: { ...(length ? { 'content-length': length } : {}), [ZoneRpcAuthentication.headerName]: 'caller-token', }, method: 'POST', } as RequestInit) const response = await app.fetch(request) expect(response.status).toBe(413) expect(await response.json()).toMatchObject({ error: { code: 'payload_too_large' } }) expect(chunks).toBeLessThan(70) expect(cancelled).toBe(true) } expect(calls).toMatchInlineSnapshot(`[]`) } finally { await server.closeAsync() } }) test('forwards Zone bodies at the inspection limit unchanged', async () => { const calls: string[] = [] const server = await Relay.createServer( RequestListener.fromFetchHandler(async (request) => { calls.push(await request.text()) return Response.json({ id: 1, jsonrpc: '2.0', result: null }) }), ) try { const app = TestApp.create({ defaultChainId: chainId, rpc: { publicZoneUrl: server.url, url: 'http://internal.zone.test' }, zones: [TestApp.zone({ chainId, rpcUrl: 'http://internal.zone.test' })], }) const body = JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }).padEnd( 65536, ' ', ) for (const path of ['/rpc', `/rpc/${chainId}`]) { const response = await app.fetch( new Request(`http://tempo-api.test${path}`, { body, headers: { [ZoneRpcAuthentication.headerName]: 'caller-token' }, method: 'POST', }), ) expect(response.status).toBe(200) } expect(calls).toEqual([body, body]) } finally { await server.closeAsync() } }) test('keeps privileged methods blocked with caller tokens, including mixed batches', async () => { const calls: string[] = [] const server = await Relay.createServer( RequestListener.fromFetchHandler(async (request) => { calls.push(await request.text()) return Response.json({ id: 1, jsonrpc: '2.0', result: null }) }), ) try { for (const scopes of [undefined, ['data:read'], ['*']]) { const key = { ...TestApp.key, scopes: scopes ?? [] } const app = TestApp.create({ auth: scopes ? { keys: [key] } : {}, rpc: { publicZoneUrl: server.url, url: 'http://internal.zone.test' }, zones: [TestApp.zone({ chainId, rpcUrl: 'http://internal.zone.test' })], }) for (const method of [ 'eth_accounts', 'eth_sendTransaction', 'eth_sign', 'eth_signTransaction', 'tempo_fundAddress', 'admin_nodeInfo', 'debug_traceTransaction', ]) for (const batch of [false, true]) { const request = { id: 1, jsonrpc: '2.0', method } const response = await app.fetch( new Request(`http://tempo-api.test/rpc/${chainId}`, { body: JSON.stringify( batch ? [{ id: 2, jsonrpc: '2.0', method: 'eth_chainId' }, request] : request, ), headers: { ...(scopes ? { 'tempo-api-key': key.token } : {}), [ZoneRpcAuthentication.headerName]: 'unvalidated-token', }, method: 'POST', }), ) expect(response.status).toBe(!scopes && !method.startsWith('eth_') ? 401 : 403) } } expect(calls).toMatchInlineSnapshot(`[]`) } finally { await server.closeAsync() } }) test('separates caller-token requests from trusted scoped reads and their credentials', async () => { type Call = { authorization: string | null serviceId: string | null serviceSecret: string | null tempoApiKey: string | null token: string | null url: string } const calls: Call[] = [] const listener = RequestListener.fromFetchHandler(async (request) => { await request.text() calls.push({ authorization: request.headers.get('authorization'), serviceId: request.headers.get('CF-Access-Client-Id'), serviceSecret: request.headers.get('CF-Access-Client-Secret'), tempoApiKey: request.headers.get('tempo-api-key'), token: request.headers.get(ZoneRpcAuthentication.headerName), url: new URL(request.url).pathname, }) return Response.json({ id: 1, jsonrpc: '2.0', result: null }) }) const server = await Relay.createServer(listener) const internal = await Relay.createServer(listener) try { const key = { ...TestApp.key, id: 'key_zone_reader', scopes: ['data:read', `zone:${chainId}:read`], token: 'secret_zone_reader', } const app = TestApp.create({ auth: { keys: [key, TestApp.key] }, rpc: { auth: 'internal:secret', publicZoneUrl: { [chainId]: `${server.url}/public` }, url: `${internal.url}/internal`, zoneHeaders: { 'CF-Access-Client-Id': 'service-id', 'CF-Access-Client-Secret': 'service-secret', }, }, zones: [TestApp.zone({ chainId, rpcUrl: `${internal.url}/internal` })], }) for (const method of [ 'eth_chainId', 'eth_getBlockAccessList', 'eth_getStorageValues', 'eth_sendRawTransaction', 'eth_sendRawTransactionSync', 'zone_getAuthorizationTokenInfo', 'zone_getDepositStatus', ]) { const response = await app.fetch( new Request(`http://tempo-api.test/rpc/${chainId}`, { body: JSON.stringify({ id: 1, jsonrpc: '2.0', method }), headers: { authorization: 'Bearer inbound-credential', 'tempo-api-key': TestApp.key.token, [ZoneRpcAuthentication.headerName]: 'caller-token', }, method: 'POST', }), ) expect(response.status).toBe(200) expect(calls.at(-1)).toMatchInlineSnapshot(` { "authorization": null, "serviceId": "service-id", "serviceSecret": "service-secret", "tempoApiKey": null, "token": "caller-token", "url": "/public", } `) } const response = await app.fetch( new Request(`http://tempo-api.test/rpc/${chainId}`, { body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }), headers: { 'tempo-api-key': key.token }, method: 'POST', }), ) expect(response.status).toBe(200) expect(calls.at(-1)).toMatchInlineSnapshot(` { "authorization": "Basic aW50ZXJuYWw6c2VjcmV0", "serviceId": "service-id", "serviceSecret": "service-secret", "tempoApiKey": null, "token": null, "url": "/internal", } `) } finally { await server.closeAsync() await internal.closeAsync() } }) test('does not fall back when a public URL map only contains a legacy Zone alias', async () => { const calls: string[] = [] const server = await Relay.createServer( RequestListener.fromFetchHandler(async (request) => { calls.push(await request.text()) return Response.json({ id: 1, jsonrpc: '2.0', result: null }) }), ) try { const app = TestApp.create({ rpc: { publicZoneUrl: { 1_424_310_001: `${server.url}/public` }, url: server.url, }, zones: [TestApp.zone({ chainId: 1_424_310_003, rpcUrl: server.url })], }) const response = await app.fetch( new Request('http://tempo-api.test/rpc/1424310001', { body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }), headers: { [ZoneRpcAuthentication.headerName]: 'caller-token' }, method: 'POST', }), ) expect(response.status).toBe(502) expect(calls).toMatchInlineSnapshot(`[]`) } finally { await server.closeAsync() } }) })