From 240fd912256909ec21fbd80f9c7dfbedd0b2b37b Mon Sep 17 00:00:00 2001 From: epistemedeus Date: Mon, 10 Aug 2026 12:58:35 -0700 Subject: [PATCH] Fix x402 challenge-result retries --- src/lib/x402/fetch-middleware.ts | 20 +++++++++++------ test/unit/lib/x402/fetch-middleware.test.ts | 25 +++++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/lib/x402/fetch-middleware.ts b/src/lib/x402/fetch-middleware.ts index 76fa926b..442eaa93 100644 --- a/src/lib/x402/fetch-middleware.ts +++ b/src/lib/x402/fetch-middleware.ts @@ -174,7 +174,7 @@ async function getOrSignPayment( paymentCache: X402PaymentCache, schemePreference?: SchemePreference ): Promise { - if (!getToolByName || !init?.body) { + if (!init?.body) { return undefined; } @@ -189,6 +189,18 @@ async function getOrSignPayment( return undefined; } + // The bridge can populate this cache after receiving a payment-required + // CallToolResult. That retry must not depend on proactive tools/list metadata: + // challenge-first servers may omit _meta.x402 entirely. + if (paymentCache.signature) { + logger.debug(`Using cached payment signature for tool "${toolName}"`); + return paymentCache.signature; + } + + if (!getToolByName) { + return undefined; + } + // Look up tool metadata const tool = getToolByName(toolName); if (!tool) { @@ -203,12 +215,6 @@ async function getOrSignPayment( return undefined; } - // Return cached signature if available - if (paymentCache.signature) { - logger.debug(`Using cached payment signature for tool "${toolName}"`); - return paymentCache.signature; - } - const accept = selectAcceptFromToolMeta(x402, schemePreference); if (!accept) { logger.debug( diff --git a/test/unit/lib/x402/fetch-middleware.test.ts b/test/unit/lib/x402/fetch-middleware.test.ts index 49f925ec..b4cd7639 100644 --- a/test/unit/lib/x402/fetch-middleware.test.ts +++ b/test/unit/lib/x402/fetch-middleware.test.ts @@ -97,6 +97,31 @@ beforeEach(() => { // --------------------------------------------------------------------------- describe('createX402FetchMiddleware proactive sign', () => { + it('reuses a challenge-signed cache entry when the tool has no proactive x402 metadata', async () => { + const cachedPayload = { + x402Version: 2, + payload: { signature: '0xsig', authorization: { from: WALLET.address } }, + }; + const cachedSignature = Buffer.from(JSON.stringify(cachedPayload)).toString('base64'); + const cache: X402PaymentCache = { signature: cachedSignature }; + const baseFetch = vi.fn().mockResolvedValue(new Response('', { status: 200 })); + const fetchFn = createX402FetchMiddleware(baseFetch as never, { + wallet: WALLET, + getToolByName: () => undefined, + paymentCache: cache, + schemePreference: 'exact', + }); + + await fetchFn('https://example.test/mcp', { method: 'POST', body: toolsCallBody('paid-tool') }); + + expect(mockSignPayment).not.toHaveBeenCalled(); + expect(baseFetch).toHaveBeenCalledTimes(1); + const init = baseFetch.mock.calls[0]?.[1] as RequestInit; + expect(new Headers(init.headers).get('PAYMENT-SIGNATURE')).toBe(cachedSignature); + const body = JSON.parse(String(init.body)); + expect(body.params._meta['x402/payment']).toEqual(cachedPayload); + }); + it('with schemePreference=exact and accepts=[exact, upto], signs exact', async () => { const tool = makePaidTool({ accepts: [EXACT_ACCEPT, UPTO_ACCEPT], ...UPTO_ACCEPT }); const cache: X402PaymentCache = { signature: null };