import * as Mppx from 'mppx' import { Mppx as MppxClient, tempo as tempoClient } from 'mppx/client' import { Mppx as MppxServer, tempo as tempoServer } from 'mppx/server' import * as TestApp from '../../../test/App.js' import * as Runtime from '../../../test/runtime.js' import * as Tempo from '../../../test/Tempo.js' import type * as Log from '../../internal/Log.js' import * as Store from '../../internal/Store.js' import * as Viem from '../../internal/Viem.js' import * as Mpp from './Mpp.js' const runtime = Runtime.get() const zone = TestApp.zone({ chainId: runtime.zone.chainId, rpcUrl: runtime.zone.internalRpcUrl, }) const key = { environment: 'production', id: 'key_mpp_zone', orgId: 'org_mpp_zone', scopes: ['mpp:write'], token: 'secret_mpp_zone', } satisfies TestApp.kvStore.Key const input = { challenge: { expires: new Date(Date.now() + 3_600_000).toISOString(), id: 'ch_01j3j1k2l3m4n5p6q7r8s9t0u', intent: 'charge', method: 'tempo', realm: 'merchant.example', request: { amount: '1', currency: Tempo.currency, methodDetails: { chainId: zone.id, feePayer: true }, recipient: Tempo.accounts[0].address, }, }, payload: { signature: `0x${'aa'.repeat(65)}`, type: 'transaction' }, } satisfies Mpp.RelayInput describe('MPP Zone authorization', () => { test.each(['validate', 'broadcast'] as const)( '%s refuses unscoped challenge chains before resolving RPC', async (operation) => { const resolutions: number[] = [] const entries: Log.Entry[] = [] for (const scopes of [['mpp:write'], ['mpp:write', `zone:${zone.id + 1}:write`]]) { const app = TestApp.create({ auth: { keys: [{ ...key, scopes }] }, defaultChainId: zone.id, logger: (entry) => void entries.push(entry), mpp: { feePayer: Tempo.accounts[2], state: Store.memory() }, rpc: ({ chainId }) => { resolutions.push(chainId) return { url: runtime.zone.internalRpcUrl } }, zones: [zone], }) for (const request of [ input.challenge.request, { ...input.challenge.request, chainId: zone.id, methodDetails: { feePayer: true } }, { ...input.challenge.request, methodDetails: { feePayer: true } }, ]) { const response = await post(app, operation, { ...input, challenge: { ...input.challenge, request }, }) expect(response.status).toBe(200) expect(await response.json()).toEqual({ error: { code: 'policy_denied', message: `Chain id ${zone.id} is a zone. Use an API key granting \`zone:${zone.id}:read\` or \`zone:${zone.id}:write\`.`, }, success: false, }) } } expect(resolutions).toMatchInlineSnapshot(`[]`) expect(entries).toHaveLength(6) for (const entry of entries) expect(entry.mpp).toMatchObject({ errorCode: 'policy_denied', outcome: 'failure' }) }, ) test.each(['validate', 'broadcast'] as const)( '%s confines wildcard sandbox keys to testnet without requiring sponsorship', async (operation) => { const resolutions: number[] = [] const app = TestApp.create({ auth: { keys: [{ ...key, environment: 'sandbox', scopes: ['*'] }] }, rpc: ({ chainId }) => { resolutions.push(chainId) return { url: runtime.zone.internalRpcUrl } }, zones: [{ ...zone, sourceId: Viem.chainId.mainnet }], }) for (const chainId of [Viem.chainId.mainnet, zone.id]) { const response = await post(app, operation, { ...input, challenge: { ...input.challenge, request: { ...input.challenge.request, methodDetails: { chainId, feePayer: false } }, }, }) expect(response.status).toBe(200) expect(await response.json()).toMatchInlineSnapshot(` { "error": { "code": "policy_denied", "message": "Sandbox API keys only support testnet. Use a testnet challenge.", }, "success": false, } `) } expect(resolutions).toMatchInlineSnapshot(`[]`) }, ) test('requires active billing before mainnet Zone fee-payer access', async () => { const resolutions: number[] = [] const app = TestApp.create({ auth: { keys: [{ ...key, scopes: ['mpp:write', `zone:${zone.id}:write`] }] }, mpp: { feePayer: Tempo.accounts[2], state: Store.memory() }, rpc: ({ chainId }) => { resolutions.push(chainId) return { url: runtime.zone.internalRpcUrl } }, zones: [{ ...zone, sourceId: Viem.chainId.mainnet }], }) expect(await (await post(app, 'broadcast', input)).json()).toMatchInlineSnapshot(` { "error": { "code": "policy_denied", "message": "Active billing is required for mainnet fee sponsorship.", }, "success": false, } `) expect(resolutions).toMatchInlineSnapshot(`[]`) }) test('allows scoped validation but requires write scope to broadcast', async () => { const input = await credential() const app = TestApp.create({ auth: { keys: [{ ...key, scopes: ['mpp:write', `zone:${zone.id}:read`] }] }, zones: [zone], }) expect(await (await post(app, 'validate', input)).json()).toMatchInlineSnapshot(` { "success": true, } `) expect(await (await post(app, 'broadcast', input)).json()).toEqual({ error: { code: 'policy_denied', message: `Broadcasting to zone ${zone.id} requires \`zone:${zone.id}:write\`.`, }, success: false, }) }) test.each([ { environment: 'production', scopes: ['mpp:write', `zone:${zone.id}:write`], sourceId: 4217 }, { environment: 'production', scopes: ['*'], sourceId: 4217 }, { environment: 'sandbox', scopes: ['mpp:write', `zone:${zone.id}:write`], sourceId: 42431 }, ] as const)('allows authorized $environment Zone credentials with $scopes', async (options) => { const input = await credential() const app = TestApp.create({ auth: { keys: [{ ...key, environment: options.environment, scopes: [...options.scopes] }] }, zones: [{ ...zone, sourceId: options.sourceId }], }) for (const operation of ['validate', 'broadcast'] as const) expect(await (await post(app, operation, input)).json()).toMatchObject({ success: true }) }) }) async function credential(): Promise { const client = Viem.getClient({ chainId: zone.id, zone }) const issuer = MppxServer.create({ methods: [ tempoServer.charge({ account: Tempo.accounts[0], currency: Tempo.currency, getClient: () => client, }), ], realm: 'merchant.example', secretKey: 'test-mpp-secret-key-for-payment-sessions', }) const challenge = await issuer.challenge.tempo.charge({ amount: '0' }) const payer = MppxClient.create({ methods: [tempoClient({ account: Tempo.accounts[1], getClient: () => client })], polyfill: false, }) return Mppx.Credential.deserialize( await payer.createCredential( new Response(null, { headers: { 'www-authenticate': Mppx.Challenge.serialize(challenge) }, status: 402, }), ), ) } function post( app: ReturnType, operation: 'validate' | 'broadcast', input: Mpp.RelayInput, ) { return app.fetch( new Request(`http://tempo-api.test/v1/mpp/${operation}?chainId=42431`, { body: JSON.stringify(input), headers: { 'content-type': 'application/json', 'tempo-api-key': key.token }, method: 'POST', }), ) }